From a273c232a1e555f2896d24131afaa6be6c5e28e0 Mon Sep 17 00:00:00 2001 From: brendamuir <100768211+brendamuir@users.noreply.github.com> Date: Thu, 26 Jan 2023 17:45:09 +0000 Subject: [PATCH 001/117] Alerting: fix default template link (#62251) --- .../alerting/fundamentals/alert-rules/message-templating.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/alerting/fundamentals/alert-rules/message-templating.md b/docs/sources/alerting/fundamentals/alert-rules/message-templating.md index 0d43a2d948c..cd563f1e4c0 100644 --- a/docs/sources/alerting/fundamentals/alert-rules/message-templating.md +++ b/docs/sources/alerting/fundamentals/alert-rules/message-templating.md @@ -18,7 +18,7 @@ weight: 415 Notifications sent via contact points are built using notification templates. Grafana's default templates are based on the [Go templating system](https://golang.org/pkg/text/template) where some fields are evaluated as text, while others are evaluated as HTML (which can affect escaping). The default template, defined in [default_template.go](https://github.com/grafana/alerting/blob/main/alerting/notifier/channels/default_template.go), is a useful reference for custom templates. -Since most of the contact point fields can be templated, you can create reusable custom templates and use them in multiple contact points. The default template is defined in [default_template.go](https://github.com/grafana/alerting/blob/main/alerting/notifier/channels/default_template.go) which can serve as a useful reference or starting point for custom templates. +Since most of the contact point fields can be templated, you can create reusable custom templates and use them in multiple contact points. ### Using templates From a5a85e03985aca2d572f06adc3adf834b4a53338 Mon Sep 17 00:00:00 2001 From: Brett Buddin Date: Thu, 26 Jan 2023 13:14:02 -0500 Subject: [PATCH 002/117] InfluxDB: Send retention policy with InfluxQL queries if its been specified. (#62149) * InfluxDB: Send retention policy with InfluQL queries if it's been specified. In InfluxDB v2, due to technical limitations of the InfluxDB v1 compatibility layer, retention policies in a query (e.g. ".") aren't honored and must be specified in the URL query parameter `rp` to be applied. Grafana doesn't send this query parameter which results in all queries resolving to the default retention policy when querying InfluxDB v2 servers using InfluxQL. This addresses the issue by sending the `rp` query parameter for queries that have specified a retention policy in the `target` given to `runExploreQuery`. The outcomes are: 1. InfluxQL queries executed against InfluxDB v2 databases will have the necessary retention policy information for queries like `SHOW FIELD KEYS FROM measurement` to function correctly. 2. InfluxQL queries executed against InfluxDB v1 databases will be unaffected, because this `rp` query parameter is unsupported there. You can read more about the rentention policy mapping behavior of InfluxDB v2 in our documentation: - https://docs.influxdata.com/influxdb/v2.6/reference/api/influxdb-1x/dbrp/#when-querying-data - https://docs.influxdata.com/influxdb/v2.6/reference/api/influxdb-1x/query/#query-a-non-default-retention-policy * Use the ? operator Co-authored-by: Ryan McKinley Co-authored-by: Ryan McKinley --- public/app/plugins/datasource/influxdb/datasource.ts | 4 ++++ .../app/plugins/datasource/influxdb/influxQLMetadataQuery.ts | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index 878cb0ced9b..33dabd8cd23 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -403,6 +403,10 @@ export default class InfluxDatasource extends DataSourceWithBackend> => { const builder = new InfluxQueryBuilder(target, datasource.database); const q = builder.buildExploreQuery(type, withKey, withMeasurementFilter); - return datasource.metricFindQuery(q); + const options = { policy: target.policy }; + return datasource.metricFindQuery(q, options); }; export async function getAllPolicies(datasource: InfluxDatasource): Promise { From 9a25a03e495042f4cd91d7e2efbfa25b3a7aa7de Mon Sep 17 00:00:00 2001 From: Guilherme Caulada Date: Thu, 26 Jan 2023 18:04:13 -0300 Subject: [PATCH 003/117] Tests: Add boilerplate code to support e2e tests on enterprise (#61959) * Add e2e boilerplate for enterprise tests * Remove enterprise symlink file * Add tsconfig to e2e extensions folder and gitignore * Update run-suite to use extensions folder * Remove unnecessary tsconfig file * Update e2e enterprise paths on gitignore * Copy symlinked e2e enterprise files on run-suite * Add cleanup command to run-suite * Improve cleanup and setup for enterprise e2e tests * Update e2e path for enterprise tests on gitignore * Support to run different e2e tests for each license --- .gitignore | 2 ++ e2e/run-suite | 25 +++++++++++++++++++++++++ e2e/start-and-run-suite | 12 +++++++++++- package.json | 3 +++ scripts/grafana-server/start-server | 3 ++- 5 files changed, 43 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index a4c02bfa4ca..7c44ef5bdd0 100644 --- a/.gitignore +++ b/.gitignore @@ -157,6 +157,8 @@ compilation-stats.json /e2e/benchmarks/**/results/* /e2e/benchmarks/**/results /e2e/build_results.zip +/e2e/extensions +/e2e/extensions-suite # grafana server /scripts/grafana-server/server.log diff --git a/e2e/run-suite b/e2e/run-suite index d74a41d67f4..8c446f3145b 100755 --- a/e2e/run-suite +++ b/e2e/run-suite @@ -18,6 +18,7 @@ args=("$@") CMD="start" PARAMS="" +CLEANUP="" declare -A env=( [BASE_URL]=${BASE_URL:-"http://$HOST:$PORT"} @@ -25,6 +26,7 @@ declare -A env=( ) testFilesForSingleSuite="*.spec.ts" +rootForEnterpriseSuite="extensions-suite" declare -A cypressConfig=( [integrationFolder]=../../e2e @@ -64,6 +66,27 @@ case "$1" in cypressConfig[screenshotsFolder]=../../e2e/benchmarks/"${args[1]}"/screenshots cypressConfig[testFiles]=$testFilesForSingleSuite ;; + "enterprise") + echo "Enterprise" + CLEANUP="rm -rf ../../e2e/extensions-suite" + SETUP="cp -Lr ../../e2e/extensions ../../e2e/extensions-suite" + enterpriseSuite=$(basename "${args[1]}") + case "$2" in + "debug") + echo -e "Debug mode" + env[SLOWMO]=1 + PARAMS="--no-exit" + enterpriseSuite=$(basename "${args[2]}") + ;; + "dev") + echo "Dev mode" + CMD="open" + enterpriseSuite=$(basename "${args[2]}") + ;; + esac + cypressConfig[testFiles]=$rootForEnterpriseSuite/$enterpriseSuite/*-suite/*.spec.ts + $CLEANUP && $SETUP + ;; "") ;; *) @@ -96,3 +119,5 @@ function join () { yarn $CMD --env "$(join env)" \ --config "$(join cypressConfig)" \ $PARAMS + +$CLEANUP diff --git a/e2e/start-and-run-suite b/e2e/start-and-run-suite index 2c5c7d1d996..d29b7775b96 100755 --- a/e2e/start-and-run-suite +++ b/e2e/start-and-run-suite @@ -2,11 +2,21 @@ . scripts/grafana-server/variables +LICENSE_PATH="" + +if [ "$1" = "enterprise" ]; then + if [ "$2" != "dev" ] && [ "$2" != "debug" ]; then + LICENSE_PATH=$2/license.jwt + else + LICENSE_PATH=$3/license.jwt + fi +fi + if [ "$BASE_URL" != "" ]; then echo -e "BASE_URL set, skipping starting server" else # Start it in the background - ./scripts/grafana-server/start-server 2>&1 > scripts/grafana-server/server.log & + ./scripts/grafana-server/start-server $LICENSE_PATH 2>&1 > scripts/grafana-server/server.log & ./scripts/grafana-server/wait-for-grafana fi diff --git a/package.json b/package.json index 15ef5287b34..84329226067 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,9 @@ "e2e:debug": "./e2e/start-and-run-suite debug", "e2e:dev": "./e2e/start-and-run-suite dev", "e2e:benchmark:live": "./e2e/start-and-run-suite benchmark live", + "e2e:enterprise": "./e2e/start-and-run-suite enterprise", + "e2e:enterprise:dev": "./e2e/start-and-run-suite enterprise dev", + "e2e:enterprise:debug": "./e2e/start-and-run-suite enterprise debug", "test": "jest --notify --watch", "test:coverage": "jest --coverage", "test:coverage:changes": "jest --coverage --changedSince=origin/main", diff --git a/scripts/grafana-server/start-server b/scripts/grafana-server/start-server index 7af0f4933ca..577210fa03d 100755 --- a/scripts/grafana-server/start-server +++ b/scripts/grafana-server/start-server @@ -48,7 +48,8 @@ $RUNDIR/bin/"$ARCH"grafana-server \ --pidfile=$RUNDIR/pid \ cfg:server.http_port=$PORT \ cfg:server.router_logging=1 \ - cfg:app_mode=development + cfg:app_mode=development \ + cfg:enterprise.license_path=$1 # 2>&1 > $RUNDIR/output.log & # cfg:log.level=debug \ From 8379a29b53b27b6f88f0ad71abb1d8fc25a88165 Mon Sep 17 00:00:00 2001 From: Matthew Jacobson Date: Thu, 26 Jan 2023 16:13:08 -0500 Subject: [PATCH 004/117] Alerting: Improve comments on alert table migration immutability (#62161) * Alerting: Improve comments around alert migration immutability * Reunite alerting config history migrations --- .../sqlstore/migrations/ualert/tables.go | 170 ++++++++++++++---- .../sqlstore/migrations/ualert/ualert.go | 77 -------- 2 files changed, 135 insertions(+), 112 deletions(-) diff --git a/pkg/services/sqlstore/migrations/ualert/tables.go b/pkg/services/sqlstore/migrations/ualert/tables.go index de7953e7747..6598f094149 100644 --- a/pkg/services/sqlstore/migrations/ualert/tables.go +++ b/pkg/services/sqlstore/migrations/ualert/tables.go @@ -3,6 +3,8 @@ package ualert import ( "fmt" + "xorm.io/xorm" + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" ) @@ -12,34 +14,45 @@ const DefaultFieldMaxLength = 190 // UIDMaxLength is the standard size for fields that contain UIDs. const UIDMaxLength = 40 -// AddMigration defines database migrations. +// AddTablesMigrations defines database migrations that affect Grafana Alerting tables. func AddTablesMigrations(mg *migrator.Migrator) { - AddAlertDefinitionMigrations(mg, 60) - AddAlertDefinitionVersionMigrations(mg) - // Create alert_instance table - AlertInstanceMigration(mg) - - // Create alert_rule - AddAlertRuleMigrations(mg, 60) - AddAlertRuleVersionMigrations(mg) - - // Create Alertmanager configurations - AddAlertmanagerConfigMigrations(mg) - - // Create Admin Configuration - AddAlertAdminConfigMigrations(mg) - - // Create provisioning data table - AddProvisioningMigrations(mg) - - AddAlertImageMigrations(mg) - - AddAlertmanagerConfigHistoryMigrations(mg) - ExtractAlertmanagerConfigurationHistoryMigration(mg) + // Migrations are meant to be immutable, any modifications to table structure + // should come in the form of a new migration appended to the end of AddTablesMigrations + // instead of modifying an existing one. This ensure that tables are modified in a consistent and correct order. + historicalTableMigrations(mg) } -// AddAlertDefinitionMigrations should not be modified. -func AddAlertDefinitionMigrations(mg *migrator.Migrator, defaultIntervalSeconds int64) { +// historicalTableMigrations contains those migrations that existed prior to creating the improved messaging around migration immutability. +func historicalTableMigrations(mg *migrator.Migrator) { + // DO NOT EDIT + addAlertDefinitionMigrations(mg, 60) + addAlertDefinitionVersionMigrations(mg) + // Create alert_instance table + alertInstanceMigration(mg) + + // Create alert_rule + addAlertRuleMigrations(mg, 60) + addAlertRuleVersionMigrations(mg) + + // Create Alertmanager configurations + addAlertmanagerConfigMigrations(mg) + + // Create Admin Configuration + addAlertAdminConfigMigrations(mg) + + // Create provisioning data table + addProvisioningMigrations(mg) + + addAlertImageMigrations(mg) + + addAlertmanagerConfigHistoryMigrations(mg) + + extractAlertmanagerConfigurationHistoryMigration(mg) +} + +// addAlertDefinitionMigrations should not be modified. +func addAlertDefinitionMigrations(mg *migrator.Migrator, defaultIntervalSeconds int64) { + // DO NOT EDIT mg.AddMigration("delete alert_definition table", migrator.NewDropTableMigration("alert_definition")) alertDefinition := migrator.Table{ @@ -87,8 +100,9 @@ func AddAlertDefinitionMigrations(mg *migrator.Migrator, defaultIntervalSeconds mg.AddMigration("drop alert_definition table", migrator.NewDropTableMigration("alert_definition")) } -// AddAlertDefinitionMigrations should not be modified. -func AddAlertDefinitionVersionMigrations(mg *migrator.Migrator) { +// addAlertDefinitionMigrations should not be modified. +func addAlertDefinitionVersionMigrations(mg *migrator.Migrator) { + // DO NOT EDIT mg.AddMigration("delete alert_definition_version table", migrator.NewDropTableMigration("alert_definition_version")) alertDefinitionVersion := migrator.Table{ @@ -120,7 +134,8 @@ func AddAlertDefinitionVersionMigrations(mg *migrator.Migrator) { mg.AddMigration("drop alert_definition_version table", migrator.NewDropTableMigration("alert_definition_version")) } -func AlertInstanceMigration(mg *migrator.Migrator) { +func alertInstanceMigration(mg *migrator.Migrator) { + // DO NOT EDIT alertInstance := migrator.Table{ Name: "alert_instance", Columns: []*migrator.Column{ @@ -171,7 +186,8 @@ func AlertInstanceMigration(mg *migrator.Migrator) { })) } -func AddAlertRuleMigrations(mg *migrator.Migrator, defaultIntervalSeconds int64) { +func addAlertRuleMigrations(mg *migrator.Migrator, defaultIntervalSeconds int64) { + // DO NOT EDIT alertRule := migrator.Table{ Name: "alert_rule", Columns: []*migrator.Column{ @@ -272,7 +288,8 @@ func AddAlertRuleMigrations(mg *migrator.Migrator, defaultIntervalSeconds int64) )) } -func AddAlertRuleVersionMigrations(mg *migrator.Migrator) { +func addAlertRuleVersionMigrations(mg *migrator.Migrator) { + // DO NOT EDIT alertRuleVersion := migrator.Table{ Name: "alert_rule_version", Columns: []*migrator.Column{ @@ -335,7 +352,8 @@ func AddAlertRuleVersionMigrations(mg *migrator.Migrator) { )) } -func AddAlertmanagerConfigMigrations(mg *migrator.Migrator) { +func addAlertmanagerConfigMigrations(mg *migrator.Migrator) { + // DO NOT EDIT alertConfiguration := migrator.Table{ Name: "alert_configuration", Columns: []*migrator.Column{ @@ -368,7 +386,8 @@ func AddAlertmanagerConfigMigrations(mg *migrator.Migrator) { })) } -func AddAlertmanagerConfigHistoryMigrations(mg *migrator.Migrator) { +func addAlertmanagerConfigHistoryMigrations(mg *migrator.Migrator) { + // DO NOT EDIT alertConfigHistory := migrator.Table{ Name: "alert_configuration_history", Columns: []*migrator.Column{ @@ -388,7 +407,8 @@ func AddAlertmanagerConfigHistoryMigrations(mg *migrator.Migrator) { mg.AddMigration("create_alert_configuration_history_table", migrator.NewAddTableMigration(alertConfigHistory)) } -func AddAlertAdminConfigMigrations(mg *migrator.Migrator) { +func addAlertAdminConfigMigrations(mg *migrator.Migrator) { + // DO NOT EDIT adminConfiguration := migrator.Table{ Name: "ngalert_configuration", Columns: []*migrator.Column{ @@ -411,7 +431,8 @@ func AddAlertAdminConfigMigrations(mg *migrator.Migrator) { })) } -func AddProvisioningMigrations(mg *migrator.Migrator) { +func addProvisioningMigrations(mg *migrator.Migrator) { + // DO NOT EDIT provisioningTable := migrator.Table{ Name: "provenance_type", Columns: []*migrator.Column{ @@ -430,7 +451,8 @@ func AddProvisioningMigrations(mg *migrator.Migrator) { mg.AddMigration("add index to uniquify (record_key, record_type, org_id) columns", migrator.NewAddIndexMigration(provisioningTable, provisioningTable.Indices[0])) } -func AddAlertImageMigrations(mg *migrator.Migrator) { +func addAlertImageMigrations(mg *migrator.Migrator) { + // DO NOT EDIT imageTable := migrator.Table{ Name: "alert_image", Columns: []*migrator.Column{ @@ -453,3 +475,81 @@ func AddAlertImageMigrations(mg *migrator.Migrator) { Postgres("ALTER TABLE alert_image ALTER COLUMN url TYPE VARCHAR(2048);"). Mysql("ALTER TABLE alert_image MODIFY url VARCHAR(2048) NOT NULL;")) } + +func extractAlertmanagerConfigurationHistoryMigration(mg *migrator.Migrator) { + if !mg.Cfg.UnifiedAlerting.IsEnabled() { + return + } + // Since it's not always consistent as to what state the org ID indexes are in, just drop them all and rebuild from scratch. + // This is not expensive since this table is guaranteed to have a small number of rows. + mg.AddMigration("drop non-unique orgID index on alert_configuration", migrator.NewDropIndexMigration(migrator.Table{Name: "alert_configuration"}, &migrator.Index{Cols: []string{"org_id"}})) + mg.AddMigration("drop unique orgID index on alert_configuration if exists", migrator.NewDropIndexMigration(migrator.Table{Name: "alert_configuration"}, &migrator.Index{Type: migrator.UniqueIndex, Cols: []string{"org_id"}})) + mg.AddMigration("extract alertmanager configuration history to separate table", &extractAlertmanagerConfigurationHistory{}) + mg.AddMigration("add unique index on orgID to alert_configuration", migrator.NewAddIndexMigration(migrator.Table{Name: "alert_configuration"}, &migrator.Index{Type: migrator.UniqueIndex, Cols: []string{"org_id"}})) +} + +type extractAlertmanagerConfigurationHistory struct { + migrator.MigrationBase +} + +// extractAMConfigHistoryConfigModel is the model of an alertmanager configuration row, at the time that the extractAlertmanagerConfigurationHistory migration was run. +// This is not to be used outside of the extractAlertmanagerConfigurationHistory migration. +type extractAMConfigHistoryConfigModel struct { + ID int64 `xorm:"pk autoincr 'id'"` + AlertmanagerConfiguration string + ConfigurationHash string + ConfigurationVersion string + CreatedAt int64 `xorm:"created"` + Default bool + OrgID int64 `xorm:"org_id"` +} + +func (c extractAlertmanagerConfigurationHistory) SQL(migrator.Dialect) string { + return codeMigration +} + +func (c extractAlertmanagerConfigurationHistory) Exec(sess *xorm.Session, migrator *migrator.Migrator) error { + // DO NOT EDIT + var orgs []int64 + if err := sess.Table("alert_configuration").Distinct("org_id").Find(&orgs); err != nil { + return fmt.Errorf("failed to retrieve the organizations with alerting configurations: %w", err) + } + + // Clear out the history table, just in case. It should already be empty. + if _, err := sess.Exec("DELETE FROM alert_configuration_history"); err != nil { + return fmt.Errorf("failed to clear the config history table: %w", err) + } + + for _, orgID := range orgs { + var activeConfigID int64 + has, err := sess.SQL(`SELECT MAX(id) FROM alert_configuration WHERE org_id = ?`, orgID).Get(&activeConfigID) + if err != nil { + return fmt.Errorf("failed to query active config ID for org %d: %w", orgID, err) + } + if !has { + return fmt.Errorf("we previously found a config for org, but later it was unexpectedly missing: %d", orgID) + } + + history := make([]extractAMConfigHistoryConfigModel, 0) + err = sess.Table("alert_configuration").Where("org_id = ? AND id < ?", orgID, activeConfigID).Find(&history) + if err != nil { + return fmt.Errorf("failed to query for non-active configs for org %d: %w", orgID, err) + } + + // Set the IDs back to the default, so XORM will ignore the field and auto-assign them. + for i := range history { + history[i].ID = 0 + } + + _, err = sess.Table("alert_configuration_history").InsertMulti(history) + if err != nil { + return fmt.Errorf("failed to insert historical configs for org: %d: %w", orgID, err) + } + + _, err = sess.Exec("DELETE FROM alert_configuration WHERE org_id = ? AND id < ?", orgID, activeConfigID) + if err != nil { + return fmt.Errorf("failed to evict old configurations for org after moving to history table: %d: %w", orgID, err) + } + } + return nil +} diff --git a/pkg/services/sqlstore/migrations/ualert/ualert.go b/pkg/services/sqlstore/migrations/ualert/ualert.go index d2073b9f023..29765e1ccd1 100644 --- a/pkg/services/sqlstore/migrations/ualert/ualert.go +++ b/pkg/services/sqlstore/migrations/ualert/ualert.go @@ -927,80 +927,3 @@ func (s *uidSet) generateUid() (string, error) { return "", errors.New("failed to generate UID") } - -func ExtractAlertmanagerConfigurationHistoryMigration(mg *migrator.Migrator) { - if !mg.Cfg.UnifiedAlerting.IsEnabled() { - return - } - // Since it's not always consistent as to what state the org ID indexes are in, just drop them all and rebuild from scratch. - // This is not expensive since this table is guaranteed to have a small number of rows. - mg.AddMigration("drop non-unique orgID index on alert_configuration", migrator.NewDropIndexMigration(migrator.Table{Name: "alert_configuration"}, &migrator.Index{Cols: []string{"org_id"}})) - mg.AddMigration("drop unique orgID index on alert_configuration if exists", migrator.NewDropIndexMigration(migrator.Table{Name: "alert_configuration"}, &migrator.Index{Type: migrator.UniqueIndex, Cols: []string{"org_id"}})) - mg.AddMigration("extract alertmanager configuration history to separate table", &extractAlertmanagerConfigurationHistory{}) - mg.AddMigration("add unique index on orgID to alert_configuration", migrator.NewAddIndexMigration(migrator.Table{Name: "alert_configuration"}, &migrator.Index{Type: migrator.UniqueIndex, Cols: []string{"org_id"}})) -} - -type extractAlertmanagerConfigurationHistory struct { - migrator.MigrationBase -} - -// extractAMConfigHistoryConfigModel is the model of an alertmanager configuration row, at the time that the extractAlertmanagerConfigurationHistory migration was run. -// This is not to be used outside of the extractAlertmanagerConfigurationHistory migration. -type extractAMConfigHistoryConfigModel struct { - ID int64 `xorm:"pk autoincr 'id'"` - AlertmanagerConfiguration string - ConfigurationHash string - ConfigurationVersion string - CreatedAt int64 `xorm:"created"` - Default bool - OrgID int64 `xorm:"org_id"` -} - -func (c extractAlertmanagerConfigurationHistory) SQL(migrator.Dialect) string { - return codeMigration -} - -func (c extractAlertmanagerConfigurationHistory) Exec(sess *xorm.Session, migrator *migrator.Migrator) error { - var orgs []int64 - if err := sess.Table("alert_configuration").Distinct("org_id").Find(&orgs); err != nil { - return fmt.Errorf("failed to retrieve the organizations with alerting configurations: %w", err) - } - - // Clear out the history table, just in case. It should already be empty. - if _, err := sess.Exec("DELETE FROM alert_configuration_history"); err != nil { - return fmt.Errorf("failed to clear the config history table: %w", err) - } - - for _, orgID := range orgs { - var activeConfigID int64 - has, err := sess.SQL(`SELECT MAX(id) FROM alert_configuration WHERE org_id = ?`, orgID).Get(&activeConfigID) - if err != nil { - return fmt.Errorf("failed to query active config ID for org %d: %w", orgID, err) - } - if !has { - return fmt.Errorf("we previously found a config for org, but later it was unexpectedly missing: %d", orgID) - } - - history := make([]extractAMConfigHistoryConfigModel, 0) - err = sess.Table("alert_configuration").Where("org_id = ? AND id < ?", orgID, activeConfigID).Find(&history) - if err != nil { - return fmt.Errorf("failed to query for non-active configs for org %d: %w", orgID, err) - } - - // Set the IDs back to the default, so XORM will ignore the field and auto-assign them. - for i := range history { - history[i].ID = 0 - } - - _, err = sess.Table("alert_configuration_history").InsertMulti(history) - if err != nil { - return fmt.Errorf("failed to insert historical configs for org: %d: %w", orgID, err) - } - - _, err = sess.Exec("DELETE FROM alert_configuration WHERE org_id = ? AND id < ?", orgID, activeConfigID) - if err != nil { - return fmt.Errorf("failed to evict old configurations for org after moving to history table: %d: %w", orgID, err) - } - } - return nil -} From 6c5a5737721914cdce87bf27990a89e0fe6d47cb Mon Sep 17 00:00:00 2001 From: idafurjes <36131195+idafurjes@users.noreply.github.com> Date: Fri, 27 Jan 2023 08:50:36 +0100 Subject: [PATCH 005/117] Chore: Move ReqContext to contexthandler service (#62102) * Chore: Move ReqContext to contexthandler service * Rename package to contextmodel * Generate ngalert files * Remove unused imports --- pkg/api/accesscontrol.go | 6 +- pkg/api/admin.go | 6 +- pkg/api/admin_encryption.go | 16 ++-- pkg/api/admin_provisioning.go | 12 +-- pkg/api/admin_users.go | 19 ++-- pkg/api/admin_users_test.go | 16 ++-- pkg/api/alerting.go | 49 +++++----- pkg/api/annotations.go | 28 +++--- pkg/api/annotations_test.go | 10 +- pkg/api/api.go | 6 +- pkg/api/apikey.go | 8 +- pkg/api/avatar/avatar.go | 4 +- pkg/api/comments.go | 6 +- pkg/api/common_test.go | 19 ++-- pkg/api/dashboard.go | 36 +++---- pkg/api/dashboard_permission.go | 6 +- pkg/api/dashboard_permission_test.go | 4 +- pkg/api/dashboard_snapshot.go | 14 +-- pkg/api/dashboard_test.go | 12 +-- pkg/api/dataproxy.go | 6 +- pkg/api/datasources.go | 36 +++---- pkg/api/datasources_test.go | 14 +-- pkg/api/folder.go | 20 ++-- pkg/api/folder_permission.go | 6 +- pkg/api/folder_permission_test.go | 4 +- pkg/api/folder_test.go | 5 +- pkg/api/frontend_logging_test.go | 6 +- pkg/api/frontend_metrics.go | 4 +- pkg/api/frontendsettings.go | 8 +- pkg/api/grafana_com_proxy.go | 4 +- pkg/api/index.go | 10 +- pkg/api/ldap_debug.go | 9 +- pkg/api/ldap_debug_test.go | 7 +- pkg/api/login.go | 21 +++-- pkg/api/login_oauth.go | 9 +- pkg/api/login_test.go | 19 ++-- pkg/api/metrics.go | 4 +- pkg/api/openapi3.go | 4 +- pkg/api/org.go | 22 ++--- pkg/api/org_invite.go | 14 +-- pkg/api/org_users.go | 29 +++--- pkg/api/password.go | 5 +- pkg/api/playlist.go | 18 ++-- pkg/api/plugin_dashboards.go | 4 +- pkg/api/plugin_proxy.go | 6 +- pkg/api/plugin_resource.go | 12 +-- pkg/api/pluginproxy/ds_proxy.go | 8 +- pkg/api/pluginproxy/ds_proxy_test.go | 43 ++++----- pkg/api/pluginproxy/pluginproxy.go | 6 +- pkg/api/pluginproxy/pluginproxy_test.go | 22 ++--- pkg/api/plugins.go | 22 ++--- pkg/api/plugins_test.go | 4 +- pkg/api/preferences.go | 16 ++-- pkg/api/quota.go | 14 +-- pkg/api/render.go | 3 +- pkg/api/response/response.go | 10 +- pkg/api/response/web_hack.go | 14 +-- pkg/api/routing/routing.go | 6 +- pkg/api/search.go | 7 +- pkg/api/short_url.go | 6 +- pkg/api/short_url_test.go | 4 +- pkg/api/signup.go | 8 +- pkg/api/swagger.go | 4 +- pkg/api/team.go | 18 ++-- pkg/api/team_members.go | 10 +- pkg/api/team_test.go | 6 +- pkg/api/user.go | 37 ++++---- pkg/api/user_test.go | 5 +- pkg/api/user_token.go | 10 +- pkg/api/user_token_test.go | 12 +-- pkg/infra/appcontext/user.go | 4 +- pkg/infra/appcontext/user_test.go | 4 +- pkg/infra/usagestats/service/api.go | 4 +- pkg/infra/usagestats/service/api_test.go | 4 +- pkg/middleware/auth.go | 34 +++---- pkg/middleware/auth_test.go | 6 +- pkg/middleware/cookies/cookies.go | 4 +- pkg/middleware/dashboard_redirect.go | 6 +- pkg/middleware/logger.go | 4 +- pkg/middleware/logger_test.go | 10 +- pkg/middleware/middleware.go | 4 +- pkg/middleware/middleware_test.go | 9 +- pkg/middleware/quota.go | 4 +- pkg/middleware/recovery_test.go | 6 +- pkg/middleware/testing.go | 10 +- pkg/middleware/validate_host.go | 4 +- pkg/models/user_auth.go | 5 +- pkg/plugins/accesscontrol.go | 6 +- .../manager/client/clienttest/clienttest.go | 6 +- pkg/services/accesscontrol/accesscontrol.go | 24 ++--- pkg/services/accesscontrol/api/api.go | 10 +- pkg/services/accesscontrol/middleware.go | 22 ++--- pkg/services/accesscontrol/middleware_test.go | 14 +-- .../accesscontrol/resourcepermissions/api.go | 14 +-- .../resourcepermissions/api_test.go | 4 +- .../resourcepermissions/middleware.go | 6 +- pkg/services/contexthandler/auth_jwt.go | 3 +- .../contexthandler/auth_proxy_test.go | 3 +- .../contexthandler/authproxy/authproxy.go | 21 +++-- .../authproxy/authproxy_test.go | 6 +- pkg/services/contexthandler/contexthandler.go | 27 +++--- .../contexthandler/contexthandler_test.go | 6 +- .../contexthandler/model/model.go} | 2 +- .../contexthandler/model/model_test.go} | 2 +- pkg/services/correlations/api.go | 14 +-- pkg/services/dashboardimport/api/api.go | 10 +- pkg/services/dashboardimport/api/api_test.go | 6 +- .../datasourceproxy/datasourceproxy.go | 14 +-- pkg/services/export/service.go | 18 ++-- pkg/services/export/stub.go | 10 +- pkg/services/featuremgmt/manager.go | 4 +- pkg/services/hooks/hooks.go | 9 +- pkg/services/libraryelements/api.go | 16 ++-- .../libraryelements/libraryelements_test.go | 5 +- pkg/services/licensing/oss.go | 4 +- pkg/services/live/live.go | 34 +++---- pkg/services/live/pushhttp/push.go | 6 +- pkg/services/navtree/navtree.go | 4 +- pkg/services/navtree/navtreeimpl/admin.go | 10 +- pkg/services/navtree/navtreeimpl/applinks.go | 10 +- .../navtree/navtreeimpl/applinks_test.go | 6 +- pkg/services/navtree/navtreeimpl/navtree.go | 26 ++--- pkg/services/ngalert/api/api_alertmanager.go | 26 ++--- .../ngalert/api/api_alertmanager_test.go | 16 ++-- pkg/services/ngalert/api/api_configuration.go | 12 +-- pkg/services/ngalert/api/api_prometheus.go | 6 +- .../ngalert/api/api_prometheus_test.go | 14 +-- pkg/services/ngalert/api/api_provisioning.go | 50 +++++----- .../ngalert/api/api_provisioning_test.go | 6 +- pkg/services/ngalert/api/api_ruler.go | 14 +-- pkg/services/ngalert/api/api_ruler_test.go | 6 +- pkg/services/ngalert/api/api_testing.go | 10 +- pkg/services/ngalert/api/api_testing_test.go | 10 +- pkg/services/ngalert/api/configuration.go | 12 +-- .../ngalert/api/forking_alertmanager.go | 50 +++++----- .../ngalert/api/forking_prometheus.go | 12 +-- pkg/services/ngalert/api/forking_ruler.go | 28 +++--- .../api/generated_base_api_alertmanager.go | 94 +++++++++---------- .../api/generated_base_api_configuration.go | 22 ++--- .../api/generated_base_api_prometheus.go | 18 ++-- .../api/generated_base_api_provisioning.go | 94 +++++++++---------- .../ngalert/api/generated_base_api_ruler.go | 50 +++++----- .../ngalert/api/generated_base_api_testing.go | 18 ++-- pkg/services/ngalert/api/lotex_am.go | 26 ++--- pkg/services/ngalert/api/lotex_prom.go | 8 +- pkg/services/ngalert/api/lotex_ruler.go | 16 ++-- pkg/services/ngalert/api/lotex_ruler_test.go | 4 +- pkg/services/ngalert/api/provisioning.go | 48 +++++----- pkg/services/ngalert/api/testing_api.go | 10 +- .../templates/controller-api.mustache | 5 +- pkg/services/ngalert/api/util.go | 10 +- pkg/services/ngalert/api/util_test.go | 4 +- pkg/services/ngalert/metrics/ngalert.go | 6 +- pkg/services/publicdashboards/api/api.go | 12 +-- .../publicdashboards/api/common_test.go | 4 +- .../publicdashboards/api/middleware.go | 16 ++-- .../publicdashboards/api/middleware_test.go | 10 +- pkg/services/publicdashboards/api/query.go | 8 +- pkg/services/query/query_test.go | 6 +- pkg/services/queryhistory/api.go | 16 ++-- .../queryhistory/queryhistory_test.go | 6 +- .../querylibrary/querylibraryimpl/http.go | 8 +- pkg/services/quota/quota.go | 4 +- pkg/services/quota/quotaimpl/quota.go | 6 +- pkg/services/quota/quotatest/fake.go | 4 +- pkg/services/searchV2/http.go | 4 +- pkg/services/searchusers/searchusers.go | 12 +-- pkg/services/serviceaccounts/api/api.go | 24 ++--- pkg/services/serviceaccounts/api/token.go | 8 +- pkg/services/star/api/api.go | 12 +-- .../store/entity/httpentitystore/service.go | 20 ++-- pkg/services/store/http.go | 20 ++-- pkg/services/store/k8saccess/client.go | 4 +- pkg/services/store/k8saccess/http.go | 6 +- pkg/services/store/utils.go | 4 +- .../supportbundles/supportbundlesimpl/api.go | 12 +-- pkg/services/thumbs/dummy.go | 16 ++-- pkg/services/thumbs/service.go | 35 +++---- pkg/web/webtest/webtest.go | 14 +-- pkg/web/webtest/webtest_test.go | 12 +-- 180 files changed, 1208 insertions(+), 1182 deletions(-) rename pkg/{models/context.go => services/contexthandler/model/model.go} (99%) rename pkg/{models/context_test.go => services/contexthandler/model/model_test.go} (97%) diff --git a/pkg/api/accesscontrol.go b/pkg/api/accesscontrol.go index 035cf4ac315..55945b28a4d 100644 --- a/pkg/api/accesscontrol.go +++ b/pkg/api/accesscontrol.go @@ -3,9 +3,9 @@ package api import ( "fmt" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/org" @@ -435,7 +435,7 @@ func (hs *HTTPServer) declareFixedRoles() error { // Metadata helpers // getAccessControlMetadata returns the accesscontrol metadata associated with a given resource -func (hs *HTTPServer) getAccessControlMetadata(c *models.ReqContext, +func (hs *HTTPServer) getAccessControlMetadata(c *contextmodel.ReqContext, orgID int64, prefix string, resourceID string) ac.Metadata { ids := map[string]bool{resourceID: true} return hs.getMultiAccessControlMetadata(c, orgID, prefix, ids)[resourceID] @@ -443,7 +443,7 @@ func (hs *HTTPServer) getAccessControlMetadata(c *models.ReqContext, // getMultiAccessControlMetadata returns the accesscontrol metadata associated with a given set of resources // Context must contain permissions in the given org (see LoadPermissionsMiddleware or AuthorizeInOrgMiddleware) -func (hs *HTTPServer) getMultiAccessControlMetadata(c *models.ReqContext, +func (hs *HTTPServer) getMultiAccessControlMetadata(c *contextmodel.ReqContext, orgID int64, prefix string, resourceIDs map[string]bool) map[string]ac.Metadata { if hs.AccessControl.IsDisabled() || !c.QueryBool("accesscontrol") { return map[string]ac.Metadata{} diff --git a/pkg/api/admin.go b/pkg/api/admin.go index 347aa45c20d..19ea7269cbf 100644 --- a/pkg/api/admin.go +++ b/pkg/api/admin.go @@ -5,8 +5,8 @@ import ( "net/http" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/stats" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -25,7 +25,7 @@ import ( // 200: adminGetSettingsResponse // 401: unauthorisedError // 403: forbiddenError -func (hs *HTTPServer) AdminGetSettings(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminGetSettings(c *contextmodel.ReqContext) response.Response { settings, err := hs.getAuthorizedSettings(c.Req.Context(), c.SignedInUser, hs.SettingsProvider.Current()) if err != nil { return response.Error(http.StatusForbidden, "Failed to authorize settings", err) @@ -45,7 +45,7 @@ func (hs *HTTPServer) AdminGetSettings(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) AdminGetStats(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminGetStats(c *contextmodel.ReqContext) response.Response { statsQuery := stats.GetAdminStatsQuery{} if err := hs.statsService.GetAdminStats(c.Req.Context(), &statsQuery); err != nil { diff --git a/pkg/api/admin_encryption.go b/pkg/api/admin_encryption.go index 9f00aa2efd5..e6d850d292a 100644 --- a/pkg/api/admin_encryption.go +++ b/pkg/api/admin_encryption.go @@ -5,11 +5,11 @@ import ( "net/http" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" skv "github.com/grafana/grafana/pkg/services/secrets/kvstore" ) -func (hs *HTTPServer) AdminRotateDataEncryptionKeys(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminRotateDataEncryptionKeys(c *contextmodel.ReqContext) response.Response { if err := hs.SecretsService.RotateDataKeys(c.Req.Context()); err != nil { return response.Error(http.StatusInternalServerError, "Failed to rotate data keys", err) } @@ -17,7 +17,7 @@ func (hs *HTTPServer) AdminRotateDataEncryptionKeys(c *models.ReqContext) respon return response.Respond(http.StatusNoContent, "") } -func (hs *HTTPServer) AdminReEncryptEncryptionKeys(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminReEncryptEncryptionKeys(c *contextmodel.ReqContext) response.Response { if err := hs.SecretsService.ReEncryptDataKeys(c.Req.Context()); err != nil { return response.Error(http.StatusInternalServerError, "Failed to re-encrypt data keys", err) } @@ -25,7 +25,7 @@ func (hs *HTTPServer) AdminReEncryptEncryptionKeys(c *models.ReqContext) respons return response.Respond(http.StatusOK, "Data encryption keys re-encrypted successfully") } -func (hs *HTTPServer) AdminReEncryptSecrets(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminReEncryptSecrets(c *contextmodel.ReqContext) response.Response { success, err := hs.secretsMigrator.ReEncryptSecrets(c.Req.Context()) if err != nil { return response.Error(http.StatusInternalServerError, "Failed to re-encrypt secrets", err) @@ -38,7 +38,7 @@ func (hs *HTTPServer) AdminReEncryptSecrets(c *models.ReqContext) response.Respo return response.Respond(http.StatusOK, "Secrets re-encrypted successfully") } -func (hs *HTTPServer) AdminRollbackSecrets(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminRollbackSecrets(c *contextmodel.ReqContext) response.Response { success, err := hs.secretsMigrator.RollBackSecrets(c.Req.Context()) if err != nil { return response.Error(http.StatusInternalServerError, "Failed to rollback secrets", err) @@ -53,7 +53,7 @@ func (hs *HTTPServer) AdminRollbackSecrets(c *models.ReqContext) response.Respon // To migrate to the plugin, it must be installed and configured // so as not to lose access to migrated secrets -func (hs *HTTPServer) AdminMigrateSecretsToPlugin(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminMigrateSecretsToPlugin(c *contextmodel.ReqContext) response.Response { if skv.EvaluateRemoteSecretsPlugin(c.Req.Context(), hs.secretsPluginManager, hs.Cfg) != nil { hs.log.Warn("Received secrets plugin migration request while plugin is not available") return response.Respond(http.StatusBadRequest, "Secrets plugin is not available") @@ -68,7 +68,7 @@ func (hs *HTTPServer) AdminMigrateSecretsToPlugin(c *models.ReqContext) response // To migrate from the plugin, it must be installed only // as it is possible the user disabled it and then wants to migrate -func (hs *HTTPServer) AdminMigrateSecretsFromPlugin(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminMigrateSecretsFromPlugin(c *contextmodel.ReqContext) response.Response { if hs.secretsPluginManager.SecretsManager(c.Req.Context()) == nil { hs.log.Warn("Received secrets plugin migration request while plugin is not installed") return response.Respond(http.StatusBadRequest, "Secrets plugin is not installed") @@ -81,7 +81,7 @@ func (hs *HTTPServer) AdminMigrateSecretsFromPlugin(c *models.ReqContext) respon return response.Respond(http.StatusOK, "Secret migration from plugin triggered successfully") } -func (hs *HTTPServer) AdminDeleteAllSecretsManagerPluginSecrets(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminDeleteAllSecretsManagerPluginSecrets(c *contextmodel.ReqContext) response.Response { if hs.secretsPluginManager.SecretsManager(c.Req.Context()) == nil { hs.log.Warn("Received secrets plugin deletion request while plugin is not installed") return response.Respond(http.StatusBadRequest, "Secrets plugin is not installed") diff --git a/pkg/api/admin_provisioning.go b/pkg/api/admin_provisioning.go index d770c0fba02..704a2e51b99 100644 --- a/pkg/api/admin_provisioning.go +++ b/pkg/api/admin_provisioning.go @@ -5,7 +5,7 @@ import ( "errors" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" ) // swagger:route POST /admin/provisioning/dashboards/reload admin_provisioning adminProvisioningReloadDashboards @@ -23,7 +23,7 @@ import ( // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) AdminProvisioningReloadDashboards(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminProvisioningReloadDashboards(c *contextmodel.ReqContext) response.Response { err := hs.ProvisioningService.ProvisionDashboards(c.Req.Context()) if err != nil && !errors.Is(err, context.Canceled) { return response.Error(500, "", err) @@ -46,7 +46,7 @@ func (hs *HTTPServer) AdminProvisioningReloadDashboards(c *models.ReqContext) re // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) AdminProvisioningReloadDatasources(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminProvisioningReloadDatasources(c *contextmodel.ReqContext) response.Response { err := hs.ProvisioningService.ProvisionDatasources(c.Req.Context()) if err != nil { return response.Error(500, "", err) @@ -69,7 +69,7 @@ func (hs *HTTPServer) AdminProvisioningReloadDatasources(c *models.ReqContext) r // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) AdminProvisioningReloadPlugins(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminProvisioningReloadPlugins(c *contextmodel.ReqContext) response.Response { err := hs.ProvisioningService.ProvisionPlugins(c.Req.Context()) if err != nil { return response.Error(500, "Failed to reload plugins config", err) @@ -92,7 +92,7 @@ func (hs *HTTPServer) AdminProvisioningReloadPlugins(c *models.ReqContext) respo // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) AdminProvisioningReloadNotifications(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminProvisioningReloadNotifications(c *contextmodel.ReqContext) response.Response { err := hs.ProvisioningService.ProvisionNotifications(c.Req.Context()) if err != nil { return response.Error(500, "", err) @@ -100,7 +100,7 @@ func (hs *HTTPServer) AdminProvisioningReloadNotifications(c *models.ReqContext) return response.Success("Notifications config reloaded") } -func (hs *HTTPServer) AdminProvisioningReloadAlerting(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminProvisioningReloadAlerting(c *contextmodel.ReqContext) response.Response { err := hs.ProvisioningService.ProvisionAlerting(c.Req.Context()) if err != nil { return response.Error(500, "", err) diff --git a/pkg/api/admin_users.go b/pkg/api/admin_users.go index 18628f1114b..af03d961188 100644 --- a/pkg/api/admin_users.go +++ b/pkg/api/admin_users.go @@ -15,6 +15,7 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/auth" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util" @@ -38,7 +39,7 @@ import ( // 403: forbiddenError // 412: preconditionFailedError // 500: internalServerError -func (hs *HTTPServer) AdminCreateUser(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminCreateUser(c *contextmodel.ReqContext) response.Response { form := dtos.AdminCreateUserForm{} if err := web.Bind(c.Req, &form); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -104,7 +105,7 @@ func (hs *HTTPServer) AdminCreateUser(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) AdminUpdateUserPassword(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminUpdateUserPassword(c *contextmodel.ReqContext) response.Response { form := dtos.AdminUpdateUserPasswordForm{} if err := web.Bind(c.Req, &form); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -156,7 +157,7 @@ func (hs *HTTPServer) AdminUpdateUserPassword(c *models.ReqContext) response.Res // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) AdminUpdateUserPermissions(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminUpdateUserPermissions(c *contextmodel.ReqContext) response.Response { form := dtos.AdminUpdateUserPermissionsForm{} if err := web.Bind(c.Req, &form); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -193,7 +194,7 @@ func (hs *HTTPServer) AdminUpdateUserPermissions(c *models.ReqContext) response. // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) AdminDeleteUser(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminDeleteUser(c *contextmodel.ReqContext) response.Response { userID, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", err) @@ -285,7 +286,7 @@ func (hs *HTTPServer) AdminDeleteUser(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) AdminDisableUser(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminDisableUser(c *contextmodel.ReqContext) response.Response { userID, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", err) @@ -328,7 +329,7 @@ func (hs *HTTPServer) AdminDisableUser(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) AdminEnableUser(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminEnableUser(c *contextmodel.ReqContext) response.Response { userID, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", err) @@ -366,7 +367,7 @@ func (hs *HTTPServer) AdminEnableUser(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) AdminLogoutUser(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminLogoutUser(c *contextmodel.ReqContext) response.Response { userID, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", err) @@ -392,7 +393,7 @@ func (hs *HTTPServer) AdminLogoutUser(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) AdminGetUserAuthTokens(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminGetUserAuthTokens(c *contextmodel.ReqContext) response.Response { userID, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", err) @@ -417,7 +418,7 @@ func (hs *HTTPServer) AdminGetUserAuthTokens(c *models.ReqContext) response.Resp // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) AdminRevokeUserAuthToken(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminRevokeUserAuthToken(c *contextmodel.ReqContext) response.Response { cmd := auth.RevokeAuthTokenCmd{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) diff --git a/pkg/api/admin_users_test.go b/pkg/api/admin_users_test.go index b6e72dc0e59..f6d2fb0aa35 100644 --- a/pkg/api/admin_users_test.go +++ b/pkg/api/admin_users_test.go @@ -13,9 +13,9 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db/dbtest" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth/authtest" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/login/logintest" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" @@ -242,7 +242,7 @@ func putAdminScenario(t *testing.T, desc string, url string, routePattern string } sc := setupScenarioContext(t, url) - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c @@ -267,7 +267,7 @@ func adminLogoutUserScenario(t *testing.T, desc string, url string, routePattern } sc := setupScenarioContext(t, url) - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { t.Log("Route handler invoked", "url", c.Req.URL) sc.context = c @@ -295,7 +295,7 @@ func adminRevokeUserAuthTokenScenario(t *testing.T, desc string, url string, rou sc := setupScenarioContext(t, url) sc.userAuthTokenService = fakeAuthTokenService - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c @@ -323,7 +323,7 @@ func adminGetUserAuthTokensScenario(t *testing.T, desc string, url string, route sc := setupScenarioContext(t, url) sc.userAuthTokenService = fakeAuthTokenService - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { sc.context = c sc.context.UserID = testUserID sc.context.OrgID = testOrgID @@ -355,7 +355,7 @@ func adminDisableUserScenario(t *testing.T, desc string, action string, url stri sc.sqlStore = hs.SQLStore sc.authInfoService = authInfoService sc.userService = hs.userService - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { sc.context = c sc.context.UserID = testUserID @@ -381,7 +381,7 @@ func adminDeleteUserScenario(t *testing.T, desc string, url string, routePattern sc := setupScenarioContext(t, url) sc.sqlStore = hs.SQLStore sc.authInfoService = &logintest.AuthInfoServiceFake{} - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { sc.context = c sc.context.UserID = testUserID @@ -402,7 +402,7 @@ func adminCreateUserScenario(t *testing.T, desc string, url string, routePattern } sc := setupScenarioContext(t, url) - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 86fa1884d80..6aec309aa74 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" alertmodels "github.com/grafana/grafana/pkg/services/alerting/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/guardian" @@ -23,7 +24,7 @@ import ( "github.com/grafana/grafana/pkg/web" ) -func (hs *HTTPServer) ValidateOrgAlert(c *models.ReqContext) { +func (hs *HTTPServer) ValidateOrgAlert(c *contextmodel.ReqContext) { id, err := strconv.ParseInt(web.Params(c.Req)[":alertId"], 10, 64) if err != nil { c.JsonApiErr(http.StatusBadRequest, "alertId is invalid", nil) @@ -51,7 +52,7 @@ func (hs *HTTPServer) ValidateOrgAlert(c *models.ReqContext) { // 200: getDashboardStatesResponse // 400: badRequestError // 500: internalServerError -func (hs *HTTPServer) GetAlertStatesForDashboard(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetAlertStatesForDashboard(c *contextmodel.ReqContext) response.Response { dashboardID := c.QueryInt64("dashboardId") if dashboardID == 0 { @@ -78,7 +79,7 @@ func (hs *HTTPServer) GetAlertStatesForDashboard(c *models.ReqContext) response. // 200: getAlertsResponse // 401: unauthorisedError // 500: internalServerError -func (hs *HTTPServer) GetAlerts(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetAlerts(c *contextmodel.ReqContext) response.Response { dashboardQuery := c.Query("dashboardQuery") dashboardTags := c.QueryStrings("dashboardTag") stringDashboardIDs := c.QueryStrings("dashboardId") @@ -165,7 +166,7 @@ func (hs *HTTPServer) GetAlerts(c *models.ReqContext) response.Response { // 422: unprocessableEntityError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) AlertTest(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AlertTest(c *contextmodel.ReqContext) response.Response { dto := dtos.AlertTestCommand{} if err := web.Bind(c.Req, &dto); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -219,7 +220,7 @@ func (hs *HTTPServer) AlertTest(c *models.ReqContext) response.Response { // 200: getAlertResponse // 401: unauthorisedError // 500: internalServerError -func (hs *HTTPServer) GetAlert(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetAlert(c *contextmodel.ReqContext) response.Response { id, err := strconv.ParseInt(web.Params(c.Req)[":alertId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "alertId is invalid", err) @@ -233,8 +234,8 @@ func (hs *HTTPServer) GetAlert(c *models.ReqContext) response.Response { return response.JSON(http.StatusOK, &query.Result) } -func (hs *HTTPServer) GetAlertNotifiers(ngalertEnabled bool) func(*models.ReqContext) response.Response { - return func(_ *models.ReqContext) response.Response { +func (hs *HTTPServer) GetAlertNotifiers(ngalertEnabled bool) func(*contextmodel.ReqContext) response.Response { + return func(_ *contextmodel.ReqContext) response.Response { if ngalertEnabled { return response.JSON(http.StatusOK, channels_config.GetAvailableNotifiers()) } @@ -255,7 +256,7 @@ func (hs *HTTPServer) GetAlertNotifiers(ngalertEnabled bool) func(*models.ReqCon // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetAlertNotificationLookup(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetAlertNotificationLookup(c *contextmodel.ReqContext) response.Response { alertNotifications, err := hs.getAlertNotificationsInternal(c) if err != nil { return response.Error(500, "Failed to get alert notifications", err) @@ -281,7 +282,7 @@ func (hs *HTTPServer) GetAlertNotificationLookup(c *models.ReqContext) response. // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetAlertNotifications(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetAlertNotifications(c *contextmodel.ReqContext) response.Response { alertNotifications, err := hs.getAlertNotificationsInternal(c) if err != nil { return response.Error(500, "Failed to get alert notifications", err) @@ -296,7 +297,7 @@ func (hs *HTTPServer) GetAlertNotifications(c *models.ReqContext) response.Respo return response.JSON(http.StatusOK, result) } -func (hs *HTTPServer) getAlertNotificationsInternal(c *models.ReqContext) ([]*alertmodels.AlertNotification, error) { +func (hs *HTTPServer) getAlertNotificationsInternal(c *contextmodel.ReqContext) ([]*alertmodels.AlertNotification, error) { query := &alertmodels.GetAllAlertNotificationsQuery{OrgId: c.OrgID} if err := hs.AlertNotificationService.GetAllAlertNotifications(c.Req.Context(), query); err != nil { @@ -318,7 +319,7 @@ func (hs *HTTPServer) getAlertNotificationsInternal(c *models.ReqContext) ([]*al // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetAlertNotificationByID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetAlertNotificationByID(c *contextmodel.ReqContext) response.Response { notificationId, err := strconv.ParseInt(web.Params(c.Req)[":notificationId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "notificationId is invalid", err) @@ -355,7 +356,7 @@ func (hs *HTTPServer) GetAlertNotificationByID(c *models.ReqContext) response.Re // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetAlertNotificationByUID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetAlertNotificationByUID(c *contextmodel.ReqContext) response.Response { query := &alertmodels.GetAlertNotificationsWithUidQuery{ OrgId: c.OrgID, Uid: web.Params(c.Req)[":uid"], @@ -388,7 +389,7 @@ func (hs *HTTPServer) GetAlertNotificationByUID(c *models.ReqContext) response.R // 403: forbiddenError // 409: conflictError // 500: internalServerError -func (hs *HTTPServer) CreateAlertNotification(c *models.ReqContext) response.Response { +func (hs *HTTPServer) CreateAlertNotification(c *contextmodel.ReqContext) response.Response { cmd := alertmodels.CreateAlertNotificationCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -421,7 +422,7 @@ func (hs *HTTPServer) CreateAlertNotification(c *models.ReqContext) response.Res // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) UpdateAlertNotification(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateAlertNotification(c *contextmodel.ReqContext) response.Response { cmd := alertmodels.UpdateAlertNotificationCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -468,7 +469,7 @@ func (hs *HTTPServer) UpdateAlertNotification(c *models.ReqContext) response.Res // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) UpdateAlertNotificationByUID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateAlertNotificationByUID(c *contextmodel.ReqContext) response.Response { cmd := alertmodels.UpdateAlertNotificationWithUidCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -568,7 +569,7 @@ func (hs *HTTPServer) fillWithSecureSettingsDataByUID(ctx context.Context, cmd * // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) DeleteAlertNotification(c *models.ReqContext) response.Response { +func (hs *HTTPServer) DeleteAlertNotification(c *contextmodel.ReqContext) response.Response { notificationId, err := strconv.ParseInt(web.Params(c.Req)[":notificationId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "notificationId is invalid", err) @@ -601,7 +602,7 @@ func (hs *HTTPServer) DeleteAlertNotification(c *models.ReqContext) response.Res // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) DeleteAlertNotificationByUID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) DeleteAlertNotificationByUID(c *contextmodel.ReqContext) response.Response { cmd := alertmodels.DeleteAlertNotificationWithUidCommand{ OrgId: c.OrgID, Uid: web.Params(c.Req)[":uid"], @@ -633,7 +634,7 @@ func (hs *HTTPServer) DeleteAlertNotificationByUID(c *models.ReqContext) respons // 403: forbiddenError // 412: SMTPNotEnabledError // 500: internalServerError -func (hs *HTTPServer) NotificationTest(c *models.ReqContext) response.Response { +func (hs *HTTPServer) NotificationTest(c *contextmodel.ReqContext) response.Response { dto := dtos.NotificationTestCommand{} if err := web.Bind(c.Req, &dto); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -672,14 +673,14 @@ func (hs *HTTPServer) NotificationTest(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) PauseAlert(legacyAlertingEnabled *bool) func(c *models.ReqContext) response.Response { +func (hs *HTTPServer) PauseAlert(legacyAlertingEnabled *bool) func(c *contextmodel.ReqContext) response.Response { if legacyAlertingEnabled == nil || !*legacyAlertingEnabled { - return func(_ *models.ReqContext) response.Response { + return func(_ *contextmodel.ReqContext) response.Response { return response.Error(http.StatusBadRequest, "legacy alerting is disabled, so this call has no effect.", nil) } } - return func(c *models.ReqContext) response.Response { + return func(c *contextmodel.ReqContext) response.Response { dto := dtos.PauseAlertCommand{} if err := web.Bind(c.Req, &dto); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -754,14 +755,14 @@ func (hs *HTTPServer) PauseAlert(legacyAlertingEnabled *bool) func(c *models.Req // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) PauseAllAlerts(legacyAlertingEnabled *bool) func(c *models.ReqContext) response.Response { +func (hs *HTTPServer) PauseAllAlerts(legacyAlertingEnabled *bool) func(c *contextmodel.ReqContext) response.Response { if legacyAlertingEnabled == nil || !*legacyAlertingEnabled { - return func(_ *models.ReqContext) response.Response { + return func(_ *contextmodel.ReqContext) response.Response { return response.Error(http.StatusBadRequest, "legacy alerting is disabled, so this call has no effect.", nil) } } - return func(c *models.ReqContext) response.Response { + return func(c *contextmodel.ReqContext) response.Response { dto := dtos.PauseAllAlertsCommand{} if err := web.Bind(c.Req, &dto); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index cddf8d1529e..4937349c09e 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -9,9 +9,9 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/annotations" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/guardian" @@ -31,7 +31,7 @@ import ( // 200: getAnnotationsResponse // 401: unauthorisedError // 500: internalServerError -func (hs *HTTPServer) GetAnnotations(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetAnnotations(c *contextmodel.ReqContext) response.Response { query := &annotations.ItemQuery{ From: c.QueryInt64("from"), To: c.QueryInt64("to"), @@ -114,7 +114,7 @@ func (e *AnnotationError) Error() string { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) PostAnnotation(c *models.ReqContext) response.Response { +func (hs *HTTPServer) PostAnnotation(c *contextmodel.ReqContext) response.Response { cmd := dtos.PostAnnotationsCmd{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -185,7 +185,7 @@ func formatGraphiteAnnotation(what string, data string) string { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) PostGraphiteAnnotation(c *models.ReqContext) response.Response { +func (hs *HTTPServer) PostGraphiteAnnotation(c *contextmodel.ReqContext) response.Response { cmd := dtos.PostGraphiteAnnotationsCmd{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -250,7 +250,7 @@ func (hs *HTTPServer) PostGraphiteAnnotation(c *models.ReqContext) response.Resp // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) UpdateAnnotation(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateAnnotation(c *contextmodel.ReqContext) response.Response { cmd := dtos.UpdateAnnotationsCmd{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -306,7 +306,7 @@ func (hs *HTTPServer) UpdateAnnotation(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) PatchAnnotation(c *models.ReqContext) response.Response { +func (hs *HTTPServer) PatchAnnotation(c *contextmodel.ReqContext) response.Response { cmd := dtos.PatchAnnotationsCmd{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -371,7 +371,7 @@ func (hs *HTTPServer) PatchAnnotation(c *models.ReqContext) response.Response { // 200: okResponse // 401: unauthorisedError // 500: internalServerError -func (hs *HTTPServer) MassDeleteAnnotations(c *models.ReqContext) response.Response { +func (hs *HTTPServer) MassDeleteAnnotations(c *contextmodel.ReqContext) response.Response { cmd := dtos.MassDeleteAnnotationsCmd{} err := web.Bind(c.Req, &cmd) if err != nil { @@ -447,7 +447,7 @@ func (hs *HTTPServer) MassDeleteAnnotations(c *models.ReqContext) response.Respo // 200: getAnnotationByIDResponse // 401: unauthorisedError // 500: internalServerError -func (hs *HTTPServer) GetAnnotationByID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetAnnotationByID(c *contextmodel.ReqContext) response.Response { annotationID, err := strconv.ParseInt(web.Params(c.Req)[":annotationId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "annotationId is invalid", err) @@ -476,7 +476,7 @@ func (hs *HTTPServer) GetAnnotationByID(c *models.ReqContext) response.Response // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) DeleteAnnotationByID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) DeleteAnnotationByID(c *contextmodel.ReqContext) response.Response { annotationID, err := strconv.ParseInt(web.Params(c.Req)[":annotationId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "annotationId is invalid", err) @@ -502,7 +502,7 @@ func (hs *HTTPServer) DeleteAnnotationByID(c *models.ReqContext) response.Respon return response.Success("Annotation deleted") } -func (hs *HTTPServer) canSaveAnnotation(c *models.ReqContext, annotation *annotations.ItemDTO) (bool, error) { +func (hs *HTTPServer) canSaveAnnotation(c *contextmodel.ReqContext, annotation *annotations.ItemDTO) (bool, error) { if annotation.GetType() == annotations.Dashboard { return canEditDashboard(c, annotation.DashboardId) } else { @@ -513,7 +513,7 @@ func (hs *HTTPServer) canSaveAnnotation(c *models.ReqContext, annotation *annota } } -func canEditDashboard(c *models.ReqContext, dashboardID int64) (bool, error) { +func canEditDashboard(c *contextmodel.ReqContext, dashboardID int64) (bool, error) { guard, err := guardian.New(c.Req.Context(), dashboardID, c.OrgID, c.SignedInUser) if err != nil { return false, err @@ -555,7 +555,7 @@ func findAnnotationByID(ctx context.Context, repo annotations.Repository, annota // 200: getAnnotationTagsResponse // 401: unauthorisedError // 500: internalServerError -func (hs *HTTPServer) GetAnnotationTags(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetAnnotationTags(c *contextmodel.ReqContext) response.Response { query := &annotations.TagsQuery{ OrgID: c.OrgID, Tag: c.Query("tag"), @@ -612,7 +612,7 @@ func AnnotationTypeScopeResolver(annotationsRepo annotations.Repository) (string }) } -func (hs *HTTPServer) canCreateAnnotation(c *models.ReqContext, dashboardId int64) (bool, error) { +func (hs *HTTPServer) canCreateAnnotation(c *contextmodel.ReqContext, dashboardId int64) (bool, error) { if dashboardId != 0 { if !hs.AccessControl.IsDisabled() { evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, accesscontrol.ScopeAnnotationsTypeDashboard) @@ -632,7 +632,7 @@ func (hs *HTTPServer) canCreateAnnotation(c *models.ReqContext, dashboardId int6 } } -func (hs *HTTPServer) canMassDeleteAnnotations(c *models.ReqContext, dashboardID int64) (bool, error) { +func (hs *HTTPServer) canMassDeleteAnnotations(c *contextmodel.ReqContext, dashboardID int64) (bool, error) { if dashboardID == 0 { evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsDelete, accesscontrol.ScopeAnnotationsTypeOrganization) return hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator) diff --git a/pkg/api/annotations_test.go b/pkg/api/annotations_test.go index f5c015f03ff..0ec81d2c44a 100644 --- a/pkg/api/annotations_test.go +++ b/pkg/api/annotations_test.go @@ -17,11 +17,11 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db/dbtest" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/annotations/annotationstest" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/org" @@ -296,7 +296,7 @@ func postAnnotationScenario(t *testing.T, desc string, url string, routePattern sc := setupScenarioContext(t, url) sc.dashboardService = dashSvc - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c @@ -320,7 +320,7 @@ func putAnnotationScenario(t *testing.T, desc string, url string, routePattern s hs.SQLStore = store sc := setupScenarioContext(t, url) - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c @@ -345,7 +345,7 @@ func patchAnnotationScenario(t *testing.T, desc string, url string, routePattern hs.SQLStore = store sc := setupScenarioContext(t, url) - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c @@ -370,7 +370,7 @@ func deleteAnnotationsScenario(t *testing.T, desc string, url string, routePatte hs.DashboardService = dashSvc sc := setupScenarioContext(t, url) - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c diff --git a/pkg/api/api.go b/pkg/api/api.go index 807c33fe786..154c4156930 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -33,11 +33,11 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/auth" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/correlations" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources" @@ -173,8 +173,8 @@ func (hs *HTTPServer) registerRoutes() { ) } - r.Get("/explore", authorize(func(c *models.ReqContext) { - if f, ok := reqSignedIn.(func(c *models.ReqContext)); ok { + r.Get("/explore", authorize(func(c *contextmodel.ReqContext) { + if f, ok := reqSignedIn.(func(c *contextmodel.ReqContext)); ok { f(c) } middleware.EnsureEditorOrViewerCanEdit(c) diff --git a/pkg/api/apikey.go b/pkg/api/apikey.go index 79a89680f8a..02ba9b6cfce 100644 --- a/pkg/api/apikey.go +++ b/pkg/api/apikey.go @@ -9,8 +9,8 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/components/apikeygen" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/apikey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/web" ) @@ -26,7 +26,7 @@ import ( // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetAPIKeys(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetAPIKeys(c *contextmodel.ReqContext) response.Response { query := apikey.GetApiKeysQuery{OrgId: c.OrgID, User: c.SignedInUser, IncludeExpired: c.QueryBool("includeExpired")} if err := hs.apiKeyService.GetAPIKeys(c.Req.Context(), &query); err != nil { @@ -70,7 +70,7 @@ func (hs *HTTPServer) GetAPIKeys(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) DeleteAPIKey(c *models.ReqContext) response.Response { +func (hs *HTTPServer) DeleteAPIKey(c *contextmodel.ReqContext) response.Response { id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", err) @@ -104,7 +104,7 @@ func (hs *HTTPServer) DeleteAPIKey(c *models.ReqContext) response.Response { // 403: forbiddenError // 409: conflictError // 500: internalServerError -func (hs *HTTPServer) AddAPIKey(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AddAPIKey(c *contextmodel.ReqContext) response.Response { cmd := apikey.AddCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) diff --git a/pkg/api/avatar/avatar.go b/pkg/api/avatar/avatar.go index 6ba3e485c88..bcfe2eb77c6 100644 --- a/pkg/api/avatar/avatar.go +++ b/pkg/api/avatar/avatar.go @@ -21,7 +21,7 @@ import ( "time" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" gocache "github.com/patrickmn/go-cache" @@ -98,7 +98,7 @@ type AvatarCacheServer struct { var validMD5 = regexp.MustCompile("^[a-fA-F0-9]{32}$") -func (a *AvatarCacheServer) Handler(ctx *models.ReqContext) { +func (a *AvatarCacheServer) Handler(ctx *contextmodel.ReqContext) { hash := web.Params(ctx.Req)[":hash"] if len(hash) != 32 || !validMD5.MatchString(hash) { diff --git a/pkg/api/comments.go b/pkg/api/comments.go index b1f7a88f8f6..3a3d3c52b42 100644 --- a/pkg/api/comments.go +++ b/pkg/api/comments.go @@ -5,14 +5,14 @@ import ( "net/http" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/comments" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" ) -func (hs *HTTPServer) commentsGet(c *models.ReqContext) response.Response { +func (hs *HTTPServer) commentsGet(c *contextmodel.ReqContext) response.Response { cmd := comments.GetCmd{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -29,7 +29,7 @@ func (hs *HTTPServer) commentsGet(c *models.ReqContext) response.Response { }) } -func (hs *HTTPServer) commentsCreate(c *models.ReqContext) response.Response { +func (hs *HTTPServer) commentsCreate(c *contextmodel.ReqContext) response.Response { cmd := comments.CreateCmd{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index 5c32a3f6f89..3e74a3e2ebc 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -35,6 +35,7 @@ import ( "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/contexthandler/authproxy" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" dashboardsstore "github.com/grafana/grafana/pkg/services/dashboards/database" dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service" @@ -76,7 +77,7 @@ func loggedInUserScenarioWithRole(t *testing.T, desc string, method string, url sc := setupScenarioContext(t, url) sc.sqlStore = sqlStore sc.userService = usertest.NewUserServiceFake() - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { sc.context = c sc.context.UserID = testUserID sc.context.OrgID = testOrgID @@ -102,7 +103,7 @@ func loggedInUserScenarioWithRole(t *testing.T, desc string, method string, url func anonymousUserScenario(t *testing.T, desc string, method string, url string, routePattern string, fn scenarioFunc) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { sc := setupScenarioContext(t, url) - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { sc.context = c if sc.handlerFunc != nil { return sc.handlerFunc(sc.context) @@ -178,7 +179,7 @@ type scenarioContext struct { t *testing.T cfg *setting.Cfg m *web.Mux - context *models.ReqContext + context *contextmodel.ReqContext resp *httptest.ResponseRecorder handlerFunc handlerFunc defaultHandler web.Handler @@ -197,7 +198,7 @@ func (sc *scenarioContext) exec() { } type scenarioFunc func(c *scenarioContext) -type handlerFunc func(c *models.ReqContext) response.Response +type handlerFunc func(c *contextmodel.ReqContext) response.Response func getContextHandler(t *testing.T, cfg *setting.Cfg) *contexthandler.ContextHandler { t.Helper() @@ -266,7 +267,7 @@ type accessControlScenarioContext struct { // initCtx is used in a middleware to set the initial context // of the request server side. Can be used to pretend sign in. - initCtx *models.ReqContext + initCtx *contextmodel.ReqContext // hs is a minimal HTTPServer for the accesscontrol tests to pass. hs *HTTPServer @@ -302,17 +303,17 @@ func userWithPermissions(orgID int64, permissions []accesscontrol.Permission) *u } // setInitCtxSignedInUser sets a copy of the user in initCtx -func setInitCtxSignedInUser(initCtx *models.ReqContext, user user.SignedInUser) { +func setInitCtxSignedInUser(initCtx *contextmodel.ReqContext, user user.SignedInUser) { initCtx.IsSignedIn = true initCtx.SignedInUser = &user } -func setInitCtxSignedInViewer(initCtx *models.ReqContext) { +func setInitCtxSignedInViewer(initCtx *contextmodel.ReqContext) { initCtx.IsSignedIn = true initCtx.SignedInUser = &user.SignedInUser{UserID: testUserID, OrgID: 1, OrgRole: org.RoleViewer, Login: testUserLogin} } -func setInitCtxSignedInOrgAdmin(initCtx *models.ReqContext) { +func setInitCtxSignedInOrgAdmin(initCtx *contextmodel.ReqContext) { initCtx.IsSignedIn = true initCtx.SignedInUser = &user.SignedInUser{UserID: testUserID, OrgID: 1, OrgRole: org.RoleAdmin, Login: testUserLogin} } @@ -434,7 +435,7 @@ func setupHTTPServerWithCfgDb( m := web.New() // middleware to set the test initial context - initCtx := &models.ReqContext{} + initCtx := &contextmodel.ReqContext{} m.Use(func(c *web.Context) { initCtx.Context = c initCtx.Logger = log.New("api-test") diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 95b1e98960c..ce2ca9d31aa 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -18,9 +18,9 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/kinds/dashboard" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/alerting" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" dashver "github.com/grafana/grafana/pkg/services/dashboardversion" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -39,7 +39,7 @@ const ( anonString = "Anonymous" ) -func (hs *HTTPServer) isDashboardStarredByUser(c *models.ReqContext, dashID int64) (bool, error) { +func (hs *HTTPServer) isDashboardStarredByUser(c *contextmodel.ReqContext, dashID int64) (bool, error) { if !c.IsSignedIn { return false, nil } @@ -63,7 +63,7 @@ func dashboardGuardianResponse(err error) response.Response { // 200: trimDashboardResponse // 401: unauthorisedError // 500: internalServerError -func (hs *HTTPServer) TrimDashboard(c *models.ReqContext) response.Response { +func (hs *HTTPServer) TrimDashboard(c *contextmodel.ReqContext) response.Response { cmd := dashboards.TrimDashboardCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -93,7 +93,7 @@ func (hs *HTTPServer) TrimDashboard(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetDashboard(c *contextmodel.ReqContext) response.Response { uid := web.Params(c.Req)[":uid"] dash, rsp := hs.getDashboardHelper(c.Req.Context(), c.OrgID, 0, uid) if rsp != nil { @@ -242,7 +242,7 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response { return response.JSON(http.StatusOK, dto) } -func (hs *HTTPServer) getAnnotationPermissionsByScope(c *models.ReqContext, actions *dtos.AnnotationActions, scope string) { +func (hs *HTTPServer) getAnnotationPermissionsByScope(c *contextmodel.ReqContext, actions *dtos.AnnotationActions, scope string) { var err error evaluate := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, scope) @@ -302,11 +302,11 @@ func (hs *HTTPServer) getDashboardHelper(ctx context.Context, orgID int64, id in // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) DeleteDashboardByUID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) DeleteDashboardByUID(c *contextmodel.ReqContext) response.Response { return hs.deleteDashboard(c) } -func (hs *HTTPServer) deleteDashboard(c *models.ReqContext) response.Response { +func (hs *HTTPServer) deleteDashboard(c *contextmodel.ReqContext) response.Response { dash, rsp := hs.getDashboardHelper(c.Req.Context(), c.OrgID, 0, web.Params(c.Req)[":uid"]) if rsp != nil { return rsp @@ -365,7 +365,7 @@ func (hs *HTTPServer) deleteDashboard(c *models.ReqContext) response.Response { // 412: preconditionFailedError // 422: unprocessableEntityError // 500: internalServerError -func (hs *HTTPServer) PostDashboard(c *models.ReqContext) response.Response { +func (hs *HTTPServer) PostDashboard(c *contextmodel.ReqContext) response.Response { cmd := dashboards.SaveDashboardCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -396,7 +396,7 @@ func (hs *HTTPServer) PostDashboard(c *models.ReqContext) response.Response { return hs.postDashboard(c, cmd) } -func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd dashboards.SaveDashboardCommand) response.Response { +func (hs *HTTPServer) postDashboard(c *contextmodel.ReqContext, cmd dashboards.SaveDashboardCommand) response.Response { ctx := c.Req.Context() var err error cmd.OrgID = c.OrgID @@ -517,7 +517,7 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd dashboards.SaveDas // 200: getHomeDashboardResponse // 401: unauthorisedError // 500: internalServerError -func (hs *HTTPServer) GetHomeDashboard(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetHomeDashboard(c *contextmodel.ReqContext) response.Response { prefsQuery := pref.GetPreferenceWithDefaultsQuery{OrgID: c.OrgID, UserID: c.SignedInUser.UserID, Teams: c.Teams} homePage := hs.Cfg.HomePage @@ -575,7 +575,7 @@ func (hs *HTTPServer) GetHomeDashboard(c *models.ReqContext) response.Response { return response.JSON(http.StatusOK, &dash) } -func (hs *HTTPServer) addGettingStartedPanelToHomeDashboard(c *models.ReqContext, dash *simplejson.Json) { +func (hs *HTTPServer) addGettingStartedPanelToHomeDashboard(c *contextmodel.ReqContext, dash *simplejson.Json) { // We only add this getting started panel for Admins who have not dismissed it, // and if a custom default home dashboard hasn't been configured if !c.HasUserRole(org.RoleAdmin) || @@ -626,7 +626,7 @@ func (hs *HTTPServer) addGettingStartedPanelToHomeDashboard(c *models.ReqContext // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetDashboardVersions(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetDashboardVersions(c *contextmodel.ReqContext) response.Response { var dashID int64 var err error @@ -709,7 +709,7 @@ func (hs *HTTPServer) GetDashboardVersions(c *models.ReqContext) response.Respon // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetDashboardVersion(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetDashboardVersion(c *contextmodel.ReqContext) response.Response { var dashID int64 var err error @@ -784,7 +784,7 @@ func (hs *HTTPServer) GetDashboardVersion(c *models.ReqContext) response.Respons // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) ValidateDashboard(c *models.ReqContext) response.Response { +func (hs *HTTPServer) ValidateDashboard(c *contextmodel.ReqContext) response.Response { cmd := dashboards.ValidateDashboardCommand{} if err := web.Bind(c.Req, &cmd); err != nil { @@ -846,7 +846,7 @@ func (hs *HTTPServer) ValidateDashboard(c *models.ReqContext) response.Response // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) CalculateDashboardDiff(c *models.ReqContext) response.Response { +func (hs *HTTPServer) CalculateDashboardDiff(c *contextmodel.ReqContext) response.Response { apiOptions := dtos.CalculateDiffOptions{} if err := web.Bind(c.Req, &apiOptions); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -958,7 +958,7 @@ func (hs *HTTPServer) CalculateDashboardDiff(c *models.ReqContext) response.Resp // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) RestoreDashboardVersion(c *models.ReqContext) response.Response { +func (hs *HTTPServer) RestoreDashboardVersion(c *contextmodel.ReqContext) response.Response { var dashID int64 var err error @@ -1016,7 +1016,7 @@ func (hs *HTTPServer) RestoreDashboardVersion(c *models.ReqContext) response.Res // 200: getDashboardsTagsResponse // 401: unauthorisedError // 500: internalServerError -func (hs *HTTPServer) GetDashboardTags(c *models.ReqContext) { +func (hs *HTTPServer) GetDashboardTags(c *contextmodel.ReqContext) { query := dashboards.GetDashboardTagsQuery{OrgID: c.OrgID} queryResult, err := hs.DashboardService.GetDashboardTags(c.Req.Context(), &query) if err != nil { @@ -1028,7 +1028,7 @@ func (hs *HTTPServer) GetDashboardTags(c *models.ReqContext) { } // GetDashboardUIDs converts internal ids to UIDs -func (hs *HTTPServer) GetDashboardUIDs(c *models.ReqContext) { +func (hs *HTTPServer) GetDashboardUIDs(c *contextmodel.ReqContext) { ids := strings.Split(web.Params(c.Req)[":ids"], ",") uids := make([]string, 0, len(ids)) diff --git a/pkg/api/dashboard_permission.go b/pkg/api/dashboard_permission.go index 6548547ee14..9f83b6e7579 100644 --- a/pkg/api/dashboard_permission.go +++ b/pkg/api/dashboard_permission.go @@ -9,8 +9,8 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/web" @@ -41,7 +41,7 @@ import ( // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetDashboardPermissionList(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetDashboardPermissionList(c *contextmodel.ReqContext) response.Response { var dashID int64 var err error dashUID := web.Params(c.Req)[":uid"] @@ -123,7 +123,7 @@ func (hs *HTTPServer) GetDashboardPermissionList(c *models.ReqContext) response. // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) UpdateDashboardPermissions(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateDashboardPermissions(c *contextmodel.ReqContext) response.Response { var dashID int64 var err error apiCmd := dtos.UpdateDashboardACLCommand{} diff --git a/pkg/api/dashboard_permission_test.go b/pkg/api/dashboard_permission_test.go index 4ae44e3a02e..6088acfdf96 100644 --- a/pkg/api/dashboard_permission_test.go +++ b/pkg/api/dashboard_permission_test.go @@ -15,8 +15,8 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/models" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -354,7 +354,7 @@ func updateDashboardPermissionScenario(t *testing.T, ctx updatePermissionContext t.Run(fmt.Sprintf("%s %s", ctx.desc, ctx.url), func(t *testing.T) { sc := setupScenarioContext(t, ctx.url) - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(ctx.cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index 5d017a82838..a367c69dd5c 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -12,7 +12,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/metrics" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboardsnapshots" "github.com/grafana/grafana/pkg/services/guardian" @@ -33,7 +33,7 @@ var client = &http.Client{ // Responses: // 200: getSharingOptionsResponse // 401: unauthorisedError -func (hs *HTTPServer) GetSharingOptions(c *models.ReqContext) { +func (hs *HTTPServer) GetSharingOptions(c *contextmodel.ReqContext) { c.JSON(http.StatusOK, util.DynMap{ "snapshotEnabled": hs.Cfg.SnapshotEnabled, "externalSnapshotURL": hs.Cfg.ExternalSnapshotUrl, @@ -105,7 +105,7 @@ func createOriginalDashboardURL(cmd *dashboardsnapshots.CreateDashboardSnapshotC // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) CreateDashboardSnapshot(c *models.ReqContext) response.Response { +func (hs *HTTPServer) CreateDashboardSnapshot(c *contextmodel.ReqContext) response.Response { if !hs.Cfg.SnapshotEnabled { c.JsonApiErr(http.StatusForbidden, "Dashboard Snapshots are disabled", nil) return nil @@ -200,7 +200,7 @@ func (hs *HTTPServer) CreateDashboardSnapshot(c *models.ReqContext) response.Res // 400: badRequestError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetDashboardSnapshot(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetDashboardSnapshot(c *contextmodel.ReqContext) response.Response { if !hs.Cfg.SnapshotEnabled { c.JsonApiErr(http.StatusForbidden, "Dashboard Snapshots are disabled", nil) return nil @@ -284,7 +284,7 @@ func deleteExternalDashboardSnapshot(externalUrl string) error { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) DeleteDashboardSnapshotByDeleteKey(c *models.ReqContext) response.Response { +func (hs *HTTPServer) DeleteDashboardSnapshotByDeleteKey(c *contextmodel.ReqContext) response.Response { if !hs.Cfg.SnapshotEnabled { c.JsonApiErr(http.StatusForbidden, "Dashboard Snapshots are disabled", nil) return nil @@ -329,7 +329,7 @@ func (hs *HTTPServer) DeleteDashboardSnapshotByDeleteKey(c *models.ReqContext) r // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) DeleteDashboardSnapshot(c *models.ReqContext) response.Response { +func (hs *HTTPServer) DeleteDashboardSnapshot(c *contextmodel.ReqContext) response.Response { if !hs.Cfg.SnapshotEnabled { c.JsonApiErr(http.StatusForbidden, "Dashboard Snapshots are disabled", nil) return nil @@ -399,7 +399,7 @@ func (hs *HTTPServer) DeleteDashboardSnapshot(c *models.ReqContext) response.Res // Responses: // 200: searchDashboardSnapshotsResponse // 500: internalServerError -func (hs *HTTPServer) SearchDashboardSnapshots(c *models.ReqContext) response.Response { +func (hs *HTTPServer) SearchDashboardSnapshots(c *contextmodel.ReqContext) response.Response { if !hs.Cfg.SnapshotEnabled { c.JsonApiErr(http.StatusForbidden, "Dashboard Snapshots are disabled", nil) return nil diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 0623d413707..2d2f0d42d22 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -22,7 +22,6 @@ import ( "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/infra/usagestats" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/registry/corekind" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" @@ -30,6 +29,7 @@ import ( accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/annotations/annotationstest" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/dashboards/service" @@ -58,7 +58,7 @@ func TestGetHomeDashboard(t *testing.T) { httpReq, err := http.NewRequest(http.MethodGet, "", nil) require.NoError(t, err) httpReq.Header.Add("Content-Type", "application/json") - req := &models.ReqContext{SignedInUser: &user.SignedInUser{}, Context: &web.Context{Req: httpReq}} + req := &contextmodel.ReqContext{SignedInUser: &user.SignedInUser{}, Context: &web.Context{Req: httpReq}} cfg := setting.NewCfg() cfg.StaticRootPath = "../../public/" prefService := preftest.NewPreferenceServiceFake() @@ -1076,7 +1076,7 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s } sc := setupScenarioContext(t, url) - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c @@ -1108,7 +1108,7 @@ func postValidateScenario(t *testing.T, desc string, url string, routePattern st } sc := setupScenarioContext(t, url) - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c @@ -1148,7 +1148,7 @@ func postDiffScenario(t *testing.T, desc string, url string, routePattern string } sc := setupScenarioContext(t, url) - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c @@ -1190,7 +1190,7 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout sc := setupScenarioContext(t, url) sc.sqlStore = sqlStore sc.dashboardVersionService = fakeDashboardVersionService - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go index 09edcfd379a..b7be8b84979 100644 --- a/pkg/api/dataproxy.go +++ b/pkg/api/dataproxy.go @@ -1,6 +1,6 @@ package api -import "github.com/grafana/grafana/pkg/models" +import contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" // swagger:route GET /datasources/proxy/{id}/{datasource_proxy_route} datasources datasourceProxyGETcalls // @@ -56,7 +56,7 @@ import "github.com/grafana/grafana/pkg/models" // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) ProxyDataSourceRequest(c *models.ReqContext) { +func (hs *HTTPServer) ProxyDataSourceRequest(c *contextmodel.ReqContext) { hs.DataProxy.ProxyDataSourceRequest(c) } @@ -102,7 +102,7 @@ func (hs *HTTPServer) ProxyDataSourceRequest(c *models.ReqContext) { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) ProxyDataSourceRequestWithUID(c *models.ReqContext) { +func (hs *HTTPServer) ProxyDataSourceRequestWithUID(c *contextmodel.ReqContext) { hs.DataProxy.ProxyDatasourceRequestWithUID(c, "") } diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index 7e2ff3f32ec..b2efa244fe3 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -17,8 +17,8 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins/adapters" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/datasources/permissions" "github.com/grafana/grafana/pkg/services/user" @@ -42,7 +42,7 @@ var secretsPluginError datasources.ErrDatasourceSecretsPluginUserFriendly // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetDataSources(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetDataSources(c *contextmodel.ReqContext) response.Response { query := datasources.GetDataSourcesQuery{OrgId: c.OrgID, DataSourceLimit: hs.Cfg.DataSourceLimit} if err := hs.DataSourcesService.GetDataSources(c.Req.Context(), &query); err != nil { @@ -106,7 +106,7 @@ func (hs *HTTPServer) GetDataSources(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetDataSourceById(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetDataSourceById(c *contextmodel.ReqContext) response.Response { id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", nil) @@ -151,7 +151,7 @@ func (hs *HTTPServer) GetDataSourceById(c *models.ReqContext) response.Response // 404: notFoundError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) DeleteDataSourceById(c *models.ReqContext) response.Response { +func (hs *HTTPServer) DeleteDataSourceById(c *contextmodel.ReqContext) response.Response { id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", err) @@ -202,7 +202,7 @@ func (hs *HTTPServer) DeleteDataSourceById(c *models.ReqContext) response.Respon // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetDataSourceByUID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetDataSourceByUID(c *contextmodel.ReqContext) response.Response { ds, err := hs.getRawDataSourceByUID(c.Req.Context(), web.Params(c.Req)[":uid"], c.OrgID) if err != nil { @@ -233,7 +233,7 @@ func (hs *HTTPServer) GetDataSourceByUID(c *models.ReqContext) response.Response // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) DeleteDataSourceByUID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) DeleteDataSourceByUID(c *contextmodel.ReqContext) response.Response { uid := web.Params(c.Req)[":uid"] if uid == "" { @@ -283,7 +283,7 @@ func (hs *HTTPServer) DeleteDataSourceByUID(c *models.ReqContext) response.Respo // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) DeleteDataSourceByName(c *models.ReqContext) response.Response { +func (hs *HTTPServer) DeleteDataSourceByName(c *contextmodel.ReqContext) response.Response { name := web.Params(c.Req)[":name"] if name == "" { @@ -365,7 +365,7 @@ func validateJSONData(jsonData *simplejson.Json, cfg *setting.Cfg) error { // 403: forbiddenError // 409: conflictError // 500: internalServerError -func (hs *HTTPServer) AddDataSource(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AddDataSource(c *contextmodel.ReqContext) response.Response { cmd := datasources.AddDataSourceCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -431,7 +431,7 @@ func (hs *HTTPServer) AddDataSource(c *models.ReqContext) response.Response { // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) UpdateDataSourceByID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateDataSourceByID(c *contextmodel.ReqContext) response.Response { cmd := datasources.UpdateDataSourceCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -475,7 +475,7 @@ func (hs *HTTPServer) UpdateDataSourceByID(c *models.ReqContext) response.Respon // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) UpdateDataSourceByUID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateDataSourceByUID(c *contextmodel.ReqContext) response.Response { cmd := datasources.UpdateDataSourceCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -500,7 +500,7 @@ func (hs *HTTPServer) UpdateDataSourceByUID(c *models.ReqContext) response.Respo return hs.updateDataSourceByID(c, ds, cmd) } -func (hs *HTTPServer) updateDataSourceByID(c *models.ReqContext, ds *datasources.DataSource, cmd datasources.UpdateDataSourceCommand) response.Response { +func (hs *HTTPServer) updateDataSourceByID(c *contextmodel.ReqContext, ds *datasources.DataSource, cmd datasources.UpdateDataSourceCommand) response.Response { if ds.ReadOnly { return response.Error(403, "Cannot update read-only data source", nil) } @@ -579,7 +579,7 @@ func (hs *HTTPServer) getRawDataSourceByUID(ctx context.Context, uid string, org // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetDataSourceByName(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetDataSourceByName(c *contextmodel.ReqContext) response.Response { query := datasources.GetDataSourceQuery{Name: web.Params(c.Req)[":name"], OrgId: c.OrgID} if err := hs.DataSourcesService.GetDataSource(c.Req.Context(), &query); err != nil { @@ -606,7 +606,7 @@ func (hs *HTTPServer) GetDataSourceByName(c *models.ReqContext) response.Respons // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetDataSourceIdByName(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetDataSourceIdByName(c *contextmodel.ReqContext) response.Response { query := datasources.GetDataSourceQuery{Name: web.Params(c.Req)[":name"], OrgId: c.OrgID} if err := hs.DataSourcesService.GetDataSource(c.Req.Context(), &query); err != nil { @@ -639,7 +639,7 @@ func (hs *HTTPServer) GetDataSourceIdByName(c *models.ReqContext) response.Respo // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) CallDatasourceResource(c *models.ReqContext) { +func (hs *HTTPServer) CallDatasourceResource(c *contextmodel.ReqContext) { datasourceID, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { c.JsonApiErr(http.StatusBadRequest, "id is invalid", nil) @@ -675,7 +675,7 @@ func (hs *HTTPServer) CallDatasourceResource(c *models.ReqContext) { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) CallDatasourceResourceWithUID(c *models.ReqContext) { +func (hs *HTTPServer) CallDatasourceResourceWithUID(c *contextmodel.ReqContext) { dsUID := web.Params(c.Req)[":uid"] if !util.IsValidShortUID(dsUID) { c.JsonApiErr(http.StatusBadRequest, "UID is invalid", nil) @@ -746,7 +746,7 @@ func (hs *HTTPServer) convertModelToDtos(ctx context.Context, ds *datasources.Da // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) CheckDatasourceHealthWithUID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) CheckDatasourceHealthWithUID(c *contextmodel.ReqContext) response.Response { dsUID := web.Params(c.Req)[":uid"] if !util.IsValidShortUID(dsUID) { return response.Error(http.StatusBadRequest, "UID is invalid", nil) @@ -776,7 +776,7 @@ func (hs *HTTPServer) CheckDatasourceHealthWithUID(c *models.ReqContext) respons // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) CheckDatasourceHealth(c *models.ReqContext) response.Response { +func (hs *HTTPServer) CheckDatasourceHealth(c *contextmodel.ReqContext) response.Response { datasourceID, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", nil) @@ -792,7 +792,7 @@ func (hs *HTTPServer) CheckDatasourceHealth(c *models.ReqContext) response.Respo return hs.checkDatasourceHealth(c, ds) } -func (hs *HTTPServer) checkDatasourceHealth(c *models.ReqContext, ds *datasources.DataSource) response.Response { +func (hs *HTTPServer) checkDatasourceHealth(c *contextmodel.ReqContext, ds *datasources.DataSource) response.Response { plugin, exists := hs.pluginStore.Plugin(c.Req.Context(), ds.Type) if !exists { return response.Error(http.StatusInternalServerError, "Unable to find datasource plugin", nil) diff --git a/pkg/api/datasources_test.go b/pkg/api/datasources_test.go index f9b313c6b5a..be708e89e6f 100644 --- a/pkg/api/datasources_test.go +++ b/pkg/api/datasources_test.go @@ -16,11 +16,11 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db/dbtest" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/datasources/permissions" "github.com/grafana/grafana/pkg/setting" @@ -88,7 +88,7 @@ func TestAddDataSource_InvalidURL(t *testing.T) { Cfg: setting.NewCfg(), } - sc.m.Post(sc.url, routing.Wrap(func(c *models.ReqContext) response.Response { + sc.m.Post(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(datasources.AddDataSourceCommand{ Name: "Test", Url: "invalid:url", @@ -119,7 +119,7 @@ func TestAddDataSource_URLWithoutProtocol(t *testing.T) { sc := setupScenarioContext(t, "/api/datasources") - sc.m.Post(sc.url, routing.Wrap(func(c *models.ReqContext) response.Response { + sc.m.Post(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(datasources.AddDataSourceCommand{ Name: name, Url: url, @@ -149,7 +149,7 @@ func TestAddDataSource_InvalidJSONData(t *testing.T) { jsonData := simplejson.New() jsonData.Set("httpHeaderName1", hs.Cfg.AuthProxyHeaderName) - sc.m.Post(sc.url, routing.Wrap(func(c *models.ReqContext) response.Response { + sc.m.Post(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(datasources.AddDataSourceCommand{ Name: "Test", Url: "localhost:5432", @@ -173,7 +173,7 @@ func TestUpdateDataSource_InvalidURL(t *testing.T) { } sc := setupScenarioContext(t, "/api/datasources/1234") - sc.m.Put(sc.url, routing.Wrap(func(c *models.ReqContext) response.Response { + sc.m.Put(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(datasources.AddDataSourceCommand{ Name: "Test", Url: "invalid:url", @@ -201,7 +201,7 @@ func TestUpdateDataSource_InvalidJSONData(t *testing.T) { jsonData := simplejson.New() jsonData.Set("httpHeaderName1", hs.Cfg.AuthProxyHeaderName) - sc.m.Put(sc.url, routing.Wrap(func(c *models.ReqContext) response.Response { + sc.m.Put(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(datasources.AddDataSourceCommand{ Name: "Test", Url: "localhost:5432", @@ -233,7 +233,7 @@ func TestUpdateDataSource_URLWithoutProtocol(t *testing.T) { sc := setupScenarioContext(t, "/api/datasources/1234") - sc.m.Put(sc.url, routing.Wrap(func(c *models.ReqContext) response.Response { + sc.m.Put(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(datasources.AddDataSourceCommand{ Name: name, Url: url, diff --git a/pkg/api/folder.go b/pkg/api/folder.go index 20cede8a574..5470d46bda8 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -8,7 +8,7 @@ import ( "github.com/grafana/grafana/pkg/api/apierrors" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" @@ -31,7 +31,7 @@ import ( // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetFolders(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetFolders(c *contextmodel.ReqContext) response.Response { var folders []*folder.Folder var err error if hs.Features.IsEnabled(featuremgmt.FlagNestedFolders) { @@ -82,7 +82,7 @@ func (hs *HTTPServer) GetFolders(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetFolderByUID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetFolderByUID(c *contextmodel.ReqContext) response.Response { uid := web.Params(c.Req)[":uid"] folder, err := hs.folderService.Get(c.Req.Context(), &folder.GetFolderQuery{OrgID: c.OrgID, UID: &uid, SignedInUser: c.SignedInUser}) if err != nil { @@ -109,7 +109,7 @@ func (hs *HTTPServer) GetFolderByUID(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetFolderByID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetFolderByID(c *contextmodel.ReqContext) response.Response { id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", err) @@ -139,7 +139,7 @@ func (hs *HTTPServer) GetFolderByID(c *models.ReqContext) response.Response { // 403: forbiddenError // 409: conflictError // 500: internalServerError -func (hs *HTTPServer) CreateFolder(c *models.ReqContext) response.Response { +func (hs *HTTPServer) CreateFolder(c *contextmodel.ReqContext) response.Response { cmd := folder.CreateFolderCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -167,7 +167,7 @@ func (hs *HTTPServer) CreateFolder(c *models.ReqContext) response.Response { return response.JSON(http.StatusOK, hs.newToFolderDto(c, g, folder)) } -func (hs *HTTPServer) MoveFolder(c *models.ReqContext) response.Response { +func (hs *HTTPServer) MoveFolder(c *contextmodel.ReqContext) response.Response { if hs.Features.IsEnabled(featuremgmt.FlagNestedFolders) { cmd := folder.MoveFolderCommand{} if err := web.Bind(c.Req, &cmd); err != nil { @@ -205,7 +205,7 @@ func (hs *HTTPServer) MoveFolder(c *models.ReqContext) response.Response { // 404: notFoundError // 409: conflictError // 500: internalServerError -func (hs *HTTPServer) UpdateFolder(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateFolder(c *contextmodel.ReqContext) response.Response { cmd := folder.UpdateFolderCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -240,7 +240,7 @@ func (hs *HTTPServer) UpdateFolder(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) DeleteFolder(c *models.ReqContext) response.Response { // temporarily adding this function to HTTPServer, will be removed from HTTPServer when librarypanels featuretoggle is removed +func (hs *HTTPServer) DeleteFolder(c *contextmodel.ReqContext) response.Response { // temporarily adding this function to HTTPServer, will be removed from HTTPServer when librarypanels featuretoggle is removed err := hs.LibraryElementService.DeleteLibraryElementsInFolder(c.Req.Context(), c.SignedInUser, web.Params(c.Req)[":uid"]) if err != nil { if errors.Is(err, libraryelements.ErrFolderHasConnectedLibraryElements) { @@ -258,7 +258,7 @@ func (hs *HTTPServer) DeleteFolder(c *models.ReqContext) response.Response { // return response.JSON(http.StatusOK, "") } -func (hs *HTTPServer) newToFolderDto(c *models.ReqContext, g guardian.DashboardGuardian, folder *folder.Folder) dtos.Folder { +func (hs *HTTPServer) newToFolderDto(c *contextmodel.ReqContext, g guardian.DashboardGuardian, folder *folder.Folder) dtos.Folder { canEdit, _ := g.CanEdit() canSave, _ := g.CanSave() canAdmin, _ := g.CanAdmin() @@ -293,7 +293,7 @@ func (hs *HTTPServer) newToFolderDto(c *models.ReqContext, g guardian.DashboardG } } -func (hs *HTTPServer) searchFolders(c *models.ReqContext) ([]*folder.Folder, error) { +func (hs *HTTPServer) searchFolders(c *contextmodel.ReqContext) ([]*folder.Folder, error) { searchQuery := search.Query{ SignedInUser: c.SignedInUser, DashboardIds: make([]int64, 0), diff --git a/pkg/api/folder_permission.go b/pkg/api/folder_permission.go index 530d0a4fe17..d7916f4bb0e 100644 --- a/pkg/api/folder_permission.go +++ b/pkg/api/folder_permission.go @@ -8,7 +8,7 @@ import ( "github.com/grafana/grafana/pkg/api/apierrors" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/guardian" @@ -26,7 +26,7 @@ import ( // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetFolderPermissionList(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetFolderPermissionList(c *contextmodel.ReqContext) response.Response { uid := web.Params(c.Req)[":uid"] folder, err := hs.folderService.Get(c.Req.Context(), &folder.GetFolderQuery{OrgID: c.OrgID, UID: &uid, SignedInUser: c.SignedInUser}) @@ -83,7 +83,7 @@ func (hs *HTTPServer) GetFolderPermissionList(c *models.ReqContext) response.Res // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) UpdateFolderPermissions(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateFolderPermissions(c *contextmodel.ReqContext) response.Response { apiCmd := dtos.UpdateDashboardACLCommand{} if err := web.Bind(c.Req, &apiCmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) diff --git a/pkg/api/folder_permission_test.go b/pkg/api/folder_permission_test.go index 9f30eb92f24..03eeed7abe8 100644 --- a/pkg/api/folder_permission_test.go +++ b/pkg/api/folder_permission_test.go @@ -13,8 +13,8 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/db/dbtest" - "github.com/grafana/grafana/pkg/models" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" service "github.com/grafana/grafana/pkg/services/dashboards/service" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -361,7 +361,7 @@ func updateFolderPermissionScenario(t *testing.T, ctx updatePermissionContext, h t.Run(fmt.Sprintf("%s %s", ctx.desc, ctx.url), func(t *testing.T) { sc := setupScenarioContext(t, ctx.url) - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(ctx.cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index 3a8f201560f..8e7034c0844 100644 --- a/pkg/api/folder_test.go +++ b/pkg/api/folder_test.go @@ -18,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" @@ -256,7 +257,7 @@ func createFolderScenario(t *testing.T, desc string, url string, routePattern st } sc := setupScenarioContext(t, url) - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c @@ -286,7 +287,7 @@ func updateFolderScenario(t *testing.T, desc string, url string, routePattern st } sc := setupScenarioContext(t, url) - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c diff --git a/pkg/api/frontend_logging_test.go b/pkg/api/frontend_logging_test.go index 81f64a50223..563ae9ecd7a 100644 --- a/pkg/api/frontend_logging_test.go +++ b/pkg/api/frontend_logging_test.go @@ -15,8 +15,8 @@ import ( "github.com/grafana/grafana/pkg/api/frontendlogging" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" @@ -89,7 +89,7 @@ func logSentryEventScenario(t *testing.T, desc string, event frontendlogging.Fro loggingHandler := NewFrontendLogMessageHandler(sourceMapStore) - handler := routing.Wrap(func(c *models.ReqContext) response.Response { + handler := routing.Wrap(func(c *contextmodel.ReqContext) response.Response { sc.context = c c.Req.Body = mockRequestBody(event) c.Req.Header.Add("Content-Type", "application/json") @@ -162,7 +162,7 @@ func logGrafanaJavascriptAgentEventScenario(t *testing.T, desc string, event fro loggingHandler := GrafanaJavascriptAgentLogMessageHandler(sourceMapStore) - handler := routing.Wrap(func(c *models.ReqContext) response.Response { + handler := routing.Wrap(func(c *contextmodel.ReqContext) response.Response { sc.context = c c.Req.Body = mockRequestBody(event) c.Req.Header.Add("Content-Type", "application/json") diff --git a/pkg/api/frontend_metrics.go b/pkg/api/frontend_metrics.go index 6c6901786d2..bb5f35e0ce3 100644 --- a/pkg/api/frontend_metrics.go +++ b/pkg/api/frontend_metrics.go @@ -5,11 +5,11 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/metrics" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/web" ) -func (hs *HTTPServer) PostFrontendMetrics(c *models.ReqContext) response.Response { +func (hs *HTTPServer) PostFrontendMetrics(c *contextmodel.ReqContext) response.Response { cmd := metrics.PostFrontendMetricsCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 3c65148c382..a92d2ff25a8 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -5,9 +5,9 @@ import ( "net/http" "strconv" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/licensing" @@ -18,7 +18,7 @@ import ( "github.com/grafana/grafana/pkg/util" ) -func (hs *HTTPServer) GetFrontendSettings(c *models.ReqContext) { +func (hs *HTTPServer) GetFrontendSettings(c *contextmodel.ReqContext) { settings, err := hs.getFrontendSettingsMap(c) if err != nil { c.JsonApiErr(400, "Failed to get frontend settings", err) @@ -29,7 +29,7 @@ func (hs *HTTPServer) GetFrontendSettings(c *models.ReqContext) { } // getFrontendSettingsMap returns a json object with all the settings needed for front end initialisation. -func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]interface{}, error) { +func (hs *HTTPServer) getFrontendSettingsMap(c *contextmodel.ReqContext) (map[string]interface{}, error) { enabledPlugins, err := hs.enabledPlugins(c.Req.Context(), c.OrgID) if err != nil { return nil, err @@ -231,7 +231,7 @@ func isSupportBundlesEnabled(hs *HTTPServer) bool { hs.Features.IsEnabled(featuremgmt.FlagSupportBundles) } -func (hs *HTTPServer) getFSDataSources(c *models.ReqContext, enabledPlugins EnabledPlugins) (map[string]plugins.DataSourceDTO, error) { +func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, enabledPlugins EnabledPlugins) (map[string]plugins.DataSourceDTO, error) { orgDataSources := make([]*datasources.DataSource, 0) if c.OrgID != 0 { query := datasources.GetDataSourcesQuery{OrgId: c.OrgID, DataSourceLimit: hs.Cfg.DataSourceLimit} diff --git a/pkg/api/grafana_com_proxy.go b/pkg/api/grafana_com_proxy.go index ca9cf6a71bb..a00a0d4a80d 100644 --- a/pkg/api/grafana_com_proxy.go +++ b/pkg/api/grafana_com_proxy.go @@ -8,7 +8,7 @@ import ( "time" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util/proxyutil" "github.com/grafana/grafana/pkg/web" @@ -45,7 +45,7 @@ func ReverseProxyGnetReq(logger log.Logger, proxyPath string, version string, gr return proxyutil.NewReverseProxy(logger, director) } -func (hs *HTTPServer) ProxyGnetRequest(c *models.ReqContext) { +func (hs *HTTPServer) ProxyGnetRequest(c *contextmodel.ReqContext) { proxyPath := web.Params(c.Req)["*"] proxy := ReverseProxyGnetReq(c.Logger, proxyPath, hs.Cfg.BuildVersion, hs.Cfg.GrafanaComAPIURL) proxy.Transport = grafanaComProxyTransport diff --git a/pkg/api/index.go b/pkg/api/index.go index 7037121e86b..a8b5888c2b6 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -6,8 +6,8 @@ import ( "strings" "github.com/grafana/grafana/pkg/api/dtos" - "github.com/grafana/grafana/pkg/models" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" @@ -21,7 +21,7 @@ const ( darkName = "dark" ) -func (hs *HTTPServer) editorInAnyFolder(c *models.ReqContext) bool { +func (hs *HTTPServer) editorInAnyFolder(c *contextmodel.ReqContext) bool { hasEditPermissionInFoldersQuery := folder.HasEditPermissionInFoldersQuery{SignedInUser: c.SignedInUser} hasEditPermissionInFoldersQueryResult, err := hs.DashboardService.HasEditPermissionInFolders(c.Req.Context(), &hasEditPermissionInFoldersQuery) if err != nil { @@ -30,7 +30,7 @@ func (hs *HTTPServer) editorInAnyFolder(c *models.ReqContext) bool { return hasEditPermissionInFoldersQueryResult } -func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewData, error) { +func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexViewData, error) { hasAccess := ac.HasAccess(hs.AccessControl, c) hasEditPerm := hasAccess(hs.editorInAnyFolder, ac.EvalAny(ac.EvalPermission(dashboards.ActionDashboardsCreate), ac.EvalPermission(dashboards.ActionFoldersCreate))) @@ -167,7 +167,7 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat return &data, nil } -func (hs *HTTPServer) Index(c *models.ReqContext) { +func (hs *HTTPServer) Index(c *contextmodel.ReqContext) { data, err := hs.setIndexViewData(c) if err != nil { c.Handle(hs.Cfg, 500, "Failed to get settings", err) @@ -176,7 +176,7 @@ func (hs *HTTPServer) Index(c *models.ReqContext) { c.HTML(http.StatusOK, "index", data) } -func (hs *HTTPServer) NotFoundHandler(c *models.ReqContext) { +func (hs *HTTPServer) NotFoundHandler(c *contextmodel.ReqContext) { if c.IsApiRequest() { c.JsonApiErr(404, "Not found", nil) return diff --git a/pkg/api/ldap_debug.go b/pkg/api/ldap_debug.go index 875e838f8f8..501b303ca84 100644 --- a/pkg/api/ldap_debug.go +++ b/pkg/api/ldap_debug.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/multildap" @@ -117,7 +118,7 @@ func (user *LDAPUserDTO) FetchOrgs(ctx context.Context, orga org.Service) error // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) ReloadLDAPCfg(c *models.ReqContext) response.Response { +func (hs *HTTPServer) ReloadLDAPCfg(c *contextmodel.ReqContext) response.Response { if !ldap.IsEnabled() { return response.Error(http.StatusBadRequest, "LDAP is not enabled", nil) } @@ -143,7 +144,7 @@ func (hs *HTTPServer) ReloadLDAPCfg(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetLDAPStatus(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetLDAPStatus(c *contextmodel.ReqContext) response.Response { if !ldap.IsEnabled() { return response.Error(http.StatusBadRequest, "LDAP is not enabled", nil) } @@ -196,7 +197,7 @@ func (hs *HTTPServer) GetLDAPStatus(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) PostSyncUserWithLDAP(c *models.ReqContext) response.Response { +func (hs *HTTPServer) PostSyncUserWithLDAP(c *contextmodel.ReqContext) response.Response { if !ldap.IsEnabled() { return response.Error(http.StatusBadRequest, "LDAP is not enabled", nil) } @@ -292,7 +293,7 @@ func (hs *HTTPServer) PostSyncUserWithLDAP(c *models.ReqContext) response.Respon // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetUserFromLDAP(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetUserFromLDAP(c *contextmodel.ReqContext) response.Response { if !ldap.IsEnabled() { return response.Error(http.StatusBadRequest, "LDAP is not enabled", nil) } diff --git a/pkg/api/ldap_debug_test.go b/pkg/api/ldap_debug_test.go index e95a4bbeb5c..cf969dc9822 100644 --- a/pkg/api/ldap_debug_test.go +++ b/pkg/api/ldap_debug_test.go @@ -17,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/auth/authtest" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/login/loginservice" "github.com/grafana/grafana/pkg/services/login/logintest" @@ -71,7 +72,7 @@ func getUserFromLDAPContext(t *testing.T, requestURL string, searchOrgRst []*org hs := &HTTPServer{Cfg: setting.NewCfg(), ldapGroups: ldap.ProvideGroupsService(), orgService: &orgtest.FakeOrgService{ExpectedOrgs: searchOrgRst}} - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { sc.context = c return hs.GetUserFromLDAP(c) }) @@ -318,7 +319,7 @@ func getLDAPStatusContext(t *testing.T) *scenarioContext { hs := &HTTPServer{Cfg: setting.NewCfg()} - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { sc.context = c return hs.GetLDAPStatus(c) }) @@ -386,7 +387,7 @@ func postSyncUserWithLDAPContext(t *testing.T, requestURL string, preHook func(* userService: userService, } - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { sc.context = c return hs.PostSyncUserWithLDAP(c) }) diff --git a/pkg/api/login.go b/pkg/api/login.go index c2d81a77636..429f080339f 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -18,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/authn" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/featuremgmt" loginService "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/secrets" @@ -80,7 +81,7 @@ func (hs *HTTPServer) CookieOptionsFromCfg() cookies.CookieOptions { } } -func (hs *HTTPServer) LoginView(c *models.ReqContext) { +func (hs *HTTPServer) LoginView(c *contextmodel.ReqContext) { viewData, err := setIndexViewData(hs, c) if err != nil { c.Handle(hs.Cfg, 500, "Failed to get settings", err) @@ -139,7 +140,7 @@ func (hs *HTTPServer) LoginView(c *models.ReqContext) { c.HTML(http.StatusOK, getViewIndex(), viewData) } -func (hs *HTTPServer) tryAutoLogin(c *models.ReqContext) bool { +func (hs *HTTPServer) tryAutoLogin(c *contextmodel.ReqContext) bool { samlAutoLogin := hs.samlAutoLoginEnabled() oauthInfos := hs.SocialService.GetOAuthInfoProviders() @@ -185,7 +186,7 @@ func (hs *HTTPServer) tryAutoLogin(c *models.ReqContext) bool { return false } -func (hs *HTTPServer) LoginAPIPing(c *models.ReqContext) response.Response { +func (hs *HTTPServer) LoginAPIPing(c *contextmodel.ReqContext) response.Response { if c.IsSignedIn || c.IsAnonymous { return response.JSON(http.StatusOK, "Logged in") } @@ -193,7 +194,7 @@ func (hs *HTTPServer) LoginAPIPing(c *models.ReqContext) response.Response { return response.Error(401, "Unauthorized", nil) } -func (hs *HTTPServer) LoginPost(c *models.ReqContext) response.Response { +func (hs *HTTPServer) LoginPost(c *contextmodel.ReqContext) response.Response { if hs.Features.IsEnabled(featuremgmt.FlagAuthnService) { identity, err := hs.authnService.Login(c.Req.Context(), authn.ClientForm, &authn.Request{HTTPRequest: c.Req, Resp: c.Resp}) if err != nil { @@ -313,7 +314,7 @@ func (hs *HTTPServer) LoginPost(c *models.ReqContext) response.Response { return resp } -func (hs *HTTPServer) loginUserWithUser(user *user.User, c *models.ReqContext) error { +func (hs *HTTPServer) loginUserWithUser(user *user.User, c *contextmodel.ReqContext) error { if user == nil { return errors.New("could not login user") } @@ -338,7 +339,7 @@ func (hs *HTTPServer) loginUserWithUser(user *user.User, c *models.ReqContext) e return nil } -func (hs *HTTPServer) Logout(c *models.ReqContext) { +func (hs *HTTPServer) Logout(c *contextmodel.ReqContext) { // If SAML is enabled and this is a SAML user use saml logout if hs.samlSingleLogoutEnabled() { getAuthQuery := models.GetAuthInfoQuery{UserId: c.UserID} @@ -372,7 +373,7 @@ func (hs *HTTPServer) Logout(c *models.ReqContext) { } } -func (hs *HTTPServer) tryGetEncryptedCookie(ctx *models.ReqContext, cookieName string) (string, bool) { +func (hs *HTTPServer) tryGetEncryptedCookie(ctx *contextmodel.ReqContext, cookieName string) (string, bool) { cookie := ctx.GetCookie(cookieName) if cookie == "" { return "", false @@ -387,7 +388,7 @@ func (hs *HTTPServer) tryGetEncryptedCookie(ctx *models.ReqContext, cookieName s return string(decryptedError), err == nil } -func (hs *HTTPServer) trySetEncryptedCookie(ctx *models.ReqContext, cookieName string, value string, maxAge int) error { +func (hs *HTTPServer) trySetEncryptedCookie(ctx *contextmodel.ReqContext, cookieName string, value string, maxAge int) error { encryptedError, err := hs.SecretsService.Encrypt(ctx.Req.Context(), []byte(value), secrets.WithoutScope()) if err != nil { return err @@ -398,7 +399,7 @@ func (hs *HTTPServer) trySetEncryptedCookie(ctx *models.ReqContext, cookieName s return nil } -func (hs *HTTPServer) redirectWithError(ctx *models.ReqContext, err error, v ...interface{}) { +func (hs *HTTPServer) redirectWithError(ctx *contextmodel.ReqContext, err error, v ...interface{}) { ctx.Logger.Warn(err.Error(), v...) if err := hs.trySetEncryptedCookie(ctx, loginErrorCookieName, getLoginExternalError(err), 60); err != nil { hs.log.Error("Failed to set encrypted cookie", "err", err) @@ -407,7 +408,7 @@ func (hs *HTTPServer) redirectWithError(ctx *models.ReqContext, err error, v ... ctx.Redirect(hs.Cfg.AppSubURL + "/login") } -func (hs *HTTPServer) RedirectResponseWithError(ctx *models.ReqContext, err error, v ...interface{}) *response.RedirectResponse { +func (hs *HTTPServer) RedirectResponseWithError(ctx *contextmodel.ReqContext, err error, v ...interface{}) *response.RedirectResponse { ctx.Logger.Error(err.Error(), v...) if err := hs.trySetEncryptedCookie(ctx, loginErrorCookieName, getLoginExternalError(err), 60); err != nil { hs.log.Error("Failed to set encrypted cookie", "err", err) diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index 84bb08d7fe4..35fe2b156a5 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -18,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/middleware/cookies" "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -68,7 +69,7 @@ func genPKCECode() (string, string, error) { return string(ascii), pkce, nil } -func (hs *HTTPServer) OAuthLogin(ctx *models.ReqContext) { +func (hs *HTTPServer) OAuthLogin(ctx *contextmodel.ReqContext) { loginInfo := models.LoginInfo{ AuthModule: "oauth", } @@ -308,7 +309,7 @@ func (hs *HTTPServer) buildExternalUserInfo(token *oauth2.Token, userInfo *socia // SyncUser syncs a Grafana user profile with the corresponding OAuth profile. func (hs *HTTPServer) SyncUser( - ctx *models.ReqContext, + ctx *contextmodel.ReqContext, extUser *models.ExternalUserInfo, connect social.SocialConnector, ) (*user.User, error) { @@ -350,7 +351,7 @@ type LoginError struct { Err error } -func (hs *HTTPServer) handleOAuthLoginError(ctx *models.ReqContext, info models.LoginInfo, err LoginError) { +func (hs *HTTPServer) handleOAuthLoginError(ctx *contextmodel.ReqContext, info models.LoginInfo, err LoginError) { ctx.Handle(hs.Cfg, err.HttpStatus, err.PublicMessage, err.Err) info.Error = err.Err @@ -362,7 +363,7 @@ func (hs *HTTPServer) handleOAuthLoginError(ctx *models.ReqContext, info models. hs.HooksService.RunLoginHook(&info, ctx) } -func (hs *HTTPServer) handleOAuthLoginErrorWithRedirect(ctx *models.ReqContext, info models.LoginInfo, err error, v ...interface{}) { +func (hs *HTTPServer) handleOAuthLoginErrorWithRedirect(ctx *contextmodel.ReqContext, info models.LoginInfo, err error, v ...interface{}) { hs.redirectWithError(ctx, err, v...) info.Error = err diff --git a/pkg/api/login_test.go b/pkg/api/login_test.go index 9e09ba3deaa..45fd7cef0ea 100644 --- a/pkg/api/login_test.go +++ b/pkg/api/login_test.go @@ -21,6 +21,7 @@ import ( "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth/authtest" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/hooks" "github.com/grafana/grafana/pkg/services/licensing" @@ -40,7 +41,7 @@ func fakeSetIndexViewData(t *testing.T) { t.Cleanup(func() { setIndexViewData = origSetIndexViewData }) - setIndexViewData = func(*HTTPServer, *models.ReqContext) (*dtos.IndexViewData, error) { + setIndexViewData = func(*HTTPServer, *contextmodel.ReqContext) (*dtos.IndexViewData, error) { data := &dtos.IndexViewData{ User: &dtos.CurrentUser{}, Settings: map[string]interface{}{}, @@ -104,7 +105,7 @@ func TestLoginErrorCookieAPIEndpoint(t *testing.T) { SecretsService: secretsService, } - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { hs.LoginView(c) return response.Empty(http.StatusOK) }) @@ -152,7 +153,7 @@ func TestLoginViewRedirect(t *testing.T) { } hs.Cfg.CookieSecure = true - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.IsSignedIn = true c.SignedInUser = &user.SignedInUser{ UserID: 10, @@ -328,7 +329,7 @@ func TestLoginPostRedirect(t *testing.T) { } hs.Cfg.CookieSecure = true - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Header.Set("Content-Type", "application/json") c.Req.Body = io.NopCloser(bytes.NewBufferString(`{"user":"admin","password":"admin"}`)) return hs.LoginPost(c) @@ -492,7 +493,7 @@ func TestLoginOAuthRedirect(t *testing.T) { SocialService: mock, } - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { hs.LoginView(c) return response.Empty(http.StatusOK) }) @@ -518,7 +519,7 @@ func TestLoginInternal(t *testing.T) { log: log.New("test"), } - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.URL.RawQuery = "disableAutoLogin=true" hs.LoginView(c) return response.Empty(http.StatusOK) @@ -570,7 +571,7 @@ func setupAuthProxyLoginTest(t *testing.T, enableLoginToken bool) *scenarioConte SocialService: &mockSocialService{}, } - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.IsSignedIn = true c.SignedInUser = &user.SignedInUser{ UserID: 10, @@ -592,7 +593,7 @@ type loginHookTest struct { info *models.LoginInfo } -func (r *loginHookTest) LoginHook(loginInfo *models.LoginInfo, req *models.ReqContext) { +func (r *loginHookTest) LoginHook(loginInfo *models.LoginInfo, req *contextmodel.ReqContext) { r.info = loginInfo } @@ -608,7 +609,7 @@ func TestLoginPostRunLokingHook(t *testing.T) { HooksService: hookService, } - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Header.Set("Content-Type", "application/json") c.Req.Body = io.NopCloser(bytes.NewBufferString(`{"user":"admin","password":"admin"}`)) x := hs.LoginPost(c) diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go index 6f74a46a3f9..695a28e21de 100644 --- a/pkg/api/metrics.go +++ b/pkg/api/metrics.go @@ -9,7 +9,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/web" @@ -46,7 +46,7 @@ func (hs *HTTPServer) handleQueryMetricsError(err error) *response.NormalRespons // 400: badRequestError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) QueryMetricsV2(c *models.ReqContext) response.Response { +func (hs *HTTPServer) QueryMetricsV2(c *contextmodel.ReqContext) response.Response { reqDTO := dtos.MetricRequest{} if err := web.Bind(c.Req, &reqDTO); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) diff --git a/pkg/api/openapi3.go b/pkg/api/openapi3.go index ad01675cf08..a5f3eb483a4 100644 --- a/pkg/api/openapi3.go +++ b/pkg/api/openapi3.go @@ -3,9 +3,9 @@ package api import ( "net/http" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" ) -func openapi3(c *models.ReqContext) { +func openapi3(c *contextmodel.ReqContext) { c.HTML(http.StatusOK, "openapi3", nil) } diff --git a/pkg/api/org.go b/pkg/api/org.go index 9030412153f..c4c2ea7a5d7 100644 --- a/pkg/api/org.go +++ b/pkg/api/org.go @@ -9,7 +9,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/metrics" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" @@ -25,7 +25,7 @@ import ( // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetCurrentOrg(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetCurrentOrg(c *contextmodel.ReqContext) response.Response { return hs.getOrgHelper(c.Req.Context(), c.OrgID) } @@ -41,7 +41,7 @@ func (hs *HTTPServer) GetCurrentOrg(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetOrgByID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetOrgByID(c *contextmodel.ReqContext) response.Response { orgId, err := strconv.ParseInt(web.Params(c.Req)[":orgId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "orgId is invalid", err) @@ -61,7 +61,7 @@ func (hs *HTTPServer) GetOrgByID(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetOrgByName(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetOrgByName(c *contextmodel.ReqContext) response.Response { orga, err := hs.orgService.GetByName(c.Req.Context(), &org.GetOrgByNameQuery{Name: web.Params(c.Req)[":name"]}) if err != nil { if errors.Is(err, org.ErrOrgNotFound) { @@ -126,7 +126,7 @@ func (hs *HTTPServer) getOrgHelper(ctx context.Context, orgID int64) response.Re // 403: forbiddenError // 409: conflictError // 500: internalServerError -func (hs *HTTPServer) CreateOrg(c *models.ReqContext) response.Response { +func (hs *HTTPServer) CreateOrg(c *contextmodel.ReqContext) response.Response { cmd := org.CreateOrgCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -163,7 +163,7 @@ func (hs *HTTPServer) CreateOrg(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) UpdateCurrentOrg(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateCurrentOrg(c *contextmodel.ReqContext) response.Response { form := dtos.UpdateOrgForm{} if err := web.Bind(c.Req, &form); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -184,7 +184,7 @@ func (hs *HTTPServer) UpdateCurrentOrg(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) UpdateOrg(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateOrg(c *contextmodel.ReqContext) response.Response { form := dtos.UpdateOrgForm{} if err := web.Bind(c.Req, &form); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -218,7 +218,7 @@ func (hs *HTTPServer) updateOrgHelper(ctx context.Context, form dtos.UpdateOrgFo // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) UpdateCurrentOrgAddress(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateCurrentOrgAddress(c *contextmodel.ReqContext) response.Response { form := dtos.UpdateOrgAddressForm{} if err := web.Bind(c.Req, &form); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -236,7 +236,7 @@ func (hs *HTTPServer) UpdateCurrentOrgAddress(c *models.ReqContext) response.Res // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) UpdateOrgAddress(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateOrgAddress(c *contextmodel.ReqContext) response.Response { form := dtos.UpdateOrgAddressForm{} if err := web.Bind(c.Req, &form); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -282,7 +282,7 @@ func (hs *HTTPServer) updateOrgAddressHelper(ctx context.Context, form dtos.Upda // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) DeleteOrgByID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) DeleteOrgByID(c *contextmodel.ReqContext) response.Response { orgID, err := strconv.ParseInt(web.Params(c.Req)[":orgId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "orgId is invalid", err) @@ -314,7 +314,7 @@ func (hs *HTTPServer) DeleteOrgByID(c *models.ReqContext) response.Response { // 403: forbiddenError // 409: conflictError // 500: internalServerError -func (hs *HTTPServer) SearchOrgs(c *models.ReqContext) response.Response { +func (hs *HTTPServer) SearchOrgs(c *contextmodel.ReqContext) response.Response { perPage := c.QueryInt("perpage") if perPage <= 0 { perPage = 1000 diff --git a/pkg/api/org_invite.go b/pkg/api/org_invite.go index 30271ed2f55..855c26c2922 100644 --- a/pkg/api/org_invite.go +++ b/pkg/api/org_invite.go @@ -12,8 +12,8 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/events" "github.com/grafana/grafana/pkg/infra/metrics" - "github.com/grafana/grafana/pkg/models" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/org" tempuser "github.com/grafana/grafana/pkg/services/temp_user" @@ -32,7 +32,7 @@ import ( // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetPendingOrgInvites(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetPendingOrgInvites(c *contextmodel.ReqContext) response.Response { query := tempuser.GetTempUsersQuery{OrgID: c.OrgID, Status: tempuser.TmpUserInvitePending} queryResult, err := hs.tempUserService.GetTempUsersQuery(c.Req.Context(), &query) @@ -58,7 +58,7 @@ func (hs *HTTPServer) GetPendingOrgInvites(c *models.ReqContext) response.Respon // 403: forbiddenError // 412: SMTPNotEnabledError // 500: internalServerError -func (hs *HTTPServer) AddOrgInvite(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AddOrgInvite(c *contextmodel.ReqContext) response.Response { inviteDto := dtos.AddInviteForm{} if err := web.Bind(c.Req, &inviteDto); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -145,7 +145,7 @@ func (hs *HTTPServer) AddOrgInvite(c *models.ReqContext) response.Response { return response.Success(fmt.Sprintf("Created invite for %s", inviteDto.LoginOrEmail)) } -func (hs *HTTPServer) inviteExistingUserToOrg(c *models.ReqContext, user *user.User, inviteDto *dtos.AddInviteForm) response.Response { +func (hs *HTTPServer) inviteExistingUserToOrg(c *contextmodel.ReqContext, user *user.User, inviteDto *dtos.AddInviteForm) response.Response { // user exists, add org role createOrgUserCmd := org.AddOrgUserCommand{OrgID: c.OrgID, UserID: user.ID, Role: inviteDto.Role} if err := hs.orgService.AddOrgUser(c.Req.Context(), &createOrgUserCmd); err != nil { @@ -187,7 +187,7 @@ func (hs *HTTPServer) inviteExistingUserToOrg(c *models.ReqContext, user *user.U // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) RevokeInvite(c *models.ReqContext) response.Response { +func (hs *HTTPServer) RevokeInvite(c *contextmodel.ReqContext) response.Response { if ok, rsp := hs.updateTempUserStatus(c.Req.Context(), web.Params(c.Req)[":code"], tempuser.TmpUserRevoked); !ok { return rsp } @@ -198,7 +198,7 @@ func (hs *HTTPServer) RevokeInvite(c *models.ReqContext) response.Response { // GetInviteInfoByCode gets a pending user invite corresponding to a certain code. // A response containing an InviteInfo object is returned if the invite is found. // If a (pending) invite is not found, 404 is returned. -func (hs *HTTPServer) GetInviteInfoByCode(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetInviteInfoByCode(c *contextmodel.ReqContext) response.Response { query := tempuser.GetTempUserByCodeQuery{Code: web.Params(c.Req)[":code"]} queryResult, err := hs.tempUserService.GetTempUserByCode(c.Req.Context(), &query) if err != nil { @@ -221,7 +221,7 @@ func (hs *HTTPServer) GetInviteInfoByCode(c *models.ReqContext) response.Respons }) } -func (hs *HTTPServer) CompleteInvite(c *models.ReqContext) response.Response { +func (hs *HTTPServer) CompleteInvite(c *contextmodel.ReqContext) response.Response { completeInvite := dtos.CompleteInviteForm{} var err error if err = web.Bind(c.Req, &completeInvite); err != nil { diff --git a/pkg/api/org_users.go b/pkg/api/org_users.go index a3151f2936b..264c833d77b 100644 --- a/pkg/api/org_users.go +++ b/pkg/api/org_users.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" @@ -32,7 +33,7 @@ import ( // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) AddOrgUserToCurrentOrg(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AddOrgUserToCurrentOrg(c *contextmodel.ReqContext) response.Response { cmd := org.AddOrgUserCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -55,7 +56,7 @@ func (hs *HTTPServer) AddOrgUserToCurrentOrg(c *models.ReqContext) response.Resp // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) AddOrgUser(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AddOrgUser(c *contextmodel.ReqContext) response.Response { cmd := org.AddOrgUserCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -69,7 +70,7 @@ func (hs *HTTPServer) AddOrgUser(c *models.ReqContext) response.Response { return hs.addOrgUserHelper(c, cmd) } -func (hs *HTTPServer) addOrgUserHelper(c *models.ReqContext, cmd org.AddOrgUserCommand) response.Response { +func (hs *HTTPServer) addOrgUserHelper(c *contextmodel.ReqContext, cmd org.AddOrgUserCommand) response.Response { if !cmd.Role.IsValid() { return response.Error(400, "Invalid role specified", nil) } @@ -114,7 +115,7 @@ func (hs *HTTPServer) addOrgUserHelper(c *models.ReqContext, cmd org.AddOrgUserC // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetOrgUsersForCurrentOrg(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetOrgUsersForCurrentOrg(c *contextmodel.ReqContext) response.Response { result, err := hs.searchOrgUsersHelper(c, &org.SearchOrgUsersQuery{ OrgID: c.OrgID, Query: c.Query("query"), @@ -143,7 +144,7 @@ func (hs *HTTPServer) GetOrgUsersForCurrentOrg(c *models.ReqContext) response.Re // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetOrgUsersForCurrentOrgLookup(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetOrgUsersForCurrentOrgLookup(c *contextmodel.ReqContext) response.Response { orgUsersResult, err := hs.searchOrgUsersHelper(c, &org.SearchOrgUsersQuery{ OrgID: c.OrgID, Query: c.Query("query"), @@ -184,7 +185,7 @@ func (hs *HTTPServer) GetOrgUsersForCurrentOrgLookup(c *models.ReqContext) respo // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetOrgUsers(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetOrgUsers(c *contextmodel.ReqContext) response.Response { orgId, err := strconv.ParseInt(web.Params(c.Req)[":orgId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "orgId is invalid", err) @@ -219,7 +220,7 @@ func (hs *HTTPServer) GetOrgUsers(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) SearchOrgUsers(c *models.ReqContext) response.Response { +func (hs *HTTPServer) SearchOrgUsers(c *contextmodel.ReqContext) response.Response { orgID, err := strconv.ParseInt(web.Params(c.Req)[":orgId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "orgId is invalid", err) @@ -252,7 +253,7 @@ func (hs *HTTPServer) SearchOrgUsers(c *models.ReqContext) response.Response { // SearchOrgUsersWithPaging is an HTTP handler to search for org users with paging. // GET /api/org/users/search -func (hs *HTTPServer) SearchOrgUsersWithPaging(c *models.ReqContext) response.Response { +func (hs *HTTPServer) SearchOrgUsersWithPaging(c *contextmodel.ReqContext) response.Response { perPage := c.QueryInt("perpage") if perPage <= 0 { perPage = 1000 @@ -279,7 +280,7 @@ func (hs *HTTPServer) SearchOrgUsersWithPaging(c *models.ReqContext) response.Re return response.JSON(http.StatusOK, result) } -func (hs *HTTPServer) searchOrgUsersHelper(c *models.ReqContext, query *org.SearchOrgUsersQuery) (*org.SearchOrgUsersQueryResult, error) { +func (hs *HTTPServer) searchOrgUsersHelper(c *contextmodel.ReqContext, query *org.SearchOrgUsersQuery) (*org.SearchOrgUsersQueryResult, error) { result, err := hs.orgService.SearchOrgUsers(c.Req.Context(), query) if err != nil { return nil, err @@ -335,7 +336,7 @@ func (hs *HTTPServer) searchOrgUsersHelper(c *models.ReqContext, query *org.Sear // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) UpdateOrgUserForCurrentOrg(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateOrgUserForCurrentOrg(c *contextmodel.ReqContext) response.Response { cmd := org.UpdateOrgUserCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -362,7 +363,7 @@ func (hs *HTTPServer) UpdateOrgUserForCurrentOrg(c *models.ReqContext) response. // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) UpdateOrgUser(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateOrgUser(c *contextmodel.ReqContext) response.Response { cmd := org.UpdateOrgUserCommand{} var err error if err := web.Bind(c.Req, &cmd); err != nil { @@ -379,7 +380,7 @@ func (hs *HTTPServer) UpdateOrgUser(c *models.ReqContext) response.Response { return hs.updateOrgUserHelper(c, cmd) } -func (hs *HTTPServer) updateOrgUserHelper(c *models.ReqContext, cmd org.UpdateOrgUserCommand) response.Response { +func (hs *HTTPServer) updateOrgUserHelper(c *contextmodel.ReqContext, cmd org.UpdateOrgUserCommand) response.Response { if !cmd.Role.IsValid() { return response.Error(400, "Invalid role specified", nil) } @@ -409,7 +410,7 @@ func (hs *HTTPServer) updateOrgUserHelper(c *models.ReqContext, cmd org.UpdateOr // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) RemoveOrgUserForCurrentOrg(c *models.ReqContext) response.Response { +func (hs *HTTPServer) RemoveOrgUserForCurrentOrg(c *contextmodel.ReqContext) response.Response { userId, err := strconv.ParseInt(web.Params(c.Req)[":userId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "userId is invalid", err) @@ -435,7 +436,7 @@ func (hs *HTTPServer) RemoveOrgUserForCurrentOrg(c *models.ReqContext) response. // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) RemoveOrgUser(c *models.ReqContext) response.Response { +func (hs *HTTPServer) RemoveOrgUser(c *contextmodel.ReqContext) response.Response { userId, err := strconv.ParseInt(web.Params(c.Req)[":userId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "userId is invalid", err) diff --git a/pkg/api/password.go b/pkg/api/password.go index 5d60de08254..585c11f2909 100644 --- a/pkg/api/password.go +++ b/pkg/api/password.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/user" @@ -16,7 +17,7 @@ import ( "github.com/grafana/grafana/pkg/web" ) -func (hs *HTTPServer) SendResetPasswordEmail(c *models.ReqContext) response.Response { +func (hs *HTTPServer) SendResetPasswordEmail(c *contextmodel.ReqContext) response.Response { form := dtos.SendResetPasswordEmailForm{} if err := web.Bind(c.Req, &form); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -54,7 +55,7 @@ func (hs *HTTPServer) SendResetPasswordEmail(c *models.ReqContext) response.Resp return response.Success("Email sent") } -func (hs *HTTPServer) ResetPassword(c *models.ReqContext) response.Response { +func (hs *HTTPServer) ResetPassword(c *contextmodel.ReqContext) response.Response { form := dtos.ResetUserPasswordForm{} if err := web.Bind(c.Req, &form); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) diff --git a/pkg/api/playlist.go b/pkg/api/playlist.go index e37f4d5c577..b9303bff7b3 100644 --- a/pkg/api/playlist.go +++ b/pkg/api/playlist.go @@ -5,12 +5,12 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/playlist" "github.com/grafana/grafana/pkg/web" ) -func (hs *HTTPServer) ValidateOrgPlaylist(c *models.ReqContext) { +func (hs *HTTPServer) ValidateOrgPlaylist(c *contextmodel.ReqContext) { uid := web.Params(c.Req)[":uid"] query := playlist.GetPlaylistByUidQuery{UID: uid, OrgId: c.OrgID} p, err := hs.playlistService.GetWithoutItems(c.Req.Context(), &query) @@ -38,7 +38,7 @@ func (hs *HTTPServer) ValidateOrgPlaylist(c *models.ReqContext) { // Responses: // 200: searchPlaylistsResponse // 500: internalServerError -func (hs *HTTPServer) SearchPlaylists(c *models.ReqContext) response.Response { +func (hs *HTTPServer) SearchPlaylists(c *contextmodel.ReqContext) response.Response { query := c.Query("query") limit := c.QueryInt("limit") @@ -70,7 +70,7 @@ func (hs *HTTPServer) SearchPlaylists(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetPlaylist(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetPlaylist(c *contextmodel.ReqContext) response.Response { uid := web.Params(c.Req)[":uid"] cmd := playlist.GetPlaylistByUidQuery{UID: uid, OrgId: c.OrgID} @@ -92,7 +92,7 @@ func (hs *HTTPServer) GetPlaylist(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetPlaylistItems(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetPlaylistItems(c *contextmodel.ReqContext) response.Response { uid := web.Params(c.Req)[":uid"] cmd := playlist.GetPlaylistByUidQuery{UID: uid, OrgId: c.OrgID} @@ -114,7 +114,7 @@ func (hs *HTTPServer) GetPlaylistItems(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetPlaylistDashboards(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetPlaylistDashboards(c *contextmodel.ReqContext) response.Response { playlistUID := web.Params(c.Req)[":uid"] playlists, err := hs.LoadPlaylistDashboards(c.Req.Context(), c.OrgID, c.SignedInUser, playlistUID) @@ -135,7 +135,7 @@ func (hs *HTTPServer) GetPlaylistDashboards(c *models.ReqContext) response.Respo // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) DeletePlaylist(c *models.ReqContext) response.Response { +func (hs *HTTPServer) DeletePlaylist(c *contextmodel.ReqContext) response.Response { uid := web.Params(c.Req)[":uid"] cmd := playlist.DeletePlaylistCommand{UID: uid, OrgId: c.OrgID} @@ -156,7 +156,7 @@ func (hs *HTTPServer) DeletePlaylist(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) CreatePlaylist(c *models.ReqContext) response.Response { +func (hs *HTTPServer) CreatePlaylist(c *contextmodel.ReqContext) response.Response { cmd := playlist.CreatePlaylistCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -181,7 +181,7 @@ func (hs *HTTPServer) CreatePlaylist(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) UpdatePlaylist(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdatePlaylist(c *contextmodel.ReqContext) response.Response { cmd := playlist.UpdatePlaylistCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) diff --git a/pkg/api/plugin_dashboards.go b/pkg/api/plugin_dashboards.go index 7710c41ca1e..4446b490825 100644 --- a/pkg/api/plugin_dashboards.go +++ b/pkg/api/plugin_dashboards.go @@ -5,8 +5,8 @@ import ( "net/http" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/plugindashboards" "github.com/grafana/grafana/pkg/web" ) @@ -14,7 +14,7 @@ import ( // GetPluginDashboards get plugin dashboards. // // /api/plugins/:pluginId/dashboards -func (hs *HTTPServer) GetPluginDashboards(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetPluginDashboards(c *contextmodel.ReqContext) response.Response { pluginID := web.Params(c.Req)[":pluginId"] listReq := &plugindashboards.ListPluginDashboardsRequest{ diff --git a/pkg/api/plugin_proxy.go b/pkg/api/plugin_proxy.go index 509c1f784bf..0ff09d45bb0 100644 --- a/pkg/api/plugin_proxy.go +++ b/pkg/api/plugin_proxy.go @@ -9,12 +9,12 @@ import ( "time" "github.com/grafana/grafana/pkg/api/pluginproxy" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/pluginsettings" "github.com/grafana/grafana/pkg/web" ) -func (hs *HTTPServer) ProxyPluginRequest(c *models.ReqContext) { +func (hs *HTTPServer) ProxyPluginRequest(c *contextmodel.ReqContext) { var once sync.Once var pluginProxyTransport *http.Transport once.Do(func() { @@ -63,6 +63,6 @@ func extractProxyPath(originalRawPath string) string { return pluginProxyPathRegexp.ReplaceAllString(originalRawPath, "") } -func getProxyPath(c *models.ReqContext) string { +func getProxyPath(c *contextmodel.ReqContext) string { return extractProxyPath(c.Req.URL.EscapedPath()) } diff --git a/pkg/api/plugin_resource.go b/pkg/api/plugin_resource.go index 970c4d6a4bc..a9b0ab61af0 100644 --- a/pkg/api/plugin_resource.go +++ b/pkg/api/plugin_resource.go @@ -12,8 +12,8 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins/backendplugin" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/util/proxyutil" "github.com/grafana/grafana/pkg/web" @@ -22,11 +22,11 @@ import ( // CallResource passes a resource call from a plugin to the backend plugin. // // /api/plugins/:pluginId/resources/* -func (hs *HTTPServer) CallResource(c *models.ReqContext) { +func (hs *HTTPServer) CallResource(c *contextmodel.ReqContext) { hs.callPluginResource(c, web.Params(c.Req)[":pluginId"]) } -func (hs *HTTPServer) callPluginResource(c *models.ReqContext, pluginID string) { +func (hs *HTTPServer) callPluginResource(c *contextmodel.ReqContext, pluginID string) { pCtx, found, err := hs.PluginContextProvider.Get(c.Req.Context(), pluginID, c.SignedInUser) if err != nil { c.JsonApiErr(500, "Failed to get plugin settings", err) @@ -48,7 +48,7 @@ func (hs *HTTPServer) callPluginResource(c *models.ReqContext, pluginID string) } } -func (hs *HTTPServer) callPluginResourceWithDataSource(c *models.ReqContext, pluginID string, ds *datasources.DataSource) { +func (hs *HTTPServer) callPluginResourceWithDataSource(c *contextmodel.ReqContext, pluginID string, ds *datasources.DataSource) { pCtx, found, err := hs.PluginContextProvider.GetWithDataSource(c.Req.Context(), pluginID, c.SignedInUser, ds) if err != nil { c.JsonApiErr(500, "Failed to get plugin settings", err) @@ -81,7 +81,7 @@ func (hs *HTTPServer) callPluginResourceWithDataSource(c *models.ReqContext, plu } } -func (hs *HTTPServer) pluginResourceRequest(c *models.ReqContext) (*http.Request, error) { +func (hs *HTTPServer) pluginResourceRequest(c *contextmodel.ReqContext) (*http.Request, error) { clonedReq := c.Req.Clone(c.Req.Context()) rawURL := web.Params(c.Req)["*"] if clonedReq.URL.RawQuery != "" { @@ -207,7 +207,7 @@ func (hs *HTTPServer) flushStream(stream callResourceClientResponseStream, w htt } } -func handleCallResourceError(err error, reqCtx *models.ReqContext) { +func handleCallResourceError(err error, reqCtx *contextmodel.ReqContext) { if errors.Is(err, backendplugin.ErrPluginUnavailable) { reqCtx.JsonApiErr(503, "Plugin unavailable", err) return diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go index ba450c9f056..fa9cc95fa6b 100644 --- a/pkg/api/pluginproxy/ds_proxy.go +++ b/pkg/api/pluginproxy/ds_proxy.go @@ -17,8 +17,8 @@ import ( "github.com/grafana/grafana/pkg/infra/httpclient" glog "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/oauthtoken" "github.com/grafana/grafana/pkg/setting" @@ -33,7 +33,7 @@ var ( type DataSourceProxy struct { ds *datasources.DataSource - ctx *models.ReqContext + ctx *contextmodel.ReqContext targetUrl *url.URL proxyPath string matchedRoute *plugins.Route @@ -50,7 +50,7 @@ type httpClient interface { } // NewDataSourceProxy creates a new Datasource proxy -func NewDataSourceProxy(ds *datasources.DataSource, pluginRoutes []*plugins.Route, ctx *models.ReqContext, +func NewDataSourceProxy(ds *datasources.DataSource, pluginRoutes []*plugins.Route, ctx *contextmodel.ReqContext, proxyPath string, cfg *setting.Cfg, clientProvider httpclient.Provider, oAuthTokenService oauthtoken.OAuthTokenService, dsService datasources.DataSourceService, tracer tracing.Tracer) (*DataSourceProxy, error) { @@ -343,7 +343,7 @@ func (proxy *DataSourceProxy) logRequest() { "body", body) } -func checkWhiteList(c *models.ReqContext, host string) bool { +func checkWhiteList(c *contextmodel.ReqContext, host string) bool { if host != "" && len(setting.DataProxyWhiteList) > 0 { if _, exists := setting.DataProxyWhiteList[host]; !exists { c.JsonApiErr(403, "Data proxy hostname and ip are not included in whitelist", nil) diff --git a/pkg/api/pluginproxy/ds_proxy_test.go b/pkg/api/pluginproxy/ds_proxy_test.go index a552bb0ba5f..ff085671da7 100644 --- a/pkg/api/pluginproxy/ds_proxy_test.go +++ b/pkg/api/pluginproxy/ds_proxy_test.go @@ -28,6 +28,7 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" datasourceservice "github.com/grafana/grafana/pkg/services/datasources/service" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -128,10 +129,10 @@ func TestDataSourceProxy_routeRule(t *testing.T) { }, } - setUp := func() (*models.ReqContext, *http.Request) { + setUp := func() (*contextmodel.ReqContext, *http.Request) { req, err := http.NewRequest("GET", "http://localhost/asd", nil) require.NoError(t, err) - ctx := &models.ReqContext{ + ctx := &contextmodel.ReqContext{ Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgRole: org.RoleEditor}, } @@ -286,7 +287,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { req, err := http.NewRequest("GET", "http://localhost/asd", nil) require.NoError(t, err) - ctx := &models.ReqContext{ + ctx := &contextmodel.ReqContext{ Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgRole: org.RoleEditor}, } @@ -372,7 +373,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { t.Run("When proxying graphite", func(t *testing.T) { var routes []*plugins.Route ds := &datasources.DataSource{Url: "htttp://graphite:8080", Type: datasources.DS_GRAPHITE} - ctx := &models.ReqContext{} + ctx := &contextmodel.ReqContext{} sqlStore := db.InitTestDB(t) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) @@ -401,7 +402,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { User: "user", } - ctx := &models.ReqContext{} + ctx := &contextmodel.ReqContext{} var routes []*plugins.Route sqlStore := db.InitTestDB(t) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) @@ -429,7 +430,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { JsonData: json, } - ctx := &models.ReqContext{} + ctx := &contextmodel.ReqContext{} var routes []*plugins.Route sqlStore := db.InitTestDB(t) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) @@ -461,7 +462,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { JsonData: json, } - ctx := &models.ReqContext{} + ctx := &contextmodel.ReqContext{} var pluginRoutes []*plugins.Route sqlStore := db.InitTestDB(t) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) @@ -488,7 +489,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { Type: "custom-datasource", Url: "http://host/root/", } - ctx := &models.ReqContext{} + ctx := &contextmodel.ReqContext{} var routes []*plugins.Route sqlStore := db.InitTestDB(t) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) @@ -522,7 +523,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { req, err := http.NewRequest("GET", "http://localhost/asd", nil) require.NoError(t, err) - ctx := &models.ReqContext{ + ctx := &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{UserID: 1}, Context: &web.Context{Req: req}, } @@ -563,7 +564,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { t.Run("When SendUserHeader config is enabled", func(t *testing.T) { req := getDatasourceProxiedRequest( t, - &models.ReqContext{ + &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{ Login: "test_user", }, @@ -576,7 +577,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { t.Run("When SendUserHeader config is disabled", func(t *testing.T) { req := getDatasourceProxiedRequest( t, - &models.ReqContext{ + &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{ Login: "test_user", }, @@ -590,7 +591,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { t.Run("When SendUserHeader config is enabled but user is anonymous", func(t *testing.T) { req := getDatasourceProxiedRequest( t, - &models.ReqContext{ + &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{IsAnonymous: true}, }, &setting.Cfg{SendUserHeader: true}, @@ -635,7 +636,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) { writeCb func(w http.ResponseWriter, r *http.Request) } - setUp := func(t *testing.T, cfgs ...setUpCfg) (*models.ReqContext, *datasources.DataSource) { + setUp := func(t *testing.T, cfgs ...setUpCfg) (*contextmodel.ReqContext, *datasources.DataSource) { writeErr = nil backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -668,7 +669,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) { } } - return &models.ReqContext{ + return &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{}, Context: &web.Context{ Req: httptest.NewRequest("GET", "/render", nil), @@ -822,7 +823,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) { } func TestNewDataSourceProxy_InvalidURL(t *testing.T) { - ctx := models.ReqContext{ + ctx := contextmodel.ReqContext{ Context: &web.Context{}, SignedInUser: &user.SignedInUser{OrgRole: org.RoleEditor}, } @@ -846,7 +847,7 @@ func TestNewDataSourceProxy_InvalidURL(t *testing.T) { } func TestNewDataSourceProxy_ProtocolLessURL(t *testing.T) { - ctx := models.ReqContext{ + ctx := contextmodel.ReqContext{ Context: &web.Context{}, SignedInUser: &user.SignedInUser{OrgRole: org.RoleEditor}, } @@ -871,7 +872,7 @@ func TestNewDataSourceProxy_ProtocolLessURL(t *testing.T) { // Test wth MSSQL type data sources. func TestNewDataSourceProxy_MSSQL(t *testing.T) { - ctx := models.ReqContext{ + ctx := contextmodel.ReqContext{ Context: &web.Context{}, SignedInUser: &user.SignedInUser{OrgRole: org.RoleEditor}, } @@ -926,7 +927,7 @@ func TestNewDataSourceProxy_MSSQL(t *testing.T) { } // getDatasourceProxiedRequest is a helper for easier setup of tests based on global config and ReqContext. -func getDatasourceProxiedRequest(t *testing.T, ctx *models.ReqContext, cfg *setting.Cfg) *http.Request { +func getDatasourceProxiedRequest(t *testing.T, ctx *contextmodel.ReqContext, cfg *setting.Cfg) *http.Request { ds := &datasources.DataSource{ Type: "custom", Url: "http://host/root/", @@ -1052,7 +1053,7 @@ func createAuthTest(t *testing.T, secretsStore secretskvs.SecretsKVStore, dsType } func runDatasourceAuthTest(t *testing.T, secretsService secrets.Service, secretsStore secretskvs.SecretsKVStore, cfg *setting.Cfg, test *testCase) { - ctx := &models.ReqContext{} + ctx := &contextmodel.ReqContext{} tracer := tracing.InitializeTracerForTest() var routes []*plugins.Route @@ -1089,10 +1090,10 @@ func Test_PathCheck(t *testing.T) { } tracer := tracing.InitializeTracerForTest() - setUp := func() (*models.ReqContext, *http.Request) { + setUp := func() (*contextmodel.ReqContext, *http.Request) { req, err := http.NewRequest("GET", "http://localhost/asd", nil) require.NoError(t, err) - ctx := &models.ReqContext{ + ctx := &contextmodel.ReqContext{ Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgRole: org.RoleViewer}, } diff --git a/pkg/api/pluginproxy/pluginproxy.go b/pkg/api/pluginproxy/pluginproxy.go index e77b83d0cb3..a69abb209be 100644 --- a/pkg/api/pluginproxy/pluginproxy.go +++ b/pkg/api/pluginproxy/pluginproxy.go @@ -8,8 +8,8 @@ import ( "net/url" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/pluginsettings" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/setting" @@ -22,7 +22,7 @@ import ( type PluginProxy struct { ps *pluginsettings.DTO pluginRoutes []*plugins.Route - ctx *models.ReqContext + ctx *contextmodel.ReqContext proxyPath string matchedRoute *plugins.Route cfg *setting.Cfg @@ -32,7 +32,7 @@ type PluginProxy struct { } // NewPluginProxy creates a plugin proxy. -func NewPluginProxy(ps *pluginsettings.DTO, routes []*plugins.Route, ctx *models.ReqContext, +func NewPluginProxy(ps *pluginsettings.DTO, routes []*plugins.Route, ctx *contextmodel.ReqContext, proxyPath string, cfg *setting.Cfg, secretsService secrets.Service, tracer tracing.Tracer, transport *http.Transport) (*PluginProxy, error) { return &PluginProxy{ diff --git a/pkg/api/pluginproxy/pluginproxy_test.go b/pkg/api/pluginproxy/pluginproxy_test.go index d1e18072caa..4ac71cb9291 100644 --- a/pkg/api/pluginproxy/pluginproxy_test.go +++ b/pkg/api/pluginproxy/pluginproxy_test.go @@ -10,8 +10,8 @@ import ( "testing" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/pluginsettings" "github.com/grafana/grafana/pkg/services/secrets" @@ -49,7 +49,7 @@ func TestPluginProxy(t *testing.T) { }, }, secretsService, - &models.ReqContext{ + &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{ Login: "test_user", }, @@ -72,7 +72,7 @@ func TestPluginProxy(t *testing.T) { t, &pluginsettings.DTO{}, secretsService, - &models.ReqContext{ + &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{ Login: "test_user", }, @@ -96,7 +96,7 @@ func TestPluginProxy(t *testing.T) { t, &pluginsettings.DTO{}, secretsService, - &models.ReqContext{ + &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{ Login: "test_user", }, @@ -119,7 +119,7 @@ func TestPluginProxy(t *testing.T) { t, &pluginsettings.DTO{}, secretsService, - &models.ReqContext{ + &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{IsAnonymous: true}, Context: &web.Context{ Req: httpReq, @@ -150,7 +150,7 @@ func TestPluginProxy(t *testing.T) { }, }, secretsService, - &models.ReqContext{ + &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{ Login: "test_user", }, @@ -178,7 +178,7 @@ func TestPluginProxy(t *testing.T) { t, &pluginsettings.DTO{}, secretsService, - &models.ReqContext{ + &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{ Login: "test_user", }, @@ -216,7 +216,7 @@ func TestPluginProxy(t *testing.T) { SecureJSONData: encryptedJsonData, }, secretsService, - &models.ReqContext{ + &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{ Login: "test_user", }, @@ -250,7 +250,7 @@ func TestPluginProxy(t *testing.T) { }, } - ctx := &models.ReqContext{ + ctx := &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{}, Context: &web.Context{ Req: httptest.NewRequest("GET", "/", nil), @@ -388,7 +388,7 @@ func TestPluginProxyRoutes(t *testing.T) { responseWriter := web.NewResponseWriter("GET", httptest.NewRecorder()) - ctx := &models.ReqContext{ + ctx := &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{}, Context: &web.Context{ Req: httptest.NewRequest("GET", tc.proxyPath, nil), @@ -420,7 +420,7 @@ func TestPluginProxyRoutes(t *testing.T) { } // getPluginProxiedRequest is a helper for easier setup of tests based on global config and ReqContext. -func getPluginProxiedRequest(t *testing.T, ps *pluginsettings.DTO, secretsService secrets.Service, ctx *models.ReqContext, cfg *setting.Cfg, route *plugins.Route) *http.Request { +func getPluginProxiedRequest(t *testing.T, ps *pluginsettings.DTO, secretsService secrets.Service, ctx *contextmodel.ReqContext, cfg *setting.Cfg, route *plugins.Route) *http.Request { // insert dummy route if none is specified if route == nil { route = &plugins.Route{ diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 757c39fd999..423b731eff5 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -18,12 +18,12 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin" "github.com/grafana/grafana/pkg/plugins/repo" "github.com/grafana/grafana/pkg/plugins/storage" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/org" @@ -33,7 +33,7 @@ import ( "github.com/grafana/grafana/pkg/web" ) -func (hs *HTTPServer) GetPluginList(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetPluginList(c *contextmodel.ReqContext) response.Response { typeFilter := c.Query("type") enabledFilter := c.Query("enabled") embeddedFilter := c.Query("embedded") @@ -158,7 +158,7 @@ func (hs *HTTPServer) GetPluginList(c *models.ReqContext) response.Response { return response.JSON(http.StatusOK, result) } -func (hs *HTTPServer) GetPluginSettingByID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetPluginSettingByID(c *contextmodel.ReqContext) response.Response { pluginID := web.Params(c.Req)[":pluginId"] plugin, exists := hs.pluginStore.Plugin(c.Req.Context(), pluginID) @@ -227,7 +227,7 @@ func (hs *HTTPServer) GetPluginSettingByID(c *models.ReqContext) response.Respon return response.JSON(http.StatusOK, dto) } -func (hs *HTTPServer) UpdatePluginSetting(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdatePluginSetting(c *contextmodel.ReqContext) response.Response { cmd := pluginsettings.UpdatePluginSettingCmd{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -256,7 +256,7 @@ func (hs *HTTPServer) UpdatePluginSetting(c *models.ReqContext) response.Respons return response.Success("Plugin settings updated") } -func (hs *HTTPServer) GetPluginMarkdown(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetPluginMarkdown(c *contextmodel.ReqContext) response.Response { pluginID := web.Params(c.Req)[":pluginId"] name := web.Params(c.Req)[":name"] @@ -286,7 +286,7 @@ func (hs *HTTPServer) GetPluginMarkdown(c *models.ReqContext) response.Response // CollectPluginMetrics collect metrics from a plugin. // // /api/plugins/:pluginId/metrics -func (hs *HTTPServer) CollectPluginMetrics(c *models.ReqContext) response.Response { +func (hs *HTTPServer) CollectPluginMetrics(c *contextmodel.ReqContext) response.Response { pluginID := web.Params(c.Req)[":pluginId"] resp, err := hs.pluginClient.CollectMetrics(c.Req.Context(), &backend.CollectMetricsRequest{PluginContext: backend.PluginContext{PluginID: pluginID}}) if err != nil { @@ -302,7 +302,7 @@ func (hs *HTTPServer) CollectPluginMetrics(c *models.ReqContext) response.Respon // getPluginAssets returns public plugin assets (images, JS, etc.) // // /public/plugins/:pluginId/* -func (hs *HTTPServer) getPluginAssets(c *models.ReqContext) { +func (hs *HTTPServer) getPluginAssets(c *contextmodel.ReqContext) { pluginID := web.Params(c.Req)[":pluginId"] plugin, exists := hs.pluginStore.Plugin(c.Req.Context(), pluginID) if !exists { @@ -359,7 +359,7 @@ func (hs *HTTPServer) getPluginAssets(c *models.ReqContext) { // CheckHealth returns the health of a plugin. // /api/plugins/:pluginId/health -func (hs *HTTPServer) CheckHealth(c *models.ReqContext) response.Response { +func (hs *HTTPServer) CheckHealth(c *contextmodel.ReqContext) response.Response { pluginID := web.Params(c.Req)[":pluginId"] pCtx, found, err := hs.PluginContextProvider.Get(c.Req.Context(), pluginID, c.SignedInUser) @@ -401,11 +401,11 @@ func (hs *HTTPServer) CheckHealth(c *models.ReqContext) response.Response { return response.JSON(http.StatusOK, payload) } -func (hs *HTTPServer) GetPluginErrorsList(_ *models.ReqContext) response.Response { +func (hs *HTTPServer) GetPluginErrorsList(_ *contextmodel.ReqContext) response.Response { return response.JSON(http.StatusOK, hs.pluginErrorResolver.PluginErrors()) } -func (hs *HTTPServer) InstallPlugin(c *models.ReqContext) response.Response { +func (hs *HTTPServer) InstallPlugin(c *contextmodel.ReqContext) response.Response { dto := dtos.InstallPluginCommand{} if err := web.Bind(c.Req, &dto); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -444,7 +444,7 @@ func (hs *HTTPServer) InstallPlugin(c *models.ReqContext) response.Response { return response.JSON(http.StatusOK, []byte{}) } -func (hs *HTTPServer) UninstallPlugin(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UninstallPlugin(c *contextmodel.ReqContext) response.Response { pluginID := web.Params(c.Req)[":pluginId"] err := hs.pluginInstaller.Remove(c.Req.Context(), pluginID) diff --git a/pkg/api/plugins_test.go b/pkg/api/plugins_test.go index 7f2c783a518..1c85d632ead 100644 --- a/pkg/api/plugins_test.go +++ b/pkg/api/plugins_test.go @@ -20,9 +20,9 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log/logtest" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgtest" "github.com/grafana/grafana/pkg/services/pluginsettings" @@ -392,7 +392,7 @@ func pluginAssetScenario(t *testing.T, desc string, url string, urlPattern strin } sc := setupScenarioContext(t, url) - sc.defaultHandler = func(c *models.ReqContext) { + sc.defaultHandler = func(c *contextmodel.ReqContext) { sc.context = c hs.getPluginAssets(c) } diff --git a/pkg/api/preferences.go b/pkg/api/preferences.go index 456b5a31dc6..f008270c444 100644 --- a/pkg/api/preferences.go +++ b/pkg/api/preferences.go @@ -7,7 +7,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/kinds/preferences" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" pref "github.com/grafana/grafana/pkg/services/preference" "github.com/grafana/grafana/pkg/web" @@ -20,7 +20,7 @@ const ( ) // POST /api/preferences/set-home-dash -func (hs *HTTPServer) SetHomeDashboard(c *models.ReqContext) response.Response { +func (hs *HTTPServer) SetHomeDashboard(c *contextmodel.ReqContext) response.Response { cmd := pref.SavePreferenceCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -61,7 +61,7 @@ func (hs *HTTPServer) SetHomeDashboard(c *models.ReqContext) response.Response { // 200: getPreferencesResponse // 401: unauthorisedError // 500: internalServerError -func (hs *HTTPServer) GetUserPreferences(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetUserPreferences(c *contextmodel.ReqContext) response.Response { return hs.getPreferencesFor(c.Req.Context(), c.OrgID, c.UserID, 0) } @@ -125,7 +125,7 @@ func (hs *HTTPServer) getPreferencesFor(ctx context.Context, orgID, userID, team // 400: badRequestError // 401: unauthorisedError // 500: internalServerError -func (hs *HTTPServer) UpdateUserPreferences(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateUserPreferences(c *contextmodel.ReqContext) response.Response { dtoCmd := dtos.UpdatePrefsCmd{} if err := web.Bind(c.Req, &dtoCmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -182,7 +182,7 @@ func (hs *HTTPServer) updatePreferencesFor(ctx context.Context, orgID, userID, t // 400: badRequestError // 401: unauthorisedError // 500: internalServerError -func (hs *HTTPServer) PatchUserPreferences(c *models.ReqContext) response.Response { +func (hs *HTTPServer) PatchUserPreferences(c *contextmodel.ReqContext) response.Response { dtoCmd := dtos.PatchPrefsCmd{} if err := web.Bind(c.Req, &dtoCmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -241,7 +241,7 @@ func (hs *HTTPServer) patchPreferencesFor(ctx context.Context, orgID, userID, te // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetOrgPreferences(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetOrgPreferences(c *contextmodel.ReqContext) response.Response { return hs.getPreferencesFor(c.Req.Context(), c.OrgID, 0, 0) } @@ -255,7 +255,7 @@ func (hs *HTTPServer) GetOrgPreferences(c *models.ReqContext) response.Response // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) UpdateOrgPreferences(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateOrgPreferences(c *contextmodel.ReqContext) response.Response { dtoCmd := dtos.UpdatePrefsCmd{} if err := web.Bind(c.Req, &dtoCmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -274,7 +274,7 @@ func (hs *HTTPServer) UpdateOrgPreferences(c *models.ReqContext) response.Respon // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) PatchOrgPreferences(c *models.ReqContext) response.Response { +func (hs *HTTPServer) PatchOrgPreferences(c *contextmodel.ReqContext) response.Response { dtoCmd := dtos.PatchPrefsCmd{} if err := web.Bind(c.Req, &dtoCmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) diff --git a/pkg/api/quota.go b/pkg/api/quota.go index dd0ee9f538d..d712e55824a 100644 --- a/pkg/api/quota.go +++ b/pkg/api/quota.go @@ -5,7 +5,7 @@ import ( "strconv" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/web" ) @@ -22,7 +22,7 @@ import ( // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetCurrentOrgQuotas(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetCurrentOrgQuotas(c *contextmodel.ReqContext) response.Response { return hs.getOrgQuotasHelper(c, c.OrgID) } @@ -38,7 +38,7 @@ func (hs *HTTPServer) GetCurrentOrgQuotas(c *models.ReqContext) response.Respons // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetOrgQuotas(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetOrgQuotas(c *contextmodel.ReqContext) response.Response { orgId, err := strconv.ParseInt(web.Params(c.Req)[":orgId"], 10, 64) if err != nil { return response.Err(quota.ErrBadRequest.Errorf("orgId is invalid: %w", err)) @@ -46,7 +46,7 @@ func (hs *HTTPServer) GetOrgQuotas(c *models.ReqContext) response.Response { return hs.getOrgQuotasHelper(c, orgId) } -func (hs *HTTPServer) getOrgQuotasHelper(c *models.ReqContext, orgID int64) response.Response { +func (hs *HTTPServer) getOrgQuotasHelper(c *contextmodel.ReqContext, orgID int64) response.Response { q, err := hs.QuotaService.GetQuotasByScope(c.Req.Context(), quota.OrgScope, orgID) if err != nil { return response.ErrOrFallback(http.StatusInternalServerError, "failed to get quota", err) @@ -69,7 +69,7 @@ func (hs *HTTPServer) getOrgQuotasHelper(c *models.ReqContext, orgID int64) resp // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) UpdateOrgQuota(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateOrgQuota(c *contextmodel.ReqContext) response.Response { cmd := quota.UpdateQuotaCmd{} var err error if err := web.Bind(c.Req, &cmd); err != nil { @@ -113,7 +113,7 @@ func (hs *HTTPServer) UpdateOrgQuota(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetUserQuotas(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetUserQuotas(c *contextmodel.ReqContext) response.Response { id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Err(quota.ErrBadRequest.Errorf("id is invalid: %w", err)) @@ -142,7 +142,7 @@ func (hs *HTTPServer) GetUserQuotas(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) UpdateUserQuota(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateUserQuota(c *contextmodel.ReqContext) response.Response { cmd := quota.UpdateQuotaCmd{} var err error if err := web.Bind(c.Req, &cmd); err != nil { diff --git a/pkg/api/render.go b/pkg/api/render.go index 6b2c3480998..de80fbf8db5 100644 --- a/pkg/api/render.go +++ b/pkg/api/render.go @@ -8,12 +8,13 @@ import ( "time" "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" ) -func (hs *HTTPServer) RenderToPng(c *models.ReqContext) { +func (hs *HTTPServer) RenderToPng(c *contextmodel.ReqContext) { queryReader, err := util.NewURLQueryReader(c.Req.URL) if err != nil { c.Handle(hs.Cfg, 400, "Render parameters error", err) diff --git a/pkg/api/response/response.go b/pkg/api/response/response.go index 744a8f0ab45..d131bff1caa 100644 --- a/pkg/api/response/response.go +++ b/pkg/api/response/response.go @@ -11,7 +11,7 @@ import ( jsoniter "github.com/json-iterator/go" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util/errutil" ) @@ -19,7 +19,7 @@ import ( // Response is an HTTP response interface. type Response interface { // WriteTo writes to a context. - WriteTo(ctx *models.ReqContext) + WriteTo(ctx *contextmodel.ReqContext) // Body gets the response's body. Body() []byte // Status gets the response's status. @@ -77,7 +77,7 @@ func (r *NormalResponse) ErrMessage() string { return r.errMessage } -func (r *NormalResponse) WriteTo(ctx *models.ReqContext) { +func (r *NormalResponse) WriteTo(ctx *contextmodel.ReqContext) { if r.err != nil { v := map[string]interface{}{} traceID := tracing.TraceIDFromContext(ctx.Req.Context(), false) @@ -132,7 +132,7 @@ func (r StreamingResponse) Body() []byte { // WriteTo writes the response to the provided context. // Required to implement api.Response. -func (r StreamingResponse) WriteTo(ctx *models.ReqContext) { +func (r StreamingResponse) WriteTo(ctx *contextmodel.ReqContext) { header := ctx.Resp.Header() for k, v := range r.header { header[k] = v @@ -155,7 +155,7 @@ type RedirectResponse struct { } // WriteTo writes to a response. -func (r *RedirectResponse) WriteTo(ctx *models.ReqContext) { +func (r *RedirectResponse) WriteTo(ctx *contextmodel.ReqContext) { ctx.Redirect(r.location) } diff --git a/pkg/api/response/web_hack.go b/pkg/api/response/web_hack.go index 81f3bfa4ac5..52aa9ee38a7 100644 --- a/pkg/api/response/web_hack.go +++ b/pkg/api/response/web_hack.go @@ -7,17 +7,17 @@ import ( "fmt" "net/http" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/web" ) type ( handlerStd = func(http.ResponseWriter, *http.Request) handlerStdCtx = func(http.ResponseWriter, *http.Request, *web.Context) - handlerStdReqCtx = func(http.ResponseWriter, *http.Request, *models.ReqContext) - handlerReqCtx = func(*models.ReqContext) - handlerReqCtxRes = func(*models.ReqContext) Response + handlerStdReqCtx = func(http.ResponseWriter, *http.Request, *contextmodel.ReqContext) + handlerReqCtx = func(*contextmodel.ReqContext) + handlerReqCtxRes = func(*contextmodel.ReqContext) Response handlerCtx = func(*web.Context) ) @@ -67,11 +67,11 @@ func webCtx(w http.ResponseWriter, r *http.Request) *web.Context { return ctx } -func reqCtx(w http.ResponseWriter, r *http.Request) *models.ReqContext { +func reqCtx(w http.ResponseWriter, r *http.Request) *contextmodel.ReqContext { wCtx := webCtx(w, r) - reqCtx, ok := wCtx.Req.Context().Value(ctxkey.Key{}).(*models.ReqContext) + reqCtx, ok := wCtx.Req.Context().Value(ctxkey.Key{}).(*contextmodel.ReqContext) if !ok { - panic("no *models.ReqContext found") + panic("no *contextmodel.ReqContext found") } return reqCtx } diff --git a/pkg/api/routing/routing.go b/pkg/api/routing/routing.go index 204ede8bf56..c5f5ec09759 100644 --- a/pkg/api/routing/routing.go +++ b/pkg/api/routing/routing.go @@ -2,7 +2,7 @@ package routing import ( "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/web" ) @@ -12,8 +12,8 @@ var ( } ) -func Wrap(handler func(c *models.ReqContext) response.Response) web.Handler { - return func(c *models.ReqContext) { +func Wrap(handler func(c *contextmodel.ReqContext) response.Response) web.Handler { + return func(c *contextmodel.ReqContext) { if res := handler(c); res != nil { res.WriteTo(c) } diff --git a/pkg/api/search.go b/pkg/api/search.go index 5ebf630f606..91b8e2c06ac 100644 --- a/pkg/api/search.go +++ b/pkg/api/search.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/search" "github.com/grafana/grafana/pkg/util" @@ -20,7 +21,7 @@ import ( // 401: unauthorisedError // 422: unprocessableEntityError // 500: internalServerError -func (hs *HTTPServer) Search(c *models.ReqContext) response.Response { +func (hs *HTTPServer) Search(c *contextmodel.ReqContext) response.Response { query := c.Query("query") tags := c.QueryStrings("tag") starred := c.Query("starred") @@ -94,7 +95,7 @@ func (hs *HTTPServer) Search(c *models.ReqContext) response.Response { return hs.searchHitsWithMetadata(c, searchQuery.Result) } -func (hs *HTTPServer) searchHitsWithMetadata(c *models.ReqContext, hits models.HitList) response.Response { +func (hs *HTTPServer) searchHitsWithMetadata(c *contextmodel.ReqContext, hits models.HitList) response.Response { folderUIDs := make(map[string]bool) dashboardUIDs := make(map[string]bool) @@ -136,7 +137,7 @@ func (hs *HTTPServer) searchHitsWithMetadata(c *models.ReqContext, hits models.H // Responses: // 200: listSortOptionsResponse // 401: unauthorisedError -func (hs *HTTPServer) ListSortOptions(c *models.ReqContext) response.Response { +func (hs *HTTPServer) ListSortOptions(c *contextmodel.ReqContext) response.Response { opts := hs.SearchService.SortOptions() res := []util.DynMap{} diff --git a/pkg/api/short_url.go b/pkg/api/short_url.go index 040818547ae..f3ff7a6a8db 100644 --- a/pkg/api/short_url.go +++ b/pkg/api/short_url.go @@ -7,7 +7,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/shorturls" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" @@ -15,7 +15,7 @@ import ( ) // createShortURL handles requests to create short URLs. -func (hs *HTTPServer) createShortURL(c *models.ReqContext) response.Response { +func (hs *HTTPServer) createShortURL(c *contextmodel.ReqContext) response.Response { cmd := dtos.CreateShortURLCmd{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Err(shorturls.ErrShortURLBadRequest.Errorf("bad request data: %w", err)) @@ -37,7 +37,7 @@ func (hs *HTTPServer) createShortURL(c *models.ReqContext) response.Response { return response.JSON(http.StatusOK, dto) } -func (hs *HTTPServer) redirectFromShortURL(c *models.ReqContext) { +func (hs *HTTPServer) redirectFromShortURL(c *contextmodel.ReqContext) { shortURLUID := web.Params(c.Req)[":uid"] if !util.IsValidShortUID(shortURLUID) { diff --git a/pkg/api/short_url_test.go b/pkg/api/short_url_test.go index 4a25feec1a6..17a4597958f 100644 --- a/pkg/api/short_url_test.go +++ b/pkg/api/short_url_test.go @@ -10,7 +10,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/shorturls" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -61,7 +61,7 @@ func createShortURLScenario(t *testing.T, desc string, url string, routePattern } sc := setupScenarioContext(t, url) - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c diff --git a/pkg/api/signup.go b/pkg/api/signup.go index 221749cb690..b14f74b4920 100644 --- a/pkg/api/signup.go +++ b/pkg/api/signup.go @@ -10,7 +10,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/events" "github.com/grafana/grafana/pkg/infra/metrics" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" tempuser "github.com/grafana/grafana/pkg/services/temp_user" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -19,7 +19,7 @@ import ( ) // GET /api/user/signup/options -func GetSignUpOptions(c *models.ReqContext) response.Response { +func GetSignUpOptions(c *contextmodel.ReqContext) response.Response { return response.JSON(http.StatusOK, util.DynMap{ "verifyEmailEnabled": setting.VerifyEmailEnabled, "autoAssignOrg": setting.AutoAssignOrg, @@ -27,7 +27,7 @@ func GetSignUpOptions(c *models.ReqContext) response.Response { } // POST /api/user/signup -func (hs *HTTPServer) SignUp(c *models.ReqContext) response.Response { +func (hs *HTTPServer) SignUp(c *contextmodel.ReqContext) response.Response { form := dtos.SignUpForm{} var err error if err = web.Bind(c.Req, &form); err != nil { @@ -75,7 +75,7 @@ func (hs *HTTPServer) SignUp(c *models.ReqContext) response.Response { return response.JSON(http.StatusOK, util.DynMap{"status": "SignUpCreated"}) } -func (hs *HTTPServer) SignUpStep2(c *models.ReqContext) response.Response { +func (hs *HTTPServer) SignUpStep2(c *contextmodel.ReqContext) response.Response { form := dtos.SignUpStep2Form{} if err := web.Bind(c.Req, &form); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) diff --git a/pkg/api/swagger.go b/pkg/api/swagger.go index 7c96b576b9f..667871fbe1b 100644 --- a/pkg/api/swagger.go +++ b/pkg/api/swagger.go @@ -3,9 +3,9 @@ package api import ( "net/http" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" ) -func swaggerUI(c *models.ReqContext) { +func swaggerUI(c *contextmodel.ReqContext) { c.HTML(http.StatusOK, "swagger", nil) } diff --git a/pkg/api/team.go b/pkg/api/team.go index d845b4f6ab1..5d5d2b6ed22 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -7,7 +7,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/team" @@ -25,7 +25,7 @@ import ( // 403: forbiddenError // 409: conflictError // 500: internalServerError -func (hs *HTTPServer) CreateTeam(c *models.ReqContext) response.Response { +func (hs *HTTPServer) CreateTeam(c *contextmodel.ReqContext) response.Response { cmd := team.CreateTeamCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -78,7 +78,7 @@ func (hs *HTTPServer) CreateTeam(c *models.ReqContext) response.Response { // 404: notFoundError // 409: conflictError // 500: internalServerError -func (hs *HTTPServer) UpdateTeam(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateTeam(c *contextmodel.ReqContext) response.Response { cmd := team.UpdateTeamCommand{} var err error if err := web.Bind(c.Req, &cmd); err != nil { @@ -116,7 +116,7 @@ func (hs *HTTPServer) UpdateTeam(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) DeleteTeamByID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) DeleteTeamByID(c *contextmodel.ReqContext) response.Response { orgID := c.OrgID teamID, err := strconv.ParseInt(web.Params(c.Req)[":teamId"], 10, 64) if err != nil { @@ -148,7 +148,7 @@ func (hs *HTTPServer) DeleteTeamByID(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response { +func (hs *HTTPServer) SearchTeams(c *contextmodel.ReqContext) response.Response { perPage := c.QueryInt("perpage") if perPage <= 0 { perPage = 1000 @@ -202,7 +202,7 @@ func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response { // UserFilter returns the user ID used in a filter when querying a team // 1. If the user is a viewer or editor, this will return the user's ID. // 2. If the user is an admin, this will return models.FilterIgnoreUser (0) -func userFilter(c *models.ReqContext) int64 { +func userFilter(c *contextmodel.ReqContext) int64 { userIdFilter := c.SignedInUser.UserID if c.OrgRole == org.RoleAdmin { userIdFilter = team.FilterIgnoreUser @@ -220,7 +220,7 @@ func userFilter(c *models.ReqContext) int64 { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetTeamByID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetTeamByID(c *contextmodel.ReqContext) response.Response { teamId, err := strconv.ParseInt(web.Params(c.Req)[":teamId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "teamId is invalid", err) @@ -264,7 +264,7 @@ func (hs *HTTPServer) GetTeamByID(c *models.ReqContext) response.Response { // 200: getPreferencesResponse // 401: unauthorisedError // 500: internalServerError -func (hs *HTTPServer) GetTeamPreferences(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetTeamPreferences(c *contextmodel.ReqContext) response.Response { teamId, err := strconv.ParseInt(web.Params(c.Req)[":teamId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "teamId is invalid", err) @@ -290,7 +290,7 @@ func (hs *HTTPServer) GetTeamPreferences(c *models.ReqContext) response.Response // 400: badRequestError // 401: unauthorisedError // 500: internalServerError -func (hs *HTTPServer) UpdateTeamPreferences(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateTeamPreferences(c *contextmodel.ReqContext) response.Response { dtoCmd := dtos.UpdatePrefsCmd{} if err := web.Bind(c.Req, &dtoCmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) diff --git a/pkg/api/team_members.go b/pkg/api/team_members.go index 630d99b8d85..3da7f5eda3b 100644 --- a/pkg/api/team_members.go +++ b/pkg/api/team_members.go @@ -9,8 +9,8 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/team" @@ -28,7 +28,7 @@ import ( // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetTeamMembers(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetTeamMembers(c *contextmodel.ReqContext) response.Response { teamId, err := strconv.ParseInt(web.Params(c.Req)[":teamId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "teamId is invalid", err) @@ -79,7 +79,7 @@ func (hs *HTTPServer) GetTeamMembers(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) AddTeamMember(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AddTeamMember(c *contextmodel.ReqContext) response.Response { cmd := team.AddTeamMemberCommand{} var err error if err := web.Bind(c.Req, &cmd); err != nil { @@ -125,7 +125,7 @@ func (hs *HTTPServer) AddTeamMember(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) UpdateTeamMember(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateTeamMember(c *contextmodel.ReqContext) response.Response { cmd := team.UpdateTeamMemberCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -181,7 +181,7 @@ func getPermissionName(permission dashboards.PermissionType) string { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) RemoveTeamMember(c *models.ReqContext) response.Response { +func (hs *HTTPServer) RemoveTeamMember(c *contextmodel.ReqContext) response.Response { orgId := c.OrgID teamId, err := strconv.ParseInt(web.Params(c.Req)[":teamId"], 10, 64) if err != nil { diff --git a/pkg/api/team_test.go b/pkg/api/team_test.go index 3577ce501be..1423a2ea7c4 100644 --- a/pkg/api/team_test.go +++ b/pkg/api/team_test.go @@ -14,10 +14,10 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/log/logtest" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" pref "github.com/grafana/grafana/pkg/services/preference" "github.com/grafana/grafana/pkg/services/preference/preftest" @@ -124,7 +124,7 @@ func TestTeamAPIEndpoint(t *testing.T) { t.Run("with no real signed in user", func(t *testing.T) { logger := &logtest.Fake{} - c := &models.ReqContext{ + c := &contextmodel.ReqContext{ Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{}, Logger: logger, @@ -141,7 +141,7 @@ func TestTeamAPIEndpoint(t *testing.T) { t.Run("with real signed in user", func(t *testing.T) { logger := &logtest.Fake{} - c := &models.ReqContext{ + c := &contextmodel.ReqContext{ Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{UserID: 42}, Logger: logger, diff --git a/pkg/api/user.go b/pkg/api/user.go index 91345a48cfa..90f5bb152af 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/team" @@ -29,7 +30,7 @@ import ( // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetSignedInUser(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetSignedInUser(c *contextmodel.ReqContext) response.Response { return hs.getUserUserProfile(c, c.UserID) } @@ -43,7 +44,7 @@ func (hs *HTTPServer) GetSignedInUser(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetUserByID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetUserByID(c *contextmodel.ReqContext) response.Response { id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", err) @@ -51,7 +52,7 @@ func (hs *HTTPServer) GetUserByID(c *models.ReqContext) response.Response { return hs.getUserUserProfile(c, id) } -func (hs *HTTPServer) getUserUserProfile(c *models.ReqContext, userID int64) response.Response { +func (hs *HTTPServer) getUserUserProfile(c *contextmodel.ReqContext, userID int64) response.Response { query := user.GetUserProfileQuery{UserID: userID} userProfile, err := hs.userService.GetProfile(c.Req.Context(), &query) @@ -86,7 +87,7 @@ func (hs *HTTPServer) getUserUserProfile(c *models.ReqContext, userID int64) res // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetUserByLoginOrEmail(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetUserByLoginOrEmail(c *contextmodel.ReqContext) response.Response { query := user.GetUserByLoginQuery{LoginOrEmail: c.Query("loginOrEmail")} usr, err := hs.userService.GetByLogin(c.Req.Context(), &query) if err != nil { @@ -118,7 +119,7 @@ func (hs *HTTPServer) GetUserByLoginOrEmail(c *models.ReqContext) response.Respo // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) UpdateSignedInUser(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateSignedInUser(c *contextmodel.ReqContext) response.Response { cmd := user.UpdateUserCommand{} var err error if err = web.Bind(c.Req, &cmd); err != nil { @@ -152,7 +153,7 @@ func (hs *HTTPServer) UpdateSignedInUser(c *models.ReqContext) response.Response // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) UpdateUser(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateUser(c *contextmodel.ReqContext) response.Response { cmd := user.UpdateUserCommand{} var err error if err = web.Bind(c.Req, &cmd); err != nil { @@ -171,7 +172,7 @@ func (hs *HTTPServer) UpdateUser(c *models.ReqContext) response.Response { } // POST /api/users/:id/using/:orgId -func (hs *HTTPServer) UpdateUserActiveOrg(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateUserActiveOrg(c *contextmodel.ReqContext) response.Response { userID, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", err) @@ -250,7 +251,7 @@ func (hs *HTTPServer) isExternalUser(ctx context.Context, userID int64) (bool, e // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetSignedInUserOrgList(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetSignedInUserOrgList(c *contextmodel.ReqContext) response.Response { return hs.getUserOrgList(c.Req.Context(), c.UserID) } @@ -265,7 +266,7 @@ func (hs *HTTPServer) GetSignedInUserOrgList(c *models.ReqContext) response.Resp // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetSignedInUserTeamList(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetSignedInUserTeamList(c *contextmodel.ReqContext) response.Response { return hs.getUserTeamList(c, c.OrgID, c.UserID) } @@ -281,7 +282,7 @@ func (hs *HTTPServer) GetSignedInUserTeamList(c *models.ReqContext) response.Res // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetUserTeams(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetUserTeams(c *contextmodel.ReqContext) response.Response { id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", err) @@ -289,7 +290,7 @@ func (hs *HTTPServer) GetUserTeams(c *models.ReqContext) response.Response { return hs.getUserTeamList(c, c.OrgID, id) } -func (hs *HTTPServer) getUserTeamList(c *models.ReqContext, orgID int64, userID int64) response.Response { +func (hs *HTTPServer) getUserTeamList(c *contextmodel.ReqContext, orgID int64, userID int64) response.Response { query := team.GetTeamsByUserQuery{OrgID: orgID, UserID: userID, SignedInUser: c.SignedInUser} queryResult, err := hs.teamService.GetTeamsByUser(c.Req.Context(), &query) @@ -315,7 +316,7 @@ func (hs *HTTPServer) getUserTeamList(c *models.ReqContext, orgID int64, userID // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetUserOrgList(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetUserOrgList(c *contextmodel.ReqContext) response.Response { id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", err) @@ -365,7 +366,7 @@ func (hs *HTTPServer) validateUsingOrg(ctx context.Context, userID int64, orgID // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) UserSetUsingOrg(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UserSetUsingOrg(c *contextmodel.ReqContext) response.Response { orgID, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", err) @@ -385,7 +386,7 @@ func (hs *HTTPServer) UserSetUsingOrg(c *models.ReqContext) response.Response { } // GET /profile/switch-org/:id -func (hs *HTTPServer) ChangeActiveOrgAndRedirectToHome(c *models.ReqContext) { +func (hs *HTTPServer) ChangeActiveOrgAndRedirectToHome(c *contextmodel.ReqContext) { orgID, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { c.JsonApiErr(http.StatusBadRequest, "id is invalid", err) @@ -420,7 +421,7 @@ func (hs *HTTPServer) ChangeActiveOrgAndRedirectToHome(c *models.ReqContext) { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) ChangeUserPassword(c *models.ReqContext) response.Response { +func (hs *HTTPServer) ChangeUserPassword(c *contextmodel.ReqContext) response.Response { cmd := user.ChangeUserPasswordCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -468,7 +469,7 @@ func (hs *HTTPServer) ChangeUserPassword(c *models.ReqContext) response.Response } // redirectToChangePassword handles GET /.well-known/change-password. -func redirectToChangePassword(c *models.ReqContext) { +func redirectToChangePassword(c *contextmodel.ReqContext) { c.Redirect("/profile/password", 302) } @@ -481,7 +482,7 @@ func redirectToChangePassword(c *models.ReqContext) { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) SetHelpFlag(c *models.ReqContext) response.Response { +func (hs *HTTPServer) SetHelpFlag(c *contextmodel.ReqContext) response.Response { flag, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", err) @@ -511,7 +512,7 @@ func (hs *HTTPServer) SetHelpFlag(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) ClearHelpFlags(c *models.ReqContext) response.Response { +func (hs *HTTPServer) ClearHelpFlags(c *contextmodel.ReqContext) response.Response { cmd := user.SetUserHelpFlagCommand{ UserID: c.UserID, HelpFlags1: user.HelpFlags1(0), diff --git a/pkg/api/user_test.go b/pkg/api/user_test.go index 2bc4c156d0d..8ef586ccf21 100644 --- a/pkg/api/user_test.go +++ b/pkg/api/user_test.go @@ -21,6 +21,7 @@ import ( "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/models" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/login/authinfoservice" authinfostore "github.com/grafana/grafana/pkg/services/login/authinfoservice/database" "github.com/grafana/grafana/pkg/services/login/logintest" @@ -255,7 +256,7 @@ func updateUserScenario(t *testing.T, ctx updateUserContext, hs *HTTPServer) { sc.authInfoService = &logintest.AuthInfoServiceFake{} hs.authInfoService = sc.authInfoService - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(ctx.cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c @@ -308,7 +309,7 @@ func updateSignedInUserScenario(t *testing.T, ctx updateUserContext, hs *HTTPSer sc.authInfoService = &logintest.AuthInfoServiceFake{} hs.authInfoService = sc.authInfoService - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(ctx.cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c diff --git a/pkg/api/user_token.go b/pkg/api/user_token.go index 3e12fca2d2e..dc13fa1ce77 100644 --- a/pkg/api/user_token.go +++ b/pkg/api/user_token.go @@ -8,8 +8,8 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" @@ -27,7 +27,7 @@ import ( // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetUserAuthTokens(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetUserAuthTokens(c *contextmodel.ReqContext) response.Response { return hs.getUserAuthTokensInternal(c, c.UserID) } @@ -43,7 +43,7 @@ func (hs *HTTPServer) GetUserAuthTokens(c *models.ReqContext) response.Response // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) RevokeUserAuthToken(c *models.ReqContext) response.Response { +func (hs *HTTPServer) RevokeUserAuthToken(c *contextmodel.ReqContext) response.Response { cmd := auth.RevokeAuthTokenCmd{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -72,7 +72,7 @@ func (hs *HTTPServer) logoutUserFromAllDevicesInternal(ctx context.Context, user }) } -func (hs *HTTPServer) getUserAuthTokensInternal(c *models.ReqContext, userID int64) response.Response { +func (hs *HTTPServer) getUserAuthTokensInternal(c *contextmodel.ReqContext, userID int64) response.Response { userQuery := user.GetUserByIDQuery{ID: userID} _, err := hs.userService.GetByID(c.Req.Context(), &userQuery) @@ -144,7 +144,7 @@ func (hs *HTTPServer) getUserAuthTokensInternal(c *models.ReqContext, userID int return response.JSON(http.StatusOK, result) } -func (hs *HTTPServer) revokeUserAuthTokenInternal(c *models.ReqContext, userID int64, cmd auth.RevokeAuthTokenCmd) response.Response { +func (hs *HTTPServer) revokeUserAuthTokenInternal(c *contextmodel.ReqContext, userID int64, cmd auth.RevokeAuthTokenCmd) response.Response { userQuery := user.GetUserByIDQuery{ID: userID} _, err := hs.userService.GetByID(c.Req.Context(), &userQuery) if err != nil { diff --git a/pkg/api/user_token_test.go b/pkg/api/user_token_test.go index 093a27011b9..a6d54f951fc 100644 --- a/pkg/api/user_token_test.go +++ b/pkg/api/user_token_test.go @@ -10,9 +10,9 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth/authtest" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/usertest" @@ -158,7 +158,7 @@ func revokeUserAuthTokenScenario(t *testing.T, desc string, url string, routePat sc := setupScenarioContext(t, url) sc.userAuthTokenService = fakeAuthTokenService - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(cmd) sc.context = c sc.context.UserID = userId @@ -185,7 +185,7 @@ func getUserAuthTokensScenario(t *testing.T, desc string, url string, routePatte sc := setupScenarioContext(t, url) sc.userAuthTokenService = fakeAuthTokenService - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { sc.context = c sc.context.UserID = userId sc.context.OrgID = testOrgID @@ -208,7 +208,7 @@ func logoutUserFromAllDevicesInternalScenario(t *testing.T, desc string, userId } sc := setupScenarioContext(t, "/") - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { sc.context = c sc.context.UserID = testUserID sc.context.OrgID = testOrgID @@ -235,7 +235,7 @@ func revokeUserAuthTokenInternalScenario(t *testing.T, desc string, cmd auth.Rev sc := setupScenarioContext(t, "/") sc.userAuthTokenService = fakeAuthTokenService - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { sc.context = c sc.context.UserID = testUserID sc.context.OrgID = testOrgID @@ -260,7 +260,7 @@ func getUserAuthTokensInternalScenario(t *testing.T, desc string, token *auth.Us sc := setupScenarioContext(t, "/") sc.userAuthTokenService = fakeAuthTokenService - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { sc.context = c sc.context.UserID = testUserID sc.context.OrgID = testOrgID diff --git a/pkg/infra/appcontext/user.go b/pkg/infra/appcontext/user.go index 604ec008b7a..d2bd57aa000 100644 --- a/pkg/infra/appcontext/user.go +++ b/pkg/infra/appcontext/user.go @@ -4,8 +4,8 @@ import ( "context" "fmt" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" grpccontext "github.com/grafana/grafana/pkg/services/grpcserver/context" "github.com/grafana/grafana/pkg/services/user" ) @@ -33,7 +33,7 @@ func User(ctx context.Context) (*user.SignedInUser, error) { } // Set by incoming HTTP request - c, ok := ctxkey.Get(ctx).(*models.ReqContext) + c, ok := ctxkey.Get(ctx).(*contextmodel.ReqContext) if ok && c.SignedInUser != nil { return c.SignedInUser, nil } diff --git a/pkg/infra/appcontext/user_test.go b/pkg/infra/appcontext/user_test.go index 91913c692f1..5122d9044e0 100644 --- a/pkg/infra/appcontext/user_test.go +++ b/pkg/infra/appcontext/user_test.go @@ -8,8 +8,8 @@ import ( "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" grpccontext "github.com/grafana/grafana/pkg/services/grpcserver/context" "github.com/grafana/grafana/pkg/services/user" "github.com/stretchr/testify/require" @@ -47,7 +47,7 @@ func TestUserFromContext(t *testing.T) { t.Run("should return user set by HTTP ReqContext", func(t *testing.T) { expected := testUser() - ctx := ctxkey.Set(context.Background(), &models.ReqContext{ + ctx := ctxkey.Set(context.Background(), &contextmodel.ReqContext{ SignedInUser: expected, }) actual, err := appcontext.User(ctx) diff --git a/pkg/infra/usagestats/service/api.go b/pkg/infra/usagestats/service/api.go index 836f73d3202..46ebed121aa 100644 --- a/pkg/infra/usagestats/service/api.go +++ b/pkg/infra/usagestats/service/api.go @@ -6,8 +6,8 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" ) const rootUrl = "/api/admin" @@ -20,7 +20,7 @@ func (uss *UsageStats) registerAPIEndpoints() { }) } -func (uss *UsageStats) getUsageReportPreview(ctx *models.ReqContext) response.Response { +func (uss *UsageStats) getUsageReportPreview(ctx *contextmodel.ReqContext) response.Response { usageReport, err := uss.GetUsageReport(ctx.Req.Context()) if err != nil { return response.Error(http.StatusInternalServerError, "failed to get usage report", err) diff --git a/pkg/infra/usagestats/service/api_test.go b/pkg/infra/usagestats/service/api_test.go index 94acc44e189..92ab183954f 100644 --- a/pkg/infra/usagestats/service/api_test.go +++ b/pkg/infra/usagestats/service/api_test.go @@ -11,8 +11,8 @@ import ( "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/stats" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -93,7 +93,7 @@ type testContext struct { func contextProvider(tc *testContext) web.Handler { return func(c *web.Context) { signedIn := tc.user != nil - reqCtx := &models.ReqContext{ + reqCtx := &contextmodel.ReqContext{ Context: c, SignedInUser: tc.user, IsSignedIn: signedIn, diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go index aeef297dc16..8ac2e457c7d 100644 --- a/pkg/middleware/auth.go +++ b/pkg/middleware/auth.go @@ -10,9 +10,9 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/middleware/cookies" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/auth" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/org" @@ -27,7 +27,7 @@ type AuthOptions struct { ReqNoAnonynmous bool } -func accessForbidden(c *models.ReqContext) { +func accessForbidden(c *contextmodel.ReqContext) { if c.IsApiRequest() { c.JsonApiErr(403, "Permission denied", nil) return @@ -36,7 +36,7 @@ func accessForbidden(c *models.ReqContext) { c.Redirect(setting.AppSubUrl + "/") } -func notAuthorized(c *models.ReqContext) { +func notAuthorized(c *contextmodel.ReqContext) { if c.IsApiRequest() { c.WriteErrOrFallback(http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized), c.LookupTokenErr) return @@ -46,7 +46,7 @@ func notAuthorized(c *models.ReqContext) { c.Redirect(setting.AppSubUrl + "/login") } -func tokenRevoked(c *models.ReqContext, err *auth.TokenRevokedError) { +func tokenRevoked(c *contextmodel.ReqContext, err *auth.TokenRevokedError) { if c.IsApiRequest() { c.JSON(401, map[string]interface{}{ "message": "Token revoked", @@ -62,7 +62,7 @@ func tokenRevoked(c *models.ReqContext, err *auth.TokenRevokedError) { c.Redirect(setting.AppSubUrl + "/login") } -func writeRedirectCookie(c *models.ReqContext) { +func writeRedirectCookie(c *contextmodel.ReqContext) { redirectTo := c.Req.RequestURI if setting.AppSubUrl != "" && !strings.HasPrefix(redirectTo, setting.AppSubUrl) { redirectTo = setting.AppSubUrl + c.Req.RequestURI @@ -83,14 +83,14 @@ func removeForceLoginParams(str string) string { return forceLoginParamsRegexp.ReplaceAllString(str, "") } -func EnsureEditorOrViewerCanEdit(c *models.ReqContext) { +func EnsureEditorOrViewerCanEdit(c *contextmodel.ReqContext) { if !c.SignedInUser.HasRole(org.RoleEditor) && !setting.ViewersCanEdit { accessForbidden(c) } } -func CanAdminPlugins(cfg *setting.Cfg) func(c *models.ReqContext) { - return func(c *models.ReqContext) { +func CanAdminPlugins(cfg *setting.Cfg) func(c *contextmodel.ReqContext) { + return func(c *contextmodel.ReqContext) { if !plugins.ReqCanAdminPlugins(cfg)(c) { accessForbidden(c) return @@ -99,7 +99,7 @@ func CanAdminPlugins(cfg *setting.Cfg) func(c *models.ReqContext) { } func RoleAuth(roles ...org.RoleType) web.Handler { - return func(c *models.ReqContext) { + return func(c *contextmodel.ReqContext) { ok := false for _, role := range roles { if role == c.OrgRole { @@ -114,7 +114,7 @@ func RoleAuth(roles ...org.RoleType) web.Handler { } func Auth(options *AuthOptions) web.Handler { - return func(c *models.ReqContext) { + return func(c *contextmodel.ReqContext) { forceLogin := false if c.AllowAnonymous { forceLogin = shouldForceLogin(c) @@ -153,7 +153,7 @@ func Auth(options *AuthOptions) web.Handler { // Intended for when feature flags open up access to APIs that // are otherwise only available to admins. func AdminOrEditorAndFeatureEnabled(enabled bool) web.Handler { - return func(c *models.ReqContext) { + return func(c *contextmodel.ReqContext) { if c.OrgRole == org.RoleAdmin { return } @@ -169,7 +169,7 @@ func AdminOrEditorAndFeatureEnabled(enabled bool) web.Handler { // SnapshotPublicModeOrSignedIn creates a middleware that allows access // if snapshot public mode is enabled or if user is signed in. func SnapshotPublicModeOrSignedIn(cfg *setting.Cfg) web.Handler { - return func(c *models.ReqContext) { + return func(c *contextmodel.ReqContext) { if cfg.SnapshotPublicMode { return } @@ -181,7 +181,7 @@ func SnapshotPublicModeOrSignedIn(cfg *setting.Cfg) web.Handler { } } -func ReqNotSignedIn(c *models.ReqContext) { +func ReqNotSignedIn(c *contextmodel.ReqContext) { if c.IsSignedIn { c.Redirect(setting.AppSubUrl + "/") } @@ -190,7 +190,7 @@ func ReqNotSignedIn(c *models.ReqContext) { // NoAuth creates a middleware that doesn't require any authentication. // If forceLogin param is set it will redirect the user to the login page. func NoAuth() web.Handler { - return func(c *models.ReqContext) { + return func(c *contextmodel.ReqContext) { if shouldForceLogin(c) { notAuthorized(c) return @@ -200,7 +200,7 @@ func NoAuth() web.Handler { // shouldForceLogin checks if user should be enforced to login. // Returns true if forceLogin parameter is set. -func shouldForceLogin(c *models.ReqContext) bool { +func shouldForceLogin(c *contextmodel.ReqContext) bool { forceLogin := false forceLoginParam, err := strconv.ParseBool(c.Req.URL.Query().Get("forceLogin")) if err == nil { @@ -210,8 +210,8 @@ func shouldForceLogin(c *models.ReqContext) bool { return forceLogin } -func OrgAdminDashOrFolderAdminOrTeamAdmin(ss db.DB, ds dashboards.DashboardService, ts team.Service) func(c *models.ReqContext) { - return func(c *models.ReqContext) { +func OrgAdminDashOrFolderAdminOrTeamAdmin(ss db.DB, ds dashboards.DashboardService, ts team.Service) func(c *contextmodel.ReqContext) { + return func(c *contextmodel.ReqContext) { if c.OrgRole == org.RoleAdmin { return } diff --git a/pkg/middleware/auth_test.go b/pkg/middleware/auth_test.go index bcef0a863e6..9e12a2ffd64 100644 --- a/pkg/middleware/auth_test.go +++ b/pkg/middleware/auth_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/setting" ) @@ -87,7 +87,7 @@ func TestMiddlewareAuth(t *testing.T) { middlewareScenario(t, "Snapshot public mode disabled and unauthenticated request should return 401", func( t *testing.T, sc *scenarioContext) { - sc.m.Get("/api/snapshot", func(c *models.ReqContext) { + sc.m.Get("/api/snapshot", func(c *contextmodel.ReqContext) { c.IsSignedIn = false }, SnapshotPublicModeOrSignedIn(sc.cfg), sc.defaultHandler) sc.fakeReq("GET", "/api/snapshot").exec() @@ -96,7 +96,7 @@ func TestMiddlewareAuth(t *testing.T) { middlewareScenario(t, "Snapshot public mode disabled and authenticated request should return 200", func( t *testing.T, sc *scenarioContext) { - sc.m.Get("/api/snapshot", func(c *models.ReqContext) { + sc.m.Get("/api/snapshot", func(c *contextmodel.ReqContext) { c.IsSignedIn = true }, SnapshotPublicModeOrSignedIn(sc.cfg), sc.defaultHandler) sc.fakeReq("GET", "/api/snapshot").exec() diff --git a/pkg/middleware/cookies/cookies.go b/pkg/middleware/cookies/cookies.go index 3d068cff2aa..915e396a8e6 100644 --- a/pkg/middleware/cookies/cookies.go +++ b/pkg/middleware/cookies/cookies.go @@ -5,7 +5,7 @@ import ( "net/url" "time" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/setting" ) @@ -55,7 +55,7 @@ func WriteCookie(w http.ResponseWriter, name string, value string, maxAge int, g http.SetCookie(w, &cookie) } -func WriteSessionCookie(ctx *models.ReqContext, cfg *setting.Cfg, value string, maxLifetime time.Duration) { +func WriteSessionCookie(ctx *contextmodel.ReqContext, cfg *setting.Cfg, value string, maxLifetime time.Duration) { if cfg.Env == setting.Dev { ctx.Logger.Info("New token", "unhashed token", value) } diff --git a/pkg/middleware/dashboard_redirect.go b/pkg/middleware/dashboard_redirect.go index 496f8243e81..5409cfa2179 100644 --- a/pkg/middleware/dashboard_redirect.go +++ b/pkg/middleware/dashboard_redirect.go @@ -4,14 +4,14 @@ import ( "fmt" "strings" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/setting" ) // In Grafana v7.0 we changed panel edit & view query parameters. // This middleware tries to detect those old url parameters and direct to the new url query params -func RedirectFromLegacyPanelEditURL(cfg *setting.Cfg) func(c *models.ReqContext) { - return func(c *models.ReqContext) { +func RedirectFromLegacyPanelEditURL(cfg *setting.Cfg) func(c *contextmodel.ReqContext) { + return func(c *contextmodel.ReqContext) { queryParams := c.Req.URL.Query() panelID, hasPanelID := queryParams["panelId"] diff --git a/pkg/middleware/logger.go b/pkg/middleware/logger.go index 4366ca45e45..83d78b82ec9 100644 --- a/pkg/middleware/logger.go +++ b/pkg/middleware/logger.go @@ -21,8 +21,8 @@ import ( "time" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/contexthandler" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" @@ -89,7 +89,7 @@ var sensitiveQueryStrings = [...]string{ "auth_token", } -func SanitizeURL(ctx *models.ReqContext, s string) string { +func SanitizeURL(ctx *contextmodel.ReqContext, s string) string { if s == "" { return s } diff --git a/pkg/middleware/logger_test.go b/pkg/middleware/logger_test.go index 1b74bb7246c..fd17c87787e 100644 --- a/pkg/middleware/logger_test.go +++ b/pkg/middleware/logger_test.go @@ -4,13 +4,13 @@ import ( "testing" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/stretchr/testify/assert" ) func Test_sanitizeURL(t *testing.T) { type args struct { - ctx *models.ReqContext + ctx *contextmodel.ReqContext s string } tests := []struct { @@ -21,7 +21,7 @@ func Test_sanitizeURL(t *testing.T) { { name: "Receiving empty string should return it", args: args{ - ctx: &models.ReqContext{ + ctx: &contextmodel.ReqContext{ Logger: log.New("test.logger"), }, s: "", @@ -31,7 +31,7 @@ func Test_sanitizeURL(t *testing.T) { { name: "Receiving valid URL string should return it parsed", args: args{ - ctx: &models.ReqContext{ + ctx: &contextmodel.ReqContext{ Logger: log.New("test.logger"), }, s: "https://grafana.com/", @@ -41,7 +41,7 @@ func Test_sanitizeURL(t *testing.T) { { name: "Receiving invalid URL string should return empty string", args: args{ - ctx: &models.ReqContext{ + ctx: &contextmodel.ReqContext{ Logger: log.New("test.logger"), }, s: "this is not a valid URL", diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 9bb6c6cb835..2bc0893e5b7 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" @@ -21,7 +21,7 @@ var ( ReqOrgAdmin = RoleAuth(org.RoleAdmin) ) -func HandleNoCacheHeader(ctx *models.ReqContext) { +func HandleNoCacheHeader(ctx *contextmodel.ReqContext) { ctx.SkipCache = ctx.Req.Header.Get("X-Grafana-NoCache") == "true" } diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 8ee2ad64a19..5aeda6a8a83 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -33,6 +33,7 @@ import ( "github.com/grafana/grafana/pkg/services/authn/authntest" "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/contexthandler/authproxy" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/login/loginservice" "github.com/grafana/grafana/pkg/services/login/logintest" @@ -162,7 +163,7 @@ func TestMiddlewareContext(t *testing.T) { middlewareScenario(t, "middleware should add Cache-Control header for requests with HTML response", func( t *testing.T, sc *scenarioContext) { - sc.handlerFunc = func(c *models.ReqContext) { + sc.handlerFunc = func(c *contextmodel.ReqContext) { t.Log("Handler called") data := &dtos.IndexViewData{ User: &dtos.CurrentUser{}, @@ -721,7 +722,7 @@ func TestMiddlewareContext(t *testing.T) { body := "key=value" sc.req.Body = io.NopCloser(strings.NewReader(body)) - sc.handlerFunc = func(c *models.ReqContext) { + sc.handlerFunc = func(c *contextmodel.ReqContext) { t.Log("Handler called") defer func() { err := c.Req.Body.Close() @@ -745,7 +746,7 @@ func TestMiddlewareContext(t *testing.T) { body := "key=value" sc.req.Body = io.NopCloser(strings.NewReader(body)) - sc.handlerFunc = func(c *models.ReqContext) { + sc.handlerFunc = func(c *contextmodel.ReqContext) { t.Log("Handler called") defer func() { err := c.Req.Body.Close() @@ -889,7 +890,7 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc, cbs ...func( sc.jwtAuthService = ctxHdlr.JWTAuthService.(*jwt.FakeJWTService) sc.remoteCacheService = ctxHdlr.RemoteCache - sc.defaultHandler = func(c *models.ReqContext) { + sc.defaultHandler = func(c *contextmodel.ReqContext) { require.NotNil(t, c) t.Log("Default HTTP handler called") sc.context = c diff --git a/pkg/middleware/quota.go b/pkg/middleware/quota.go index 7a0689ff11d..31568e7fd5a 100644 --- a/pkg/middleware/quota.go +++ b/pkg/middleware/quota.go @@ -3,7 +3,7 @@ package middleware import ( "fmt" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/web" ) @@ -15,7 +15,7 @@ func Quota(quotaService quota.Service) func(string) web.Handler { } //https://open.spotify.com/track/7bZSoBEAEEUsGEuLOf94Jm?si=T1Tdju5qRSmmR0zph_6RBw fuuuuunky return func(targetSrv string) web.Handler { - return func(c *models.ReqContext) { + return func(c *contextmodel.ReqContext) { limitReached, err := quotaService.QuotaReached(c, quota.TargetSrv(targetSrv)) if err != nil { c.JsonApiErr(500, "Failed to get quota", err) diff --git a/pkg/middleware/recovery_test.go b/pkg/middleware/recovery_test.go index 1a8fe8537c5..8a4251c7917 100644 --- a/pkg/middleware/recovery_test.go +++ b/pkg/middleware/recovery_test.go @@ -9,8 +9,8 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/remotecache" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth/authtest" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" ) @@ -42,7 +42,7 @@ func TestRecoveryMiddleware(t *testing.T) { }) } -func panicHandler(c *models.ReqContext) { +func panicHandler(c *contextmodel.ReqContext) { panic("Handler has panicked") } @@ -73,7 +73,7 @@ func recoveryScenario(t *testing.T, desc string, url string, fn scenarioFunc) { // mock out gc goroutine sc.m.Use(OrgRedirect(cfg, sc.userService)) - sc.defaultHandler = func(c *models.ReqContext) { + sc.defaultHandler = func(c *contextmodel.ReqContext) { sc.context = c if sc.handlerFunc != nil { sc.handlerFunc(sc.context) diff --git a/pkg/middleware/testing.go b/pkg/middleware/testing.go index 2c27fcf7ae2..6caa397c4e8 100644 --- a/pkg/middleware/testing.go +++ b/pkg/middleware/testing.go @@ -11,12 +11,12 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/remotecache" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/apikey/apikeytest" "github.com/grafana/grafana/pkg/services/auth/authtest" "github.com/grafana/grafana/pkg/services/auth/jwt" "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/login/loginservice" "github.com/grafana/grafana/pkg/services/org/orgtest" "github.com/grafana/grafana/pkg/services/user/usertest" @@ -27,7 +27,7 @@ import ( type scenarioContext struct { t *testing.T m *web.Mux - context *models.ReqContext + context *contextmodel.ReqContext resp *httptest.ResponseRecorder apiKey string authHeader string @@ -80,7 +80,7 @@ func (sc *scenarioContext) fakeReq(method, url string) *scenarioContext { req, err := http.NewRequest(method, url, nil) require.NoError(sc.t, err) - reqCtx := &models.ReqContext{ + reqCtx := &contextmodel.ReqContext{ Context: web.FromContext(req.Context()), } sc.req = req.WithContext(ctxkey.Set(req.Context(), reqCtx)) @@ -102,7 +102,7 @@ func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map req.URL.RawQuery = q.Encode() require.NoError(sc.t, err) - reqCtx := &models.ReqContext{ + reqCtx := &contextmodel.ReqContext{ Context: web.FromContext(req.Context()), } sc.req = req.WithContext(ctxkey.Set(req.Context(), reqCtx)) @@ -147,4 +147,4 @@ func (sc *scenarioContext) exec() { } type scenarioFunc func(t *testing.T, c *scenarioContext) -type handlerFunc func(c *models.ReqContext) +type handlerFunc func(c *contextmodel.ReqContext) diff --git a/pkg/middleware/validate_host.go b/pkg/middleware/validate_host.go index bf9d2db9c3d..3b75552cd58 100644 --- a/pkg/middleware/validate_host.go +++ b/pkg/middleware/validate_host.go @@ -3,13 +3,13 @@ package middleware import ( "strings" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" ) func ValidateHostHeader(cfg *setting.Cfg) web.Handler { - return func(c *models.ReqContext) { + return func(c *contextmodel.ReqContext) { // ignore local render calls if c.IsRenderCall { return diff --git a/pkg/models/user_auth.go b/pkg/models/user_auth.go index ec1f70e75a0..bebdfdccf61 100644 --- a/pkg/models/user_auth.go +++ b/pkg/models/user_auth.go @@ -4,6 +4,7 @@ import ( "fmt" "time" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -59,7 +60,7 @@ type RequestURIKey struct{} // COMMANDS type UpsertUserCommand struct { - ReqContext *ReqContext + ReqContext *contextmodel.ReqContext ExternalUser *ExternalUserInfo UserLookupParams SignupAllowed bool @@ -89,7 +90,7 @@ type DeleteAuthInfoCommand struct { // QUERIES type LoginUserQuery struct { - ReqContext *ReqContext + ReqContext *contextmodel.ReqContext Username string Password string User *user.User diff --git a/pkg/plugins/accesscontrol.go b/pkg/plugins/accesscontrol.go index fef15b2404b..cc8c1b59e65 100644 --- a/pkg/plugins/accesscontrol.go +++ b/pkg/plugins/accesscontrol.go @@ -1,8 +1,8 @@ package plugins import ( - "github.com/grafana/grafana/pkg/models" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/setting" ) @@ -22,9 +22,9 @@ var ( AdminAccessEvaluator = ac.EvalAny(ac.EvalPermission(ActionWrite), ac.EvalPermission(ActionInstall)) ) -func ReqCanAdminPlugins(cfg *setting.Cfg) func(rc *models.ReqContext) bool { +func ReqCanAdminPlugins(cfg *setting.Cfg) func(rc *contextmodel.ReqContext) bool { // Legacy handler that protects access to the Configuration > Plugins page - return func(rc *models.ReqContext) bool { + return func(rc *contextmodel.ReqContext) bool { return rc.OrgRole == org.RoleAdmin || cfg.PluginAdminEnabled && rc.IsGrafanaAdmin } } diff --git a/pkg/plugins/manager/client/clienttest/clienttest.go b/pkg/plugins/manager/client/clienttest/clienttest.go index f88d07d3376..64dcd0a78e1 100644 --- a/pkg/plugins/manager/client/clienttest/clienttest.go +++ b/pkg/plugins/manager/client/clienttest/clienttest.go @@ -7,10 +7,10 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/manager/client" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/web" "github.com/stretchr/testify/require" @@ -130,7 +130,7 @@ type ClientDecoratorTest struct { TestClient *TestClient Middlewares []plugins.ClientMiddleware Decorator *client.Decorator - ReqContext *models.ReqContext + ReqContext *contextmodel.ReqContext QueryDataReq *backend.QueryDataRequest QueryDataCtx context.Context CallResourceReq *backend.CallResourceRequest @@ -181,7 +181,7 @@ func NewClientDecoratorTest(t *testing.T, opts ...ClientDecoratorTestOption) *Cl func WithReqContext(req *http.Request, user *user.SignedInUser) ClientDecoratorTestOption { return ClientDecoratorTestOption(func(cdt *ClientDecoratorTest) { if cdt.ReqContext == nil { - cdt.ReqContext = &models.ReqContext{ + cdt.ReqContext = &contextmodel.ReqContext{ Context: &web.Context{}, SignedInUser: user, } diff --git a/pkg/services/accesscontrol/accesscontrol.go b/pkg/services/accesscontrol/accesscontrol.go index ef2ca546a57..16ca6324ee0 100644 --- a/pkg/services/accesscontrol/accesscontrol.go +++ b/pkg/services/accesscontrol/accesscontrol.go @@ -5,8 +5,8 @@ import ( "fmt" "strings" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/registry" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -100,8 +100,8 @@ type User struct { } // HasGlobalAccess checks user access with globally assigned permissions only -func HasGlobalAccess(ac AccessControl, service Service, c *models.ReqContext) func(fallback func(*models.ReqContext) bool, evaluator Evaluator) bool { - return func(fallback func(*models.ReqContext) bool, evaluator Evaluator) bool { +func HasGlobalAccess(ac AccessControl, service Service, c *contextmodel.ReqContext) func(fallback func(*contextmodel.ReqContext) bool, evaluator Evaluator) bool { + return func(fallback func(*contextmodel.ReqContext) bool, evaluator Evaluator) bool { if ac.IsDisabled() { return fallback(c) } @@ -131,8 +131,8 @@ func HasGlobalAccess(ac AccessControl, service Service, c *models.ReqContext) fu } } -func HasAccess(ac AccessControl, c *models.ReqContext) func(fallback func(*models.ReqContext) bool, evaluator Evaluator) bool { - return func(fallback func(*models.ReqContext) bool, evaluator Evaluator) bool { +func HasAccess(ac AccessControl, c *contextmodel.ReqContext) func(fallback func(*contextmodel.ReqContext) bool, evaluator Evaluator) bool { + return func(fallback func(*contextmodel.ReqContext) bool, evaluator Evaluator) bool { if ac.IsDisabled() { return fallback(c) } @@ -147,31 +147,31 @@ func HasAccess(ac AccessControl, c *models.ReqContext) func(fallback func(*model } } -var ReqSignedIn = func(c *models.ReqContext) bool { +var ReqSignedIn = func(c *contextmodel.ReqContext) bool { return c.IsSignedIn } -var ReqGrafanaAdmin = func(c *models.ReqContext) bool { +var ReqGrafanaAdmin = func(c *contextmodel.ReqContext) bool { return c.IsGrafanaAdmin } // ReqViewer returns true if the current user has org.RoleViewer. Note: this can be anonymous user as well -var ReqViewer = func(c *models.ReqContext) bool { +var ReqViewer = func(c *contextmodel.ReqContext) bool { return c.OrgRole.Includes(org.RoleViewer) } -var ReqOrgAdmin = func(c *models.ReqContext) bool { +var ReqOrgAdmin = func(c *contextmodel.ReqContext) bool { return c.OrgRole == org.RoleAdmin } -var ReqOrgAdminOrEditor = func(c *models.ReqContext) bool { +var ReqOrgAdminOrEditor = func(c *contextmodel.ReqContext) bool { return c.OrgRole == org.RoleAdmin || c.OrgRole == org.RoleEditor } // ReqHasRole generates a fallback to check whether the user has a role // Note that while ReqOrgAdmin returns false for a Grafana Admin / Viewer, ReqHasRole(org.RoleAdmin) will return true -func ReqHasRole(role org.RoleType) func(c *models.ReqContext) bool { - return func(c *models.ReqContext) bool { return c.HasRole(role) } +func ReqHasRole(role org.RoleType) func(c *contextmodel.ReqContext) bool { + return func(c *contextmodel.ReqContext) bool { return c.HasRole(role) } } func BuildPermissionsMap(permissions []Permission) map[string]bool { diff --git a/pkg/services/accesscontrol/api/api.go b/pkg/services/accesscontrol/api/api.go index a3d279e103f..b0f620dd31b 100644 --- a/pkg/services/accesscontrol/api/api.go +++ b/pkg/services/accesscontrol/api/api.go @@ -7,8 +7,8 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/web" ) @@ -47,7 +47,7 @@ func (api *AccessControlAPI) RegisterAPIEndpoints() { } // GET /api/access-control/user/actions -func (api *AccessControlAPI) getUserActions(c *models.ReqContext) response.Response { +func (api *AccessControlAPI) getUserActions(c *contextmodel.ReqContext) response.Response { reloadCache := c.QueryBool("reloadcache") permissions, err := api.Service.GetUserPermissions(c.Req.Context(), c.SignedInUser, ac.Options{ReloadCache: reloadCache}) @@ -59,7 +59,7 @@ func (api *AccessControlAPI) getUserActions(c *models.ReqContext) response.Respo } // GET /api/access-control/user/permissions -func (api *AccessControlAPI) getUserPermissions(c *models.ReqContext) response.Response { +func (api *AccessControlAPI) getUserPermissions(c *contextmodel.ReqContext) response.Response { reloadCache := c.QueryBool("reloadcache") permissions, err := api.Service.GetUserPermissions(c.Req.Context(), c.SignedInUser, ac.Options{ReloadCache: reloadCache}) @@ -71,7 +71,7 @@ func (api *AccessControlAPI) getUserPermissions(c *models.ReqContext) response.R } // GET /api/access-control/users/permissions -func (api *AccessControlAPI) searchUsersPermissions(c *models.ReqContext) response.Response { +func (api *AccessControlAPI) searchUsersPermissions(c *contextmodel.ReqContext) response.Response { searchOptions := ac.SearchOptions{ ActionPrefix: c.Query("actionPrefix"), Action: c.Query("action"), @@ -98,7 +98,7 @@ func (api *AccessControlAPI) searchUsersPermissions(c *models.ReqContext) respon } // GET /api/access-control/user/:userID/permissions/search -func (api *AccessControlAPI) searchUserPermissions(c *models.ReqContext) response.Response { +func (api *AccessControlAPI) searchUserPermissions(c *contextmodel.ReqContext) response.Response { userIDString := web.Params(c.Req)[":userID"] userID, err := strconv.ParseInt(userIDString, 10, 64) if err != nil { diff --git a/pkg/services/accesscontrol/middleware.go b/pkg/services/accesscontrol/middleware.go index 888fc2a615d..2c3f1299f59 100644 --- a/pkg/services/accesscontrol/middleware.go +++ b/pkg/services/accesscontrol/middleware.go @@ -14,8 +14,8 @@ import ( "time" "github.com/grafana/grafana/pkg/middleware/cookies" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models/usertoken" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -29,7 +29,7 @@ func Middleware(ac AccessControl) func(web.Handler, Evaluator) web.Handler { return fallback } - return func(c *models.ReqContext) { + return func(c *contextmodel.ReqContext) { if c.AllowAnonymous { forceLogin, _ := strconv.ParseBool(c.Req.URL.Query().Get("forceLogin")) // ignoring error, assuming false for non-true values is ok. orgID, err := strconv.ParseInt(c.Req.URL.Query().Get("orgId"), 10, 64) @@ -53,7 +53,7 @@ func Middleware(ac AccessControl) func(web.Handler, Evaluator) web.Handler { } } -func authorize(c *models.ReqContext, ac AccessControl, user *user.SignedInUser, evaluator Evaluator) { +func authorize(c *contextmodel.ReqContext, ac AccessControl, user *user.SignedInUser, evaluator Evaluator) { injected, err := evaluator.MutateScopes(c.Req.Context(), scopeInjector(scopeParams{ OrgID: c.OrgID, URLParams: web.Params(c.Req), @@ -70,7 +70,7 @@ func authorize(c *models.ReqContext, ac AccessControl, user *user.SignedInUser, } } -func deny(c *models.ReqContext, evaluator Evaluator, err error) { +func deny(c *contextmodel.ReqContext, evaluator Evaluator, err error) { id := newID() if err != nil { c.Logger.Error("Error from access control system", "error", err, "accessErrorID", id) @@ -106,7 +106,7 @@ func deny(c *models.ReqContext, evaluator Evaluator, err error) { }) } -func unauthorized(c *models.ReqContext, err error) { +func unauthorized(c *contextmodel.ReqContext, err error) { if c.IsApiRequest() { response := map[string]interface{}{ "message": "Unauthorized", @@ -129,7 +129,7 @@ func unauthorized(c *models.ReqContext, err error) { c.Redirect(setting.AppSubUrl + "/login") } -func writeRedirectCookie(c *models.ReqContext) { +func writeRedirectCookie(c *contextmodel.ReqContext) { redirectTo := c.Req.RequestURI if setting.AppSubUrl != "" && !strings.HasPrefix(redirectTo, setting.AppSubUrl) { redirectTo = setting.AppSubUrl + c.Req.RequestURI @@ -159,7 +159,7 @@ func newID() string { return "ACE" + id } -type OrgIDGetter func(c *models.ReqContext) (int64, error) +type OrgIDGetter func(c *contextmodel.ReqContext) (int64, error) type userCache interface { GetSignedInUserWithCacheCtx(ctx context.Context, query *user.GetSignedInUserQuery) (*user.SignedInUser, error) @@ -171,7 +171,7 @@ func AuthorizeInOrgMiddleware(ac AccessControl, service Service, cache userCache return fallback } - return func(c *models.ReqContext) { + return func(c *contextmodel.ReqContext) { // using a copy of the user not to modify the signedInUser, yet perform the permission evaluation in another org userCopy := *(c.SignedInUser) orgID, err := getTargetOrg(c) @@ -211,7 +211,7 @@ func AuthorizeInOrgMiddleware(ac AccessControl, service Service, cache userCache } } -func UseOrgFromContextParams(c *models.ReqContext) (int64, error) { +func UseOrgFromContextParams(c *contextmodel.ReqContext) (int64, error) { orgID, err := strconv.ParseInt(web.Params(c.Req)[":orgId"], 10, 64) // Special case of macaron handling invalid params @@ -222,12 +222,12 @@ func UseOrgFromContextParams(c *models.ReqContext) (int64, error) { return orgID, nil } -func UseGlobalOrg(c *models.ReqContext) (int64, error) { +func UseGlobalOrg(c *contextmodel.ReqContext) (int64, error) { return GlobalOrgID, nil } func LoadPermissionsMiddleware(service Service) web.Handler { - return func(c *models.ReqContext) { + return func(c *contextmodel.ReqContext) { if service.IsDisabled() { return } diff --git a/pkg/services/accesscontrol/middleware_test.go b/pkg/services/accesscontrol/middleware_test.go index 7ba3e180126..c9f3626058c 100644 --- a/pkg/services/accesscontrol/middleware_test.go +++ b/pkg/services/accesscontrol/middleware_test.go @@ -8,10 +8,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/web" ) @@ -55,7 +55,7 @@ func TestMiddleware(t *testing.T) { for _, test := range tests { t.Run(test.desc, func(t *testing.T) { fallbackCalled := false - fallback := func(c *models.ReqContext) { + fallback := func(c *contextmodel.ReqContext) { fallbackCalled = true } @@ -66,7 +66,7 @@ func TestMiddleware(t *testing.T) { server.Use(accesscontrol.Middleware(test.ac)(fallback, test.evaluator)) endpointCalled := false - server.Get("/", func(c *models.ReqContext) { + server.Get("/", func(c *contextmodel.ReqContext) { endpointCalled = true c.Resp.WriteHeader(http.StatusOK) }) @@ -99,13 +99,13 @@ func TestMiddleware_forceLogin(t *testing.T) { server := web.New() server.UseMiddleware(web.Renderer("../../public/views", "[[", "]]")) - server.Get("/endpoint", func(c *models.ReqContext) { + server.Get("/endpoint", func(c *contextmodel.ReqContext) { endpointCalled = true c.Resp.WriteHeader(http.StatusOK) }) ac := mock.New().WithPermissions([]accesscontrol.Permission{{Action: "endpoint:read", Scope: "endpoint:1"}}) - server.Use(contextProvider(func(c *models.ReqContext) { + server.Use(contextProvider(func(c *contextmodel.ReqContext) { c.AllowAnonymous = true c.SignedInUser.IsAnonymous = true c.IsSignedIn = false @@ -129,9 +129,9 @@ func TestMiddleware_forceLogin(t *testing.T) { } } -func contextProvider(modifiers ...func(c *models.ReqContext)) web.Handler { +func contextProvider(modifiers ...func(c *contextmodel.ReqContext)) web.Handler { return func(c *web.Context) { - reqCtx := &models.ReqContext{ + reqCtx := &contextmodel.ReqContext{ Context: c, Logger: log.New(""), SignedInUser: &user.SignedInUser{}, diff --git a/pkg/services/accesscontrol/resourcepermissions/api.go b/pkg/services/accesscontrol/resourcepermissions/api.go index 094221df7e6..71a0a136aa8 100644 --- a/pkg/services/accesscontrol/resourcepermissions/api.go +++ b/pkg/services/accesscontrol/resourcepermissions/api.go @@ -8,8 +8,8 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/web" ) @@ -68,7 +68,7 @@ type Description struct { Permissions []string `json:"permissions"` } -func (a *api) getDescription(c *models.ReqContext) response.Response { +func (a *api) getDescription(c *contextmodel.ReqContext) response.Response { return response.JSON(http.StatusOK, &Description{ Permissions: a.permissions, Assignments: a.service.options.Assignments, @@ -91,7 +91,7 @@ type resourcePermissionDTO struct { Permission string `json:"permission"` } -func (a *api) getPermissions(c *models.ReqContext) response.Response { +func (a *api) getPermissions(c *contextmodel.ReqContext) response.Response { resourceID := web.Params(c.Req)[":resourceID"] permissions, err := a.service.GetPermissions(c.Req.Context(), c.SignedInUser, resourceID) @@ -144,7 +144,7 @@ type setPermissionsCommand struct { Permissions []accesscontrol.SetResourcePermissionCommand `json:"permissions"` } -func (a *api) setUserPermission(c *models.ReqContext) response.Response { +func (a *api) setUserPermission(c *contextmodel.ReqContext) response.Response { userID, err := strconv.ParseInt(web.Params(c.Req)[":userID"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "userID is invalid", err) @@ -164,7 +164,7 @@ func (a *api) setUserPermission(c *models.ReqContext) response.Response { return permissionSetResponse(cmd) } -func (a *api) setTeamPermission(c *models.ReqContext) response.Response { +func (a *api) setTeamPermission(c *contextmodel.ReqContext) response.Response { teamID, err := strconv.ParseInt(web.Params(c.Req)[":teamID"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "teamID is invalid", err) @@ -184,7 +184,7 @@ func (a *api) setTeamPermission(c *models.ReqContext) response.Response { return permissionSetResponse(cmd) } -func (a *api) setBuiltinRolePermission(c *models.ReqContext) response.Response { +func (a *api) setBuiltinRolePermission(c *contextmodel.ReqContext) response.Response { builtInRole := web.Params(c.Req)[":builtInRole"] resourceID := web.Params(c.Req)[":resourceID"] @@ -201,7 +201,7 @@ func (a *api) setBuiltinRolePermission(c *models.ReqContext) response.Response { return permissionSetResponse(cmd) } -func (a *api) setPermissions(c *models.ReqContext) response.Response { +func (a *api) setPermissions(c *contextmodel.ReqContext) response.Response { resourceID := web.Params(c.Req)[":resourceID"] cmd := setPermissionsCommand{} diff --git a/pkg/services/accesscontrol/resourcepermissions/api_test.go b/pkg/services/accesscontrol/resourcepermissions/api_test.go index 955f30a1189..06a918738af 100644 --- a/pkg/services/accesscontrol/resourcepermissions/api_test.go +++ b/pkg/services/accesscontrol/resourcepermissions/api_test.go @@ -15,9 +15,9 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/sqlstore" @@ -444,7 +444,7 @@ type testContext struct { func contextProvider(tc *testContext) web.Handler { return func(c *web.Context) { signedIn := tc.user != nil - reqCtx := &models.ReqContext{ + reqCtx := &contextmodel.ReqContext{ Context: c, SignedInUser: tc.user, IsSignedIn: signedIn, diff --git a/pkg/services/accesscontrol/resourcepermissions/middleware.go b/pkg/services/accesscontrol/resourcepermissions/middleware.go index fead18fb9b8..7acb80d3392 100644 --- a/pkg/services/accesscontrol/resourcepermissions/middleware.go +++ b/pkg/services/accesscontrol/resourcepermissions/middleware.go @@ -3,12 +3,12 @@ package resourcepermissions import ( "net/http" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/web" ) func disableMiddleware(shouldDisable bool) web.Handler { - return func(c *models.ReqContext) { + return func(c *contextmodel.ReqContext) { if shouldDisable { c.Resp.WriteHeader(http.StatusNotFound) return @@ -16,4 +16,4 @@ func disableMiddleware(shouldDisable bool) web.Handler { } } -func nopMiddleware(c *models.ReqContext) {} +func nopMiddleware(c *contextmodel.ReqContext) {} diff --git a/pkg/services/contexthandler/auth_jwt.go b/pkg/services/contexthandler/auth_jwt.go index 9944d240bae..68558418a4d 100644 --- a/pkg/services/contexthandler/auth_jwt.go +++ b/pkg/services/contexthandler/auth_jwt.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models/roletype" authJWT "github.com/grafana/grafana/pkg/services/auth/jwt" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" ) @@ -22,7 +23,7 @@ const ( UserNotFound = "User not found" ) -func (h *ContextHandler) initContextWithJWT(ctx *models.ReqContext, orgId int64) bool { +func (h *ContextHandler) initContextWithJWT(ctx *contextmodel.ReqContext, orgId int64) bool { if !h.Cfg.JWTAuthEnabled || h.Cfg.JWTAuthHeaderName == "" { return false } diff --git a/pkg/services/contexthandler/auth_proxy_test.go b/pkg/services/contexthandler/auth_proxy_test.go index 2f409ff256d..55cf1ada314 100644 --- a/pkg/services/contexthandler/auth_proxy_test.go +++ b/pkg/services/contexthandler/auth_proxy_test.go @@ -17,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/services/auth/jwt" "github.com/grafana/grafana/pkg/services/authn/authntest" "github.com/grafana/grafana/pkg/services/contexthandler/authproxy" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/login/loginservice" "github.com/grafana/grafana/pkg/services/org/orgtest" @@ -42,7 +43,7 @@ func TestInitContextWithAuthProxy_CachedInvalidUserID(t *testing.T) { req, err := http.NewRequest("POST", "http://example.com", nil) require.NoError(t, err) - ctx := &models.ReqContext{ + ctx := &contextmodel.ReqContext{ Context: &web.Context{Req: req}, Logger: log.New("Test"), } diff --git a/pkg/services/contexthandler/authproxy/authproxy.go b/pkg/services/contexthandler/authproxy/authproxy.go index 73c9523d50a..1cf0a7993d1 100644 --- a/pkg/services/contexthandler/authproxy/authproxy.go +++ b/pkg/services/contexthandler/authproxy/authproxy.go @@ -17,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/remotecache" "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/multildap" @@ -98,7 +99,7 @@ func (auth *AuthProxy) IsEnabled() bool { } // HasHeader checks if we have specified header -func (auth *AuthProxy) HasHeader(reqCtx *models.ReqContext) bool { +func (auth *AuthProxy) HasHeader(reqCtx *contextmodel.ReqContext) bool { header := auth.getDecodedHeader(reqCtx, auth.cfg.AuthProxyHeaderName) return len(header) != 0 } @@ -149,7 +150,7 @@ func HashCacheKey(key string) (string, error) { // getKey forms a key for the cache based on the headers received as part of the authentication flow. // Our configuration supports multiple headers. The main header contains the email or username. // And the additional ones that allow us to specify extra attributes: Name, Email, Role, or Groups. -func (auth *AuthProxy) getKey(reqCtx *models.ReqContext) (string, error) { +func (auth *AuthProxy) getKey(reqCtx *contextmodel.ReqContext) (string, error) { header := auth.getDecodedHeader(reqCtx, auth.cfg.AuthProxyHeaderName) key := strings.TrimSpace(header) // start the key with the main header @@ -165,7 +166,7 @@ func (auth *AuthProxy) getKey(reqCtx *models.ReqContext) (string, error) { } // Login logs in user ID by whatever means possible. -func (auth *AuthProxy) Login(reqCtx *models.ReqContext, ignoreCache bool) (int64, error) { +func (auth *AuthProxy) Login(reqCtx *contextmodel.ReqContext, ignoreCache bool) (int64, error) { if !ignoreCache { // Error here means absent cache - we don't need to handle that id, err := auth.getUserViaCache(reqCtx) @@ -195,7 +196,7 @@ func (auth *AuthProxy) Login(reqCtx *models.ReqContext, ignoreCache bool) (int64 } // getUserViaCache gets user ID from cache. -func (auth *AuthProxy) getUserViaCache(reqCtx *models.ReqContext) (int64, error) { +func (auth *AuthProxy) getUserViaCache(reqCtx *contextmodel.ReqContext) (int64, error) { cacheKey, err := auth.getKey(reqCtx) if err != nil { return 0, err @@ -212,7 +213,7 @@ func (auth *AuthProxy) getUserViaCache(reqCtx *models.ReqContext) (int64, error) } // RemoveUserFromCache removes user from cache. -func (auth *AuthProxy) RemoveUserFromCache(reqCtx *models.ReqContext) error { +func (auth *AuthProxy) RemoveUserFromCache(reqCtx *contextmodel.ReqContext) error { cacheKey, err := auth.getKey(reqCtx) if err != nil { return err @@ -227,7 +228,7 @@ func (auth *AuthProxy) RemoveUserFromCache(reqCtx *models.ReqContext) error { } // LoginViaLDAP logs in user via LDAP request -func (auth *AuthProxy) LoginViaLDAP(reqCtx *models.ReqContext) (int64, error) { +func (auth *AuthProxy) LoginViaLDAP(reqCtx *contextmodel.ReqContext) (int64, error) { config, err := getLDAPConfig(auth.cfg) if err != nil { return 0, newError("failed to get LDAP config", err) @@ -259,7 +260,7 @@ func (auth *AuthProxy) LoginViaLDAP(reqCtx *models.ReqContext) (int64, error) { } // loginViaHeader logs in user from the header only -func (auth *AuthProxy) loginViaHeader(reqCtx *models.ReqContext) (int64, error) { +func (auth *AuthProxy) loginViaHeader(reqCtx *contextmodel.ReqContext) (int64, error) { header := auth.getDecodedHeader(reqCtx, auth.cfg.AuthProxyHeaderName) extUser := &models.ExternalUserInfo{ AuthModule: login.AuthProxyAuthModule, @@ -323,7 +324,7 @@ func (auth *AuthProxy) loginViaHeader(reqCtx *models.ReqContext) (int64, error) } // getDecodedHeader gets decoded value of a header with given headerName -func (auth *AuthProxy) getDecodedHeader(reqCtx *models.ReqContext, headerName string) string { +func (auth *AuthProxy) getDecodedHeader(reqCtx *contextmodel.ReqContext, headerName string) string { headerValue := reqCtx.Req.Header.Get(headerName) if auth.cfg.AuthProxyHeadersEncoded { @@ -334,7 +335,7 @@ func (auth *AuthProxy) getDecodedHeader(reqCtx *models.ReqContext, headerName st } // headersIterator iterates over all non-empty supported additional headers -func (auth *AuthProxy) headersIterator(reqCtx *models.ReqContext, fn func(field string, header string)) { +func (auth *AuthProxy) headersIterator(reqCtx *contextmodel.ReqContext, fn func(field string, header string)) { for _, field := range supportedHeaderFields { h := auth.cfg.AuthProxyHeaders[field] if h == "" { @@ -356,7 +357,7 @@ func (auth *AuthProxy) GetSignedInUser(userID int64, orgID int64) (*user.SignedI } // Remember user in cache -func (auth *AuthProxy) Remember(reqCtx *models.ReqContext, id int64) error { +func (auth *AuthProxy) Remember(reqCtx *contextmodel.ReqContext, id int64) error { key, err := auth.getKey(reqCtx) if err != nil { return err diff --git a/pkg/services/contexthandler/authproxy/authproxy_test.go b/pkg/services/contexthandler/authproxy/authproxy_test.go index 736776cc94d..a646a01e68f 100644 --- a/pkg/services/contexthandler/authproxy/authproxy_test.go +++ b/pkg/services/contexthandler/authproxy/authproxy_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/remotecache" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/login/loginservice" "github.com/grafana/grafana/pkg/services/multildap" @@ -23,7 +23,7 @@ import ( const hdrName = "markelog" const id int64 = 42 -func prepareMiddleware(t *testing.T, remoteCache *remotecache.RemoteCache, configureReq func(*http.Request, *setting.Cfg)) (*AuthProxy, *models.ReqContext) { +func prepareMiddleware(t *testing.T, remoteCache *remotecache.RemoteCache, configureReq func(*http.Request, *setting.Cfg)) (*AuthProxy, *contextmodel.ReqContext) { t.Helper() req, err := http.NewRequest("POST", "http://example.com", nil) @@ -38,7 +38,7 @@ func prepareMiddleware(t *testing.T, remoteCache *remotecache.RemoteCache, confi req.Header.Set(cfg.AuthProxyHeaderName, hdrName) } - ctx := &models.ReqContext{ + ctx := &contextmodel.ReqContext{ Context: &web.Context{Req: req}, } diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index d9351fca9f4..034cc80bcc0 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -27,6 +27,7 @@ import ( "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/contexthandler/authproxy" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/oauthtoken" @@ -99,8 +100,8 @@ type ContextHandler struct { type reqContextKey = ctxkey.Key // FromContext returns the ReqContext value stored in a context.Context, if any. -func FromContext(c context.Context) *models.ReqContext { - if reqCtx, ok := c.Value(reqContextKey{}).(*models.ReqContext); ok { +func FromContext(c context.Context) *contextmodel.ReqContext { + if reqCtx, ok := c.Value(reqContextKey{}).(*contextmodel.ReqContext); ok { return reqCtx } return nil @@ -114,7 +115,7 @@ func (h *ContextHandler) Middleware(next http.Handler) http.Handler { _, span := h.tracer.Start(ctx, "Auth - Middleware") defer span.End() - reqContext := &models.ReqContext{ + reqContext := &contextmodel.ReqContext{ Context: mContext, SignedInUser: &user.SignedInUser{}, IsSignedIn: false, @@ -218,7 +219,7 @@ func (h *ContextHandler) Middleware(next http.Handler) http.Handler { }) } -func (h *ContextHandler) initContextWithAnonymousUser(reqContext *models.ReqContext) bool { +func (h *ContextHandler) initContextWithAnonymousUser(reqContext *contextmodel.ReqContext) bool { _, span := h.tracer.Start(reqContext.Req.Context(), "initContextWithAnonymousUser") defer span.End() @@ -282,7 +283,7 @@ func (h *ContextHandler) getAPIKey(ctx context.Context, keyString string) (*apik return keyQuery.Result, nil } -func (h *ContextHandler) initContextWithAPIKey(reqContext *models.ReqContext) bool { +func (h *ContextHandler) initContextWithAPIKey(reqContext *contextmodel.ReqContext) bool { header := reqContext.Req.Header.Get("Authorization") parts := strings.SplitN(header, " ", 2) var keyString string @@ -396,7 +397,7 @@ func (h *ContextHandler) initContextWithAPIKey(reqContext *models.ReqContext) bo return true } -func (h *ContextHandler) initContextWithBasicAuth(reqContext *models.ReqContext, orgID int64) bool { +func (h *ContextHandler) initContextWithBasicAuth(reqContext *contextmodel.ReqContext, orgID int64) bool { if !h.Cfg.BasicAuthEnabled { return false } @@ -456,7 +457,7 @@ func (h *ContextHandler) initContextWithBasicAuth(reqContext *models.ReqContext, return true } -func (h *ContextHandler) initContextWithToken(reqContext *models.ReqContext, orgID int64) bool { +func (h *ContextHandler) initContextWithToken(reqContext *contextmodel.ReqContext, orgID int64) bool { if h.Cfg.LoginCookieName == "" { return false } @@ -528,7 +529,7 @@ func (h *ContextHandler) initContextWithToken(reqContext *models.ReqContext, org return true } -func (h *ContextHandler) deleteInvalidCookieEndOfRequestFunc(reqContext *models.ReqContext) web.BeforeFunc { +func (h *ContextHandler) deleteInvalidCookieEndOfRequestFunc(reqContext *contextmodel.ReqContext) web.BeforeFunc { return func(w web.ResponseWriter) { if w.Written() { reqContext.Logger.Debug("Response written, skipping invalid cookie delete") @@ -540,7 +541,7 @@ func (h *ContextHandler) deleteInvalidCookieEndOfRequestFunc(reqContext *models. } } -func (h *ContextHandler) rotateEndOfRequestFunc(reqContext *models.ReqContext) web.BeforeFunc { +func (h *ContextHandler) rotateEndOfRequestFunc(reqContext *contextmodel.ReqContext) web.BeforeFunc { return func(w web.ResponseWriter) { // if response has already been written, skip. if w.Written() { @@ -581,7 +582,7 @@ func (h *ContextHandler) rotateEndOfRequestFunc(reqContext *models.ReqContext) w } } -func (h *ContextHandler) initContextWithRenderAuth(reqContext *models.ReqContext) bool { +func (h *ContextHandler) initContextWithRenderAuth(reqContext *contextmodel.ReqContext) bool { key := reqContext.GetCookie("renderKey") if key == "" { return false @@ -617,7 +618,7 @@ func (h *ContextHandler) initContextWithRenderAuth(reqContext *models.ReqContext return true } -func logUserIn(reqContext *models.ReqContext, auth *authproxy.AuthProxy, username string, logger log.Logger, ignoreCache bool) (int64, error) { +func logUserIn(reqContext *contextmodel.ReqContext, auth *authproxy.AuthProxy, username string, logger log.Logger, ignoreCache bool) (int64, error) { logger.Debug("Trying to log user in", "username", username, "ignoreCache", ignoreCache) // Try to log in user via various providers id, err := auth.Login(reqContext, ignoreCache) @@ -634,7 +635,7 @@ func logUserIn(reqContext *models.ReqContext, auth *authproxy.AuthProxy, usernam return id, nil } -func (h *ContextHandler) handleError(ctx *models.ReqContext, err error, statusCode int, cb func(error)) { +func (h *ContextHandler) handleError(ctx *contextmodel.ReqContext, err error, statusCode int, cb func(error)) { details := err var e authproxy.Error if errors.As(err, &e) { @@ -647,7 +648,7 @@ func (h *ContextHandler) handleError(ctx *models.ReqContext, err error, statusCo } } -func (h *ContextHandler) initContextWithAuthProxy(reqContext *models.ReqContext, orgID int64) bool { +func (h *ContextHandler) initContextWithAuthProxy(reqContext *contextmodel.ReqContext, orgID int64) bool { username := reqContext.Req.Header.Get(h.Cfg.AuthProxyHeaderName) logger := log.New("auth.proxy") diff --git a/pkg/services/contexthandler/contexthandler_test.go b/pkg/services/contexthandler/contexthandler_test.go index a86329cd2ca..870222a6884 100644 --- a/pkg/services/contexthandler/contexthandler_test.go +++ b/pkg/services/contexthandler/contexthandler_test.go @@ -12,9 +12,9 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth/authtest" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" ) @@ -77,7 +77,7 @@ func TestTokenRotationAtEndOfRequest(t *testing.T) { } func initTokenRotationScenario(ctx context.Context, t *testing.T, ctxHdlr *ContextHandler) ( - *models.ReqContext, *httptest.ResponseRecorder, error) { + *contextmodel.ReqContext, *httptest.ResponseRecorder, error) { t.Helper() ctxHdlr.Cfg.LoginCookieName = "login_token" @@ -92,7 +92,7 @@ func initTokenRotationScenario(ctx context.Context, t *testing.T, ctxHdlr *Conte if err != nil { return nil, nil, err } - reqContext := &models.ReqContext{ + reqContext := &contextmodel.ReqContext{ Context: &web.Context{Req: req}, Logger: log.New("testlogger"), } diff --git a/pkg/models/context.go b/pkg/services/contexthandler/model/model.go similarity index 99% rename from pkg/models/context.go rename to pkg/services/contexthandler/model/model.go index 97763e39b7e..0ae7d23c3b9 100644 --- a/pkg/models/context.go +++ b/pkg/services/contexthandler/model/model.go @@ -1,4 +1,4 @@ -package models +package contextmodel import ( "errors" diff --git a/pkg/models/context_test.go b/pkg/services/contexthandler/model/model_test.go similarity index 97% rename from pkg/models/context_test.go rename to pkg/services/contexthandler/model/model_test.go index 37b5b58cc5f..aa492d30999 100644 --- a/pkg/models/context_test.go +++ b/pkg/services/contexthandler/model/model_test.go @@ -1,4 +1,4 @@ -package models +package contextmodel import ( "net/http" diff --git a/pkg/services/correlations/api.go b/pkg/services/correlations/api.go index aebba66f847..773236a6eb6 100644 --- a/pkg/services/correlations/api.go +++ b/pkg/services/correlations/api.go @@ -7,8 +7,8 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/web" @@ -43,7 +43,7 @@ func (s *CorrelationsService) registerAPIEndpoints() { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (s *CorrelationsService) createHandler(c *models.ReqContext) response.Response { +func (s *CorrelationsService) createHandler(c *contextmodel.ReqContext) response.Response { cmd := CreateCorrelationCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -93,7 +93,7 @@ type CreateCorrelationResponse struct { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (s *CorrelationsService) deleteHandler(c *models.ReqContext) response.Response { +func (s *CorrelationsService) deleteHandler(c *contextmodel.ReqContext) response.Response { cmd := DeleteCorrelationCommand{ UID: web.Params(c.Req)[":correlationUID"], SourceUID: web.Params(c.Req)[":uid"], @@ -147,7 +147,7 @@ type DeleteCorrelationResponse struct { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (s *CorrelationsService) updateHandler(c *models.ReqContext) response.Response { +func (s *CorrelationsService) updateHandler(c *contextmodel.ReqContext) response.Response { cmd := UpdateCorrelationCommand{} if err := web.Bind(c.Req, &cmd); err != nil { if errors.Is(err, ErrUpdateCorrelationEmptyParams) { @@ -208,7 +208,7 @@ type UpdateCorrelationResponse struct { // 401: unauthorisedError // 404: notFoundError // 500: internalServerError -func (s *CorrelationsService) getCorrelationHandler(c *models.ReqContext) response.Response { +func (s *CorrelationsService) getCorrelationHandler(c *contextmodel.ReqContext) response.Response { query := GetCorrelationQuery{ UID: web.Params(c.Req)[":correlationUID"], SourceUID: web.Params(c.Req)[":uid"], @@ -255,7 +255,7 @@ type GetCorrelationResponse struct { // 401: unauthorisedError // 404: notFoundError // 500: internalServerError -func (s *CorrelationsService) getCorrelationsBySourceUIDHandler(c *models.ReqContext) response.Response { +func (s *CorrelationsService) getCorrelationsBySourceUIDHandler(c *contextmodel.ReqContext) response.Response { query := GetCorrelationsBySourceUIDQuery{ SourceUID: web.Params(c.Req)[":uid"], OrgId: c.OrgID, @@ -298,7 +298,7 @@ type GetCorrelationsBySourceUIDResponse struct { // 401: unauthorisedError // 404: notFoundError // 500: internalServerError -func (s *CorrelationsService) getCorrelationsHandler(c *models.ReqContext) response.Response { +func (s *CorrelationsService) getCorrelationsHandler(c *contextmodel.ReqContext) response.Response { query := GetCorrelationsQuery{ OrgId: c.OrgID, } diff --git a/pkg/services/dashboardimport/api/api.go b/pkg/services/dashboardimport/api/api.go index f491d645bdc..24ddb46c9f4 100644 --- a/pkg/services/dashboardimport/api/api.go +++ b/pkg/services/dashboardimport/api/api.go @@ -7,9 +7,9 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboardimport" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/quota" @@ -55,7 +55,7 @@ func (api *ImportDashboardAPI) RegisterAPIEndpoints(routeRegister routing.RouteR // 412: preconditionFailedError // 422: unprocessableEntityError // 500: internalServerError -func (api *ImportDashboardAPI) ImportDashboard(c *models.ReqContext) response.Response { +func (api *ImportDashboardAPI) ImportDashboard(c *contextmodel.ReqContext) response.Response { req := dashboardimport.ImportDashboardRequest{} if err := web.Bind(c.Req, &req); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -84,12 +84,12 @@ func (api *ImportDashboardAPI) ImportDashboard(c *models.ReqContext) response.Re } type QuotaService interface { - QuotaReached(c *models.ReqContext, target quota.TargetSrv) (bool, error) + QuotaReached(c *contextmodel.ReqContext, target quota.TargetSrv) (bool, error) } -type quotaServiceFunc func(c *models.ReqContext, target quota.TargetSrv) (bool, error) +type quotaServiceFunc func(c *contextmodel.ReqContext, target quota.TargetSrv) (bool, error) -func (fn quotaServiceFunc) QuotaReached(c *models.ReqContext, target quota.TargetSrv) (bool, error) { +func (fn quotaServiceFunc) QuotaReached(c *contextmodel.ReqContext, target quota.TargetSrv) (bool, error) { return fn(c, target) } diff --git a/pkg/services/dashboardimport/api/api_test.go b/pkg/services/dashboardimport/api/api_test.go index d688e019109..6d5a07b8862 100644 --- a/pkg/services/dashboardimport/api/api_test.go +++ b/pkg/services/dashboardimport/api/api_test.go @@ -9,8 +9,8 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/models" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboardimport" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/user" @@ -166,10 +166,10 @@ func (s *serviceMock) ImportDashboard(ctx context.Context, req *dashboardimport. return nil, nil } -func quotaReached(c *models.ReqContext, target quota.TargetSrv) (bool, error) { +func quotaReached(c *contextmodel.ReqContext, target quota.TargetSrv) (bool, error) { return true, nil } -func quotaNotReached(c *models.ReqContext, target quota.TargetSrv) (bool, error) { +func quotaNotReached(c *contextmodel.ReqContext, target quota.TargetSrv) (bool, error) { return false, nil } diff --git a/pkg/services/datasourceproxy/datasourceproxy.go b/pkg/services/datasourceproxy/datasourceproxy.go index c6346fa9682..de382029364 100644 --- a/pkg/services/datasourceproxy/datasourceproxy.go +++ b/pkg/services/datasourceproxy/datasourceproxy.go @@ -12,8 +12,8 @@ import ( "github.com/grafana/grafana/pkg/infra/httpclient" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/oauthtoken" "github.com/grafana/grafana/pkg/services/secrets" @@ -52,7 +52,7 @@ type DataSourceProxyService struct { secretsService secrets.Service } -func (p *DataSourceProxyService) ProxyDataSourceRequest(c *models.ReqContext) { +func (p *DataSourceProxyService) ProxyDataSourceRequest(c *contextmodel.ReqContext) { id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { c.JsonApiErr(http.StatusBadRequest, "id is invalid", err) @@ -61,7 +61,7 @@ func (p *DataSourceProxyService) ProxyDataSourceRequest(c *models.ReqContext) { p.ProxyDatasourceRequestWithID(c, id) } -func (p *DataSourceProxyService) ProxyDatasourceRequestWithUID(c *models.ReqContext, dsUID string) { +func (p *DataSourceProxyService) ProxyDatasourceRequestWithUID(c *contextmodel.ReqContext, dsUID string) { c.TimeRequest(metrics.MDataSourceProxyReqTimer) if dsUID == "" { // if datasource UID is not provided, fetch it from the uid path parameter @@ -81,7 +81,7 @@ func (p *DataSourceProxyService) ProxyDatasourceRequestWithUID(c *models.ReqCont p.proxyDatasourceRequest(c, ds) } -func (p *DataSourceProxyService) ProxyDatasourceRequestWithID(c *models.ReqContext, dsID int64) { +func (p *DataSourceProxyService) ProxyDatasourceRequestWithID(c *contextmodel.ReqContext, dsID int64) { c.TimeRequest(metrics.MDataSourceProxyReqTimer) ds, err := p.DataSourceCache.GetDatasource(c.Req.Context(), dsID, c.SignedInUser, c.SkipCache) @@ -92,7 +92,7 @@ func (p *DataSourceProxyService) ProxyDatasourceRequestWithID(c *models.ReqConte p.proxyDatasourceRequest(c, ds) } -func toAPIError(c *models.ReqContext, err error) { +func toAPIError(c *contextmodel.ReqContext, err error) { if errors.Is(err, datasources.ErrDataSourceAccessDenied) { c.JsonApiErr(http.StatusForbidden, "Access denied to datasource", err) return @@ -104,7 +104,7 @@ func toAPIError(c *models.ReqContext, err error) { c.JsonApiErr(http.StatusInternalServerError, "Unable to load datasource meta data", err) } -func (p *DataSourceProxyService) proxyDatasourceRequest(c *models.ReqContext, ds *datasources.DataSource) { +func (p *DataSourceProxyService) proxyDatasourceRequest(c *contextmodel.ReqContext, ds *datasources.DataSource) { err := p.PluginRequestValidator.Validate(ds.Url, c.Req) if err != nil { c.JsonApiErr(http.StatusForbidden, "Access denied", err) @@ -138,6 +138,6 @@ func extractProxyPath(originalRawPath string) string { return proxyPathRegexp.ReplaceAllString(originalRawPath, "") } -func getProxyPath(c *models.ReqContext) string { +func getProxyPath(c *contextmodel.ReqContext) string { return extractProxyPath(c.Req.URL.EscapedPath()) } diff --git a/pkg/services/export/service.go b/pkg/services/export/service.go index 3e7f03e14d1..173635c524b 100644 --- a/pkg/services/export/service.go +++ b/pkg/services/export/service.go @@ -14,7 +14,7 @@ import ( "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboardsnapshots" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -27,16 +27,16 @@ import ( type ExportService interface { // List folder contents - HandleGetStatus(c *models.ReqContext) response.Response + HandleGetStatus(c *contextmodel.ReqContext) response.Response // List Get Options - HandleGetOptions(c *models.ReqContext) response.Response + HandleGetOptions(c *contextmodel.ReqContext) response.Response // Read raw file contents out of the store - HandleRequestExport(c *models.ReqContext) response.Response + HandleRequestExport(c *contextmodel.ReqContext) response.Response // Cancel any running export - HandleRequestStop(c *models.ReqContext) response.Response + HandleRequestStop(c *contextmodel.ReqContext) response.Response } var exporters = []Exporter{ @@ -186,21 +186,21 @@ func ProvideService(db db.DB, features featuremgmt.FeatureToggles, gl *live.Graf } } -func (ex *StandardExport) HandleGetOptions(c *models.ReqContext) response.Response { +func (ex *StandardExport) HandleGetOptions(c *contextmodel.ReqContext) response.Response { info := map[string]interface{}{ "exporters": exporters, } return response.JSON(http.StatusOK, info) } -func (ex *StandardExport) HandleGetStatus(c *models.ReqContext) response.Response { +func (ex *StandardExport) HandleGetStatus(c *contextmodel.ReqContext) response.Response { ex.mutex.Lock() defer ex.mutex.Unlock() return response.JSON(http.StatusOK, ex.exportJob.getStatus()) } -func (ex *StandardExport) HandleRequestStop(c *models.ReqContext) response.Response { +func (ex *StandardExport) HandleRequestStop(c *contextmodel.ReqContext) response.Response { ex.mutex.Lock() defer ex.mutex.Unlock() @@ -209,7 +209,7 @@ func (ex *StandardExport) HandleRequestStop(c *models.ReqContext) response.Respo return response.JSON(http.StatusOK, ex.exportJob.getStatus()) } -func (ex *StandardExport) HandleRequestExport(c *models.ReqContext) response.Response { +func (ex *StandardExport) HandleRequestExport(c *contextmodel.ReqContext) response.Response { var cfg ExportConfig err := json.NewDecoder(c.Req.Body).Decode(&cfg) if err != nil { diff --git a/pkg/services/export/stub.go b/pkg/services/export/stub.go index a0ad9a106f8..fe8d9768d6b 100644 --- a/pkg/services/export/stub.go +++ b/pkg/services/export/stub.go @@ -4,25 +4,25 @@ import ( "net/http" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" ) var _ ExportService = new(StubExport) type StubExport struct{} -func (ex *StubExport) HandleGetStatus(c *models.ReqContext) response.Response { +func (ex *StubExport) HandleGetStatus(c *contextmodel.ReqContext) response.Response { return response.Error(http.StatusForbidden, "feature not enabled", nil) } -func (ex *StubExport) HandleGetOptions(c *models.ReqContext) response.Response { +func (ex *StubExport) HandleGetOptions(c *contextmodel.ReqContext) response.Response { return response.Error(http.StatusForbidden, "feature not enabled", nil) } -func (ex *StubExport) HandleRequestExport(c *models.ReqContext) response.Response { +func (ex *StubExport) HandleRequestExport(c *contextmodel.ReqContext) response.Response { return response.Error(http.StatusForbidden, "feature not enabled", nil) } -func (ex *StubExport) HandleRequestStop(c *models.ReqContext) response.Response { +func (ex *StubExport) HandleRequestStop(c *contextmodel.ReqContext) response.Response { return response.Error(http.StatusForbidden, "feature not enabled", nil) } diff --git a/pkg/services/featuremgmt/manager.go b/pkg/services/featuremgmt/manager.go index 19a9f91c6e3..223393fde8f 100644 --- a/pkg/services/featuremgmt/manager.go +++ b/pkg/services/featuremgmt/manager.go @@ -8,7 +8,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/licensing" ) @@ -151,7 +151,7 @@ func (fm *FeatureManager) GetFlags() []FeatureFlag { return v } -func (fm *FeatureManager) HandleGetSettings(c *models.ReqContext) { +func (fm *FeatureManager) HandleGetSettings(c *contextmodel.ReqContext) { res := make(map[string]interface{}, 3) res["enabled"] = fm.GetEnabled(c.Req.Context()) diff --git a/pkg/services/hooks/hooks.go b/pkg/services/hooks/hooks.go index 87e93f7e721..1729067e676 100644 --- a/pkg/services/hooks/hooks.go +++ b/pkg/services/hooks/hooks.go @@ -3,11 +3,12 @@ package hooks import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" ) -type IndexDataHook func(indexData *dtos.IndexViewData, req *models.ReqContext) +type IndexDataHook func(indexData *dtos.IndexViewData, req *contextmodel.ReqContext) -type LoginHook func(loginInfo *models.LoginInfo, req *models.ReqContext) +type LoginHook func(loginInfo *models.LoginInfo, req *contextmodel.ReqContext) type HooksService struct { indexDataHooks []IndexDataHook @@ -22,7 +23,7 @@ func (srv *HooksService) AddIndexDataHook(hook IndexDataHook) { srv.indexDataHooks = append(srv.indexDataHooks, hook) } -func (srv *HooksService) RunIndexDataHooks(indexData *dtos.IndexViewData, req *models.ReqContext) { +func (srv *HooksService) RunIndexDataHooks(indexData *dtos.IndexViewData, req *contextmodel.ReqContext) { for _, hook := range srv.indexDataHooks { hook(indexData, req) } @@ -32,7 +33,7 @@ func (srv *HooksService) AddLoginHook(hook LoginHook) { srv.loginHooks = append(srv.loginHooks, hook) } -func (srv *HooksService) RunLoginHook(loginInfo *models.LoginInfo, req *models.ReqContext) { +func (srv *HooksService) RunLoginHook(loginInfo *models.LoginInfo, req *contextmodel.ReqContext) { for _, hook := range srv.loginHooks { hook(loginInfo, req) } diff --git a/pkg/services/libraryelements/api.go b/pkg/services/libraryelements/api.go index 08b46cc7bfc..8443fc4f85a 100644 --- a/pkg/services/libraryelements/api.go +++ b/pkg/services/libraryelements/api.go @@ -7,7 +7,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/web" @@ -38,7 +38,7 @@ func (l *LibraryElementService) registerAPIEndpoints() { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (l *LibraryElementService) createHandler(c *models.ReqContext) response.Response { +func (l *LibraryElementService) createHandler(c *contextmodel.ReqContext) response.Response { cmd := CreateLibraryElementCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -88,7 +88,7 @@ func (l *LibraryElementService) createHandler(c *models.ReqContext) response.Res // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (l *LibraryElementService) deleteHandler(c *models.ReqContext) response.Response { +func (l *LibraryElementService) deleteHandler(c *contextmodel.ReqContext) response.Response { id, err := l.deleteLibraryElement(c.Req.Context(), c.SignedInUser, web.Params(c.Req)[":uid"]) if err != nil { return toLibraryElementError(err, "Failed to delete library element") @@ -111,7 +111,7 @@ func (l *LibraryElementService) deleteHandler(c *models.ReqContext) response.Res // 401: unauthorisedError // 404: notFoundError // 500: internalServerError -func (l *LibraryElementService) getHandler(c *models.ReqContext) response.Response { +func (l *LibraryElementService) getHandler(c *contextmodel.ReqContext) response.Response { element, err := l.getLibraryElementByUid(c.Req.Context(), c.SignedInUser, web.Params(c.Req)[":uid"]) if err != nil { return toLibraryElementError(err, "Failed to get library element") @@ -132,7 +132,7 @@ func (l *LibraryElementService) getHandler(c *models.ReqContext) response.Respon // 200: getLibraryElementsResponse // 401: unauthorisedError // 500: internalServerError -func (l *LibraryElementService) getAllHandler(c *models.ReqContext) response.Response { +func (l *LibraryElementService) getAllHandler(c *contextmodel.ReqContext) response.Response { query := searchLibraryElementsQuery{ perPage: c.QueryInt("perPage"), page: c.QueryInt("page"), @@ -166,7 +166,7 @@ func (l *LibraryElementService) getAllHandler(c *models.ReqContext) response.Res // 404: notFoundError // 412: preconditionFailedError // 500: internalServerError -func (l *LibraryElementService) patchHandler(c *models.ReqContext) response.Response { +func (l *LibraryElementService) patchHandler(c *contextmodel.ReqContext) response.Response { cmd := PatchLibraryElementCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -213,7 +213,7 @@ func (l *LibraryElementService) patchHandler(c *models.ReqContext) response.Resp // 401: unauthorisedError // 404: notFoundError // 500: internalServerError -func (l *LibraryElementService) getConnectionsHandler(c *models.ReqContext) response.Response { +func (l *LibraryElementService) getConnectionsHandler(c *contextmodel.ReqContext) response.Response { connections, err := l.getConnections(c.Req.Context(), c.SignedInUser, web.Params(c.Req)[":uid"]) if err != nil { return toLibraryElementError(err, "Failed to get connections") @@ -233,7 +233,7 @@ func (l *LibraryElementService) getConnectionsHandler(c *models.ReqContext) resp // 401: unauthorisedError // 404: notFoundError // 500: internalServerError -func (l *LibraryElementService) getByNameHandler(c *models.ReqContext) response.Response { +func (l *LibraryElementService) getByNameHandler(c *contextmodel.ReqContext) response.Response { elements, err := l.getLibraryElementsByName(c.Req.Context(), c.SignedInUser, web.Params(c.Req)[":name"]) if err != nil { return toLibraryElementError(err, "Failed to get library element") diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index b9ebdf9f2dc..79171ff888a 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -22,6 +22,7 @@ import ( "github.com/grafana/grafana/pkg/models" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/alerting" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/database" dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service" @@ -258,7 +259,7 @@ func getCreateCommandWithModel(folderID int64, name string, kind models.LibraryE type scenarioContext struct { ctx *web.Context service *LibraryElementService - reqContext *models.ReqContext + reqContext *contextmodel.ReqContext user user.SignedInUser folder *folder.Folder initialResult libraryElementResult @@ -468,7 +469,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo ctx: &webCtx, service: &service, sqlStore: sqlStore, - reqContext: &models.ReqContext{ + reqContext: &contextmodel.ReqContext{ Context: &webCtx, SignedInUser: &usr, }, diff --git a/pkg/services/licensing/oss.go b/pkg/services/licensing/oss.go index 34c7fbe15e0..8139d63542d 100644 --- a/pkg/services/licensing/oss.go +++ b/pkg/services/licensing/oss.go @@ -2,7 +2,7 @@ package licensing import ( "github.com/grafana/grafana/pkg/api/dtos" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/hooks" "github.com/grafana/grafana/pkg/services/navtree" "github.com/grafana/grafana/pkg/setting" @@ -54,7 +54,7 @@ func ProvideService(cfg *setting.Cfg, hooksService *hooks.HooksService) *OSSLice Cfg: cfg, HooksService: hooksService, } - l.HooksService.AddIndexDataHook(func(indexData *dtos.IndexViewData, req *models.ReqContext) { + l.HooksService.AddIndexDataHook(func(indexData *dtos.IndexViewData, req *contextmodel.ReqContext) { if !req.IsGrafanaAdmin { return } diff --git a/pkg/services/live/live.go b/pkg/services/live/live.go index c763ccff7de..7954ac4df46 100644 --- a/pkg/services/live/live.go +++ b/pkg/services/live/live.go @@ -22,12 +22,12 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/plugincontext" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/comments/commentmodel" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -350,7 +350,7 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r CheckOrigin: checkOrigin, }) - g.websocketHandler = func(ctx *models.ReqContext) { + g.websocketHandler = func(ctx *contextmodel.ReqContext) { user := ctx.SignedInUser // Centrifuge expects Credentials in context with a current user ID. @@ -363,7 +363,7 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r wsHandler.ServeHTTP(ctx.Resp, r) } - g.pushWebsocketHandler = func(ctx *models.ReqContext) { + g.pushWebsocketHandler = func(ctx *contextmodel.ReqContext) { user := ctx.SignedInUser newCtx := livecontext.SetContextSignedUser(ctx.Req.Context(), user) newCtx = livecontext.SetContextStreamID(newCtx, web.Params(ctx.Req)[":streamId"]) @@ -371,7 +371,7 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r pushWSHandler.ServeHTTP(ctx.Resp, r) } - g.pushPipelineWebsocketHandler = func(ctx *models.ReqContext) { + g.pushPipelineWebsocketHandler = func(ctx *contextmodel.ReqContext) { user := ctx.SignedInUser newCtx := livecontext.SetContextSignedUser(ctx.Req.Context(), user) newCtx = livecontext.SetContextChannelID(newCtx, web.Params(ctx.Req)["*"]) @@ -971,7 +971,7 @@ func (g *GrafanaLive) ClientCount(orgID int64, channel string) (int, error) { return len(p.Presence), nil } -func (g *GrafanaLive) HandleHTTPPublish(ctx *models.ReqContext) response.Response { +func (g *GrafanaLive) HandleHTTPPublish(ctx *contextmodel.ReqContext) response.Response { cmd := dtos.LivePublishCmd{} if err := web.Bind(ctx.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -1047,7 +1047,7 @@ type streamChannelListResponse struct { } // HandleListHTTP returns metadata so the UI can build a nice form -func (g *GrafanaLive) HandleListHTTP(c *models.ReqContext) response.Response { +func (g *GrafanaLive) HandleListHTTP(c *contextmodel.ReqContext) response.Response { var channels []*managedstream.ManagedChannel var err error if g.IsHA() { @@ -1065,7 +1065,7 @@ func (g *GrafanaLive) HandleListHTTP(c *models.ReqContext) response.Response { } // HandleInfoHTTP special http response for -func (g *GrafanaLive) HandleInfoHTTP(ctx *models.ReqContext) response.Response { +func (g *GrafanaLive) HandleInfoHTTP(ctx *contextmodel.ReqContext) response.Response { path := web.Params(ctx.Req)["*"] if path == "grafana/dashboards/gitops" { return response.JSON(http.StatusOK, util.DynMap{ @@ -1078,7 +1078,7 @@ func (g *GrafanaLive) HandleInfoHTTP(ctx *models.ReqContext) response.Response { } // HandleChannelRulesListHTTP ... -func (g *GrafanaLive) HandleChannelRulesListHTTP(c *models.ReqContext) response.Response { +func (g *GrafanaLive) HandleChannelRulesListHTTP(c *contextmodel.ReqContext) response.Response { result, err := g.pipelineStorage.ListChannelRules(c.Req.Context(), c.OrgID) if err != nil { return response.Error(http.StatusInternalServerError, "Failed to get channel rules", err) @@ -1139,7 +1139,7 @@ func (s *DryRunRuleStorage) ListChannelRules(_ context.Context, _ int64) ([]pipe } // HandlePipelineConvertTestHTTP ... -func (g *GrafanaLive) HandlePipelineConvertTestHTTP(c *models.ReqContext) response.Response { +func (g *GrafanaLive) HandlePipelineConvertTestHTTP(c *contextmodel.ReqContext) response.Response { body, err := io.ReadAll(c.Req.Body) if err != nil { return response.Error(http.StatusInternalServerError, "Error reading body", err) @@ -1184,7 +1184,7 @@ func (g *GrafanaLive) HandlePipelineConvertTestHTTP(c *models.ReqContext) respon } // HandleChannelRulesPostHTTP ... -func (g *GrafanaLive) HandleChannelRulesPostHTTP(c *models.ReqContext) response.Response { +func (g *GrafanaLive) HandleChannelRulesPostHTTP(c *contextmodel.ReqContext) response.Response { body, err := io.ReadAll(c.Req.Body) if err != nil { return response.Error(http.StatusInternalServerError, "Error reading body", err) @@ -1204,7 +1204,7 @@ func (g *GrafanaLive) HandleChannelRulesPostHTTP(c *models.ReqContext) response. } // HandleChannelRulesPutHTTP ... -func (g *GrafanaLive) HandleChannelRulesPutHTTP(c *models.ReqContext) response.Response { +func (g *GrafanaLive) HandleChannelRulesPutHTTP(c *contextmodel.ReqContext) response.Response { body, err := io.ReadAll(c.Req.Body) if err != nil { return response.Error(http.StatusInternalServerError, "Error reading body", err) @@ -1227,7 +1227,7 @@ func (g *GrafanaLive) HandleChannelRulesPutHTTP(c *models.ReqContext) response.R } // HandleChannelRulesDeleteHTTP ... -func (g *GrafanaLive) HandleChannelRulesDeleteHTTP(c *models.ReqContext) response.Response { +func (g *GrafanaLive) HandleChannelRulesDeleteHTTP(c *contextmodel.ReqContext) response.Response { body, err := io.ReadAll(c.Req.Body) if err != nil { return response.Error(http.StatusInternalServerError, "Error reading body", err) @@ -1248,7 +1248,7 @@ func (g *GrafanaLive) HandleChannelRulesDeleteHTTP(c *models.ReqContext) respons } // HandlePipelineEntitiesListHTTP ... -func (g *GrafanaLive) HandlePipelineEntitiesListHTTP(_ *models.ReqContext) response.Response { +func (g *GrafanaLive) HandlePipelineEntitiesListHTTP(_ *contextmodel.ReqContext) response.Response { return response.JSON(http.StatusOK, util.DynMap{ "subscribers": pipeline.SubscribersRegistry, "dataOutputs": pipeline.DataOutputsRegistry, @@ -1259,7 +1259,7 @@ func (g *GrafanaLive) HandlePipelineEntitiesListHTTP(_ *models.ReqContext) respo } // HandleWriteConfigsListHTTP ... -func (g *GrafanaLive) HandleWriteConfigsListHTTP(c *models.ReqContext) response.Response { +func (g *GrafanaLive) HandleWriteConfigsListHTTP(c *contextmodel.ReqContext) response.Response { backends, err := g.pipelineStorage.ListWriteConfigs(c.Req.Context(), c.OrgID) if err != nil { return response.Error(http.StatusInternalServerError, "Failed to get write configs", err) @@ -1274,7 +1274,7 @@ func (g *GrafanaLive) HandleWriteConfigsListHTTP(c *models.ReqContext) response. } // HandleWriteConfigsPostHTTP ... -func (g *GrafanaLive) HandleWriteConfigsPostHTTP(c *models.ReqContext) response.Response { +func (g *GrafanaLive) HandleWriteConfigsPostHTTP(c *contextmodel.ReqContext) response.Response { body, err := io.ReadAll(c.Req.Body) if err != nil { return response.Error(http.StatusInternalServerError, "Error reading body", err) @@ -1294,7 +1294,7 @@ func (g *GrafanaLive) HandleWriteConfigsPostHTTP(c *models.ReqContext) response. } // HandleWriteConfigsPutHTTP ... -func (g *GrafanaLive) HandleWriteConfigsPutHTTP(c *models.ReqContext) response.Response { +func (g *GrafanaLive) HandleWriteConfigsPutHTTP(c *contextmodel.ReqContext) response.Response { body, err := io.ReadAll(c.Req.Body) if err != nil { return response.Error(http.StatusInternalServerError, "Error reading body", err) @@ -1338,7 +1338,7 @@ func (g *GrafanaLive) HandleWriteConfigsPutHTTP(c *models.ReqContext) response.R } // HandleWriteConfigsDeleteHTTP ... -func (g *GrafanaLive) HandleWriteConfigsDeleteHTTP(c *models.ReqContext) response.Response { +func (g *GrafanaLive) HandleWriteConfigsDeleteHTTP(c *contextmodel.ReqContext) response.Response { body, err := io.ReadAll(c.Req.Body) if err != nil { return response.Error(http.StatusInternalServerError, "Error reading body", err) diff --git a/pkg/services/live/pushhttp/push.go b/pkg/services/live/pushhttp/push.go index 61291cd7e4f..302a5665c28 100644 --- a/pkg/services/live/pushhttp/push.go +++ b/pkg/services/live/pushhttp/push.go @@ -7,7 +7,7 @@ import ( "net/http" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/live" "github.com/grafana/grafana/pkg/services/live/convert" "github.com/grafana/grafana/pkg/services/live/pushurl" @@ -45,7 +45,7 @@ func (g *Gateway) Run(ctx context.Context) error { return ctx.Err() } -func (g *Gateway) Handle(ctx *models.ReqContext) { +func (g *Gateway) Handle(ctx *contextmodel.ReqContext) { streamID := web.Params(ctx.Req)[":streamId"] stream, err := g.GrafanaLive.ManagedStreamRunner.GetOrCreateStream(ctx.SignedInUser.OrgID, liveDto.ScopeStream, streamID) @@ -98,7 +98,7 @@ func (g *Gateway) Handle(ctx *models.ReqContext) { ctx.Resp.WriteHeader(http.StatusOK) } -func (g *Gateway) HandlePipelinePush(ctx *models.ReqContext) { +func (g *Gateway) HandlePipelinePush(ctx *contextmodel.ReqContext) { channelID := web.Params(ctx.Req)["*"] body, err := io.ReadAll(ctx.Req.Body) diff --git a/pkg/services/navtree/navtree.go b/pkg/services/navtree/navtree.go index 37cbe63c27d..8b641347547 100644 --- a/pkg/services/navtree/navtree.go +++ b/pkg/services/navtree/navtree.go @@ -1,10 +1,10 @@ package navtree import ( - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" pref "github.com/grafana/grafana/pkg/services/preference" ) type Service interface { - GetNavTree(c *models.ReqContext, hasEditPerm bool, prefs *pref.Preference) (*NavTreeRoot, error) + GetNavTree(c *contextmodel.ReqContext, hasEditPerm bool, prefs *pref.Preference) (*NavTreeRoot, error) } diff --git a/pkg/services/navtree/navtreeimpl/admin.go b/pkg/services/navtree/navtreeimpl/admin.go index 00c6cf09fde..9a565dc37d4 100644 --- a/pkg/services/navtree/navtreeimpl/admin.go +++ b/pkg/services/navtree/navtreeimpl/admin.go @@ -1,9 +1,9 @@ package navtreeimpl import ( - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/correlations" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -12,7 +12,7 @@ import ( "github.com/grafana/grafana/pkg/services/serviceaccounts" ) -func (s *ServiceImpl) getOrgAdminNode(c *models.ReqContext) (*navtree.NavLink, error) { +func (s *ServiceImpl) getOrgAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink, error) { var configNodes []*navtree.NavLink hasAccess := ac.HasAccess(s.accessControl, c) @@ -119,7 +119,7 @@ func (s *ServiceImpl) getOrgAdminNode(c *models.ReqContext) (*navtree.NavLink, e return configNode, nil } -func (s *ServiceImpl) getServerAdminNode(c *models.ReqContext) *navtree.NavLink { +func (s *ServiceImpl) getServerAdminNode(c *contextmodel.ReqContext) *navtree.NavLink { hasAccess := ac.HasAccess(s.accessControl, c) hasGlobalAccess := ac.HasGlobalAccess(s.accessControl, s.accesscontrolService, c) orgsAccessEvaluator := ac.EvalPermission(ac.ActionOrgsRead) @@ -204,11 +204,11 @@ func (s *ServiceImpl) getServerAdminNode(c *models.ReqContext) *navtree.NavLink return adminNode } -func (s *ServiceImpl) ReqCanAdminTeams(c *models.ReqContext) bool { +func (s *ServiceImpl) ReqCanAdminTeams(c *contextmodel.ReqContext) bool { return c.OrgRole == org.RoleAdmin || (s.cfg.EditorsCanAdmin && c.OrgRole == org.RoleEditor) } -func enableServiceAccount(s *ServiceImpl, c *models.ReqContext) bool { +func enableServiceAccount(s *ServiceImpl, c *contextmodel.ReqContext) bool { hasAccess := ac.HasAccess(s.accessControl, c) return hasAccess(ac.ReqOrgAdmin, serviceaccounts.AccessEvaluator) } diff --git a/pkg/services/navtree/navtreeimpl/applinks.go b/pkg/services/navtree/navtreeimpl/applinks.go index dddffbcb5d0..d2bccdb4c56 100644 --- a/pkg/services/navtree/navtreeimpl/applinks.go +++ b/pkg/services/navtree/navtreeimpl/applinks.go @@ -5,16 +5,16 @@ import ( "sort" "strconv" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/navtree" "github.com/grafana/grafana/pkg/services/pluginsettings" "github.com/grafana/grafana/pkg/util" ) -func (s *ServiceImpl) addAppLinks(treeRoot *navtree.NavTreeRoot, c *models.ReqContext) error { +func (s *ServiceImpl) addAppLinks(treeRoot *navtree.NavTreeRoot, c *contextmodel.ReqContext) error { topNavEnabled := s.features.IsEnabled(featuremgmt.FlagTopnav) hasAccess := ac.HasAccess(s.accessControl, c) appLinks := []*navtree.NavLink{} @@ -64,7 +64,7 @@ func (s *ServiceImpl) addAppLinks(treeRoot *navtree.NavTreeRoot, c *models.ReqCo return nil } -func (s *ServiceImpl) processAppPlugin(plugin plugins.PluginDTO, c *models.ReqContext, topNavEnabled bool, treeRoot *navtree.NavTreeRoot) *navtree.NavLink { +func (s *ServiceImpl) processAppPlugin(plugin plugins.PluginDTO, c *contextmodel.ReqContext, topNavEnabled bool, treeRoot *navtree.NavTreeRoot) *navtree.NavLink { hasAccessToInclude := s.hasAccessToInclude(c, plugin.ID) appLink := &navtree.NavLink{ Text: plugin.Name, @@ -176,7 +176,7 @@ func (s *ServiceImpl) processAppPlugin(plugin plugins.PluginDTO, c *models.ReqCo return nil } -func (s *ServiceImpl) addPluginToSection(c *models.ReqContext, treeRoot *navtree.NavTreeRoot, plugin plugins.PluginDTO, appLink *navtree.NavLink) { +func (s *ServiceImpl) addPluginToSection(c *contextmodel.ReqContext, treeRoot *navtree.NavTreeRoot, plugin plugins.PluginDTO, appLink *navtree.NavLink) { // Handle moving apps into specific navtree sections alertingNode := treeRoot.FindById(navtree.NavIDAlerting) sectionID := navtree.NavIDApps @@ -241,7 +241,7 @@ func (s *ServiceImpl) addPluginToSection(c *models.ReqContext, treeRoot *navtree } } -func (s *ServiceImpl) hasAccessToInclude(c *models.ReqContext, pluginID string) func(include *plugins.Includes) bool { +func (s *ServiceImpl) hasAccessToInclude(c *contextmodel.ReqContext, pluginID string) func(include *plugins.Includes) bool { hasAccess := ac.HasAccess(s.accessControl, c) return func(include *plugins.Includes) bool { useRBAC := s.features.IsEnabled(featuremgmt.FlagAccessControlOnCall) && diff --git a/pkg/services/navtree/navtreeimpl/applinks_test.go b/pkg/services/navtree/navtreeimpl/applinks_test.go index 79d39b287ca..2864a5fa9c8 100644 --- a/pkg/services/navtree/navtreeimpl/applinks_test.go +++ b/pkg/services/navtree/navtreeimpl/applinks_test.go @@ -5,12 +5,12 @@ import ( "testing" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/plugins" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/navtree" @@ -23,7 +23,7 @@ import ( func TestAddAppLinks(t *testing.T) { httpReq, _ := http.NewRequest(http.MethodGet, "", nil) - reqCtx := &models.ReqContext{SignedInUser: &user.SignedInUser{}, Context: &web.Context{Req: httpReq}} + reqCtx := &contextmodel.ReqContext{SignedInUser: &user.SignedInUser{}, Context: &web.Context{Req: httpReq}} permissions := []ac.Permission{ {Action: plugins.ActionAppAccess, Scope: "*"}, {Action: plugins.ActionInstall, Scope: "*"}, @@ -388,7 +388,7 @@ func TestReadingNavigationSettings(t *testing.T) { func TestAddAppLinksAccessControl(t *testing.T) { httpReq, _ := http.NewRequest(http.MethodGet, "", nil) user := &user.SignedInUser{OrgID: 1} - reqCtx := &models.ReqContext{SignedInUser: user, Context: &web.Context{Req: httpReq}} + reqCtx := &contextmodel.ReqContext{SignedInUser: user, Context: &web.Context{Req: httpReq}} catalogReadAction := "test-app1.catalog:read" testApp1 := plugins.PluginDTO{ diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index 00740daa92c..57c6097614c 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -7,10 +7,10 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apikey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -71,7 +71,7 @@ func ProvideService(cfg *setting.Cfg, accessControl ac.AccessControl, pluginStor } //nolint:gocyclo -func (s *ServiceImpl) GetNavTree(c *models.ReqContext, hasEditPerm bool, prefs *pref.Preference) (*navtree.NavTreeRoot, error) { +func (s *ServiceImpl) GetNavTree(c *contextmodel.ReqContext, hasEditPerm bool, prefs *pref.Preference) (*navtree.NavTreeRoot, error) { hasAccess := ac.HasAccess(s.accessControl, c) treeRoot := &navtree.NavTreeRoot{} @@ -111,7 +111,7 @@ func (s *ServiceImpl) GetNavTree(c *models.ReqContext, hasEditPerm bool, prefs * treeRoot.AddSection(dashboardLink) } - canExplore := func(context *models.ReqContext) bool { + canExplore := func(context *contextmodel.ReqContext) bool { return c.OrgRole == org.RoleAdmin || c.OrgRole == org.RoleEditor || setting.ViewersCanEdit } @@ -210,7 +210,7 @@ func (s *ServiceImpl) GetNavTree(c *models.ReqContext, hasEditPerm bool, prefs * return treeRoot, nil } -func (s *ServiceImpl) getHomeNode(c *models.ReqContext, prefs *pref.Preference) *navtree.NavLink { +func (s *ServiceImpl) getHomeNode(c *contextmodel.ReqContext, prefs *pref.Preference) *navtree.NavLink { homeUrl := s.cfg.AppSubURL + "/" homePage := s.cfg.HomePage @@ -232,7 +232,7 @@ func (s *ServiceImpl) getHomeNode(c *models.ReqContext, prefs *pref.Preference) return homeNode } -func (s *ServiceImpl) addHelpLinks(treeRoot *navtree.NavTreeRoot, c *models.ReqContext) { +func (s *ServiceImpl) addHelpLinks(treeRoot *navtree.NavTreeRoot, c *contextmodel.ReqContext) { if setting.HelpEnabled { helpVersion := fmt.Sprintf(`%s v%s (%s)`, setting.ApplicationName, setting.BuildVersion, setting.BuildCommit) if s.cfg.AnonymousHideVersion && !c.IsSignedIn { @@ -261,7 +261,7 @@ func (s *ServiceImpl) addHelpLinks(treeRoot *navtree.NavTreeRoot, c *models.ReqC } } -func (s *ServiceImpl) getProfileNode(c *models.ReqContext) *navtree.NavLink { +func (s *ServiceImpl) getProfileNode(c *contextmodel.ReqContext) *navtree.NavLink { // Only set login if it's different from the name var login string if c.SignedInUser.Login != c.SignedInUser.NameOrFallback() { @@ -311,7 +311,7 @@ func (s *ServiceImpl) getProfileNode(c *models.ReqContext) *navtree.NavLink { } } -func (s *ServiceImpl) buildStarredItemsNavLinks(c *models.ReqContext) ([]*navtree.NavLink, error) { +func (s *ServiceImpl) buildStarredItemsNavLinks(c *contextmodel.ReqContext) ([]*navtree.NavLink, error) { starredItemsChildNavs := []*navtree.NavLink{} query := star.GetUserStarsQuery{ @@ -357,9 +357,9 @@ func (s *ServiceImpl) buildStarredItemsNavLinks(c *models.ReqContext) ([]*navtre return starredItemsChildNavs, nil } -func (s *ServiceImpl) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm bool) []*navtree.NavLink { +func (s *ServiceImpl) buildDashboardNavLinks(c *contextmodel.ReqContext, hasEditPerm bool) []*navtree.NavLink { hasAccess := ac.HasAccess(s.accessControl, c) - hasEditPermInAnyFolder := func(c *models.ReqContext) bool { + hasEditPermInAnyFolder := func(c *contextmodel.ReqContext) bool { return hasEditPerm } @@ -446,7 +446,7 @@ func (s *ServiceImpl) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm b return dashboardChildNavs } -func (s *ServiceImpl) buildLegacyAlertNavLinks(c *models.ReqContext) *navtree.NavLink { +func (s *ServiceImpl) buildLegacyAlertNavLinks(c *contextmodel.ReqContext) *navtree.NavLink { var alertChildNavs []*navtree.NavLink alertChildNavs = append(alertChildNavs, &navtree.NavLink{ Text: "Alert rules", Id: "alert-list", Url: s.cfg.AppSubURL + "/alerting/list", Icon: "list-ul", @@ -478,7 +478,7 @@ func (s *ServiceImpl) buildLegacyAlertNavLinks(c *models.ReqContext) *navtree.Na return &alertNav } -func (s *ServiceImpl) buildAlertNavLinks(c *models.ReqContext, hasEditPerm bool) *navtree.NavLink { +func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext, hasEditPerm bool) *navtree.NavLink { hasAccess := ac.HasAccess(s.accessControl, c) var alertChildNavs []*navtree.NavLink @@ -517,7 +517,7 @@ func (s *ServiceImpl) buildAlertNavLinks(c *models.ReqContext, hasEditPerm bool) }) } - fallbackHasEditPerm := func(*models.ReqContext) bool { return hasEditPerm } + fallbackHasEditPerm := func(*contextmodel.ReqContext) bool { return hasEditPerm } if hasAccess(fallbackHasEditPerm, ac.EvalAny(ac.EvalPermission(ac.ActionAlertingRuleCreate), ac.EvalPermission(ac.ActionAlertingRuleExternalWrite))) { if !s.features.IsEnabled(featuremgmt.FlagTopnav) { @@ -555,7 +555,7 @@ func (s *ServiceImpl) buildAlertNavLinks(c *models.ReqContext, hasEditPerm bool) return nil } -func (s *ServiceImpl) buildDataConnectionsNavLink(c *models.ReqContext) *navtree.NavLink { +func (s *ServiceImpl) buildDataConnectionsNavLink(c *contextmodel.ReqContext) *navtree.NavLink { hasAccess := ac.HasAccess(s.accessControl, c) var children []*navtree.NavLink diff --git a/pkg/services/ngalert/api/api_alertmanager.go b/pkg/services/ngalert/api/api_alertmanager.go index 7b801795eeb..0bcf6efa641 100644 --- a/pkg/services/ngalert/api/api_alertmanager.go +++ b/pkg/services/ngalert/api/api_alertmanager.go @@ -14,8 +14,8 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/notifier" "github.com/grafana/grafana/pkg/services/ngalert/store" @@ -42,7 +42,7 @@ func (e UnknownReceiverError) Error() string { return fmt.Sprintf("unknown receiver: %s", e.UID) } -func (srv AlertmanagerSrv) RouteGetAMStatus(c *models.ReqContext) response.Response { +func (srv AlertmanagerSrv) RouteGetAMStatus(c *contextmodel.ReqContext) response.Response { am, errResp := srv.AlertmanagerFor(c.OrgID) if errResp != nil { return errResp @@ -51,7 +51,7 @@ func (srv AlertmanagerSrv) RouteGetAMStatus(c *models.ReqContext) response.Respo return response.JSON(http.StatusOK, am.GetStatus()) } -func (srv AlertmanagerSrv) RouteCreateSilence(c *models.ReqContext, postableSilence apimodels.PostableSilence) response.Response { +func (srv AlertmanagerSrv) RouteCreateSilence(c *contextmodel.ReqContext, postableSilence apimodels.PostableSilence) response.Response { err := postableSilence.Validate(strfmt.Default) if err != nil { srv.log.Error("silence failed validation", "error", err) @@ -92,7 +92,7 @@ func (srv AlertmanagerSrv) RouteCreateSilence(c *models.ReqContext, postableSile }) } -func (srv AlertmanagerSrv) RouteDeleteAlertingConfig(c *models.ReqContext) response.Response { +func (srv AlertmanagerSrv) RouteDeleteAlertingConfig(c *contextmodel.ReqContext) response.Response { am, errResp := srv.AlertmanagerFor(c.OrgID) if errResp != nil { return errResp @@ -106,7 +106,7 @@ func (srv AlertmanagerSrv) RouteDeleteAlertingConfig(c *models.ReqContext) respo return response.JSON(http.StatusAccepted, util.DynMap{"message": "configuration deleted; the default is applied"}) } -func (srv AlertmanagerSrv) RouteDeleteSilence(c *models.ReqContext, silenceID string) response.Response { +func (srv AlertmanagerSrv) RouteDeleteSilence(c *contextmodel.ReqContext, silenceID string) response.Response { am, errResp := srv.AlertmanagerFor(c.OrgID) if errResp != nil { return errResp @@ -121,7 +121,7 @@ func (srv AlertmanagerSrv) RouteDeleteSilence(c *models.ReqContext, silenceID st return response.JSON(http.StatusOK, util.DynMap{"message": "silence deleted"}) } -func (srv AlertmanagerSrv) RouteGetAlertingConfig(c *models.ReqContext) response.Response { +func (srv AlertmanagerSrv) RouteGetAlertingConfig(c *contextmodel.ReqContext) response.Response { config, err := srv.mam.GetAlertmanagerConfiguration(c.Req.Context(), c.OrgID) if err != nil { if errors.Is(err, store.ErrNoAlertmanagerConfiguration) { @@ -132,7 +132,7 @@ func (srv AlertmanagerSrv) RouteGetAlertingConfig(c *models.ReqContext) response return response.JSON(http.StatusOK, config) } -func (srv AlertmanagerSrv) RouteGetAMAlertGroups(c *models.ReqContext) response.Response { +func (srv AlertmanagerSrv) RouteGetAMAlertGroups(c *contextmodel.ReqContext) response.Response { am, errResp := srv.AlertmanagerFor(c.OrgID) if errResp != nil { return errResp @@ -156,7 +156,7 @@ func (srv AlertmanagerSrv) RouteGetAMAlertGroups(c *models.ReqContext) response. return response.JSON(http.StatusOK, groups) } -func (srv AlertmanagerSrv) RouteGetAMAlerts(c *models.ReqContext) response.Response { +func (srv AlertmanagerSrv) RouteGetAMAlerts(c *contextmodel.ReqContext) response.Response { am, errResp := srv.AlertmanagerFor(c.OrgID) if errResp != nil { return errResp @@ -183,7 +183,7 @@ func (srv AlertmanagerSrv) RouteGetAMAlerts(c *models.ReqContext) response.Respo return response.JSON(http.StatusOK, alerts) } -func (srv AlertmanagerSrv) RouteGetSilence(c *models.ReqContext, silenceID string) response.Response { +func (srv AlertmanagerSrv) RouteGetSilence(c *contextmodel.ReqContext, silenceID string) response.Response { am, errResp := srv.AlertmanagerFor(c.OrgID) if errResp != nil { return errResp @@ -200,7 +200,7 @@ func (srv AlertmanagerSrv) RouteGetSilence(c *models.ReqContext, silenceID strin return response.JSON(http.StatusOK, gettableSilence) } -func (srv AlertmanagerSrv) RouteGetSilences(c *models.ReqContext) response.Response { +func (srv AlertmanagerSrv) RouteGetSilences(c *contextmodel.ReqContext) response.Response { am, errResp := srv.AlertmanagerFor(c.OrgID) if errResp != nil { return errResp @@ -217,7 +217,7 @@ func (srv AlertmanagerSrv) RouteGetSilences(c *models.ReqContext) response.Respo return response.JSON(http.StatusOK, gettableSilences) } -func (srv AlertmanagerSrv) RoutePostAlertingConfig(c *models.ReqContext, body apimodels.PostableUserConfig) response.Response { +func (srv AlertmanagerSrv) RoutePostAlertingConfig(c *contextmodel.ReqContext, body apimodels.PostableUserConfig) response.Response { currentConfig, err := srv.mam.GetAlertmanagerConfiguration(c.Req.Context(), c.OrgID) // If a config is present and valid we proceed with the guard, otherwise we // just bypass the guard which is okay as we are anyway in an invalid state. @@ -248,7 +248,7 @@ func (srv AlertmanagerSrv) RoutePostAlertingConfig(c *models.ReqContext, body ap return ErrResp(http.StatusInternalServerError, err, "") } -func (srv AlertmanagerSrv) RouteGetReceivers(c *models.ReqContext) response.Response { +func (srv AlertmanagerSrv) RouteGetReceivers(c *contextmodel.ReqContext) response.Response { am, errResp := srv.AlertmanagerFor(c.OrgID) if errResp != nil { return errResp @@ -258,7 +258,7 @@ func (srv AlertmanagerSrv) RouteGetReceivers(c *models.ReqContext) response.Resp return response.JSON(http.StatusOK, rcvs) } -func (srv AlertmanagerSrv) RoutePostTestReceivers(c *models.ReqContext, body apimodels.TestReceiversConfigBodyParams) response.Response { +func (srv AlertmanagerSrv) RoutePostTestReceivers(c *contextmodel.ReqContext, body apimodels.TestReceiversConfigBodyParams) response.Response { if err := srv.crypto.LoadSecureSettings(c.Req.Context(), c.OrgID, body.Receivers); err != nil { var unknownReceiverError UnknownReceiverError if errors.As(err, &unknownReceiverError) { diff --git a/pkg/services/ngalert/api/api_alertmanager_test.go b/pkg/services/ngalert/api/api_alertmanager_test.go index 0553775fc66..ce9cb47d4b2 100644 --- a/pkg/services/ngalert/api/api_alertmanager_test.go +++ b/pkg/services/ngalert/api/api_alertmanager_test.go @@ -16,9 +16,9 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" acMock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/metrics" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" @@ -166,7 +166,7 @@ func TestAlertmanagerConfig(t *testing.T) { sut := createSut(t, nil) t.Run("assert 404 Not Found when applying config to nonexistent org", func(t *testing.T) { - rc := models.ReqContext{ + rc := contextmodel.ReqContext{ Context: &web.Context{ Req: &http.Request{}, }, @@ -183,7 +183,7 @@ func TestAlertmanagerConfig(t *testing.T) { }) t.Run("assert 202 when config successfully applied", func(t *testing.T) { - rc := models.ReqContext{ + rc := contextmodel.ReqContext{ Context: &web.Context{ Req: &http.Request{}, }, @@ -200,7 +200,7 @@ func TestAlertmanagerConfig(t *testing.T) { t.Run("assert 202 when alertmanager to configure is not ready", func(t *testing.T) { sut := createSut(t, nil) - rc := models.ReqContext{ + rc := contextmodel.ReqContext{ Context: &web.Context{ Req: &http.Request{}, }, @@ -330,7 +330,7 @@ func TestSilenceCreate(t *testing.T) { for _, cas := range cases { t.Run(cas.name, func(t *testing.T) { - rc := models.ReqContext{ + rc := contextmodel.ReqContext{ Context: &web.Context{ Req: &http.Request{}, }, @@ -456,7 +456,7 @@ func TestRouteCreateSilence(t *testing.T) { ac := tesCase.accessControl() sut := createSut(t, ac) - rc := models.ReqContext{ + rc := contextmodel.ReqContext{ Context: &web.Context{ Req: &http.Request{}, }, @@ -622,8 +622,8 @@ func withEmptyID(silence *apimodels.PostableSilence) { silence.ID = "" } -func createRequestCtxInOrg(org int64) *models.ReqContext { - return &models.ReqContext{ +func createRequestCtxInOrg(org int64) *contextmodel.ReqContext { + return &contextmodel.ReqContext{ Context: &web.Context{ Req: &http.Request{}, }, diff --git a/pkg/services/ngalert/api/api_configuration.go b/pkg/services/ngalert/api/api_configuration.go index 4d78029bd64..9ffb08c4ff5 100644 --- a/pkg/services/ngalert/api/api_configuration.go +++ b/pkg/services/ngalert/api/api_configuration.go @@ -8,7 +8,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" @@ -26,7 +26,7 @@ type ConfigSrv struct { log log.Logger } -func (srv ConfigSrv) RouteGetAlertmanagers(c *models.ReqContext) response.Response { +func (srv ConfigSrv) RouteGetAlertmanagers(c *contextmodel.ReqContext) response.Response { urls := srv.alertmanagerProvider.AlertmanagersFor(c.OrgID) droppedURLs := srv.alertmanagerProvider.DroppedAlertmanagersFor(c.OrgID) ams := v1.AlertManagersResult{Active: make([]v1.AlertManager, len(urls)), Dropped: make([]v1.AlertManager, len(droppedURLs))} @@ -43,7 +43,7 @@ func (srv ConfigSrv) RouteGetAlertmanagers(c *models.ReqContext) response.Respon }) } -func (srv ConfigSrv) RouteGetNGalertConfig(c *models.ReqContext) response.Response { +func (srv ConfigSrv) RouteGetNGalertConfig(c *contextmodel.ReqContext) response.Response { if c.OrgRole != org.RoleAdmin { return accessForbiddenResp() } @@ -65,7 +65,7 @@ func (srv ConfigSrv) RouteGetNGalertConfig(c *models.ReqContext) response.Respon return response.JSON(http.StatusOK, resp) } -func (srv ConfigSrv) RoutePostNGalertConfig(c *models.ReqContext, body apimodels.PostableNGalertConfig) response.Response { +func (srv ConfigSrv) RoutePostNGalertConfig(c *contextmodel.ReqContext, body apimodels.PostableNGalertConfig) response.Response { if c.OrgRole != org.RoleAdmin { return accessForbiddenResp() } @@ -99,7 +99,7 @@ func (srv ConfigSrv) RoutePostNGalertConfig(c *models.ReqContext, body apimodels return response.JSON(http.StatusCreated, util.DynMap{"message": "admin configuration updated"}) } -func (srv ConfigSrv) RouteDeleteNGalertConfig(c *models.ReqContext) response.Response { +func (srv ConfigSrv) RouteDeleteNGalertConfig(c *contextmodel.ReqContext) response.Response { if c.OrgRole != org.RoleAdmin { return accessForbiddenResp() } @@ -135,7 +135,7 @@ func (srv ConfigSrv) externalAlertmanagers(ctx context.Context, orgID int64) ([] return alertmanagers, nil } -func (srv ConfigSrv) RouteGetAlertingStatus(c *models.ReqContext) response.Response { +func (srv ConfigSrv) RouteGetAlertingStatus(c *contextmodel.ReqContext) response.Response { sendsAlertsTo := ngmodels.InternalAlertmanager cfg, err := srv.store.GetAdminConfiguration(c.OrgID) diff --git a/pkg/services/ngalert/api/api_prometheus.go b/pkg/services/ngalert/api/api_prometheus.go index c894258bf20..7c1a682e9fe 100644 --- a/pkg/services/ngalert/api/api_prometheus.go +++ b/pkg/services/ngalert/api/api_prometheus.go @@ -12,8 +12,8 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/folder" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/eval" @@ -32,7 +32,7 @@ type PrometheusSrv struct { const queryIncludeInternalLabels = "includeInternalLabels" -func (srv PrometheusSrv) RouteGetAlertStatuses(c *models.ReqContext) response.Response { +func (srv PrometheusSrv) RouteGetAlertStatuses(c *contextmodel.ReqContext) response.Response { alertResponse := apimodels.AlertResponse{ DiscoveryBase: apimodels.DiscoveryBase{ Status: "success", @@ -105,7 +105,7 @@ func getPanelIDFromRequest(r *http.Request) (int64, error) { return 0, nil } -func (srv PrometheusSrv) RouteGetRuleStatuses(c *models.ReqContext) response.Response { +func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) response.Response { dashboardUID := c.Query("dashboard_uid") panelID, err := getPanelIDFromRequest(c.Req) if err != nil { diff --git a/pkg/services/ngalert/api/api_prometheus_test.go b/pkg/services/ngalert/api/api_prometheus_test.go index 4bf06b8b4a4..0014b331aa1 100644 --- a/pkg/services/ngalert/api/api_prometheus_test.go +++ b/pkg/services/ngalert/api/api_prometheus_test.go @@ -15,8 +15,8 @@ import ( alertingModels "github.com/grafana/alerting/alerting/models" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/eval" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" @@ -93,7 +93,7 @@ func TestRouteGetAlertStatuses(t *testing.T) { _, _, _, api := setupAPI(t) req, err := http.NewRequest("GET", "/api/v1/alerts", nil) require.NoError(t, err) - c := &models.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgID: orgID}} + c := &contextmodel.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgID: orgID}} r := api.RouteGetAlertStatuses(c) require.Equal(t, http.StatusOK, r.Status()) @@ -112,7 +112,7 @@ func TestRouteGetAlertStatuses(t *testing.T) { fakeAIM.GenerateAlertInstances(1, util.GenerateShortUID(), 2) req, err := http.NewRequest("GET", "/api/v1/alerts", nil) require.NoError(t, err) - c := &models.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgID: orgID}} + c := &contextmodel.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgID: orgID}} r := api.RouteGetAlertStatuses(c) require.Equal(t, http.StatusOK, r.Status()) @@ -154,7 +154,7 @@ func TestRouteGetAlertStatuses(t *testing.T) { fakeAIM.GenerateAlertInstances(1, util.GenerateShortUID(), 2, withAlertingState()) req, err := http.NewRequest("GET", "/api/v1/alerts", nil) require.NoError(t, err) - c := &models.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgID: orgID}} + c := &contextmodel.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgID: orgID}} r := api.RouteGetAlertStatuses(c) require.Equal(t, http.StatusOK, r.Status()) @@ -196,7 +196,7 @@ func TestRouteGetAlertStatuses(t *testing.T) { fakeAIM.GenerateAlertInstances(orgID, util.GenerateShortUID(), 2) req, err := http.NewRequest("GET", "/api/v1/alerts?includeInternalLabels=true", nil) require.NoError(t, err) - c := &models.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgID: orgID}} + c := &contextmodel.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgID: orgID}} r := api.RouteGetAlertStatuses(c) require.Equal(t, http.StatusOK, r.Status()) @@ -258,7 +258,7 @@ func TestRouteGetRuleStatuses(t *testing.T) { req, err := http.NewRequest("GET", "/api/v1/rules", nil) require.NoError(t, err) - c := &models.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer}} + c := &contextmodel.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer}} t.Run("with no rules", func(t *testing.T) { _, _, _, api := setupAPI(t) @@ -328,7 +328,7 @@ func TestRouteGetRuleStatuses(t *testing.T) { req, err := http.NewRequest("GET", "/api/v1/rules?includeInternalLabels=true", nil) require.NoError(t, err) - c := &models.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer}} + c := &contextmodel.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer}} r := api.RouteGetRuleStatuses(c) require.Equal(t, http.StatusOK, r.Status()) diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index 748b78106aa..35f2e8c99bf 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -7,7 +7,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" alerting_models "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/provisioning" @@ -62,7 +62,7 @@ type AlertRuleService interface { ReplaceRuleGroup(ctx context.Context, orgID int64, group alerting_models.AlertRuleGroup, userID int64, provenance alerting_models.Provenance) error } -func (srv *ProvisioningSrv) RouteGetPolicyTree(c *models.ReqContext) response.Response { +func (srv *ProvisioningSrv) RouteGetPolicyTree(c *contextmodel.ReqContext) response.Response { policies, err := srv.policies.GetPolicyTree(c.Req.Context(), c.OrgID) if errors.Is(err, store.ErrNoAlertmanagerConfiguration) { return ErrResp(http.StatusNotFound, err, "") @@ -74,7 +74,7 @@ func (srv *ProvisioningSrv) RouteGetPolicyTree(c *models.ReqContext) response.Re return response.JSON(http.StatusOK, policies) } -func (srv *ProvisioningSrv) RoutePutPolicyTree(c *models.ReqContext, tree definitions.Route) response.Response { +func (srv *ProvisioningSrv) RoutePutPolicyTree(c *contextmodel.ReqContext, tree definitions.Route) response.Response { err := srv.policies.UpdatePolicyTree(c.Req.Context(), c.OrgID, tree, alerting_models.ProvenanceAPI) if errors.Is(err, store.ErrNoAlertmanagerConfiguration) { return ErrResp(http.StatusNotFound, err, "") @@ -89,7 +89,7 @@ func (srv *ProvisioningSrv) RoutePutPolicyTree(c *models.ReqContext, tree defini return response.JSON(http.StatusAccepted, util.DynMap{"message": "policies updated"}) } -func (srv *ProvisioningSrv) RouteResetPolicyTree(c *models.ReqContext) response.Response { +func (srv *ProvisioningSrv) RouteResetPolicyTree(c *contextmodel.ReqContext) response.Response { tree, err := srv.policies.ResetPolicyTree(c.Req.Context(), c.OrgID) if err != nil { return ErrResp(http.StatusInternalServerError, err, "") @@ -97,7 +97,7 @@ func (srv *ProvisioningSrv) RouteResetPolicyTree(c *models.ReqContext) response. return response.JSON(http.StatusAccepted, tree) } -func (srv *ProvisioningSrv) RouteGetContactPoints(c *models.ReqContext) response.Response { +func (srv *ProvisioningSrv) RouteGetContactPoints(c *contextmodel.ReqContext) response.Response { q := provisioning.ContactPointQuery{ Name: c.Query("name"), OrgID: c.OrgID, @@ -109,7 +109,7 @@ func (srv *ProvisioningSrv) RouteGetContactPoints(c *models.ReqContext) response return response.JSON(http.StatusOK, cps) } -func (srv *ProvisioningSrv) RoutePostContactPoint(c *models.ReqContext, cp definitions.EmbeddedContactPoint) response.Response { +func (srv *ProvisioningSrv) RoutePostContactPoint(c *contextmodel.ReqContext, cp definitions.EmbeddedContactPoint) response.Response { // TODO: provenance is hardcoded for now, change it later to make it more flexible contactPoint, err := srv.contactPointService.CreateContactPoint(c.Req.Context(), c.OrgID, cp, alerting_models.ProvenanceAPI) if errors.Is(err, provisioning.ErrValidation) { @@ -121,7 +121,7 @@ func (srv *ProvisioningSrv) RoutePostContactPoint(c *models.ReqContext, cp defin return response.JSON(http.StatusAccepted, contactPoint) } -func (srv *ProvisioningSrv) RoutePutContactPoint(c *models.ReqContext, cp definitions.EmbeddedContactPoint, UID string) response.Response { +func (srv *ProvisioningSrv) RoutePutContactPoint(c *contextmodel.ReqContext, cp definitions.EmbeddedContactPoint, UID string) response.Response { cp.UID = UID err := srv.contactPointService.UpdateContactPoint(c.Req.Context(), c.OrgID, cp, alerting_models.ProvenanceAPI) if errors.Is(err, provisioning.ErrValidation) { @@ -136,7 +136,7 @@ func (srv *ProvisioningSrv) RoutePutContactPoint(c *models.ReqContext, cp defini return response.JSON(http.StatusAccepted, util.DynMap{"message": "contactpoint updated"}) } -func (srv *ProvisioningSrv) RouteDeleteContactPoint(c *models.ReqContext, UID string) response.Response { +func (srv *ProvisioningSrv) RouteDeleteContactPoint(c *contextmodel.ReqContext, UID string) response.Response { err := srv.contactPointService.DeleteContactPoint(c.Req.Context(), c.OrgID, UID) if err != nil { return ErrResp(http.StatusInternalServerError, err, "") @@ -144,7 +144,7 @@ func (srv *ProvisioningSrv) RouteDeleteContactPoint(c *models.ReqContext, UID st return response.JSON(http.StatusAccepted, util.DynMap{"message": "contactpoint deleted"}) } -func (srv *ProvisioningSrv) RouteGetTemplates(c *models.ReqContext) response.Response { +func (srv *ProvisioningSrv) RouteGetTemplates(c *contextmodel.ReqContext) response.Response { templates, err := srv.templates.GetTemplates(c.Req.Context(), c.OrgID) if err != nil { return ErrResp(http.StatusInternalServerError, err, "") @@ -156,7 +156,7 @@ func (srv *ProvisioningSrv) RouteGetTemplates(c *models.ReqContext) response.Res return response.JSON(http.StatusOK, result) } -func (srv *ProvisioningSrv) RouteGetTemplate(c *models.ReqContext, name string) response.Response { +func (srv *ProvisioningSrv) RouteGetTemplate(c *contextmodel.ReqContext, name string) response.Response { templates, err := srv.templates.GetTemplates(c.Req.Context(), c.OrgID) if err != nil { return ErrResp(http.StatusInternalServerError, err, "") @@ -167,7 +167,7 @@ func (srv *ProvisioningSrv) RouteGetTemplate(c *models.ReqContext, name string) return response.Empty(http.StatusNotFound) } -func (srv *ProvisioningSrv) RoutePutTemplate(c *models.ReqContext, body definitions.NotificationTemplateContent, name string) response.Response { +func (srv *ProvisioningSrv) RoutePutTemplate(c *contextmodel.ReqContext, body definitions.NotificationTemplateContent, name string) response.Response { tmpl := definitions.NotificationTemplate{ Name: name, Template: body.Template, @@ -183,7 +183,7 @@ func (srv *ProvisioningSrv) RoutePutTemplate(c *models.ReqContext, body definiti return response.JSON(http.StatusAccepted, modified) } -func (srv *ProvisioningSrv) RouteDeleteTemplate(c *models.ReqContext, name string) response.Response { +func (srv *ProvisioningSrv) RouteDeleteTemplate(c *contextmodel.ReqContext, name string) response.Response { err := srv.templates.DeleteTemplate(c.Req.Context(), c.OrgID, name) if err != nil { return ErrResp(http.StatusInternalServerError, err, "") @@ -191,7 +191,7 @@ func (srv *ProvisioningSrv) RouteDeleteTemplate(c *models.ReqContext, name strin return response.JSON(http.StatusNoContent, nil) } -func (srv *ProvisioningSrv) RouteGetMuteTiming(c *models.ReqContext, name string) response.Response { +func (srv *ProvisioningSrv) RouteGetMuteTiming(c *contextmodel.ReqContext, name string) response.Response { timings, err := srv.muteTimings.GetMuteTimings(c.Req.Context(), c.OrgID) if err != nil { return ErrResp(http.StatusInternalServerError, err, "") @@ -204,7 +204,7 @@ func (srv *ProvisioningSrv) RouteGetMuteTiming(c *models.ReqContext, name string return response.Empty(http.StatusNotFound) } -func (srv *ProvisioningSrv) RouteGetMuteTimings(c *models.ReqContext) response.Response { +func (srv *ProvisioningSrv) RouteGetMuteTimings(c *contextmodel.ReqContext) response.Response { timings, err := srv.muteTimings.GetMuteTimings(c.Req.Context(), c.OrgID) if err != nil { return ErrResp(http.StatusInternalServerError, err, "") @@ -212,7 +212,7 @@ func (srv *ProvisioningSrv) RouteGetMuteTimings(c *models.ReqContext) response.R return response.JSON(http.StatusOK, timings) } -func (srv *ProvisioningSrv) RoutePostMuteTiming(c *models.ReqContext, mt definitions.MuteTimeInterval) response.Response { +func (srv *ProvisioningSrv) RoutePostMuteTiming(c *contextmodel.ReqContext, mt definitions.MuteTimeInterval) response.Response { mt.Provenance = alerting_models.ProvenanceAPI created, err := srv.muteTimings.CreateMuteTiming(c.Req.Context(), mt, c.OrgID) if err != nil { @@ -224,7 +224,7 @@ func (srv *ProvisioningSrv) RoutePostMuteTiming(c *models.ReqContext, mt definit return response.JSON(http.StatusCreated, created) } -func (srv *ProvisioningSrv) RoutePutMuteTiming(c *models.ReqContext, mt definitions.MuteTimeInterval, name string) response.Response { +func (srv *ProvisioningSrv) RoutePutMuteTiming(c *contextmodel.ReqContext, mt definitions.MuteTimeInterval, name string) response.Response { mt.Name = name mt.Provenance = alerting_models.ProvenanceAPI updated, err := srv.muteTimings.UpdateMuteTiming(c.Req.Context(), mt, c.OrgID) @@ -240,7 +240,7 @@ func (srv *ProvisioningSrv) RoutePutMuteTiming(c *models.ReqContext, mt definiti return response.JSON(http.StatusAccepted, updated) } -func (srv *ProvisioningSrv) RouteDeleteMuteTiming(c *models.ReqContext, name string) response.Response { +func (srv *ProvisioningSrv) RouteDeleteMuteTiming(c *contextmodel.ReqContext, name string) response.Response { err := srv.muteTimings.DeleteMuteTiming(c.Req.Context(), name, c.OrgID) if err != nil { return ErrResp(http.StatusInternalServerError, err, "") @@ -248,7 +248,7 @@ func (srv *ProvisioningSrv) RouteDeleteMuteTiming(c *models.ReqContext, name str return response.JSON(http.StatusNoContent, nil) } -func (srv *ProvisioningSrv) RouteGetAlertRules(c *models.ReqContext) response.Response { +func (srv *ProvisioningSrv) RouteGetAlertRules(c *contextmodel.ReqContext) response.Response { rules, err := srv.alertRules.GetAlertRules(c.Req.Context(), c.OrgID) if err != nil { return ErrResp(http.StatusInternalServerError, err, "") @@ -256,7 +256,7 @@ func (srv *ProvisioningSrv) RouteGetAlertRules(c *models.ReqContext) response.Re return response.JSON(http.StatusOK, definitions.NewAlertRules(rules)) } -func (srv *ProvisioningSrv) RouteRouteGetAlertRule(c *models.ReqContext, UID string) response.Response { +func (srv *ProvisioningSrv) RouteRouteGetAlertRule(c *contextmodel.ReqContext, UID string) response.Response { rule, provenace, err := srv.alertRules.GetAlertRule(c.Req.Context(), c.OrgID, UID) if err != nil { return ErrResp(http.StatusInternalServerError, err, "") @@ -264,7 +264,7 @@ func (srv *ProvisioningSrv) RouteRouteGetAlertRule(c *models.ReqContext, UID str return response.JSON(http.StatusOK, definitions.NewAlertRule(rule, provenace)) } -func (srv *ProvisioningSrv) RoutePostAlertRule(c *models.ReqContext, ar definitions.ProvisionedAlertRule) response.Response { +func (srv *ProvisioningSrv) RoutePostAlertRule(c *contextmodel.ReqContext, ar definitions.ProvisionedAlertRule) response.Response { upstreamModel, err := ar.UpstreamModel() upstreamModel.OrgID = c.OrgID if err != nil { @@ -289,7 +289,7 @@ func (srv *ProvisioningSrv) RoutePostAlertRule(c *models.ReqContext, ar definiti return response.JSON(http.StatusCreated, resp) } -func (srv *ProvisioningSrv) RoutePutAlertRule(c *models.ReqContext, ar definitions.ProvisionedAlertRule, UID string) response.Response { +func (srv *ProvisioningSrv) RoutePutAlertRule(c *contextmodel.ReqContext, ar definitions.ProvisionedAlertRule, UID string) response.Response { updated, err := ar.UpstreamModel() if err != nil { ErrResp(http.StatusBadRequest, err, "") @@ -315,7 +315,7 @@ func (srv *ProvisioningSrv) RoutePutAlertRule(c *models.ReqContext, ar definitio return response.JSON(http.StatusOK, resp) } -func (srv *ProvisioningSrv) RouteDeleteAlertRule(c *models.ReqContext, UID string) response.Response { +func (srv *ProvisioningSrv) RouteDeleteAlertRule(c *contextmodel.ReqContext, UID string) response.Response { err := srv.alertRules.DeleteAlertRule(c.Req.Context(), c.OrgID, UID, alerting_models.ProvenanceAPI) if err != nil { return ErrResp(http.StatusInternalServerError, err, "") @@ -323,7 +323,7 @@ func (srv *ProvisioningSrv) RouteDeleteAlertRule(c *models.ReqContext, UID strin return response.JSON(http.StatusNoContent, "") } -func (srv *ProvisioningSrv) RouteGetAlertRuleGroup(c *models.ReqContext, folder string, group string) response.Response { +func (srv *ProvisioningSrv) RouteGetAlertRuleGroup(c *contextmodel.ReqContext, folder string, group string) response.Response { g, err := srv.alertRules.GetRuleGroup(c.Req.Context(), c.OrgID, folder, group) if err != nil { if errors.Is(err, store.ErrAlertRuleGroupNotFound) { @@ -334,7 +334,7 @@ func (srv *ProvisioningSrv) RouteGetAlertRuleGroup(c *models.ReqContext, folder return response.JSON(http.StatusOK, definitions.NewAlertRuleGroupFromModel(g)) } -func (srv *ProvisioningSrv) RoutePutAlertRuleGroup(c *models.ReqContext, ag definitions.AlertRuleGroup, folderUID string, group string) response.Response { +func (srv *ProvisioningSrv) RoutePutAlertRuleGroup(c *contextmodel.ReqContext, ag definitions.AlertRuleGroup, folderUID string, group string) response.Response { ag.FolderUID = folderUID ag.Title = group groupModel, err := ag.ToModel() @@ -354,7 +354,7 @@ func (srv *ProvisioningSrv) RoutePutAlertRuleGroup(c *models.ReqContext, ag defi return response.JSON(http.StatusOK, ag) } -func determineProvenance(ctx *models.ReqContext) alerting_models.Provenance { +func determineProvenance(ctx *contextmodel.ReqContext) alerting_models.Provenance { if _, disabled := ctx.Req.Header[disableProvenanceHeaderName]; disabled { return alerting_models.ProvenanceNone } diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index 240751e4c56..3408e0a34c5 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -16,7 +16,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" - gfcore "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/provisioning" @@ -438,8 +438,8 @@ func createProvisioningSrvSutFromEnv(t *testing.T, env *testEnvironment) Provisi } } -func createTestRequestCtx() gfcore.ReqContext { - return gfcore.ReqContext{ +func createTestRequestCtx() contextmodel.ReqContext { + return contextmodel.ReqContext{ Context: &web.Context{ Req: &http.Request{}, }, diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go index 635899411b2..732ec14dcdb 100644 --- a/pkg/services/ngalert/api/api_ruler.go +++ b/pkg/services/ngalert/api/api_ruler.go @@ -21,7 +21,7 @@ import ( "github.com/grafana/grafana/pkg/api/apierrors" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/schedule" @@ -53,7 +53,7 @@ var ( // or, if non-empty, a specific group of rules in the namespace. // Returns http.StatusUnauthorized if user does not have access to any of the rules that match the filter. // Returns http.StatusBadRequest if all rules that match the filter and the user is authorized to delete are provisioned. -func (srv RulerSrv) RouteDeleteAlertRules(c *models.ReqContext, namespaceTitle string, group string) response.Response { +func (srv RulerSrv) RouteDeleteAlertRules(c *contextmodel.ReqContext, namespaceTitle string, group string) response.Response { namespace, err := srv.store.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.OrgID, c.SignedInUser, true) if err != nil { return toNamespaceErrorResponse(err) @@ -155,7 +155,7 @@ func (srv RulerSrv) RouteDeleteAlertRules(c *models.ReqContext, namespaceTitle s } // RouteGetNamespaceRulesConfig returns all rules in a specific folder that user has access to -func (srv RulerSrv) RouteGetNamespaceRulesConfig(c *models.ReqContext, namespaceTitle string) response.Response { +func (srv RulerSrv) RouteGetNamespaceRulesConfig(c *contextmodel.ReqContext, namespaceTitle string) response.Response { namespace, err := srv.store.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.OrgID, c.SignedInUser, false) if err != nil { return toNamespaceErrorResponse(err) @@ -197,7 +197,7 @@ func (srv RulerSrv) RouteGetNamespaceRulesConfig(c *models.ReqContext, namespace // RouteGetRulesGroupConfig returns rules that belong to a specific group in a specific namespace (folder). // If user does not have access to at least one of the rule in the group, returns status 401 Unauthorized -func (srv RulerSrv) RouteGetRulesGroupConfig(c *models.ReqContext, namespaceTitle string, ruleGroup string) response.Response { +func (srv RulerSrv) RouteGetRulesGroupConfig(c *contextmodel.ReqContext, namespaceTitle string, ruleGroup string) response.Response { namespace, err := srv.store.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.OrgID, c.SignedInUser, false) if err != nil { return toNamespaceErrorResponse(err) @@ -232,7 +232,7 @@ func (srv RulerSrv) RouteGetRulesGroupConfig(c *models.ReqContext, namespaceTitl } // RouteGetRulesConfig returns all alert rules that are available to the current user -func (srv RulerSrv) RouteGetRulesConfig(c *models.ReqContext) response.Response { +func (srv RulerSrv) RouteGetRulesConfig(c *contextmodel.ReqContext) response.Response { namespaceMap, err := srv.store.GetUserVisibleNamespaces(c.Req.Context(), c.OrgID, c.SignedInUser) if err != nil { return ErrResp(http.StatusInternalServerError, err, "failed to get namespaces visible to the user") @@ -301,7 +301,7 @@ func (srv RulerSrv) RouteGetRulesConfig(c *models.ReqContext) response.Response return response.JSON(http.StatusOK, result) } -func (srv RulerSrv) RoutePostNameRulesConfig(c *models.ReqContext, ruleGroupConfig apimodels.PostableRuleGroupConfig, namespaceTitle string) response.Response { +func (srv RulerSrv) RoutePostNameRulesConfig(c *contextmodel.ReqContext, ruleGroupConfig apimodels.PostableRuleGroupConfig, namespaceTitle string) response.Response { namespace, err := srv.store.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.OrgID, c.SignedInUser, true) if err != nil { return toNamespaceErrorResponse(err) @@ -325,7 +325,7 @@ func (srv RulerSrv) RoutePostNameRulesConfig(c *models.ReqContext, ruleGroupConf // updateAlertRulesInGroup calculates changes (rules to add,update,delete), verifies that the user is authorized to do the calculated changes and updates database. // All operations are performed in a single transaction -func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, groupKey ngmodels.AlertRuleGroupKey, rules []*ngmodels.AlertRule) response.Response { +func (srv RulerSrv) updateAlertRulesInGroup(c *contextmodel.ReqContext, groupKey ngmodels.AlertRuleGroupKey, rules []*ngmodels.AlertRule) response.Response { var finalChanges *store.GroupDelta hasAccess := accesscontrol.HasAccess(srv.ac, c) err := srv.xactManager.InTransaction(c.Req.Context(), func(tranCtx context.Context) error { diff --git a/pkg/services/ngalert/api/api_ruler_test.go b/pkg/services/ngalert/api/api_ruler_test.go index 9f7d07103a2..43ee46caa8e 100644 --- a/pkg/services/ngalert/api/api_ruler_test.go +++ b/pkg/services/ngalert/api/api_ruler_test.go @@ -14,9 +14,9 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/log" - models2 "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" acMock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/folder" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" @@ -657,7 +657,7 @@ func createService(ac *acMock.Mock, store *fakes.RuleStore, scheduler schedule.S } } -func createRequestContext(orgID int64, role org.RoleType, params map[string]string) *models2.ReqContext { +func createRequestContext(orgID int64, role org.RoleType, params map[string]string) *contextmodel.ReqContext { uri, _ := url.Parse("http://localhost") ctx := web.Context{Req: &http.Request{ URL: uri, @@ -666,7 +666,7 @@ func createRequestContext(orgID int64, role org.RoleType, params map[string]stri ctx.Req = web.SetURLParams(ctx.Req, params) } - return &models2.ReqContext{ + return &contextmodel.ReqContext{ IsSignedIn: true, SignedInUser: &user.SignedInUser{ OrgRole: role, diff --git a/pkg/services/ngalert/api/api_testing.go b/pkg/services/ngalert/api/api_testing.go index 91a3d09a1dd..acc9e0d6614 100644 --- a/pkg/services/ngalert/api/api_testing.go +++ b/pkg/services/ngalert/api/api_testing.go @@ -12,8 +12,8 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" @@ -35,7 +35,7 @@ type TestingApiSrv struct { featureManager featuremgmt.FeatureToggles } -func (srv TestingApiSrv) RouteTestGrafanaRuleConfig(c *models.ReqContext, body apimodels.TestRulePayload) response.Response { +func (srv TestingApiSrv) RouteTestGrafanaRuleConfig(c *contextmodel.ReqContext, body apimodels.TestRulePayload) response.Response { if body.Type() != apimodels.GrafanaBackend || body.GrafanaManagedCondition == nil { return errorToResponse(backendTypeDoesNotMatchPayloadTypeError(apimodels.GrafanaBackend, body.Type().String())) } @@ -73,7 +73,7 @@ func (srv TestingApiSrv) RouteTestGrafanaRuleConfig(c *models.ReqContext, body a }) } -func (srv TestingApiSrv) RouteTestRuleConfig(c *models.ReqContext, body apimodels.TestRulePayload, datasourceUID string) response.Response { +func (srv TestingApiSrv) RouteTestRuleConfig(c *contextmodel.ReqContext, body apimodels.TestRulePayload, datasourceUID string) response.Response { if body.Type() != apimodels.LoTexRulerBackend { return errorToResponse(backendTypeDoesNotMatchPayloadTypeError(apimodels.LoTexRulerBackend, body.Type().String())) } @@ -111,7 +111,7 @@ func (srv TestingApiSrv) RouteTestRuleConfig(c *models.ReqContext, body apimodel ) } -func (srv TestingApiSrv) RouteEvalQueries(c *models.ReqContext, cmd apimodels.EvalQueriesPayload) response.Response { +func (srv TestingApiSrv) RouteEvalQueries(c *contextmodel.ReqContext, cmd apimodels.EvalQueriesPayload) response.Response { if !authorizeDatasourceAccessForRule(&ngmodels.AlertRule{Data: cmd.Data}, func(evaluator accesscontrol.Evaluator) bool { return accesscontrol.HasAccess(srv.accessControl, c)(accesscontrol.ReqSignedIn, evaluator) }) { @@ -145,7 +145,7 @@ func (srv TestingApiSrv) RouteEvalQueries(c *models.ReqContext, cmd apimodels.Ev return response.JSONStreaming(http.StatusOK, evalResults) } -func (srv TestingApiSrv) BacktestAlertRule(c *models.ReqContext, cmd apimodels.BacktestConfig) response.Response { +func (srv TestingApiSrv) BacktestAlertRule(c *contextmodel.ReqContext, cmd apimodels.BacktestConfig) response.Response { if !srv.featureManager.IsEnabled(featuremgmt.FlagAlertingBacktesting) { return ErrResp(http.StatusNotFound, nil, "Backgtesting API is not enabled") } diff --git a/pkg/services/ngalert/api/api_testing_test.go b/pkg/services/ngalert/api/api_testing_test.go index e76f5ca7749..af0d5d03047 100644 --- a/pkg/services/ngalert/api/api_testing_test.go +++ b/pkg/services/ngalert/api/api_testing_test.go @@ -9,9 +9,9 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - models2 "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" acMock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" fakes "github.com/grafana/grafana/pkg/services/datasources/fakes" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" @@ -24,7 +24,7 @@ import ( func TestRouteTestGrafanaRuleConfig(t *testing.T) { t.Run("when fine-grained access is enabled", func(t *testing.T) { - rc := &models2.ReqContext{ + rc := &contextmodel.ReqContext{ Context: &web.Context{ Req: &http.Request{}, }, @@ -95,7 +95,7 @@ func TestRouteTestGrafanaRuleConfig(t *testing.T) { }) t.Run("when fine-grained access is disabled", func(t *testing.T) { - rc := &models2.ReqContext{ + rc := &contextmodel.ReqContext{ Context: &web.Context{ Req: &http.Request{}, }, @@ -152,7 +152,7 @@ func TestRouteTestGrafanaRuleConfig(t *testing.T) { func TestRouteEvalQueries(t *testing.T) { t.Run("when fine-grained access is enabled", func(t *testing.T) { - rc := &models2.ReqContext{ + rc := &contextmodel.ReqContext{ Context: &web.Context{ Req: &http.Request{}, }, @@ -222,7 +222,7 @@ func TestRouteEvalQueries(t *testing.T) { }) t.Run("when fine-grained access is disabled", func(t *testing.T) { - rc := &models2.ReqContext{ + rc := &contextmodel.ReqContext{ Context: &web.Context{ Req: &http.Request{}, }, diff --git a/pkg/services/ngalert/api/configuration.go b/pkg/services/ngalert/api/configuration.go index 7bc382c92ab..4998f81a960 100644 --- a/pkg/services/ngalert/api/configuration.go +++ b/pkg/services/ngalert/api/configuration.go @@ -2,7 +2,7 @@ package api import ( "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ) @@ -17,22 +17,22 @@ func NewConfiguration(grafana *ConfigSrv) *ConfigurationApiHandler { } } -func (f *ConfigurationApiHandler) handleRouteGetAlertmanagers(c *models.ReqContext) response.Response { +func (f *ConfigurationApiHandler) handleRouteGetAlertmanagers(c *contextmodel.ReqContext) response.Response { return f.grafana.RouteGetAlertmanagers(c) } -func (f *ConfigurationApiHandler) handleRouteGetNGalertConfig(c *models.ReqContext) response.Response { +func (f *ConfigurationApiHandler) handleRouteGetNGalertConfig(c *contextmodel.ReqContext) response.Response { return f.grafana.RouteGetNGalertConfig(c) } -func (f *ConfigurationApiHandler) handleRoutePostNGalertConfig(c *models.ReqContext, body apimodels.PostableNGalertConfig) response.Response { +func (f *ConfigurationApiHandler) handleRoutePostNGalertConfig(c *contextmodel.ReqContext, body apimodels.PostableNGalertConfig) response.Response { return f.grafana.RoutePostNGalertConfig(c, body) } -func (f *ConfigurationApiHandler) handleRouteDeleteNGalertConfig(c *models.ReqContext) response.Response { +func (f *ConfigurationApiHandler) handleRouteDeleteNGalertConfig(c *contextmodel.ReqContext) response.Response { return f.grafana.RouteDeleteNGalertConfig(c) } -func (f *ConfigurationApiHandler) handleRouteGetStatus(c *models.ReqContext) response.Response { +func (f *ConfigurationApiHandler) handleRouteGetStatus(c *contextmodel.ReqContext) response.Response { return f.grafana.RouteGetAlertingStatus(c) } diff --git a/pkg/services/ngalert/api/forking_alertmanager.go b/pkg/services/ngalert/api/forking_alertmanager.go index c6d2a43ef8b..a45f35c9a98 100644 --- a/pkg/services/ngalert/api/forking_alertmanager.go +++ b/pkg/services/ngalert/api/forking_alertmanager.go @@ -2,7 +2,7 @@ package api import ( "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ) @@ -22,7 +22,7 @@ func NewForkingAM(datasourceCache datasources.CacheService, proxy *LotexAM, graf } } -func (f *AlertmanagerApiHandler) getService(ctx *models.ReqContext) (*LotexAM, error) { +func (f *AlertmanagerApiHandler) getService(ctx *contextmodel.ReqContext) (*LotexAM, error) { _, err := getDatasourceByUID(ctx, f.DatasourceCache, apimodels.AlertmanagerBackend) if err != nil { return nil, err @@ -30,7 +30,7 @@ func (f *AlertmanagerApiHandler) getService(ctx *models.ReqContext) (*LotexAM, e return f.AMSvc, nil } -func (f *AlertmanagerApiHandler) handleRouteGetAMStatus(ctx *models.ReqContext, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetAMStatus(ctx *contextmodel.ReqContext, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -39,7 +39,7 @@ func (f *AlertmanagerApiHandler) handleRouteGetAMStatus(ctx *models.ReqContext, return s.RouteGetAMStatus(ctx) } -func (f *AlertmanagerApiHandler) handleRouteCreateSilence(ctx *models.ReqContext, body apimodels.PostableSilence, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRouteCreateSilence(ctx *contextmodel.ReqContext, body apimodels.PostableSilence, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -48,7 +48,7 @@ func (f *AlertmanagerApiHandler) handleRouteCreateSilence(ctx *models.ReqContext return s.RouteCreateSilence(ctx, body) } -func (f *AlertmanagerApiHandler) handleRouteDeleteAlertingConfig(ctx *models.ReqContext, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRouteDeleteAlertingConfig(ctx *contextmodel.ReqContext, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -57,7 +57,7 @@ func (f *AlertmanagerApiHandler) handleRouteDeleteAlertingConfig(ctx *models.Req return s.RouteDeleteAlertingConfig(ctx) } -func (f *AlertmanagerApiHandler) handleRouteDeleteSilence(ctx *models.ReqContext, silenceID string, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRouteDeleteSilence(ctx *contextmodel.ReqContext, silenceID string, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -66,7 +66,7 @@ func (f *AlertmanagerApiHandler) handleRouteDeleteSilence(ctx *models.ReqContext return s.RouteDeleteSilence(ctx, silenceID) } -func (f *AlertmanagerApiHandler) handleRouteGetAlertingConfig(ctx *models.ReqContext, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetAlertingConfig(ctx *contextmodel.ReqContext, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -75,7 +75,7 @@ func (f *AlertmanagerApiHandler) handleRouteGetAlertingConfig(ctx *models.ReqCon return s.RouteGetAlertingConfig(ctx) } -func (f *AlertmanagerApiHandler) handleRouteGetAMAlertGroups(ctx *models.ReqContext, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetAMAlertGroups(ctx *contextmodel.ReqContext, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -84,7 +84,7 @@ func (f *AlertmanagerApiHandler) handleRouteGetAMAlertGroups(ctx *models.ReqCont return s.RouteGetAMAlertGroups(ctx) } -func (f *AlertmanagerApiHandler) handleRouteGetAMAlerts(ctx *models.ReqContext, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetAMAlerts(ctx *contextmodel.ReqContext, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -93,7 +93,7 @@ func (f *AlertmanagerApiHandler) handleRouteGetAMAlerts(ctx *models.ReqContext, return s.RouteGetAMAlerts(ctx) } -func (f *AlertmanagerApiHandler) handleRouteGetSilence(ctx *models.ReqContext, silenceID string, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetSilence(ctx *contextmodel.ReqContext, silenceID string, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -102,7 +102,7 @@ func (f *AlertmanagerApiHandler) handleRouteGetSilence(ctx *models.ReqContext, s return s.RouteGetSilence(ctx, silenceID) } -func (f *AlertmanagerApiHandler) handleRouteGetSilences(ctx *models.ReqContext, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetSilences(ctx *contextmodel.ReqContext, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -111,7 +111,7 @@ func (f *AlertmanagerApiHandler) handleRouteGetSilences(ctx *models.ReqContext, return s.RouteGetSilences(ctx) } -func (f *AlertmanagerApiHandler) handleRoutePostAlertingConfig(ctx *models.ReqContext, body apimodels.PostableUserConfig, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRoutePostAlertingConfig(ctx *contextmodel.ReqContext, body apimodels.PostableUserConfig, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -122,7 +122,7 @@ func (f *AlertmanagerApiHandler) handleRoutePostAlertingConfig(ctx *models.ReqCo return s.RoutePostAlertingConfig(ctx, body) } -func (f *AlertmanagerApiHandler) handleRoutePostAMAlerts(ctx *models.ReqContext, body apimodels.PostableAlerts, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRoutePostAMAlerts(ctx *contextmodel.ReqContext, body apimodels.PostableAlerts, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -131,53 +131,53 @@ func (f *AlertmanagerApiHandler) handleRoutePostAMAlerts(ctx *models.ReqContext, return s.RoutePostAMAlerts(ctx, body) } -func (f *AlertmanagerApiHandler) handleRouteDeleteGrafanaSilence(ctx *models.ReqContext, id string) response.Response { +func (f *AlertmanagerApiHandler) handleRouteDeleteGrafanaSilence(ctx *contextmodel.ReqContext, id string) response.Response { return f.GrafanaSvc.RouteDeleteSilence(ctx, id) } -func (f *AlertmanagerApiHandler) handleRouteDeleteGrafanaAlertingConfig(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) handleRouteDeleteGrafanaAlertingConfig(ctx *contextmodel.ReqContext) response.Response { return f.GrafanaSvc.RouteDeleteAlertingConfig(ctx) } -func (f *AlertmanagerApiHandler) handleRouteCreateGrafanaSilence(ctx *models.ReqContext, body apimodels.PostableSilence) response.Response { +func (f *AlertmanagerApiHandler) handleRouteCreateGrafanaSilence(ctx *contextmodel.ReqContext, body apimodels.PostableSilence) response.Response { return f.GrafanaSvc.RouteCreateSilence(ctx, body) } -func (f *AlertmanagerApiHandler) handleRouteGetGrafanaAMStatus(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetGrafanaAMStatus(ctx *contextmodel.ReqContext) response.Response { return f.GrafanaSvc.RouteGetAMStatus(ctx) } -func (f *AlertmanagerApiHandler) handleRouteGetGrafanaAMAlerts(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetGrafanaAMAlerts(ctx *contextmodel.ReqContext) response.Response { return f.GrafanaSvc.RouteGetAMAlerts(ctx) } -func (f *AlertmanagerApiHandler) handleRouteGetGrafanaAMAlertGroups(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetGrafanaAMAlertGroups(ctx *contextmodel.ReqContext) response.Response { return f.GrafanaSvc.RouteGetAMAlertGroups(ctx) } -func (f *AlertmanagerApiHandler) handleRouteGetGrafanaAlertingConfig(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetGrafanaAlertingConfig(ctx *contextmodel.ReqContext) response.Response { return f.GrafanaSvc.RouteGetAlertingConfig(ctx) } -func (f *AlertmanagerApiHandler) handleRouteGetGrafanaSilence(ctx *models.ReqContext, id string) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetGrafanaSilence(ctx *contextmodel.ReqContext, id string) response.Response { return f.GrafanaSvc.RouteGetSilence(ctx, id) } -func (f *AlertmanagerApiHandler) handleRouteGetGrafanaSilences(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetGrafanaSilences(ctx *contextmodel.ReqContext) response.Response { return f.GrafanaSvc.RouteGetSilences(ctx) } -func (f *AlertmanagerApiHandler) handleRoutePostGrafanaAlertingConfig(ctx *models.ReqContext, conf apimodels.PostableUserConfig) response.Response { +func (f *AlertmanagerApiHandler) handleRoutePostGrafanaAlertingConfig(ctx *contextmodel.ReqContext, conf apimodels.PostableUserConfig) response.Response { if !conf.AlertmanagerConfig.ReceiverType().Can(apimodels.GrafanaReceiverType) { return errorToResponse(backendTypeDoesNotMatchPayloadTypeError(apimodels.GrafanaBackend, conf.AlertmanagerConfig.ReceiverType().String())) } return f.GrafanaSvc.RoutePostAlertingConfig(ctx, conf) } -func (f *AlertmanagerApiHandler) handleRouteGetGrafanaReceivers(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetGrafanaReceivers(ctx *contextmodel.ReqContext) response.Response { return f.GrafanaSvc.RouteGetReceivers(ctx) } -func (f *AlertmanagerApiHandler) handleRoutePostTestGrafanaReceivers(ctx *models.ReqContext, conf apimodels.TestReceiversConfigBodyParams) response.Response { +func (f *AlertmanagerApiHandler) handleRoutePostTestGrafanaReceivers(ctx *contextmodel.ReqContext, conf apimodels.TestReceiversConfigBodyParams) response.Response { return f.GrafanaSvc.RoutePostTestReceivers(ctx, conf) } diff --git a/pkg/services/ngalert/api/forking_prometheus.go b/pkg/services/ngalert/api/forking_prometheus.go index 492983e6a7e..c0fa78b9f74 100644 --- a/pkg/services/ngalert/api/forking_prometheus.go +++ b/pkg/services/ngalert/api/forking_prometheus.go @@ -2,7 +2,7 @@ package api import ( "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ) @@ -22,7 +22,7 @@ func NewForkingProm(datasourceCache datasources.CacheService, proxy *LotexProm, } } -func (f *PrometheusApiHandler) handleRouteGetAlertStatuses(ctx *models.ReqContext, dsUID string) response.Response { +func (f *PrometheusApiHandler) handleRouteGetAlertStatuses(ctx *contextmodel.ReqContext, dsUID string) response.Response { t, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -30,7 +30,7 @@ func (f *PrometheusApiHandler) handleRouteGetAlertStatuses(ctx *models.ReqContex return t.RouteGetAlertStatuses(ctx) } -func (f *PrometheusApiHandler) handleRouteGetRuleStatuses(ctx *models.ReqContext, dsUID string) response.Response { +func (f *PrometheusApiHandler) handleRouteGetRuleStatuses(ctx *contextmodel.ReqContext, dsUID string) response.Response { t, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -38,15 +38,15 @@ func (f *PrometheusApiHandler) handleRouteGetRuleStatuses(ctx *models.ReqContext return t.RouteGetRuleStatuses(ctx) } -func (f *PrometheusApiHandler) handleRouteGetGrafanaAlertStatuses(ctx *models.ReqContext) response.Response { +func (f *PrometheusApiHandler) handleRouteGetGrafanaAlertStatuses(ctx *contextmodel.ReqContext) response.Response { return f.GrafanaSvc.RouteGetAlertStatuses(ctx) } -func (f *PrometheusApiHandler) handleRouteGetGrafanaRuleStatuses(ctx *models.ReqContext) response.Response { +func (f *PrometheusApiHandler) handleRouteGetGrafanaRuleStatuses(ctx *contextmodel.ReqContext) response.Response { return f.GrafanaSvc.RouteGetRuleStatuses(ctx) } -func (f *PrometheusApiHandler) getService(ctx *models.ReqContext) (*LotexProm, error) { +func (f *PrometheusApiHandler) getService(ctx *contextmodel.ReqContext) (*LotexProm, error) { _, err := getDatasourceByUID(ctx, f.DatasourceCache, apimodels.LoTexRulerBackend) if err != nil { return nil, err diff --git a/pkg/services/ngalert/api/forking_ruler.go b/pkg/services/ngalert/api/forking_ruler.go index b415204bca4..e26afac7abb 100644 --- a/pkg/services/ngalert/api/forking_ruler.go +++ b/pkg/services/ngalert/api/forking_ruler.go @@ -2,7 +2,7 @@ package api import ( "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ) @@ -22,7 +22,7 @@ func NewForkingRuler(datasourceCache datasources.CacheService, lotex *LotexRuler } } -func (f *RulerApiHandler) handleRouteDeleteNamespaceRulesConfig(ctx *models.ReqContext, dsUID, namespace string) response.Response { +func (f *RulerApiHandler) handleRouteDeleteNamespaceRulesConfig(ctx *contextmodel.ReqContext, dsUID, namespace string) response.Response { t, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -30,7 +30,7 @@ func (f *RulerApiHandler) handleRouteDeleteNamespaceRulesConfig(ctx *models.ReqC return t.RouteDeleteNamespaceRulesConfig(ctx, namespace) } -func (f *RulerApiHandler) handleRouteDeleteRuleGroupConfig(ctx *models.ReqContext, dsUID, namespace, group string) response.Response { +func (f *RulerApiHandler) handleRouteDeleteRuleGroupConfig(ctx *contextmodel.ReqContext, dsUID, namespace, group string) response.Response { t, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -38,7 +38,7 @@ func (f *RulerApiHandler) handleRouteDeleteRuleGroupConfig(ctx *models.ReqContex return t.RouteDeleteRuleGroupConfig(ctx, namespace, group) } -func (f *RulerApiHandler) handleRouteGetNamespaceRulesConfig(ctx *models.ReqContext, dsUID, namespace string) response.Response { +func (f *RulerApiHandler) handleRouteGetNamespaceRulesConfig(ctx *contextmodel.ReqContext, dsUID, namespace string) response.Response { t, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -46,7 +46,7 @@ func (f *RulerApiHandler) handleRouteGetNamespaceRulesConfig(ctx *models.ReqCont return t.RouteGetNamespaceRulesConfig(ctx, namespace) } -func (f *RulerApiHandler) handleRouteGetRulegGroupConfig(ctx *models.ReqContext, dsUID, namespace, group string) response.Response { +func (f *RulerApiHandler) handleRouteGetRulegGroupConfig(ctx *contextmodel.ReqContext, dsUID, namespace, group string) response.Response { t, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -54,7 +54,7 @@ func (f *RulerApiHandler) handleRouteGetRulegGroupConfig(ctx *models.ReqContext, return t.RouteGetRulegGroupConfig(ctx, namespace, group) } -func (f *RulerApiHandler) handleRouteGetRulesConfig(ctx *models.ReqContext, dsUID string) response.Response { +func (f *RulerApiHandler) handleRouteGetRulesConfig(ctx *contextmodel.ReqContext, dsUID string) response.Response { t, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -62,7 +62,7 @@ func (f *RulerApiHandler) handleRouteGetRulesConfig(ctx *models.ReqContext, dsUI return t.RouteGetRulesConfig(ctx) } -func (f *RulerApiHandler) handleRoutePostNameRulesConfig(ctx *models.ReqContext, conf apimodels.PostableRuleGroupConfig, dsUID, namespace string) response.Response { +func (f *RulerApiHandler) handleRoutePostNameRulesConfig(ctx *contextmodel.ReqContext, conf apimodels.PostableRuleGroupConfig, dsUID, namespace string) response.Response { t, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -73,27 +73,27 @@ func (f *RulerApiHandler) handleRoutePostNameRulesConfig(ctx *models.ReqContext, return t.RoutePostNameRulesConfig(ctx, conf, namespace) } -func (f *RulerApiHandler) handleRouteDeleteNamespaceGrafanaRulesConfig(ctx *models.ReqContext, namespace string) response.Response { +func (f *RulerApiHandler) handleRouteDeleteNamespaceGrafanaRulesConfig(ctx *contextmodel.ReqContext, namespace string) response.Response { return f.GrafanaRuler.RouteDeleteAlertRules(ctx, namespace, "") } -func (f *RulerApiHandler) handleRouteDeleteGrafanaRuleGroupConfig(ctx *models.ReqContext, namespace, groupName string) response.Response { +func (f *RulerApiHandler) handleRouteDeleteGrafanaRuleGroupConfig(ctx *contextmodel.ReqContext, namespace, groupName string) response.Response { return f.GrafanaRuler.RouteDeleteAlertRules(ctx, namespace, groupName) } -func (f *RulerApiHandler) handleRouteGetNamespaceGrafanaRulesConfig(ctx *models.ReqContext, namespace string) response.Response { +func (f *RulerApiHandler) handleRouteGetNamespaceGrafanaRulesConfig(ctx *contextmodel.ReqContext, namespace string) response.Response { return f.GrafanaRuler.RouteGetNamespaceRulesConfig(ctx, namespace) } -func (f *RulerApiHandler) handleRouteGetGrafanaRuleGroupConfig(ctx *models.ReqContext, namespace, group string) response.Response { +func (f *RulerApiHandler) handleRouteGetGrafanaRuleGroupConfig(ctx *contextmodel.ReqContext, namespace, group string) response.Response { return f.GrafanaRuler.RouteGetRulesGroupConfig(ctx, namespace, group) } -func (f *RulerApiHandler) handleRouteGetGrafanaRulesConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) handleRouteGetGrafanaRulesConfig(ctx *contextmodel.ReqContext) response.Response { return f.GrafanaRuler.RouteGetRulesConfig(ctx) } -func (f *RulerApiHandler) handleRoutePostNameGrafanaRulesConfig(ctx *models.ReqContext, conf apimodels.PostableRuleGroupConfig, namespace string) response.Response { +func (f *RulerApiHandler) handleRoutePostNameGrafanaRulesConfig(ctx *contextmodel.ReqContext, conf apimodels.PostableRuleGroupConfig, namespace string) response.Response { payloadType := conf.Type() if payloadType != apimodels.GrafanaBackend { return errorToResponse(backendTypeDoesNotMatchPayloadTypeError(apimodels.GrafanaBackend, conf.Type().String())) @@ -101,7 +101,7 @@ func (f *RulerApiHandler) handleRoutePostNameGrafanaRulesConfig(ctx *models.ReqC return f.GrafanaRuler.RoutePostNameRulesConfig(ctx, conf, namespace) } -func (f *RulerApiHandler) getService(ctx *models.ReqContext) (*LotexRuler, error) { +func (f *RulerApiHandler) getService(ctx *contextmodel.ReqContext) (*LotexRuler, error) { _, err := getDatasourceByUID(ctx, f.DatasourceCache, apimodels.LoTexRulerBackend) if err != nil { return nil, err diff --git a/pkg/services/ngalert/api/generated_base_api_alertmanager.go b/pkg/services/ngalert/api/generated_base_api_alertmanager.go index c6b35d35fe0..65ec0ad8b5a 100644 --- a/pkg/services/ngalert/api/generated_base_api_alertmanager.go +++ b/pkg/services/ngalert/api/generated_base_api_alertmanager.go @@ -12,39 +12,39 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/web" ) type AlertmanagerApi interface { - RouteCreateGrafanaSilence(*models.ReqContext) response.Response - RouteCreateSilence(*models.ReqContext) response.Response - RouteDeleteAlertingConfig(*models.ReqContext) response.Response - RouteDeleteGrafanaAlertingConfig(*models.ReqContext) response.Response - RouteDeleteGrafanaSilence(*models.ReqContext) response.Response - RouteDeleteSilence(*models.ReqContext) response.Response - RouteGetAMAlertGroups(*models.ReqContext) response.Response - RouteGetAMAlerts(*models.ReqContext) response.Response - RouteGetAMStatus(*models.ReqContext) response.Response - RouteGetAlertingConfig(*models.ReqContext) response.Response - RouteGetGrafanaAMAlertGroups(*models.ReqContext) response.Response - RouteGetGrafanaAMAlerts(*models.ReqContext) response.Response - RouteGetGrafanaAMStatus(*models.ReqContext) response.Response - RouteGetGrafanaAlertingConfig(*models.ReqContext) response.Response - RouteGetGrafanaReceivers(*models.ReqContext) response.Response - RouteGetGrafanaSilence(*models.ReqContext) response.Response - RouteGetGrafanaSilences(*models.ReqContext) response.Response - RouteGetSilence(*models.ReqContext) response.Response - RouteGetSilences(*models.ReqContext) response.Response - RoutePostAMAlerts(*models.ReqContext) response.Response - RoutePostAlertingConfig(*models.ReqContext) response.Response - RoutePostGrafanaAlertingConfig(*models.ReqContext) response.Response - RoutePostTestGrafanaReceivers(*models.ReqContext) response.Response + RouteCreateGrafanaSilence(*contextmodel.ReqContext) response.Response + RouteCreateSilence(*contextmodel.ReqContext) response.Response + RouteDeleteAlertingConfig(*contextmodel.ReqContext) response.Response + RouteDeleteGrafanaAlertingConfig(*contextmodel.ReqContext) response.Response + RouteDeleteGrafanaSilence(*contextmodel.ReqContext) response.Response + RouteDeleteSilence(*contextmodel.ReqContext) response.Response + RouteGetAMAlertGroups(*contextmodel.ReqContext) response.Response + RouteGetAMAlerts(*contextmodel.ReqContext) response.Response + RouteGetAMStatus(*contextmodel.ReqContext) response.Response + RouteGetAlertingConfig(*contextmodel.ReqContext) response.Response + RouteGetGrafanaAMAlertGroups(*contextmodel.ReqContext) response.Response + RouteGetGrafanaAMAlerts(*contextmodel.ReqContext) response.Response + RouteGetGrafanaAMStatus(*contextmodel.ReqContext) response.Response + RouteGetGrafanaAlertingConfig(*contextmodel.ReqContext) response.Response + RouteGetGrafanaReceivers(*contextmodel.ReqContext) response.Response + RouteGetGrafanaSilence(*contextmodel.ReqContext) response.Response + RouteGetGrafanaSilences(*contextmodel.ReqContext) response.Response + RouteGetSilence(*contextmodel.ReqContext) response.Response + RouteGetSilences(*contextmodel.ReqContext) response.Response + RoutePostAMAlerts(*contextmodel.ReqContext) response.Response + RoutePostAlertingConfig(*contextmodel.ReqContext) response.Response + RoutePostGrafanaAlertingConfig(*contextmodel.ReqContext) response.Response + RoutePostTestGrafanaReceivers(*contextmodel.ReqContext) response.Response } -func (f *AlertmanagerApiHandler) RouteCreateGrafanaSilence(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteCreateGrafanaSilence(ctx *contextmodel.ReqContext) response.Response { // Parse Request Body conf := apimodels.PostableSilence{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -52,7 +52,7 @@ func (f *AlertmanagerApiHandler) RouteCreateGrafanaSilence(ctx *models.ReqContex } return f.handleRouteCreateGrafanaSilence(ctx, conf) } -func (f *AlertmanagerApiHandler) RouteCreateSilence(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteCreateSilence(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] // Parse Request Body @@ -62,80 +62,80 @@ func (f *AlertmanagerApiHandler) RouteCreateSilence(ctx *models.ReqContext) resp } return f.handleRouteCreateSilence(ctx, conf, datasourceUIDParam) } -func (f *AlertmanagerApiHandler) RouteDeleteAlertingConfig(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteDeleteAlertingConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] return f.handleRouteDeleteAlertingConfig(ctx, datasourceUIDParam) } -func (f *AlertmanagerApiHandler) RouteDeleteGrafanaAlertingConfig(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteDeleteGrafanaAlertingConfig(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteDeleteGrafanaAlertingConfig(ctx) } -func (f *AlertmanagerApiHandler) RouteDeleteGrafanaSilence(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteDeleteGrafanaSilence(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters silenceIdParam := web.Params(ctx.Req)[":SilenceId"] return f.handleRouteDeleteGrafanaSilence(ctx, silenceIdParam) } -func (f *AlertmanagerApiHandler) RouteDeleteSilence(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteDeleteSilence(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters silenceIdParam := web.Params(ctx.Req)[":SilenceId"] datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] return f.handleRouteDeleteSilence(ctx, silenceIdParam, datasourceUIDParam) } -func (f *AlertmanagerApiHandler) RouteGetAMAlertGroups(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteGetAMAlertGroups(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] return f.handleRouteGetAMAlertGroups(ctx, datasourceUIDParam) } -func (f *AlertmanagerApiHandler) RouteGetAMAlerts(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteGetAMAlerts(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] return f.handleRouteGetAMAlerts(ctx, datasourceUIDParam) } -func (f *AlertmanagerApiHandler) RouteGetAMStatus(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteGetAMStatus(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] return f.handleRouteGetAMStatus(ctx, datasourceUIDParam) } -func (f *AlertmanagerApiHandler) RouteGetAlertingConfig(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteGetAlertingConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] return f.handleRouteGetAlertingConfig(ctx, datasourceUIDParam) } -func (f *AlertmanagerApiHandler) RouteGetGrafanaAMAlertGroups(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteGetGrafanaAMAlertGroups(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetGrafanaAMAlertGroups(ctx) } -func (f *AlertmanagerApiHandler) RouteGetGrafanaAMAlerts(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteGetGrafanaAMAlerts(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetGrafanaAMAlerts(ctx) } -func (f *AlertmanagerApiHandler) RouteGetGrafanaAMStatus(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteGetGrafanaAMStatus(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetGrafanaAMStatus(ctx) } -func (f *AlertmanagerApiHandler) RouteGetGrafanaAlertingConfig(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteGetGrafanaAlertingConfig(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetGrafanaAlertingConfig(ctx) } -func (f *AlertmanagerApiHandler) RouteGetGrafanaReceivers(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteGetGrafanaReceivers(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetGrafanaReceivers(ctx) } -func (f *AlertmanagerApiHandler) RouteGetGrafanaSilence(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteGetGrafanaSilence(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters silenceIdParam := web.Params(ctx.Req)[":SilenceId"] return f.handleRouteGetGrafanaSilence(ctx, silenceIdParam) } -func (f *AlertmanagerApiHandler) RouteGetGrafanaSilences(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteGetGrafanaSilences(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetGrafanaSilences(ctx) } -func (f *AlertmanagerApiHandler) RouteGetSilence(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteGetSilence(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters silenceIdParam := web.Params(ctx.Req)[":SilenceId"] datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] return f.handleRouteGetSilence(ctx, silenceIdParam, datasourceUIDParam) } -func (f *AlertmanagerApiHandler) RouteGetSilences(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteGetSilences(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] return f.handleRouteGetSilences(ctx, datasourceUIDParam) } -func (f *AlertmanagerApiHandler) RoutePostAMAlerts(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RoutePostAMAlerts(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] // Parse Request Body @@ -145,7 +145,7 @@ func (f *AlertmanagerApiHandler) RoutePostAMAlerts(ctx *models.ReqContext) respo } return f.handleRoutePostAMAlerts(ctx, conf, datasourceUIDParam) } -func (f *AlertmanagerApiHandler) RoutePostAlertingConfig(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RoutePostAlertingConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] // Parse Request Body @@ -155,7 +155,7 @@ func (f *AlertmanagerApiHandler) RoutePostAlertingConfig(ctx *models.ReqContext) } return f.handleRoutePostAlertingConfig(ctx, conf, datasourceUIDParam) } -func (f *AlertmanagerApiHandler) RoutePostGrafanaAlertingConfig(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RoutePostGrafanaAlertingConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Request Body conf := apimodels.PostableUserConfig{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -163,7 +163,7 @@ func (f *AlertmanagerApiHandler) RoutePostGrafanaAlertingConfig(ctx *models.ReqC } return f.handleRoutePostGrafanaAlertingConfig(ctx, conf) } -func (f *AlertmanagerApiHandler) RoutePostTestGrafanaReceivers(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RoutePostTestGrafanaReceivers(ctx *contextmodel.ReqContext) response.Response { // Parse Request Body conf := apimodels.TestReceiversConfigBodyParams{} if err := web.Bind(ctx.Req, &conf); err != nil { diff --git a/pkg/services/ngalert/api/generated_base_api_configuration.go b/pkg/services/ngalert/api/generated_base_api_configuration.go index 8a8912b96b5..c0685d12a1c 100644 --- a/pkg/services/ngalert/api/generated_base_api_configuration.go +++ b/pkg/services/ngalert/api/generated_base_api_configuration.go @@ -12,33 +12,33 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/web" ) type ConfigurationApi interface { - RouteDeleteNGalertConfig(*models.ReqContext) response.Response - RouteGetAlertmanagers(*models.ReqContext) response.Response - RouteGetNGalertConfig(*models.ReqContext) response.Response - RouteGetStatus(*models.ReqContext) response.Response - RoutePostNGalertConfig(*models.ReqContext) response.Response + RouteDeleteNGalertConfig(*contextmodel.ReqContext) response.Response + RouteGetAlertmanagers(*contextmodel.ReqContext) response.Response + RouteGetNGalertConfig(*contextmodel.ReqContext) response.Response + RouteGetStatus(*contextmodel.ReqContext) response.Response + RoutePostNGalertConfig(*contextmodel.ReqContext) response.Response } -func (f *ConfigurationApiHandler) RouteDeleteNGalertConfig(ctx *models.ReqContext) response.Response { +func (f *ConfigurationApiHandler) RouteDeleteNGalertConfig(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteDeleteNGalertConfig(ctx) } -func (f *ConfigurationApiHandler) RouteGetAlertmanagers(ctx *models.ReqContext) response.Response { +func (f *ConfigurationApiHandler) RouteGetAlertmanagers(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetAlertmanagers(ctx) } -func (f *ConfigurationApiHandler) RouteGetNGalertConfig(ctx *models.ReqContext) response.Response { +func (f *ConfigurationApiHandler) RouteGetNGalertConfig(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetNGalertConfig(ctx) } -func (f *ConfigurationApiHandler) RouteGetStatus(ctx *models.ReqContext) response.Response { +func (f *ConfigurationApiHandler) RouteGetStatus(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetStatus(ctx) } -func (f *ConfigurationApiHandler) RoutePostNGalertConfig(ctx *models.ReqContext) response.Response { +func (f *ConfigurationApiHandler) RoutePostNGalertConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Request Body conf := apimodels.PostableNGalertConfig{} if err := web.Bind(ctx.Req, &conf); err != nil { diff --git a/pkg/services/ngalert/api/generated_base_api_prometheus.go b/pkg/services/ngalert/api/generated_base_api_prometheus.go index 05297027bc3..a4f04e25199 100644 --- a/pkg/services/ngalert/api/generated_base_api_prometheus.go +++ b/pkg/services/ngalert/api/generated_base_api_prometheus.go @@ -12,30 +12,30 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/web" ) type PrometheusApi interface { - RouteGetAlertStatuses(*models.ReqContext) response.Response - RouteGetGrafanaAlertStatuses(*models.ReqContext) response.Response - RouteGetGrafanaRuleStatuses(*models.ReqContext) response.Response - RouteGetRuleStatuses(*models.ReqContext) response.Response + RouteGetAlertStatuses(*contextmodel.ReqContext) response.Response + RouteGetGrafanaAlertStatuses(*contextmodel.ReqContext) response.Response + RouteGetGrafanaRuleStatuses(*contextmodel.ReqContext) response.Response + RouteGetRuleStatuses(*contextmodel.ReqContext) response.Response } -func (f *PrometheusApiHandler) RouteGetAlertStatuses(ctx *models.ReqContext) response.Response { +func (f *PrometheusApiHandler) RouteGetAlertStatuses(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] return f.handleRouteGetAlertStatuses(ctx, datasourceUIDParam) } -func (f *PrometheusApiHandler) RouteGetGrafanaAlertStatuses(ctx *models.ReqContext) response.Response { +func (f *PrometheusApiHandler) RouteGetGrafanaAlertStatuses(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetGrafanaAlertStatuses(ctx) } -func (f *PrometheusApiHandler) RouteGetGrafanaRuleStatuses(ctx *models.ReqContext) response.Response { +func (f *PrometheusApiHandler) RouteGetGrafanaRuleStatuses(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetGrafanaRuleStatuses(ctx) } -func (f *PrometheusApiHandler) RouteGetRuleStatuses(ctx *models.ReqContext) response.Response { +func (f *PrometheusApiHandler) RouteGetRuleStatuses(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] return f.handleRouteGetRuleStatuses(ctx, datasourceUIDParam) diff --git a/pkg/services/ngalert/api/generated_base_api_provisioning.go b/pkg/services/ngalert/api/generated_base_api_provisioning.go index d7d8c8b9776..aa72ebe5efa 100644 --- a/pkg/services/ngalert/api/generated_base_api_provisioning.go +++ b/pkg/services/ngalert/api/generated_base_api_provisioning.go @@ -12,95 +12,95 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/web" ) type ProvisioningApi interface { - RouteDeleteAlertRule(*models.ReqContext) response.Response - RouteDeleteContactpoints(*models.ReqContext) response.Response - RouteDeleteMuteTiming(*models.ReqContext) response.Response - RouteDeleteTemplate(*models.ReqContext) response.Response - RouteGetAlertRule(*models.ReqContext) response.Response - RouteGetAlertRuleGroup(*models.ReqContext) response.Response - RouteGetAlertRules(*models.ReqContext) response.Response - RouteGetContactpoints(*models.ReqContext) response.Response - RouteGetMuteTiming(*models.ReqContext) response.Response - RouteGetMuteTimings(*models.ReqContext) response.Response - RouteGetPolicyTree(*models.ReqContext) response.Response - RouteGetTemplate(*models.ReqContext) response.Response - RouteGetTemplates(*models.ReqContext) response.Response - RoutePostAlertRule(*models.ReqContext) response.Response - RoutePostContactpoints(*models.ReqContext) response.Response - RoutePostMuteTiming(*models.ReqContext) response.Response - RoutePutAlertRule(*models.ReqContext) response.Response - RoutePutAlertRuleGroup(*models.ReqContext) response.Response - RoutePutContactpoint(*models.ReqContext) response.Response - RoutePutMuteTiming(*models.ReqContext) response.Response - RoutePutPolicyTree(*models.ReqContext) response.Response - RoutePutTemplate(*models.ReqContext) response.Response - RouteResetPolicyTree(*models.ReqContext) response.Response + RouteDeleteAlertRule(*contextmodel.ReqContext) response.Response + RouteDeleteContactpoints(*contextmodel.ReqContext) response.Response + RouteDeleteMuteTiming(*contextmodel.ReqContext) response.Response + RouteDeleteTemplate(*contextmodel.ReqContext) response.Response + RouteGetAlertRule(*contextmodel.ReqContext) response.Response + RouteGetAlertRuleGroup(*contextmodel.ReqContext) response.Response + RouteGetAlertRules(*contextmodel.ReqContext) response.Response + RouteGetContactpoints(*contextmodel.ReqContext) response.Response + RouteGetMuteTiming(*contextmodel.ReqContext) response.Response + RouteGetMuteTimings(*contextmodel.ReqContext) response.Response + RouteGetPolicyTree(*contextmodel.ReqContext) response.Response + RouteGetTemplate(*contextmodel.ReqContext) response.Response + RouteGetTemplates(*contextmodel.ReqContext) response.Response + RoutePostAlertRule(*contextmodel.ReqContext) response.Response + RoutePostContactpoints(*contextmodel.ReqContext) response.Response + RoutePostMuteTiming(*contextmodel.ReqContext) response.Response + RoutePutAlertRule(*contextmodel.ReqContext) response.Response + RoutePutAlertRuleGroup(*contextmodel.ReqContext) response.Response + RoutePutContactpoint(*contextmodel.ReqContext) response.Response + RoutePutMuteTiming(*contextmodel.ReqContext) response.Response + RoutePutPolicyTree(*contextmodel.ReqContext) response.Response + RoutePutTemplate(*contextmodel.ReqContext) response.Response + RouteResetPolicyTree(*contextmodel.ReqContext) response.Response } -func (f *ProvisioningApiHandler) RouteDeleteAlertRule(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteDeleteAlertRule(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters uIDParam := web.Params(ctx.Req)[":UID"] return f.handleRouteDeleteAlertRule(ctx, uIDParam) } -func (f *ProvisioningApiHandler) RouteDeleteContactpoints(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteDeleteContactpoints(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters uIDParam := web.Params(ctx.Req)[":UID"] return f.handleRouteDeleteContactpoints(ctx, uIDParam) } -func (f *ProvisioningApiHandler) RouteDeleteMuteTiming(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteDeleteMuteTiming(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters nameParam := web.Params(ctx.Req)[":name"] return f.handleRouteDeleteMuteTiming(ctx, nameParam) } -func (f *ProvisioningApiHandler) RouteDeleteTemplate(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteDeleteTemplate(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters nameParam := web.Params(ctx.Req)[":name"] return f.handleRouteDeleteTemplate(ctx, nameParam) } -func (f *ProvisioningApiHandler) RouteGetAlertRule(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteGetAlertRule(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters uIDParam := web.Params(ctx.Req)[":UID"] return f.handleRouteGetAlertRule(ctx, uIDParam) } -func (f *ProvisioningApiHandler) RouteGetAlertRuleGroup(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteGetAlertRuleGroup(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters folderUIDParam := web.Params(ctx.Req)[":FolderUID"] groupParam := web.Params(ctx.Req)[":Group"] return f.handleRouteGetAlertRuleGroup(ctx, folderUIDParam, groupParam) } -func (f *ProvisioningApiHandler) RouteGetAlertRules(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteGetAlertRules(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetAlertRules(ctx) } -func (f *ProvisioningApiHandler) RouteGetContactpoints(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteGetContactpoints(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetContactpoints(ctx) } -func (f *ProvisioningApiHandler) RouteGetMuteTiming(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteGetMuteTiming(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters nameParam := web.Params(ctx.Req)[":name"] return f.handleRouteGetMuteTiming(ctx, nameParam) } -func (f *ProvisioningApiHandler) RouteGetMuteTimings(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteGetMuteTimings(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetMuteTimings(ctx) } -func (f *ProvisioningApiHandler) RouteGetPolicyTree(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteGetPolicyTree(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetPolicyTree(ctx) } -func (f *ProvisioningApiHandler) RouteGetTemplate(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteGetTemplate(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters nameParam := web.Params(ctx.Req)[":name"] return f.handleRouteGetTemplate(ctx, nameParam) } -func (f *ProvisioningApiHandler) RouteGetTemplates(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteGetTemplates(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetTemplates(ctx) } -func (f *ProvisioningApiHandler) RoutePostAlertRule(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RoutePostAlertRule(ctx *contextmodel.ReqContext) response.Response { // Parse Request Body conf := apimodels.ProvisionedAlertRule{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -108,7 +108,7 @@ func (f *ProvisioningApiHandler) RoutePostAlertRule(ctx *models.ReqContext) resp } return f.handleRoutePostAlertRule(ctx, conf) } -func (f *ProvisioningApiHandler) RoutePostContactpoints(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RoutePostContactpoints(ctx *contextmodel.ReqContext) response.Response { // Parse Request Body conf := apimodels.EmbeddedContactPoint{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -116,7 +116,7 @@ func (f *ProvisioningApiHandler) RoutePostContactpoints(ctx *models.ReqContext) } return f.handleRoutePostContactpoints(ctx, conf) } -func (f *ProvisioningApiHandler) RoutePostMuteTiming(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RoutePostMuteTiming(ctx *contextmodel.ReqContext) response.Response { // Parse Request Body conf := apimodels.MuteTimeInterval{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -124,7 +124,7 @@ func (f *ProvisioningApiHandler) RoutePostMuteTiming(ctx *models.ReqContext) res } return f.handleRoutePostMuteTiming(ctx, conf) } -func (f *ProvisioningApiHandler) RoutePutAlertRule(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RoutePutAlertRule(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters uIDParam := web.Params(ctx.Req)[":UID"] // Parse Request Body @@ -134,7 +134,7 @@ func (f *ProvisioningApiHandler) RoutePutAlertRule(ctx *models.ReqContext) respo } return f.handleRoutePutAlertRule(ctx, conf, uIDParam) } -func (f *ProvisioningApiHandler) RoutePutAlertRuleGroup(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RoutePutAlertRuleGroup(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters folderUIDParam := web.Params(ctx.Req)[":FolderUID"] groupParam := web.Params(ctx.Req)[":Group"] @@ -145,7 +145,7 @@ func (f *ProvisioningApiHandler) RoutePutAlertRuleGroup(ctx *models.ReqContext) } return f.handleRoutePutAlertRuleGroup(ctx, conf, folderUIDParam, groupParam) } -func (f *ProvisioningApiHandler) RoutePutContactpoint(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RoutePutContactpoint(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters uIDParam := web.Params(ctx.Req)[":UID"] // Parse Request Body @@ -155,7 +155,7 @@ func (f *ProvisioningApiHandler) RoutePutContactpoint(ctx *models.ReqContext) re } return f.handleRoutePutContactpoint(ctx, conf, uIDParam) } -func (f *ProvisioningApiHandler) RoutePutMuteTiming(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RoutePutMuteTiming(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters nameParam := web.Params(ctx.Req)[":name"] // Parse Request Body @@ -165,7 +165,7 @@ func (f *ProvisioningApiHandler) RoutePutMuteTiming(ctx *models.ReqContext) resp } return f.handleRoutePutMuteTiming(ctx, conf, nameParam) } -func (f *ProvisioningApiHandler) RoutePutPolicyTree(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RoutePutPolicyTree(ctx *contextmodel.ReqContext) response.Response { // Parse Request Body conf := apimodels.Route{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -173,7 +173,7 @@ func (f *ProvisioningApiHandler) RoutePutPolicyTree(ctx *models.ReqContext) resp } return f.handleRoutePutPolicyTree(ctx, conf) } -func (f *ProvisioningApiHandler) RoutePutTemplate(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RoutePutTemplate(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters nameParam := web.Params(ctx.Req)[":name"] // Parse Request Body @@ -183,7 +183,7 @@ func (f *ProvisioningApiHandler) RoutePutTemplate(ctx *models.ReqContext) respon } return f.handleRoutePutTemplate(ctx, conf, nameParam) } -func (f *ProvisioningApiHandler) RouteResetPolicyTree(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteResetPolicyTree(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteResetPolicyTree(ctx) } diff --git a/pkg/services/ngalert/api/generated_base_api_ruler.go b/pkg/services/ngalert/api/generated_base_api_ruler.go index ac584615874..b5687db2a3f 100644 --- a/pkg/services/ngalert/api/generated_base_api_ruler.go +++ b/pkg/services/ngalert/api/generated_base_api_ruler.go @@ -12,84 +12,84 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/web" ) type RulerApi interface { - RouteDeleteGrafanaRuleGroupConfig(*models.ReqContext) response.Response - RouteDeleteNamespaceGrafanaRulesConfig(*models.ReqContext) response.Response - RouteDeleteNamespaceRulesConfig(*models.ReqContext) response.Response - RouteDeleteRuleGroupConfig(*models.ReqContext) response.Response - RouteGetGrafanaRuleGroupConfig(*models.ReqContext) response.Response - RouteGetGrafanaRulesConfig(*models.ReqContext) response.Response - RouteGetNamespaceGrafanaRulesConfig(*models.ReqContext) response.Response - RouteGetNamespaceRulesConfig(*models.ReqContext) response.Response - RouteGetRulegGroupConfig(*models.ReqContext) response.Response - RouteGetRulesConfig(*models.ReqContext) response.Response - RoutePostNameGrafanaRulesConfig(*models.ReqContext) response.Response - RoutePostNameRulesConfig(*models.ReqContext) response.Response + RouteDeleteGrafanaRuleGroupConfig(*contextmodel.ReqContext) response.Response + RouteDeleteNamespaceGrafanaRulesConfig(*contextmodel.ReqContext) response.Response + RouteDeleteNamespaceRulesConfig(*contextmodel.ReqContext) response.Response + RouteDeleteRuleGroupConfig(*contextmodel.ReqContext) response.Response + RouteGetGrafanaRuleGroupConfig(*contextmodel.ReqContext) response.Response + RouteGetGrafanaRulesConfig(*contextmodel.ReqContext) response.Response + RouteGetNamespaceGrafanaRulesConfig(*contextmodel.ReqContext) response.Response + RouteGetNamespaceRulesConfig(*contextmodel.ReqContext) response.Response + RouteGetRulegGroupConfig(*contextmodel.ReqContext) response.Response + RouteGetRulesConfig(*contextmodel.ReqContext) response.Response + RoutePostNameGrafanaRulesConfig(*contextmodel.ReqContext) response.Response + RoutePostNameRulesConfig(*contextmodel.ReqContext) response.Response } -func (f *RulerApiHandler) RouteDeleteGrafanaRuleGroupConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RouteDeleteGrafanaRuleGroupConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters namespaceParam := web.Params(ctx.Req)[":Namespace"] groupnameParam := web.Params(ctx.Req)[":Groupname"] return f.handleRouteDeleteGrafanaRuleGroupConfig(ctx, namespaceParam, groupnameParam) } -func (f *RulerApiHandler) RouteDeleteNamespaceGrafanaRulesConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RouteDeleteNamespaceGrafanaRulesConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters namespaceParam := web.Params(ctx.Req)[":Namespace"] return f.handleRouteDeleteNamespaceGrafanaRulesConfig(ctx, namespaceParam) } -func (f *RulerApiHandler) RouteDeleteNamespaceRulesConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RouteDeleteNamespaceRulesConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] namespaceParam := web.Params(ctx.Req)[":Namespace"] return f.handleRouteDeleteNamespaceRulesConfig(ctx, datasourceUIDParam, namespaceParam) } -func (f *RulerApiHandler) RouteDeleteRuleGroupConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RouteDeleteRuleGroupConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] namespaceParam := web.Params(ctx.Req)[":Namespace"] groupnameParam := web.Params(ctx.Req)[":Groupname"] return f.handleRouteDeleteRuleGroupConfig(ctx, datasourceUIDParam, namespaceParam, groupnameParam) } -func (f *RulerApiHandler) RouteGetGrafanaRuleGroupConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RouteGetGrafanaRuleGroupConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters namespaceParam := web.Params(ctx.Req)[":Namespace"] groupnameParam := web.Params(ctx.Req)[":Groupname"] return f.handleRouteGetGrafanaRuleGroupConfig(ctx, namespaceParam, groupnameParam) } -func (f *RulerApiHandler) RouteGetGrafanaRulesConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RouteGetGrafanaRulesConfig(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetGrafanaRulesConfig(ctx) } -func (f *RulerApiHandler) RouteGetNamespaceGrafanaRulesConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RouteGetNamespaceGrafanaRulesConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters namespaceParam := web.Params(ctx.Req)[":Namespace"] return f.handleRouteGetNamespaceGrafanaRulesConfig(ctx, namespaceParam) } -func (f *RulerApiHandler) RouteGetNamespaceRulesConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RouteGetNamespaceRulesConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] namespaceParam := web.Params(ctx.Req)[":Namespace"] return f.handleRouteGetNamespaceRulesConfig(ctx, datasourceUIDParam, namespaceParam) } -func (f *RulerApiHandler) RouteGetRulegGroupConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RouteGetRulegGroupConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] namespaceParam := web.Params(ctx.Req)[":Namespace"] groupnameParam := web.Params(ctx.Req)[":Groupname"] return f.handleRouteGetRulegGroupConfig(ctx, datasourceUIDParam, namespaceParam, groupnameParam) } -func (f *RulerApiHandler) RouteGetRulesConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RouteGetRulesConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] return f.handleRouteGetRulesConfig(ctx, datasourceUIDParam) } -func (f *RulerApiHandler) RoutePostNameGrafanaRulesConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RoutePostNameGrafanaRulesConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters namespaceParam := web.Params(ctx.Req)[":Namespace"] // Parse Request Body @@ -99,7 +99,7 @@ func (f *RulerApiHandler) RoutePostNameGrafanaRulesConfig(ctx *models.ReqContext } return f.handleRoutePostNameGrafanaRulesConfig(ctx, conf, namespaceParam) } -func (f *RulerApiHandler) RoutePostNameRulesConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RoutePostNameRulesConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] namespaceParam := web.Params(ctx.Req)[":Namespace"] diff --git a/pkg/services/ngalert/api/generated_base_api_testing.go b/pkg/services/ngalert/api/generated_base_api_testing.go index 6018bc58344..6b3e104efab 100644 --- a/pkg/services/ngalert/api/generated_base_api_testing.go +++ b/pkg/services/ngalert/api/generated_base_api_testing.go @@ -12,20 +12,20 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/web" ) type TestingApi interface { - BacktestConfig(*models.ReqContext) response.Response - RouteEvalQueries(*models.ReqContext) response.Response - RouteTestRuleConfig(*models.ReqContext) response.Response - RouteTestRuleGrafanaConfig(*models.ReqContext) response.Response + BacktestConfig(*contextmodel.ReqContext) response.Response + RouteEvalQueries(*contextmodel.ReqContext) response.Response + RouteTestRuleConfig(*contextmodel.ReqContext) response.Response + RouteTestRuleGrafanaConfig(*contextmodel.ReqContext) response.Response } -func (f *TestingApiHandler) BacktestConfig(ctx *models.ReqContext) response.Response { +func (f *TestingApiHandler) BacktestConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Request Body conf := apimodels.BacktestConfig{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -33,7 +33,7 @@ func (f *TestingApiHandler) BacktestConfig(ctx *models.ReqContext) response.Resp } return f.handleBacktestConfig(ctx, conf) } -func (f *TestingApiHandler) RouteEvalQueries(ctx *models.ReqContext) response.Response { +func (f *TestingApiHandler) RouteEvalQueries(ctx *contextmodel.ReqContext) response.Response { // Parse Request Body conf := apimodels.EvalQueriesPayload{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -41,7 +41,7 @@ func (f *TestingApiHandler) RouteEvalQueries(ctx *models.ReqContext) response.Re } return f.handleRouteEvalQueries(ctx, conf) } -func (f *TestingApiHandler) RouteTestRuleConfig(ctx *models.ReqContext) response.Response { +func (f *TestingApiHandler) RouteTestRuleConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] // Parse Request Body @@ -51,7 +51,7 @@ func (f *TestingApiHandler) RouteTestRuleConfig(ctx *models.ReqContext) response } return f.handleRouteTestRuleConfig(ctx, conf, datasourceUIDParam) } -func (f *TestingApiHandler) RouteTestRuleGrafanaConfig(ctx *models.ReqContext) response.Response { +func (f *TestingApiHandler) RouteTestRuleGrafanaConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Request Body conf := apimodels.TestRulePayload{} if err := web.Bind(ctx.Req, &conf); err != nil { diff --git a/pkg/services/ngalert/api/lotex_am.go b/pkg/services/ngalert/api/lotex_am.go index 9db6b947767..0a4ac25864e 100644 --- a/pkg/services/ngalert/api/lotex_am.go +++ b/pkg/services/ngalert/api/lotex_am.go @@ -12,7 +12,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/web" @@ -61,7 +61,7 @@ func NewLotexAM(proxy *AlertingProxy, log log.Logger) *LotexAM { } func (am *LotexAM) withAMReq( - ctx *models.ReqContext, + ctx *contextmodel.ReqContext, method string, endpoint string, pathParams []string, @@ -110,7 +110,7 @@ func (am *LotexAM) withAMReq( ) } -func (am *LotexAM) RouteGetAMStatus(ctx *models.ReqContext) response.Response { +func (am *LotexAM) RouteGetAMStatus(ctx *contextmodel.ReqContext) response.Response { return am.withAMReq( ctx, http.MethodGet, @@ -122,7 +122,7 @@ func (am *LotexAM) RouteGetAMStatus(ctx *models.ReqContext) response.Response { ) } -func (am *LotexAM) RouteCreateSilence(ctx *models.ReqContext, silenceBody apimodels.PostableSilence) response.Response { +func (am *LotexAM) RouteCreateSilence(ctx *contextmodel.ReqContext, silenceBody apimodels.PostableSilence) response.Response { blob, err := json.Marshal(silenceBody) if err != nil { return ErrResp(500, err, "Failed marshal silence") @@ -138,7 +138,7 @@ func (am *LotexAM) RouteCreateSilence(ctx *models.ReqContext, silenceBody apimod ) } -func (am *LotexAM) RouteDeleteAlertingConfig(ctx *models.ReqContext) response.Response { +func (am *LotexAM) RouteDeleteAlertingConfig(ctx *contextmodel.ReqContext) response.Response { return am.withAMReq( ctx, http.MethodDelete, @@ -150,7 +150,7 @@ func (am *LotexAM) RouteDeleteAlertingConfig(ctx *models.ReqContext) response.Re ) } -func (am *LotexAM) RouteDeleteSilence(ctx *models.ReqContext, silenceID string) response.Response { +func (am *LotexAM) RouteDeleteSilence(ctx *contextmodel.ReqContext, silenceID string) response.Response { return am.withAMReq( ctx, http.MethodDelete, @@ -162,7 +162,7 @@ func (am *LotexAM) RouteDeleteSilence(ctx *models.ReqContext, silenceID string) ) } -func (am *LotexAM) RouteGetAlertingConfig(ctx *models.ReqContext) response.Response { +func (am *LotexAM) RouteGetAlertingConfig(ctx *contextmodel.ReqContext) response.Response { return am.withAMReq( ctx, http.MethodGet, @@ -174,7 +174,7 @@ func (am *LotexAM) RouteGetAlertingConfig(ctx *models.ReqContext) response.Respo ) } -func (am *LotexAM) RouteGetAMAlertGroups(ctx *models.ReqContext) response.Response { +func (am *LotexAM) RouteGetAMAlertGroups(ctx *contextmodel.ReqContext) response.Response { return am.withAMReq( ctx, http.MethodGet, @@ -186,7 +186,7 @@ func (am *LotexAM) RouteGetAMAlertGroups(ctx *models.ReqContext) response.Respon ) } -func (am *LotexAM) RouteGetAMAlerts(ctx *models.ReqContext) response.Response { +func (am *LotexAM) RouteGetAMAlerts(ctx *contextmodel.ReqContext) response.Response { return am.withAMReq( ctx, http.MethodGet, @@ -198,7 +198,7 @@ func (am *LotexAM) RouteGetAMAlerts(ctx *models.ReqContext) response.Response { ) } -func (am *LotexAM) RouteGetSilence(ctx *models.ReqContext, silenceID string) response.Response { +func (am *LotexAM) RouteGetSilence(ctx *contextmodel.ReqContext, silenceID string) response.Response { return am.withAMReq( ctx, http.MethodGet, @@ -210,7 +210,7 @@ func (am *LotexAM) RouteGetSilence(ctx *models.ReqContext, silenceID string) res ) } -func (am *LotexAM) RouteGetSilences(ctx *models.ReqContext) response.Response { +func (am *LotexAM) RouteGetSilences(ctx *contextmodel.ReqContext) response.Response { return am.withAMReq( ctx, http.MethodGet, @@ -222,7 +222,7 @@ func (am *LotexAM) RouteGetSilences(ctx *models.ReqContext) response.Response { ) } -func (am *LotexAM) RoutePostAlertingConfig(ctx *models.ReqContext, config apimodels.PostableUserConfig) response.Response { +func (am *LotexAM) RoutePostAlertingConfig(ctx *contextmodel.ReqContext, config apimodels.PostableUserConfig) response.Response { yml, err := yaml.Marshal(&config) if err != nil { return ErrResp(500, err, "Failed marshal alert manager configuration ") @@ -239,7 +239,7 @@ func (am *LotexAM) RoutePostAlertingConfig(ctx *models.ReqContext, config apimod ) } -func (am *LotexAM) RoutePostAMAlerts(ctx *models.ReqContext, alerts apimodels.PostableAlerts) response.Response { +func (am *LotexAM) RoutePostAMAlerts(ctx *contextmodel.ReqContext, alerts apimodels.PostableAlerts) response.Response { yml, err := yaml.Marshal(alerts) if err != nil { return ErrResp(500, err, "Failed marshal postable alerts") diff --git a/pkg/services/ngalert/api/lotex_prom.go b/pkg/services/ngalert/api/lotex_prom.go index f94797a4294..e3be8525bb1 100644 --- a/pkg/services/ngalert/api/lotex_prom.go +++ b/pkg/services/ngalert/api/lotex_prom.go @@ -6,7 +6,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/web" ) @@ -38,7 +38,7 @@ func NewLotexProm(proxy *AlertingProxy, log log.Logger) *LotexProm { } } -func (p *LotexProm) RouteGetAlertStatuses(ctx *models.ReqContext) response.Response { +func (p *LotexProm) RouteGetAlertStatuses(ctx *contextmodel.ReqContext) response.Response { endpoints, err := p.getEndpoints(ctx) if err != nil { return ErrResp(http.StatusInternalServerError, err, "") @@ -57,7 +57,7 @@ func (p *LotexProm) RouteGetAlertStatuses(ctx *models.ReqContext) response.Respo ) } -func (p *LotexProm) RouteGetRuleStatuses(ctx *models.ReqContext) response.Response { +func (p *LotexProm) RouteGetRuleStatuses(ctx *contextmodel.ReqContext) response.Response { endpoints, err := p.getEndpoints(ctx) if err != nil { return ErrResp(http.StatusInternalServerError, err, "") @@ -76,7 +76,7 @@ func (p *LotexProm) RouteGetRuleStatuses(ctx *models.ReqContext) response.Respon ) } -func (p *LotexProm) getEndpoints(ctx *models.ReqContext) (*promEndpoints, error) { +func (p *LotexProm) getEndpoints(ctx *contextmodel.ReqContext) (*promEndpoints, error) { datasourceUID := web.Params(ctx.Req)[":DatasourceUID"] if datasourceUID == "" { return nil, fmt.Errorf("datasource UID is invalid") diff --git a/pkg/services/ngalert/api/lotex_ruler.go b/pkg/services/ngalert/api/lotex_ruler.go index 1b1dc49289c..9f94a9be7ad 100644 --- a/pkg/services/ngalert/api/lotex_ruler.go +++ b/pkg/services/ngalert/api/lotex_ruler.go @@ -6,13 +6,13 @@ import ( "net/http" "net/url" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/web" "gopkg.in/yaml.v3" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" ) const ( @@ -55,7 +55,7 @@ func NewLotexRuler(proxy *AlertingProxy, log log.Logger) *LotexRuler { } } -func (r *LotexRuler) RouteDeleteNamespaceRulesConfig(ctx *models.ReqContext, namespace string) response.Response { +func (r *LotexRuler) RouteDeleteNamespaceRulesConfig(ctx *contextmodel.ReqContext, namespace string) response.Response { legacyRulerPrefix, err := r.validateAndGetPrefix(ctx) if err != nil { return ErrResp(500, err, "") @@ -73,7 +73,7 @@ func (r *LotexRuler) RouteDeleteNamespaceRulesConfig(ctx *models.ReqContext, nam ) } -func (r *LotexRuler) RouteDeleteRuleGroupConfig(ctx *models.ReqContext, namespace string, group string) response.Response { +func (r *LotexRuler) RouteDeleteRuleGroupConfig(ctx *contextmodel.ReqContext, namespace string, group string) response.Response { legacyRulerPrefix, err := r.validateAndGetPrefix(ctx) if err != nil { return ErrResp(500, err, "") @@ -96,7 +96,7 @@ func (r *LotexRuler) RouteDeleteRuleGroupConfig(ctx *models.ReqContext, namespac ) } -func (r *LotexRuler) RouteGetNamespaceRulesConfig(ctx *models.ReqContext, namespace string) response.Response { +func (r *LotexRuler) RouteGetNamespaceRulesConfig(ctx *contextmodel.ReqContext, namespace string) response.Response { legacyRulerPrefix, err := r.validateAndGetPrefix(ctx) if err != nil { return ErrResp(500, err, "") @@ -118,7 +118,7 @@ func (r *LotexRuler) RouteGetNamespaceRulesConfig(ctx *models.ReqContext, namesp ) } -func (r *LotexRuler) RouteGetRulegGroupConfig(ctx *models.ReqContext, namespace string, group string) response.Response { +func (r *LotexRuler) RouteGetRulegGroupConfig(ctx *contextmodel.ReqContext, namespace string, group string) response.Response { legacyRulerPrefix, err := r.validateAndGetPrefix(ctx) if err != nil { return ErrResp(500, err, "") @@ -141,7 +141,7 @@ func (r *LotexRuler) RouteGetRulegGroupConfig(ctx *models.ReqContext, namespace ) } -func (r *LotexRuler) RouteGetRulesConfig(ctx *models.ReqContext) response.Response { +func (r *LotexRuler) RouteGetRulesConfig(ctx *contextmodel.ReqContext) response.Response { legacyRulerPrefix, err := r.validateAndGetPrefix(ctx) if err != nil { return ErrResp(500, err, "") @@ -160,7 +160,7 @@ func (r *LotexRuler) RouteGetRulesConfig(ctx *models.ReqContext) response.Respon ) } -func (r *LotexRuler) RoutePostNameRulesConfig(ctx *models.ReqContext, conf apimodels.PostableRuleGroupConfig, ns string) response.Response { +func (r *LotexRuler) RoutePostNameRulesConfig(ctx *contextmodel.ReqContext, conf apimodels.PostableRuleGroupConfig, ns string) response.Response { legacyRulerPrefix, err := r.validateAndGetPrefix(ctx) if err != nil { return ErrResp(500, err, "") @@ -173,7 +173,7 @@ func (r *LotexRuler) RoutePostNameRulesConfig(ctx *models.ReqContext, conf apimo return r.withReq(ctx, http.MethodPost, u, bytes.NewBuffer(yml), jsonExtractor(nil), nil) } -func (r *LotexRuler) validateAndGetPrefix(ctx *models.ReqContext) (string, error) { +func (r *LotexRuler) validateAndGetPrefix(ctx *contextmodel.ReqContext) (string, error) { datasourceUID := web.Params(ctx.Req)[":DatasourceUID"] if datasourceUID == "" { return "", fmt.Errorf("datasource UID is invalid") diff --git a/pkg/services/ngalert/api/lotex_ruler_test.go b/pkg/services/ngalert/api/lotex_ruler_test.go index a9800a90406..8631782e358 100644 --- a/pkg/services/ngalert/api/lotex_ruler_test.go +++ b/pkg/services/ngalert/api/lotex_ruler_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasourceproxy" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/user" @@ -98,7 +98,7 @@ func TestLotexRuler_ValidateAndGetPrefix(t *testing.T) { // Setup request context. httpReq, err := http.NewRequest(http.MethodGet, "http://grafanacloud.com"+tt.urlParams, nil) require.NoError(t, err) - ctx := &models.ReqContext{Context: &web.Context{Req: web.SetURLParams(httpReq, tt.namedParams)}} + ctx := &contextmodel.ReqContext{Context: &web.Context{Req: web.SetURLParams(httpReq, tt.namedParams)}} prefix, err := ruler.validateAndGetPrefix(ctx) require.Equal(t, tt.expected, prefix) diff --git a/pkg/services/ngalert/api/provisioning.go b/pkg/services/ngalert/api/provisioning.go index 6dc74c75c1c..23387e36c11 100644 --- a/pkg/services/ngalert/api/provisioning.go +++ b/pkg/services/ngalert/api/provisioning.go @@ -2,7 +2,7 @@ package api import ( "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ) @@ -16,94 +16,94 @@ func NewProvisioningApi(svc *ProvisioningSrv) *ProvisioningApiHandler { } } -func (f *ProvisioningApiHandler) handleRouteGetPolicyTree(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) handleRouteGetPolicyTree(ctx *contextmodel.ReqContext) response.Response { return f.svc.RouteGetPolicyTree(ctx) } -func (f *ProvisioningApiHandler) handleRoutePutPolicyTree(ctx *models.ReqContext, route apimodels.Route) response.Response { +func (f *ProvisioningApiHandler) handleRoutePutPolicyTree(ctx *contextmodel.ReqContext, route apimodels.Route) response.Response { return f.svc.RoutePutPolicyTree(ctx, route) } -func (f *ProvisioningApiHandler) handleRouteGetContactpoints(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) handleRouteGetContactpoints(ctx *contextmodel.ReqContext) response.Response { return f.svc.RouteGetContactPoints(ctx) } -func (f *ProvisioningApiHandler) handleRoutePostContactpoints(ctx *models.ReqContext, cp apimodels.EmbeddedContactPoint) response.Response { +func (f *ProvisioningApiHandler) handleRoutePostContactpoints(ctx *contextmodel.ReqContext, cp apimodels.EmbeddedContactPoint) response.Response { return f.svc.RoutePostContactPoint(ctx, cp) } -func (f *ProvisioningApiHandler) handleRoutePutContactpoint(ctx *models.ReqContext, cp apimodels.EmbeddedContactPoint, UID string) response.Response { +func (f *ProvisioningApiHandler) handleRoutePutContactpoint(ctx *contextmodel.ReqContext, cp apimodels.EmbeddedContactPoint, UID string) response.Response { return f.svc.RoutePutContactPoint(ctx, cp, UID) } -func (f *ProvisioningApiHandler) handleRouteDeleteContactpoints(ctx *models.ReqContext, UID string) response.Response { +func (f *ProvisioningApiHandler) handleRouteDeleteContactpoints(ctx *contextmodel.ReqContext, UID string) response.Response { return f.svc.RouteDeleteContactPoint(ctx, UID) } -func (f *ProvisioningApiHandler) handleRouteGetTemplates(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) handleRouteGetTemplates(ctx *contextmodel.ReqContext) response.Response { return f.svc.RouteGetTemplates(ctx) } -func (f *ProvisioningApiHandler) handleRouteGetTemplate(ctx *models.ReqContext, name string) response.Response { +func (f *ProvisioningApiHandler) handleRouteGetTemplate(ctx *contextmodel.ReqContext, name string) response.Response { return f.svc.RouteGetTemplate(ctx, name) } -func (f *ProvisioningApiHandler) handleRoutePutTemplate(ctx *models.ReqContext, body apimodels.NotificationTemplateContent, name string) response.Response { +func (f *ProvisioningApiHandler) handleRoutePutTemplate(ctx *contextmodel.ReqContext, body apimodels.NotificationTemplateContent, name string) response.Response { return f.svc.RoutePutTemplate(ctx, body, name) } -func (f *ProvisioningApiHandler) handleRouteDeleteTemplate(ctx *models.ReqContext, name string) response.Response { +func (f *ProvisioningApiHandler) handleRouteDeleteTemplate(ctx *contextmodel.ReqContext, name string) response.Response { return f.svc.RouteDeleteTemplate(ctx, name) } -func (f *ProvisioningApiHandler) handleRouteGetMuteTiming(ctx *models.ReqContext, name string) response.Response { +func (f *ProvisioningApiHandler) handleRouteGetMuteTiming(ctx *contextmodel.ReqContext, name string) response.Response { return f.svc.RouteGetMuteTiming(ctx, name) } -func (f *ProvisioningApiHandler) handleRouteGetMuteTimings(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) handleRouteGetMuteTimings(ctx *contextmodel.ReqContext) response.Response { return f.svc.RouteGetMuteTimings(ctx) } -func (f *ProvisioningApiHandler) handleRoutePostMuteTiming(ctx *models.ReqContext, mt apimodels.MuteTimeInterval) response.Response { +func (f *ProvisioningApiHandler) handleRoutePostMuteTiming(ctx *contextmodel.ReqContext, mt apimodels.MuteTimeInterval) response.Response { return f.svc.RoutePostMuteTiming(ctx, mt) } -func (f *ProvisioningApiHandler) handleRoutePutMuteTiming(ctx *models.ReqContext, mt apimodels.MuteTimeInterval, name string) response.Response { +func (f *ProvisioningApiHandler) handleRoutePutMuteTiming(ctx *contextmodel.ReqContext, mt apimodels.MuteTimeInterval, name string) response.Response { return f.svc.RoutePutMuteTiming(ctx, mt, name) } -func (f *ProvisioningApiHandler) handleRouteDeleteMuteTiming(ctx *models.ReqContext, name string) response.Response { +func (f *ProvisioningApiHandler) handleRouteDeleteMuteTiming(ctx *contextmodel.ReqContext, name string) response.Response { return f.svc.RouteDeleteMuteTiming(ctx, name) } -func (f *ProvisioningApiHandler) handleRouteGetAlertRules(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) handleRouteGetAlertRules(ctx *contextmodel.ReqContext) response.Response { return f.svc.RouteGetAlertRules(ctx) } -func (f *ProvisioningApiHandler) handleRouteGetAlertRule(ctx *models.ReqContext, UID string) response.Response { +func (f *ProvisioningApiHandler) handleRouteGetAlertRule(ctx *contextmodel.ReqContext, UID string) response.Response { return f.svc.RouteRouteGetAlertRule(ctx, UID) } -func (f *ProvisioningApiHandler) handleRoutePostAlertRule(ctx *models.ReqContext, ar apimodels.ProvisionedAlertRule) response.Response { +func (f *ProvisioningApiHandler) handleRoutePostAlertRule(ctx *contextmodel.ReqContext, ar apimodels.ProvisionedAlertRule) response.Response { return f.svc.RoutePostAlertRule(ctx, ar) } -func (f *ProvisioningApiHandler) handleRoutePutAlertRule(ctx *models.ReqContext, ar apimodels.ProvisionedAlertRule, UID string) response.Response { +func (f *ProvisioningApiHandler) handleRoutePutAlertRule(ctx *contextmodel.ReqContext, ar apimodels.ProvisionedAlertRule, UID string) response.Response { return f.svc.RoutePutAlertRule(ctx, ar, UID) } -func (f *ProvisioningApiHandler) handleRouteDeleteAlertRule(ctx *models.ReqContext, UID string) response.Response { +func (f *ProvisioningApiHandler) handleRouteDeleteAlertRule(ctx *contextmodel.ReqContext, UID string) response.Response { return f.svc.RouteDeleteAlertRule(ctx, UID) } -func (f *ProvisioningApiHandler) handleRouteResetPolicyTree(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) handleRouteResetPolicyTree(ctx *contextmodel.ReqContext) response.Response { return f.svc.RouteResetPolicyTree(ctx) } -func (f *ProvisioningApiHandler) handleRouteGetAlertRuleGroup(ctx *models.ReqContext, folder, group string) response.Response { +func (f *ProvisioningApiHandler) handleRouteGetAlertRuleGroup(ctx *contextmodel.ReqContext, folder, group string) response.Response { return f.svc.RouteGetAlertRuleGroup(ctx, folder, group) } -func (f *ProvisioningApiHandler) handleRoutePutAlertRuleGroup(ctx *models.ReqContext, ag apimodels.AlertRuleGroup, folder, group string) response.Response { +func (f *ProvisioningApiHandler) handleRoutePutAlertRuleGroup(ctx *contextmodel.ReqContext, ag apimodels.AlertRuleGroup, folder, group string) response.Response { return f.svc.RoutePutAlertRuleGroup(ctx, ag, folder, group) } diff --git a/pkg/services/ngalert/api/testing_api.go b/pkg/services/ngalert/api/testing_api.go index 8d3094f8709..23da0884de3 100644 --- a/pkg/services/ngalert/api/testing_api.go +++ b/pkg/services/ngalert/api/testing_api.go @@ -2,7 +2,7 @@ package api import ( "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ) @@ -17,18 +17,18 @@ func NewTestingApi(svc *TestingApiSrv) *TestingApiHandler { } } -func (f *TestingApiHandler) handleRouteTestRuleConfig(c *models.ReqContext, body apimodels.TestRulePayload, dsUID string) response.Response { +func (f *TestingApiHandler) handleRouteTestRuleConfig(c *contextmodel.ReqContext, body apimodels.TestRulePayload, dsUID string) response.Response { return f.svc.RouteTestRuleConfig(c, body, dsUID) } -func (f *TestingApiHandler) handleRouteTestRuleGrafanaConfig(c *models.ReqContext, body apimodels.TestRulePayload) response.Response { +func (f *TestingApiHandler) handleRouteTestRuleGrafanaConfig(c *contextmodel.ReqContext, body apimodels.TestRulePayload) response.Response { return f.svc.RouteTestGrafanaRuleConfig(c, body) } -func (f *TestingApiHandler) handleRouteEvalQueries(c *models.ReqContext, body apimodels.EvalQueriesPayload) response.Response { +func (f *TestingApiHandler) handleRouteEvalQueries(c *contextmodel.ReqContext, body apimodels.EvalQueriesPayload) response.Response { return f.svc.RouteEvalQueries(c, body) } -func (f *TestingApiHandler) handleBacktestConfig(ctx *models.ReqContext, conf apimodels.BacktestConfig) response.Response { +func (f *TestingApiHandler) handleBacktestConfig(ctx *contextmodel.ReqContext, conf apimodels.BacktestConfig) response.Response { return f.svc.BacktestAlertRule(ctx, conf) } diff --git a/pkg/services/ngalert/api/tooling/swagger-codegen/templates/controller-api.mustache b/pkg/services/ngalert/api/tooling/swagger-codegen/templates/controller-api.mustache index 34dfce4f1ad..7a7f7498108 100644 --- a/pkg/services/ngalert/api/tooling/swagger-codegen/templates/controller-api.mustache +++ b/pkg/services/ngalert/api/tooling/swagger-codegen/templates/controller-api.mustache @@ -11,14 +11,15 @@ import ( apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/middleware" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" ) type {{classname}} interface { {{#operation}} - {{nickname}}(*models.ReqContext) response.Response{{/operation}} + {{nickname}}(*contextmodel.ReqContext) response.Response{{/operation}} } {{#operations}}{{#operation}} -func (f *{{classname}}Handler) {{nickname}}(ctx *models.ReqContext) response.Response { {{#hasPathParams}} +func (f *{{classname}}Handler) {{nickname}}(ctx *contextmodel.ReqContext) response.Response { {{#hasPathParams}} // Parse Path Parameters{{/hasPathParams}}{{#pathParams}} {{paramName}}Param := web.Params(ctx.Req)[":{{baseName}}"]{{/pathParams}} {{#bodyParams}} diff --git a/pkg/services/ngalert/api/util.go b/pkg/services/ngalert/api/util.go index 3a101965855..cdb5c170216 100644 --- a/pkg/services/ngalert/api/util.go +++ b/pkg/services/ngalert/api/util.go @@ -15,8 +15,8 @@ import ( "gopkg.in/yaml.v3" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasourceproxy" "github.com/grafana/grafana/pkg/services/datasources" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" @@ -34,7 +34,7 @@ func toMacaronPath(path string) string { })) } -func getDatasourceByUID(ctx *models.ReqContext, cache datasources.CacheService, expectedType apimodels.Backend) (*datasources.DataSource, error) { +func getDatasourceByUID(ctx *contextmodel.ReqContext, cache datasources.CacheService, expectedType apimodels.Backend) (*datasources.DataSource, error) { datasourceUID := web.Params(ctx.Req)[":DatasourceUID"] ds, err := cache.GetDatasourceByUID(ctx.Req.Context(), datasourceUID, ctx.SignedInUser, ctx.SkipCache) if err != nil { @@ -69,12 +69,12 @@ func (w *safeMacaronWrapper) CloseNotify() <-chan bool { // createProxyContext creates a new request context that is provided down to the data source proxy. // The request context -// 1. overwrites the underlying response writer used by a *models.ReqContext because AlertingProxy needs to intercept +// 1. overwrites the underlying response writer used by a *contextmodel.ReqContext because AlertingProxy needs to intercept // the response from the data source to analyze it and probably change // 2. elevates the current user permissions to Editor if both conditions are met: RBAC is enabled, user does not have Editor role. // This is needed to bypass the plugin authorization, which still relies on the legacy roles. // This elevation can be considered safe because all upstream calls are protected by the RBAC on web request router level. -func (p *AlertingProxy) createProxyContext(ctx *models.ReqContext, request *http.Request, response *response.NormalResponse) *models.ReqContext { +func (p *AlertingProxy) createProxyContext(ctx *contextmodel.ReqContext, request *http.Request, response *response.NormalResponse) *contextmodel.ReqContext { cpy := *ctx cpyMCtx := *cpy.Context cpyMCtx.Resp = web.NewResponseWriter(ctx.Req.Method, &safeMacaronWrapper{response}) @@ -100,7 +100,7 @@ type AlertingProxy struct { // withReq proxies a different request func (p *AlertingProxy) withReq( - ctx *models.ReqContext, + ctx *contextmodel.ReqContext, method string, u *url.URL, body io.Reader, diff --git a/pkg/services/ngalert/api/util_test.go b/pkg/services/ngalert/api/util_test.go index ce79d0fed9f..6c5d5e18980 100644 --- a/pkg/services/ngalert/api/util_test.go +++ b/pkg/services/ngalert/api/util_test.go @@ -10,9 +10,9 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/auth" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" models2 "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" @@ -41,7 +41,7 @@ func TestToMacaronPath(t *testing.T) { } func TestAlertingProxy_createProxyContext(t *testing.T) { - ctx := &models.ReqContext{ + ctx := &contextmodel.ReqContext{ Context: &web.Context{ Req: &http.Request{}, }, diff --git a/pkg/services/ngalert/metrics/ngalert.go b/pkg/services/ngalert/metrics/ngalert.go index 035ed3a3992..9681847786a 100644 --- a/pkg/services/ngalert/metrics/ngalert.go +++ b/pkg/services/ngalert/metrics/ngalert.go @@ -12,7 +12,7 @@ import ( "github.com/prometheus/client_golang/prometheus/promauto" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/util/ticker" @@ -305,12 +305,12 @@ func (m *OrgRegistries) RemoveOrgRegistry(org int64) { func Instrument( method, path string, - action func(*models.ReqContext) response.Response, + action func(*contextmodel.ReqContext) response.Response, metrics *API, ) web.Handler { normalizedPath := MakeLabelValue(path) - return func(c *models.ReqContext) { + return func(c *contextmodel.ReqContext) { start := time.Now() res := action(c) diff --git a/pkg/services/publicdashboards/api/api.go b/pkg/services/publicdashboards/api/api.go index 1837911e62d..a40d8776009 100644 --- a/pkg/services/publicdashboards/api/api.go +++ b/pkg/services/publicdashboards/api/api.go @@ -8,8 +8,8 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/publicdashboards" @@ -89,7 +89,7 @@ func (api *Api) RegisterAPIEndpoints() { // ListPublicDashboards Gets list of public dashboards by orgId // GET /api/dashboards/public-dashboards -func (api *Api) ListPublicDashboards(c *models.ReqContext) response.Response { +func (api *Api) ListPublicDashboards(c *contextmodel.ReqContext) response.Response { resp, err := api.PublicDashboardService.FindAll(c.Req.Context(), c.SignedInUser, c.OrgID) if err != nil { return response.Err(err) @@ -99,7 +99,7 @@ func (api *Api) ListPublicDashboards(c *models.ReqContext) response.Response { // GetPublicDashboard Gets public dashboard for dashboard // GET /api/dashboards/uid/:dashboardUid/public-dashboards -func (api *Api) GetPublicDashboard(c *models.ReqContext) response.Response { +func (api *Api) GetPublicDashboard(c *contextmodel.ReqContext) response.Response { // exit if we don't have a valid dashboardUid dashboardUid := web.Params(c.Req)[":dashboardUid"] if !tokens.IsValidShortUID(dashboardUid) { @@ -120,7 +120,7 @@ func (api *Api) GetPublicDashboard(c *models.ReqContext) response.Response { // CreatePublicDashboard Sets public dashboard for dashboard // POST /api/dashboards/uid/:dashboardUid/public-dashboards -func (api *Api) CreatePublicDashboard(c *models.ReqContext) response.Response { +func (api *Api) CreatePublicDashboard(c *contextmodel.ReqContext) response.Response { // exit if we don't have a valid dashboardUid dashboardUid := web.Params(c.Req)[":dashboardUid"] if !tokens.IsValidShortUID(dashboardUid) { @@ -152,7 +152,7 @@ func (api *Api) CreatePublicDashboard(c *models.ReqContext) response.Response { // UpdatePublicDashboard Sets public dashboard for dashboard // PUT /api/dashboards/uid/:dashboardUid/public-dashboards/:uid -func (api *Api) UpdatePublicDashboard(c *models.ReqContext) response.Response { +func (api *Api) UpdatePublicDashboard(c *contextmodel.ReqContext) response.Response { // exit if we don't have a valid dashboardUid dashboardUid := web.Params(c.Req)[":dashboardUid"] if !tokens.IsValidShortUID(dashboardUid) { @@ -190,7 +190,7 @@ func (api *Api) UpdatePublicDashboard(c *models.ReqContext) response.Response { // Delete a public dashboard // DELETE /api/dashboards/uid/:dashboardUid/public-dashboards/:uid -func (api *Api) DeletePublicDashboard(c *models.ReqContext) response.Response { +func (api *Api) DeletePublicDashboard(c *contextmodel.ReqContext) response.Response { uid := web.Params(c.Req)[":uid"] if !tokens.IsValidShortUID(uid) { return response.Err(ErrInvalidUid.Errorf("UpdatePublicDashboard: invalid Uid %s", uid)) diff --git a/pkg/services/publicdashboards/api/common_test.go b/pkg/services/publicdashboards/api/common_test.go index a2cf1bb6238..47a96d39363 100644 --- a/pkg/services/publicdashboards/api/common_test.go +++ b/pkg/services/publicdashboards/api/common_test.go @@ -15,12 +15,12 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes" datasourceService "github.com/grafana/grafana/pkg/services/datasources/service" @@ -82,7 +82,7 @@ type testContext struct { func contextProvider(tc *testContext) web.Handler { return func(c *web.Context) { signedIn := tc.user != nil - reqCtx := &models.ReqContext{ + reqCtx := &contextmodel.ReqContext{ Context: c, SignedInUser: tc.user, IsSignedIn: signedIn, diff --git a/pkg/services/publicdashboards/api/middleware.go b/pkg/services/publicdashboards/api/middleware.go index 856bc2899ad..78a42c5cf23 100644 --- a/pkg/services/publicdashboards/api/middleware.go +++ b/pkg/services/publicdashboards/api/middleware.go @@ -4,15 +4,15 @@ import ( "net/http" "github.com/grafana/grafana/pkg/infra/metrics" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/publicdashboards" "github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens" "github.com/grafana/grafana/pkg/web" ) // SetPublicDashboardOrgIdOnContext Adds orgId to context based on org of public dashboard -func SetPublicDashboardOrgIdOnContext(publicDashboardService publicdashboards.Service) func(c *models.ReqContext) { - return func(c *models.ReqContext) { +func SetPublicDashboardOrgIdOnContext(publicDashboardService publicdashboards.Service) func(c *contextmodel.ReqContext) { + return func(c *contextmodel.ReqContext) { accessToken, ok := web.Params(c.Req)[":accessToken"] if !ok || !tokens.IsValidAccessToken(accessToken) { return @@ -29,15 +29,15 @@ func SetPublicDashboardOrgIdOnContext(publicDashboardService publicdashboards.Se } // SetPublicDashboardFlag Adds public dashboard flag on context -func SetPublicDashboardFlag(c *models.ReqContext) { +func SetPublicDashboardFlag(c *contextmodel.ReqContext) { c.IsPublicDashboardView = true } // RequiresExistingAccessToken Middleware to enforce that a public dashboards exists before continuing to handler. This // method will query the database to ensure that it exists. // Use when we want to enforce a public dashboard is valid on an endpoint we do not maintain -func RequiresExistingAccessToken(publicDashboardService publicdashboards.Service) func(c *models.ReqContext) { - return func(c *models.ReqContext) { +func RequiresExistingAccessToken(publicDashboardService publicdashboards.Service) func(c *contextmodel.ReqContext) { + return func(c *contextmodel.ReqContext) { accessToken, ok := web.Params(c.Req)[":accessToken"] if !ok { @@ -62,8 +62,8 @@ func RequiresExistingAccessToken(publicDashboardService publicdashboards.Service } } -func CountPublicDashboardRequest() func(c *models.ReqContext) { - return func(c *models.ReqContext) { +func CountPublicDashboardRequest() func(c *contextmodel.ReqContext) { + return func(c *contextmodel.ReqContext) { metrics.MPublicDashboardRequestCount.Inc() } } diff --git a/pkg/services/publicdashboards/api/middleware_test.go b/pkg/services/publicdashboards/api/middleware_test.go index 20680679ab5..b0462107cf4 100644 --- a/pkg/services/publicdashboards/api/middleware_test.go +++ b/pkg/services/publicdashboards/api/middleware_test.go @@ -8,7 +8,7 @@ import ( "errors" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/publicdashboards" "github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens" "github.com/grafana/grafana/pkg/services/user" @@ -146,21 +146,21 @@ func TestSetPublicDashboardOrgIdOnContext(t *testing.T) { func TestSetPublicDashboardFlag(t *testing.T) { t.Run("Adds context.IsPublicDashboardView=true to request", func(t *testing.T) { - ctx := &models.ReqContext{} + ctx := &contextmodel.ReqContext{} SetPublicDashboardFlag(ctx) assert.True(t, ctx.IsPublicDashboardView) }) } // This is a helper to test middleware. It handles creating a -// proper models.ReqContext, setting web parameters, executing middleware, and +// proper contextmodel.ReqContext, setting web parameters, executing middleware, and // returning a response. Response will default to result of // httptest.NewRecorder() return value and will only change if modified by the // middlware as this will no accept a handler method -func runMw(t *testing.T, ctx *models.ReqContext, httpmethod string, path string, webparams map[string]string, mw func(c *models.ReqContext)) (*models.ReqContext, *httptest.ResponseRecorder) { +func runMw(t *testing.T, ctx *contextmodel.ReqContext, httpmethod string, path string, webparams map[string]string, mw func(c *contextmodel.ReqContext)) (*contextmodel.ReqContext, *httptest.ResponseRecorder) { // create valid request context and set 0 values if they don't exist if ctx == nil { - ctx = &models.ReqContext{} + ctx = &contextmodel.ReqContext{} } if ctx.Context == nil { ctx.Context = &web.Context{} diff --git a/pkg/services/publicdashboards/api/query.go b/pkg/services/publicdashboards/api/query.go index 2f9473ca9f0..98a3172c760 100644 --- a/pkg/services/publicdashboards/api/query.go +++ b/pkg/services/publicdashboards/api/query.go @@ -6,7 +6,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" @@ -15,7 +15,7 @@ import ( // ViewPublicDashboard Gets public dashboard // GET /api/public/dashboards/:accessToken -func (api *Api) ViewPublicDashboard(c *models.ReqContext) response.Response { +func (api *Api) ViewPublicDashboard(c *contextmodel.ReqContext) response.Response { accessToken := web.Params(c.Req)[":accessToken"] if !tokens.IsValidAccessToken(accessToken) { return response.Err(ErrInvalidAccessToken.Errorf("ViewPublicDashboard: invalid access token")) @@ -53,7 +53,7 @@ func (api *Api) ViewPublicDashboard(c *models.ReqContext) response.Response { // QueryPublicDashboard returns all results for a given panel on a public dashboard // POST /api/public/dashboard/:accessToken/panels/:panelId/query -func (api *Api) QueryPublicDashboard(c *models.ReqContext) response.Response { +func (api *Api) QueryPublicDashboard(c *contextmodel.ReqContext) response.Response { accessToken := web.Params(c.Req)[":accessToken"] if !tokens.IsValidAccessToken(accessToken) { return response.Err(ErrInvalidAccessToken.Errorf("QueryPublicDashboard: invalid access token")) @@ -79,7 +79,7 @@ func (api *Api) QueryPublicDashboard(c *models.ReqContext) response.Response { // GetAnnotations returns annotations for a public dashboard // GET /api/public/dashboards/:accessToken/annotations -func (api *Api) GetAnnotations(c *models.ReqContext) response.Response { +func (api *Api) GetAnnotations(c *contextmodel.ReqContext) response.Response { accessToken := web.Params(c.Req)[":accessToken"] if !tokens.IsValidAccessToken(accessToken) { return response.Err(ErrInvalidAccessToken.Errorf("GetAnnotations: invalid access token")) diff --git a/pkg/services/query/query_test.go b/pkg/services/query/query_test.go index e9c9c4a5c57..188fea5516a 100644 --- a/pkg/services/query/query_test.go +++ b/pkg/services/query/query_test.go @@ -16,11 +16,11 @@ import ( "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/plugins" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes" dsSvc "github.com/grafana/grafana/pkg/services/datasources/service" @@ -211,7 +211,7 @@ func TestParseMetricRequest(t *testing.T) { httpreq, err := http.NewRequest(http.MethodPost, "http://localhost/", bytes.NewReader([]byte{})) require.NoError(t, err) - reqCtx := &models.ReqContext{ + reqCtx := &contextmodel.ReqContext{ Context: &web.Context{}, } ctx := ctxkey.Set(context.Background(), reqCtx) @@ -325,7 +325,7 @@ func TestQueryDataMultipleSources(t *testing.T) { httpreq, err := http.NewRequest(http.MethodPost, "http://localhost/ds/query?expression=true", bytes.NewReader([]byte{})) require.NoError(t, err) - reqCtx := &models.ReqContext{ + reqCtx := &contextmodel.ReqContext{ Context: &web.Context{}, } ctx := ctxkey.Set(context.Background(), reqCtx) diff --git a/pkg/services/queryhistory/api.go b/pkg/services/queryhistory/api.go index 8b99fadb467..43a9fed05eb 100644 --- a/pkg/services/queryhistory/api.go +++ b/pkg/services/queryhistory/api.go @@ -6,7 +6,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/tsdb/legacydata" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" @@ -36,7 +36,7 @@ func (s *QueryHistoryService) registerAPIEndpoints() { // 400: badRequestError // 401: unauthorisedError // 500: internalServerError -func (s *QueryHistoryService) createHandler(c *models.ReqContext) response.Response { +func (s *QueryHistoryService) createHandler(c *contextmodel.ReqContext) response.Response { cmd := CreateQueryInQueryHistoryCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -62,7 +62,7 @@ func (s *QueryHistoryService) createHandler(c *models.ReqContext) response.Respo // 200: getQueryHistorySearchResponse // 401: unauthorisedError // 500: internalServerError -func (s *QueryHistoryService) searchHandler(c *models.ReqContext) response.Response { +func (s *QueryHistoryService) searchHandler(c *contextmodel.ReqContext) response.Response { timeRange := legacydata.NewDataTimeRange(c.Query("from"), c.Query("to")) query := SearchInQueryHistoryQuery{ @@ -94,7 +94,7 @@ func (s *QueryHistoryService) searchHandler(c *models.ReqContext) response.Respo // 200: getQueryHistoryDeleteQueryResponse // 401: unauthorisedError // 500: internalServerError -func (s *QueryHistoryService) deleteHandler(c *models.ReqContext) response.Response { +func (s *QueryHistoryService) deleteHandler(c *contextmodel.ReqContext) response.Response { queryUID := web.Params(c.Req)[":uid"] if len(queryUID) > 0 && !util.IsValidShortUID(queryUID) { return response.Error(http.StatusNotFound, "Query in query history not found", nil) @@ -122,7 +122,7 @@ func (s *QueryHistoryService) deleteHandler(c *models.ReqContext) response.Respo // 400: badRequestError // 401: unauthorisedError // 500: internalServerError -func (s *QueryHistoryService) patchCommentHandler(c *models.ReqContext) response.Response { +func (s *QueryHistoryService) patchCommentHandler(c *contextmodel.ReqContext) response.Response { queryUID := web.Params(c.Req)[":uid"] if len(queryUID) > 0 && !util.IsValidShortUID(queryUID) { return response.Error(http.StatusNotFound, "Query in query history not found", nil) @@ -151,7 +151,7 @@ func (s *QueryHistoryService) patchCommentHandler(c *models.ReqContext) response // 200: getQueryHistoryResponse // 401: unauthorisedError // 500: internalServerError -func (s *QueryHistoryService) starHandler(c *models.ReqContext) response.Response { +func (s *QueryHistoryService) starHandler(c *contextmodel.ReqContext) response.Response { queryUID := web.Params(c.Req)[":uid"] if len(queryUID) > 0 && !util.IsValidShortUID(queryUID) { return response.Error(http.StatusNotFound, "Query in query history not found", nil) @@ -175,7 +175,7 @@ func (s *QueryHistoryService) starHandler(c *models.ReqContext) response.Respons // 200: getQueryHistoryResponse // 401: unauthorisedError // 500: internalServerError -func (s *QueryHistoryService) unstarHandler(c *models.ReqContext) response.Response { +func (s *QueryHistoryService) unstarHandler(c *contextmodel.ReqContext) response.Response { queryUID := web.Params(c.Req)[":uid"] if len(queryUID) > 0 && !util.IsValidShortUID(queryUID) { return response.Error(http.StatusNotFound, "Query in query history not found", nil) @@ -200,7 +200,7 @@ func (s *QueryHistoryService) unstarHandler(c *models.ReqContext) response.Respo // 400: badRequestError // 401: unauthorisedError // 500: internalServerError -func (s *QueryHistoryService) migrateHandler(c *models.ReqContext) response.Response { +func (s *QueryHistoryService) migrateHandler(c *contextmodel.ReqContext) response.Response { cmd := MigrateQueriesToQueryHistoryCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) diff --git a/pkg/services/queryhistory/queryhistory_test.go b/pkg/services/queryhistory/queryhistory_test.go index 2976281b6d8..e598c45b67f 100644 --- a/pkg/services/queryhistory/queryhistory_test.go +++ b/pkg/services/queryhistory/queryhistory_test.go @@ -15,7 +15,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/quota/quotatest" @@ -35,7 +35,7 @@ var ( type scenarioContext struct { ctx *web.Context service *QueryHistoryService - reqContext *models.ReqContext + reqContext *contextmodel.ReqContext sqlStore db.DB initialResult QueryHistoryResponse } @@ -82,7 +82,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo ctx: &ctx, service: &service, sqlStore: sqlStore, - reqContext: &models.ReqContext{ + reqContext: &contextmodel.ReqContext{ Context: &ctx, SignedInUser: &usr, }, diff --git a/pkg/services/querylibrary/querylibraryimpl/http.go b/pkg/services/querylibrary/querylibraryimpl/http.go index 34c25f3054c..10ee1d1fc91 100644 --- a/pkg/services/querylibrary/querylibraryimpl/http.go +++ b/pkg/services/querylibrary/querylibraryimpl/http.go @@ -9,7 +9,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/querylibrary" ) @@ -21,7 +21,7 @@ func (s *queriesServiceHTTPHandler) IsDisabled() bool { return s.service.IsDisabled() } -func (s *queriesServiceHTTPHandler) delete(c *models.ReqContext) response.Response { +func (s *queriesServiceHTTPHandler) delete(c *contextmodel.ReqContext) response.Response { uid := c.Query("uid") err := s.service.Delete(c.Req.Context(), c.SignedInUser, uid) if err != nil { @@ -40,7 +40,7 @@ func (s *queriesServiceHTTPHandler) RegisterHTTPRoutes(routes routing.RouteRegis routes.Delete("/", reqSignedIn, routing.Wrap(s.delete)) } -func (s *queriesServiceHTTPHandler) getBatch(c *models.ReqContext) response.Response { +func (s *queriesServiceHTTPHandler) getBatch(c *contextmodel.ReqContext) response.Response { uids := c.QueryStrings("uid") queries, err := s.service.GetBatch(c.Req.Context(), c.SignedInUser, uids) @@ -51,7 +51,7 @@ func (s *queriesServiceHTTPHandler) getBatch(c *models.ReqContext) response.Resp return response.JSON(200, queries) } -func (s *queriesServiceHTTPHandler) update(c *models.ReqContext) response.Response { +func (s *queriesServiceHTTPHandler) update(c *contextmodel.ReqContext) response.Response { body, err := io.ReadAll(c.Req.Body) if err != nil { return response.Error(500, "error reading bytes", err) diff --git a/pkg/services/quota/quota.go b/pkg/services/quota/quota.go index 13045f41de2..700db61a403 100644 --- a/pkg/services/quota/quota.go +++ b/pkg/services/quota/quota.go @@ -3,7 +3,7 @@ package quota import ( "context" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" ) type Service interface { @@ -16,7 +16,7 @@ type Service interface { // If the cmd.UseID is set, then the user quota are updated. Update(ctx context.Context, cmd *UpdateQuotaCmd) error // QuotaReached is called by the quota middleware for applying quota enforcement to API handlers - QuotaReached(c *models.ReqContext, targetSrv TargetSrv) (bool, error) + QuotaReached(c *contextmodel.ReqContext, targetSrv TargetSrv) (bool, error) // CheckQuotaReached checks if the quota limitations have been reached for a specific service CheckQuotaReached(ctx context.Context, targetSrv TargetSrv, scopeParams *ScopeParameters) (bool, error) // DeleteQuotaForUser deletes custom quota limitations for the user diff --git a/pkg/services/quota/quotaimpl/quota.go b/pkg/services/quota/quotaimpl/quota.go index 65763f20463..1eebaad635a 100644 --- a/pkg/services/quota/quotaimpl/quota.go +++ b/pkg/services/quota/quotaimpl/quota.go @@ -6,7 +6,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/setting" "golang.org/x/sync/errgroup" @@ -15,7 +15,7 @@ import ( type serviceDisabled struct { } -func (s *serviceDisabled) QuotaReached(c *models.ReqContext, targetSrv quota.TargetSrv) (bool, error) { +func (s *serviceDisabled) QuotaReached(c *contextmodel.ReqContext, targetSrv quota.TargetSrv) (bool, error) { return false, nil } @@ -75,7 +75,7 @@ func (s *service) IsDisabled() bool { } // QuotaReached checks that quota is reached for a target. Runs CheckQuotaReached and take context and scope parameters from the request context -func (s *service) QuotaReached(c *models.ReqContext, targetSrv quota.TargetSrv) (bool, error) { +func (s *service) QuotaReached(c *contextmodel.ReqContext, targetSrv quota.TargetSrv) (bool, error) { // No request context means this is a background service, like LDAP Background Sync if c == nil { return false, nil diff --git a/pkg/services/quota/quotatest/fake.go b/pkg/services/quota/quotatest/fake.go index d62267d9276..16fa2bc6403 100644 --- a/pkg/services/quota/quotatest/fake.go +++ b/pkg/services/quota/quotatest/fake.go @@ -3,7 +3,7 @@ package quotatest import ( "context" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/quota" ) @@ -24,7 +24,7 @@ func (f *FakeQuotaService) Update(ctx context.Context, cmd *quota.UpdateQuotaCmd return nil } -func (f *FakeQuotaService) QuotaReached(c *models.ReqContext, target quota.TargetSrv) (bool, error) { +func (f *FakeQuotaService) QuotaReached(c *contextmodel.ReqContext, target quota.TargetSrv) (bool, error) { return f.reached, f.err } diff --git a/pkg/services/searchV2/http.go b/pkg/services/searchV2/http.go index 0d1da01ad83..4bc03fcb1ee 100644 --- a/pkg/services/searchV2/http.go +++ b/pkg/services/searchV2/http.go @@ -10,7 +10,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/prometheus/client_golang/prometheus" ) @@ -30,7 +30,7 @@ func (s *searchHTTPService) RegisterHTTPRoutes(storageRoute routing.RouteRegiste storageRoute.Post("/", middleware.ReqSignedIn, routing.Wrap(s.doQuery)) } -func (s *searchHTTPService) doQuery(c *models.ReqContext) response.Response { +func (s *searchHTTPService) doQuery(c *contextmodel.ReqContext) response.Response { searchReadinessCheckResp := s.search.IsReady(c.Req.Context(), c.OrgID) if !searchReadinessCheckResp.IsReady { dashboardSearchNotServedRequestsCounter.With(prometheus.Labels{ diff --git a/pkg/services/searchusers/searchusers.go b/pkg/services/searchusers/searchusers.go index 4e57c678036..bd82503d691 100644 --- a/pkg/services/searchusers/searchusers.go +++ b/pkg/services/searchusers/searchusers.go @@ -5,14 +5,14 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/user" ) type Service interface { - SearchUsers(c *models.ReqContext) response.Response - SearchUsersWithPaging(c *models.ReqContext) response.Response + SearchUsers(c *contextmodel.ReqContext) response.Response + SearchUsersWithPaging(c *contextmodel.ReqContext) response.Response } type OSSService struct { @@ -39,7 +39,7 @@ func ProvideUsersService(searchUserFilter user.SearchUserFilter, userService use // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (s *OSSService) SearchUsers(c *models.ReqContext) response.Response { +func (s *OSSService) SearchUsers(c *contextmodel.ReqContext) response.Response { result, err := s.SearchUser(c) if err != nil { return response.Error(500, "Failed to fetch users", err) @@ -58,7 +58,7 @@ func (s *OSSService) SearchUsers(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (s *OSSService) SearchUsersWithPaging(c *models.ReqContext) response.Response { +func (s *OSSService) SearchUsersWithPaging(c *contextmodel.ReqContext) response.Response { result, err := s.SearchUser(c) if err != nil { return response.Error(500, "Failed to fetch users", err) @@ -67,7 +67,7 @@ func (s *OSSService) SearchUsersWithPaging(c *models.ReqContext) response.Respon return response.JSON(http.StatusOK, result) } -func (s *OSSService) SearchUser(c *models.ReqContext) (*user.SearchUserQueryResult, error) { +func (s *OSSService) SearchUser(c *contextmodel.ReqContext) (*user.SearchUserQueryResult, error) { perPage := c.QueryInt("perpage") if perPage <= 0 { perPage = 1000 diff --git a/pkg/services/serviceaccounts/api/api.go b/pkg/services/serviceaccounts/api/api.go index fc20c400a3a..d48ee7efb1c 100644 --- a/pkg/services/serviceaccounts/api/api.go +++ b/pkg/services/serviceaccounts/api/api.go @@ -11,9 +11,9 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apikey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts/database" @@ -117,7 +117,7 @@ func (api *ServiceAccountsAPI) RegisterAPIEndpoints() { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (api *ServiceAccountsAPI) CreateServiceAccount(c *models.ReqContext) response.Response { +func (api *ServiceAccountsAPI) CreateServiceAccount(c *contextmodel.ReqContext) response.Response { cmd := serviceaccounts.CreateServiceAccountForm{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "Bad request data", err) @@ -171,7 +171,7 @@ func (api *ServiceAccountsAPI) CreateServiceAccount(c *models.ReqContext) respon // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (api *ServiceAccountsAPI) RetrieveServiceAccount(ctx *models.ReqContext) response.Response { +func (api *ServiceAccountsAPI) RetrieveServiceAccount(ctx *contextmodel.ReqContext) response.Response { scopeID, err := strconv.ParseInt(web.Params(ctx.Req)[":serviceAccountId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "Service Account ID is invalid", err) @@ -218,7 +218,7 @@ func (api *ServiceAccountsAPI) RetrieveServiceAccount(ctx *models.ReqContext) re // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (api *ServiceAccountsAPI) UpdateServiceAccount(c *models.ReqContext) response.Response { +func (api *ServiceAccountsAPI) UpdateServiceAccount(c *contextmodel.ReqContext) response.Response { scopeID, err := strconv.ParseInt(web.Params(c.Req)[":serviceAccountId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "Service Account ID is invalid", err) @@ -286,7 +286,7 @@ func (api *ServiceAccountsAPI) validateRole(r *org.RoleType, orgRole *org.RoleTy // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (api *ServiceAccountsAPI) DeleteServiceAccount(ctx *models.ReqContext) response.Response { +func (api *ServiceAccountsAPI) DeleteServiceAccount(ctx *contextmodel.ReqContext) response.Response { scopeID, err := strconv.ParseInt(web.Params(ctx.Req)[":serviceAccountId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "Service account ID is invalid", err) @@ -310,7 +310,7 @@ func (api *ServiceAccountsAPI) DeleteServiceAccount(ctx *models.ReqContext) resp // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (api *ServiceAccountsAPI) SearchOrgServiceAccountsWithPaging(c *models.ReqContext) response.Response { +func (api *ServiceAccountsAPI) SearchOrgServiceAccountsWithPaging(c *contextmodel.ReqContext) response.Response { ctx := c.Req.Context() perPage := c.QueryInt("perpage") if perPage <= 0 { @@ -365,7 +365,7 @@ func (api *ServiceAccountsAPI) SearchOrgServiceAccountsWithPaging(c *models.ReqC } // GET /api/serviceaccounts/migrationstatus -func (api *ServiceAccountsAPI) GetAPIKeysMigrationStatus(ctx *models.ReqContext) response.Response { +func (api *ServiceAccountsAPI) GetAPIKeysMigrationStatus(ctx *contextmodel.ReqContext) response.Response { upgradeStatus, err := api.service.GetAPIKeysMigrationStatus(ctx.Req.Context(), ctx.OrgID) if err != nil { return response.Error(http.StatusInternalServerError, "Internal server error", err) @@ -374,7 +374,7 @@ func (api *ServiceAccountsAPI) GetAPIKeysMigrationStatus(ctx *models.ReqContext) } // POST /api/serviceaccounts/hideapikeys -func (api *ServiceAccountsAPI) HideApiKeysTab(ctx *models.ReqContext) response.Response { +func (api *ServiceAccountsAPI) HideApiKeysTab(ctx *contextmodel.ReqContext) response.Response { if err := api.service.HideApiKeysTab(ctx.Req.Context(), ctx.OrgID); err != nil { return response.Error(http.StatusInternalServerError, "Internal server error", err) } @@ -382,7 +382,7 @@ func (api *ServiceAccountsAPI) HideApiKeysTab(ctx *models.ReqContext) response.R } // POST /api/serviceaccounts/migrate -func (api *ServiceAccountsAPI) MigrateApiKeysToServiceAccounts(ctx *models.ReqContext) response.Response { +func (api *ServiceAccountsAPI) MigrateApiKeysToServiceAccounts(ctx *contextmodel.ReqContext) response.Response { if err := api.service.MigrateApiKeysToServiceAccounts(ctx.Req.Context(), ctx.OrgID); err != nil { return response.Error(http.StatusInternalServerError, "Internal server error", err) } @@ -391,7 +391,7 @@ func (api *ServiceAccountsAPI) MigrateApiKeysToServiceAccounts(ctx *models.ReqCo } // POST /api/serviceaccounts/migrate/:keyId -func (api *ServiceAccountsAPI) ConvertToServiceAccount(ctx *models.ReqContext) response.Response { +func (api *ServiceAccountsAPI) ConvertToServiceAccount(ctx *contextmodel.ReqContext) response.Response { keyId, err := strconv.ParseInt(web.Params(ctx.Req)[":keyId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "Key ID is invalid", err) @@ -405,7 +405,7 @@ func (api *ServiceAccountsAPI) ConvertToServiceAccount(ctx *models.ReqContext) r } // POST /api/serviceaccounts/revert/:keyId -func (api *ServiceAccountsAPI) RevertApiKey(ctx *models.ReqContext) response.Response { +func (api *ServiceAccountsAPI) RevertApiKey(ctx *contextmodel.ReqContext) response.Response { keyId, err := strconv.ParseInt(web.Params(ctx.Req)[":keyId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "key ID is invalid", err) @@ -421,7 +421,7 @@ func (api *ServiceAccountsAPI) RevertApiKey(ctx *models.ReqContext) response.Res return response.Success("reverted service account to API key") } -func (api *ServiceAccountsAPI) getAccessControlMetadata(c *models.ReqContext, saIDs map[string]bool) map[string]accesscontrol.Metadata { +func (api *ServiceAccountsAPI) getAccessControlMetadata(c *contextmodel.ReqContext, saIDs map[string]bool) map[string]accesscontrol.Metadata { if api.accesscontrol.IsDisabled() || !c.QueryBool("accesscontrol") { return map[string]accesscontrol.Metadata{} } diff --git a/pkg/services/serviceaccounts/api/token.go b/pkg/services/serviceaccounts/api/token.go index 6be829ccad5..5867cc7734f 100644 --- a/pkg/services/serviceaccounts/api/token.go +++ b/pkg/services/serviceaccounts/api/token.go @@ -9,8 +9,8 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" apikeygenprefix "github.com/grafana/grafana/pkg/components/apikeygenprefixed" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/apikey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts/database" "github.com/grafana/grafana/pkg/web" @@ -66,7 +66,7 @@ const sevenDaysAhead = 7 * 24 * time.Hour // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (api *ServiceAccountsAPI) ListTokens(ctx *models.ReqContext) response.Response { +func (api *ServiceAccountsAPI) ListTokens(ctx *contextmodel.ReqContext) response.Response { saID, err := strconv.ParseInt(web.Params(ctx.Req)[":serviceAccountId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "Service Account ID is invalid", err) @@ -127,7 +127,7 @@ func (api *ServiceAccountsAPI) ListTokens(ctx *models.ReqContext) response.Respo // 404: notFoundError // 409: conflictError // 500: internalServerError -func (api *ServiceAccountsAPI) CreateToken(c *models.ReqContext) response.Response { +func (api *ServiceAccountsAPI) CreateToken(c *contextmodel.ReqContext) response.Response { saID, err := strconv.ParseInt(web.Params(c.Req)[":serviceAccountId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "Service Account ID is invalid", err) @@ -210,7 +210,7 @@ func (api *ServiceAccountsAPI) CreateToken(c *models.ReqContext) response.Respon // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (api *ServiceAccountsAPI) DeleteToken(c *models.ReqContext) response.Response { +func (api *ServiceAccountsAPI) DeleteToken(c *contextmodel.ReqContext) response.Response { saID, err := strconv.ParseInt(web.Params(c.Req)[":serviceAccountId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "Service Account ID is invalid", err) diff --git a/pkg/services/star/api/api.go b/pkg/services/star/api/api.go index 1867e952116..ec231596e4c 100644 --- a/pkg/services/star/api/api.go +++ b/pkg/services/star/api/api.go @@ -6,7 +6,7 @@ import ( "strconv" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/star" "github.com/grafana/grafana/pkg/web" @@ -45,7 +45,7 @@ func (api *API) getDashboardHelper(ctx context.Context, orgID int64, id int64, u return result, nil } -func (api *API) GetStars(c *models.ReqContext) response.Response { +func (api *API) GetStars(c *contextmodel.ReqContext) response.Response { query := star.GetUserStarsQuery{ UserID: c.SignedInUser.UserID, } @@ -85,7 +85,7 @@ func (api *API) GetStars(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (api *API) StarDashboard(c *models.ReqContext) response.Response { +func (api *API) StarDashboard(c *contextmodel.ReqContext) response.Response { id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "Invalid dashboard ID", nil) @@ -115,7 +115,7 @@ func (api *API) StarDashboard(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (api *API) StarDashboardByUID(c *models.ReqContext) response.Response { +func (api *API) StarDashboardByUID(c *contextmodel.ReqContext) response.Response { uid := web.Params(c.Req)[":uid"] if uid == "" { return response.Error(http.StatusBadRequest, "Invalid dashboard UID", nil) @@ -151,7 +151,7 @@ func (api *API) StarDashboardByUID(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (api *API) UnstarDashboard(c *models.ReqContext) response.Response { +func (api *API) UnstarDashboard(c *contextmodel.ReqContext) response.Response { id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "Invalid dashboard ID", nil) @@ -181,7 +181,7 @@ func (api *API) UnstarDashboard(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (api *API) UnstarDashboardByUID(c *models.ReqContext) response.Response { +func (api *API) UnstarDashboardByUID(c *contextmodel.ReqContext) response.Response { uid := web.Params(c.Req)[":uid"] if uid == "" { return response.Error(http.StatusBadRequest, "Invalid dashboard UID", nil) diff --git a/pkg/services/store/entity/httpentitystore/service.go b/pkg/services/store/entity/httpentitystore/service.go index 786c8a28bfd..649206ed0b0 100644 --- a/pkg/services/store/entity/httpentitystore/service.go +++ b/pkg/services/store/entity/httpentitystore/service.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/middleware" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/store/entity" "github.com/grafana/grafana/pkg/services/store/kind" "github.com/grafana/grafana/pkg/util" @@ -17,7 +18,6 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/models" ) type HTTPEntityStore interface { @@ -60,7 +60,7 @@ func (s *httpEntityStore) RegisterHTTPRoutes(route routing.RouteRegister) { // This function will extract UID+Kind from the requested path "*" in our router // This is far from ideal! but is at least consistent for these endpoints. // This will quickly be revisited as we explore how to encode UID+Kind in a "GRN" format -func (s *httpEntityStore) getGRNFromRequest(c *models.ReqContext) (*entity.GRN, map[string]string, error) { +func (s *httpEntityStore) getGRNFromRequest(c *contextmodel.ReqContext) (*entity.GRN, map[string]string, error) { params := web.Params(c.Req) // Read parameters that are encoded in the URL vals := c.Req.URL.Query() @@ -76,7 +76,7 @@ func (s *httpEntityStore) getGRNFromRequest(c *models.ReqContext) (*entity.GRN, }, params, nil } -func (s *httpEntityStore) doGetEntity(c *models.ReqContext) response.Response { +func (s *httpEntityStore) doGetEntity(c *contextmodel.ReqContext) response.Response { grn, params, err := s.getGRNFromRequest(c) if err != nil { return response.Error(400, err.Error(), err) @@ -111,7 +111,7 @@ func (s *httpEntityStore) doGetEntity(c *models.ReqContext) response.Response { return response.JSON(200, rsp) } -func (s *httpEntityStore) doGetRawEntity(c *models.ReqContext) response.Response { +func (s *httpEntityStore) doGetRawEntity(c *contextmodel.ReqContext) response.Response { grn, params, err := s.getGRNFromRequest(c) if err != nil { return response.Error(400, err.Error(), err) @@ -161,7 +161,7 @@ func (s *httpEntityStore) doGetRawEntity(c *models.ReqContext) response.Response const MAX_UPLOAD_SIZE = 5 * 1024 * 1024 // 5MB -func (s *httpEntityStore) doWriteEntity(c *models.ReqContext) response.Response { +func (s *httpEntityStore) doWriteEntity(c *contextmodel.ReqContext) response.Response { grn, params, err := s.getGRNFromRequest(c) if err != nil { return response.Error(400, err.Error(), err) @@ -187,7 +187,7 @@ func (s *httpEntityStore) doWriteEntity(c *models.ReqContext) response.Response return response.JSON(200, rsp) } -func (s *httpEntityStore) doDeleteEntity(c *models.ReqContext) response.Response { +func (s *httpEntityStore) doDeleteEntity(c *contextmodel.ReqContext) response.Response { grn, params, err := s.getGRNFromRequest(c) if err != nil { return response.Error(400, err.Error(), err) @@ -202,7 +202,7 @@ func (s *httpEntityStore) doDeleteEntity(c *models.ReqContext) response.Response return response.JSON(200, rsp) } -func (s *httpEntityStore) doGetHistory(c *models.ReqContext) response.Response { +func (s *httpEntityStore) doGetHistory(c *contextmodel.ReqContext) response.Response { grn, params, err := s.getGRNFromRequest(c) if err != nil { return response.Error(400, err.Error(), err) @@ -219,7 +219,7 @@ func (s *httpEntityStore) doGetHistory(c *models.ReqContext) response.Response { return response.JSON(200, rsp) } -func (s *httpEntityStore) doUpload(c *models.ReqContext) response.Response { +func (s *httpEntityStore) doUpload(c *contextmodel.ReqContext) response.Response { c.Req.Body = http.MaxBytesReader(c.Resp, c.Req.Body, MAX_UPLOAD_SIZE) if err := c.Req.ParseMultipartForm(MAX_UPLOAD_SIZE); err != nil { msg := fmt.Sprintf("Please limit file uploaded under %s", util.ByteCountSI(MAX_UPLOAD_SIZE)) @@ -302,11 +302,11 @@ func (s *httpEntityStore) doUpload(c *models.ReqContext) response.Response { return response.JSON(200, rsp) } -func (s *httpEntityStore) doListFolder(c *models.ReqContext) response.Response { +func (s *httpEntityStore) doListFolder(c *contextmodel.ReqContext) response.Response { return response.JSON(501, "Not implemented yet") } -func (s *httpEntityStore) doSearch(c *models.ReqContext) response.Response { +func (s *httpEntityStore) doSearch(c *contextmodel.ReqContext) response.Response { vals := c.Req.URL.Query() req := &entity.EntitySearchRequest{ diff --git a/pkg/services/store/http.go b/pkg/services/store/http.go index 47219d2652c..52de0d15a95 100644 --- a/pkg/services/store/http.go +++ b/pkg/services/store/http.go @@ -11,7 +11,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" ) @@ -57,7 +57,7 @@ func (s *standardStorageService) RegisterHTTPRoutes(storageRoute routing.RouteRe storageRoute.Get("/config", reqGrafanaAdmin, routing.Wrap(s.getConfig)) } -func (s *standardStorageService) doWrite(c *models.ReqContext) response.Response { +func (s *standardStorageService) doWrite(c *contextmodel.ReqContext) response.Response { scope, path := getPathAndScope(c) cmd := &WriteValueRequest{} if err := web.Bind(c.Req, cmd); err != nil { @@ -71,7 +71,7 @@ func (s *standardStorageService) doWrite(c *models.ReqContext) response.Response return response.JSON(200, rsp) } -func (s *standardStorageService) doUpload(c *models.ReqContext) response.Response { +func (s *standardStorageService) doUpload(c *contextmodel.ReqContext) response.Response { type rspInfo struct { Message string `json:"message,omitempty"` Path string `json:"path,omitempty"` @@ -158,7 +158,7 @@ func getMultipartFormValue(req *http.Request, key string) string { return v[0] } -func (s *standardStorageService) read(c *models.ReqContext) response.Response { +func (s *standardStorageService) read(c *contextmodel.ReqContext) response.Response { // full path is api/storage/read/upload/example.jpg, but we only want the part after read scope, path := getPathAndScope(c) file, err := s.Read(c.Req.Context(), c.SignedInUser, scope+"/"+path) @@ -177,7 +177,7 @@ func (s *standardStorageService) read(c *models.ReqContext) response.Response { return response.Respond(200, file.Contents) } -func (s *standardStorageService) getOptions(c *models.ReqContext) response.Response { +func (s *standardStorageService) getOptions(c *contextmodel.ReqContext) response.Response { scope, path := getPathAndScope(c) opts, err := s.getWorkflowOptions(c.Req.Context(), c.SignedInUser, scope+"/"+path) if err != nil { @@ -186,7 +186,7 @@ func (s *standardStorageService) getOptions(c *models.ReqContext) response.Respo return response.JSON(200, opts) } -func (s *standardStorageService) doDelete(c *models.ReqContext) response.Response { +func (s *standardStorageService) doDelete(c *contextmodel.ReqContext) response.Response { // full path is api/storage/delete/upload/example.jpg, but we only want the part after upload scope, path := getPathAndScope(c) @@ -201,7 +201,7 @@ func (s *standardStorageService) doDelete(c *models.ReqContext) response.Respons }) } -func (s *standardStorageService) doDeleteFolder(c *models.ReqContext) response.Response { +func (s *standardStorageService) doDeleteFolder(c *contextmodel.ReqContext) response.Response { body, err := io.ReadAll(c.Req.Body) if err != nil { return response.Error(500, "error reading bytes", err) @@ -230,7 +230,7 @@ func (s *standardStorageService) doDeleteFolder(c *models.ReqContext) response.R }) } -func (s *standardStorageService) doCreateFolder(c *models.ReqContext) response.Response { +func (s *standardStorageService) doCreateFolder(c *contextmodel.ReqContext) response.Response { body, err := io.ReadAll(c.Req.Body) if err != nil { return response.Error(500, "error reading bytes", err) @@ -257,7 +257,7 @@ func (s *standardStorageService) doCreateFolder(c *models.ReqContext) response.R }) } -func (s *standardStorageService) list(c *models.ReqContext) response.Response { +func (s *standardStorageService) list(c *contextmodel.ReqContext) response.Response { params := web.Params(c.Req) path := params["*"] frame, err := s.List(c.Req.Context(), c.SignedInUser, path) @@ -270,7 +270,7 @@ func (s *standardStorageService) list(c *models.ReqContext) response.Response { return response.JSONStreaming(http.StatusOK, frame) } -func (s *standardStorageService) getConfig(c *models.ReqContext) response.Response { +func (s *standardStorageService) getConfig(c *contextmodel.ReqContext) response.Response { roots := make([]RootStorageMeta, 0) orgId := c.OrgID t := s.tree diff --git a/pkg/services/store/k8saccess/client.go b/pkg/services/store/k8saccess/client.go index ecdefb90191..f8d143ecdbf 100644 --- a/pkg/services/store/k8saccess/client.go +++ b/pkg/services/store/k8saccess/client.go @@ -4,7 +4,7 @@ import ( "net/http" "net/url" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/web" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/kubernetes" @@ -84,7 +84,7 @@ func defaultServerUrlFor(config *rest.Config) (*url.URL, string, error) { return rest.DefaultServerURL(host, config.APIPath, schema.GroupVersion{}, defaultTLS) } -func (s *clientWrapper) doProxy(c *models.ReqContext) { +func (s *clientWrapper) doProxy(c *contextmodel.ReqContext) { if s.baseURL == nil { c.Resp.WriteHeader(500) return diff --git a/pkg/services/store/k8saccess/http.go b/pkg/services/store/k8saccess/http.go index 503bc9e4f9d..4a4f64c7210 100644 --- a/pkg/services/store/k8saccess/http.go +++ b/pkg/services/store/k8saccess/http.go @@ -4,7 +4,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" ) type httpHelper struct { @@ -25,7 +25,7 @@ func newHTTPHelper(access *k8sAccess, router routing.RouteRegister) *httpHelper return s } -func (s *httpHelper) showClientInfo(c *models.ReqContext) response.Response { +func (s *httpHelper) showClientInfo(c *contextmodel.ReqContext) response.Response { if s.access.sys != nil { info := s.access.sys.getInfo() if s.access.sys.err != nil { @@ -38,7 +38,7 @@ func (s *httpHelper) showClientInfo(c *models.ReqContext) response.Response { }) } -func (s *httpHelper) doProxy(c *models.ReqContext) { +func (s *httpHelper) doProxy(c *contextmodel.ReqContext) { // TODO... this does not yet do a real proxy if s.access.sys != nil { if s.access.sys.err == nil { diff --git a/pkg/services/store/utils.go b/pkg/services/store/utils.go index fe275b51291..6537af8ab7b 100644 --- a/pkg/services/store/utils.go +++ b/pkg/services/store/utils.go @@ -3,7 +3,7 @@ package store import ( "strings" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/web" ) @@ -32,7 +32,7 @@ func splitFirstSegment(path string) (string, string) { return path, "" } -func getPathAndScope(c *models.ReqContext) (string, string) { +func getPathAndScope(c *contextmodel.ReqContext) (string, string) { params := web.Params(c.Req) path := params["*"] if path == "" { diff --git a/pkg/services/supportbundles/supportbundlesimpl/api.go b/pkg/services/supportbundles/supportbundlesimpl/api.go index 373789d1a31..74551db8419 100644 --- a/pkg/services/supportbundles/supportbundlesimpl/api.go +++ b/pkg/services/supportbundles/supportbundlesimpl/api.go @@ -10,9 +10,9 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models/roletype" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/supportbundles" "github.com/grafana/grafana/pkg/web" ) @@ -49,7 +49,7 @@ func (s *Service) registerAPIEndpoints(httpServer *grafanaApi.HTTPServer, routeR }) } -func (s *Service) handleList(ctx *models.ReqContext) response.Response { +func (s *Service) handleList(ctx *contextmodel.ReqContext) response.Response { bundles, err := s.list(ctx.Req.Context()) if err != nil { return response.Error(http.StatusInternalServerError, "failed to list bundles", err) @@ -63,7 +63,7 @@ func (s *Service) handleList(ctx *models.ReqContext) response.Response { return response.JSON(http.StatusOK, data) } -func (s *Service) handleCreate(ctx *models.ReqContext) response.Response { +func (s *Service) handleCreate(ctx *contextmodel.ReqContext) response.Response { type command struct { Collectors []string `json:"collectors"` } @@ -86,7 +86,7 @@ func (s *Service) handleCreate(ctx *models.ReqContext) response.Response { return response.JSON(http.StatusCreated, data) } -func (s *Service) handleDownload(ctx *models.ReqContext) response.Response { +func (s *Service) handleDownload(ctx *contextmodel.ReqContext) response.Response { uid := web.Params(ctx.Req)[":uid"] bundle, err := s.get(ctx.Req.Context(), uid) if err != nil { @@ -102,7 +102,7 @@ func (s *Service) handleDownload(ctx *models.ReqContext) response.Response { return response.CreateNormalResponse(ctx.Resp.Header(), bundle.TarBytes, http.StatusOK) } -func (s *Service) handleRemove(ctx *models.ReqContext) response.Response { +func (s *Service) handleRemove(ctx *contextmodel.ReqContext) response.Response { uid := web.Params(ctx.Req)[":uid"] err := s.remove(ctx.Req.Context(), uid) if err != nil { @@ -112,7 +112,7 @@ func (s *Service) handleRemove(ctx *models.ReqContext) response.Response { return response.Respond(http.StatusOK, "successfully removed the support bundle") } -func (s *Service) handleGetCollectors(ctx *models.ReqContext) response.Response { +func (s *Service) handleGetCollectors(ctx *contextmodel.ReqContext) response.Response { collectors := make([]supportbundles.Collector, 0, len(s.collectors)) for _, c := range s.collectors { diff --git a/pkg/services/thumbs/dummy.go b/pkg/services/thumbs/dummy.go index 09d2045cb5b..9f77fe37891 100644 --- a/pkg/services/thumbs/dummy.go +++ b/pkg/services/thumbs/dummy.go @@ -5,7 +5,7 @@ import ( "net/http" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" ) // When the feature flag is not enabled we just implement a dummy service @@ -15,15 +15,15 @@ func (ds *dummyService) GetUsageStats(ctx context.Context) map[string]interface{ return make(map[string]interface{}) } -func (ds *dummyService) GetImage(c *models.ReqContext) { +func (ds *dummyService) GetImage(c *contextmodel.ReqContext) { c.JSON(400, map[string]string{"error": "invalid size"}) } -func (ds *dummyService) UpdateThumbnailState(c *models.ReqContext) { +func (ds *dummyService) UpdateThumbnailState(c *contextmodel.ReqContext) { c.JSON(400, map[string]string{"error": "invalid size"}) } -func (ds *dummyService) SetImage(c *models.ReqContext) { +func (ds *dummyService) SetImage(c *contextmodel.ReqContext) { c.JSON(400, map[string]string{"error": "invalid size"}) } @@ -31,7 +31,7 @@ func (ds *dummyService) Enabled() bool { return false } -func (ds *dummyService) GetDashboardPreviewsSetupSettings(c *models.ReqContext) dashboardPreviewsSetupConfig { +func (ds *dummyService) GetDashboardPreviewsSetupSettings(c *contextmodel.ReqContext) dashboardPreviewsSetupConfig { return dashboardPreviewsSetupConfig{ SystemRequirements: dashboardPreviewsSystemRequirements{ Met: false, @@ -41,19 +41,19 @@ func (ds *dummyService) GetDashboardPreviewsSetupSettings(c *models.ReqContext) } } -func (ds *dummyService) StartCrawler(c *models.ReqContext) response.Response { +func (ds *dummyService) StartCrawler(c *contextmodel.ReqContext) response.Response { result := make(map[string]string) result["error"] = "Not enabled" return response.JSON(http.StatusOK, result) } -func (ds *dummyService) StopCrawler(c *models.ReqContext) response.Response { +func (ds *dummyService) StopCrawler(c *contextmodel.ReqContext) response.Response { result := make(map[string]string) result["error"] = "Not enabled" return response.JSON(http.StatusOK, result) } -func (ds *dummyService) CrawlerStatus(c *models.ReqContext) response.Response { +func (ds *dummyService) CrawlerStatus(c *contextmodel.ReqContext) response.Response { result := make(map[string]string) result["error"] = "Not enabled" return response.JSON(http.StatusOK, result) diff --git a/pkg/services/thumbs/service.go b/pkg/services/thumbs/service.go index da15dc8a1d2..f34eb8af566 100644 --- a/pkg/services/thumbs/service.go +++ b/pkg/services/thumbs/service.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/infra/serverlock" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/registry" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources/permissions" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -32,17 +33,17 @@ type Service interface { registry.ProvidesUsageStats Run(ctx context.Context) error Enabled() bool - GetImage(c *models.ReqContext) - GetDashboardPreviewsSetupSettings(c *models.ReqContext) dashboardPreviewsSetupConfig + GetImage(c *contextmodel.ReqContext) + GetDashboardPreviewsSetupSettings(c *contextmodel.ReqContext) dashboardPreviewsSetupConfig // from dashboard page - SetImage(c *models.ReqContext) // form post - UpdateThumbnailState(c *models.ReqContext) + SetImage(c *contextmodel.ReqContext) // form post + UpdateThumbnailState(c *contextmodel.ReqContext) // Must be admin - StartCrawler(c *models.ReqContext) response.Response - StopCrawler(c *models.ReqContext) response.Response - CrawlerStatus(c *models.ReqContext) response.Response + StartCrawler(c *contextmodel.ReqContext) response.Response + StopCrawler(c *contextmodel.ReqContext) response.Response + CrawlerStatus(c *contextmodel.ReqContext) response.Response } type thumbService struct { @@ -154,7 +155,7 @@ func (hs *thumbService) Enabled() bool { return hs.features.IsEnabled(featuremgmt.FlagDashboardPreviews) } -func (hs *thumbService) parseImageReq(c *models.ReqContext, checkSave bool) *previewRequest { +func (hs *thumbService) parseImageReq(c *contextmodel.ReqContext, checkSave bool) *previewRequest { params := web.Params(c.Req) kind, err := ParseThumbnailKind(params[":kind"]) @@ -199,7 +200,7 @@ type updateThumbnailStateRequest struct { State ThumbnailState `json:"state" binding:"Required"` } -func (hs *thumbService) UpdateThumbnailState(c *models.ReqContext) { +func (hs *thumbService) UpdateThumbnailState(c *contextmodel.ReqContext) { req := hs.parseImageReq(c, false) if req == nil { return // already returned value @@ -231,7 +232,7 @@ func (hs *thumbService) UpdateThumbnailState(c *models.ReqContext) { c.JSON(http.StatusOK, map[string]string{"success": "true"}) } -func (hs *thumbService) GetImage(c *models.ReqContext) { +func (hs *thumbService) GetImage(c *contextmodel.ReqContext) { req := hs.parseImageReq(c, false) if req == nil { return // already returned value @@ -274,7 +275,7 @@ func (hs *thumbService) GetImage(c *models.ReqContext) { } } -func (hs *thumbService) hasAccessToPreview(c *models.ReqContext, res *DashboardThumbnail, req *previewRequest) bool { +func (hs *thumbService) hasAccessToPreview(c *contextmodel.ReqContext, res *DashboardThumbnail, req *previewRequest) bool { if !hs.licensing.FeatureEnabled("accesscontrol.enforcement") { return true } @@ -318,7 +319,7 @@ func (hs *thumbService) hasAccessToPreview(c *models.ReqContext, res *DashboardT return true } -func (hs *thumbService) GetDashboardPreviewsSetupSettings(c *models.ReqContext) dashboardPreviewsSetupConfig { +func (hs *thumbService) GetDashboardPreviewsSetupSettings(c *contextmodel.ReqContext) dashboardPreviewsSetupConfig { return hs.getDashboardPreviewsSetupSettings(c.Req.Context()) } @@ -361,7 +362,7 @@ func (hs *thumbService) getSystemRequirements(ctx context.Context) dashboardPrev } // Hack for now -- lets you upload images explicitly -func (hs *thumbService) SetImage(c *models.ReqContext) { +func (hs *thumbService) SetImage(c *contextmodel.ReqContext) { req := hs.parseImageReq(c, false) if req == nil { return // already returned value @@ -423,7 +424,7 @@ func (hs *thumbService) SetImage(c *models.ReqContext) { c.JSON(http.StatusOK, map[string]int{"OK": len(fileBytes)}) } -func (hs *thumbService) StartCrawler(c *models.ReqContext) response.Response { +func (hs *thumbService) StartCrawler(c *contextmodel.ReqContext) response.Response { body, err := io.ReadAll(c.Req.Body) if err != nil { return response.Error(500, "error reading bytes", err) @@ -451,7 +452,7 @@ func (hs *thumbService) StartCrawler(c *models.ReqContext) response.Response { return response.JSON(http.StatusOK, status) } -func (hs *thumbService) StopCrawler(c *models.ReqContext) response.Response { +func (hs *thumbService) StopCrawler(c *contextmodel.ReqContext) response.Response { msg, err := hs.renderer.Stop() if err != nil { return response.Error(500, "error starting", err) @@ -459,7 +460,7 @@ func (hs *thumbService) StopCrawler(c *models.ReqContext) response.Response { return response.JSON(http.StatusOK, msg) } -func (hs *thumbService) CrawlerStatus(c *models.ReqContext) response.Response { +func (hs *thumbService) CrawlerStatus(c *contextmodel.ReqContext) response.Response { msg, err := hs.renderer.Status() if err != nil { return response.Error(500, "error starting", err) @@ -468,7 +469,7 @@ func (hs *thumbService) CrawlerStatus(c *models.ReqContext) response.Response { } // Ideally this service would not require first looking up the full dashboard just to bet the id! -func (hs *thumbService) getStatus(c *models.ReqContext, uid string, checkSave bool) (int, error) { +func (hs *thumbService) getStatus(c *contextmodel.ReqContext, uid string, checkSave bool) (int, error) { guardian, err := guardian.NewByUID(c.Req.Context(), uid, c.OrgID, c.SignedInUser) if err != nil { return 0, err diff --git a/pkg/web/webtest/webtest.go b/pkg/web/webtest/webtest.go index d76f6b56b5d..d29764e2e45 100644 --- a/pkg/web/webtest/webtest.go +++ b/pkg/web/webtest/webtest.go @@ -10,13 +10,13 @@ import ( "github.com/google/uuid" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/web" ) -var requests = map[string]*models.ReqContext{} +var requests = map[string]*contextmodel.ReqContext{} type Server struct { t testing.TB @@ -30,7 +30,7 @@ func NewServer(t testing.TB, routeRegister routing.RouteRegister) *Server { t.Helper() m := web.New() - initCtx := &models.ReqContext{} + initCtx := &contextmodel.ReqContext{} m.Use(func(c *web.Context) { initCtx.Context = c initCtx.Logger = log.New("api-test") @@ -104,20 +104,20 @@ func requestIdentifierFromRequest(req *http.Request) string { return req.Header.Get("X-GRAFANA-WEB-TEST-ID") } -func RequestWithWebContext(req *http.Request, c *models.ReqContext) *http.Request { +func RequestWithWebContext(req *http.Request, c *contextmodel.ReqContext) *http.Request { reqID := requestIdentifierFromRequest(req) requests[reqID] = c return req } func RequestWithSignedInUser(req *http.Request, user *user.SignedInUser) *http.Request { - return RequestWithWebContext(req, &models.ReqContext{ + return RequestWithWebContext(req, &contextmodel.ReqContext{ SignedInUser: user, IsSignedIn: true, }) } -func requestContextFromRequest(req *http.Request) *models.ReqContext { +func requestContextFromRequest(req *http.Request) *contextmodel.ReqContext { reqID := requestIdentifierFromRequest(req) val, exists := requests[reqID] if !exists { @@ -130,7 +130,7 @@ func requestContextFromRequest(req *http.Request) *models.ReqContext { func requestContextMiddleware() web.Middleware { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - c := ctxkey.Get(r.Context()).(*models.ReqContext) + c := ctxkey.Get(r.Context()).(*contextmodel.ReqContext) ctx := requestContextFromRequest(r) if ctx != nil { diff --git a/pkg/web/webtest/webtest_test.go b/pkg/web/webtest/webtest_test.go index f184f0e6bc5..e250605cada 100644 --- a/pkg/web/webtest/webtest_test.go +++ b/pkg/web/webtest/webtest_test.go @@ -9,7 +9,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/user" "github.com/stretchr/testify/require" ) @@ -17,7 +17,7 @@ import ( func TestServer(t *testing.T) { routeRegister := routing.NewRouteRegister() var actualRequest *http.Request - routeRegister.Post("/api", routing.Wrap(func(c *models.ReqContext) response.Response { + routeRegister.Post("/api", routing.Wrap(func(c *contextmodel.ReqContext) response.Response { actualRequest = c.Req return response.JSON(http.StatusOK, c.SignedInUser) })) @@ -68,7 +68,7 @@ func verifyRequest(t *testing.T, s *Server, req *http.Request, expectedBody stri require.NotEmpty(t, requestIdentifierFromRequest(req)) - req = RequestWithWebContext(req, &models.ReqContext{ + req = RequestWithWebContext(req, &contextmodel.ReqContext{ IsSignedIn: true, }) require.NotNil(t, req) @@ -79,7 +79,7 @@ func verifyRequest(t *testing.T, s *Server, req *http.Request, expectedBody stri func TestServerClient(t *testing.T) { routeRegister := routing.NewRouteRegister() - routeRegister.Get("/test", routing.Wrap(func(c *models.ReqContext) response.Response { + routeRegister.Get("/test", routing.Wrap(func(c *contextmodel.ReqContext) response.Response { return response.JSON(http.StatusOK, c.SignedInUser) })) @@ -87,7 +87,7 @@ func TestServerClient(t *testing.T) { t.Run("Making a request with user 1 should return user 1 as signed in user", func(t *testing.T) { req := s.NewRequest(http.MethodGet, "/test", nil) - req = RequestWithWebContext(req, &models.ReqContext{ + req = RequestWithWebContext(req, &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{ UserID: 1, }, @@ -109,7 +109,7 @@ func TestServerClient(t *testing.T) { t.Run("Making a request with user 2 should return user 2 as signed in user", func(t *testing.T) { req := s.NewRequest(http.MethodGet, "/test", nil) - req = RequestWithWebContext(req, &models.ReqContext{ + req = RequestWithWebContext(req, &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{ UserID: 2, }, From 4c45dea71d71201f1f7203729dae3fc813ebead5 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Fri, 27 Jan 2023 02:12:19 -0600 Subject: [PATCH 006/117] Chore: uPlot 1.6.24 (#62279) --- package.json | 2 +- packages/grafana-data/package.json | 2 +- packages/grafana-ui/package.json | 2 +- yarn.lock | 14 +++++++------- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 84329226067..443277ab0c5 100644 --- a/package.json +++ b/package.json @@ -409,7 +409,7 @@ "tether-drop": "https://github.com/torkelo/drop", "tinycolor2": "1.4.2", "tslib": "2.4.1", - "uplot": "1.6.23", + "uplot": "1.6.24", "uuid": "9.0.0", "vendor": "link:./public/vendor", "visjs-network": "4.25.0", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 1d6ac0f86a5..2336c7fb26a 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -54,7 +54,7 @@ "rxjs": "7.5.7", "tinycolor2": "1.4.2", "tslib": "2.4.1", - "uplot": "1.6.23", + "uplot": "1.6.24", "xss": "1.0.14" }, "devDependencies": { diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index ab5bc4838b5..db5413276cb 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -109,7 +109,7 @@ "slate-react": "0.22.10", "tinycolor2": "1.4.2", "tslib": "2.4.1", - "uplot": "1.6.23", + "uplot": "1.6.24", "uuid": "9.0.0" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 22ff07c4a2a..5c9c7c1ce11 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4640,7 +4640,7 @@ __metadata: tinycolor2: 1.4.2 tslib: 2.4.1 typescript: 4.8.4 - uplot: 1.6.23 + uplot: 1.6.24 xss: 1.0.14 peerDependencies: react: ^16.8.0 || ^17.0.0 @@ -5170,7 +5170,7 @@ __metadata: tinycolor2: 1.4.2 tslib: 2.4.1 typescript: 4.8.4 - uplot: 1.6.23 + uplot: 1.6.24 uuid: 9.0.0 webpack: 5.74.0 peerDependencies: @@ -21945,7 +21945,7 @@ __metadata: ts-node: 10.9.1 tslib: 2.4.1 typescript: 4.8.4 - uplot: 1.6.23 + uplot: 1.6.24 uuid: 9.0.0 vendor: "link:./public/vendor" visjs-network: 4.25.0 @@ -37952,10 +37952,10 @@ __metadata: languageName: node linkType: hard -"uplot@npm:1.6.23": - version: 1.6.23 - resolution: "uplot@npm:1.6.23" - checksum: 4fd2b6340b09f8cbff5c136238962c4e31621267a17c321e0183d021c72338973ba16fd943858248983edc4cf9307e352f99e806b6ffb5eaff7a132adaff2bff +"uplot@npm:1.6.24": + version: 1.6.24 + resolution: "uplot@npm:1.6.24" + checksum: 253e389dc6db40e1231d1c63913dea1e6987856cfde985da036e70786ef0077afc40f3ed6883c54d95a97ecedc3ea52a7fa815e88f3c3d381b61c15e745f8a40 languageName: node linkType: hard From 6706f08ecd07dab0c9889a2a0d9aad9409ff92ed Mon Sep 17 00:00:00 2001 From: Jack Baldry Date: Fri, 27 Jan 2023 08:27:36 +0000 Subject: [PATCH 007/117] Replace grafana/docs-squad with more specific group grafana/docs-grafana (#62174) --- .github/CODEOWNERS | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 97f26eee916..8a118e42a87 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -12,26 +12,26 @@ # This should make it easy to add new rules without breaking existing ones. # Documentation -/docs/ @grafana/docs-squad -/contribute/ @grafana/docs-squad -/docs/sources/developers/plugins/ @grafana/docs-squad @grafana/plugins-platform-frontend @grafana/plugins-platform-backend -/docs/sources/developers/plugins/backend/ @grafana/docs-squad @grafana/plugins-platform-backend -/.changelog-archive @grafana/docs-squad -CHANGELOG.md @grafana/docs-squad -CODE_OF_CONDUCT.md @grafana/docs-squad -CONTRIBUTING.md @grafana/docs-squad +/docs/ @grafana/docs-grafana +/contribute/ @grafana/docs-grafana +/docs/sources/developers/plugins/ @grafana/docs-grafana @grafana/plugins-platform-frontend @grafana/plugins-platform-backend +/docs/sources/developers/plugins/backend/ @grafana/docs-grafana @grafana/plugins-platform-backend +/.changelog-archive @grafana/docs-grafana +CHANGELOG.md @grafana/docs-grafana +CODE_OF_CONDUCT.md @grafana/docs-grafana +CONTRIBUTING.md @grafana/docs-grafana GOVERNANCE.md @RichiH -HALL_OF_FAME.md @grafana/docs-squad +HALL_OF_FAME.md @grafana/docs-grafana ISSUE_TRIAGE.md @grafana/grafana-community-support LICENSE @torkelo LICENSING.md @torkelo MAINTAINERS.md @RichiH NOTICE.md @torkelo -README.md @grafana/docs-squad +README.md @grafana/docs-grafana ROADMAP.md @torkelo SECURITY.md @grafana/security-team SUPPORT.md @torkelo -UPGRADING_DEPENDENCIES.md @grafana/docs-squad +UPGRADING_DEPENDENCIES.md @grafana/docs-grafana WORKFLOW.md @torkelo @@ -575,7 +575,7 @@ embed.go @grafana/grafana-as-code /.github/workflows/codeql-analysis.yml @DanCech /.github/workflows/commands.yml @torkelo /.github/workflows/detect-breaking-changes-* @grafana/plugins-platform-frontend -/.github/workflows/doc-validator.yml @grafana/docs-squad +/.github/workflows/doc-validator.yml @grafana/docs-grafana /.github/workflows/enterprise-pr-check.yml @grafana/grafana-release-eng /.github/workflows/epic-add-to-platform-ux-parent-project.yml @meanmina /.github/workflows/github-release.yml @torkelo @@ -589,8 +589,8 @@ embed.go @grafana/grafana-as-code /.github/workflows/pr-codeql-analysis-python.yml @DanCech /.github/workflows/pr-commands-closed.yml @tolzhabayev /.github/workflows/pr-commands.yml @marefr -/.github/workflows/publish-technical-documentation-next.yml @grafana/docs-squad -/.github/workflows/publish-technical-documentation-release.yml @grafana/docs-squad +/.github/workflows/publish-technical-documentation-next.yml @grafana/docs-grafana +/.github/workflows/publish-technical-documentation-release.yml @grafana/docs-grafana /.github/workflows/remove-milestone.yml @grafana/user-essentials /.github/workflows/sbom-report.yml @grafana/security-team /.github/workflows/scripts/json-file-to-job-output.js @grafana/plugins-platform-frontend @@ -610,6 +610,3 @@ embed.go @grafana/grafana-as-code /conf/provisioning/datasources/ @grafana/plugins-platform-backend /conf/provisioning/notifiers/ @bergquist /conf/provisioning/plugins/ @grafana/plugins-platform-backend - - - From 05bf2419528631e6dedfd34bf853f48b4bb3a552 Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Fri, 27 Jan 2023 03:46:21 -0500 Subject: [PATCH 008/117] Alerting: Update state manager to return StateTransitions when Delete or Reset (#62264) * update Delete and Reset methods to return state transitions this will be used by notifier code to decide whether alert needs to be sent or not. * update scheduler to provide reason to delete states and use transitions * update FromAlertsStateToStoppedAlert to accept StateTransition and filter by old state * fixup * fix tests --- pkg/services/ngalert/models/alert_rule.go | 2 + pkg/services/ngalert/schedule/compat.go | 13 +++-- pkg/services/ngalert/schedule/compat_test.go | 16 +++-- pkg/services/ngalert/schedule/schedule.go | 5 +- pkg/services/ngalert/state/manager.go | 58 +++++++++++-------- pkg/services/ngalert/state/manager_test.go | 61 +++++++++++++++----- 6 files changed, 104 insertions(+), 51 deletions(-) diff --git a/pkg/services/ngalert/models/alert_rule.go b/pkg/services/ngalert/models/alert_rule.go index 0a0cbb4607c..f7d58293b26 100644 --- a/pkg/services/ngalert/models/alert_rule.go +++ b/pkg/services/ngalert/models/alert_rule.go @@ -13,6 +13,7 @@ import ( "github.com/google/go-cmp/cmp/cmpopts" alertingModels "github.com/grafana/alerting/alerting/models" + "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/util/cmputil" ) @@ -111,6 +112,7 @@ const ( StateReasonError = "Error" StateReasonPaused = "Paused" StateReasonUpdated = "Updated" + StateReasonRuleDeleted = "RuleDeleted" ) var ( diff --git a/pkg/services/ngalert/schedule/compat.go b/pkg/services/ngalert/schedule/compat.go index 40590e8059c..a4754d72a22 100644 --- a/pkg/services/ngalert/schedule/compat.go +++ b/pkg/services/ngalert/schedule/compat.go @@ -15,6 +15,7 @@ import ( "github.com/prometheus/common/model" alertingModels "github.com/grafana/alerting/alerting/models" + apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/eval" ngModels "github.com/grafana/grafana/pkg/services/ngalert/models" @@ -152,16 +153,16 @@ func FromStateTransitionToPostableAlerts(firingStates []state.StateTransition, s return alerts } -// FromAlertsStateToStoppedAlert converts firingStates that have evaluation state either eval.Alerting or eval.NoData or eval.Error to models.PostableAlert that are accepted by notifiers. -// Returns a list of alert instances that have expiration time.Now -func FromAlertsStateToStoppedAlert(firingStates []*state.State, appURL *url.URL, clock clock.Clock) apimodels.PostableAlerts { +// FromAlertsStateToStoppedAlert selects only transitions from firing states (states eval.Alerting, eval.NoData, eval.Error) +// and converts them to models.PostableAlert with EndsAt set to time.Now +func FromAlertsStateToStoppedAlert(firingStates []state.StateTransition, appURL *url.URL, clock clock.Clock) apimodels.PostableAlerts { alerts := apimodels.PostableAlerts{PostableAlerts: make([]models.PostableAlert, 0, len(firingStates))} ts := clock.Now() - for _, alertState := range firingStates { - if alertState.State == eval.Normal || alertState.State == eval.Pending { + for _, transition := range firingStates { + if transition.PreviousState == eval.Normal || transition.PreviousState == eval.Pending { continue } - postableAlert := stateToPostableAlert(alertState, appURL) + postableAlert := stateToPostableAlert(transition.State, appURL) postableAlert.EndsAt = strfmt.DateTime(ts) alerts.PostableAlerts = append(alerts.PostableAlerts, *postableAlert) } diff --git a/pkg/services/ngalert/schedule/compat_test.go b/pkg/services/ngalert/schedule/compat_test.go index dc36ca27e1f..8443f9a4cc8 100644 --- a/pkg/services/ngalert/schedule/compat_test.go +++ b/pkg/services/ngalert/schedule/compat_test.go @@ -14,6 +14,7 @@ import ( "github.com/stretchr/testify/require" alertingModels "github.com/grafana/alerting/alerting/models" + "github.com/grafana/grafana/pkg/services/ngalert/eval" ngModels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/state" @@ -222,9 +223,14 @@ func Test_FromAlertsStateToStoppedAlert(t *testing.T) { } evalStates := [...]eval.State{eval.Normal, eval.Alerting, eval.Pending, eval.Error, eval.NoData} - states := make([]*state.State, 0, len(evalStates)) - for _, s := range evalStates { - states = append(states, randomState(s)) + states := make([]state.StateTransition, 0, len(evalStates)*len(evalStates)) + for _, to := range evalStates { + for _, from := range evalStates { + states = append(states, state.StateTransition{ + State: randomState(to), + PreviousState: from, + }) + } } clk := clock.NewMock() @@ -232,10 +238,10 @@ func Test_FromAlertsStateToStoppedAlert(t *testing.T) { expected := make([]models.PostableAlert, 0, len(states)) for _, s := range states { - if !(s.State == eval.Alerting || s.State == eval.Error || s.State == eval.NoData) { + if !(s.PreviousState == eval.Alerting || s.PreviousState == eval.Error || s.PreviousState == eval.NoData) { continue } - alert := stateToPostableAlert(s, appURL) + alert := stateToPostableAlert(s.State, appURL) alert.EndsAt = strfmt.DateTime(clk.Now()) expected = append(expected, *alert) } diff --git a/pkg/services/ngalert/schedule/schedule.go b/pkg/services/ngalert/schedule/schedule.go index f8353a23413..be4d3f43137 100644 --- a/pkg/services/ngalert/schedule/schedule.go +++ b/pkg/services/ngalert/schedule/schedule.go @@ -12,6 +12,7 @@ import ( "go.opentelemetry.io/otel/attribute" alertingModels "github.com/grafana/alerting/alerting/models" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/datasources" @@ -324,7 +325,7 @@ func (sch *schedule) ruleRoutine(grafanaCtx context.Context, key ngmodels.AlertR evalDuration := sch.metrics.EvalDuration.WithLabelValues(orgID) evalTotalFailures := sch.metrics.EvalFailures.WithLabelValues(orgID) - notify := func(states []*state.State) { + notify := func(states []state.StateTransition) { expiredAlerts := FromAlertsStateToStoppedAlert(states, sch.appURL, sch.clock) if len(expiredAlerts.PostableAlerts) > 0 { sch.alertsSender.Send(key, expiredAlerts) @@ -508,7 +509,7 @@ func (sch *schedule) ruleRoutine(grafanaCtx context.Context, key ngmodels.AlertR // cases. ctx, cancelFunc := context.WithTimeout(context.Background(), time.Minute) defer cancelFunc() - states := sch.stateManager.DeleteStateByRuleUID(ngmodels.WithRuleKey(ctx, key), key) + states := sch.stateManager.DeleteStateByRuleUID(ngmodels.WithRuleKey(ctx, key), key, ngmodels.StateReasonRuleDeleted) notify(states) } logger.Debug("Stopping alert rule routine") diff --git a/pkg/services/ngalert/state/manager.go b/pkg/services/ngalert/state/manager.go index 9d222fda1f2..76e65c06434 100644 --- a/pkg/services/ngalert/state/manager.go +++ b/pkg/services/ngalert/state/manager.go @@ -7,6 +7,7 @@ import ( "github.com/benbjohnson/clock" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/metrics" @@ -167,14 +168,38 @@ func (st *Manager) Get(orgID int64, alertRuleUID, stateId string) *State { // DeleteStateByRuleUID removes the rule instances from cache and instanceStore. A closed channel is returned to be able // to gracefully handle the clear state step in scheduler in case we do not need to use the historian to save state // history. -func (st *Manager) DeleteStateByRuleUID(ctx context.Context, ruleKey ngModels.AlertRuleKey) []*State { - logger := st.log.New(ruleKey.LogContext()...) +func (st *Manager) DeleteStateByRuleUID(ctx context.Context, ruleKey ngModels.AlertRuleKey, reason string) []StateTransition { + logger := st.log.FromContext(ctx) logger.Debug("Resetting state of the rule") states := st.cache.removeByRuleUID(ruleKey.OrgID, ruleKey.UID) + if len(states) == 0 { - return states + return nil } + + now := st.clock.Now() + transitions := make([]StateTransition, 0, len(states)) + for _, s := range states { + oldState := s.State + oldReason := s.StateReason + startsAt := s.StartsAt + if s.State != eval.Normal { + startsAt = now + } + s.SetNormal(reason, startsAt, now) + // Set Resolved property so the scheduler knows to send a postable alert + // to Alertmanager. + s.Resolved = oldState == eval.Alerting + s.LastEvaluationTime = now + s.Values = map[string]float64{} + transitions = append(transitions, StateTransition{ + State: s, + PreviousState: oldState, + PreviousStateReason: oldReason, + }) + } + if st.instanceStore != nil { err := st.instanceStore.DeleteAlertInstancesByRule(ctx, ruleKey) if err != nil { @@ -183,32 +208,17 @@ func (st *Manager) DeleteStateByRuleUID(ctx context.Context, ruleKey ngModels.Al } logger.Info("Rules state was reset", "states", len(states)) - return states + return transitions } // ResetStateByRuleUID removes the rule instances from cache and instanceStore and saves state history. If the state // history has to be saved, rule must not be nil. -func (st *Manager) ResetStateByRuleUID(ctx context.Context, rule *ngModels.AlertRule, reason string) []*State { +func (st *Manager) ResetStateByRuleUID(ctx context.Context, rule *ngModels.AlertRule, reason string) []StateTransition { ruleKey := rule.GetKey() - states := st.DeleteStateByRuleUID(ctx, ruleKey) + transitions := st.DeleteStateByRuleUID(ctx, ruleKey, reason) - if rule == nil || st.historian == nil { - return states - } - transitions := make([]StateTransition, 0, len(states)) - for _, s := range states { - oldState := s.State - oldReason := s.StateReason - state := *s - now := time.Now() - state.SetNormal(reason, s.StartsAt, now) - state.LastEvaluationTime = now - state.Values = map[string]float64{} - transitions = append(transitions, StateTransition{ - State: &state, - PreviousState: oldState, - PreviousStateReason: oldReason, - }) + if rule == nil || st.historian == nil || len(transitions) == 0 { + return transitions } ruleMeta := history_model.NewRuleMeta(rule, st.log) @@ -219,7 +229,7 @@ func (st *Manager) ResetStateByRuleUID(ctx context.Context, rule *ngModels.Alert st.log.FromContext(ctx).Error("Error updating historian state reset transitions", append(ruleKey.LogContext(), "reason", reason, "error", err)...) } }() - return states + return transitions } // ProcessEvalResults updates the current states that belong to a rule with the evaluation results. diff --git a/pkg/services/ngalert/state/manager_test.go b/pkg/services/ngalert/state/manager_test.go index 047c861ca80..48fc6998f23 100644 --- a/pkg/services/ngalert/state/manager_test.go +++ b/pkg/services/ngalert/state/manager_test.go @@ -27,6 +27,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/state" "github.com/grafana/grafana/pkg/services/ngalert/state/historian" "github.com/grafana/grafana/pkg/services/ngalert/tests" + "github.com/grafana/grafana/pkg/util" ) var testMetrics = metrics.NewNGAlert(prometheus.NewPedanticRegistry()) @@ -2623,12 +2624,14 @@ func TestDeleteStateByRuleUID(t *testing.T) { for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { ctx := context.Background() + clk := clock.NewMock() + clk.Set(time.Now()) cfg := state.ManagerCfg{ Metrics: testMetrics.GetStateMetrics(), ExternalURL: nil, InstanceStore: dbstore, Images: &state.NoopImageService{}, - Clock: clock.New(), + Clock: clk, Historian: &state.FakeHistorian{}, } st := state.NewManager(cfg) @@ -2641,12 +2644,28 @@ func TestDeleteStateByRuleUID(t *testing.T) { assert.Equal(t, tc.startingStateCacheCount, len(existingStatesForRule)) assert.Equal(t, tc.startingInstanceDBCount, len(q.Result)) - states := st.DeleteStateByRuleUID(ctx, rule.GetKey()) + expectedReason := util.GenerateShortUID() + transitions := st.DeleteStateByRuleUID(ctx, rule.GetKey(), expectedReason) // Check that the deleted states are the same as the ones that were in cache - assert.Equal(t, tc.startingStateCacheCount, len(states)) - for _, s := range states { - assert.Equal(t, tc.expectedStates[s.CacheID], s) + assert.Equal(t, tc.startingStateCacheCount, len(transitions)) + for _, s := range transitions { + assert.Contains(t, tc.expectedStates, s.CacheID) + oldState := tc.expectedStates[s.CacheID] + assert.Equal(t, oldState.State, s.PreviousState) + assert.Equal(t, oldState.StateReason, s.PreviousStateReason) + assert.Equal(t, eval.Normal, s.State.State) + assert.Equal(t, expectedReason, s.StateReason) + if oldState.State == eval.Normal { + assert.Equal(t, oldState.StartsAt, s.StartsAt) + assert.False(t, s.Resolved) + } else { + assert.Equal(t, clk.Now(), s.StartsAt) + if oldState.State == eval.Alerting { + assert.True(t, s.Resolved) + } + } + assert.Equal(t, clk.Now(), s.EndsAt) } q = &models.ListAlertInstancesQuery{RuleOrgID: rule.OrgID, RuleUID: rule.UID} @@ -2742,12 +2761,14 @@ func TestResetStateByRuleUID(t *testing.T) { t.Run(tc.desc, func(t *testing.T) { ctx := context.Background() fakeHistorian := &state.FakeHistorian{StateTransitions: make([]state.StateTransition, 0)} + clk := clock.NewMock() + clk.Set(time.Now()) cfg := state.ManagerCfg{ Metrics: testMetrics.GetStateMetrics(), ExternalURL: nil, InstanceStore: dbstore, Images: &state.NoopImageService{}, - Clock: clock.New(), + Clock: clk, Historian: fakeHistorian, } st := state.NewManager(cfg) @@ -2760,20 +2781,32 @@ func TestResetStateByRuleUID(t *testing.T) { assert.Equal(t, tc.startingStateCacheCount, len(existingStatesForRule)) assert.Equal(t, tc.startingInstanceDBCount, len(q.Result)) - states := st.ResetStateByRuleUID(ctx, rule, models.StateReasonPaused) + transitions := st.ResetStateByRuleUID(ctx, rule, models.StateReasonPaused) // Check that the deleted states are the same as the ones that were in cache - assert.Equal(t, tc.startingStateCacheCount, len(states)) - for _, s := range states { - assert.Equal(t, tc.expectedStates[s.CacheID], s) + assert.Equal(t, tc.startingStateCacheCount, len(transitions)) + for _, s := range transitions { + assert.Contains(t, tc.expectedStates, s.CacheID) + oldState := tc.expectedStates[s.CacheID] + assert.Equal(t, oldState.State, s.PreviousState) + assert.Equal(t, oldState.StateReason, s.PreviousStateReason) + assert.Equal(t, eval.Normal, s.State.State) + assert.Equal(t, models.StateReasonPaused, s.StateReason) + if oldState.State == eval.Normal { + assert.Equal(t, oldState.StartsAt, s.StartsAt) + assert.False(t, s.Resolved) + } else { + assert.Equal(t, clk.Now(), s.StartsAt) + if oldState.State == eval.Alerting { + assert.True(t, s.Resolved) + } + } + assert.Equal(t, clk.Now(), s.EndsAt) } // Check if both entries have been added to the historian assert.Equal(t, tc.newHistorianEntriesCount, len(fakeHistorian.StateTransitions)) - for _, str := range fakeHistorian.StateTransitions { - assert.Equal(t, tc.expectedStates[str.State.CacheID].State, str.PreviousState) - assert.Equal(t, tc.expectedStates[str.State.CacheID].StateReason, str.PreviousStateReason) - } + assert.Equal(t, transitions, fakeHistorian.StateTransitions) q = &models.ListAlertInstancesQuery{RuleOrgID: rule.OrgID, RuleUID: rule.UID} _ = dbstore.ListAlertInstances(ctx, q) From f98ad926acb49ab718aa1c0daf7de426b5da925f Mon Sep 17 00:00:00 2001 From: Jack Baldry Date: Fri, 27 Jan 2023 09:14:24 +0000 Subject: [PATCH 009/117] Add Grafana tutorials originally from tutorials repository (#62124) * Add Grafana tutorials originally from tutorials repository Signed-off-by: Jack Baldry * Replace tutorials/step shortcode with ordinary headings Signed-off-by: Jack Baldry * Fix typos reported by codespell Signed-off-by: Jack Baldry * Fix doc-validator linting and run prettier Signed-off-by: Jack Baldry Signed-off-by: Jack Baldry --- .../sources/shared/tutorials/create-plugin.md | 40 ++ .../shared/tutorials/plugin-anatomy.md | 29 ++ .../shared/tutorials/publish-your-plugin.md | 77 ++++ .../shared/tutorials/set-up-environment.md | 34 ++ docs/sources/tutorials/_index.md | 9 + .../index.md | 180 +++++++++ .../build-a-data-source-plugin/index.md | 372 ++++++++++++++++++ .../build-a-panel-plugin-with-d3/index.md | 235 +++++++++++ .../tutorials/build-a-panel-plugin/index.md | 259 ++++++++++++ .../index.md | 164 ++++++++ .../tutorials/build-an-app-plugin/index.md | 208 ++++++++++ .../create-alerts-from-flux-queries/index.md | 331 ++++++++++++++++ .../tutorials/create-users-and-teams/index.md | 236 +++++++++++ .../tutorials/grafana-fundamentals/index.md | 354 +++++++++++++++++ docs/sources/tutorials/iis/index.md | 146 +++++++ .../install-grafana-on-raspberry-pi/index.md | 147 +++++++ .../tutorials/integrate-hubot/index.md | 118 ++++++ .../index.md | 260 ++++++++++++ .../run-grafana-behind-a-proxy/index.md | 222 +++++++++++ .../index.md | 101 +++++ 20 files changed, 3522 insertions(+) create mode 100755 docs/sources/shared/tutorials/create-plugin.md create mode 100644 docs/sources/shared/tutorials/plugin-anatomy.md create mode 100644 docs/sources/shared/tutorials/publish-your-plugin.md create mode 100644 docs/sources/shared/tutorials/set-up-environment.md create mode 100644 docs/sources/tutorials/_index.md create mode 100644 docs/sources/tutorials/build-a-data-source-backend-plugin/index.md create mode 100644 docs/sources/tutorials/build-a-data-source-plugin/index.md create mode 100644 docs/sources/tutorials/build-a-panel-plugin-with-d3/index.md create mode 100644 docs/sources/tutorials/build-a-panel-plugin/index.md create mode 100644 docs/sources/tutorials/build-a-streaming-data-source-plugin/index.md create mode 100644 docs/sources/tutorials/build-an-app-plugin/index.md create mode 100644 docs/sources/tutorials/create-alerts-from-flux-queries/index.md create mode 100644 docs/sources/tutorials/create-users-and-teams/index.md create mode 100644 docs/sources/tutorials/grafana-fundamentals/index.md create mode 100644 docs/sources/tutorials/iis/index.md create mode 100644 docs/sources/tutorials/install-grafana-on-raspberry-pi/index.md create mode 100644 docs/sources/tutorials/integrate-hubot/index.md create mode 100644 docs/sources/tutorials/provision-dashboards-and-data-sources/index.md create mode 100644 docs/sources/tutorials/run-grafana-behind-a-proxy/index.md create mode 100644 docs/sources/tutorials/stream-metrics-from-telegraf-to-grafana/index.md diff --git a/docs/sources/shared/tutorials/create-plugin.md b/docs/sources/shared/tutorials/create-plugin.md new file mode 100755 index 00000000000..656bce3cf9e --- /dev/null +++ b/docs/sources/shared/tutorials/create-plugin.md @@ -0,0 +1,40 @@ +--- +title: Create Plugin +--- + +Tooling for modern web development can be tricky to wrap your head around. While you certainly can write your own webpack configuration, for this guide, you'll be using grafana create-plugin tool + +Grafana [create-plugin tool](https://www.npmjs.com/package/@grafana/create-plugin) is a CLI application that simplifies Grafana plugin development, so that you can focus on code. The tool scaffolds a starter plugin and all the required configuration for you. + +1. In the plugin directory, create a plugin from template using create-plugin: + + ``` + npx @grafana/create-plugin + ``` + +1. Change directory to your newly created plugin: + + ``` + cd my-plugin + ``` + +1. Install the dependencies: + + ``` + yarn install + ``` + +1. Build the plugin: + + ``` + yarn dev + ``` + +1. Restart the Grafana server for Grafana to discover your plugin. +1. Open Grafana and go to **Configuration** -> **Plugins**. Make sure that your plugin is there. + +By default, Grafana logs whenever it discovers a plugin: + +``` +INFO[01-01|12:00:00] Registering plugin logger=plugins name=my-plugin +``` diff --git a/docs/sources/shared/tutorials/plugin-anatomy.md b/docs/sources/shared/tutorials/plugin-anatomy.md new file mode 100644 index 00000000000..4d6e2c4ead0 --- /dev/null +++ b/docs/sources/shared/tutorials/plugin-anatomy.md @@ -0,0 +1,29 @@ +--- +title: Plugin Anatomy +--- + +Plugins come in different shapes and sizes. Before we dive deeper, let's look at some of the properties that are shared by all of them. + +Every plugin you create will require at least two files: `plugin.json` and `module.ts`. + +### plugin.json + +When Grafana starts, it scans the plugin directory for any subdirectory that contains a `plugin.json` file. The `plugin.json` file contains information about your plugin, and tells Grafana about what capabilities and dependencies your plugin needs. + +While certain plugin types can have specific configuration options, let's look at the mandatory ones: + +- `type` tells Grafana what type of plugin to expect. Grafana supports three types of plugins: `panel`, `datasource`, and `app`. +- `name` is what users will see in the list of plugins. If you're creating a data source, this is typically the name of the database it connects to, such as Prometheus, PostgreSQL, or Stackdriver. +- `id` uniquely identifies your plugin, and should start with your Grafana username, to avoid clashing with other plugins. [Sign up for a Grafana account](/signup/) to claim your username. + +To see all the available configuration settings for the `plugin.json`, refer to the [plugin.json Schema](/docs/grafana/latest/plugins/developing/plugin.json/). + +### module.ts + +After discovering your plugin, Grafana loads the `module.ts` file, the entrypoint for your plugin. `module.ts` exposes the implementation of your plugin, which depends on the type of plugin you're building. + +Specifically, `module.ts` needs to expose an object that extends [GrafanaPlugin](https://github.com/grafana/grafana/blob/08bf2a54523526a7f59f7c6a8dafaace79ab87db/packages/grafana-data/src/types/plugin.ts#L124), and can be any of the following: + +- [PanelPlugin](https://github.com/grafana/grafana/blob/08bf2a54523526a7f59f7c6a8dafaace79ab87db/packages/grafana-data/src/types/panel.ts#L73) +- [DataSourcePlugin](https://github.com/grafana/grafana/blob/08bf2a54523526a7f59f7c6a8dafaace79ab87db/packages/grafana-data/src/types/datasource.ts#L33) +- [AppPlugin](https://github.com/grafana/grafana/blob/45b7de1910819ad0faa7a8aeac2481e675870ad9/packages/grafana-data/src/types/app.ts#L27) diff --git a/docs/sources/shared/tutorials/publish-your-plugin.md b/docs/sources/shared/tutorials/publish-your-plugin.md new file mode 100644 index 00000000000..097f2417608 --- /dev/null +++ b/docs/sources/shared/tutorials/publish-your-plugin.md @@ -0,0 +1,77 @@ +--- +title: Package your plugin +--- + +Once you're happy with your plugin, it's time to package it, and submit to the plugin repository. + +For users to be able to use the plugin without building it themselves, you need to make a production build of the plugin, and commit to a release branch in your repository. + +To submit a plugin to the plugin repository, you need to create a release of your plugin. While we recommend following the branching strategy outlined below, you're free to use one that makes more sense to you. + +#### Create a plugin release + +Let's create version 0.1.0 of our plugin. + +1. Create a branch called `release-0.1.x`. + + ``` + git checkout -b release-0.1.x + ``` + +1. Do a production build. + + ``` + yarn build + ``` + +1. Add the `dist` directory. + + ``` + git add -f dist + ``` + +1. Create the release commit. + + ``` + git commit -m "Release v0.1.0" + ``` + +1. Create a release tag. + + ``` + git tag -a v0.1.0 -m "Create release tag v0.1.0" + ``` + +1. Push to GitHub. `follow-tags` tells Git to push the release tag along with our release branch. + ``` + git push --set-upstream origin release-0.1.x --follow-tags + ``` + +#### Submit the plugin + +For a plugin to be published on [Grafana Plugins](/grafana/plugins/), it needs to be added to the [grafana-plugin-repository](https://github.com/grafana/grafana-plugin-repository). + +1. Fork the [grafana-plugin-repository](https://github.com/grafana/grafana-plugin-repository) + +1. Add your plugin to the `repo.json` file in the project root directory: + + ```json + { + "id": "", + "type": "", + "url": "https://github.com//my-plugin", + "versions": [ + { + "version": "", + "commit": "", + "url": "https://github.com//my-plugin" + } + ] + } + ``` + +1. [Create a pull request](https://github.com/grafana/grafana-plugin-repository/pull/new/master). + +Once your plugin has been accepted, it'll be published on [Grafana Plugin](/grafana/plugins/), available for anyone to [install](/docs/grafana/latest/plugins/installation/)! + +> We're auditing every plugin that's added to make sure it's ready to be published. This means that it might take some time before your plugin is accepted. We're working on adding more automated tests to improve this process. diff --git a/docs/sources/shared/tutorials/set-up-environment.md b/docs/sources/shared/tutorials/set-up-environment.md new file mode 100644 index 00000000000..07eb372727a --- /dev/null +++ b/docs/sources/shared/tutorials/set-up-environment.md @@ -0,0 +1,34 @@ +--- +title: Set up Environment +--- + +Before you can get started building plugins, you need to set up your environment for plugin development. + +To discover plugins, Grafana scans a _plugin directory_, the location of which depends on your operating system. + +1. Create a directory called `grafana-plugins` in your preferred workspace. + +1. Find the `plugins` property in the Grafana configuration file and set the `plugins` property to the path of your `grafana-plugins` directory. Refer to the [Grafana configuration documentation](/docs/grafana/latest/installation/configuration/#plugins) for more information. + + ```ini + [paths] + plugins = "/path/to/grafana-plugins" + ``` + +1. Restart Grafana if it's already running, to load the new configuration. + +### Alternative method: Docker + +If you don't want to install Grafana on your local machine, you can use [Docker](https://www.docker.com). + +To set up Grafana for plugin development using Docker, run the following command: + +``` +docker run -d -p 3000:3000 -v "$(pwd)"/grafana-plugins:/var/lib/grafana/plugins --name=grafana grafana/grafana:7.0.0 +``` + +Since Grafana only loads plugins on start-up, you need to restart the container whenever you add or remove a plugin. + +``` +docker restart grafana +``` diff --git a/docs/sources/tutorials/_index.md b/docs/sources/tutorials/_index.md new file mode 100644 index 00000000000..cfe6c7633df --- /dev/null +++ b/docs/sources/tutorials/_index.md @@ -0,0 +1,9 @@ +--- +title: 'Tutorials' +menuTitle: 'Tutorials' +description: 'Grafana tutorials' +--- + +# Tutorials + +{{< section >}} diff --git a/docs/sources/tutorials/build-a-data-source-backend-plugin/index.md b/docs/sources/tutorials/build-a-data-source-backend-plugin/index.md new file mode 100644 index 00000000000..9b58af7f84f --- /dev/null +++ b/docs/sources/tutorials/build-a-data-source-backend-plugin/index.md @@ -0,0 +1,180 @@ +--- +title: Build a data source backend plugin +summary: Create a backend for your data source plugin. +description: Create a backend for your data source plugin. +id: build-a-data-source-backend-plugin +categories: ['plugins'] +tags: ['beginner'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +weight: 75 +--- + +## Introduction + +Grafana supports a wide range of data sources, including Prometheus, MySQL, and even Datadog. There's a good chance you can already visualize metrics from the systems you have set up. In some cases, though, you already have an in-house metrics solution that you’d like to add to your Grafana dashboards. This tutorial teaches you to build a support for your data source. + +For more information about backend plugins, refer to the documentation on [Backend plugins](/docs/grafana/latest/developers/plugins/backend/). + +In this tutorial, you'll: + +- Build a backend for your data source +- Implement a health check for your data source +- Enable Grafana Alerting for your data source + +{{% class "prerequisite-section" %}} + +#### Prerequisites + +- Knowledge about how data sources are implemented in the frontend. +- Grafana 7.0 +- Go ([Version](https://github.com/grafana/plugin-tools/blob/main/packages/create-plugin/templates/backend/go.mod#L3)) +- [Mage](https://magefile.org/) +- NodeJS ([Version](https://github.com/grafana/plugin-tools/blob/main/packages/create-plugin/templates/common/package.json#L66)) +- yarn + {{% /class %}} + +## Set up your environment + +{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" >}} + +## Create a new plugin + +To build a backend for your data source plugin, Grafana requires a binary that it can execute when it loads the plugin during start-up. In this guide, we will build a binary using the [Grafana plugin SDK for Go](/docs/grafana/latest/developers/plugins/backend/grafana-plugin-sdk-for-go/). + +The easiest way to get started is to use the Grafana [create-plugin tool](https://www.npmjs.com/package/@grafana/create-plugin). Navigate to the plugin folder that you configured in step 1 and type: + +``` +npx @grafana/create-plugin +``` + +Follow the steps and select **datasource** as your plugin type and answer **yes** when prompted to create a backend for your plugin. + +```bash +cd my-plugin +``` + +Install frontend dependencies and build frontend parts of the plugin to _dist_ directory: + +```bash +yarn install +yarn build +``` + +Run the following to update [Grafana plugin SDK for Go](/docs/grafana/latest/developers/plugins/backend/grafana-plugin-sdk-for-go/) dependency to the latest minor version: + +```bash +go get -u github.com/grafana/grafana-plugin-sdk-go +go mod tidy +``` + +Build backend plugin binaries for Linux, Windows and Darwin to _dist_ directory: + +```bash +mage -v +``` + +Now, let's verify that the plugin you've built so far can be used in Grafana when creating a new data source: + +1. Restart your Grafana instance. +1. Open Grafana in your web browser. +1. Navigate via the side-menu to **Configuration** -> **Data Sources**. +1. Click **Add data source**. +1. Find your newly created plugin and select it. +1. Enter a name and then click **Save & Test** (ignore any errors reported for now). + +You now have a new data source instance of your plugin that is ready to use in a dashboard: + +1. Navigate via the side-menu to **Create** -> **Dashboard**. +1. Click **Add new panel**. +1. In the query tab, select the data source you just created. +1. A line graph is rendered with one series consisting of two data points. +1. Save the dashboard. + +### Troubleshooting + +#### Grafana doesn't load my plugin + +By default, Grafana requires backend plugins to be signed. To load unsigned backend plugins, you need to +configure Grafana to [allow unsigned plugins](/docs/grafana/latest/plugins/plugin-signature-verification/#allow-unsigned-plugins). +For more information, refer to [Plugin signature verification](/docs/grafana/latest/plugins/plugin-signature-verification/#backend-plugins). + +## Anatomy of a backend plugin + +The folders and files used to build the backend for the data source are: + +| file/folder | description | +| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Magefile.go` | It’s not a requirement to use mage build files, but we strongly recommend using it so that you can use the build targets provided by the plugin SDK. | +| `/go.mod ` | Go modules dependencies, [reference](https://golang.org/cmd/go/#hdr-The_go_mod_file) | +| `/src/plugin.json` | A JSON file describing the backend plugin | +| `/pkg/main.go` | Starting point of the plugin binary. | + +#### plugin.json + +The [plugin.json](/docs/grafana/latest/developers/plugins/metadata/) file is required for all plugins. When building a backend plugin these properties are important: + +| property | description | +| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| backend | Should be set to `true` for backend plugins. This tells Grafana that it should start a binary when loading the plugin. | +| executable | This is the name of the executable that Grafana expects to start, see [plugin.json reference](/docs/grafana/latest/developers/plugins/metadata/) for details. | +| alerting | Should be set to `true` if your backend datasource supports alerting. | + +In the next step we will look at the query endpoint! + +## Implement data queries + +We begin by opening the file `/pkg/plugin/plugin.go`. In this file you will see the `SampleDatasource` struct which implements the [backend.QueryDataHandler](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/backend?tab=doc#QueryDataHandler) interface. The `QueryData` method on this struct is where the data fetching happens for a data source plugin. + +Each request contains multiple queries to reduce traffic between Grafana and plugins. So you need to loop over the slice of queries, process each query, and then return the results of all queries. + +In the tutorial we have extracted a method named `query` to take care of each query model. Since each plugin has their own unique query model, Grafana sends it to the backend plugin as JSON. Therefore the plugin needs to `Unmarshal` the query model into something easier to work with. + +As you can see the sample only returns static numbers. Try to extend the plugin to return other types of data. + +You can read more about how to [build data frames in our docs](/docs/grafana/latest/developers/plugins/data-frames/). + +## Add support for health checks + +Implementing the health check handler allows Grafana to verify that a data source has been configured correctly. + +When editing a data source in Grafana's UI, you can **Save & Test** to verify that it works as expected. + +In this sample data source, there is a 50% chance that the health check will be successful. Make sure to return appropriate error messages to the users, informing them about what is misconfigured in the data source. + +Open `/pkg/plugin/plugin.go`. In this file you'll see that the `SampleDatasource` struct also implements the [backend.CheckHealthHandler](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/backend?tab=doc#CheckHealthHandler) interface. Navigate to the `CheckHealth` method to see how the health check for this sample plugin is implemented. + +## Enable Grafana Alerting + +1. Open _src/plugin.json_. +1. Add the top level `backend` property with a value of `true` to specify that your plugin supports Grafana Alerting, e.g. + ```json + { + ... + "backend": true, + "executable": "gpx_simple_datasource_backend", + "alerting": true, + "info": { + ... + } + ``` +1. Rebuild frontend parts of the plugin to _dist_ directory: + +```bash +yarn build +``` + +1. Restart your Grafana instance. +1. Open Grafana in your web browser. +1. Open the dashboard you created earlier in the _Create a new plugin_ step. +1. Edit the existing panel. +1. Click on the _Alert_ tab. +1. Click on _Create Alert_ button. +1. Edit condition and specify _IS ABOVE 10_. Change _Evaluate every_ to _10s_ and clear the _For_ field to make the alert rule evaluate quickly. +1. Save the dashboard. +1. After some time the alert rule evaluates and transitions into _Alerting_ state. + +## Summary + +In this tutorial you created a backend for your data source plugin. diff --git a/docs/sources/tutorials/build-a-data-source-plugin/index.md b/docs/sources/tutorials/build-a-data-source-plugin/index.md new file mode 100644 index 00000000000..ce1b81c6e7c --- /dev/null +++ b/docs/sources/tutorials/build-a-data-source-plugin/index.md @@ -0,0 +1,372 @@ +--- +title: Build a data source plugin +summary: Create a plugin to add support for your own data sources. +description: Create a plugin to add support for your own data sources. +id: build-a-data-source-plugin +categories: ['plugins'] +tags: ['beginner'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +weight: 70 +--- + +## Introduction + +Grafana supports a wide range of data sources, including Prometheus, MySQL, and even Datadog. There's a good chance you can already visualize metrics from the systems you have set up. In some cases, though, you already have an in-house metrics solution that you’d like to add to your Grafana dashboards. This tutorial teaches you to build a support for your data source. + +In this tutorial, you'll: + +- Build a data source to visualize a sine wave +- Construct queries using the query editor +- Configure your data source using the config editor + +{{% class "prerequisite-section" %}} + +### Prerequisites + +- Grafana >=7.0 +- NodeJS >=14 +- yarn + {{% /class %}} + +## Set up your environment + +{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" >}} + +## Create a new plugin + +{{< docs/shared lookup="tutorials/create-plugin.md" source="grafana" >}} + +## Anatomy of a plugin + +{{< docs/shared lookup="tutorials/plugin-anatomy.md" source="grafana" >}} + +## Data source plugins + +A data source in Grafana must extend the `DataSourceApi` interface, which requires you to defines two methods: `query` and `testDatasource`. + +### The `query` method + +The `query` method is the heart of any data source plugin. It accepts a query from the user, retrieves the data from an external database, and returns the data in a format that Grafana recognizes. + +``` +async query(options: DataQueryRequest): Promise +``` + +The `options` object contains the queries, or _targets_, that the user made, along with context information, like the current time interval. Use this information to query an external database. + +> The term _target_ originates from Graphite, and the earlier days of Grafana when Graphite was the only supported data source. As Grafana gained support for more data sources, the term "target" became synonymous with any type of query. + +### Test your data source + +`testDatasource` implements a health check for your data source. For example, Grafana calls this method whenever the user clicks the **Save & Test** button, after changing the connection settings. + +``` +async testDatasource() +``` + +## Data frames + +Nowadays there are countless of different databases, each with their own ways of querying data. To be able to support all the different data formats, Grafana consolidates the data into a unified data structure called _data frames_. + +Let's see how to create and return a data frame from the `query` method. In this step, you'll change the code in the starter plugin to return a [sine wave](https://en.wikipedia.org/wiki/Sine_wave). + +1. In the current `query` method, remove the code inside the `map` function. + + The `query` method now look like this: + + ```ts + async query(options: DataQueryRequest): Promise { + const { range } = options; + const from = range!.from.valueOf(); + const to = range!.to.valueOf(); + + const data = options.targets.map(target => { + // Your code goes here. + }); + + return { data }; + } + ``` + +1. In the `map` function, use the `lodash/defaults` package to set default values for query properties that haven't been set: + + ```ts + const query = defaults(target, defaultQuery); + ``` + +1. Create a data frame with a time field and a number field: + + ```ts + const frame = new MutableDataFrame({ + refId: query.refId, + fields: [ + { name: 'time', type: FieldType.time }, + { name: 'value', type: FieldType.number }, + ], + }); + ``` + + `refId` needs to be set to tell Grafana which query that generated this date frame. + +Next, we'll add the actual values to the data frame. Don't worry about the math used to calculate the values. + +1. Create a couple of helper variables: + + ```ts + // duration of the time range, in milliseconds. + const duration = to - from; + + // step determines how close in time (ms) the points will be to each other. + const step = duration / 1000; + ``` + +1. Add the values to the data frame: + + ```ts + for (let t = 0; t < duration; t += step) { + frame.add({ time: from + t, value: Math.sin((2 * Math.PI * t) / duration) }); + } + ``` + + The `frame.add()` accepts an object where the keys corresponds to the name of each field in the data frame. + +1. Return the data frame: + + ```ts + return frame; + ``` + +1. Rebuild the plugin and try it out. + +Your data source is now sending data frames that Grafana can visualize. Next, we'll look at how you can control the frequency of the sine wave by defining a _query_. + +> In this example, we're generating timestamps from the current time range. This means that you'll get the same graph no matter what time range you're using. In practice, you'd instead use the timestamps returned by your database. + +## Define a query + +Most data sources offer a way to query specific data. MySQL and PostgreSQL use SQL, while Prometheus has its own query language, called _PromQL_. No matter what query language your databases are using, Grafana lets you build support for it. + +Add support for custom queries to your data source, by implementing your own _query editor_, a React component that enables users to build their own queries, through a user-friendly graphical interface. + +A query editor can be as simple as a text field where the user edits the raw query text, or it can provide a more user-friendly form with drop-down menus and switches, that later gets converted into the raw query text before it gets sent off to the database. + +### Define the query model + +The first step in designing your query editor is to define its _query model_. The query model defines the user input to your data source. + +We want to be able to control the frequency of the sine wave, so let's add another property. + +1. Add a new number property called `frequency` to the query model: + + **src/types.ts** + + ```ts + export interface MyQuery extends DataQuery { + queryText?: string; + constant: number; + frequency: number; + } + ``` + +1. Set a default value to the new `frequency` property: + + ```ts + export const defaultQuery: Partial = { + constant: 6.5, + frequency: 1.0, + }; + ``` + +### Bind the model to a form + +Now that you've defined the query model you wish to support, the next step is to bind the model to a form. The `FormField` is a text field component from `grafana/ui` that lets you register a listener which will be invoked whenever the form field value changes. + +1. Add a new form field to the query editor to control the new frequency property. + + **QueryEditor.tsx** + + ```ts + const { queryText, constant, frequency } = query; + ``` + + ```ts + + ``` + +1. Add a event listener for the new property. + + ```ts + onFrequencyChange = (event: ChangeEvent) => { + const { onChange, query, onRunQuery } = this.props; + onChange({ ...query, frequency: parseFloat(event.target.value) }); + // executes the query + onRunQuery(); + }; + ``` + + The registered listener, `onFrequencyChange`, calls `onChange` to update the current query with the value from the form field. + + `onRunQuery();` tells Grafana to run the query after each change. For fast queries, this is recommended to provide a more responsive experience. + +### Use the property + +The new query model is now ready to use in our `query` method. + +1. In the `query` method, use the `frequency` property to adjust our equation. + + ```ts + frame.add({ time: from + t, value: Math.sin((2 * Math.PI * query.frequency * t) / duration) }); + ``` + +## Configure your data source + +To access a specific data source, you often need to configure things like hostname, credentials, or authentication method. A _config editor_ lets your users configure your data source plugin to fit their needs. + +The config editor looks similar to the query editor, in that it defines a model and binds it to a form. + +Since we're not actually connecting to an external database in our sine wave example, we don't really need many options. To show you how you can add an option however, we're going to add the _wave resolution_ as an option. + +The resolution controls how close in time the data points are to each other. A higher resolution means more points closer together, at the cost of more data being processed. + +### Define the options model + +1. Add a new number property called `resolution` to the options model. + + **types.ts** + + ```ts + export interface MyDataSourceOptions extends DataSourceJsonData { + path?: string; + resolution?: number; + } + ``` + +### Bind the model to a form + +Just like query editor, the form field in the config editor calls the registered listener whenever the value changes. + +1. Add a new form field to the query editor to control the new resolution option. + + **ConfigEditor.tsx** + + ```ts +
+ +
+ ``` + +1. Add a event listener for the new option. + + ```ts + onResolutionChange = (event: ChangeEvent) => { + const { onOptionsChange, options } = this.props; + const jsonData = { + ...options.jsonData, + resolution: parseFloat(event.target.value), + }; + onOptionsChange({ ...options, jsonData }); + }; + ``` + + The `onResolutionChange` listener calls `onOptionsChange` to update the current options with the value from the form field. + +### Use the option + +1. Create a property called `resolution` to the `DataSource` class. + + ```ts + export class DataSource extends DataSourceApi { + resolution: number; + + constructor(instanceSettings: DataSourceInstanceSettings) { + super(instanceSettings); + + this.resolution = instanceSettings.jsonData.resolution || 1000.0; + } + + // ... + ``` + +1. In the `query` method, use the `resolution` property to calculate the step size. + + **src/DataSource.ts** + + ```ts + const step = duration / this.resolution; + ``` + +## Get data from an external API + +So far, you've generated the data returned by the data source. A more realistic use case would be to fetch data from an external API. + +While you can use something like [axios](https://github.com/axios/axios) or the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to make requests, we recommend using the [`getBackendSrv`](/docs/grafana/latest/packages_api/runtime/getbackendsrv/) function from the [grafana/runtime](/docs/grafana/latest/packages_api/runtime/) package. + +The main advantage of `getBackendSrv` is that it proxies requests through the Grafana server rather making the request from the browser. This is strongly recommended when making authenticated requests to an external API. For more information on authenticating external requests, refer to [Add authentication for data source plugins](/docs/grafana/latest/developers/plugins/add-authentication-for-data-source-plugins/). + +1. Import `getBackendSrv`. + + **src/DataSource.ts** + + ```ts + import { getBackendSrv } from '@grafana/runtime'; + ``` + +1. Create a helper method `doRequest` and use the `datasourceRequest` method to make a request to your API. Replace `https://api.example.com/metrics` to point to your own API endpoint. + + ```ts + async doRequest(query: MyQuery) { + const result = await getBackendSrv().datasourceRequest({ + method: "GET", + url: "https://api.example.com/metrics", + params: query, + }) + + return result; + } + ``` + +1. Make a request for each query. `Promises.all` waits for all requests to finish before returning the data. + + ```ts + async query(options: DataQueryRequest): Promise { + const promises = options.targets.map((query) => + this.doRequest(query).then((response) => { + const frame = new MutableDataFrame({ + refId: query.refId, + fields: [ + { name: "Time", type: FieldType.time }, + { name: "Value", type: FieldType.number }, + ], + }); + + response.data.forEach((point: any) => { + frame.appendRow([point.time, point.value]); + }); + + return frame; + }) + ); + + return Promise.all(promises).then((data) => ({ data })); + } + ``` + +## Summary + +In this tutorial you built a complete data source plugin for Grafana that uses a query editor to control what data to visualize. You've added a data source option, commonly used to set connection options and more. + +### Learn more + +Learn how you can improve your plugin even further, by reading our advanced guides: + +- [Add support for variables](/docs/grafana/latest/developers/plugins/add-support-for-variables/) +- [Add support for annotations](/docs/grafana/latest/developers/plugins/add-support-for-annotations/) +- [Add support for Explore queries](/docs/grafana/latest/developers/plugins/add-support-for-explore-queries/) +- [Build a logs data source](/docs/grafana/latest/developers/plugins/build-a-logs-data-source-plugin/) diff --git a/docs/sources/tutorials/build-a-panel-plugin-with-d3/index.md b/docs/sources/tutorials/build-a-panel-plugin-with-d3/index.md new file mode 100644 index 00000000000..b5869acbbc0 --- /dev/null +++ b/docs/sources/tutorials/build-a-panel-plugin-with-d3/index.md @@ -0,0 +1,235 @@ +--- +title: Build a panel plugin with D3.js +summary: Learn how to use D3.js in your panel plugins. +description: how to use D3.js in your panel plugins. +id: build-a-panel-plugin-with-d3 +categories: ['plugins'] +tags: ['beginner'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +weight: 60 +--- + +## Introduction + +Panels are the building blocks of Grafana, and allow you to visualize data in different ways. This tutorial gives you a hands-on walkthrough of creating your own panel using [D3.js](https://d3js.org/). + +For more information about panels, refer to the documentation on [Panels](/docs/grafana/latest/features/panels/panels/). + +In this tutorial, you'll: + +- Build a simple panel plugin to visualize a bar chart. +- Learn how to use D3.js to build a panel using data-driven transformations. + +{{% class "prerequisite-section" %}} + +### Prerequisites + +- Grafana 7.0 +- NodeJS 12.x +- yarn + {{% /class %}} + +## Set up your environment + +{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" >}} + +## Create a new plugin + +{{< docs/shared lookup="tutorials/create-plugin.md" source="grafana" >}} + +## Data-driven documents + +[D3.js](https://d3js.org/) is a JavaScript library for manipulating documents based on data. It lets you transform arbitrary data into HTML, and is commonly used for creating visualizations. + +Wait a minute. Manipulating documents based on data? That's sounds an awful lot like React. In fact, much of what you can accomplish with D3 you can already do with React. So before we start looking at D3, let's see how you can create an SVG from data, using only React. + +In **SimplePanel.tsx**, change `SimplePanel` to return an `svg` with a `rect` element. + +```ts +export const SimplePanel: React.FC = ({ options, data, width, height }) => { + const theme = useTheme(); + + return ( + + + + ); +}; +``` + +One single rectangle might not be very exciting, so let's see how you can create rectangles from data. + +1. Create some data that we can visualize. + + ```ts + const values = [4, 8, 15, 16, 23, 42]; + ``` + +1. Calculate the height of each bar based on the height of the panel. + + ```ts + const barHeight = height / values.length; + ``` + +1. Inside a SVG group, `g`, create a `rect` element for every value in the dataset. Each rectangle uses the value as its width. + + ```ts + return ( + + + {values.map((value, i) => ( + + ))} + + + ); + ``` + +1. Rebuild the plugin and reload your browser to see the changes you've made. + +As you can see, React is perfectly capable of dynamically creating HTML elements. In fact, creating elements using React is often faster than creating them using D3. + +So why would you use even use D3? In the next step, we'll see how you can take advantage of D3's data transformations. + +## Transform data using D3.js + +In this step, you'll see how you can transform data using D3 before rendering it using React. + +D3 is already bundled with Grafana, and you can access it by importing the `d3` package. However, we're going to need the type definitions while developing. + +1. Install the D3 type definitions: + + ```bash + yarn add --dev @types/d3 + ``` + +1. Import `d3` in **SimplePanel.tsx**. + + ```ts + import * as d3 from 'd3'; + ``` + +In the previous step, we had to define the width of each bar in pixels. Instead, let's use _scales_ from the D3 library to make the width of each bar depend on the width of the panel. + +Scales are functions that map a range of values to another range of values. In this case, we want to map the values in our datasets to a position within our panel. + +1. Create a scale to map a value between 0 and the maximum value in the dataset, to a value between 0 and the width of the panel. We'll be using this to calculate the width of the bar. + + ```ts + const scale = d3 + .scaleLinear() + .domain([0, d3.max(values) || 0.0]) + .range([0, width]); + ``` + +1. Pass the value to the scale function to calculate the width of the bar in pixels. + + ```ts + return ( + + + {values.map((value, i) => ( + + ))} + + + ); + ``` + +As you can see, even if we're using React to render the actual elements, the D3 library contains useful tools that you can use to transform your data before rendering it. + +## Add an axis + +Another useful tool in the D3 toolbox is the ability to generate _axes_. Adding axes to our chart makes it easier for the user to understand the differences between each bar. + +Let's see how you can use D3 to add a horizontal axis to your bar chart. + +1. Create a D3 axis. Notice that by using the same scale as before, we make sure that the bar width aligns with the ticks on the axis. + + ```ts + const axis = d3.axisBottom(scale); + ``` + +1. Generate the axis. While D3 needs to generate the elements for the axis, we can encapsulate it by generating them within an anonymous function which we pass as a `ref` to a group element `g`. + + ```ts + { + d3.select(node).call(axis as any); + }} + /> + ``` + +By default, the axis renders at the top of the SVG element. We'd like to move it to the bottom, but to do that, we first need to make room for it by decreasing the height of each bar. + +1. Calculate the new bar height based on the padded height. + + ```ts + const padding = 20; + const chartHeight = height - padding; + const barHeight = chartHeight / values.length; + ``` + +1. Translate the axis by adding a transform to the `g` element. + + ```ts + { + d3.select(node).call(axis as any); + }} + /> + ``` + +Congrats! You've created a simple and responsive bar chart. + +## Complete example + +```ts +import React from 'react'; +import { PanelProps } from '@grafana/data'; +import { SimpleOptions } from 'types'; +import { useTheme } from '@grafana/ui'; +import * as d3 from 'd3'; + +interface Props extends PanelProps {} + +export const SimplePanel: React.FC = ({ options, data, width, height }) => { + const theme = useTheme(); + + const values = [4, 8, 15, 16, 23, 42]; + + const scale = d3 + .scaleLinear() + .domain([0, d3.max(values) || 0.0]) + .range([0, width]); + + const axis = d3.axisBottom(scale); + + const padding = 20; + const chartHeight = height - padding; + const barHeight = chartHeight / values.length; + + return ( + + + {values.map((value, i) => ( + + ))} + + { + d3.select(node).call(axis as any); + }} + /> + + ); +}; +``` + +## Summary + +In this tutorial you built a panel plugin with D3.js. diff --git a/docs/sources/tutorials/build-a-panel-plugin/index.md b/docs/sources/tutorials/build-a-panel-plugin/index.md new file mode 100644 index 00000000000..9ee28ce4943 --- /dev/null +++ b/docs/sources/tutorials/build-a-panel-plugin/index.md @@ -0,0 +1,259 @@ +--- +title: Build a panel plugin +summary: Learn how to create a custom visualization for your dashboards. +description: Learn how to create a custom visualization for your dashboards. +id: build-a-panel-plugin +categories: ['plugins'] +tags: ['beginner'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +weight: 50 +--- + +## Introduction + +Panels are the building blocks of Grafana. They allow you to visualize data in different ways. While Grafana has several types of panels already built-in, you can also build your own panel, to add support for other visualizations. + +For more information about panels, refer to the documentation on [Panels](/docs/grafana/latest/panels/). + +{{% class "prerequisite-section" %}} + +### Prerequisites + +- Grafana >=7.0 +- NodeJS >=14 +- yarn + {{% /class %}} + +## Set up your environment + +{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" >}} + +## Create a new plugin + +{{< docs/shared lookup="tutorials/create-plugin.md" source="grafana" >}} + +## Anatomy of a plugin + +{{< docs/shared lookup="tutorials/plugin-anatomy.md" source="grafana" >}} + +## Panel plugins + +Since Grafana 6.x, panels are [ReactJS components](https://reactjs.org/docs/components-and-props.html). + +Prior to Grafana 6.0, plugins were written in [AngularJS](https://angular.io/). Even though we still support plugins written in AngularJS, we highly recommend that you write new plugins using ReactJS. + +### Panel properties + +The [PanelProps](https://github.com/grafana/grafana/blob/747b546c260f9a448e2cb56319f796d0301f4bb9/packages/grafana-data/src/types/panel.ts#L27-L40) interface exposes runtime information about the panel, such as panel dimensions, and the current time range. + +You can access the panel properties through `props`, as seen in your plugin. + +**src/SimplePanel.tsx** + +```js +const { options, data, width, height } = props; +``` + +### Development workflow + +Next, you'll learn the basic workflow of making a change to your panel, building it, and reloading Grafana to reflect the changes you made. + +First, you need to add your panel to a dashboard: + +1. Open Grafana in your browser. +1. Create a new dashboard, and add a new panel. +1. Select your panel from the list of visualization types. +1. Save the dashboard. + +Now that you can view your panel, try making a change to the panel plugin: + +1. In `SimplePanel.tsx`, change the fill color of the circle. +1. Run `yarn dev` to build the plugin. +1. In the browser, reload Grafana with the new changes. + +## Add panel options + +Sometimes you want to offer the users of your panel an option to configure the behavior of your plugin. By configuring _panel options_ for your plugin, your panel will be able to accept user input. + +In the previous step, you changed the fill color of the circle in the code. Let's change the code so that the plugin user can configure the color from the panel editor. + +#### Add an option + +Panel options are defined in a _panel options object_. `SimpleOptions` is an interface that describes the options object. + +1. In `types.ts`, add a `CircleColor` type to hold the colors the users can choose from: + + ``` + type CircleColor = 'red' | 'green' | 'blue'; + ``` + +1. In the `SimpleOptions` interface, add a new option called `color`: + + ``` + color: CircleColor; + ``` + +Here's the updated options definition: + +**src/types.ts** + +```ts +type SeriesSize = 'sm' | 'md' | 'lg'; +type CircleColor = 'red' | 'green' | 'blue'; + +// interface defining panel options type +export interface SimpleOptions { + text: string; + showSeriesCount: boolean; + seriesCountSize: SeriesSize; + color: CircleColor; +} +``` + +#### Add an option control + +To change the option from the panel editor, you need to bind the `color` option to an _option control_. + +Grafana supports a range of option controls, such as text inputs, switches, and radio groups. + +Let's create a radio control and bind it to the `color` option. + +1. In `src/module.ts`, add the control at the end of the builder: + + ```ts + .addRadio({ + path: 'color', + name: 'Circle color', + defaultValue: 'red', + settings: { + options: [ + { + value: 'red', + label: 'Red', + }, + { + value: 'green', + label: 'Green', + }, + { + value: 'blue', + label: 'Blue', + }, + ], + } + }); + ``` + + The `path` is used to bind the control to an option. You can bind a control to nested option by specifying the full path within a options object, for example `colors.background`. + +Grafana builds an options editor for you and displays it in the panel editor sidebar in the **Display** section. + +#### Use the new option + +You're almost done. You've added a new option and a corresponding control to change the value. But the plugin isn't using the option yet. Let's change that. + +1. To convert option value to the colors used by the current theme, add a `switch` statement right before the `return` statement in `SimplePanel.tsx`. + + **src/SimplePanel.tsx** + + ```ts + let color: string; + switch (options.color) { + case 'red': + color = theme.palette.redBase; + break; + case 'green': + color = theme.palette.greenBase; + break; + case 'blue': + color = theme.palette.blue95; + break; + } + ``` + +1. Configure the circle to use the color. + + ```ts + + + + ``` + +Now, when you change the color in the panel editor, the fill color of the circle changes as well. + +## Create dynamic panels using data frames + +Most panels visualize dynamic data from a Grafana data source. In this step, you'll create one circle per series, each with a radius equal to the last value in the series. + +> To use data from queries in your panel, you need to set up a data source. If you don't have one available, you can use the [TestData DB](/docs/grafana/latest/features/datasources/testdata) data source while developing. + +The results from a data source query within your panel are available in the `data` property inside your panel component. + +```ts +const { data } = props; +``` + +`data.series` contains the series returned from a data source query. Each series is represented as a data structure called _data frame_. A data frame resembles a table, where data is stored by columns, or _fields_, instead of rows. Every value in a field share the same data type, such as string, number, or time. + +Here's an example of a data frame with a time field, `Time`, and a number field, `Value`: + +| Time | Value | +| ------------- | ----- | +| 1589189388597 | 32.4 | +| 1589189406480 | 27.2 | +| 1589189513721 | 15.0 | + +Let's see how you can retrieve data from a data frame and use it in your visualization. + +1. Get the last value of each field of type `number`, by adding the following to `SimplePanel.tsx`, before the `return` statement: + + ```ts + const radii = data.series + .map((series) => series.fields.find((field) => field.type === 'number')) + .map((field) => field?.values.get(field.values.length - 1)); + ``` + + `radii` will contain the last values in each of the series that are returned from a data source query. You'll use these to set the radius for each circle. + +1. Change the `svg` element to the following: + + ```ts + + + {radii.map((radius, index) => { + const step = width / radii.length; + return ; + })} + + + ``` + + Note how we're creating a `` element for each value in `radii`: + + ```ts + { + radii.map((radius, index) => { + const step = width / radii.length; + return ; + }); + } + ``` + + We use the `transform` here to distribute the circle horizontally within the panel. + +1. Rebuild your plugin and try it out by adding multiple queries to the panel. Refresh the dashboard. + +If you want to know more about data frames, check out our introduction to [Data frames](/docs/grafana/latest/developers/plugins/data-frames/). + +## Summary + +In this tutorial you learned how to create a custom visualization for your dashboards. diff --git a/docs/sources/tutorials/build-a-streaming-data-source-plugin/index.md b/docs/sources/tutorials/build-a-streaming-data-source-plugin/index.md new file mode 100644 index 00000000000..5790542b0fc --- /dev/null +++ b/docs/sources/tutorials/build-a-streaming-data-source-plugin/index.md @@ -0,0 +1,164 @@ +--- +title: Build a streaming data source backend plugin +summary: Create a backend for your data source plugin with streaming capabilities. +description: Create a backend for your data source plugin with streaming capabilities. +id: build-a-streaming-data-source-backend-plugin +categories: ['plugins'] +tags: ['beginner'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +weight: 75 +--- + +## Introduction + +Grafana supports a wide range of data sources, including Prometheus, MySQL, and even Datadog. In previous tutorials we have shown how to extend Grafana capabilities to query custom data sources by [building a backend datasource plugin](/tutorials/build-a-data-source-backend-plugin/). In this tutorial we take a step further and add streaming capabilities to the backend datasource plugin. Streaming allows plugins to push data to Grafana panels as soon as data appears (without periodic polling from UI side). + +For more information about backend plugins, refer to the documentation on [Backend plugins](/docs/grafana/latest/developers/plugins/backend/). + +In this tutorial, you'll: + +- Extend a backend plugin with streaming capabilities + +{{% class "prerequisite-section" %}} + +#### Prerequisites + +- Knowledge about how data sources are implemented in the frontend. +- Knowledge about [backend datasource anatomy](/tutorials/build-a-data-source-backend-plugin/) +- Grafana 8.0+ +- Go ([Version](https://github.com/grafana/plugin-tools/blob/main/packages/create-plugin/templates/backend/go.mod#L3)) +- [Mage](https://magefile.org/) +- NodeJS ([Version](https://github.com/grafana/plugin-tools/blob/main/packages/create-plugin/templates/common/package.json#L66)) +- yarn + {{% /class %}} + +## Set up your environment + +{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" >}} + +## Create a new plugin + +To build a backend for your data source plugin, Grafana requires a binary that it can execute when it loads the plugin during start-up. In this guide, we will build a binary using the [Grafana plugin SDK for Go](/docs/grafana/latest/developers/plugins/backend/grafana-plugin-sdk-for-go/). + +The easiest way to get started is to use the Grafana [create-plugin tool](https://www.npmjs.com/package/@grafana/create-plugin). Navigate to the plugin folder that you configured in step 1 and type: + +``` +npx @grafana/create-plugin +``` + +Follow the steps and select **datasource** as your plugin type and answer **yes** when prompted to create a backend for your plugin. + +```bash +cd my-plugin +``` + +Install frontend dependencies and build frontend parts of the plugin to _dist_ directory: + +```bash +yarn install +yarn build +``` + +Run the following to update [Grafana plugin SDK for Go](/docs/grafana/latest/developers/plugins/backend/grafana-plugin-sdk-for-go/) dependency to the latest minor version: + +```bash +go get -u github.com/grafana/grafana-plugin-sdk-go +go mod tidy +``` + +Build backend plugin binaries for Linux, Windows and Darwin to _dist_ directory: + +```bash +mage -v +``` + +Now, let's verify that the plugin you've built can be used in Grafana when creating a new data source: + +1. Restart your Grafana instance. +1. Open Grafana in your web browser. +1. Navigate via the side-menu to **Configuration** -> **Data Sources**. +1. Click **Add data source**. +1. Find your newly created plugin and select it. +1. Enter a name and then click **Save & Test** (ignore any errors reported for now). + +You now have a new data source instance of your plugin that is ready to use in a dashboard. To confirm, follow these steps: + +1. Navigate via the side-menu to **Create** -> **Dashboard**. +1. Click **Add new panel**. +1. In the query tab, select the data source you just created. +1. A line graph is rendered with one series consisting of two data points. +1. Save the dashboard. + +### Troubleshooting + +#### Grafana doesn't load my plugin + +By default, Grafana requires backend plugins to be signed. To load unsigned backend plugins, you need to +configure Grafana to [allow unsigned plugins](/docs/grafana/latest/plugins/plugin-signature-verification/#allow-unsigned-plugins). +For more information, refer to [Plugin signature verification](/docs/grafana/latest/plugins/plugin-signature-verification/#backend-plugins). + +## Anatomy of a backend plugin + +As you may notice till this moment we did the same steps described in [build a backend datasource plugin tutorial](/tutorials/build-a-data-source-backend-plugin/). At this point, you should be familiar with backend plugin structure and a way how data querying and health check capabilities could be implemented. Let's take the next step and discuss how datasource plugin can handle data streaming. + +## Add streaming capabilities + +What we want to achieve here is to issue a query to load initial data from a datasource plugin and then switching to data streaming mode where the plugin will push data frames to Grafana time-series panel. + +In short – implementing a streaming plugin means implementing a `backend.StreamHandler` interface which contains `SubscribeStream`, `RunStream`, and `PublishStream` methods. + +`SubscribeStream` is a method where the plugin has a chance to authorize user subscription requests to a channel. Users on the frontend side subscribe to different channels to consume real-time data. + +When returning a `data.Frame` with initial data we can return a special field `Channel` to let the frontend know that we are going to stream data frames after initial data load. When the frontend receives a frame with a `Channel` set it automatically issues a subscription request to that channel. + +Channel is a string identifier of topic to which clients can subscribe in Grafana Live. See a documentation of Grafana Live for [details about channel structure](/docs/grafana/latest/live/live-channel/). + +As said in docs in Grafana Live channel consists of 3 parts delimited by `/`: + +- Scope +- Namespace +- Path + +For datasource plugin channels Grafana uses `ds` scope. Namespace in the case of datasource channels is a datasource unique ID (UID) which is issued by Grafana at the moment of datasource creation. The path is a custom string that plugin authors free to choose themselves (just make sure it consists of allowed symbols). I.e. datasource channel looks like `ds//`. + +So to let the frontend know that we are going to stream data we set a `Channel` field into frame metadata inside `QueryData` implementation. In our tutorial it's a `ds//stream`. The frontend will issue a subscription request to this channel. + +Inside `SubscribeStream` implementation we check whether a user allowed to subscribe on a channel path. If yes – we return an OK status code to tell Grafana user can join a channel: + +```go +status := backend.SubscribeStreamStatusPermissionDenied +if req.Path == "stream" { + // Allow subscribing only on expected path. + status = backend.SubscribeStreamStatusOK +} +return &backend.SubscribeStreamResponse{ + Status: status, +}, nil +``` + +As soon as the first subscriber joins a channel Grafana opens a unidirectional stream to consume streaming frames from a plugin. To handle this and to push data towards clients we implement a `RunStream` method which provides a way to push JSON data into a channel. So we can push data frame like this (error handling skipped): + +```go +// Send frame to stream including both frame schema and data frame parts. +_ = sender.SendFrame(frame, data.IncludeAll) +``` + +Open example datasource query editor and make sure `With Streaming` toggle is on. After doing this you should see data displayed and then periodically updated by streaming frames coming periodically from `RunStream` method. + +The important thing to note is that Grafana opens a unidirectional stream only once per channel upon the first subscriber joined. Every other subscription request will be still authorized by `SubscribeStream` method but the new `RunStream` won't be issued. I.e. you can have many active subscribers but only one running stream. At this moment this guarantee works for a single Grafana instance, we are planning to support this for highly-available Grafana setup (many Grafana instances behind load-balancer) in future releases. + +The stream will be automatically closed as soon as all subscriber users left. + +For the tutorial use case, we only need to properly implement `SubscribeStream` and `RunStream` - we don't need to handle publications to a channel from users. But we still need to write `PublishStream` method to fully implement `backend.StreamHandler` interface. Inside `PublishStream` we just do not allow any publications from users since we are pushing data from a backend: + +```go +return &backend.PublishStreamResponse{ + Status: backend.PublishStreamStatusPermissionDenied, +}, nil +``` + +## Summary + +In this tutorial you created a backend for your data source plugin with streaming capabilities. diff --git a/docs/sources/tutorials/build-an-app-plugin/index.md b/docs/sources/tutorials/build-an-app-plugin/index.md new file mode 100644 index 00000000000..29936f02ab8 --- /dev/null +++ b/docs/sources/tutorials/build-an-app-plugin/index.md @@ -0,0 +1,208 @@ +--- +title: Build an app plugin +summary: Learn at how to create an app for Grafana. +description: Learn at how to create an app for Grafana. +id: build-an-app-plugin +categories: ['plugins'] +tags: ['beginner'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +weight: 50 +draft: true +--- + +## Introduction + +App plugins are Grafana plugins that can bundle data source and panel plugins within one package. They also let you create _custom pages_ within Grafana. Custom pages enable the plugin author to include things like documentation, sign-up forms, or to control other services over HTTP. + +Data source and panel plugins will show up like normal plugins. The app pages will be available in the main menu. + +{{% class "prerequisite-section" %}} + +### Prerequisites + +- Grafana 7.0 +- NodeJS 12.x +- yarn + {{% /class %}} + +## Set up your environment + +{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" >}} + +## Create a new plugin + +{{< docs/shared lookup="tutorials/create-plugin.md" source="grafana" >}} + +## Anatomy of a plugin + +{{< docs/shared lookup="tutorials/plugin-anatomy.md" source="grafana" >}} + +## App plugins + +App plugins let you bundle resources such as dashboards, panels, and data sources into a single plugin. + +Any resource you want to include needs to be added to the `includes` property in the `plugin.json` file. To add a resource to your app plugin, you need to include it to the `plugin.json`. + +Plugins that are included in an app plugin are available like any other plugin. + +Dashboards and pages can be added to the app menu by setting `addToNav` to `true`. + +By setting `"defaultNav": true`, users can navigate to the dashboard by clicking the app icon in the side menu. + +## Add a custom page + +App plugins let you extend the Grafana user interface through the use of _custom pages_. + +Any requests sent to `/a/`, e.g. `/a/myorgid-simple-app/`, are routed to the _root page_ of the app plugin. The root page is a React component that returns the content for a given route. + +While you're free to implement your own routing, in this tutorial you'll use a tab-based navigation page that you can use by calling `onNavChange`. + +Let's add a tab for managing server instances. + +1. In the `src/pages` directory, add a new file called `Instances.tsx`. This component contains the content for the new tab. + + ```ts + import { AppRootProps } from '@grafana/data'; + import React, { FC } from 'react'; + + export const Instances: FC = ({ query, path, meta }) => { + return

Hello

; + }; + ``` + +1. Register the page by adding it to the `pages` array in `src/pages/index.ts`. + + **index.ts** + + ```ts + import { Instances } from './Instances'; + ``` + + ```ts + { + component: Instances, + icon: 'file-alt', + id: 'instances', + text: 'Instances', + } + ``` + +1. Add the page to the app menu, by including it in `plugin.json`. This will be the main view of the app, so we'll set `defaultNav` to let users quickly get to it by clicking the app icon in the side menu. + + **plugin.json** + + ```json + "includes": [ + { + "type": "page", + "name": "Instances", + "path": "/a/myorgid-simple-app?tab=instances", + "role": "Viewer", + "addToNav": true, + "defaultNav": true + } + ] + ``` + +> **Note:** While `page` includes typically reference pages created by the app, you can set `path` to any URL, internal or external. Try setting `path` to `https://grafana.com`. + +## Configure the app + +Let's add a new configuration page where users are able to configure default zone and regions for any instances they create. + +1. In `module.ts`, add new configuration page using the `addConfigPage` method. `body` is the React component that renders the page content. + + **module.ts** + + ```ts + .addConfigPage({ + title: 'Defaults', + icon: 'fa fa-info', + body: DefaultsConfigPage, + id: 'defaults', + }) + ``` + +## Add a dashboard + +#### Include a dashboard in your app + +1. In `src/`, create a new directory called `dashboards`. +1. Create a file called `overview.json` in the `dashboards` directory. +1. Copy the JSON definition for the dashboard you want to include and paste it into `overview.json`. If you don't have one available, you can find a sample dashboard at the end of this step. +1. In `plugin.json`, add the following object to the `includes` property. + + - The `name` of the dashboard needs to be the same as the `title` in the dashboard JSON model. + - `path` points out the file that contains the dashboard definition, relative to the `plugin.json` file. + + ```json + "includes": [ + { + "type": "dashboard", + "name": "System overview", + "path": "dashboards/overview.json", + "addToNav": true + } + ] + ``` + +1. Save and restart Grafana to load the new changes. + +## Bundle a plugin + +An app plugin can contain panel and data source plugins that get installed along with the app plugin. + +In this step, you'll add a data source to your app plugin. You can add panel plugins the same way by changing `datasource` to `panel`. + +1. In `src/`, create a new directory called `datasources`. +1. Create a new data source using Grafana create-plugin tool in a temporary directory. + + ```bash + mkdir tmp + cd tmp + npx @grafana/create-plugin + ``` + +1. Move the `src` directory in the data source plugin to `src/datasources`, and rename it to `my-datasource`. + + ```bash + mv ./my-datasource/src ../src/datasources/my-datasource + ``` + +Any bundled plugins are built along with the app plugin. Grafana looks for any subdirectory containing a `plugin.json` file and attempts to load a plugin in that directory. + +To let users know that your plugin bundles other plugins, you can optionally display it on the plugin configuration page. This is not done automatically, so you need to add it to the `plugin.json`. + +1. Include the data source in the `plugin.json`. The `name` property is only used for displaying in the Grafana UI. + + ```json + "includes": [ + { + "type": "datasource", + "name": "My data source" + } + ] + ``` + +#### Include external plugins + +If you want to let users know that your app requires an existing plugin, you can add it as a dependency in `plugin.json`. Note that they'll still need to install it themselves. + +```json +"dependencies": { + "plugins": [ + { + "type": "panel", + "name": "Worldmap Panel", + "id": "grafana-worldmap-panel", + "version": "^0.3.2" + } + ] +} +``` + +## Summary + +In this tutorial you learned how to create an app plugin. diff --git a/docs/sources/tutorials/create-alerts-from-flux-queries/index.md b/docs/sources/tutorials/create-alerts-from-flux-queries/index.md new file mode 100644 index 00000000000..405b4f861cd --- /dev/null +++ b/docs/sources/tutorials/create-alerts-from-flux-queries/index.md @@ -0,0 +1,331 @@ +--- +title: How to create Grafana alerts with InfluxDB and the Flux query language +summary: Create complex alerts from Flux queries in the new Grafana Alerting +description: Create complex alerts from Flux queries in the new Grafana Alerting +id: grafana-alerts-flux-queries +categories: ['alerting'] +tags: ['advanced'] +status: published +authors: ['grant_pinkos'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +weight: 70 +--- + +# How to create Grafana alerts with InfluxDB and the Flux query language + +[Grafana Alerting](/docs/grafana/latest/alerting/) represents a powerful new approach to systems observability and incident response management. While the alerting platform is perhaps best known for its strong integrations with Prometheus, the system works with numerous popular data sources including InfluxDB. In this tutorial we will learn how to create Grafana alerts using InfluxDB and the newer Flux query language. We will cover five common scenarios from the most basic to the most complex. Together, these five scenarios will provide an excellent guide for almost any type of alerting query that you wish to create using Grafana and Flux. + +Before we dive into our alerting scenarios, it is worth considering the development of InfluxDB's two popular query languages: InfluxQL and Flux. Originally, InfluxDB used [InfluxQL](https://docs.influxdata.com/influxdb/v2.5/reference/syntax/influxql/spec/) as their query language, which uses a SQL-like syntax. But beginning with InfluxDB v1.8, the company introduced [Flux](https://docs.influxdata.com/flux/v0.x/), "an open source functional data scripting language designed for querying, analyzing, and acting on data." "Flux," its official documentation goes on to state, "unifies code for querying, processing, writing, and acting on data into a single syntax. The language is designed to be usable, readable, flexible, composable, testable, contributable, and shareable." + +In the following five examples we will see just how powerful and flexible the new Flux query language can be. We will also see just how well Flux pairs with Grafana Alerting. + +## Example 1: Create an alert when a value is above or below a set threshold + +Our first example uses a common real-world scenario for InfluxDB and Grafana Alerting. Popular with IoT and edge applications, InfluxDB excels at on-site, real-time observability. In this example, and in fact for many of the following examples, we will consider the hypothetical scenario where we are monitoring a number of fluid tanks in a manufacturing plant. This scenario, [based on an actual application of InfluxDB and Alerting](/go/grafanaconline/2021/plant-efficiency-grafana-cloud/), will allow us to work through Grafana's various alerting setups, progressing from the simplest to the most complex. + +For Example 1, let's consider the following scenario: we are monitoring one tank, `A5`, for which we are storing real-time temperature data. We need to make sure that the temperature in this tank is always greater than 30 °C and less than 60 °C. + +We want to write a Grafana alert that will trigger whenever the temperature in tank `A5` crosses the lower threshold of 30 °C or the upper threshold of 60 °C. + +To do this, we'll: 1. create a Grafana alert rule. 1. add a Flux query. 1. add expressions to the alert rule. + +### Create a Grafana Alert rule + +1. Open the Grafana alerting menu and select **Alert rules**. +1. Click **New alert rule**. +1. Give your alert rule a name and then select **Grafana managed alert**. + For InfluxDB, you will always create a [Grafana managed rule](/docs/grafana/latest/alerting/alerting-rules/create-grafana-managed-rule/#add-grafana-managed-rule). + +### Add an initial Flux query to the alert rule + +Still in the **Step 2** section of the Alert rule page, you will see three boxes: a query editor (`A`), and then two sections labelled `B` and `C`. You will use these three sections to construct your rule. Let's move through them one by one. + +First, we want to query the data in our imaginary InfluxDB instance to obtain a time series graph of the temperature of tank A5. For this you would choose your InfluxDB data source from the dropdown and then write a query like this: + + ``` + from(bucket: "RetroEncabulator") + |> range(start: v.timeRangeStart, stop: v.timeRangeStop) + |> filter(fn: (r) => r["_measurement"] == "TemperatureData") + |> filter(fn: (r) => r["Tank"] == "A5") + |> filter(fn: (r) => r["_field"] == "Temperature") + |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) + |> yield(name: "mean") + ``` + +This is a fairly typical Flux query. Let's go through it function by function. We begin using [the `from()` function](https://docs.influxdata.com/flux/v0.x/stdlib/influxdata/influxdb/from/) to choose the correct bucket where our tank data resides. Then we use [a `range()` function](https://docs.influxdata.com/flux/v0.x/stdlib/universe/range/) to filter our rows based on time constraints. Then we pass our data through three [`filter()` functions](https://docs.influxdata.com/flux/v0.x/stdlib/universe/filter/) to narrow our results. We choose a specific [`measurement` (a special keyword in InfluxDB)](https://docs.influxdata.com/influxdb/v1.8/concepts/glossary/#measurement), then our tank in question (`A5`), and then a specific [`field` (another special keyword in InfluxDB)](https://docs.influxdata.com/influxdb/v1.8/concepts/glossary/#field). After this we pass the data into [an `aggregateWindow()` function](https://docs.influxdata.com/flux/v0.x/stdlib/universe/aggregatewindow/), which downsamples our data into specific periods of time, and then finally [a `yield()` function](https://docs.influxdata.com/flux/v0.x/stdlib/universe/yield/), which specifies which final result we want: `mean`. + +This Flux query will yield a time-series graph like this: + +![grafana alerts from flux queries](/media/tutorials/screenshot-flux-timeseries-graph.png) + +### Add expressions to your Grafana Alert rule + +With data now appearing in our rule setup, our next step is to create an [expression](/docs/grafana/v9.0/panels/query-a-data-source/use-expressions-to-manipulate-data/about-expressions/#using-expressions). Move to section `B`. For this scenario, we want to create a Reduce expression that will reduce the above to a single value. In this image, you can see that we have chosen to reduce our time-series data the `Last` value from input `A`. In this case, it returns a value 53 degrees celsius for Tank A5: + +![grafana alerts from flux queries](/media/tutorials/screenshot-flux-reduce-expression.png) + +Finally, we need to create a math expression that Grafana will alert on. In our case we will write an expression with two conditions separated by the OR `||` operator. We want to trigger an alert any time our result in section `B` is less than 30 or more than 60. This looks like `$B < 30 || $B > 60`: + +![grafana alerts from flux queries](/media/tutorials/screenshot-flux-math-expression.png) + +Set the alert condition to `C - expression`. We can now preview our alert. Here is a preview of this alert when the state is `Normal`: + +![grafana alerts from flux queries](/media/tutorials/screenshot-flux-alert-preview-state-normal.png) + +And here is a preview of this alert when the state is `Alerting`: + +![grafana alerts from flux queries](/media/tutorials/screenshot-flux-alert-alert-preview-state-alerting.png) + +Note that the Reduce expression above is needed. Without it, when previewing the results, Grafana would display `invalid format of evaluation results for the alert definition B: looks like time series data, only reduced data can be alerted on`. + +💡Tip: In case your locale is still stubbornly using Fahrenheit, we can modify the above Flux query by adding (before the aggregateWindow statement) a map() function to to convert (or map) the values from °C to °F. Note that we are not creating a new field. We are simply remapping the existing value. + +```flux +|> map(fn: (r) => ({r with _value: r._value * 1.8 + 32.0})) +``` + +### Conclusion + +Using these three steps you can create a Flux-based Grafana Alert that will trigger on either of two thresholds from a single data source. But what if you need to trigger an alert based on **multiple conditions and from multiple time-series**? In example two we will cover this very scenario. + +## Example 2: how to create a Grafana alert from two queries and two conditions + +Let's mix things up a bit for example two and leave our imaginary manufacturing plant. Imagine you're an assistant to the great Dr. Emmett Brown from Back to the Future, and Doc has tasked you with the following challenge: "I want an alert sent to me every time both conditions for time travel are met: when the velocity of a vehicle reaches 88 miles per hour and an object generates 1.21 jigowatts of electricity." + +Let's assume we are tracking this data in InfluxDB and Grafana. Let's also assume that each of the above data sources comes from different buckets. How do we alert on this? How do we use Grafana and Flux to alert on two distinct conditions originating from two distinct data sources? + +### Add two Flux queries to your Grafana Alert rule + +Like we did in example 1, let's first mock up our queries. Our query for our vehicle data is very similar to our last query. We use a `from()`, `range()`, and a sequence of `filter()` functions. We then use `AggregateWindow()` and `yield()` to narrow our data even more. In this case, the result is a time series tracking the velocity of our 1983 DeLorean: + +```flux +from(bucket: "vehicles") +|> range(start: v.timeRangeStart, stop: v.timeRangeStop) +|> filter(fn: (r) => r["_measurement"] == "VehicleData") +|> filter(fn: (r) => r["VehicleType"] == "DeLorean") +|> filter(fn: (r) => r["VehicleYear"] == "1983") +|> filter(fn: (r) => r["_field"] == "velocity") +|> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) +|> yield(name: "mean") +``` + +Our second query will trigger an alert whenever our electricity resource (the lightning strike on the Hill Valley clocktower) reaches the needed 1.21 jigowatts. A query like this would look very similar to our vehicle velocity query: + +```flux +from(bucket: "HillValley") +|> range(start: v.timeRangeStart, stop: v.timeRangeStop) +|> filter(fn: (r) => r["_measurement"] == "ElectricityData") +|> filter(fn: (r) => r["Location"] == "clocktower") +|> filter(fn: (r) => r["Source"] == "lightning") +|> filter(fn: (r) => r["_field"] == "power") +|> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) +|> yield(name: "mean") +``` + +We are now ready to modify this data using expressions. + +### Add expressions to your Grafana Alert rule + +1. Let's now use the same steps to reduce each query to the last (most recent) value. Reducing Query `A` to a single value might look like this: + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-additional-queries-reduce-expression-A.png) + +1. And here we are reducing query `B`: + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-additional-queries-reduce-expression-B.png) + +1. Now, in section `C` we need to create a math expression to be alerted on. In this case we will use the AND `&&` operator to specify that two conditions must be met: the value of `C` (the reduced value from query `A`) must be greater than 88.0 while the value of `D` (the reduced value from query `B`) must be greater than 1.21. We write this as `$C > 88.0 && $D > 1.21` + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-additional-queries-math-expression.png) + +And here is a preview of our alerts: + +![grafana alerts from flux queries](https://raw.githubusercontent.com/grafana/tutorials/master/content/tutorials/assets/flux-additional-queries-alert-preview.png) + +💡Tip: If your data in InfluxDB happens to have an unnecessarily large number of digits to the right of the decimal (such as 1.2104705741732575 shown above), and you want your Grafana alerts to be more legible, try using {{ printf "%.2f" $values.D.Value }}. For example, in the annotation Summary, we could write the following: + +``` +{{ $values.D.Labels.Source }} at the {{ $values.D.Labels.Location }} has generated {{ printf "%.2f" $values.D.Value }} jigowatts.` +``` + +This will display as follows: +![grafana alerts from flux queries](/media/tutorials/screenshot-flux-tip-significant-figures.png)) + +You can reference our documentation on [alert message templating](/docs/grafana/latest/alerting/contact-points/message-templating/) to learn more about this powerful feature. + +### Conclusion + +In this example we showed how to create a Flux-based alert that uses two distinct conditions from two distinct queries that use data from two distinct data sources. For example three we will switch gears and tackle another popular alerting scenario: how to create an alert based on an aggregated (per day) value. + +## Example 3: how to create a Grafana Alert based on an aggregated (per-day) value + +One of the most common requests in [Grafana's community forum](https://community.grafana.com) involves graphing daily electrical consumption and production. This sort of data is very often stored in InfluxDB. In this example we will see how to aggregate time series data into a per-day value and then alert on it. + +Let’s assume our electricity meter sends a reading to InfluxDB once per hour and contains the total kWh used for that hour. We want to write a query that will aggregate these per-hour values into a per-day value, then create an alert that triggers when the power consumption (kWh) exceeds 5,000 kWh per day. + +### Add an initial Flux query to your Grafana Alert rule + +1. Let's begin by examining a typical query and the resulting time graph for our hourly data across a 7-day period. A query like this is shown below: + + ```flux + from(bucket: "RetroEncabulator") + |> range(start: v.timeRangeStart, stop: v.timeRangeStop) + |> filter(fn: (r) => r["_measurement"] == "ElectricityData") + |> filter(fn: (r) => r["Location"] == "PlantD5") + |> filter(fn: (r) => r["_field"] == "power_consumed") + |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) + |> yield(name: "power") + ``` + + We can see the same pattern of Flux functions here that we say in examples 1 and 2. A query like this would produce a graph similar to the following: + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-aggregatewindow-timeseries-graph.png) + +1. Now let's adjust our query to calculate daily usage. With many datasources, this can be a rather complex operation. But with Flux, by simply changing the aggregateWindow function parameters we can calculate the daily usage over the same 7-day period: + + ```flux + from(bucket: "RetroEncabulator") + |> range(start: v.timeRangeStart, stop: v.timeRangeStop) + |> filter(fn: (r) => r["_measurement"] == "ElectricityData") + |> filter(fn: (r) => r["Location"] == "PlantD5") + |> filter(fn: (r) => r["_field"] == "power_consumed") + |> aggregateWindow(every: 1d, fn: sum) + |> yield(name: "power") + ``` + + Note how we've adjusted our `aggregateWindow()` function to `aggregateWindow(every: 1d, fn: sum)`. This results in a graph like so: + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-aggregatewindow-aggregated.png) + +1. Add expressions to your Grafana Alert rule. + + Now that we have our per-day query correct, we can continue using the same pattern as before, adding expressions to reduce and perform math on our results. + + As before, let's reduce our query to a single value: + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-aggregatewindow-reduce-expression.png) + + Now create a math expression to be alerted on and set the evaluation behavior. In this case we want to write `$B > 5000`: + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-aggregatewindow-math-expression.png) + + And now we are alerting on our daily electricity consumption whenever we exceed 5000 kWh. Here is preview of our alert: + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-aggregatewindow-alert-preview.png) + +### Conclusion + +Plotting and aggregating electrical consumption is a common use case for combining InfluxDB and Grafana. Using Flux, we saw just how easy it can be to group our data by day and then alert on that daily value. In our next two examples we will examine the more complex form of Grafana Alert: multidimensional alerts. + +## Example 4: create a dynamic (multidimensional) Grafana Alert using Flux + +Let’s return to our fluid tanks from example 1, but this time let’s assume we have 5 tanks (A5, B4, C3, D2, and E1). We are now tracking the temperature in five tanks: A5, B4, C3, D2, and E1. + +We want to create one multidimensional alert that will notify us whenever the temperature in any tank is less than 30 °C or greater than 60 °C. + +### Add an initial Flux query to your Grafana Alert rule + +We begin, as always, by writing our initial query. This is very similar to our query in example 1, but note how our third `filter()` function captures the data from all five tanks and not just `A5`: + +```flux +from(bucket: "HyperEncabulator") +|> range(start: v.timeRangeStart, stop: v.timeRangeStop) +|> filter(fn: (r) => r["_measurement"] == "TemperatureData") +|> filter(fn: (r) => r["MeasType"] == "actual") +|> filter(fn: (r) => r["Tank"] == "A5" or r["Tank"] == "B4" or r["Tank"] == "C3" or r["Tank"] == "D2" or r["Tank"] == "E1") +|> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) +|> yield(name: "mean") +``` + +💡Tip: If the tanks were shut down every night from 23:00 to 07:00, they would possibly fall below the 30 °C threshold. If one did not want to receive alerts during those hours, one can use the Flux function hourSelection() which filters rows by time values in a specified hour range. + +```flux +|> hourSelection(start: 7, stop: 23)` +``` + +A query like the one above will produce a time series graph like this: + +![grafana alerts from flux queries](/media/tutorials/screenshot-flux-multidimensional-timeseries-graph.png) + +### Add expressions to your Grafana Alert rule + +1. We create a Reduce expression that will reduce the time series for each tank to a single value. This gives us five distinct temperatures: + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-multidimensional-reduce-expression.png)) + +1. Create a math expression to be alerted on. This is the exact same expression from example 1, `$B < 30 || $B > 60`: + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-multidimensional-math-expression.png) + +As we can see three tanks are within the acceptable thresholds while two tanks have crossed the upper boundary. This would trigger an alert for tanks `D2` and `E1`. + +### Conclusion + +With multidimensional alerts we can avoid repeating ourselves. But what if the scenario were even more complex? In the next and final example, we will examine how to use multidimensional alerts to create the most dynamic alerts possible. + +## Example 5: how to create a dynamic (multidimensional) Grafana Alert using multiple queries and multiple thresholds with Flux + +For this final example let's continue with our five fluid tanks and their five datasets.Let’s assume again that each tank has a temperature controller with a setpoint value that is stored in InfluxDB. Let’s mix things up and assume that each tank has a _different_ setpoint, where we always need to be within 3 degrees of the setpoint. + +We want to create one multidimensional alert that will cover each unique scenario for each tank, triggering an alert whenever any tank's temperature moves beyond its unique allowable range. + +To better visualize this challenge, here is a table representing our five tanks, their temperature setpoints, and their allowable range: + +| Tank | Setpoint | Allowable Range (±3) | +| ---- | -------- | -------------------- | +| A5 | 45 | 42 to 48 | +| B4 | 55 | 52 to 58 | +| C3 | 60 | 57 to 63 | +| D2 | 72 | 69 to 75 | +| E1 | 80 | 77 to 83 | + +With Grafana Alerting, we can create a single multidimensional rule to cover all 5 tanks, and we can use Flux to compare the setpoint and actual value for each tank. In other words, one multidimensional alert can monitor 5 separate tanks, each with different setpoints and actual values, but all with one common "allowable threshold" (i.e. a temperature difference of ±3 degrees). + +### Add an initial Flux query to your Grafana Alert rule + +Let's begin with our data query. It is similar to our past queries, only now more complex. We must add extra functions to get our data into the proper format, including a `pivot()`, `map()`, `rename()`, `keep()`, and `drop()` function: + +```flux +from(bucket: "HyperEncabulator") + |> range(start: v.timeRangeStart, stop: v.timeRangeStop) + |> filter(fn: (r) => r["_measurement"] == "TemperatureData") + |> filter(fn: (r) => r["MeasType"] == "actual" or r["MeasType"] == "setpoint") + |> filter(fn: (r) => r["Tank"] == "A5" or r["Tank"] == "B4" or r["Tank"] == "C3" or r["Tank"] == "D2" or r["Tank"] == "E1") + |> filter(fn: (r) => r["_field"] == "Temperature") + |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) + |> pivot(rowKey:["_time"], columnKey: ["MeasType"], valueColumn: "_value") + |> map(fn: (r) => ({ r with _value: (r.setpoint - r.actual)})) + |> rename(columns: {_value: "difference"}) + |> keep(columns: ["_time", "difference", "Tank"]) + |> drop(columns: ["actual", "setpoint"]) + |> yield(name: "mean") +``` + +Note in the above that we are calculating the difference between the actual and the setpoint. The way Grafana parses the result from InfluxDB is that if a \_value column is found, it is assumed to be a time-series. The quick workaround is to add the following `rename()` function: + +```flux + |> rename(columns: {_value: "something"}) +``` + +The above query results in this time series: + +![grafana alerts from flux queries](/media/tutorials/screenshot-flux-complex-query-timeseries-graph.png) + +### Add expressions to your Grafana Alert rule + +1. Again, we create a Reduce expression for the above query to reduce each of the above to a single value. This value represents the temperature differential between each tank's setpoint and its actual real-time temperature: + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-complex-query-reduce-expression.png) + +1. Now we create a math expression to be alerted on. This time we will create a condition that checks if the absolute value of our reduce calculation is greater than 3, `abs($(B))>3.0`: + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-complex-query-math-expression.png) + +We can now see that two tanks, `D2` and `E1`, are evaluating to true. When we preview the alert we can see that those two tanks will trigger a notification and change their state from `Normal` to `Alerting`: + +![grafana alerts from flux queries](/media/tutorials/screenshot-flux-complex-query-alert-preview-state-normal.png) +![grafana alerts from flux queries](/media/tutorials/screenshot-flux-complex-query-alert-preview-state-alerting.png) + +### Conclusion + +Flux queries and Grafana Unified Alerting are a powerful combination to identify practically any alertable conditions in your dataset, or across your entire system. For more information on Grafana Alerting, [visit the documentation here](/docs/grafana/latest/alerting/). For more information on the Flux query language, [you can visit that documentation as well](https://docs.influxdata.com/flux/v0.x/). diff --git a/docs/sources/tutorials/create-users-and-teams/index.md b/docs/sources/tutorials/create-users-and-teams/index.md new file mode 100644 index 00000000000..23b02d1d663 --- /dev/null +++ b/docs/sources/tutorials/create-users-and-teams/index.md @@ -0,0 +1,236 @@ +--- +title: Create users and teams +summary: Learn how to set up teams and users. +description: Learn how to set up teams and users. +id: create-users-and-teams +categories: ['administration'] +tags: ['beginner'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +weight: 20 +--- + +## Introduction + +This tutorial is for admins or anyone that wants to learn how to manage +users in Grafana. You'll add multiple local users, organize them into teams, +and make sure they're only able to access the resources they need. + +### Scenario + +_Graphona_, a fictional telemarketing company, has asked you to configure Grafana +for their teams. + +In this scenario, you'll: + +- Create users and organize them into teams. +- Manage resource access for each user and team through roles and folders. + +{{% class "prerequisite-section" %}} + +### Prerequisites + +- Grafana 7.0 or newer, this tutorial was tested with Grafana 8.5. +- A user with the Admin or Server Admin role. + {{% /class %}} + +## Add users + +In Grafana, all users are granted an _organization role_ that determines what +resources they can access. + +There are three types of organization roles in Grafana. The **Grafana Admin** is +a global role, the default `admin` user has this role. + +- **Grafana Admin -** Manage organizations, users, and view server-wide settings. +- **Organization Administrator -** Manage data sources, teams, and users within an organization. +- **Editor -** Create and edit dashboards. +- **Viewer -** View dashboards. + +> **Note**: You can also configure Grafana to allow [anonymous access](/docs/grafana/latest/auth/overview/#anonymous-authentication), to make dashboards available even to those who don't have a Grafana user account. That's how Grafana Labs made https://play.grafana.org publicly available. + +### Exercise + +Graphona has asked you to add a group of early adopters that work in the Marketing and Engineering teams. They'll need to be able to edit their own team's dashboards, but want to have view access to dashboards that belong to the other team. + +| Name | Email | Username | +| ----------------- | ----------------------------- | ----------------- | +| Almaz Russom | almaz.russom@example.com | almaz.russom | +| Brenda Tilman | brenda.tilman@example.com | brenda.tilman | +| Mada Rawdha Tahan | mada.rawdha.tahan@example.com | mada.rawdha.tahan | +| Yuan Yang | yuan.yang@example.com | yuan.yang | + +#### Add users + +Repeat the following steps for each of the employees in the table above to create the new user accounts: + +1. Log in as a user that has the **Server Admin** role. +1. On the sidebar, click the **Server Admin** (shield) icon. +1. Choose **Users** from the menu drop-down, then click **New User**. +1. Enter the **Name**, **Email**, **Username**, and **Password** from the table above. +1. Click the **Create User** button to create the account. + +When you create a user they are granted the Viewer role by default, which means that they won't be able to make any changes to any of the resources in Grafana. That's ok for now, you'll grant more user permissions by adding users to _teams_ in the next step. + +## Assign users to teams + +Teams let you grant permissions to a group of users, instead of granting permissions to individual users one at a time. + +Teams are useful when onboarding new colleagues. When you add a user to a team, they get access to all resources assigned to that team. + +### Exercise + +In this step, you'll create two teams and assign users to them. + +| Username | Team | +| ----------------- | ----------- | +| brenda.tilman | Marketing | +| mada.rawdha.tahan | Marketing | +| almaz.russom | Engineering | +| yuan.yang | Engineering | + +#### Create a team + +Create the _Marketing_ and _Engineering_ teams. + +1. In the sidebar, hover your mouse over the **Configuration** (gear) icon and + then click **Teams**. +1. Click **New team**. +1. In **Name**, enter the name of the team: either _Marketing_ or _Engineering_. + You do not need to enter an email. +1. Click **Create**. +1. Click on the **Teams** link at the top of the page to return to teams page and create the second team. + +#### Add a user to a team + +Repeat these steps for each user to assign them to their team. Refer to the table above for team assignments. + +1. Click the team name _Marketing_ or _Engineering_ to add members to that team. +1. Click **Add member**. +1. In the **Add team member** box, click the drop-down arrow to choose the user you want to add to the team . +1. Click **Add to team**. + +When you're done, you'll have two teams with two users assigned to each. + +## Manage resource access with folders + +It's a good practice to use folders to organize collections of related dashboards. You can assign permissions at the folder level to individual users or teams. + +### Exercise + +The Marketing team is going to use Grafana for analytics, while the Engineering team wants to monitor the application they're building. + +You'll create two folders, _Analytics_ and _Application_, where each team can add their own dashboards. The teams still want to be able to view each other's dashboards. + +| Folder | Team | Permissions | +| ----------- | ----------- | ----------- | +| Analytics | Marketing | Edit | +| | Engineering | View | +| Application | Marketing | View | +| | Engineering | Edit | + +Repeat the following steps for each folder. You'll move through all three steps for each folder before moving on to the next one. + +#### Add a folder for each team + +1. In the sidebar, hover your cursor over the **Dashboards** (four squares) icon and then click **Browse**. +1. To create a folder, click **New Folder**. +1. In **Name**, enter the folder name. +1. Click **Create**. +1. Stay in the folder view and move on to the next sections to edit permissions for this folder. + +#### Remove the viewer role from folder permissions + +By default, when you create a folder, all users with the Viewer role are granted permission to view the folder. + +In this example, Graphona wants to explicitly grant teams access to folders. To support this, you need to remove the Viewer role from the list of permissions: + +1. Go to the **Permissions** tab. +1. Remove the Viewer role from the list, by clicking the red button on the right. +1. Stay in the permissions tab and move on to the next section to grant folder permissions for each team. + +#### Grant folder permissions to a team: + +1. Click **Add Permission**. +1. In the **Add Permission For** dialog, make sure "Team" is selected in the first box. +1. In the second box, select the team to grant access to. +1. In the third box, select the access you want to grant. +1. Click **Save**. +1. Repeat for the other team. +1. Click the **Dashboards** link at the top of the page to return to the dashboard list. + +When you're finished, you'll have two empty folders, the contents of which can only be viewed by members of the Marketing or Engineering teams. Only Marketing team members can edit the contents of the Analytics folder, only Engineering team members can edit the contents of the Application folder. + +## Define granular permissions + +By using folders and teams, you avoid having to manage permissions for individual users. + +However, there are times when you need to configure permissions on a more granular level. For these cases, Grafana allows you to override permissions for specific dashboards. + +### Exercise + +Graphona has hired a consultant to assist the Marketing team. The consultant should only be able to access the SEO dashboard in the Analytics folder. + +| Name | Email | Username | +| ---------- | -------------------------------- | ---------- | +| Luc Masson | luc.masson@exampleconsulting.com | luc.masson | + +#### Add a new user + +1. In the sidebar, click the **Server Admin** (shield) icon. +1. In the Users tab, click **New user**. +1. In **Name**, enter the name of the user. +1. In **E-mail**, enter the email of the user. +1. In **Username**, enter the username that the user will use to log in. +1. In **Password**, enter a password. The user can change their password once they log in. +1. Click **Create user** to create the user account. + +#### Create a dashboard + +1. In the sidebar, click the **Create** (plus) icon to create a new dashboard. +1. In the top right corner, click the cog icon to go to **Dashboard settings**. +1. In **Name**, enter **SEO**. +1. Click **Save Dashboard**. +1. In the **Save dashboard as...** pop-up, choose the **Analytics** folder from the drop-down and click **Save**. + +#### Grant a user permission to view dashboard + +1. In the top right corner of your dashboard, click the cog icon to go to **Dashboard settings**. +1. Go to the **Permissions** tab, and click **Add Permission**. +1. In the **Add Permission For** dialog, select **User** in the first box. +1. In the second box, select the user to grant access to: Luc Masson. +1. In the third box, select **View**. +1. Click **Save**. +1. Click **Save dashboard**. +1. Add a note about giving Luc Masson Viewer permission for the dashboard and then click **Save**. + +You've created a new user and given them unique permissions to view a single dashboard within a folder. + +#### Check your work + +You can repeat these steps to log in as the other users you've created see the differences in the viewer and editor roles. + +For this example, you can log in as the user `luc.masson` to see that they can only access the SEO dashboard. + +1. Click the profile (avatar) button in the bottom left corner, choose **Sign out**. +1. Enter `luc.masson` as the username. +1. Enter the password you created for Luc. +1. Click **Log in**. +1. In the sidebar, hover your cursor over the **Dashboards** (four squares) icon and then click **Browse**. +1. You'll notice that you won't see the **Analytics** folder in the folder view because we did not give Luc folder permission. +1. Click on the list icon (3 lines) to see the dashboard list. +1. Click on the **SEO dashboard**, there shouldn't be any editing permissions since we assigned Luc the viewer role. + +## Summary + +In this tutorial, you've configured Grafana for an organization: + +- You added users to your organization. +- You created teams to manage permissions for groups of users. +- You configured permissions for folders and dashboard. + +### Learn more + +- [Organization Roles](/docs/grafana/next/administration/manage-users-and-permissions/about-users-and-permissions/#organization-roles) +- [Permissions Overview](/docs/grafana/latest/administration/manage-users-and-permissions/about-users-and-permissions/#about-users-and-permissions) diff --git a/docs/sources/tutorials/grafana-fundamentals/index.md b/docs/sources/tutorials/grafana-fundamentals/index.md new file mode 100644 index 00000000000..5374ac91ae0 --- /dev/null +++ b/docs/sources/tutorials/grafana-fundamentals/index.md @@ -0,0 +1,354 @@ +--- +title: Grafana fundamentals +summary: Get familiar with Grafana +description: Get familiar with Grafana +id: grafana-fundamentals +categories: ['fundamentals'] +tags: ['beginner'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +weight: 10 +--- + +## Introduction + +In this tutorial, you'll learn how to use Grafana to set up a monitoring solution for your application. + +In this tutorial, you'll: + +- Explore metrics and logs +- Build dashboards +- Annotate dashboards +- Set up alerts + +{{% class "prerequisite-section" %}} + +### Prerequisites + +- [Docker](https://docs.docker.com/install/) +- [Docker Compose](https://docs.docker.com/compose/) (included in Docker for Desktop for macOS and Windows) +- [Git](https://git-scm.com/) + {{% /class %}} + +## Set up the sample application + +This tutorial uses a sample application to demonstrate some of the features in Grafana. To complete the exercises in this tutorial, you need to download the files to your local machine. + +In this step, you'll set up the sample application, as well as supporting services, such as [Prometheus](https://prometheus.io/) and [Loki](/oss/loki/). + +1. Clone the [github.com/grafana/tutorial-environment](https://github.com/grafana/tutorial-environment) repository. + + ``` + git clone https://github.com/grafana/tutorial-environment.git + ``` + +1. Change to the directory where you cloned this repository: + + ``` + cd tutorial-environment + ``` + +1. Make sure Docker is running: + + ``` + docker ps + ``` + + No errors means it is running. If you get an error, then start Docker and then run the command again. + +1. Start the sample application: + + ``` + docker-compose up -d + ``` + + The first time you run `docker-compose up -d`, Docker downloads all the necessary resources for the tutorial. This might take a few minutes, depending on your internet connection. + + > **Note:** If you already have Grafana, Loki, or Prometheus running on your system, then you might see errors because the Docker image is trying to use ports that your local installations are already using. Stop the services, then run the command again. + +1. Ensure all services are up-and-running: + + ``` + docker-compose ps + ``` + + In the **State** column, it should say `Up` for all services. + +1. Browse to the sample application on [localhost:8081](http://localhost:8081). + +### Grafana News + +The sample application, Grafana News, lets you post links and vote for the ones you like. + +To add a link: + +1. In **Title**, enter **Example**. +1. In **URL**, enter **https://example.com**. +1. Click **Submit** to add the link. + + The link appears in the list under the Grafana News heading. + +To vote for a link, click the triangle icon next to the name of the link. + +## Log in to Grafana + +Grafana is an open-source platform for monitoring and observability that lets you visualize and explore the state of your systems. + +1. Open a new tab. +1. Browse to [localhost:3000](http://localhost:3000). +1. In **email or username**, enter **admin**. +1. In **password**, enter **admin**. +1. Click **Log In**. + + The first time you log in, you're asked to change your password: + +1. In **New password**, enter your new password. +1. In **Confirm new password**, enter the same password. +1. Click **Save**. + +The first thing you see is the Home dashboard, which helps you get started. + +To the far left you can see the _sidebar_, a set of quick access icons for navigating Grafana. + +## Add a metrics data source + +The sample application exposes metrics which are stored in [Prometheus](https://prometheus.io/), a popular time series database (TSDB). + +To be able to visualize the metrics from Prometheus, you first need to add it as a data source in Grafana. + +1. In the sidebar, hover your cursor over the **Configuration** (gear) icon, and then click **Data sources**. +1. Click **Add data source**. +1. In the list of data sources, click **Prometheus**. +1. In the URL box, enter **http\://prometheus:9090**. +1. Click **Save & test**. + + Prometheus is now available as a data source in Grafana. + +## Explore your metrics + +Grafana Explore is a workflow for troubleshooting and data exploration. In this step, you'll be using Explore to create ad-hoc queries to understand the metrics exposed by the sample application. + +> Ad-hoc queries are queries that are made interactively, with the purpose of exploring data. An ad-hoc query is commonly followed by another, more specific query. + +1. In the sidebar, click the **Explore** (compass) icon. +1. In the **Query editor**, where it says _Enter a PromQL query…_, enter `tns_request_duration_seconds_count` and then press Shift + Enter. + A graph appears. +1. In the top right corner, click the dropdown arrow on the **Run Query** button, and then select **5s**. Grafana runs your query and updates the graph every 5 seconds. + + You just made your first _PromQL_ query! [PromQL](https://prometheus.io/docs/prometheus/latest/querying/basics/) is a powerful query language that lets you select and aggregate time series data stored in Prometheus. + + `tns_request_duration_seconds_count` is a _counter_, a type of metric whose value only ever increases. Rather than visualizing the actual value, you can use counters to calculate the _rate of change_, i.e. how fast the value increases. + +1. Add the [`rate`](https://prometheus.io/docs/prometheus/latest/querying/functions/#rate) function to your query to visualize the rate of requests per second. Enter the following in the **Query editor** and then press Shift + Enter. + + ``` + rate(tns_request_duration_seconds_count[5m]) + ``` + + Immediately below the graph there's an area where each time series is listed with a colored icon next to it. This area is called the _legend_. + + PromQL lets you group the time series by their labels, using the [`sum`](https://prometheus.io/docs/prometheus/latest/querying/operators/#aggregation-operators) aggregation operator. + +1. Add the `sum` aggregation operator to your query to group time series by route: + + ``` + sum(rate(tns_request_duration_seconds_count[5m])) by(route) + ``` + +1. Go back to the [sample application](http://localhost:8081) and generate some traffic by adding new links, voting, or just refresh the browser. + +1. In the upper-right corner, click the _time picker_, and select **Last 5 minutes**. By zooming in on the last few minutes, it's easier to see when you receive new data. + +Depending on your use case, you might want to group on other labels. Try grouping by other labels, such as `status_code`, by changing the `by(route)` part of the query. + +## Add a logging data source + +Grafana supports log data sources, like [Loki](/oss/loki/). Just like for metrics, you first need to add your data source to Grafana. + +1. In the sidebar, hover your cursor over the **Configuration** (gear) icon, and then click **Data Sources**. +1. Click **Add data source**. +1. In the list of data sources, click **Loki**. +1. In the URL box, enter [http://loki:3100](http://loki:3100). +1. Click **Save & Test** to save your changes. + +Loki is now available as a data source in Grafana. + +## Explore your logs + +Grafana Explore not only lets you make ad-hoc queries for metrics, but lets you explore your logs as well. + +1. In the sidebar, click the **Explore** (compass) icon. +1. In the data source list at the top, select the **Loki** data source. +1. In the **Query editor**, enter: + + ``` + {filename="/var/log/tns-app.log"} + ``` + +1. Grafana displays all logs within the log file of the sample application. The height of each bar in the graph encodes the number of logs that were generated at that time. + +1. Click and drag across the bars in the graph to filter logs based on time. + +Not only does Loki let you filter logs based on labels, but on specific occurrences. + +Let's generate an error, and analyze it with Explore. + +1. In the [sample application](http://localhost:8081), post a new link without a URL to generate an error in your browser that says `empty url`. +1. Go back to Grafana and enter the following query to filter log lines based on a substring: + + ``` + {filename="/var/log/tns-app.log"} |= "error" + ``` + +1. Click on the log line that says `level=error msg="empty url"` to see more information about the error. + + > **Note:** If you're in Live mode, clicking logs will not show more information about the error. Instead, stop and exit the live stream, then click the log line there. + +Logs are helpful for understanding what went wrong. Later in this tutorial, you'll see how you can correlate logs with metrics from Prometheus to better understand the context of the error. + +## Build a dashboard + +A _dashboard_ gives you an at-a-glance view of your data and lets you track metrics through different visualizations. + +Dashboards consist of _panels_, each representing a part of the story you want your dashboard to tell. + +Every panel consists of a _query_ and a _visualization_. The query defines _what_ data you want to display, whereas the visualization defines _how_ the data is displayed. + +1. In the sidebar, hover your cursor over the **Create** (plus sign) icon and then click **Dashboard**. +1. Click **Add a new panel**. +1. In the **Query editor** below the graph, enter the query from earlier and then press Shift + Enter: + + ``` + sum(rate(tns_request_duration_seconds_count[5m])) by(route) + ``` + +1. In the **Legend** field, enter **{{route}}** to rename the time series in the legend. The graph legend updates when you click outside the field. +1. In the Panel editor on the right, under **Settings**, change the panel title to "Traffic". +1. Click **Apply** in the top-right corner to save the panel and go back to the dashboard view. +1. Click the **Save dashboard** (disk) icon at the top of the dashboard to save your dashboard. +1. Enter a name in the **Dashboard name** field and then click **Save**. + +## Annotate events + +When things go bad, it often helps if you understand the context in which the failure occurred. Time of last deploy, system changes, or database migration can offer insight into what might have caused an outage. Annotations allow you to represent such events directly on your graphs. + +In the next part of the tutorial, we will simulate some common use cases that someone would add annotations for. + +1. To manually add an annotation, click anywhere in your graph, then click **Add annotation**. +1. In **Description**, enter **Migrated user database**. +1. Click **Save**. + + Grafana adds your annotation to the graph. Hover your mouse over the base of the annotation to read the text. + +Grafana also lets you annotate a time interval, with _region annotations_. + +Add a region annotation: + +1. Press Ctrl (or Cmd on macOS), then click and drag across the graph to select an area. +1. In **Description**, enter **Performed load tests**. +1. In **Tags**, enter **testing**. + +Manually annotating your dashboard is fine for those single events. For regularly occurring events, such as deploying a new release, Grafana supports querying annotations from one of your data sources. Let's create an annotation using the Loki data source we added earlier. + +1. At the top of the dashboard, click the **Dashboard settings** (gear) icon. +1. Go to **Annotations** and click **Add annotation query**. +1. In **Name**, enter **Errors**. +1. In **Data source**, select **Loki**. +1. In **Query**, enter the following query: + + ``` + {filename="/var/log/tns-app.log"} |= "error" + ``` + + + +1. Click **Add**. Grafana displays the Annotations list, with your new annotation. +1. Click the **Go back** arrow to return to your dashboard. +1. At the top of your dashboard, there is now a toggle to display the results of the newly created annotation query. Press it so that it's enabled. + +The log lines returned by your query are now displayed as annotations in the graph. + +Being able to combine data from multiple data sources in one graph allows you to correlate information from both Prometheus and Loki. + +Annotations also work very well alongside alerts. In the next and final section, we will set up an alert for our app `grafana.news` and then we will trigger it. This will provide a quick intro to our new Alerting platform. + +## Create a Grafana Managed Alert + +Alerts allow you to identify problems in your system moments after they occur. By quickly identifying unintended changes in your system, you can minimize disruptions to your services. + +Grafana's new alerting platform debuted with Grafana 8. A year later, with Grafana 9, it became the default alerting method. In this step we will create a Grafana Managed Alert. Then we will trigger our new alert and send a test message to a dummy endpoint. + +The most basic alert consists of two parts: + +1. A _Contact Point_ - A Contact point defines how Grafana delivers an alert. When the conditions of an _alert rule_ are met, Grafana notifies the contact points, or channels, configured for that alert. Some popular channels include email, webhooks, Slack notifications, and PagerDuty notifications. +1. An _Alert rule_ - An Alert rule defines one or more _conditions_ that Grafana regularly evaluates. When these evaluations meet the rule's criteria, the alert is triggered. + +To begin, let's set up a webhook Contact Point. Once we have a usable endpoint, we'll write an alert rule and trigger a notification. + +### Create a Contact Point for Grafana Managed Alerts + +In this step, we'll set up a new Contact Point. This contact point will use the _webhooks_ channel. In order to make this work, we also need an endpoint for our webhook channel to receive the alert. We will use [requestbin.com](https://requestbin.com) to quickly set up that test endpoint. This way we can make sure that our alert is actually sending a notification somewhere. + +1. Browse to [requestbin.com](https://requestbin.com). +1. Under the **Create Request Bin** button, click the **public bin** link. + +Your request bin is now waiting for the first request. + +1. Copy the endpoint URL. + +Next, let's configure a Contact Point in Grafana's Alerting UI to send notifications to our Request Bin. + +1. Return to Grafana. In Grafana's sidebar, hover your cursor over the **Alerting** (bell) icon and then click **Contact points**. +1. Click **+ New contact point**. +1. In **Name**, write **RequestBin**. +1. In **Contact point type**, choose **Webhook**. +1. In **Url**, paste the endpoint to your request bin. +1. Click **Test** to send a test alert to your request bin. +1. Navigate back to the request bin you created earlier. On the left side, there's now a `POST /` entry. Click it to see what information Grafana sent. +1. Return to Grafana and click **Save contact point**. + +We have now created a dummy webhook endpoint and created a new Alerting Contact Point in Grafana. Now we can create an alert rule and link it to this new channel. + +### Add an Alert Rule to Grafana + +Now that Grafana knows how to notify us, it's time to set up an alert rule: + +1. In Grafana's sidebar, hover the cursor over the **Alerting** (bell) icon and then click **Alert rules**. +1. Click **+ New alert rule**. +1. For **Section 1**, name the rule `fundamentals-test`, and set **Rule type** to **Grafana Managed Alert**. For **Folder** type `fundamentals` and in the box that appears, press **Create: fundamentals**. +1. For **Section 2**, find the **query A** box. Choose your Prometheus datasource and enter the same query that we used in our earlier panel: `sum(rate(tns_request_duration_seconds_count[5m])) by(route)`. Press **Run queries**. You should see some data in the graph. +1. Now scroll down to the **query B** box. For **Operation** choose `Classic condition`. [You can read more about classic and multi-dimensional conditions here](/docs/grafana/latest/alerting/unified-alerting/alerting-rules/create-grafana-managed-rule/#single-and-multi-dimensional-rule). For conditions enter the following: `WHEN last() OF A IS ABOVE 0.2` +1. In **Section 3**, enter `30s` for the **Evaluate every** field. For the purposes of this tutorial, the evaluation interval is intentionally short. This makes it easier to test. In the **for** field, enter `0m`. This setting makes Grafana wait until an alert has fired for a given time before Grafana sends the notification. +1. In **Section 4**, you can add some sample text to your summary message. [Read more about message templating here](/docs/grafana/latest/alerting/unified-alerting/message-templating/). +1. Click **Save and exit** at the top of the page. +1. In Grafana's sidebar, hover the cursor over the **Alerting** (bell) icon and then click **Notification policies**. +1. Under **Root policy**, press **Edit** and change the **Default contact point** to **RequestBin**. As a system grows, admins can use the **Notification policies** setting to organize and match alert rules to specific contact points. + +### Trigger a Grafana Managed Alert + +We have now configured an alert rule and a contact point. Now let's see if we can trigger a Grafana Managed Alert by generating some traffic on our sample application. + +1. Browse to [localhost:8081](http://localhost:8081). +1. Repeatedly click the vote button or refresh the page to generate a traffic spike. + +Once the query `sum(rate(tns_request_duration_seconds_count[5m])) by(route)` returns a value greater than `0.2` Grafana will trigger our alert. Browse to the Request Bin we created earlier and find the sent Grafana alert notification with details and metadata. + +## Summary + +In this tutorial you learned about fundamental features of Grafana. To do so, we ran several Docker containers on your local machine. When you are ready to clean up this local tutorial environment, run the following command: + +``` +docker-compose down -v +``` + +### Learn more + +Check out the links below to continue your learning journey with Grafana's LGTM stack. + +- [Prometheus](/docs/grafana/latest/features/datasources/prometheus/) +- [Loki](/docs/grafana/latest/features/datasources/loki/) +- [Explore](/docs/grafana/latest/features/explore/) +- [Alerting Overview](/docs/grafana/latest/alerting/) +- [Alert rules](/docs/grafana/latest/alerting/create-alerts/) +- [Contact Points](/docs/grafana/latest/alerting/notifications/) diff --git a/docs/sources/tutorials/iis/index.md b/docs/sources/tutorials/iis/index.md new file mode 100644 index 00000000000..ec4a190a128 --- /dev/null +++ b/docs/sources/tutorials/iis/index.md @@ -0,0 +1,146 @@ +--- +title: Use IIS with URL Rewrite as a reverse proxy +summary: Learn how to set up Grafana behind IIS with URL Rewrite. +description: Learn how to set up Grafana behind IIS with URL Rewrite. +id: iis +categories: ['administration'] +tags: ['advanced'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +aliases: ['/docs/grafana/latest/tutorials/iis/'] +--- + +# Use IIS with URL Rewrite as a reverse proxy + +If you want Grafana to be a subpath/subfolder under a website in IIS then the Application Request Routing (ARR) and URL Rewrite modules for ISS can be used to support this. + +Example: + +- Parent site: http://yourdomain.com:8080 +- Grafana: http://localhost:3000 + +Grafana as a subpath: http://yourdomain.com:8080/grafana + +Other Examples: + +- If the application is only served on the local server, the parent site can also look like http://localhost:8080. +- If your domain is served using https on port 443, and thus the port is not normally entered in the address of your site, then the need to specify a port for the parent site in the configuration steps below can be eliminated. + +## Setup + +Install the URL Rewrite module for IIS. + +- Download and install the URL Rewrite module for IIS: https://www.iis.net/downloads/microsoft/url-rewrite + +You will also need the Application Request Routing (ARR) module for IIS for proxy forwarding + +- Download and install ARR module for IIS: https://www.iis.net/downloads/microsoft/application-request-routing + +## Grafana Config + +The Grafana config can be set by creating a file named/editing the existing file named `custom.ini` in the `conf` subdirectory of your Grafana installation. See the [installation instructions](http://docs.grafana.org/installation/windows/#configure) for more details. + +Using the example from above, if the subpath is `grafana` (you can set this to whatever is required) and the parent site is `yourdomain.com:8080`, then you would add this to the `custom.ini` config file: + +```bash +[server] +domain = yourdomain.com:8080 +root_url = %(protocol)s://%(domain)s/grafana/ +``` + +Restart the Grafana server after changing the config file. + +Configured address to serve Grafana: http://yourdomain.com:8080/grafana + +--- + +If you already have a subpath on your domain, configure it as follows: + +- Your Parent Site Address: http://yourdomain.com/existingsubpath + +```bash +[server] +domain = yourdomain.com/existingsubpath +root_url = %(protocol)s://%(domain)s/grafana/ +``` + +Restart the Grafana server after changing the config file. + +Configured address to serve Grafana: http://yourdomain.com/existingsubpath/grafana + +## IIS Config + +### Step 1: Forward Proxy + +1. Open the IIS Manager and click on the server +2. In the admin console for the server, double click on the Application Request Routing option: +3. Click the `Server Proxy Settings` action on the right-hand pane +4. Select the `Enable proxy` checkbox so that it is enabled +5. Click `Apply` and proceed with the URL Rewriting configuration + +**Note:** If you don't enable the Forward Proxy, you will most likely get 404 Not Found if you only apply the URL Rewrite rule + +### Step 2: URL Rewriting + +1. In the IIS Manager, click on the website that grafana will run under. For example, select the website that is bound to the http://yourdomain.com domain. +2. In the admin console for this website, double click on the URL Rewrite option: + +{{< figure src="/static/img/docs/tutorials/IIS_admin_console.png" max-width="800px" >}} + +3. Click on the `Add Rule(s)...` action +4. Choose the Blank Rule template for an Inbound Rule + +{{< figure src="/static/img/docs/tutorials/IIS_add_inbound_rule.png" max-width="800px" >}} + +5. Create an Inbound Rule for the website with the following settings: + +- pattern: `grafana(/)?(.*)` (if you have customised the subpath that will be used, use that instead of `grafana`) +- check the `Ignore case` checkbox +- rewrite URL set to `http://localhost:3000/{R:2}` +- check the `Append query string` checkbox +- check the `Stop processing of subsequent rules` checkbox + +{{< figure src="/static/img/docs/tutorials/IIS_url_rewrite.png" max-width="800px" >}} + +6. If your version of Grafana is greater than 8.3.5, you also need to configure the reverse proxy to preserve host headers. + +- This can be achieved by configuring the IIS config file by running this in a cmd prompt + `%windir%\system32\inetsrv\appcmd.exe set config -section:system.webServer/proxy -preserveHostHeader:true /commit:apphost` +- More information here https://github.com/grafana/grafana/issues/45261 + +Finally, navigate to `http://yourdomain.com:8080/grafana` and you should come to the Grafana login page. + +## Troubleshooting + +### 404 error + +When navigating to the Grafana URL (`http://yourdomain.com:8080/grafana`) and a `HTTP Error 404.0 - Not Found` error is returned, then either: + +- The pattern for the Inbound Rule is incorrect. Edit the rule, click on the `Test pattern...` button, test the part of the URL after `http://yourdomain.com:8080/` and make sure it matches. For `grafana/login` the test should return 3 capture groups: {R:0}: `grafana` {R:1}: `/` and {R:2}: `login`. +- The `root_url` setting in the Grafana config file does not match the parent URL with subpath. + +### Grafana Website only shows text with no images or css + +{{< figure src="/static/img/docs/tutorials/IIS_proxy_error.png" max-width="800px" >}} + +1. The `root_url` setting in the Grafana config file does not match the parent URL with subpath. This could happen if the root_url is commented out by mistake (`;` is used for commenting out a line in .ini files): + + `; root_url = %(protocol)s://%(domain)s/grafana/` + +2. or if the subpath in the `root_url` setting does not match the subpath used in the pattern in the Inbound Rule in IIS: + + `root_url = %(protocol)s://%(domain)s/grafana/` + + pattern in Inbound Rule: `wrongsubpath(/)?(.*)` + +3. or if the Rewrite URL in the Inbound Rule is incorrect. + + The Rewrite URL should not include the subpath. + + The Rewrite URL should contain the capture group from the pattern matching that returns the part of the URL after the subpath. The pattern used above returns three capture groups and the third one {R:2} returns the part of the URL after `http://yourdomain.com:8080/grafana/`. + +### You see an 'Error updating options: origin not allowed' error + +- Ensure you have undertaken step 6 above, to configure IIS to preserve host headers by edit IIS config by running this in cmd prompt: + `%windir%\system32\inetsrv\appcmd.exe set config -section:system.webServer/proxy -preserveHostHeader:true /commit:apphost` diff --git a/docs/sources/tutorials/install-grafana-on-raspberry-pi/index.md b/docs/sources/tutorials/install-grafana-on-raspberry-pi/index.md new file mode 100644 index 00000000000..16870ac0f92 --- /dev/null +++ b/docs/sources/tutorials/install-grafana-on-raspberry-pi/index.md @@ -0,0 +1,147 @@ +--- +title: Install Grafana on Raspberry Pi +summary: Get Grafana set up on your Raspberry Pi. +description: Get Grafana set up on your Raspberry Pi. +id: install-grafana-on-raspberry-pi +categories: ['administration'] +tags: ['beginner'] +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +--- + +## Introduction + +The Raspberry Pi is a tiny, affordable, yet capable computer that can run a range of different applications. Even Grafana! + +Many people are running Grafana on Raspberry Pi as a way to monitor their home, for things like indoor temperature, humidity, or energy usage. + +In this tutorial, you'll: + +- Set up a Raspberry Pi using a version of Raspberry Pi OS (previously called "Raspbian") that does not require you to connect a keyboard or monitor (this is often called "headless"). +- Install Grafana on your Raspberry Pi. + +{{% class "prerequisite-section" %}} + +### Prerequisites + +- Raspberry Pi +- SD card + {{% /class %}} + +## Set up your Raspberry Pi + +Before we can install Grafana, you first need to set up your Raspberry Pi. + +For this tutorial, you'll configure your Raspberry Pi to be _headless_. This means you don't need to connect a monitor, keyboard, or mouse to your Raspberry Pi. All configuration is done from your regular computer. + +#### Download and install Raspberry Pi Imager + +Before we get started, you need to download and install the [Raspberry Pi Imager](https://www.raspberrypi.org/software/). + +We'll use the Raspberry Pi Imager to flash the operating system image to the SD card. You download the imager directly from the official Raspberry Pi website and it's available for Ubuntu Linux, macOS, and Windows. + +Follow the directions on the website to download and install the imager. + +#### Install Raspberry Pi OS + +Now it is time to install Raspberry Pi OS. + +1. Insert the SD card into your regular computer from which you plan to install Raspberry Pi OS. +1. Run the Raspberry Pi Imager that you downloaded and installed. +1. To select an operating system, click **Choose OS** in the imager. You will be shown a list of available options. +1. From the list, select **Raspberry Pi OS (other)** and then select **Raspberry Pi OS Lite**, which is a Debian-based operating system for the Raspberry Pi. Since you're going to run a headless Raspberry Pi, you won't need the desktop dependencies. +1. To select where you want to put the operating system image, click **Choose Storage** in the imager and then select the SD card you already inserted into your computer. +1. The final step in the imager to click **Write**. When you do, the imager will write the Raspberry Pi OS Lite image to the SD card and verify that it has been written correctly. +1. Eject the SD card from your computer, and insert it again. + +While you _could_ fire up the Raspberry Pi now, we don't yet have any way of accessing it. + +1. Create an empty file called `ssh` in the boot directory. This enables SSH so that you can log in remotely. + + The next step is only required if you want the Raspberry Pi to connect to your wireless network. Otherwise, connect the it to your network by using a network cable. + +1. **(Optional)** Create a file called `wpa_supplicant.conf` in the boot directory: + + ``` + ctrl_interface=/var/run/wpa_supplicant + update_config=1 + country= + + network={ + ssid="" + psk="" + } + ``` + +All the necessary files are now on the SD card. Let's start up the Raspberry Pi. + +1. Eject the SD card and insert it into the SD card slot on the Raspberry Pi. +1. Connect the power cable and make sure the LED lights are on. +1. Find the IP address of the Raspberry Pi. Usually you can find the address in the control panel for your WiFi router. + +#### Connect remotely via SSH + +1. Open up your terminal and enter the following command: + ``` + ssh pi@ + ``` +1. SSH warns you that the authenticity of the host can't be established. Type "yes" to continue connecting. +1. When asked for a password, enter the default password: `raspberry`. +1. Once you're logged in, change the default password: + ``` + passwd + ``` + +Congratulations! You've now got a tiny Linux machine running that you can hide in a closet and access from your normal workstation. + +## Install Grafana + +Now that you've got the Raspberry Pi up and running, the next step is to install Grafana. + +1. Add the APT key used to authenticate packages: + + ``` + wget -q -O - https://packages.grafana.com/gpg.key | sudo apt-key add - + ``` + +1. Add the Grafana APT repository: + + ``` + echo "deb https://packages.grafana.com/oss/deb stable main" | sudo tee -a /etc/apt/sources.list.d/grafana.list + ``` + +1. Install Grafana: + ``` + sudo apt-get update + sudo apt-get install -y grafana + ``` + +Grafana is now installed, but not yet running. To make sure Grafana starts up even if the Raspberry Pi is restarted, we need to enable and start the Grafana Systemctl service. + +1. Enable the Grafana server: + + ``` + sudo /bin/systemctl enable grafana-server + ``` + +1. Start the Grafana server: + + ``` + sudo /bin/systemctl start grafana-server + ``` + + Grafana is now running on the machine and is accessible from any device on the local network. + +1. Open a browser and go to `http://:3000`, where the IP address is the address that you used to connect to the Raspberry Pi earlier. You're greeted with the Grafana login page. +1. Log in to Grafana with the default username `admin`, and the default password `admin`. +1. Change the password for the admin user when asked. + +Congratulations! Grafana is now running on your Raspberry Pi. If the Raspberry Pi is ever restarted or turned off, Grafana will start up whenever the machine regains power. + +## Summary + +If you want to use Grafana without having to go through a full installation process, check out [Grafana Cloud](/products/cloud/), which is designed to get users up and running quickly and easily. Grafana Cloud offers a forever free plan that is genuinely useful for hobbyists, testing, and small teams. + +### Learn more + +- [Raspberry Pi Documentation](https://www.raspberrypi.org/documentation/) diff --git a/docs/sources/tutorials/integrate-hubot/index.md b/docs/sources/tutorials/integrate-hubot/index.md new file mode 100644 index 00000000000..0d0af2f3821 --- /dev/null +++ b/docs/sources/tutorials/integrate-hubot/index.md @@ -0,0 +1,118 @@ +--- +title: Integrate Hubot with Grafana +summary: Learn how to integrate Hubot with Grafana +description: Learn how to integrate Hubot with Grafana +id: integrate-hubot +categories: ['administration'] +tags: ['advanced'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +aliases: ['/docs/grafana/latest/tutorials/hubot_howto/'] +--- + +# Integrate Hubot with Grafana + +Grafana 2.0 shipped with a great feature that enables it to render any graph or panel to a PNG image. + +No matter what data source you are using, the PNG image of the Graph will look the same as it does in your browser. + +This guide will show you how to install and configure the [Hubot-Grafana](https://github.com/stephenyeargin/hubot-grafana) plugin. This plugin allows you to tell hubot to render any dashboard or graph right from a channel in Slack, Hipchat or Basecamp. The bot will respond with an image of the graph and a link that will take you to the graph. + +> _Amazon S3 Required_: The hubot-grafana script will upload the rendered graphs to Amazon S3. This +> is so Hipchat and Slack can show them reliably (they require the image to be publicly available). + +{{< figure src="/static/img/docs/tutorials/hubot_grafana.png" max-width="800px" >}} + +## What is Hubot? + +[Hubot](https://hubot.github.com/) is an universal and extensible chat bot that can be used with many chat services and has a huge library of third party plugins that allow you to automate anything from your chat rooms. + +## Install Hubot + +Hubot is very easy to install and host. If you do not already have a bot up and running please read the official [Getting Started With Hubot](https://hubot.github.com/docs/) guide. + +## Install Hubot-Grafana script + +In your Hubot project repo install the Grafana plugin using `npm`: + +```bash +npm install hubot-grafana --save +``` + +Edit the file external-scripts.json, and add hubot-grafana to the list of plugins. + +```json +["hubot-pugme", "hubot-shipit", "hubot-grafana"] +``` + +## Configure + +The `hubot-grafana` plugin requires a number of environment variables to be set in order to work properly. + +```bash +export HUBOT_GRAFANA_HOST=https://play.grafana.org +export HUBOT_GRAFANA_API_KEY=abcd01234deadbeef01234 +export HUBOT_GRAFANA_S3_BUCKET=mybucket +export HUBOT_GRAFANA_S3_ACCESS_KEY_ID=ABCDEF123456XYZ +export HUBOT_GRAFANA_S3_SECRET_ACCESS_KEY=aBcD01234dEaDbEef01234 +export HUBOT_GRAFANA_S3_PREFIX=graphs +export HUBOT_GRAFANA_S3_REGION=us-standard +``` + +### Grafana server side rendering + +The hubot plugin will take advantage of the Grafana server side rendering feature that can render any panel on the server using phantomjs. Grafana ships with a phantomjs binary (Linux only). + +To verify that this feature works try the `Direct link to rendered image` link in the panel share dialog. If you do not get an image when opening this link verify that the required font packages are installed for phantomjs to work. + +### Grafana API Key + +{{< figure src="/static/img/docs/v2/orgdropdown_api_keys.png" max-width="150px" class="docs-image--right">}} + +You need to set the environment variable `HUBOT_GRAFANA_API_KEY` to a Grafana API Key. You can add these from the API Keys page which you find in the Organization dropdown. + +### Amazon S3 + +The `S3` options are optional but for the images to work properly in services like Slack and Hipchat they need to publicly available. By specifying the `S3` options the hubot-grafana script will publish the rendered panel to `S3` and it will use that URL when it posts to Slack or Hipchat. + +## Hubot commands + +- `hubot graf list` + - Lists the available dashboards +- `hubot graf db graphite-carbon-metrics` + - Graph all panels in the dashboard +- `hubot graf db graphite-carbon-metrics:3` + - Graph only panel with id 3 of a particular dashboard +- `hubot graf db graphite-carbon-metrics:cpu` + - Graph only the panels containing "cpu" (case insensitive) in the title +- `hubot graf db graphite-carbon-metrics now-12hr` + - Get a dashboard with a window of 12 hours ago to now +- `hubot graf db graphite-carbon-metrics now-24hr now-12hr` + - Get a dashboard with a window of 24 hours ago to 12 hours ago +- `hubot graf db graphite-carbon-metrics:3 now-8d now-1d` + - Get only the third panel of a particular dashboard with a window of 8 days ago to yesterday +- `hubot graf db graphite-carbon-metrics host=carbon-a` + - Get a templated dashboard with the `$host` parameter set to `carbon-a` + +## Aliases + +Some of the hubot commands above can lengthy and you might have to remember the dashboard slug (url id). If you have a few favorite graphs you want to be able check up on often (let's say from your mobile) you can create hubot command aliases with the hubot script `hubot-alias`. + +Install it: + +```bash +npm i --save hubot-alias +``` + +Now add `hubot-alias` to the list of plugins in `external-scripts.json` and restart hubot. + +Now you can add an alias like this: + +- `hubot alias graf-lb=graf db loadbalancers:2 now-20m` + +{{< figure src="/static/img/docs/tutorials/hubot_grafana2.png" max-width="800px" >}} + +## Summary + +Grafana is going to ship with integrated Slack and Hipchat features some day but you do not have to wait for that. Grafana 2 shipped with a very clever server side rendering feature that can render any panel to a png using phantomjs. The hubot plugin for Grafana is something you can install and use today! diff --git a/docs/sources/tutorials/provision-dashboards-and-data-sources/index.md b/docs/sources/tutorials/provision-dashboards-and-data-sources/index.md new file mode 100644 index 00000000000..4554b67073f --- /dev/null +++ b/docs/sources/tutorials/provision-dashboards-and-data-sources/index.md @@ -0,0 +1,260 @@ +--- +title: Provision dashboards and data sources +summary: Treat your configuration as code. +description: Treat your configuration as code. +id: provision-dashboards-and-data-sources +categories: ['administration'] +tags: ['intermediate'] +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +weight: 40 +--- + +## Introduction + +Learn how you can reuse dashboards and data sources across multiple teams by provisioning Grafana from version-controlled configuration files. + +In this tutorial, you'll: + +- Provision dashboards. +- Provision data sources. + +{{% class "prerequisite-section" %}} + +### Prerequisites + +- Grafana 7.0 +- Administrator privileges on the system you are doing the tutorial on + {{% /class %}} + +## Configuration as code + +Configuration as code is the practice of storing the configuration of your system as a set of version controlled, human-readable configuration files, rather than in a database. These configuration files can be reused across environments to avoid duplicated resources. + +As the number of dashboards and data sources grows within your organization, manually managing changes can become tedious and error-prone. Encouraging reuse becomes important to avoid multiple teams redesigning the same dashboards. + +Grafana supports configuration as code through _provisioning_. The resources that currently supports provisioning are: + +- [Dashboards](/docs/grafana/latest/administration/provisioning/#dashboards) +- [Data sources](/docs/grafana/latest/administration/provisioning/#datasources) +- [Alert notification channels](/docs/grafana/latest/administration/provisioning/#alert-notification-channels) + +## Set the provisioning directory + +Before you can start provisioning resources, Grafana needs to know where to find the _provisioning directory_. The provisioning directory contains configuration files that are applied whenever Grafana starts and continuously updated while running. + +By default, Grafana looks for a provisioning directory in the configuration directory (grafana > conf) on the system where Grafana is installed. However, if you are a Grafana Administrator, then you might want to place the config files in a shared resource like a network folder, so you would need to change the path to the provisioning directory. + +You can set a different path by setting the `paths.provisioning` property in the main config file: + +```ini +[paths] +provisioning = +``` + +For more information about configuration files, refer to [Configuration](/docs/grafana/latest/installation/configuration/) in the [Grafana documentation](/docs/grafana/latest/). + +The provisioning directory assumes the following structure: + +``` +provisioning/ + datasources/ + + dashboards/ + + notifiers/ + +``` + +Next, we'll look at how to provision a data source. + +## Provision a data source + +Each data source provisioning config file contains a _manifest_ that specifies the desired state of a set of provisioned data sources. + +At startup, Grafana loads the configuration files and provisions the data sources listed in the manifests. + +Let's configure a [TestData DB](/docs/grafana/latest/features/datasources/testdata/) data source that you can use for your dashboards. + +#### Create a data source manifest + +1. In the `provisioning/datasources/` directory, create a file called `default.yaml` with the following content: + + ```yaml + apiVersion: 1 + + datasources: + - name: TestData DB + type: testdata + ``` + +1. Restart Grafana to load the new changes. +1. In the sidebar, hover the cursor over the **Configuration** (gear) icon and click **Data Sources**. The TestData DB appears in the list of data sources. + +> The configuration options can vary between different types of data sources. For more information on how to configure a specific data source, refer to [Data sources](/docs/grafana/latest/administration/provisioning/#datasources). + +## Provision a dashboard + +Each dashboard config file contains a manifest that specifies the desired state of a set of _dashboard providers_. + +A dashboard provider tells Grafana where to find the dashboard definitions and where to put them. + +Grafana regularly checks for changes to the dashboard definitions (by default every 10 seconds). + +Let's define a dashboard provider so that Grafana knows where to find the dashboards we want to provision. + +#### Define a dashboard provider + +In the `provisioning/dashboards/` directory, create a file called `default.yaml` with the following content: + +```yaml +apiVersion: 1 + +providers: + - name: Default # A uniquely identifiable name for the provider + folder: Services # The folder where to place the dashboards + type: file + options: + path: + + # Default path for Windows: C:/Program Files/GrafanaLabs/grafana/public/dashboards + # Default path for Linux is: /var/lib/grafana/dashboards +``` + +For more information on how to configure dashboard providers, refer to [Dashboards](/docs/grafana/latest/administration/provisioning/#dashboards). + +#### Create a dashboard definition + +1. In the dashboard definitions directory you specified in the dashboard provider, i.e. `options.path`, create a file called `cluster.json` with the following content: + + ```json + { + "__inputs": [], + "__requires": [], + "annotations": { + "list": [] + }, + "editable": false, + "gnetId": null, + "graphTooltip": 0, + "hideControls": false, + "id": null, + "links": [], + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "TestData DB", + "fill": 1, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 2, + "legend": { + "alignAsTable": false, + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "repeat": null, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "CPU Usage", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + } + ], + "refresh": "", + "rows": [], + "schemaVersion": 16, + "style": "dark", + "tags": ["kubernetes"], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], + "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + }, + "timezone": "browser", + "title": "Cluster", + "version": 0 + } + ``` + +1. Restart Grafana to provision the new dashboard or wait 10 seconds for Grafana to automatically create the dashboard. +1. In the sidebar, hover the cursor over **Dashboards** (squares) icon, and then click **Manage**. The dashboard appears in a **Services** folder. + +> If you don't specify an `id` in the dashboard definition, then Grafana assigns one during provisioning. You can set the `id` yourself if you want to reference the dashboard from other dashboards. Be careful to not use the same `id` for multiple dashboards, as this will cause a conflict. + +## Summary + +In this tutorial you learned how you to reuse dashboards and data sources across multiple teams by provisioning Grafana from version-controlled configuration files. + +Dashboard definitions can get unwieldy as more panels and configurations are added to them. There are a number of open source tools available to make it easier to manage dashboard definitions: + +- [grafana-dash-gen](https://github.com/uber/grafana-dash-gen) (Javascript) +- [grafanalib](https://github.com/weaveworks/grafanalib) (Python) +- [grafonnet-lib](https://github.com/grafana/grafonnet-lib) (Jsonnet) +- [grafyaml](https://docs.openstack.org/infra/grafyaml/) (YAML) + +### Learn more + +- [Provisioning Grafana](/docs/grafana/latest/administration/provisioning/) diff --git a/docs/sources/tutorials/run-grafana-behind-a-proxy/index.md b/docs/sources/tutorials/run-grafana-behind-a-proxy/index.md new file mode 100644 index 00000000000..10be2dee35f --- /dev/null +++ b/docs/sources/tutorials/run-grafana-behind-a-proxy/index.md @@ -0,0 +1,222 @@ +--- +title: Run Grafana behind a reverse proxy +summary: Learn how to run Grafana behind a reverse proxy +description: Learn how to run Grafana behind a reverse proxy +id: run-grafana-behind-a-proxy +categories: ['administration'] +tags: ['advanced'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +aliases: ['/docs/grafana/latest/installation/behind_proxy/'] +--- + +## Introduction + +In this tutorial, you'll configure Grafana to run behind a reverse proxy. + +When running Grafana behind a proxy, you need to configure the domain name to let Grafana know how to render links and redirects correctly. + +- In the Grafana configuration file, change `server.domain` to the domain name you'll be using: + +```bash +[server] +domain = example.com +``` + +- Restart Grafana for the new changes to take effect. + +You can also serve Grafana behind a _sub path_, such as `http://example.com/grafana`. + +To serve Grafana behind a sub path: + +- Include the sub path at the end of the `root_url`. +- Set `serve_from_sub_path` to `true`. + +```bash +[server] +domain = example.com +root_url = %(protocol)s://%(domain)s:%(http_port)s/grafana/ +serve_from_sub_path = true +``` + +Next, you need to configure your reverse proxy. + +## Configure NGINX + +[NGINX](https://www.nginx.com) is a high performance load balancer, web server, and reverse proxy. + +- In your NGINX configuration file inside `http` section, add the following: + +```nginx +# this is required to proxy Grafana Live WebSocket connections. +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +upstream grafana { + server localhost:3000; +} + +server { + listen 80; + root /usr/share/nginx/html; + index index.html index.htm; + + location / { + proxy_set_header Host $http_host; + proxy_pass http://grafana; + } + + # Proxy Grafana Live WebSocket connections. + location /api/live/ { + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $http_host; + proxy_pass http://grafana; + } +} +``` + +- Reload the NGINX configuration. +- Navigate to port 80 on the machine NGINX is running on. You're greeted by the Grafana login page. + +For Grafana Live which uses WebSocket connections you may have to raise Nginx [worker_connections](https://nginx.org/en/docs/ngx_core_module.html#worker_connections) option which is 512 by default – which limits the number of possible concurrent connections with Grafana Live. + +Also, be aware that the above configuration will work only when the `proxy_pass` value for `location /` is a literal string. If you are using a variable here, [read this GitHub issue](https://github.com/grafana/grafana/issues/18299). You will need to add [an appropriate NGINX rewrite rule](https://www.nginx.com/blog/creating-nginx-rewrite-rules/). + +To configure NGINX to serve Grafana under a _sub path_, update the `location` block: + +```nginx +# this is required to proxy Grafana Live WebSocket connections. +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +upstream grafana { + server localhost:3000; +} + +server { + listen 80; + root /usr/share/nginx/www; + index index.html index.htm; + + location /grafana/ { + rewrite ^/grafana/(.*) /$1 break; + proxy_set_header Host $http_host; + proxy_pass http://grafana; + } + + # Proxy Grafana Live WebSocket connections. + location /grafana/api/live/ { + rewrite ^/grafana/(.*) /$1 break; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $http_host; + proxy_pass http://grafana; + } +} +``` + +## Configure HAProxy + +To configure HAProxy to serve Grafana under a _sub path_: + +```bash +frontend http-in + bind *:80 + use_backend grafana_backend if { path /grafana } or { path_beg /grafana/ } + +backend grafana_backend + # Requires haproxy >= 1.6 + http-request set-path %[path,regsub(^/grafana/?,/)] + + # Works for haproxy < 1.6 + # reqrep ^([^\ ]*\ /)grafana[/]?(.*) \1\2 + + server grafana localhost:3000 +``` + +## Configure IIS + +> IIS requires that the URL Rewrite module is installed. + +To configure IIS to serve Grafana under a _sub path_, create an Inbound Rule for the parent website in IIS Manager with the following settings: + +- pattern: `grafana(/)?(.*)` +- check the `Ignore case` checkbox +- rewrite URL set to `http://localhost:3000/{R:2}` +- check the `Append query string` checkbox +- check the `Stop processing of subsequent rules` checkbox + +This is the rewrite rule that is generated in the `web.config`: + +```xml + + + + + + + + +``` + +See the [tutorial on IIS URL Rewrites](/tutorials/iis/) for more in-depth instructions. + +## Configure Traefik + +[Traefik](https://traefik.io/traefik/) Cloud Native Reverse Proxy / Load Balancer / Edge Router + +Using the docker provider the following labels will configure the router and service for a domain or subdomain routing. + +```yaml +labels: + traefik.http.routers.grafana.rule: Host(`grafana.example.com`) + traefik.http.services.grafana.loadbalancer.server.port: 3000 +``` + +To deploy on a _sub path_ + +```yaml +labels: + traefik.http.routers.grafana.rule: Host(`example.com`) && PathPrefix(`/grafana`) + traefik.http.services.grafana.loadbalancer.server.port: 3000 +``` + +Examples using the file provider. + +```yaml +http: + routers: + grafana: + rule: Host(`grafana.example.com`) + service: grafana + services: + grafana: + loadBalancer: + servers: + - url: http://192.168.30.10:3000 +``` + +```yaml +http: + routers: + grafana: + rule: Host(`example.com`) && PathPrefix(`/grafana`) + service: grafana + services: + grafana: + loadBalancer: + servers: + - url: http://192.168.30.10:3000 +``` + +## Summary + +In this tutorial you learned how to run Grafana behind a reverse proxy. diff --git a/docs/sources/tutorials/stream-metrics-from-telegraf-to-grafana/index.md b/docs/sources/tutorials/stream-metrics-from-telegraf-to-grafana/index.md new file mode 100644 index 00000000000..822006b2fcf --- /dev/null +++ b/docs/sources/tutorials/stream-metrics-from-telegraf-to-grafana/index.md @@ -0,0 +1,101 @@ +--- +title: Stream metrics from Telegraf to Grafana +summary: Use Telegraf to stream live metrics to Grafana. +description: Use Telegraf to stream live metrics to Grafana. +id: stream-metrics-from-telegraf-to-grafana +categories: ['administration'] +tags: ['beginner'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +weight: 75 +--- + +## Introduction + +Grafana v8 introduced streaming capabilities – a way to push data to UI panels in near real-time. In this tutorial we show how Grafana real-time streaming capabilities can be used together with Telegraf to instantly display system measurements. + +In this tutorial, you'll: + +- Setup Telegraf and output measurements directly to Grafana time-series panel in near real-time + +{{% class "prerequisite-section" %}} + +#### Prerequisites + +- Grafana 8.0+ +- Telegraf + {{% /class %}} + +## Run Grafana and create admin token + +1. Run Grafana following [installation instructions](/docs/grafana/latest/installation/) for your operating system +1. Log in and go to Configuration -> API Keys +1. Press "Add API key" button and create a new API token with **Admin** role + +## Configure and run Telegraf + +Telegraf is a plugin-driven server agent for collecting and sending metrics and events from databases, systems, and IoT sensors. + +You can install it following [official installation instructions](https://docs.influxdata.com/telegraf/latest/introduction/installation/). + +In this tutorial we will be using Telegraf HTTP output plugin to send metrics in Influx format to Grafana. We can use a configuration like this: + +``` +[agent] + interval = "1s" + flush_interval = "1s" + +[[inputs.cpu]] + percpu = false + totalcpu = true + +[[outputs.http]] + url = "http://localhost:3000/api/live/push/custom_stream_id" + data_format = "influx" + [outputs.http.headers] + Authorization = "Bearer " +``` + +Make sure to replace `` placeholder with your actual API key created in the previous step. Save this config into `telegraf.conf` file and run Telegraf pointing to this config file. Telegraf will periodically (once in a second) report the state of total CPU usage on a host to Grafana (which is supposed to be running on `http://localhost:3000`). Of course you can replace `custom_stream_id` to something more meaningful for your use case. + +Inside Grafana Influx data is converted to Grafana data frames and then frames are published to Grafana Live channels. In this case, the channel where CPU data will be published is `stream/custom_stream_id/cpu`. The `stream` scope is constant, the `custom_stream_id` namespace is the last part of API URL set in Telegraf configuration (`http://localhost:3000/api/live/push/telegraf`) and the path is `cpu` - the name of a measurement. + +The only thing left here is to create a dashboard with streaming data. + +## Create dashboard with streaming data + +1. Create new dashboard +1. Press Add empty panel +1. Select `-- Grafana --` datasource +1. Select `Live Measurements` query type +1. Find and select `stream/custom_stream_id/cpu` measurement for Channel field +1. Save dashboard changes + +After making these steps Grafana UI should subscribe to the channel `stream/custom_stream_id/cpu` and you should see CPU data updates coming from Telegraf in near real-time. + +## Stream using WebSocket endpoint + +If you aim for a high-frequency update sending then you may want to use the WebSocket output plugin of Telegraf (introduced in Telegraf v1.19.0) instead of the HTTP output plugin we used above. Configure WebSocket output plugin like this: + +``` +[agent] + interval = "500ms" + flush_interval = "500ms" + +[[inputs.cpu]] + percpu = false + totalcpu = true + +[[outputs.websocket]] + url = "ws://localhost:3000/api/live/push/custom_stream_id" + data_format = "influx" + [outputs.websocket.headers] + Authorization = "Bearer " +``` + +WebSocket avoids running all Grafana HTTP middleware on each request from Telegraf thus reducing Grafana backend CPU usage significantly. + +## Summary + +In this tutorial you learned how to use Telegraf to stream live metrics to Grafana. From 5ff94e528b1f983eeb5172fab85e29490a0df28a Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Fri, 27 Jan 2023 09:20:55 +0000 Subject: [PATCH 010/117] API: don't re-add /api suffix to grafana.com API URL (#62280) The old GrafanaComURL setting didn't have the /api suffix so needed it adding on by the proxy director, but the new GrafanaComAPIURL setting is assumed to already point directly to the API and doesn't need an additional suffix. This is the only place in the codebase that GrafanaComAPIURL is used. --- pkg/api/grafana_com_proxy.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/api/grafana_com_proxy.go b/pkg/api/grafana_com_proxy.go index a00a0d4a80d..088b745b82f 100644 --- a/pkg/api/grafana_com_proxy.go +++ b/pkg/api/grafana_com_proxy.go @@ -23,15 +23,15 @@ var grafanaComProxyTransport = &http.Transport{ TLSHandshakeTimeout: 10 * time.Second, } -func ReverseProxyGnetReq(logger log.Logger, proxyPath string, version string, grafanaComUrl string) *httputil.ReverseProxy { - url, _ := url.Parse(grafanaComUrl) +func ReverseProxyGnetReq(logger log.Logger, proxyPath string, version string, grafanaComAPIUrl string) *httputil.ReverseProxy { + url, _ := url.Parse(grafanaComAPIUrl) director := func(req *http.Request) { req.URL.Scheme = url.Scheme req.URL.Host = url.Host req.Host = url.Host - req.URL.Path = util.JoinURLFragments(url.Path+"/api", proxyPath) + req.URL.Path = util.JoinURLFragments(url.Path, proxyPath) // clear cookie headers req.Header.Del("Cookie") From a54d18c1f51c333ead542f2450b77fa4a8e79ae4 Mon Sep 17 00:00:00 2001 From: Jack Baldry Date: Fri, 27 Jan 2023 09:25:57 +0000 Subject: [PATCH 011/117] Revert "Add Grafana tutorials originally from tutorials repository" (#62283) Revert "Add Grafana tutorials originally from tutorials repository (#62124)" This reverts commit f98ad926acb49ab718aa1c0daf7de426b5da925f. --- .../sources/shared/tutorials/create-plugin.md | 40 -- .../shared/tutorials/plugin-anatomy.md | 29 -- .../shared/tutorials/publish-your-plugin.md | 77 ---- .../shared/tutorials/set-up-environment.md | 34 -- docs/sources/tutorials/_index.md | 9 - .../index.md | 180 --------- .../build-a-data-source-plugin/index.md | 372 ------------------ .../build-a-panel-plugin-with-d3/index.md | 235 ----------- .../tutorials/build-a-panel-plugin/index.md | 259 ------------ .../index.md | 164 -------- .../tutorials/build-an-app-plugin/index.md | 208 ---------- .../create-alerts-from-flux-queries/index.md | 331 ---------------- .../tutorials/create-users-and-teams/index.md | 236 ----------- .../tutorials/grafana-fundamentals/index.md | 354 ----------------- docs/sources/tutorials/iis/index.md | 146 ------- .../install-grafana-on-raspberry-pi/index.md | 147 ------- .../tutorials/integrate-hubot/index.md | 118 ------ .../index.md | 260 ------------ .../run-grafana-behind-a-proxy/index.md | 222 ----------- .../index.md | 101 ----- 20 files changed, 3522 deletions(-) delete mode 100755 docs/sources/shared/tutorials/create-plugin.md delete mode 100644 docs/sources/shared/tutorials/plugin-anatomy.md delete mode 100644 docs/sources/shared/tutorials/publish-your-plugin.md delete mode 100644 docs/sources/shared/tutorials/set-up-environment.md delete mode 100644 docs/sources/tutorials/_index.md delete mode 100644 docs/sources/tutorials/build-a-data-source-backend-plugin/index.md delete mode 100644 docs/sources/tutorials/build-a-data-source-plugin/index.md delete mode 100644 docs/sources/tutorials/build-a-panel-plugin-with-d3/index.md delete mode 100644 docs/sources/tutorials/build-a-panel-plugin/index.md delete mode 100644 docs/sources/tutorials/build-a-streaming-data-source-plugin/index.md delete mode 100644 docs/sources/tutorials/build-an-app-plugin/index.md delete mode 100644 docs/sources/tutorials/create-alerts-from-flux-queries/index.md delete mode 100644 docs/sources/tutorials/create-users-and-teams/index.md delete mode 100644 docs/sources/tutorials/grafana-fundamentals/index.md delete mode 100644 docs/sources/tutorials/iis/index.md delete mode 100644 docs/sources/tutorials/install-grafana-on-raspberry-pi/index.md delete mode 100644 docs/sources/tutorials/integrate-hubot/index.md delete mode 100644 docs/sources/tutorials/provision-dashboards-and-data-sources/index.md delete mode 100644 docs/sources/tutorials/run-grafana-behind-a-proxy/index.md delete mode 100644 docs/sources/tutorials/stream-metrics-from-telegraf-to-grafana/index.md diff --git a/docs/sources/shared/tutorials/create-plugin.md b/docs/sources/shared/tutorials/create-plugin.md deleted file mode 100755 index 656bce3cf9e..00000000000 --- a/docs/sources/shared/tutorials/create-plugin.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -title: Create Plugin ---- - -Tooling for modern web development can be tricky to wrap your head around. While you certainly can write your own webpack configuration, for this guide, you'll be using grafana create-plugin tool - -Grafana [create-plugin tool](https://www.npmjs.com/package/@grafana/create-plugin) is a CLI application that simplifies Grafana plugin development, so that you can focus on code. The tool scaffolds a starter plugin and all the required configuration for you. - -1. In the plugin directory, create a plugin from template using create-plugin: - - ``` - npx @grafana/create-plugin - ``` - -1. Change directory to your newly created plugin: - - ``` - cd my-plugin - ``` - -1. Install the dependencies: - - ``` - yarn install - ``` - -1. Build the plugin: - - ``` - yarn dev - ``` - -1. Restart the Grafana server for Grafana to discover your plugin. -1. Open Grafana and go to **Configuration** -> **Plugins**. Make sure that your plugin is there. - -By default, Grafana logs whenever it discovers a plugin: - -``` -INFO[01-01|12:00:00] Registering plugin logger=plugins name=my-plugin -``` diff --git a/docs/sources/shared/tutorials/plugin-anatomy.md b/docs/sources/shared/tutorials/plugin-anatomy.md deleted file mode 100644 index 4d6e2c4ead0..00000000000 --- a/docs/sources/shared/tutorials/plugin-anatomy.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: Plugin Anatomy ---- - -Plugins come in different shapes and sizes. Before we dive deeper, let's look at some of the properties that are shared by all of them. - -Every plugin you create will require at least two files: `plugin.json` and `module.ts`. - -### plugin.json - -When Grafana starts, it scans the plugin directory for any subdirectory that contains a `plugin.json` file. The `plugin.json` file contains information about your plugin, and tells Grafana about what capabilities and dependencies your plugin needs. - -While certain plugin types can have specific configuration options, let's look at the mandatory ones: - -- `type` tells Grafana what type of plugin to expect. Grafana supports three types of plugins: `panel`, `datasource`, and `app`. -- `name` is what users will see in the list of plugins. If you're creating a data source, this is typically the name of the database it connects to, such as Prometheus, PostgreSQL, or Stackdriver. -- `id` uniquely identifies your plugin, and should start with your Grafana username, to avoid clashing with other plugins. [Sign up for a Grafana account](/signup/) to claim your username. - -To see all the available configuration settings for the `plugin.json`, refer to the [plugin.json Schema](/docs/grafana/latest/plugins/developing/plugin.json/). - -### module.ts - -After discovering your plugin, Grafana loads the `module.ts` file, the entrypoint for your plugin. `module.ts` exposes the implementation of your plugin, which depends on the type of plugin you're building. - -Specifically, `module.ts` needs to expose an object that extends [GrafanaPlugin](https://github.com/grafana/grafana/blob/08bf2a54523526a7f59f7c6a8dafaace79ab87db/packages/grafana-data/src/types/plugin.ts#L124), and can be any of the following: - -- [PanelPlugin](https://github.com/grafana/grafana/blob/08bf2a54523526a7f59f7c6a8dafaace79ab87db/packages/grafana-data/src/types/panel.ts#L73) -- [DataSourcePlugin](https://github.com/grafana/grafana/blob/08bf2a54523526a7f59f7c6a8dafaace79ab87db/packages/grafana-data/src/types/datasource.ts#L33) -- [AppPlugin](https://github.com/grafana/grafana/blob/45b7de1910819ad0faa7a8aeac2481e675870ad9/packages/grafana-data/src/types/app.ts#L27) diff --git a/docs/sources/shared/tutorials/publish-your-plugin.md b/docs/sources/shared/tutorials/publish-your-plugin.md deleted file mode 100644 index 097f2417608..00000000000 --- a/docs/sources/shared/tutorials/publish-your-plugin.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -title: Package your plugin ---- - -Once you're happy with your plugin, it's time to package it, and submit to the plugin repository. - -For users to be able to use the plugin without building it themselves, you need to make a production build of the plugin, and commit to a release branch in your repository. - -To submit a plugin to the plugin repository, you need to create a release of your plugin. While we recommend following the branching strategy outlined below, you're free to use one that makes more sense to you. - -#### Create a plugin release - -Let's create version 0.1.0 of our plugin. - -1. Create a branch called `release-0.1.x`. - - ``` - git checkout -b release-0.1.x - ``` - -1. Do a production build. - - ``` - yarn build - ``` - -1. Add the `dist` directory. - - ``` - git add -f dist - ``` - -1. Create the release commit. - - ``` - git commit -m "Release v0.1.0" - ``` - -1. Create a release tag. - - ``` - git tag -a v0.1.0 -m "Create release tag v0.1.0" - ``` - -1. Push to GitHub. `follow-tags` tells Git to push the release tag along with our release branch. - ``` - git push --set-upstream origin release-0.1.x --follow-tags - ``` - -#### Submit the plugin - -For a plugin to be published on [Grafana Plugins](/grafana/plugins/), it needs to be added to the [grafana-plugin-repository](https://github.com/grafana/grafana-plugin-repository). - -1. Fork the [grafana-plugin-repository](https://github.com/grafana/grafana-plugin-repository) - -1. Add your plugin to the `repo.json` file in the project root directory: - - ```json - { - "id": "", - "type": "", - "url": "https://github.com//my-plugin", - "versions": [ - { - "version": "", - "commit": "", - "url": "https://github.com//my-plugin" - } - ] - } - ``` - -1. [Create a pull request](https://github.com/grafana/grafana-plugin-repository/pull/new/master). - -Once your plugin has been accepted, it'll be published on [Grafana Plugin](/grafana/plugins/), available for anyone to [install](/docs/grafana/latest/plugins/installation/)! - -> We're auditing every plugin that's added to make sure it's ready to be published. This means that it might take some time before your plugin is accepted. We're working on adding more automated tests to improve this process. diff --git a/docs/sources/shared/tutorials/set-up-environment.md b/docs/sources/shared/tutorials/set-up-environment.md deleted file mode 100644 index 07eb372727a..00000000000 --- a/docs/sources/shared/tutorials/set-up-environment.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: Set up Environment ---- - -Before you can get started building plugins, you need to set up your environment for plugin development. - -To discover plugins, Grafana scans a _plugin directory_, the location of which depends on your operating system. - -1. Create a directory called `grafana-plugins` in your preferred workspace. - -1. Find the `plugins` property in the Grafana configuration file and set the `plugins` property to the path of your `grafana-plugins` directory. Refer to the [Grafana configuration documentation](/docs/grafana/latest/installation/configuration/#plugins) for more information. - - ```ini - [paths] - plugins = "/path/to/grafana-plugins" - ``` - -1. Restart Grafana if it's already running, to load the new configuration. - -### Alternative method: Docker - -If you don't want to install Grafana on your local machine, you can use [Docker](https://www.docker.com). - -To set up Grafana for plugin development using Docker, run the following command: - -``` -docker run -d -p 3000:3000 -v "$(pwd)"/grafana-plugins:/var/lib/grafana/plugins --name=grafana grafana/grafana:7.0.0 -``` - -Since Grafana only loads plugins on start-up, you need to restart the container whenever you add or remove a plugin. - -``` -docker restart grafana -``` diff --git a/docs/sources/tutorials/_index.md b/docs/sources/tutorials/_index.md deleted file mode 100644 index cfe6c7633df..00000000000 --- a/docs/sources/tutorials/_index.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: 'Tutorials' -menuTitle: 'Tutorials' -description: 'Grafana tutorials' ---- - -# Tutorials - -{{< section >}} diff --git a/docs/sources/tutorials/build-a-data-source-backend-plugin/index.md b/docs/sources/tutorials/build-a-data-source-backend-plugin/index.md deleted file mode 100644 index 9b58af7f84f..00000000000 --- a/docs/sources/tutorials/build-a-data-source-backend-plugin/index.md +++ /dev/null @@ -1,180 +0,0 @@ ---- -title: Build a data source backend plugin -summary: Create a backend for your data source plugin. -description: Create a backend for your data source plugin. -id: build-a-data-source-backend-plugin -categories: ['plugins'] -tags: ['beginner'] -status: Published -authors: ['grafana_labs'] -Feedback Link: https://github.com/grafana/tutorials/issues/new -weight: 75 ---- - -## Introduction - -Grafana supports a wide range of data sources, including Prometheus, MySQL, and even Datadog. There's a good chance you can already visualize metrics from the systems you have set up. In some cases, though, you already have an in-house metrics solution that you’d like to add to your Grafana dashboards. This tutorial teaches you to build a support for your data source. - -For more information about backend plugins, refer to the documentation on [Backend plugins](/docs/grafana/latest/developers/plugins/backend/). - -In this tutorial, you'll: - -- Build a backend for your data source -- Implement a health check for your data source -- Enable Grafana Alerting for your data source - -{{% class "prerequisite-section" %}} - -#### Prerequisites - -- Knowledge about how data sources are implemented in the frontend. -- Grafana 7.0 -- Go ([Version](https://github.com/grafana/plugin-tools/blob/main/packages/create-plugin/templates/backend/go.mod#L3)) -- [Mage](https://magefile.org/) -- NodeJS ([Version](https://github.com/grafana/plugin-tools/blob/main/packages/create-plugin/templates/common/package.json#L66)) -- yarn - {{% /class %}} - -## Set up your environment - -{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" >}} - -## Create a new plugin - -To build a backend for your data source plugin, Grafana requires a binary that it can execute when it loads the plugin during start-up. In this guide, we will build a binary using the [Grafana plugin SDK for Go](/docs/grafana/latest/developers/plugins/backend/grafana-plugin-sdk-for-go/). - -The easiest way to get started is to use the Grafana [create-plugin tool](https://www.npmjs.com/package/@grafana/create-plugin). Navigate to the plugin folder that you configured in step 1 and type: - -``` -npx @grafana/create-plugin -``` - -Follow the steps and select **datasource** as your plugin type and answer **yes** when prompted to create a backend for your plugin. - -```bash -cd my-plugin -``` - -Install frontend dependencies and build frontend parts of the plugin to _dist_ directory: - -```bash -yarn install -yarn build -``` - -Run the following to update [Grafana plugin SDK for Go](/docs/grafana/latest/developers/plugins/backend/grafana-plugin-sdk-for-go/) dependency to the latest minor version: - -```bash -go get -u github.com/grafana/grafana-plugin-sdk-go -go mod tidy -``` - -Build backend plugin binaries for Linux, Windows and Darwin to _dist_ directory: - -```bash -mage -v -``` - -Now, let's verify that the plugin you've built so far can be used in Grafana when creating a new data source: - -1. Restart your Grafana instance. -1. Open Grafana in your web browser. -1. Navigate via the side-menu to **Configuration** -> **Data Sources**. -1. Click **Add data source**. -1. Find your newly created plugin and select it. -1. Enter a name and then click **Save & Test** (ignore any errors reported for now). - -You now have a new data source instance of your plugin that is ready to use in a dashboard: - -1. Navigate via the side-menu to **Create** -> **Dashboard**. -1. Click **Add new panel**. -1. In the query tab, select the data source you just created. -1. A line graph is rendered with one series consisting of two data points. -1. Save the dashboard. - -### Troubleshooting - -#### Grafana doesn't load my plugin - -By default, Grafana requires backend plugins to be signed. To load unsigned backend plugins, you need to -configure Grafana to [allow unsigned plugins](/docs/grafana/latest/plugins/plugin-signature-verification/#allow-unsigned-plugins). -For more information, refer to [Plugin signature verification](/docs/grafana/latest/plugins/plugin-signature-verification/#backend-plugins). - -## Anatomy of a backend plugin - -The folders and files used to build the backend for the data source are: - -| file/folder | description | -| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -| `Magefile.go` | It’s not a requirement to use mage build files, but we strongly recommend using it so that you can use the build targets provided by the plugin SDK. | -| `/go.mod ` | Go modules dependencies, [reference](https://golang.org/cmd/go/#hdr-The_go_mod_file) | -| `/src/plugin.json` | A JSON file describing the backend plugin | -| `/pkg/main.go` | Starting point of the plugin binary. | - -#### plugin.json - -The [plugin.json](/docs/grafana/latest/developers/plugins/metadata/) file is required for all plugins. When building a backend plugin these properties are important: - -| property | description | -| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| backend | Should be set to `true` for backend plugins. This tells Grafana that it should start a binary when loading the plugin. | -| executable | This is the name of the executable that Grafana expects to start, see [plugin.json reference](/docs/grafana/latest/developers/plugins/metadata/) for details. | -| alerting | Should be set to `true` if your backend datasource supports alerting. | - -In the next step we will look at the query endpoint! - -## Implement data queries - -We begin by opening the file `/pkg/plugin/plugin.go`. In this file you will see the `SampleDatasource` struct which implements the [backend.QueryDataHandler](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/backend?tab=doc#QueryDataHandler) interface. The `QueryData` method on this struct is where the data fetching happens for a data source plugin. - -Each request contains multiple queries to reduce traffic between Grafana and plugins. So you need to loop over the slice of queries, process each query, and then return the results of all queries. - -In the tutorial we have extracted a method named `query` to take care of each query model. Since each plugin has their own unique query model, Grafana sends it to the backend plugin as JSON. Therefore the plugin needs to `Unmarshal` the query model into something easier to work with. - -As you can see the sample only returns static numbers. Try to extend the plugin to return other types of data. - -You can read more about how to [build data frames in our docs](/docs/grafana/latest/developers/plugins/data-frames/). - -## Add support for health checks - -Implementing the health check handler allows Grafana to verify that a data source has been configured correctly. - -When editing a data source in Grafana's UI, you can **Save & Test** to verify that it works as expected. - -In this sample data source, there is a 50% chance that the health check will be successful. Make sure to return appropriate error messages to the users, informing them about what is misconfigured in the data source. - -Open `/pkg/plugin/plugin.go`. In this file you'll see that the `SampleDatasource` struct also implements the [backend.CheckHealthHandler](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/backend?tab=doc#CheckHealthHandler) interface. Navigate to the `CheckHealth` method to see how the health check for this sample plugin is implemented. - -## Enable Grafana Alerting - -1. Open _src/plugin.json_. -1. Add the top level `backend` property with a value of `true` to specify that your plugin supports Grafana Alerting, e.g. - ```json - { - ... - "backend": true, - "executable": "gpx_simple_datasource_backend", - "alerting": true, - "info": { - ... - } - ``` -1. Rebuild frontend parts of the plugin to _dist_ directory: - -```bash -yarn build -``` - -1. Restart your Grafana instance. -1. Open Grafana in your web browser. -1. Open the dashboard you created earlier in the _Create a new plugin_ step. -1. Edit the existing panel. -1. Click on the _Alert_ tab. -1. Click on _Create Alert_ button. -1. Edit condition and specify _IS ABOVE 10_. Change _Evaluate every_ to _10s_ and clear the _For_ field to make the alert rule evaluate quickly. -1. Save the dashboard. -1. After some time the alert rule evaluates and transitions into _Alerting_ state. - -## Summary - -In this tutorial you created a backend for your data source plugin. diff --git a/docs/sources/tutorials/build-a-data-source-plugin/index.md b/docs/sources/tutorials/build-a-data-source-plugin/index.md deleted file mode 100644 index ce1b81c6e7c..00000000000 --- a/docs/sources/tutorials/build-a-data-source-plugin/index.md +++ /dev/null @@ -1,372 +0,0 @@ ---- -title: Build a data source plugin -summary: Create a plugin to add support for your own data sources. -description: Create a plugin to add support for your own data sources. -id: build-a-data-source-plugin -categories: ['plugins'] -tags: ['beginner'] -status: Published -authors: ['grafana_labs'] -Feedback Link: https://github.com/grafana/tutorials/issues/new -weight: 70 ---- - -## Introduction - -Grafana supports a wide range of data sources, including Prometheus, MySQL, and even Datadog. There's a good chance you can already visualize metrics from the systems you have set up. In some cases, though, you already have an in-house metrics solution that you’d like to add to your Grafana dashboards. This tutorial teaches you to build a support for your data source. - -In this tutorial, you'll: - -- Build a data source to visualize a sine wave -- Construct queries using the query editor -- Configure your data source using the config editor - -{{% class "prerequisite-section" %}} - -### Prerequisites - -- Grafana >=7.0 -- NodeJS >=14 -- yarn - {{% /class %}} - -## Set up your environment - -{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" >}} - -## Create a new plugin - -{{< docs/shared lookup="tutorials/create-plugin.md" source="grafana" >}} - -## Anatomy of a plugin - -{{< docs/shared lookup="tutorials/plugin-anatomy.md" source="grafana" >}} - -## Data source plugins - -A data source in Grafana must extend the `DataSourceApi` interface, which requires you to defines two methods: `query` and `testDatasource`. - -### The `query` method - -The `query` method is the heart of any data source plugin. It accepts a query from the user, retrieves the data from an external database, and returns the data in a format that Grafana recognizes. - -``` -async query(options: DataQueryRequest): Promise -``` - -The `options` object contains the queries, or _targets_, that the user made, along with context information, like the current time interval. Use this information to query an external database. - -> The term _target_ originates from Graphite, and the earlier days of Grafana when Graphite was the only supported data source. As Grafana gained support for more data sources, the term "target" became synonymous with any type of query. - -### Test your data source - -`testDatasource` implements a health check for your data source. For example, Grafana calls this method whenever the user clicks the **Save & Test** button, after changing the connection settings. - -``` -async testDatasource() -``` - -## Data frames - -Nowadays there are countless of different databases, each with their own ways of querying data. To be able to support all the different data formats, Grafana consolidates the data into a unified data structure called _data frames_. - -Let's see how to create and return a data frame from the `query` method. In this step, you'll change the code in the starter plugin to return a [sine wave](https://en.wikipedia.org/wiki/Sine_wave). - -1. In the current `query` method, remove the code inside the `map` function. - - The `query` method now look like this: - - ```ts - async query(options: DataQueryRequest): Promise { - const { range } = options; - const from = range!.from.valueOf(); - const to = range!.to.valueOf(); - - const data = options.targets.map(target => { - // Your code goes here. - }); - - return { data }; - } - ``` - -1. In the `map` function, use the `lodash/defaults` package to set default values for query properties that haven't been set: - - ```ts - const query = defaults(target, defaultQuery); - ``` - -1. Create a data frame with a time field and a number field: - - ```ts - const frame = new MutableDataFrame({ - refId: query.refId, - fields: [ - { name: 'time', type: FieldType.time }, - { name: 'value', type: FieldType.number }, - ], - }); - ``` - - `refId` needs to be set to tell Grafana which query that generated this date frame. - -Next, we'll add the actual values to the data frame. Don't worry about the math used to calculate the values. - -1. Create a couple of helper variables: - - ```ts - // duration of the time range, in milliseconds. - const duration = to - from; - - // step determines how close in time (ms) the points will be to each other. - const step = duration / 1000; - ``` - -1. Add the values to the data frame: - - ```ts - for (let t = 0; t < duration; t += step) { - frame.add({ time: from + t, value: Math.sin((2 * Math.PI * t) / duration) }); - } - ``` - - The `frame.add()` accepts an object where the keys corresponds to the name of each field in the data frame. - -1. Return the data frame: - - ```ts - return frame; - ``` - -1. Rebuild the plugin and try it out. - -Your data source is now sending data frames that Grafana can visualize. Next, we'll look at how you can control the frequency of the sine wave by defining a _query_. - -> In this example, we're generating timestamps from the current time range. This means that you'll get the same graph no matter what time range you're using. In practice, you'd instead use the timestamps returned by your database. - -## Define a query - -Most data sources offer a way to query specific data. MySQL and PostgreSQL use SQL, while Prometheus has its own query language, called _PromQL_. No matter what query language your databases are using, Grafana lets you build support for it. - -Add support for custom queries to your data source, by implementing your own _query editor_, a React component that enables users to build their own queries, through a user-friendly graphical interface. - -A query editor can be as simple as a text field where the user edits the raw query text, or it can provide a more user-friendly form with drop-down menus and switches, that later gets converted into the raw query text before it gets sent off to the database. - -### Define the query model - -The first step in designing your query editor is to define its _query model_. The query model defines the user input to your data source. - -We want to be able to control the frequency of the sine wave, so let's add another property. - -1. Add a new number property called `frequency` to the query model: - - **src/types.ts** - - ```ts - export interface MyQuery extends DataQuery { - queryText?: string; - constant: number; - frequency: number; - } - ``` - -1. Set a default value to the new `frequency` property: - - ```ts - export const defaultQuery: Partial = { - constant: 6.5, - frequency: 1.0, - }; - ``` - -### Bind the model to a form - -Now that you've defined the query model you wish to support, the next step is to bind the model to a form. The `FormField` is a text field component from `grafana/ui` that lets you register a listener which will be invoked whenever the form field value changes. - -1. Add a new form field to the query editor to control the new frequency property. - - **QueryEditor.tsx** - - ```ts - const { queryText, constant, frequency } = query; - ``` - - ```ts - - ``` - -1. Add a event listener for the new property. - - ```ts - onFrequencyChange = (event: ChangeEvent) => { - const { onChange, query, onRunQuery } = this.props; - onChange({ ...query, frequency: parseFloat(event.target.value) }); - // executes the query - onRunQuery(); - }; - ``` - - The registered listener, `onFrequencyChange`, calls `onChange` to update the current query with the value from the form field. - - `onRunQuery();` tells Grafana to run the query after each change. For fast queries, this is recommended to provide a more responsive experience. - -### Use the property - -The new query model is now ready to use in our `query` method. - -1. In the `query` method, use the `frequency` property to adjust our equation. - - ```ts - frame.add({ time: from + t, value: Math.sin((2 * Math.PI * query.frequency * t) / duration) }); - ``` - -## Configure your data source - -To access a specific data source, you often need to configure things like hostname, credentials, or authentication method. A _config editor_ lets your users configure your data source plugin to fit their needs. - -The config editor looks similar to the query editor, in that it defines a model and binds it to a form. - -Since we're not actually connecting to an external database in our sine wave example, we don't really need many options. To show you how you can add an option however, we're going to add the _wave resolution_ as an option. - -The resolution controls how close in time the data points are to each other. A higher resolution means more points closer together, at the cost of more data being processed. - -### Define the options model - -1. Add a new number property called `resolution` to the options model. - - **types.ts** - - ```ts - export interface MyDataSourceOptions extends DataSourceJsonData { - path?: string; - resolution?: number; - } - ``` - -### Bind the model to a form - -Just like query editor, the form field in the config editor calls the registered listener whenever the value changes. - -1. Add a new form field to the query editor to control the new resolution option. - - **ConfigEditor.tsx** - - ```ts -
- -
- ``` - -1. Add a event listener for the new option. - - ```ts - onResolutionChange = (event: ChangeEvent) => { - const { onOptionsChange, options } = this.props; - const jsonData = { - ...options.jsonData, - resolution: parseFloat(event.target.value), - }; - onOptionsChange({ ...options, jsonData }); - }; - ``` - - The `onResolutionChange` listener calls `onOptionsChange` to update the current options with the value from the form field. - -### Use the option - -1. Create a property called `resolution` to the `DataSource` class. - - ```ts - export class DataSource extends DataSourceApi { - resolution: number; - - constructor(instanceSettings: DataSourceInstanceSettings) { - super(instanceSettings); - - this.resolution = instanceSettings.jsonData.resolution || 1000.0; - } - - // ... - ``` - -1. In the `query` method, use the `resolution` property to calculate the step size. - - **src/DataSource.ts** - - ```ts - const step = duration / this.resolution; - ``` - -## Get data from an external API - -So far, you've generated the data returned by the data source. A more realistic use case would be to fetch data from an external API. - -While you can use something like [axios](https://github.com/axios/axios) or the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to make requests, we recommend using the [`getBackendSrv`](/docs/grafana/latest/packages_api/runtime/getbackendsrv/) function from the [grafana/runtime](/docs/grafana/latest/packages_api/runtime/) package. - -The main advantage of `getBackendSrv` is that it proxies requests through the Grafana server rather making the request from the browser. This is strongly recommended when making authenticated requests to an external API. For more information on authenticating external requests, refer to [Add authentication for data source plugins](/docs/grafana/latest/developers/plugins/add-authentication-for-data-source-plugins/). - -1. Import `getBackendSrv`. - - **src/DataSource.ts** - - ```ts - import { getBackendSrv } from '@grafana/runtime'; - ``` - -1. Create a helper method `doRequest` and use the `datasourceRequest` method to make a request to your API. Replace `https://api.example.com/metrics` to point to your own API endpoint. - - ```ts - async doRequest(query: MyQuery) { - const result = await getBackendSrv().datasourceRequest({ - method: "GET", - url: "https://api.example.com/metrics", - params: query, - }) - - return result; - } - ``` - -1. Make a request for each query. `Promises.all` waits for all requests to finish before returning the data. - - ```ts - async query(options: DataQueryRequest): Promise { - const promises = options.targets.map((query) => - this.doRequest(query).then((response) => { - const frame = new MutableDataFrame({ - refId: query.refId, - fields: [ - { name: "Time", type: FieldType.time }, - { name: "Value", type: FieldType.number }, - ], - }); - - response.data.forEach((point: any) => { - frame.appendRow([point.time, point.value]); - }); - - return frame; - }) - ); - - return Promise.all(promises).then((data) => ({ data })); - } - ``` - -## Summary - -In this tutorial you built a complete data source plugin for Grafana that uses a query editor to control what data to visualize. You've added a data source option, commonly used to set connection options and more. - -### Learn more - -Learn how you can improve your plugin even further, by reading our advanced guides: - -- [Add support for variables](/docs/grafana/latest/developers/plugins/add-support-for-variables/) -- [Add support for annotations](/docs/grafana/latest/developers/plugins/add-support-for-annotations/) -- [Add support for Explore queries](/docs/grafana/latest/developers/plugins/add-support-for-explore-queries/) -- [Build a logs data source](/docs/grafana/latest/developers/plugins/build-a-logs-data-source-plugin/) diff --git a/docs/sources/tutorials/build-a-panel-plugin-with-d3/index.md b/docs/sources/tutorials/build-a-panel-plugin-with-d3/index.md deleted file mode 100644 index b5869acbbc0..00000000000 --- a/docs/sources/tutorials/build-a-panel-plugin-with-d3/index.md +++ /dev/null @@ -1,235 +0,0 @@ ---- -title: Build a panel plugin with D3.js -summary: Learn how to use D3.js in your panel plugins. -description: how to use D3.js in your panel plugins. -id: build-a-panel-plugin-with-d3 -categories: ['plugins'] -tags: ['beginner'] -status: Published -authors: ['grafana_labs'] -Feedback Link: https://github.com/grafana/tutorials/issues/new -weight: 60 ---- - -## Introduction - -Panels are the building blocks of Grafana, and allow you to visualize data in different ways. This tutorial gives you a hands-on walkthrough of creating your own panel using [D3.js](https://d3js.org/). - -For more information about panels, refer to the documentation on [Panels](/docs/grafana/latest/features/panels/panels/). - -In this tutorial, you'll: - -- Build a simple panel plugin to visualize a bar chart. -- Learn how to use D3.js to build a panel using data-driven transformations. - -{{% class "prerequisite-section" %}} - -### Prerequisites - -- Grafana 7.0 -- NodeJS 12.x -- yarn - {{% /class %}} - -## Set up your environment - -{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" >}} - -## Create a new plugin - -{{< docs/shared lookup="tutorials/create-plugin.md" source="grafana" >}} - -## Data-driven documents - -[D3.js](https://d3js.org/) is a JavaScript library for manipulating documents based on data. It lets you transform arbitrary data into HTML, and is commonly used for creating visualizations. - -Wait a minute. Manipulating documents based on data? That's sounds an awful lot like React. In fact, much of what you can accomplish with D3 you can already do with React. So before we start looking at D3, let's see how you can create an SVG from data, using only React. - -In **SimplePanel.tsx**, change `SimplePanel` to return an `svg` with a `rect` element. - -```ts -export const SimplePanel: React.FC = ({ options, data, width, height }) => { - const theme = useTheme(); - - return ( - - - - ); -}; -``` - -One single rectangle might not be very exciting, so let's see how you can create rectangles from data. - -1. Create some data that we can visualize. - - ```ts - const values = [4, 8, 15, 16, 23, 42]; - ``` - -1. Calculate the height of each bar based on the height of the panel. - - ```ts - const barHeight = height / values.length; - ``` - -1. Inside a SVG group, `g`, create a `rect` element for every value in the dataset. Each rectangle uses the value as its width. - - ```ts - return ( - - - {values.map((value, i) => ( - - ))} - - - ); - ``` - -1. Rebuild the plugin and reload your browser to see the changes you've made. - -As you can see, React is perfectly capable of dynamically creating HTML elements. In fact, creating elements using React is often faster than creating them using D3. - -So why would you use even use D3? In the next step, we'll see how you can take advantage of D3's data transformations. - -## Transform data using D3.js - -In this step, you'll see how you can transform data using D3 before rendering it using React. - -D3 is already bundled with Grafana, and you can access it by importing the `d3` package. However, we're going to need the type definitions while developing. - -1. Install the D3 type definitions: - - ```bash - yarn add --dev @types/d3 - ``` - -1. Import `d3` in **SimplePanel.tsx**. - - ```ts - import * as d3 from 'd3'; - ``` - -In the previous step, we had to define the width of each bar in pixels. Instead, let's use _scales_ from the D3 library to make the width of each bar depend on the width of the panel. - -Scales are functions that map a range of values to another range of values. In this case, we want to map the values in our datasets to a position within our panel. - -1. Create a scale to map a value between 0 and the maximum value in the dataset, to a value between 0 and the width of the panel. We'll be using this to calculate the width of the bar. - - ```ts - const scale = d3 - .scaleLinear() - .domain([0, d3.max(values) || 0.0]) - .range([0, width]); - ``` - -1. Pass the value to the scale function to calculate the width of the bar in pixels. - - ```ts - return ( - - - {values.map((value, i) => ( - - ))} - - - ); - ``` - -As you can see, even if we're using React to render the actual elements, the D3 library contains useful tools that you can use to transform your data before rendering it. - -## Add an axis - -Another useful tool in the D3 toolbox is the ability to generate _axes_. Adding axes to our chart makes it easier for the user to understand the differences between each bar. - -Let's see how you can use D3 to add a horizontal axis to your bar chart. - -1. Create a D3 axis. Notice that by using the same scale as before, we make sure that the bar width aligns with the ticks on the axis. - - ```ts - const axis = d3.axisBottom(scale); - ``` - -1. Generate the axis. While D3 needs to generate the elements for the axis, we can encapsulate it by generating them within an anonymous function which we pass as a `ref` to a group element `g`. - - ```ts - { - d3.select(node).call(axis as any); - }} - /> - ``` - -By default, the axis renders at the top of the SVG element. We'd like to move it to the bottom, but to do that, we first need to make room for it by decreasing the height of each bar. - -1. Calculate the new bar height based on the padded height. - - ```ts - const padding = 20; - const chartHeight = height - padding; - const barHeight = chartHeight / values.length; - ``` - -1. Translate the axis by adding a transform to the `g` element. - - ```ts - { - d3.select(node).call(axis as any); - }} - /> - ``` - -Congrats! You've created a simple and responsive bar chart. - -## Complete example - -```ts -import React from 'react'; -import { PanelProps } from '@grafana/data'; -import { SimpleOptions } from 'types'; -import { useTheme } from '@grafana/ui'; -import * as d3 from 'd3'; - -interface Props extends PanelProps {} - -export const SimplePanel: React.FC = ({ options, data, width, height }) => { - const theme = useTheme(); - - const values = [4, 8, 15, 16, 23, 42]; - - const scale = d3 - .scaleLinear() - .domain([0, d3.max(values) || 0.0]) - .range([0, width]); - - const axis = d3.axisBottom(scale); - - const padding = 20; - const chartHeight = height - padding; - const barHeight = chartHeight / values.length; - - return ( - - - {values.map((value, i) => ( - - ))} - - { - d3.select(node).call(axis as any); - }} - /> - - ); -}; -``` - -## Summary - -In this tutorial you built a panel plugin with D3.js. diff --git a/docs/sources/tutorials/build-a-panel-plugin/index.md b/docs/sources/tutorials/build-a-panel-plugin/index.md deleted file mode 100644 index 9ee28ce4943..00000000000 --- a/docs/sources/tutorials/build-a-panel-plugin/index.md +++ /dev/null @@ -1,259 +0,0 @@ ---- -title: Build a panel plugin -summary: Learn how to create a custom visualization for your dashboards. -description: Learn how to create a custom visualization for your dashboards. -id: build-a-panel-plugin -categories: ['plugins'] -tags: ['beginner'] -status: Published -authors: ['grafana_labs'] -Feedback Link: https://github.com/grafana/tutorials/issues/new -weight: 50 ---- - -## Introduction - -Panels are the building blocks of Grafana. They allow you to visualize data in different ways. While Grafana has several types of panels already built-in, you can also build your own panel, to add support for other visualizations. - -For more information about panels, refer to the documentation on [Panels](/docs/grafana/latest/panels/). - -{{% class "prerequisite-section" %}} - -### Prerequisites - -- Grafana >=7.0 -- NodeJS >=14 -- yarn - {{% /class %}} - -## Set up your environment - -{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" >}} - -## Create a new plugin - -{{< docs/shared lookup="tutorials/create-plugin.md" source="grafana" >}} - -## Anatomy of a plugin - -{{< docs/shared lookup="tutorials/plugin-anatomy.md" source="grafana" >}} - -## Panel plugins - -Since Grafana 6.x, panels are [ReactJS components](https://reactjs.org/docs/components-and-props.html). - -Prior to Grafana 6.0, plugins were written in [AngularJS](https://angular.io/). Even though we still support plugins written in AngularJS, we highly recommend that you write new plugins using ReactJS. - -### Panel properties - -The [PanelProps](https://github.com/grafana/grafana/blob/747b546c260f9a448e2cb56319f796d0301f4bb9/packages/grafana-data/src/types/panel.ts#L27-L40) interface exposes runtime information about the panel, such as panel dimensions, and the current time range. - -You can access the panel properties through `props`, as seen in your plugin. - -**src/SimplePanel.tsx** - -```js -const { options, data, width, height } = props; -``` - -### Development workflow - -Next, you'll learn the basic workflow of making a change to your panel, building it, and reloading Grafana to reflect the changes you made. - -First, you need to add your panel to a dashboard: - -1. Open Grafana in your browser. -1. Create a new dashboard, and add a new panel. -1. Select your panel from the list of visualization types. -1. Save the dashboard. - -Now that you can view your panel, try making a change to the panel plugin: - -1. In `SimplePanel.tsx`, change the fill color of the circle. -1. Run `yarn dev` to build the plugin. -1. In the browser, reload Grafana with the new changes. - -## Add panel options - -Sometimes you want to offer the users of your panel an option to configure the behavior of your plugin. By configuring _panel options_ for your plugin, your panel will be able to accept user input. - -In the previous step, you changed the fill color of the circle in the code. Let's change the code so that the plugin user can configure the color from the panel editor. - -#### Add an option - -Panel options are defined in a _panel options object_. `SimpleOptions` is an interface that describes the options object. - -1. In `types.ts`, add a `CircleColor` type to hold the colors the users can choose from: - - ``` - type CircleColor = 'red' | 'green' | 'blue'; - ``` - -1. In the `SimpleOptions` interface, add a new option called `color`: - - ``` - color: CircleColor; - ``` - -Here's the updated options definition: - -**src/types.ts** - -```ts -type SeriesSize = 'sm' | 'md' | 'lg'; -type CircleColor = 'red' | 'green' | 'blue'; - -// interface defining panel options type -export interface SimpleOptions { - text: string; - showSeriesCount: boolean; - seriesCountSize: SeriesSize; - color: CircleColor; -} -``` - -#### Add an option control - -To change the option from the panel editor, you need to bind the `color` option to an _option control_. - -Grafana supports a range of option controls, such as text inputs, switches, and radio groups. - -Let's create a radio control and bind it to the `color` option. - -1. In `src/module.ts`, add the control at the end of the builder: - - ```ts - .addRadio({ - path: 'color', - name: 'Circle color', - defaultValue: 'red', - settings: { - options: [ - { - value: 'red', - label: 'Red', - }, - { - value: 'green', - label: 'Green', - }, - { - value: 'blue', - label: 'Blue', - }, - ], - } - }); - ``` - - The `path` is used to bind the control to an option. You can bind a control to nested option by specifying the full path within a options object, for example `colors.background`. - -Grafana builds an options editor for you and displays it in the panel editor sidebar in the **Display** section. - -#### Use the new option - -You're almost done. You've added a new option and a corresponding control to change the value. But the plugin isn't using the option yet. Let's change that. - -1. To convert option value to the colors used by the current theme, add a `switch` statement right before the `return` statement in `SimplePanel.tsx`. - - **src/SimplePanel.tsx** - - ```ts - let color: string; - switch (options.color) { - case 'red': - color = theme.palette.redBase; - break; - case 'green': - color = theme.palette.greenBase; - break; - case 'blue': - color = theme.palette.blue95; - break; - } - ``` - -1. Configure the circle to use the color. - - ```ts - - - - ``` - -Now, when you change the color in the panel editor, the fill color of the circle changes as well. - -## Create dynamic panels using data frames - -Most panels visualize dynamic data from a Grafana data source. In this step, you'll create one circle per series, each with a radius equal to the last value in the series. - -> To use data from queries in your panel, you need to set up a data source. If you don't have one available, you can use the [TestData DB](/docs/grafana/latest/features/datasources/testdata) data source while developing. - -The results from a data source query within your panel are available in the `data` property inside your panel component. - -```ts -const { data } = props; -``` - -`data.series` contains the series returned from a data source query. Each series is represented as a data structure called _data frame_. A data frame resembles a table, where data is stored by columns, or _fields_, instead of rows. Every value in a field share the same data type, such as string, number, or time. - -Here's an example of a data frame with a time field, `Time`, and a number field, `Value`: - -| Time | Value | -| ------------- | ----- | -| 1589189388597 | 32.4 | -| 1589189406480 | 27.2 | -| 1589189513721 | 15.0 | - -Let's see how you can retrieve data from a data frame and use it in your visualization. - -1. Get the last value of each field of type `number`, by adding the following to `SimplePanel.tsx`, before the `return` statement: - - ```ts - const radii = data.series - .map((series) => series.fields.find((field) => field.type === 'number')) - .map((field) => field?.values.get(field.values.length - 1)); - ``` - - `radii` will contain the last values in each of the series that are returned from a data source query. You'll use these to set the radius for each circle. - -1. Change the `svg` element to the following: - - ```ts - - - {radii.map((radius, index) => { - const step = width / radii.length; - return ; - })} - - - ``` - - Note how we're creating a `` element for each value in `radii`: - - ```ts - { - radii.map((radius, index) => { - const step = width / radii.length; - return ; - }); - } - ``` - - We use the `transform` here to distribute the circle horizontally within the panel. - -1. Rebuild your plugin and try it out by adding multiple queries to the panel. Refresh the dashboard. - -If you want to know more about data frames, check out our introduction to [Data frames](/docs/grafana/latest/developers/plugins/data-frames/). - -## Summary - -In this tutorial you learned how to create a custom visualization for your dashboards. diff --git a/docs/sources/tutorials/build-a-streaming-data-source-plugin/index.md b/docs/sources/tutorials/build-a-streaming-data-source-plugin/index.md deleted file mode 100644 index 5790542b0fc..00000000000 --- a/docs/sources/tutorials/build-a-streaming-data-source-plugin/index.md +++ /dev/null @@ -1,164 +0,0 @@ ---- -title: Build a streaming data source backend plugin -summary: Create a backend for your data source plugin with streaming capabilities. -description: Create a backend for your data source plugin with streaming capabilities. -id: build-a-streaming-data-source-backend-plugin -categories: ['plugins'] -tags: ['beginner'] -status: Published -authors: ['grafana_labs'] -Feedback Link: https://github.com/grafana/tutorials/issues/new -weight: 75 ---- - -## Introduction - -Grafana supports a wide range of data sources, including Prometheus, MySQL, and even Datadog. In previous tutorials we have shown how to extend Grafana capabilities to query custom data sources by [building a backend datasource plugin](/tutorials/build-a-data-source-backend-plugin/). In this tutorial we take a step further and add streaming capabilities to the backend datasource plugin. Streaming allows plugins to push data to Grafana panels as soon as data appears (without periodic polling from UI side). - -For more information about backend plugins, refer to the documentation on [Backend plugins](/docs/grafana/latest/developers/plugins/backend/). - -In this tutorial, you'll: - -- Extend a backend plugin with streaming capabilities - -{{% class "prerequisite-section" %}} - -#### Prerequisites - -- Knowledge about how data sources are implemented in the frontend. -- Knowledge about [backend datasource anatomy](/tutorials/build-a-data-source-backend-plugin/) -- Grafana 8.0+ -- Go ([Version](https://github.com/grafana/plugin-tools/blob/main/packages/create-plugin/templates/backend/go.mod#L3)) -- [Mage](https://magefile.org/) -- NodeJS ([Version](https://github.com/grafana/plugin-tools/blob/main/packages/create-plugin/templates/common/package.json#L66)) -- yarn - {{% /class %}} - -## Set up your environment - -{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" >}} - -## Create a new plugin - -To build a backend for your data source plugin, Grafana requires a binary that it can execute when it loads the plugin during start-up. In this guide, we will build a binary using the [Grafana plugin SDK for Go](/docs/grafana/latest/developers/plugins/backend/grafana-plugin-sdk-for-go/). - -The easiest way to get started is to use the Grafana [create-plugin tool](https://www.npmjs.com/package/@grafana/create-plugin). Navigate to the plugin folder that you configured in step 1 and type: - -``` -npx @grafana/create-plugin -``` - -Follow the steps and select **datasource** as your plugin type and answer **yes** when prompted to create a backend for your plugin. - -```bash -cd my-plugin -``` - -Install frontend dependencies and build frontend parts of the plugin to _dist_ directory: - -```bash -yarn install -yarn build -``` - -Run the following to update [Grafana plugin SDK for Go](/docs/grafana/latest/developers/plugins/backend/grafana-plugin-sdk-for-go/) dependency to the latest minor version: - -```bash -go get -u github.com/grafana/grafana-plugin-sdk-go -go mod tidy -``` - -Build backend plugin binaries for Linux, Windows and Darwin to _dist_ directory: - -```bash -mage -v -``` - -Now, let's verify that the plugin you've built can be used in Grafana when creating a new data source: - -1. Restart your Grafana instance. -1. Open Grafana in your web browser. -1. Navigate via the side-menu to **Configuration** -> **Data Sources**. -1. Click **Add data source**. -1. Find your newly created plugin and select it. -1. Enter a name and then click **Save & Test** (ignore any errors reported for now). - -You now have a new data source instance of your plugin that is ready to use in a dashboard. To confirm, follow these steps: - -1. Navigate via the side-menu to **Create** -> **Dashboard**. -1. Click **Add new panel**. -1. In the query tab, select the data source you just created. -1. A line graph is rendered with one series consisting of two data points. -1. Save the dashboard. - -### Troubleshooting - -#### Grafana doesn't load my plugin - -By default, Grafana requires backend plugins to be signed. To load unsigned backend plugins, you need to -configure Grafana to [allow unsigned plugins](/docs/grafana/latest/plugins/plugin-signature-verification/#allow-unsigned-plugins). -For more information, refer to [Plugin signature verification](/docs/grafana/latest/plugins/plugin-signature-verification/#backend-plugins). - -## Anatomy of a backend plugin - -As you may notice till this moment we did the same steps described in [build a backend datasource plugin tutorial](/tutorials/build-a-data-source-backend-plugin/). At this point, you should be familiar with backend plugin structure and a way how data querying and health check capabilities could be implemented. Let's take the next step and discuss how datasource plugin can handle data streaming. - -## Add streaming capabilities - -What we want to achieve here is to issue a query to load initial data from a datasource plugin and then switching to data streaming mode where the plugin will push data frames to Grafana time-series panel. - -In short – implementing a streaming plugin means implementing a `backend.StreamHandler` interface which contains `SubscribeStream`, `RunStream`, and `PublishStream` methods. - -`SubscribeStream` is a method where the plugin has a chance to authorize user subscription requests to a channel. Users on the frontend side subscribe to different channels to consume real-time data. - -When returning a `data.Frame` with initial data we can return a special field `Channel` to let the frontend know that we are going to stream data frames after initial data load. When the frontend receives a frame with a `Channel` set it automatically issues a subscription request to that channel. - -Channel is a string identifier of topic to which clients can subscribe in Grafana Live. See a documentation of Grafana Live for [details about channel structure](/docs/grafana/latest/live/live-channel/). - -As said in docs in Grafana Live channel consists of 3 parts delimited by `/`: - -- Scope -- Namespace -- Path - -For datasource plugin channels Grafana uses `ds` scope. Namespace in the case of datasource channels is a datasource unique ID (UID) which is issued by Grafana at the moment of datasource creation. The path is a custom string that plugin authors free to choose themselves (just make sure it consists of allowed symbols). I.e. datasource channel looks like `ds//`. - -So to let the frontend know that we are going to stream data we set a `Channel` field into frame metadata inside `QueryData` implementation. In our tutorial it's a `ds//stream`. The frontend will issue a subscription request to this channel. - -Inside `SubscribeStream` implementation we check whether a user allowed to subscribe on a channel path. If yes – we return an OK status code to tell Grafana user can join a channel: - -```go -status := backend.SubscribeStreamStatusPermissionDenied -if req.Path == "stream" { - // Allow subscribing only on expected path. - status = backend.SubscribeStreamStatusOK -} -return &backend.SubscribeStreamResponse{ - Status: status, -}, nil -``` - -As soon as the first subscriber joins a channel Grafana opens a unidirectional stream to consume streaming frames from a plugin. To handle this and to push data towards clients we implement a `RunStream` method which provides a way to push JSON data into a channel. So we can push data frame like this (error handling skipped): - -```go -// Send frame to stream including both frame schema and data frame parts. -_ = sender.SendFrame(frame, data.IncludeAll) -``` - -Open example datasource query editor and make sure `With Streaming` toggle is on. After doing this you should see data displayed and then periodically updated by streaming frames coming periodically from `RunStream` method. - -The important thing to note is that Grafana opens a unidirectional stream only once per channel upon the first subscriber joined. Every other subscription request will be still authorized by `SubscribeStream` method but the new `RunStream` won't be issued. I.e. you can have many active subscribers but only one running stream. At this moment this guarantee works for a single Grafana instance, we are planning to support this for highly-available Grafana setup (many Grafana instances behind load-balancer) in future releases. - -The stream will be automatically closed as soon as all subscriber users left. - -For the tutorial use case, we only need to properly implement `SubscribeStream` and `RunStream` - we don't need to handle publications to a channel from users. But we still need to write `PublishStream` method to fully implement `backend.StreamHandler` interface. Inside `PublishStream` we just do not allow any publications from users since we are pushing data from a backend: - -```go -return &backend.PublishStreamResponse{ - Status: backend.PublishStreamStatusPermissionDenied, -}, nil -``` - -## Summary - -In this tutorial you created a backend for your data source plugin with streaming capabilities. diff --git a/docs/sources/tutorials/build-an-app-plugin/index.md b/docs/sources/tutorials/build-an-app-plugin/index.md deleted file mode 100644 index 29936f02ab8..00000000000 --- a/docs/sources/tutorials/build-an-app-plugin/index.md +++ /dev/null @@ -1,208 +0,0 @@ ---- -title: Build an app plugin -summary: Learn at how to create an app for Grafana. -description: Learn at how to create an app for Grafana. -id: build-an-app-plugin -categories: ['plugins'] -tags: ['beginner'] -status: Published -authors: ['grafana_labs'] -Feedback Link: https://github.com/grafana/tutorials/issues/new -weight: 50 -draft: true ---- - -## Introduction - -App plugins are Grafana plugins that can bundle data source and panel plugins within one package. They also let you create _custom pages_ within Grafana. Custom pages enable the plugin author to include things like documentation, sign-up forms, or to control other services over HTTP. - -Data source and panel plugins will show up like normal plugins. The app pages will be available in the main menu. - -{{% class "prerequisite-section" %}} - -### Prerequisites - -- Grafana 7.0 -- NodeJS 12.x -- yarn - {{% /class %}} - -## Set up your environment - -{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" >}} - -## Create a new plugin - -{{< docs/shared lookup="tutorials/create-plugin.md" source="grafana" >}} - -## Anatomy of a plugin - -{{< docs/shared lookup="tutorials/plugin-anatomy.md" source="grafana" >}} - -## App plugins - -App plugins let you bundle resources such as dashboards, panels, and data sources into a single plugin. - -Any resource you want to include needs to be added to the `includes` property in the `plugin.json` file. To add a resource to your app plugin, you need to include it to the `plugin.json`. - -Plugins that are included in an app plugin are available like any other plugin. - -Dashboards and pages can be added to the app menu by setting `addToNav` to `true`. - -By setting `"defaultNav": true`, users can navigate to the dashboard by clicking the app icon in the side menu. - -## Add a custom page - -App plugins let you extend the Grafana user interface through the use of _custom pages_. - -Any requests sent to `/a/`, e.g. `/a/myorgid-simple-app/`, are routed to the _root page_ of the app plugin. The root page is a React component that returns the content for a given route. - -While you're free to implement your own routing, in this tutorial you'll use a tab-based navigation page that you can use by calling `onNavChange`. - -Let's add a tab for managing server instances. - -1. In the `src/pages` directory, add a new file called `Instances.tsx`. This component contains the content for the new tab. - - ```ts - import { AppRootProps } from '@grafana/data'; - import React, { FC } from 'react'; - - export const Instances: FC = ({ query, path, meta }) => { - return

Hello

; - }; - ``` - -1. Register the page by adding it to the `pages` array in `src/pages/index.ts`. - - **index.ts** - - ```ts - import { Instances } from './Instances'; - ``` - - ```ts - { - component: Instances, - icon: 'file-alt', - id: 'instances', - text: 'Instances', - } - ``` - -1. Add the page to the app menu, by including it in `plugin.json`. This will be the main view of the app, so we'll set `defaultNav` to let users quickly get to it by clicking the app icon in the side menu. - - **plugin.json** - - ```json - "includes": [ - { - "type": "page", - "name": "Instances", - "path": "/a/myorgid-simple-app?tab=instances", - "role": "Viewer", - "addToNav": true, - "defaultNav": true - } - ] - ``` - -> **Note:** While `page` includes typically reference pages created by the app, you can set `path` to any URL, internal or external. Try setting `path` to `https://grafana.com`. - -## Configure the app - -Let's add a new configuration page where users are able to configure default zone and regions for any instances they create. - -1. In `module.ts`, add new configuration page using the `addConfigPage` method. `body` is the React component that renders the page content. - - **module.ts** - - ```ts - .addConfigPage({ - title: 'Defaults', - icon: 'fa fa-info', - body: DefaultsConfigPage, - id: 'defaults', - }) - ``` - -## Add a dashboard - -#### Include a dashboard in your app - -1. In `src/`, create a new directory called `dashboards`. -1. Create a file called `overview.json` in the `dashboards` directory. -1. Copy the JSON definition for the dashboard you want to include and paste it into `overview.json`. If you don't have one available, you can find a sample dashboard at the end of this step. -1. In `plugin.json`, add the following object to the `includes` property. - - - The `name` of the dashboard needs to be the same as the `title` in the dashboard JSON model. - - `path` points out the file that contains the dashboard definition, relative to the `plugin.json` file. - - ```json - "includes": [ - { - "type": "dashboard", - "name": "System overview", - "path": "dashboards/overview.json", - "addToNav": true - } - ] - ``` - -1. Save and restart Grafana to load the new changes. - -## Bundle a plugin - -An app plugin can contain panel and data source plugins that get installed along with the app plugin. - -In this step, you'll add a data source to your app plugin. You can add panel plugins the same way by changing `datasource` to `panel`. - -1. In `src/`, create a new directory called `datasources`. -1. Create a new data source using Grafana create-plugin tool in a temporary directory. - - ```bash - mkdir tmp - cd tmp - npx @grafana/create-plugin - ``` - -1. Move the `src` directory in the data source plugin to `src/datasources`, and rename it to `my-datasource`. - - ```bash - mv ./my-datasource/src ../src/datasources/my-datasource - ``` - -Any bundled plugins are built along with the app plugin. Grafana looks for any subdirectory containing a `plugin.json` file and attempts to load a plugin in that directory. - -To let users know that your plugin bundles other plugins, you can optionally display it on the plugin configuration page. This is not done automatically, so you need to add it to the `plugin.json`. - -1. Include the data source in the `plugin.json`. The `name` property is only used for displaying in the Grafana UI. - - ```json - "includes": [ - { - "type": "datasource", - "name": "My data source" - } - ] - ``` - -#### Include external plugins - -If you want to let users know that your app requires an existing plugin, you can add it as a dependency in `plugin.json`. Note that they'll still need to install it themselves. - -```json -"dependencies": { - "plugins": [ - { - "type": "panel", - "name": "Worldmap Panel", - "id": "grafana-worldmap-panel", - "version": "^0.3.2" - } - ] -} -``` - -## Summary - -In this tutorial you learned how to create an app plugin. diff --git a/docs/sources/tutorials/create-alerts-from-flux-queries/index.md b/docs/sources/tutorials/create-alerts-from-flux-queries/index.md deleted file mode 100644 index 405b4f861cd..00000000000 --- a/docs/sources/tutorials/create-alerts-from-flux-queries/index.md +++ /dev/null @@ -1,331 +0,0 @@ ---- -title: How to create Grafana alerts with InfluxDB and the Flux query language -summary: Create complex alerts from Flux queries in the new Grafana Alerting -description: Create complex alerts from Flux queries in the new Grafana Alerting -id: grafana-alerts-flux-queries -categories: ['alerting'] -tags: ['advanced'] -status: published -authors: ['grant_pinkos'] -Feedback Link: https://github.com/grafana/tutorials/issues/new -weight: 70 ---- - -# How to create Grafana alerts with InfluxDB and the Flux query language - -[Grafana Alerting](/docs/grafana/latest/alerting/) represents a powerful new approach to systems observability and incident response management. While the alerting platform is perhaps best known for its strong integrations with Prometheus, the system works with numerous popular data sources including InfluxDB. In this tutorial we will learn how to create Grafana alerts using InfluxDB and the newer Flux query language. We will cover five common scenarios from the most basic to the most complex. Together, these five scenarios will provide an excellent guide for almost any type of alerting query that you wish to create using Grafana and Flux. - -Before we dive into our alerting scenarios, it is worth considering the development of InfluxDB's two popular query languages: InfluxQL and Flux. Originally, InfluxDB used [InfluxQL](https://docs.influxdata.com/influxdb/v2.5/reference/syntax/influxql/spec/) as their query language, which uses a SQL-like syntax. But beginning with InfluxDB v1.8, the company introduced [Flux](https://docs.influxdata.com/flux/v0.x/), "an open source functional data scripting language designed for querying, analyzing, and acting on data." "Flux," its official documentation goes on to state, "unifies code for querying, processing, writing, and acting on data into a single syntax. The language is designed to be usable, readable, flexible, composable, testable, contributable, and shareable." - -In the following five examples we will see just how powerful and flexible the new Flux query language can be. We will also see just how well Flux pairs with Grafana Alerting. - -## Example 1: Create an alert when a value is above or below a set threshold - -Our first example uses a common real-world scenario for InfluxDB and Grafana Alerting. Popular with IoT and edge applications, InfluxDB excels at on-site, real-time observability. In this example, and in fact for many of the following examples, we will consider the hypothetical scenario where we are monitoring a number of fluid tanks in a manufacturing plant. This scenario, [based on an actual application of InfluxDB and Alerting](/go/grafanaconline/2021/plant-efficiency-grafana-cloud/), will allow us to work through Grafana's various alerting setups, progressing from the simplest to the most complex. - -For Example 1, let's consider the following scenario: we are monitoring one tank, `A5`, for which we are storing real-time temperature data. We need to make sure that the temperature in this tank is always greater than 30 °C and less than 60 °C. - -We want to write a Grafana alert that will trigger whenever the temperature in tank `A5` crosses the lower threshold of 30 °C or the upper threshold of 60 °C. - -To do this, we'll: 1. create a Grafana alert rule. 1. add a Flux query. 1. add expressions to the alert rule. - -### Create a Grafana Alert rule - -1. Open the Grafana alerting menu and select **Alert rules**. -1. Click **New alert rule**. -1. Give your alert rule a name and then select **Grafana managed alert**. - For InfluxDB, you will always create a [Grafana managed rule](/docs/grafana/latest/alerting/alerting-rules/create-grafana-managed-rule/#add-grafana-managed-rule). - -### Add an initial Flux query to the alert rule - -Still in the **Step 2** section of the Alert rule page, you will see three boxes: a query editor (`A`), and then two sections labelled `B` and `C`. You will use these three sections to construct your rule. Let's move through them one by one. - -First, we want to query the data in our imaginary InfluxDB instance to obtain a time series graph of the temperature of tank A5. For this you would choose your InfluxDB data source from the dropdown and then write a query like this: - - ``` - from(bucket: "RetroEncabulator") - |> range(start: v.timeRangeStart, stop: v.timeRangeStop) - |> filter(fn: (r) => r["_measurement"] == "TemperatureData") - |> filter(fn: (r) => r["Tank"] == "A5") - |> filter(fn: (r) => r["_field"] == "Temperature") - |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) - |> yield(name: "mean") - ``` - -This is a fairly typical Flux query. Let's go through it function by function. We begin using [the `from()` function](https://docs.influxdata.com/flux/v0.x/stdlib/influxdata/influxdb/from/) to choose the correct bucket where our tank data resides. Then we use [a `range()` function](https://docs.influxdata.com/flux/v0.x/stdlib/universe/range/) to filter our rows based on time constraints. Then we pass our data through three [`filter()` functions](https://docs.influxdata.com/flux/v0.x/stdlib/universe/filter/) to narrow our results. We choose a specific [`measurement` (a special keyword in InfluxDB)](https://docs.influxdata.com/influxdb/v1.8/concepts/glossary/#measurement), then our tank in question (`A5`), and then a specific [`field` (another special keyword in InfluxDB)](https://docs.influxdata.com/influxdb/v1.8/concepts/glossary/#field). After this we pass the data into [an `aggregateWindow()` function](https://docs.influxdata.com/flux/v0.x/stdlib/universe/aggregatewindow/), which downsamples our data into specific periods of time, and then finally [a `yield()` function](https://docs.influxdata.com/flux/v0.x/stdlib/universe/yield/), which specifies which final result we want: `mean`. - -This Flux query will yield a time-series graph like this: - -![grafana alerts from flux queries](/media/tutorials/screenshot-flux-timeseries-graph.png) - -### Add expressions to your Grafana Alert rule - -With data now appearing in our rule setup, our next step is to create an [expression](/docs/grafana/v9.0/panels/query-a-data-source/use-expressions-to-manipulate-data/about-expressions/#using-expressions). Move to section `B`. For this scenario, we want to create a Reduce expression that will reduce the above to a single value. In this image, you can see that we have chosen to reduce our time-series data the `Last` value from input `A`. In this case, it returns a value 53 degrees celsius for Tank A5: - -![grafana alerts from flux queries](/media/tutorials/screenshot-flux-reduce-expression.png) - -Finally, we need to create a math expression that Grafana will alert on. In our case we will write an expression with two conditions separated by the OR `||` operator. We want to trigger an alert any time our result in section `B` is less than 30 or more than 60. This looks like `$B < 30 || $B > 60`: - -![grafana alerts from flux queries](/media/tutorials/screenshot-flux-math-expression.png) - -Set the alert condition to `C - expression`. We can now preview our alert. Here is a preview of this alert when the state is `Normal`: - -![grafana alerts from flux queries](/media/tutorials/screenshot-flux-alert-preview-state-normal.png) - -And here is a preview of this alert when the state is `Alerting`: - -![grafana alerts from flux queries](/media/tutorials/screenshot-flux-alert-alert-preview-state-alerting.png) - -Note that the Reduce expression above is needed. Without it, when previewing the results, Grafana would display `invalid format of evaluation results for the alert definition B: looks like time series data, only reduced data can be alerted on`. - -💡Tip: In case your locale is still stubbornly using Fahrenheit, we can modify the above Flux query by adding (before the aggregateWindow statement) a map() function to to convert (or map) the values from °C to °F. Note that we are not creating a new field. We are simply remapping the existing value. - -```flux -|> map(fn: (r) => ({r with _value: r._value * 1.8 + 32.0})) -``` - -### Conclusion - -Using these three steps you can create a Flux-based Grafana Alert that will trigger on either of two thresholds from a single data source. But what if you need to trigger an alert based on **multiple conditions and from multiple time-series**? In example two we will cover this very scenario. - -## Example 2: how to create a Grafana alert from two queries and two conditions - -Let's mix things up a bit for example two and leave our imaginary manufacturing plant. Imagine you're an assistant to the great Dr. Emmett Brown from Back to the Future, and Doc has tasked you with the following challenge: "I want an alert sent to me every time both conditions for time travel are met: when the velocity of a vehicle reaches 88 miles per hour and an object generates 1.21 jigowatts of electricity." - -Let's assume we are tracking this data in InfluxDB and Grafana. Let's also assume that each of the above data sources comes from different buckets. How do we alert on this? How do we use Grafana and Flux to alert on two distinct conditions originating from two distinct data sources? - -### Add two Flux queries to your Grafana Alert rule - -Like we did in example 1, let's first mock up our queries. Our query for our vehicle data is very similar to our last query. We use a `from()`, `range()`, and a sequence of `filter()` functions. We then use `AggregateWindow()` and `yield()` to narrow our data even more. In this case, the result is a time series tracking the velocity of our 1983 DeLorean: - -```flux -from(bucket: "vehicles") -|> range(start: v.timeRangeStart, stop: v.timeRangeStop) -|> filter(fn: (r) => r["_measurement"] == "VehicleData") -|> filter(fn: (r) => r["VehicleType"] == "DeLorean") -|> filter(fn: (r) => r["VehicleYear"] == "1983") -|> filter(fn: (r) => r["_field"] == "velocity") -|> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) -|> yield(name: "mean") -``` - -Our second query will trigger an alert whenever our electricity resource (the lightning strike on the Hill Valley clocktower) reaches the needed 1.21 jigowatts. A query like this would look very similar to our vehicle velocity query: - -```flux -from(bucket: "HillValley") -|> range(start: v.timeRangeStart, stop: v.timeRangeStop) -|> filter(fn: (r) => r["_measurement"] == "ElectricityData") -|> filter(fn: (r) => r["Location"] == "clocktower") -|> filter(fn: (r) => r["Source"] == "lightning") -|> filter(fn: (r) => r["_field"] == "power") -|> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) -|> yield(name: "mean") -``` - -We are now ready to modify this data using expressions. - -### Add expressions to your Grafana Alert rule - -1. Let's now use the same steps to reduce each query to the last (most recent) value. Reducing Query `A` to a single value might look like this: - - ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-additional-queries-reduce-expression-A.png) - -1. And here we are reducing query `B`: - - ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-additional-queries-reduce-expression-B.png) - -1. Now, in section `C` we need to create a math expression to be alerted on. In this case we will use the AND `&&` operator to specify that two conditions must be met: the value of `C` (the reduced value from query `A`) must be greater than 88.0 while the value of `D` (the reduced value from query `B`) must be greater than 1.21. We write this as `$C > 88.0 && $D > 1.21` - - ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-additional-queries-math-expression.png) - -And here is a preview of our alerts: - -![grafana alerts from flux queries](https://raw.githubusercontent.com/grafana/tutorials/master/content/tutorials/assets/flux-additional-queries-alert-preview.png) - -💡Tip: If your data in InfluxDB happens to have an unnecessarily large number of digits to the right of the decimal (such as 1.2104705741732575 shown above), and you want your Grafana alerts to be more legible, try using {{ printf "%.2f" $values.D.Value }}. For example, in the annotation Summary, we could write the following: - -``` -{{ $values.D.Labels.Source }} at the {{ $values.D.Labels.Location }} has generated {{ printf "%.2f" $values.D.Value }} jigowatts.` -``` - -This will display as follows: -![grafana alerts from flux queries](/media/tutorials/screenshot-flux-tip-significant-figures.png)) - -You can reference our documentation on [alert message templating](/docs/grafana/latest/alerting/contact-points/message-templating/) to learn more about this powerful feature. - -### Conclusion - -In this example we showed how to create a Flux-based alert that uses two distinct conditions from two distinct queries that use data from two distinct data sources. For example three we will switch gears and tackle another popular alerting scenario: how to create an alert based on an aggregated (per day) value. - -## Example 3: how to create a Grafana Alert based on an aggregated (per-day) value - -One of the most common requests in [Grafana's community forum](https://community.grafana.com) involves graphing daily electrical consumption and production. This sort of data is very often stored in InfluxDB. In this example we will see how to aggregate time series data into a per-day value and then alert on it. - -Let’s assume our electricity meter sends a reading to InfluxDB once per hour and contains the total kWh used for that hour. We want to write a query that will aggregate these per-hour values into a per-day value, then create an alert that triggers when the power consumption (kWh) exceeds 5,000 kWh per day. - -### Add an initial Flux query to your Grafana Alert rule - -1. Let's begin by examining a typical query and the resulting time graph for our hourly data across a 7-day period. A query like this is shown below: - - ```flux - from(bucket: "RetroEncabulator") - |> range(start: v.timeRangeStart, stop: v.timeRangeStop) - |> filter(fn: (r) => r["_measurement"] == "ElectricityData") - |> filter(fn: (r) => r["Location"] == "PlantD5") - |> filter(fn: (r) => r["_field"] == "power_consumed") - |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) - |> yield(name: "power") - ``` - - We can see the same pattern of Flux functions here that we say in examples 1 and 2. A query like this would produce a graph similar to the following: - - ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-aggregatewindow-timeseries-graph.png) - -1. Now let's adjust our query to calculate daily usage. With many datasources, this can be a rather complex operation. But with Flux, by simply changing the aggregateWindow function parameters we can calculate the daily usage over the same 7-day period: - - ```flux - from(bucket: "RetroEncabulator") - |> range(start: v.timeRangeStart, stop: v.timeRangeStop) - |> filter(fn: (r) => r["_measurement"] == "ElectricityData") - |> filter(fn: (r) => r["Location"] == "PlantD5") - |> filter(fn: (r) => r["_field"] == "power_consumed") - |> aggregateWindow(every: 1d, fn: sum) - |> yield(name: "power") - ``` - - Note how we've adjusted our `aggregateWindow()` function to `aggregateWindow(every: 1d, fn: sum)`. This results in a graph like so: - - ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-aggregatewindow-aggregated.png) - -1. Add expressions to your Grafana Alert rule. - - Now that we have our per-day query correct, we can continue using the same pattern as before, adding expressions to reduce and perform math on our results. - - As before, let's reduce our query to a single value: - - ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-aggregatewindow-reduce-expression.png) - - Now create a math expression to be alerted on and set the evaluation behavior. In this case we want to write `$B > 5000`: - - ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-aggregatewindow-math-expression.png) - - And now we are alerting on our daily electricity consumption whenever we exceed 5000 kWh. Here is preview of our alert: - - ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-aggregatewindow-alert-preview.png) - -### Conclusion - -Plotting and aggregating electrical consumption is a common use case for combining InfluxDB and Grafana. Using Flux, we saw just how easy it can be to group our data by day and then alert on that daily value. In our next two examples we will examine the more complex form of Grafana Alert: multidimensional alerts. - -## Example 4: create a dynamic (multidimensional) Grafana Alert using Flux - -Let’s return to our fluid tanks from example 1, but this time let’s assume we have 5 tanks (A5, B4, C3, D2, and E1). We are now tracking the temperature in five tanks: A5, B4, C3, D2, and E1. - -We want to create one multidimensional alert that will notify us whenever the temperature in any tank is less than 30 °C or greater than 60 °C. - -### Add an initial Flux query to your Grafana Alert rule - -We begin, as always, by writing our initial query. This is very similar to our query in example 1, but note how our third `filter()` function captures the data from all five tanks and not just `A5`: - -```flux -from(bucket: "HyperEncabulator") -|> range(start: v.timeRangeStart, stop: v.timeRangeStop) -|> filter(fn: (r) => r["_measurement"] == "TemperatureData") -|> filter(fn: (r) => r["MeasType"] == "actual") -|> filter(fn: (r) => r["Tank"] == "A5" or r["Tank"] == "B4" or r["Tank"] == "C3" or r["Tank"] == "D2" or r["Tank"] == "E1") -|> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) -|> yield(name: "mean") -``` - -💡Tip: If the tanks were shut down every night from 23:00 to 07:00, they would possibly fall below the 30 °C threshold. If one did not want to receive alerts during those hours, one can use the Flux function hourSelection() which filters rows by time values in a specified hour range. - -```flux -|> hourSelection(start: 7, stop: 23)` -``` - -A query like the one above will produce a time series graph like this: - -![grafana alerts from flux queries](/media/tutorials/screenshot-flux-multidimensional-timeseries-graph.png) - -### Add expressions to your Grafana Alert rule - -1. We create a Reduce expression that will reduce the time series for each tank to a single value. This gives us five distinct temperatures: - - ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-multidimensional-reduce-expression.png)) - -1. Create a math expression to be alerted on. This is the exact same expression from example 1, `$B < 30 || $B > 60`: - - ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-multidimensional-math-expression.png) - -As we can see three tanks are within the acceptable thresholds while two tanks have crossed the upper boundary. This would trigger an alert for tanks `D2` and `E1`. - -### Conclusion - -With multidimensional alerts we can avoid repeating ourselves. But what if the scenario were even more complex? In the next and final example, we will examine how to use multidimensional alerts to create the most dynamic alerts possible. - -## Example 5: how to create a dynamic (multidimensional) Grafana Alert using multiple queries and multiple thresholds with Flux - -For this final example let's continue with our five fluid tanks and their five datasets.Let’s assume again that each tank has a temperature controller with a setpoint value that is stored in InfluxDB. Let’s mix things up and assume that each tank has a _different_ setpoint, where we always need to be within 3 degrees of the setpoint. - -We want to create one multidimensional alert that will cover each unique scenario for each tank, triggering an alert whenever any tank's temperature moves beyond its unique allowable range. - -To better visualize this challenge, here is a table representing our five tanks, their temperature setpoints, and their allowable range: - -| Tank | Setpoint | Allowable Range (±3) | -| ---- | -------- | -------------------- | -| A5 | 45 | 42 to 48 | -| B4 | 55 | 52 to 58 | -| C3 | 60 | 57 to 63 | -| D2 | 72 | 69 to 75 | -| E1 | 80 | 77 to 83 | - -With Grafana Alerting, we can create a single multidimensional rule to cover all 5 tanks, and we can use Flux to compare the setpoint and actual value for each tank. In other words, one multidimensional alert can monitor 5 separate tanks, each with different setpoints and actual values, but all with one common "allowable threshold" (i.e. a temperature difference of ±3 degrees). - -### Add an initial Flux query to your Grafana Alert rule - -Let's begin with our data query. It is similar to our past queries, only now more complex. We must add extra functions to get our data into the proper format, including a `pivot()`, `map()`, `rename()`, `keep()`, and `drop()` function: - -```flux -from(bucket: "HyperEncabulator") - |> range(start: v.timeRangeStart, stop: v.timeRangeStop) - |> filter(fn: (r) => r["_measurement"] == "TemperatureData") - |> filter(fn: (r) => r["MeasType"] == "actual" or r["MeasType"] == "setpoint") - |> filter(fn: (r) => r["Tank"] == "A5" or r["Tank"] == "B4" or r["Tank"] == "C3" or r["Tank"] == "D2" or r["Tank"] == "E1") - |> filter(fn: (r) => r["_field"] == "Temperature") - |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) - |> pivot(rowKey:["_time"], columnKey: ["MeasType"], valueColumn: "_value") - |> map(fn: (r) => ({ r with _value: (r.setpoint - r.actual)})) - |> rename(columns: {_value: "difference"}) - |> keep(columns: ["_time", "difference", "Tank"]) - |> drop(columns: ["actual", "setpoint"]) - |> yield(name: "mean") -``` - -Note in the above that we are calculating the difference between the actual and the setpoint. The way Grafana parses the result from InfluxDB is that if a \_value column is found, it is assumed to be a time-series. The quick workaround is to add the following `rename()` function: - -```flux - |> rename(columns: {_value: "something"}) -``` - -The above query results in this time series: - -![grafana alerts from flux queries](/media/tutorials/screenshot-flux-complex-query-timeseries-graph.png) - -### Add expressions to your Grafana Alert rule - -1. Again, we create a Reduce expression for the above query to reduce each of the above to a single value. This value represents the temperature differential between each tank's setpoint and its actual real-time temperature: - - ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-complex-query-reduce-expression.png) - -1. Now we create a math expression to be alerted on. This time we will create a condition that checks if the absolute value of our reduce calculation is greater than 3, `abs($(B))>3.0`: - - ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-complex-query-math-expression.png) - -We can now see that two tanks, `D2` and `E1`, are evaluating to true. When we preview the alert we can see that those two tanks will trigger a notification and change their state from `Normal` to `Alerting`: - -![grafana alerts from flux queries](/media/tutorials/screenshot-flux-complex-query-alert-preview-state-normal.png) -![grafana alerts from flux queries](/media/tutorials/screenshot-flux-complex-query-alert-preview-state-alerting.png) - -### Conclusion - -Flux queries and Grafana Unified Alerting are a powerful combination to identify practically any alertable conditions in your dataset, or across your entire system. For more information on Grafana Alerting, [visit the documentation here](/docs/grafana/latest/alerting/). For more information on the Flux query language, [you can visit that documentation as well](https://docs.influxdata.com/flux/v0.x/). diff --git a/docs/sources/tutorials/create-users-and-teams/index.md b/docs/sources/tutorials/create-users-and-teams/index.md deleted file mode 100644 index 23b02d1d663..00000000000 --- a/docs/sources/tutorials/create-users-and-teams/index.md +++ /dev/null @@ -1,236 +0,0 @@ ---- -title: Create users and teams -summary: Learn how to set up teams and users. -description: Learn how to set up teams and users. -id: create-users-and-teams -categories: ['administration'] -tags: ['beginner'] -status: Published -authors: ['grafana_labs'] -Feedback Link: https://github.com/grafana/tutorials/issues/new -weight: 20 ---- - -## Introduction - -This tutorial is for admins or anyone that wants to learn how to manage -users in Grafana. You'll add multiple local users, organize them into teams, -and make sure they're only able to access the resources they need. - -### Scenario - -_Graphona_, a fictional telemarketing company, has asked you to configure Grafana -for their teams. - -In this scenario, you'll: - -- Create users and organize them into teams. -- Manage resource access for each user and team through roles and folders. - -{{% class "prerequisite-section" %}} - -### Prerequisites - -- Grafana 7.0 or newer, this tutorial was tested with Grafana 8.5. -- A user with the Admin or Server Admin role. - {{% /class %}} - -## Add users - -In Grafana, all users are granted an _organization role_ that determines what -resources they can access. - -There are three types of organization roles in Grafana. The **Grafana Admin** is -a global role, the default `admin` user has this role. - -- **Grafana Admin -** Manage organizations, users, and view server-wide settings. -- **Organization Administrator -** Manage data sources, teams, and users within an organization. -- **Editor -** Create and edit dashboards. -- **Viewer -** View dashboards. - -> **Note**: You can also configure Grafana to allow [anonymous access](/docs/grafana/latest/auth/overview/#anonymous-authentication), to make dashboards available even to those who don't have a Grafana user account. That's how Grafana Labs made https://play.grafana.org publicly available. - -### Exercise - -Graphona has asked you to add a group of early adopters that work in the Marketing and Engineering teams. They'll need to be able to edit their own team's dashboards, but want to have view access to dashboards that belong to the other team. - -| Name | Email | Username | -| ----------------- | ----------------------------- | ----------------- | -| Almaz Russom | almaz.russom@example.com | almaz.russom | -| Brenda Tilman | brenda.tilman@example.com | brenda.tilman | -| Mada Rawdha Tahan | mada.rawdha.tahan@example.com | mada.rawdha.tahan | -| Yuan Yang | yuan.yang@example.com | yuan.yang | - -#### Add users - -Repeat the following steps for each of the employees in the table above to create the new user accounts: - -1. Log in as a user that has the **Server Admin** role. -1. On the sidebar, click the **Server Admin** (shield) icon. -1. Choose **Users** from the menu drop-down, then click **New User**. -1. Enter the **Name**, **Email**, **Username**, and **Password** from the table above. -1. Click the **Create User** button to create the account. - -When you create a user they are granted the Viewer role by default, which means that they won't be able to make any changes to any of the resources in Grafana. That's ok for now, you'll grant more user permissions by adding users to _teams_ in the next step. - -## Assign users to teams - -Teams let you grant permissions to a group of users, instead of granting permissions to individual users one at a time. - -Teams are useful when onboarding new colleagues. When you add a user to a team, they get access to all resources assigned to that team. - -### Exercise - -In this step, you'll create two teams and assign users to them. - -| Username | Team | -| ----------------- | ----------- | -| brenda.tilman | Marketing | -| mada.rawdha.tahan | Marketing | -| almaz.russom | Engineering | -| yuan.yang | Engineering | - -#### Create a team - -Create the _Marketing_ and _Engineering_ teams. - -1. In the sidebar, hover your mouse over the **Configuration** (gear) icon and - then click **Teams**. -1. Click **New team**. -1. In **Name**, enter the name of the team: either _Marketing_ or _Engineering_. - You do not need to enter an email. -1. Click **Create**. -1. Click on the **Teams** link at the top of the page to return to teams page and create the second team. - -#### Add a user to a team - -Repeat these steps for each user to assign them to their team. Refer to the table above for team assignments. - -1. Click the team name _Marketing_ or _Engineering_ to add members to that team. -1. Click **Add member**. -1. In the **Add team member** box, click the drop-down arrow to choose the user you want to add to the team . -1. Click **Add to team**. - -When you're done, you'll have two teams with two users assigned to each. - -## Manage resource access with folders - -It's a good practice to use folders to organize collections of related dashboards. You can assign permissions at the folder level to individual users or teams. - -### Exercise - -The Marketing team is going to use Grafana for analytics, while the Engineering team wants to monitor the application they're building. - -You'll create two folders, _Analytics_ and _Application_, where each team can add their own dashboards. The teams still want to be able to view each other's dashboards. - -| Folder | Team | Permissions | -| ----------- | ----------- | ----------- | -| Analytics | Marketing | Edit | -| | Engineering | View | -| Application | Marketing | View | -| | Engineering | Edit | - -Repeat the following steps for each folder. You'll move through all three steps for each folder before moving on to the next one. - -#### Add a folder for each team - -1. In the sidebar, hover your cursor over the **Dashboards** (four squares) icon and then click **Browse**. -1. To create a folder, click **New Folder**. -1. In **Name**, enter the folder name. -1. Click **Create**. -1. Stay in the folder view and move on to the next sections to edit permissions for this folder. - -#### Remove the viewer role from folder permissions - -By default, when you create a folder, all users with the Viewer role are granted permission to view the folder. - -In this example, Graphona wants to explicitly grant teams access to folders. To support this, you need to remove the Viewer role from the list of permissions: - -1. Go to the **Permissions** tab. -1. Remove the Viewer role from the list, by clicking the red button on the right. -1. Stay in the permissions tab and move on to the next section to grant folder permissions for each team. - -#### Grant folder permissions to a team: - -1. Click **Add Permission**. -1. In the **Add Permission For** dialog, make sure "Team" is selected in the first box. -1. In the second box, select the team to grant access to. -1. In the third box, select the access you want to grant. -1. Click **Save**. -1. Repeat for the other team. -1. Click the **Dashboards** link at the top of the page to return to the dashboard list. - -When you're finished, you'll have two empty folders, the contents of which can only be viewed by members of the Marketing or Engineering teams. Only Marketing team members can edit the contents of the Analytics folder, only Engineering team members can edit the contents of the Application folder. - -## Define granular permissions - -By using folders and teams, you avoid having to manage permissions for individual users. - -However, there are times when you need to configure permissions on a more granular level. For these cases, Grafana allows you to override permissions for specific dashboards. - -### Exercise - -Graphona has hired a consultant to assist the Marketing team. The consultant should only be able to access the SEO dashboard in the Analytics folder. - -| Name | Email | Username | -| ---------- | -------------------------------- | ---------- | -| Luc Masson | luc.masson@exampleconsulting.com | luc.masson | - -#### Add a new user - -1. In the sidebar, click the **Server Admin** (shield) icon. -1. In the Users tab, click **New user**. -1. In **Name**, enter the name of the user. -1. In **E-mail**, enter the email of the user. -1. In **Username**, enter the username that the user will use to log in. -1. In **Password**, enter a password. The user can change their password once they log in. -1. Click **Create user** to create the user account. - -#### Create a dashboard - -1. In the sidebar, click the **Create** (plus) icon to create a new dashboard. -1. In the top right corner, click the cog icon to go to **Dashboard settings**. -1. In **Name**, enter **SEO**. -1. Click **Save Dashboard**. -1. In the **Save dashboard as...** pop-up, choose the **Analytics** folder from the drop-down and click **Save**. - -#### Grant a user permission to view dashboard - -1. In the top right corner of your dashboard, click the cog icon to go to **Dashboard settings**. -1. Go to the **Permissions** tab, and click **Add Permission**. -1. In the **Add Permission For** dialog, select **User** in the first box. -1. In the second box, select the user to grant access to: Luc Masson. -1. In the third box, select **View**. -1. Click **Save**. -1. Click **Save dashboard**. -1. Add a note about giving Luc Masson Viewer permission for the dashboard and then click **Save**. - -You've created a new user and given them unique permissions to view a single dashboard within a folder. - -#### Check your work - -You can repeat these steps to log in as the other users you've created see the differences in the viewer and editor roles. - -For this example, you can log in as the user `luc.masson` to see that they can only access the SEO dashboard. - -1. Click the profile (avatar) button in the bottom left corner, choose **Sign out**. -1. Enter `luc.masson` as the username. -1. Enter the password you created for Luc. -1. Click **Log in**. -1. In the sidebar, hover your cursor over the **Dashboards** (four squares) icon and then click **Browse**. -1. You'll notice that you won't see the **Analytics** folder in the folder view because we did not give Luc folder permission. -1. Click on the list icon (3 lines) to see the dashboard list. -1. Click on the **SEO dashboard**, there shouldn't be any editing permissions since we assigned Luc the viewer role. - -## Summary - -In this tutorial, you've configured Grafana for an organization: - -- You added users to your organization. -- You created teams to manage permissions for groups of users. -- You configured permissions for folders and dashboard. - -### Learn more - -- [Organization Roles](/docs/grafana/next/administration/manage-users-and-permissions/about-users-and-permissions/#organization-roles) -- [Permissions Overview](/docs/grafana/latest/administration/manage-users-and-permissions/about-users-and-permissions/#about-users-and-permissions) diff --git a/docs/sources/tutorials/grafana-fundamentals/index.md b/docs/sources/tutorials/grafana-fundamentals/index.md deleted file mode 100644 index 5374ac91ae0..00000000000 --- a/docs/sources/tutorials/grafana-fundamentals/index.md +++ /dev/null @@ -1,354 +0,0 @@ ---- -title: Grafana fundamentals -summary: Get familiar with Grafana -description: Get familiar with Grafana -id: grafana-fundamentals -categories: ['fundamentals'] -tags: ['beginner'] -status: Published -authors: ['grafana_labs'] -Feedback Link: https://github.com/grafana/tutorials/issues/new -weight: 10 ---- - -## Introduction - -In this tutorial, you'll learn how to use Grafana to set up a monitoring solution for your application. - -In this tutorial, you'll: - -- Explore metrics and logs -- Build dashboards -- Annotate dashboards -- Set up alerts - -{{% class "prerequisite-section" %}} - -### Prerequisites - -- [Docker](https://docs.docker.com/install/) -- [Docker Compose](https://docs.docker.com/compose/) (included in Docker for Desktop for macOS and Windows) -- [Git](https://git-scm.com/) - {{% /class %}} - -## Set up the sample application - -This tutorial uses a sample application to demonstrate some of the features in Grafana. To complete the exercises in this tutorial, you need to download the files to your local machine. - -In this step, you'll set up the sample application, as well as supporting services, such as [Prometheus](https://prometheus.io/) and [Loki](/oss/loki/). - -1. Clone the [github.com/grafana/tutorial-environment](https://github.com/grafana/tutorial-environment) repository. - - ``` - git clone https://github.com/grafana/tutorial-environment.git - ``` - -1. Change to the directory where you cloned this repository: - - ``` - cd tutorial-environment - ``` - -1. Make sure Docker is running: - - ``` - docker ps - ``` - - No errors means it is running. If you get an error, then start Docker and then run the command again. - -1. Start the sample application: - - ``` - docker-compose up -d - ``` - - The first time you run `docker-compose up -d`, Docker downloads all the necessary resources for the tutorial. This might take a few minutes, depending on your internet connection. - - > **Note:** If you already have Grafana, Loki, or Prometheus running on your system, then you might see errors because the Docker image is trying to use ports that your local installations are already using. Stop the services, then run the command again. - -1. Ensure all services are up-and-running: - - ``` - docker-compose ps - ``` - - In the **State** column, it should say `Up` for all services. - -1. Browse to the sample application on [localhost:8081](http://localhost:8081). - -### Grafana News - -The sample application, Grafana News, lets you post links and vote for the ones you like. - -To add a link: - -1. In **Title**, enter **Example**. -1. In **URL**, enter **https://example.com**. -1. Click **Submit** to add the link. - - The link appears in the list under the Grafana News heading. - -To vote for a link, click the triangle icon next to the name of the link. - -## Log in to Grafana - -Grafana is an open-source platform for monitoring and observability that lets you visualize and explore the state of your systems. - -1. Open a new tab. -1. Browse to [localhost:3000](http://localhost:3000). -1. In **email or username**, enter **admin**. -1. In **password**, enter **admin**. -1. Click **Log In**. - - The first time you log in, you're asked to change your password: - -1. In **New password**, enter your new password. -1. In **Confirm new password**, enter the same password. -1. Click **Save**. - -The first thing you see is the Home dashboard, which helps you get started. - -To the far left you can see the _sidebar_, a set of quick access icons for navigating Grafana. - -## Add a metrics data source - -The sample application exposes metrics which are stored in [Prometheus](https://prometheus.io/), a popular time series database (TSDB). - -To be able to visualize the metrics from Prometheus, you first need to add it as a data source in Grafana. - -1. In the sidebar, hover your cursor over the **Configuration** (gear) icon, and then click **Data sources**. -1. Click **Add data source**. -1. In the list of data sources, click **Prometheus**. -1. In the URL box, enter **http\://prometheus:9090**. -1. Click **Save & test**. - - Prometheus is now available as a data source in Grafana. - -## Explore your metrics - -Grafana Explore is a workflow for troubleshooting and data exploration. In this step, you'll be using Explore to create ad-hoc queries to understand the metrics exposed by the sample application. - -> Ad-hoc queries are queries that are made interactively, with the purpose of exploring data. An ad-hoc query is commonly followed by another, more specific query. - -1. In the sidebar, click the **Explore** (compass) icon. -1. In the **Query editor**, where it says _Enter a PromQL query…_, enter `tns_request_duration_seconds_count` and then press Shift + Enter. - A graph appears. -1. In the top right corner, click the dropdown arrow on the **Run Query** button, and then select **5s**. Grafana runs your query and updates the graph every 5 seconds. - - You just made your first _PromQL_ query! [PromQL](https://prometheus.io/docs/prometheus/latest/querying/basics/) is a powerful query language that lets you select and aggregate time series data stored in Prometheus. - - `tns_request_duration_seconds_count` is a _counter_, a type of metric whose value only ever increases. Rather than visualizing the actual value, you can use counters to calculate the _rate of change_, i.e. how fast the value increases. - -1. Add the [`rate`](https://prometheus.io/docs/prometheus/latest/querying/functions/#rate) function to your query to visualize the rate of requests per second. Enter the following in the **Query editor** and then press Shift + Enter. - - ``` - rate(tns_request_duration_seconds_count[5m]) - ``` - - Immediately below the graph there's an area where each time series is listed with a colored icon next to it. This area is called the _legend_. - - PromQL lets you group the time series by their labels, using the [`sum`](https://prometheus.io/docs/prometheus/latest/querying/operators/#aggregation-operators) aggregation operator. - -1. Add the `sum` aggregation operator to your query to group time series by route: - - ``` - sum(rate(tns_request_duration_seconds_count[5m])) by(route) - ``` - -1. Go back to the [sample application](http://localhost:8081) and generate some traffic by adding new links, voting, or just refresh the browser. - -1. In the upper-right corner, click the _time picker_, and select **Last 5 minutes**. By zooming in on the last few minutes, it's easier to see when you receive new data. - -Depending on your use case, you might want to group on other labels. Try grouping by other labels, such as `status_code`, by changing the `by(route)` part of the query. - -## Add a logging data source - -Grafana supports log data sources, like [Loki](/oss/loki/). Just like for metrics, you first need to add your data source to Grafana. - -1. In the sidebar, hover your cursor over the **Configuration** (gear) icon, and then click **Data Sources**. -1. Click **Add data source**. -1. In the list of data sources, click **Loki**. -1. In the URL box, enter [http://loki:3100](http://loki:3100). -1. Click **Save & Test** to save your changes. - -Loki is now available as a data source in Grafana. - -## Explore your logs - -Grafana Explore not only lets you make ad-hoc queries for metrics, but lets you explore your logs as well. - -1. In the sidebar, click the **Explore** (compass) icon. -1. In the data source list at the top, select the **Loki** data source. -1. In the **Query editor**, enter: - - ``` - {filename="/var/log/tns-app.log"} - ``` - -1. Grafana displays all logs within the log file of the sample application. The height of each bar in the graph encodes the number of logs that were generated at that time. - -1. Click and drag across the bars in the graph to filter logs based on time. - -Not only does Loki let you filter logs based on labels, but on specific occurrences. - -Let's generate an error, and analyze it with Explore. - -1. In the [sample application](http://localhost:8081), post a new link without a URL to generate an error in your browser that says `empty url`. -1. Go back to Grafana and enter the following query to filter log lines based on a substring: - - ``` - {filename="/var/log/tns-app.log"} |= "error" - ``` - -1. Click on the log line that says `level=error msg="empty url"` to see more information about the error. - - > **Note:** If you're in Live mode, clicking logs will not show more information about the error. Instead, stop and exit the live stream, then click the log line there. - -Logs are helpful for understanding what went wrong. Later in this tutorial, you'll see how you can correlate logs with metrics from Prometheus to better understand the context of the error. - -## Build a dashboard - -A _dashboard_ gives you an at-a-glance view of your data and lets you track metrics through different visualizations. - -Dashboards consist of _panels_, each representing a part of the story you want your dashboard to tell. - -Every panel consists of a _query_ and a _visualization_. The query defines _what_ data you want to display, whereas the visualization defines _how_ the data is displayed. - -1. In the sidebar, hover your cursor over the **Create** (plus sign) icon and then click **Dashboard**. -1. Click **Add a new panel**. -1. In the **Query editor** below the graph, enter the query from earlier and then press Shift + Enter: - - ``` - sum(rate(tns_request_duration_seconds_count[5m])) by(route) - ``` - -1. In the **Legend** field, enter **{{route}}** to rename the time series in the legend. The graph legend updates when you click outside the field. -1. In the Panel editor on the right, under **Settings**, change the panel title to "Traffic". -1. Click **Apply** in the top-right corner to save the panel and go back to the dashboard view. -1. Click the **Save dashboard** (disk) icon at the top of the dashboard to save your dashboard. -1. Enter a name in the **Dashboard name** field and then click **Save**. - -## Annotate events - -When things go bad, it often helps if you understand the context in which the failure occurred. Time of last deploy, system changes, or database migration can offer insight into what might have caused an outage. Annotations allow you to represent such events directly on your graphs. - -In the next part of the tutorial, we will simulate some common use cases that someone would add annotations for. - -1. To manually add an annotation, click anywhere in your graph, then click **Add annotation**. -1. In **Description**, enter **Migrated user database**. -1. Click **Save**. - - Grafana adds your annotation to the graph. Hover your mouse over the base of the annotation to read the text. - -Grafana also lets you annotate a time interval, with _region annotations_. - -Add a region annotation: - -1. Press Ctrl (or Cmd on macOS), then click and drag across the graph to select an area. -1. In **Description**, enter **Performed load tests**. -1. In **Tags**, enter **testing**. - -Manually annotating your dashboard is fine for those single events. For regularly occurring events, such as deploying a new release, Grafana supports querying annotations from one of your data sources. Let's create an annotation using the Loki data source we added earlier. - -1. At the top of the dashboard, click the **Dashboard settings** (gear) icon. -1. Go to **Annotations** and click **Add annotation query**. -1. In **Name**, enter **Errors**. -1. In **Data source**, select **Loki**. -1. In **Query**, enter the following query: - - ``` - {filename="/var/log/tns-app.log"} |= "error" - ``` - - - -1. Click **Add**. Grafana displays the Annotations list, with your new annotation. -1. Click the **Go back** arrow to return to your dashboard. -1. At the top of your dashboard, there is now a toggle to display the results of the newly created annotation query. Press it so that it's enabled. - -The log lines returned by your query are now displayed as annotations in the graph. - -Being able to combine data from multiple data sources in one graph allows you to correlate information from both Prometheus and Loki. - -Annotations also work very well alongside alerts. In the next and final section, we will set up an alert for our app `grafana.news` and then we will trigger it. This will provide a quick intro to our new Alerting platform. - -## Create a Grafana Managed Alert - -Alerts allow you to identify problems in your system moments after they occur. By quickly identifying unintended changes in your system, you can minimize disruptions to your services. - -Grafana's new alerting platform debuted with Grafana 8. A year later, with Grafana 9, it became the default alerting method. In this step we will create a Grafana Managed Alert. Then we will trigger our new alert and send a test message to a dummy endpoint. - -The most basic alert consists of two parts: - -1. A _Contact Point_ - A Contact point defines how Grafana delivers an alert. When the conditions of an _alert rule_ are met, Grafana notifies the contact points, or channels, configured for that alert. Some popular channels include email, webhooks, Slack notifications, and PagerDuty notifications. -1. An _Alert rule_ - An Alert rule defines one or more _conditions_ that Grafana regularly evaluates. When these evaluations meet the rule's criteria, the alert is triggered. - -To begin, let's set up a webhook Contact Point. Once we have a usable endpoint, we'll write an alert rule and trigger a notification. - -### Create a Contact Point for Grafana Managed Alerts - -In this step, we'll set up a new Contact Point. This contact point will use the _webhooks_ channel. In order to make this work, we also need an endpoint for our webhook channel to receive the alert. We will use [requestbin.com](https://requestbin.com) to quickly set up that test endpoint. This way we can make sure that our alert is actually sending a notification somewhere. - -1. Browse to [requestbin.com](https://requestbin.com). -1. Under the **Create Request Bin** button, click the **public bin** link. - -Your request bin is now waiting for the first request. - -1. Copy the endpoint URL. - -Next, let's configure a Contact Point in Grafana's Alerting UI to send notifications to our Request Bin. - -1. Return to Grafana. In Grafana's sidebar, hover your cursor over the **Alerting** (bell) icon and then click **Contact points**. -1. Click **+ New contact point**. -1. In **Name**, write **RequestBin**. -1. In **Contact point type**, choose **Webhook**. -1. In **Url**, paste the endpoint to your request bin. -1. Click **Test** to send a test alert to your request bin. -1. Navigate back to the request bin you created earlier. On the left side, there's now a `POST /` entry. Click it to see what information Grafana sent. -1. Return to Grafana and click **Save contact point**. - -We have now created a dummy webhook endpoint and created a new Alerting Contact Point in Grafana. Now we can create an alert rule and link it to this new channel. - -### Add an Alert Rule to Grafana - -Now that Grafana knows how to notify us, it's time to set up an alert rule: - -1. In Grafana's sidebar, hover the cursor over the **Alerting** (bell) icon and then click **Alert rules**. -1. Click **+ New alert rule**. -1. For **Section 1**, name the rule `fundamentals-test`, and set **Rule type** to **Grafana Managed Alert**. For **Folder** type `fundamentals` and in the box that appears, press **Create: fundamentals**. -1. For **Section 2**, find the **query A** box. Choose your Prometheus datasource and enter the same query that we used in our earlier panel: `sum(rate(tns_request_duration_seconds_count[5m])) by(route)`. Press **Run queries**. You should see some data in the graph. -1. Now scroll down to the **query B** box. For **Operation** choose `Classic condition`. [You can read more about classic and multi-dimensional conditions here](/docs/grafana/latest/alerting/unified-alerting/alerting-rules/create-grafana-managed-rule/#single-and-multi-dimensional-rule). For conditions enter the following: `WHEN last() OF A IS ABOVE 0.2` -1. In **Section 3**, enter `30s` for the **Evaluate every** field. For the purposes of this tutorial, the evaluation interval is intentionally short. This makes it easier to test. In the **for** field, enter `0m`. This setting makes Grafana wait until an alert has fired for a given time before Grafana sends the notification. -1. In **Section 4**, you can add some sample text to your summary message. [Read more about message templating here](/docs/grafana/latest/alerting/unified-alerting/message-templating/). -1. Click **Save and exit** at the top of the page. -1. In Grafana's sidebar, hover the cursor over the **Alerting** (bell) icon and then click **Notification policies**. -1. Under **Root policy**, press **Edit** and change the **Default contact point** to **RequestBin**. As a system grows, admins can use the **Notification policies** setting to organize and match alert rules to specific contact points. - -### Trigger a Grafana Managed Alert - -We have now configured an alert rule and a contact point. Now let's see if we can trigger a Grafana Managed Alert by generating some traffic on our sample application. - -1. Browse to [localhost:8081](http://localhost:8081). -1. Repeatedly click the vote button or refresh the page to generate a traffic spike. - -Once the query `sum(rate(tns_request_duration_seconds_count[5m])) by(route)` returns a value greater than `0.2` Grafana will trigger our alert. Browse to the Request Bin we created earlier and find the sent Grafana alert notification with details and metadata. - -## Summary - -In this tutorial you learned about fundamental features of Grafana. To do so, we ran several Docker containers on your local machine. When you are ready to clean up this local tutorial environment, run the following command: - -``` -docker-compose down -v -``` - -### Learn more - -Check out the links below to continue your learning journey with Grafana's LGTM stack. - -- [Prometheus](/docs/grafana/latest/features/datasources/prometheus/) -- [Loki](/docs/grafana/latest/features/datasources/loki/) -- [Explore](/docs/grafana/latest/features/explore/) -- [Alerting Overview](/docs/grafana/latest/alerting/) -- [Alert rules](/docs/grafana/latest/alerting/create-alerts/) -- [Contact Points](/docs/grafana/latest/alerting/notifications/) diff --git a/docs/sources/tutorials/iis/index.md b/docs/sources/tutorials/iis/index.md deleted file mode 100644 index ec4a190a128..00000000000 --- a/docs/sources/tutorials/iis/index.md +++ /dev/null @@ -1,146 +0,0 @@ ---- -title: Use IIS with URL Rewrite as a reverse proxy -summary: Learn how to set up Grafana behind IIS with URL Rewrite. -description: Learn how to set up Grafana behind IIS with URL Rewrite. -id: iis -categories: ['administration'] -tags: ['advanced'] -status: Published -authors: ['grafana_labs'] -Feedback Link: https://github.com/grafana/tutorials/issues/new -aliases: ['/docs/grafana/latest/tutorials/iis/'] ---- - -# Use IIS with URL Rewrite as a reverse proxy - -If you want Grafana to be a subpath/subfolder under a website in IIS then the Application Request Routing (ARR) and URL Rewrite modules for ISS can be used to support this. - -Example: - -- Parent site: http://yourdomain.com:8080 -- Grafana: http://localhost:3000 - -Grafana as a subpath: http://yourdomain.com:8080/grafana - -Other Examples: - -- If the application is only served on the local server, the parent site can also look like http://localhost:8080. -- If your domain is served using https on port 443, and thus the port is not normally entered in the address of your site, then the need to specify a port for the parent site in the configuration steps below can be eliminated. - -## Setup - -Install the URL Rewrite module for IIS. - -- Download and install the URL Rewrite module for IIS: https://www.iis.net/downloads/microsoft/url-rewrite - -You will also need the Application Request Routing (ARR) module for IIS for proxy forwarding - -- Download and install ARR module for IIS: https://www.iis.net/downloads/microsoft/application-request-routing - -## Grafana Config - -The Grafana config can be set by creating a file named/editing the existing file named `custom.ini` in the `conf` subdirectory of your Grafana installation. See the [installation instructions](http://docs.grafana.org/installation/windows/#configure) for more details. - -Using the example from above, if the subpath is `grafana` (you can set this to whatever is required) and the parent site is `yourdomain.com:8080`, then you would add this to the `custom.ini` config file: - -```bash -[server] -domain = yourdomain.com:8080 -root_url = %(protocol)s://%(domain)s/grafana/ -``` - -Restart the Grafana server after changing the config file. - -Configured address to serve Grafana: http://yourdomain.com:8080/grafana - ---- - -If you already have a subpath on your domain, configure it as follows: - -- Your Parent Site Address: http://yourdomain.com/existingsubpath - -```bash -[server] -domain = yourdomain.com/existingsubpath -root_url = %(protocol)s://%(domain)s/grafana/ -``` - -Restart the Grafana server after changing the config file. - -Configured address to serve Grafana: http://yourdomain.com/existingsubpath/grafana - -## IIS Config - -### Step 1: Forward Proxy - -1. Open the IIS Manager and click on the server -2. In the admin console for the server, double click on the Application Request Routing option: -3. Click the `Server Proxy Settings` action on the right-hand pane -4. Select the `Enable proxy` checkbox so that it is enabled -5. Click `Apply` and proceed with the URL Rewriting configuration - -**Note:** If you don't enable the Forward Proxy, you will most likely get 404 Not Found if you only apply the URL Rewrite rule - -### Step 2: URL Rewriting - -1. In the IIS Manager, click on the website that grafana will run under. For example, select the website that is bound to the http://yourdomain.com domain. -2. In the admin console for this website, double click on the URL Rewrite option: - -{{< figure src="/static/img/docs/tutorials/IIS_admin_console.png" max-width="800px" >}} - -3. Click on the `Add Rule(s)...` action -4. Choose the Blank Rule template for an Inbound Rule - -{{< figure src="/static/img/docs/tutorials/IIS_add_inbound_rule.png" max-width="800px" >}} - -5. Create an Inbound Rule for the website with the following settings: - -- pattern: `grafana(/)?(.*)` (if you have customised the subpath that will be used, use that instead of `grafana`) -- check the `Ignore case` checkbox -- rewrite URL set to `http://localhost:3000/{R:2}` -- check the `Append query string` checkbox -- check the `Stop processing of subsequent rules` checkbox - -{{< figure src="/static/img/docs/tutorials/IIS_url_rewrite.png" max-width="800px" >}} - -6. If your version of Grafana is greater than 8.3.5, you also need to configure the reverse proxy to preserve host headers. - -- This can be achieved by configuring the IIS config file by running this in a cmd prompt - `%windir%\system32\inetsrv\appcmd.exe set config -section:system.webServer/proxy -preserveHostHeader:true /commit:apphost` -- More information here https://github.com/grafana/grafana/issues/45261 - -Finally, navigate to `http://yourdomain.com:8080/grafana` and you should come to the Grafana login page. - -## Troubleshooting - -### 404 error - -When navigating to the Grafana URL (`http://yourdomain.com:8080/grafana`) and a `HTTP Error 404.0 - Not Found` error is returned, then either: - -- The pattern for the Inbound Rule is incorrect. Edit the rule, click on the `Test pattern...` button, test the part of the URL after `http://yourdomain.com:8080/` and make sure it matches. For `grafana/login` the test should return 3 capture groups: {R:0}: `grafana` {R:1}: `/` and {R:2}: `login`. -- The `root_url` setting in the Grafana config file does not match the parent URL with subpath. - -### Grafana Website only shows text with no images or css - -{{< figure src="/static/img/docs/tutorials/IIS_proxy_error.png" max-width="800px" >}} - -1. The `root_url` setting in the Grafana config file does not match the parent URL with subpath. This could happen if the root_url is commented out by mistake (`;` is used for commenting out a line in .ini files): - - `; root_url = %(protocol)s://%(domain)s/grafana/` - -2. or if the subpath in the `root_url` setting does not match the subpath used in the pattern in the Inbound Rule in IIS: - - `root_url = %(protocol)s://%(domain)s/grafana/` - - pattern in Inbound Rule: `wrongsubpath(/)?(.*)` - -3. or if the Rewrite URL in the Inbound Rule is incorrect. - - The Rewrite URL should not include the subpath. - - The Rewrite URL should contain the capture group from the pattern matching that returns the part of the URL after the subpath. The pattern used above returns three capture groups and the third one {R:2} returns the part of the URL after `http://yourdomain.com:8080/grafana/`. - -### You see an 'Error updating options: origin not allowed' error - -- Ensure you have undertaken step 6 above, to configure IIS to preserve host headers by edit IIS config by running this in cmd prompt: - `%windir%\system32\inetsrv\appcmd.exe set config -section:system.webServer/proxy -preserveHostHeader:true /commit:apphost` diff --git a/docs/sources/tutorials/install-grafana-on-raspberry-pi/index.md b/docs/sources/tutorials/install-grafana-on-raspberry-pi/index.md deleted file mode 100644 index 16870ac0f92..00000000000 --- a/docs/sources/tutorials/install-grafana-on-raspberry-pi/index.md +++ /dev/null @@ -1,147 +0,0 @@ ---- -title: Install Grafana on Raspberry Pi -summary: Get Grafana set up on your Raspberry Pi. -description: Get Grafana set up on your Raspberry Pi. -id: install-grafana-on-raspberry-pi -categories: ['administration'] -tags: ['beginner'] -authors: ['grafana_labs'] -Feedback Link: https://github.com/grafana/tutorials/issues/new ---- - -## Introduction - -The Raspberry Pi is a tiny, affordable, yet capable computer that can run a range of different applications. Even Grafana! - -Many people are running Grafana on Raspberry Pi as a way to monitor their home, for things like indoor temperature, humidity, or energy usage. - -In this tutorial, you'll: - -- Set up a Raspberry Pi using a version of Raspberry Pi OS (previously called "Raspbian") that does not require you to connect a keyboard or monitor (this is often called "headless"). -- Install Grafana on your Raspberry Pi. - -{{% class "prerequisite-section" %}} - -### Prerequisites - -- Raspberry Pi -- SD card - {{% /class %}} - -## Set up your Raspberry Pi - -Before we can install Grafana, you first need to set up your Raspberry Pi. - -For this tutorial, you'll configure your Raspberry Pi to be _headless_. This means you don't need to connect a monitor, keyboard, or mouse to your Raspberry Pi. All configuration is done from your regular computer. - -#### Download and install Raspberry Pi Imager - -Before we get started, you need to download and install the [Raspberry Pi Imager](https://www.raspberrypi.org/software/). - -We'll use the Raspberry Pi Imager to flash the operating system image to the SD card. You download the imager directly from the official Raspberry Pi website and it's available for Ubuntu Linux, macOS, and Windows. - -Follow the directions on the website to download and install the imager. - -#### Install Raspberry Pi OS - -Now it is time to install Raspberry Pi OS. - -1. Insert the SD card into your regular computer from which you plan to install Raspberry Pi OS. -1. Run the Raspberry Pi Imager that you downloaded and installed. -1. To select an operating system, click **Choose OS** in the imager. You will be shown a list of available options. -1. From the list, select **Raspberry Pi OS (other)** and then select **Raspberry Pi OS Lite**, which is a Debian-based operating system for the Raspberry Pi. Since you're going to run a headless Raspberry Pi, you won't need the desktop dependencies. -1. To select where you want to put the operating system image, click **Choose Storage** in the imager and then select the SD card you already inserted into your computer. -1. The final step in the imager to click **Write**. When you do, the imager will write the Raspberry Pi OS Lite image to the SD card and verify that it has been written correctly. -1. Eject the SD card from your computer, and insert it again. - -While you _could_ fire up the Raspberry Pi now, we don't yet have any way of accessing it. - -1. Create an empty file called `ssh` in the boot directory. This enables SSH so that you can log in remotely. - - The next step is only required if you want the Raspberry Pi to connect to your wireless network. Otherwise, connect the it to your network by using a network cable. - -1. **(Optional)** Create a file called `wpa_supplicant.conf` in the boot directory: - - ``` - ctrl_interface=/var/run/wpa_supplicant - update_config=1 - country= - - network={ - ssid="" - psk="" - } - ``` - -All the necessary files are now on the SD card. Let's start up the Raspberry Pi. - -1. Eject the SD card and insert it into the SD card slot on the Raspberry Pi. -1. Connect the power cable and make sure the LED lights are on. -1. Find the IP address of the Raspberry Pi. Usually you can find the address in the control panel for your WiFi router. - -#### Connect remotely via SSH - -1. Open up your terminal and enter the following command: - ``` - ssh pi@ - ``` -1. SSH warns you that the authenticity of the host can't be established. Type "yes" to continue connecting. -1. When asked for a password, enter the default password: `raspberry`. -1. Once you're logged in, change the default password: - ``` - passwd - ``` - -Congratulations! You've now got a tiny Linux machine running that you can hide in a closet and access from your normal workstation. - -## Install Grafana - -Now that you've got the Raspberry Pi up and running, the next step is to install Grafana. - -1. Add the APT key used to authenticate packages: - - ``` - wget -q -O - https://packages.grafana.com/gpg.key | sudo apt-key add - - ``` - -1. Add the Grafana APT repository: - - ``` - echo "deb https://packages.grafana.com/oss/deb stable main" | sudo tee -a /etc/apt/sources.list.d/grafana.list - ``` - -1. Install Grafana: - ``` - sudo apt-get update - sudo apt-get install -y grafana - ``` - -Grafana is now installed, but not yet running. To make sure Grafana starts up even if the Raspberry Pi is restarted, we need to enable and start the Grafana Systemctl service. - -1. Enable the Grafana server: - - ``` - sudo /bin/systemctl enable grafana-server - ``` - -1. Start the Grafana server: - - ``` - sudo /bin/systemctl start grafana-server - ``` - - Grafana is now running on the machine and is accessible from any device on the local network. - -1. Open a browser and go to `http://:3000`, where the IP address is the address that you used to connect to the Raspberry Pi earlier. You're greeted with the Grafana login page. -1. Log in to Grafana with the default username `admin`, and the default password `admin`. -1. Change the password for the admin user when asked. - -Congratulations! Grafana is now running on your Raspberry Pi. If the Raspberry Pi is ever restarted or turned off, Grafana will start up whenever the machine regains power. - -## Summary - -If you want to use Grafana without having to go through a full installation process, check out [Grafana Cloud](/products/cloud/), which is designed to get users up and running quickly and easily. Grafana Cloud offers a forever free plan that is genuinely useful for hobbyists, testing, and small teams. - -### Learn more - -- [Raspberry Pi Documentation](https://www.raspberrypi.org/documentation/) diff --git a/docs/sources/tutorials/integrate-hubot/index.md b/docs/sources/tutorials/integrate-hubot/index.md deleted file mode 100644 index 0d0af2f3821..00000000000 --- a/docs/sources/tutorials/integrate-hubot/index.md +++ /dev/null @@ -1,118 +0,0 @@ ---- -title: Integrate Hubot with Grafana -summary: Learn how to integrate Hubot with Grafana -description: Learn how to integrate Hubot with Grafana -id: integrate-hubot -categories: ['administration'] -tags: ['advanced'] -status: Published -authors: ['grafana_labs'] -Feedback Link: https://github.com/grafana/tutorials/issues/new -aliases: ['/docs/grafana/latest/tutorials/hubot_howto/'] ---- - -# Integrate Hubot with Grafana - -Grafana 2.0 shipped with a great feature that enables it to render any graph or panel to a PNG image. - -No matter what data source you are using, the PNG image of the Graph will look the same as it does in your browser. - -This guide will show you how to install and configure the [Hubot-Grafana](https://github.com/stephenyeargin/hubot-grafana) plugin. This plugin allows you to tell hubot to render any dashboard or graph right from a channel in Slack, Hipchat or Basecamp. The bot will respond with an image of the graph and a link that will take you to the graph. - -> _Amazon S3 Required_: The hubot-grafana script will upload the rendered graphs to Amazon S3. This -> is so Hipchat and Slack can show them reliably (they require the image to be publicly available). - -{{< figure src="/static/img/docs/tutorials/hubot_grafana.png" max-width="800px" >}} - -## What is Hubot? - -[Hubot](https://hubot.github.com/) is an universal and extensible chat bot that can be used with many chat services and has a huge library of third party plugins that allow you to automate anything from your chat rooms. - -## Install Hubot - -Hubot is very easy to install and host. If you do not already have a bot up and running please read the official [Getting Started With Hubot](https://hubot.github.com/docs/) guide. - -## Install Hubot-Grafana script - -In your Hubot project repo install the Grafana plugin using `npm`: - -```bash -npm install hubot-grafana --save -``` - -Edit the file external-scripts.json, and add hubot-grafana to the list of plugins. - -```json -["hubot-pugme", "hubot-shipit", "hubot-grafana"] -``` - -## Configure - -The `hubot-grafana` plugin requires a number of environment variables to be set in order to work properly. - -```bash -export HUBOT_GRAFANA_HOST=https://play.grafana.org -export HUBOT_GRAFANA_API_KEY=abcd01234deadbeef01234 -export HUBOT_GRAFANA_S3_BUCKET=mybucket -export HUBOT_GRAFANA_S3_ACCESS_KEY_ID=ABCDEF123456XYZ -export HUBOT_GRAFANA_S3_SECRET_ACCESS_KEY=aBcD01234dEaDbEef01234 -export HUBOT_GRAFANA_S3_PREFIX=graphs -export HUBOT_GRAFANA_S3_REGION=us-standard -``` - -### Grafana server side rendering - -The hubot plugin will take advantage of the Grafana server side rendering feature that can render any panel on the server using phantomjs. Grafana ships with a phantomjs binary (Linux only). - -To verify that this feature works try the `Direct link to rendered image` link in the panel share dialog. If you do not get an image when opening this link verify that the required font packages are installed for phantomjs to work. - -### Grafana API Key - -{{< figure src="/static/img/docs/v2/orgdropdown_api_keys.png" max-width="150px" class="docs-image--right">}} - -You need to set the environment variable `HUBOT_GRAFANA_API_KEY` to a Grafana API Key. You can add these from the API Keys page which you find in the Organization dropdown. - -### Amazon S3 - -The `S3` options are optional but for the images to work properly in services like Slack and Hipchat they need to publicly available. By specifying the `S3` options the hubot-grafana script will publish the rendered panel to `S3` and it will use that URL when it posts to Slack or Hipchat. - -## Hubot commands - -- `hubot graf list` - - Lists the available dashboards -- `hubot graf db graphite-carbon-metrics` - - Graph all panels in the dashboard -- `hubot graf db graphite-carbon-metrics:3` - - Graph only panel with id 3 of a particular dashboard -- `hubot graf db graphite-carbon-metrics:cpu` - - Graph only the panels containing "cpu" (case insensitive) in the title -- `hubot graf db graphite-carbon-metrics now-12hr` - - Get a dashboard with a window of 12 hours ago to now -- `hubot graf db graphite-carbon-metrics now-24hr now-12hr` - - Get a dashboard with a window of 24 hours ago to 12 hours ago -- `hubot graf db graphite-carbon-metrics:3 now-8d now-1d` - - Get only the third panel of a particular dashboard with a window of 8 days ago to yesterday -- `hubot graf db graphite-carbon-metrics host=carbon-a` - - Get a templated dashboard with the `$host` parameter set to `carbon-a` - -## Aliases - -Some of the hubot commands above can lengthy and you might have to remember the dashboard slug (url id). If you have a few favorite graphs you want to be able check up on often (let's say from your mobile) you can create hubot command aliases with the hubot script `hubot-alias`. - -Install it: - -```bash -npm i --save hubot-alias -``` - -Now add `hubot-alias` to the list of plugins in `external-scripts.json` and restart hubot. - -Now you can add an alias like this: - -- `hubot alias graf-lb=graf db loadbalancers:2 now-20m` - -{{< figure src="/static/img/docs/tutorials/hubot_grafana2.png" max-width="800px" >}} - -## Summary - -Grafana is going to ship with integrated Slack and Hipchat features some day but you do not have to wait for that. Grafana 2 shipped with a very clever server side rendering feature that can render any panel to a png using phantomjs. The hubot plugin for Grafana is something you can install and use today! diff --git a/docs/sources/tutorials/provision-dashboards-and-data-sources/index.md b/docs/sources/tutorials/provision-dashboards-and-data-sources/index.md deleted file mode 100644 index 4554b67073f..00000000000 --- a/docs/sources/tutorials/provision-dashboards-and-data-sources/index.md +++ /dev/null @@ -1,260 +0,0 @@ ---- -title: Provision dashboards and data sources -summary: Treat your configuration as code. -description: Treat your configuration as code. -id: provision-dashboards-and-data-sources -categories: ['administration'] -tags: ['intermediate'] -authors: ['grafana_labs'] -Feedback Link: https://github.com/grafana/tutorials/issues/new -weight: 40 ---- - -## Introduction - -Learn how you can reuse dashboards and data sources across multiple teams by provisioning Grafana from version-controlled configuration files. - -In this tutorial, you'll: - -- Provision dashboards. -- Provision data sources. - -{{% class "prerequisite-section" %}} - -### Prerequisites - -- Grafana 7.0 -- Administrator privileges on the system you are doing the tutorial on - {{% /class %}} - -## Configuration as code - -Configuration as code is the practice of storing the configuration of your system as a set of version controlled, human-readable configuration files, rather than in a database. These configuration files can be reused across environments to avoid duplicated resources. - -As the number of dashboards and data sources grows within your organization, manually managing changes can become tedious and error-prone. Encouraging reuse becomes important to avoid multiple teams redesigning the same dashboards. - -Grafana supports configuration as code through _provisioning_. The resources that currently supports provisioning are: - -- [Dashboards](/docs/grafana/latest/administration/provisioning/#dashboards) -- [Data sources](/docs/grafana/latest/administration/provisioning/#datasources) -- [Alert notification channels](/docs/grafana/latest/administration/provisioning/#alert-notification-channels) - -## Set the provisioning directory - -Before you can start provisioning resources, Grafana needs to know where to find the _provisioning directory_. The provisioning directory contains configuration files that are applied whenever Grafana starts and continuously updated while running. - -By default, Grafana looks for a provisioning directory in the configuration directory (grafana > conf) on the system where Grafana is installed. However, if you are a Grafana Administrator, then you might want to place the config files in a shared resource like a network folder, so you would need to change the path to the provisioning directory. - -You can set a different path by setting the `paths.provisioning` property in the main config file: - -```ini -[paths] -provisioning = -``` - -For more information about configuration files, refer to [Configuration](/docs/grafana/latest/installation/configuration/) in the [Grafana documentation](/docs/grafana/latest/). - -The provisioning directory assumes the following structure: - -``` -provisioning/ - datasources/ - - dashboards/ - - notifiers/ - -``` - -Next, we'll look at how to provision a data source. - -## Provision a data source - -Each data source provisioning config file contains a _manifest_ that specifies the desired state of a set of provisioned data sources. - -At startup, Grafana loads the configuration files and provisions the data sources listed in the manifests. - -Let's configure a [TestData DB](/docs/grafana/latest/features/datasources/testdata/) data source that you can use for your dashboards. - -#### Create a data source manifest - -1. In the `provisioning/datasources/` directory, create a file called `default.yaml` with the following content: - - ```yaml - apiVersion: 1 - - datasources: - - name: TestData DB - type: testdata - ``` - -1. Restart Grafana to load the new changes. -1. In the sidebar, hover the cursor over the **Configuration** (gear) icon and click **Data Sources**. The TestData DB appears in the list of data sources. - -> The configuration options can vary between different types of data sources. For more information on how to configure a specific data source, refer to [Data sources](/docs/grafana/latest/administration/provisioning/#datasources). - -## Provision a dashboard - -Each dashboard config file contains a manifest that specifies the desired state of a set of _dashboard providers_. - -A dashboard provider tells Grafana where to find the dashboard definitions and where to put them. - -Grafana regularly checks for changes to the dashboard definitions (by default every 10 seconds). - -Let's define a dashboard provider so that Grafana knows where to find the dashboards we want to provision. - -#### Define a dashboard provider - -In the `provisioning/dashboards/` directory, create a file called `default.yaml` with the following content: - -```yaml -apiVersion: 1 - -providers: - - name: Default # A uniquely identifiable name for the provider - folder: Services # The folder where to place the dashboards - type: file - options: - path: - - # Default path for Windows: C:/Program Files/GrafanaLabs/grafana/public/dashboards - # Default path for Linux is: /var/lib/grafana/dashboards -``` - -For more information on how to configure dashboard providers, refer to [Dashboards](/docs/grafana/latest/administration/provisioning/#dashboards). - -#### Create a dashboard definition - -1. In the dashboard definitions directory you specified in the dashboard provider, i.e. `options.path`, create a file called `cluster.json` with the following content: - - ```json - { - "__inputs": [], - "__requires": [], - "annotations": { - "list": [] - }, - "editable": false, - "gnetId": null, - "graphTooltip": 0, - "hideControls": false, - "id": null, - "links": [], - "panels": [ - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "TestData DB", - "fill": 1, - "gridPos": { - "h": 8, - "w": 24, - "x": 0, - "y": 0 - }, - "id": 2, - "legend": { - "alignAsTable": false, - "avg": false, - "current": false, - "max": false, - "min": false, - "rightSide": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "repeat": null, - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "CPU Usage", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "refresh": "", - "rows": [], - "schemaVersion": 16, - "style": "dark", - "tags": ["kubernetes"], - "templating": { - "list": [] - }, - "time": { - "from": "now-6h", - "to": "now" - }, - "timepicker": { - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] - }, - "timezone": "browser", - "title": "Cluster", - "version": 0 - } - ``` - -1. Restart Grafana to provision the new dashboard or wait 10 seconds for Grafana to automatically create the dashboard. -1. In the sidebar, hover the cursor over **Dashboards** (squares) icon, and then click **Manage**. The dashboard appears in a **Services** folder. - -> If you don't specify an `id` in the dashboard definition, then Grafana assigns one during provisioning. You can set the `id` yourself if you want to reference the dashboard from other dashboards. Be careful to not use the same `id` for multiple dashboards, as this will cause a conflict. - -## Summary - -In this tutorial you learned how you to reuse dashboards and data sources across multiple teams by provisioning Grafana from version-controlled configuration files. - -Dashboard definitions can get unwieldy as more panels and configurations are added to them. There are a number of open source tools available to make it easier to manage dashboard definitions: - -- [grafana-dash-gen](https://github.com/uber/grafana-dash-gen) (Javascript) -- [grafanalib](https://github.com/weaveworks/grafanalib) (Python) -- [grafonnet-lib](https://github.com/grafana/grafonnet-lib) (Jsonnet) -- [grafyaml](https://docs.openstack.org/infra/grafyaml/) (YAML) - -### Learn more - -- [Provisioning Grafana](/docs/grafana/latest/administration/provisioning/) diff --git a/docs/sources/tutorials/run-grafana-behind-a-proxy/index.md b/docs/sources/tutorials/run-grafana-behind-a-proxy/index.md deleted file mode 100644 index 10be2dee35f..00000000000 --- a/docs/sources/tutorials/run-grafana-behind-a-proxy/index.md +++ /dev/null @@ -1,222 +0,0 @@ ---- -title: Run Grafana behind a reverse proxy -summary: Learn how to run Grafana behind a reverse proxy -description: Learn how to run Grafana behind a reverse proxy -id: run-grafana-behind-a-proxy -categories: ['administration'] -tags: ['advanced'] -status: Published -authors: ['grafana_labs'] -Feedback Link: https://github.com/grafana/tutorials/issues/new -aliases: ['/docs/grafana/latest/installation/behind_proxy/'] ---- - -## Introduction - -In this tutorial, you'll configure Grafana to run behind a reverse proxy. - -When running Grafana behind a proxy, you need to configure the domain name to let Grafana know how to render links and redirects correctly. - -- In the Grafana configuration file, change `server.domain` to the domain name you'll be using: - -```bash -[server] -domain = example.com -``` - -- Restart Grafana for the new changes to take effect. - -You can also serve Grafana behind a _sub path_, such as `http://example.com/grafana`. - -To serve Grafana behind a sub path: - -- Include the sub path at the end of the `root_url`. -- Set `serve_from_sub_path` to `true`. - -```bash -[server] -domain = example.com -root_url = %(protocol)s://%(domain)s:%(http_port)s/grafana/ -serve_from_sub_path = true -``` - -Next, you need to configure your reverse proxy. - -## Configure NGINX - -[NGINX](https://www.nginx.com) is a high performance load balancer, web server, and reverse proxy. - -- In your NGINX configuration file inside `http` section, add the following: - -```nginx -# this is required to proxy Grafana Live WebSocket connections. -map $http_upgrade $connection_upgrade { - default upgrade; - '' close; -} - -upstream grafana { - server localhost:3000; -} - -server { - listen 80; - root /usr/share/nginx/html; - index index.html index.htm; - - location / { - proxy_set_header Host $http_host; - proxy_pass http://grafana; - } - - # Proxy Grafana Live WebSocket connections. - location /api/live/ { - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $http_host; - proxy_pass http://grafana; - } -} -``` - -- Reload the NGINX configuration. -- Navigate to port 80 on the machine NGINX is running on. You're greeted by the Grafana login page. - -For Grafana Live which uses WebSocket connections you may have to raise Nginx [worker_connections](https://nginx.org/en/docs/ngx_core_module.html#worker_connections) option which is 512 by default – which limits the number of possible concurrent connections with Grafana Live. - -Also, be aware that the above configuration will work only when the `proxy_pass` value for `location /` is a literal string. If you are using a variable here, [read this GitHub issue](https://github.com/grafana/grafana/issues/18299). You will need to add [an appropriate NGINX rewrite rule](https://www.nginx.com/blog/creating-nginx-rewrite-rules/). - -To configure NGINX to serve Grafana under a _sub path_, update the `location` block: - -```nginx -# this is required to proxy Grafana Live WebSocket connections. -map $http_upgrade $connection_upgrade { - default upgrade; - '' close; -} - -upstream grafana { - server localhost:3000; -} - -server { - listen 80; - root /usr/share/nginx/www; - index index.html index.htm; - - location /grafana/ { - rewrite ^/grafana/(.*) /$1 break; - proxy_set_header Host $http_host; - proxy_pass http://grafana; - } - - # Proxy Grafana Live WebSocket connections. - location /grafana/api/live/ { - rewrite ^/grafana/(.*) /$1 break; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $http_host; - proxy_pass http://grafana; - } -} -``` - -## Configure HAProxy - -To configure HAProxy to serve Grafana under a _sub path_: - -```bash -frontend http-in - bind *:80 - use_backend grafana_backend if { path /grafana } or { path_beg /grafana/ } - -backend grafana_backend - # Requires haproxy >= 1.6 - http-request set-path %[path,regsub(^/grafana/?,/)] - - # Works for haproxy < 1.6 - # reqrep ^([^\ ]*\ /)grafana[/]?(.*) \1\2 - - server grafana localhost:3000 -``` - -## Configure IIS - -> IIS requires that the URL Rewrite module is installed. - -To configure IIS to serve Grafana under a _sub path_, create an Inbound Rule for the parent website in IIS Manager with the following settings: - -- pattern: `grafana(/)?(.*)` -- check the `Ignore case` checkbox -- rewrite URL set to `http://localhost:3000/{R:2}` -- check the `Append query string` checkbox -- check the `Stop processing of subsequent rules` checkbox - -This is the rewrite rule that is generated in the `web.config`: - -```xml - - - - - - - - -``` - -See the [tutorial on IIS URL Rewrites](/tutorials/iis/) for more in-depth instructions. - -## Configure Traefik - -[Traefik](https://traefik.io/traefik/) Cloud Native Reverse Proxy / Load Balancer / Edge Router - -Using the docker provider the following labels will configure the router and service for a domain or subdomain routing. - -```yaml -labels: - traefik.http.routers.grafana.rule: Host(`grafana.example.com`) - traefik.http.services.grafana.loadbalancer.server.port: 3000 -``` - -To deploy on a _sub path_ - -```yaml -labels: - traefik.http.routers.grafana.rule: Host(`example.com`) && PathPrefix(`/grafana`) - traefik.http.services.grafana.loadbalancer.server.port: 3000 -``` - -Examples using the file provider. - -```yaml -http: - routers: - grafana: - rule: Host(`grafana.example.com`) - service: grafana - services: - grafana: - loadBalancer: - servers: - - url: http://192.168.30.10:3000 -``` - -```yaml -http: - routers: - grafana: - rule: Host(`example.com`) && PathPrefix(`/grafana`) - service: grafana - services: - grafana: - loadBalancer: - servers: - - url: http://192.168.30.10:3000 -``` - -## Summary - -In this tutorial you learned how to run Grafana behind a reverse proxy. diff --git a/docs/sources/tutorials/stream-metrics-from-telegraf-to-grafana/index.md b/docs/sources/tutorials/stream-metrics-from-telegraf-to-grafana/index.md deleted file mode 100644 index 822006b2fcf..00000000000 --- a/docs/sources/tutorials/stream-metrics-from-telegraf-to-grafana/index.md +++ /dev/null @@ -1,101 +0,0 @@ ---- -title: Stream metrics from Telegraf to Grafana -summary: Use Telegraf to stream live metrics to Grafana. -description: Use Telegraf to stream live metrics to Grafana. -id: stream-metrics-from-telegraf-to-grafana -categories: ['administration'] -tags: ['beginner'] -status: Published -authors: ['grafana_labs'] -Feedback Link: https://github.com/grafana/tutorials/issues/new -weight: 75 ---- - -## Introduction - -Grafana v8 introduced streaming capabilities – a way to push data to UI panels in near real-time. In this tutorial we show how Grafana real-time streaming capabilities can be used together with Telegraf to instantly display system measurements. - -In this tutorial, you'll: - -- Setup Telegraf and output measurements directly to Grafana time-series panel in near real-time - -{{% class "prerequisite-section" %}} - -#### Prerequisites - -- Grafana 8.0+ -- Telegraf - {{% /class %}} - -## Run Grafana and create admin token - -1. Run Grafana following [installation instructions](/docs/grafana/latest/installation/) for your operating system -1. Log in and go to Configuration -> API Keys -1. Press "Add API key" button and create a new API token with **Admin** role - -## Configure and run Telegraf - -Telegraf is a plugin-driven server agent for collecting and sending metrics and events from databases, systems, and IoT sensors. - -You can install it following [official installation instructions](https://docs.influxdata.com/telegraf/latest/introduction/installation/). - -In this tutorial we will be using Telegraf HTTP output plugin to send metrics in Influx format to Grafana. We can use a configuration like this: - -``` -[agent] - interval = "1s" - flush_interval = "1s" - -[[inputs.cpu]] - percpu = false - totalcpu = true - -[[outputs.http]] - url = "http://localhost:3000/api/live/push/custom_stream_id" - data_format = "influx" - [outputs.http.headers] - Authorization = "Bearer " -``` - -Make sure to replace `` placeholder with your actual API key created in the previous step. Save this config into `telegraf.conf` file and run Telegraf pointing to this config file. Telegraf will periodically (once in a second) report the state of total CPU usage on a host to Grafana (which is supposed to be running on `http://localhost:3000`). Of course you can replace `custom_stream_id` to something more meaningful for your use case. - -Inside Grafana Influx data is converted to Grafana data frames and then frames are published to Grafana Live channels. In this case, the channel where CPU data will be published is `stream/custom_stream_id/cpu`. The `stream` scope is constant, the `custom_stream_id` namespace is the last part of API URL set in Telegraf configuration (`http://localhost:3000/api/live/push/telegraf`) and the path is `cpu` - the name of a measurement. - -The only thing left here is to create a dashboard with streaming data. - -## Create dashboard with streaming data - -1. Create new dashboard -1. Press Add empty panel -1. Select `-- Grafana --` datasource -1. Select `Live Measurements` query type -1. Find and select `stream/custom_stream_id/cpu` measurement for Channel field -1. Save dashboard changes - -After making these steps Grafana UI should subscribe to the channel `stream/custom_stream_id/cpu` and you should see CPU data updates coming from Telegraf in near real-time. - -## Stream using WebSocket endpoint - -If you aim for a high-frequency update sending then you may want to use the WebSocket output plugin of Telegraf (introduced in Telegraf v1.19.0) instead of the HTTP output plugin we used above. Configure WebSocket output plugin like this: - -``` -[agent] - interval = "500ms" - flush_interval = "500ms" - -[[inputs.cpu]] - percpu = false - totalcpu = true - -[[outputs.websocket]] - url = "ws://localhost:3000/api/live/push/custom_stream_id" - data_format = "influx" - [outputs.websocket.headers] - Authorization = "Bearer " -``` - -WebSocket avoids running all Grafana HTTP middleware on each request from Telegraf thus reducing Grafana backend CPU usage significantly. - -## Summary - -In this tutorial you learned how to use Telegraf to stream live metrics to Grafana. From 83ff974b86bb50a8f14b0c02f76224ebf45633b3 Mon Sep 17 00:00:00 2001 From: Levente Balogh Date: Fri, 27 Jan 2023 10:47:18 +0100 Subject: [PATCH 012/117] Datasources: Add the props for the "add datasource" event (#62227) chore: pass editLink to the add datasource user event --- public/app/features/datasources/state/actions.test.ts | 2 ++ public/app/features/datasources/state/actions.ts | 6 ++++-- public/app/features/datasources/tracking.ts | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/public/app/features/datasources/state/actions.test.ts b/public/app/features/datasources/state/actions.test.ts index 81e8cb6bf1c..5d06e3fb211 100644 --- a/public/app/features/datasources/state/actions.test.ts +++ b/public/app/features/datasources/state/actions.test.ts @@ -6,6 +6,7 @@ import { ThunkResult, ThunkDispatch } from 'app/types'; import { getMockDataSource } from '../__mocks__'; import * as api from '../api'; +import { DATASOURCES_ROUTES } from '../constants'; import { trackDataSourceCreated, trackDataSourceTested } from '../tracking'; import { GenericDataSourcePlugin } from '../types'; @@ -357,6 +358,7 @@ describe('addDataSource', () => { plugin_version: '1.2.3', datasource_uid: 'azure23', grafana_version: '1.0', + editLink: DATASOURCES_ROUTES.Edit.replace(':uid', 'azure23'), }); }); }); diff --git a/public/app/features/datasources/state/actions.ts b/public/app/features/datasources/state/actions.ts index 0ce72e58b28..eb8a772ac36 100644 --- a/public/app/features/datasources/state/actions.ts +++ b/public/app/features/datasources/state/actions.ts @@ -189,7 +189,7 @@ export function loadDataSourceMeta(dataSource: DataSourceSettings): ThunkResult< }; } -export function addDataSource(plugin: DataSourcePluginMeta, editLink = DATASOURCES_ROUTES.Edit): ThunkResult { +export function addDataSource(plugin: DataSourcePluginMeta, editRoute = DATASOURCES_ROUTES.Edit): ThunkResult { return async (dispatch, getStore) => { await dispatch(loadDataSources()); @@ -207,6 +207,7 @@ export function addDataSource(plugin: DataSourcePluginMeta, editLink = DATASOURC } const result = await api.createDataSource(newInstance); + const editLink = editRoute.replace(/:uid/gi, result.datasource.uid); await getDatasourceSrv().reload(); await contextSrv.fetchUserPermissions(); @@ -216,9 +217,10 @@ export function addDataSource(plugin: DataSourcePluginMeta, editLink = DATASOURC plugin_id: plugin.id, datasource_uid: result.datasource.uid, plugin_version: result.meta?.info?.version, + editLink, }); - locationService.push(editLink.replace(/:uid/gi, result.datasource.uid)); + locationService.push(editLink); }; } diff --git a/public/app/features/datasources/tracking.ts b/public/app/features/datasources/tracking.ts index 8323f5d1324..5de67f15209 100644 --- a/public/app/features/datasources/tracking.ts +++ b/public/app/features/datasources/tracking.ts @@ -24,6 +24,8 @@ type DataSourceCreatedProps = { plugin_id: string; /** The plugin version (especially interesting in external plugins - core plugins are aligned with grafana version) */ plugin_version?: string; + /** The URL that points to the edit page for the datasoruce. We are using this to be able to distinguish between the performance of different datasource edit locations. */ + editLink?: string; }; /** From 3ccafe3a5af8fc2a1539dad16277f210b4de7219 Mon Sep 17 00:00:00 2001 From: Alex Moreno Date: Fri, 27 Jan 2023 10:50:06 +0100 Subject: [PATCH 013/117] Alerting: Add is_paused attr to the POST alert rule group endpoint (#62253) Add is_paused attr to the POST alert rule group endpoint --- pkg/services/ngalert/api/api_ruler_validation.go | 1 + pkg/services/ngalert/api/tooling/api.json | 4 +++- .../ngalert/api/tooling/definitions/cortex-ruler.go | 1 + pkg/services/ngalert/api/tooling/post.json | 8 ++++++-- pkg/services/ngalert/api/tooling/spec.json | 9 ++++++--- public/api-merged.json | 4 +++- 6 files changed, 20 insertions(+), 7 deletions(-) diff --git a/pkg/services/ngalert/api/api_ruler_validation.go b/pkg/services/ngalert/api/api_ruler_validation.go index a8f86eee6ab..60c01732631 100644 --- a/pkg/services/ngalert/api/api_ruler_validation.go +++ b/pkg/services/ngalert/api/api_ruler_validation.go @@ -97,6 +97,7 @@ func validateRuleNode( RuleGroup: groupName, NoDataState: noDataState, ExecErrState: errorState, + IsPaused: ruleNode.GrafanaManagedAlert.IsPaused, } newAlertRule.For, err = validateForInterval(ruleNode) diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index 86cd1cc9299..41a5eb5e903 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -2005,6 +2005,9 @@ ], "type": "string" }, + "is_paused": { + "type": "boolean" + }, "no_data_state": { "enum": [ "Alerting", @@ -3495,7 +3498,6 @@ "type": "object" }, "gettableAlert": { - "description": "GettableAlert gettable alert", "properties": { "annotations": { "$ref": "#/definitions/labelSet" diff --git a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go index 4acab8ab44d..f93e9d4cc3b 100644 --- a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go +++ b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go @@ -374,6 +374,7 @@ type PostableGrafanaRule struct { UID string `json:"uid" yaml:"uid"` NoDataState NoDataState `json:"no_data_state" yaml:"no_data_state"` ExecErrState ExecutionErrorState `json:"exec_err_state" yaml:"exec_err_state"` + IsPaused bool `json:"is_paused" yaml:"is_paused"` } // swagger:model diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index ce003de9465..0604354ec23 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -2005,6 +2005,9 @@ ], "type": "string" }, + "is_paused": { + "type": "boolean" + }, "no_data_state": { "enum": [ "Alerting", @@ -3155,6 +3158,7 @@ "type": "object" }, "URL": { + "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.", "properties": { "ForceQuery": { "type": "boolean" @@ -3190,7 +3194,7 @@ "$ref": "#/definitions/Userinfo" } }, - "title": "URL is a custom URL type that allows validation at configuration load time.", + "title": "A URL represents a parsed URL (technically, a URI reference).", "type": "object" }, "Userinfo": { @@ -3496,7 +3500,6 @@ "type": "object" }, "gettableAlert": { - "description": "GettableAlert gettable alert", "properties": { "annotations": { "$ref": "#/definitions/labelSet" @@ -3606,6 +3609,7 @@ "type": "object" }, "gettableSilences": { + "description": "GettableSilences gettable silences", "items": { "$ref": "#/definitions/gettableSilence" }, diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index ce7a76b2117..4e0e3f16423 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -4501,6 +4501,9 @@ "Error" ] }, + "is_paused": { + "type": "boolean" + }, "no_data_state": { "type": "string", "enum": [ @@ -5887,7 +5890,6 @@ "$ref": "#/definitions/alertGroup" }, "alertGroups": { - "description": "AlertGroups alert groups", "type": "array", "items": { "$ref": "#/definitions/alertGroup" @@ -5993,7 +5995,6 @@ } }, "gettableAlert": { - "description": "GettableAlert gettable alert", "type": "object", "required": [ "labels", @@ -6050,6 +6051,7 @@ "$ref": "#/definitions/gettableAlert" }, "gettableAlerts": { + "description": "GettableAlerts gettable alerts", "type": "array", "items": { "$ref": "#/definitions/gettableAlert" @@ -6057,6 +6059,7 @@ "$ref": "#/definitions/gettableAlerts" }, "gettableSilence": { + "description": "GettableSilence gettable silence", "type": "object", "required": [ "comment", @@ -6106,6 +6109,7 @@ "$ref": "#/definitions/gettableSilence" }, "gettableSilences": { + "description": "GettableSilences gettable silences", "type": "array", "items": { "$ref": "#/definitions/gettableSilence" @@ -6296,7 +6300,6 @@ "$ref": "#/definitions/postableSilence" }, "receiver": { - "description": "Receiver receiver", "type": "object", "required": [ "active", diff --git a/public/api-merged.json b/public/api-merged.json index 8f18508d55e..9192ca1c1c3 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -15561,6 +15561,9 @@ "Error" ] }, + "is_paused": { + "type": "boolean" + }, "no_data_state": { "type": "string", "enum": [ @@ -18796,7 +18799,6 @@ } }, "gettableAlert": { - "description": "GettableAlert gettable alert", "type": "object", "required": [ "labels", From 02098c156849f2f9f06cd2380cdf70d88a46aaed Mon Sep 17 00:00:00 2001 From: Joey Tawadrous <90795735+joey-grafana@users.noreply.github.com> Date: Fri, 27 Jan 2023 10:03:52 +0000 Subject: [PATCH 014/117] Tempo: Update docs and default Tempo metrics query (#62185) Update docs and default tempo metrics query --- docs/sources/datasources/jaeger/_index.md | 2 +- docs/sources/datasources/tempo/_index.md | 2 +- public/app/features/explore/TraceView/createSpanLink.test.ts | 2 +- public/app/features/explore/TraceView/createSpanLink.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/sources/datasources/jaeger/_index.md b/docs/sources/datasources/jaeger/_index.md index 4441dc5fc61..0d2bcb14636 100644 --- a/docs/sources/datasources/jaeger/_index.md +++ b/docs/sources/datasources/jaeger/_index.md @@ -154,7 +154,7 @@ datasources: tags: [{ key: 'service.name', value: 'service' }, { key: 'job' }] queries: - name: 'Sample query' - query: 'sum(rate(tempo_spanmetrics_latency_bucket{$__tags}[5m]))' + query: 'sum(rate(traces_spanmetrics_latency_bucket{$__tags}[5m]))' secureJsonData: basicAuthPassword: my_password ``` diff --git a/docs/sources/datasources/tempo/_index.md b/docs/sources/datasources/tempo/_index.md index 124e273f6c3..419f4f78e1c 100644 --- a/docs/sources/datasources/tempo/_index.md +++ b/docs/sources/datasources/tempo/_index.md @@ -175,7 +175,7 @@ datasources: tags: [{ key: 'service.name', value: 'service' }, { key: 'job' }] queries: - name: 'Sample query' - query: 'sum(rate(tempo_spanmetrics_latency_bucket{$__tags}[5m]))' + query: 'sum(rate(traces_spanmetrics_latency_bucket{$__tags}[5m]))' serviceMap: datasourceUid: 'prometheus' search: diff --git a/public/app/features/explore/TraceView/createSpanLink.test.ts b/public/app/features/explore/TraceView/createSpanLink.test.ts index 40050a42cbe..ba208be0afc 100644 --- a/public/app/features/explore/TraceView/createSpanLink.test.ts +++ b/public/app/features/explore/TraceView/createSpanLink.test.ts @@ -496,7 +496,7 @@ describe('createSpanLinkFactory', () => { expect(defaultLink!.title).toBe('defaultQuery'); expect(defaultLink!.href).toBe( `/explore?left=${encodeURIComponent( - '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"prom1Uid","queries":[{"expr":"histogram_quantile(0.5, sum(rate(tempo_spanmetrics_latency_bucket{operation=\\"operation\\"}[5m])) by (le))","refId":"A"}]}' + '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"prom1Uid","queries":[{"expr":"histogram_quantile(0.5, sum(rate(traces_spanmetrics_latency_bucket{service=\\"test service\\"}[5m])) by (le))","refId":"A"}]}' )}` ); diff --git a/public/app/features/explore/TraceView/createSpanLink.tsx b/public/app/features/explore/TraceView/createSpanLink.tsx index 4932b3bfeab..95184af081f 100644 --- a/public/app/features/explore/TraceView/createSpanLink.tsx +++ b/public/app/features/explore/TraceView/createSpanLink.tsx @@ -476,7 +476,7 @@ function buildMetricsQuery( span: TraceSpan ): string { if (!query.query) { - return `histogram_quantile(0.5, sum(rate(tempo_spanmetrics_latency_bucket{operation="${span.operationName}"}[5m])) by (le))`; + return `histogram_quantile(0.5, sum(rate(traces_spanmetrics_latency_bucket{service="${span.process.serviceName}"}[5m])) by (le))`; } let expr = query.query; From 14185ba819272388b50344652a5e1059134330f5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Jan 2023 11:19:35 +0100 Subject: [PATCH 015/117] Update d3 to v3 (#58315) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 6 +-- packages/grafana-data/package.json | 2 +- yarn.lock | 77 +++++------------------------- 3 files changed, 15 insertions(+), 70 deletions(-) diff --git a/package.json b/package.json index 443277ab0c5..8416aea2b52 100644 --- a/package.json +++ b/package.json @@ -124,8 +124,8 @@ "@types/angular-route": "1.7.2", "@types/common-tags": "^1.8.0", "@types/d3": "7.4.0", - "@types/d3-force": "^2.1.0", - "@types/d3-scale-chromatic": "1.3.1", + "@types/d3-force": "^3.0.0", + "@types/d3-scale-chromatic": "3.0.0", "@types/debounce-promise": "3.1.5", "@types/dompurify": "^2", "@types/eslint": "8.4.9", @@ -319,7 +319,7 @@ "common-tags": "1.8.2", "core-js": "3.27.1", "d3": "7.8.2", - "d3-force": "2.1.1", + "d3-force": "3.0.0", "d3-scale-chromatic": "3.0.0", "dangerously-set-html-content": "1.0.9", "date-fns": "2.29.3", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 2336c7fb26a..90282e0cb47 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -37,7 +37,7 @@ "dependencies": { "@braintree/sanitize-url": "6.0.1", "@grafana/schema": "9.4.0-pre", - "@types/d3-interpolate": "^1.4.0", + "@types/d3-interpolate": "^3.0.0", "d3-interpolate": "3.0.1", "date-fns": "2.29.3", "eventemitter3": "4.0.7", diff --git a/yarn.lock b/yarn.lock index 5c9c7c1ce11..05a0a703a69 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4600,7 +4600,7 @@ __metadata: "@testing-library/react": 12.1.4 "@testing-library/react-hooks": 8.0.1 "@testing-library/user-event": 14.4.3 - "@types/d3-interpolate": ^1.4.0 + "@types/d3-interpolate": ^3.0.0 "@types/history": 4.7.11 "@types/jest": 29.2.3 "@types/jquery": 3.5.14 @@ -10488,13 +10488,6 @@ __metadata: languageName: node linkType: hard -"@types/d3-color@npm:^1": - version: 1.4.2 - resolution: "@types/d3-color@npm:1.4.2" - checksum: 60d0f00ceb53052a142c606465eeec49b264a8dee180ddeda121770bd24e2fe34d8c33140022d929a5bf3d862e6908a55ee86ac629d10ac93f124fb40db7022a - languageName: node - linkType: hard - "@types/d3-contour@npm:*": version: 3.0.1 resolution: "@types/d3-contour@npm:3.0.1" @@ -10558,10 +10551,10 @@ __metadata: languageName: node linkType: hard -"@types/d3-force@npm:^2.1.0": - version: 2.1.4 - resolution: "@types/d3-force@npm:2.1.4" - checksum: 635a070c68f7ed9ad3962ec7d2fca7c590349648a1b80e6e1a6084885962f84bcaf12b6998fb3aa190fe97b5d8ed2669084145dcd6b89fec6624a5a2d157beb6 +"@types/d3-force@npm:^3.0.0": + version: 3.0.4 + resolution: "@types/d3-force@npm:3.0.4" + checksum: 779fb597fb41e7bc6a5e1b8969d500deb95c4a73428c7c268bf0ca6f3ed668dd2ed6aa652de7af14d2f9c192dad4f6e7badf2c5bc330624bd8405ac88440b278 languageName: node linkType: hard @@ -10588,7 +10581,7 @@ __metadata: languageName: node linkType: hard -"@types/d3-interpolate@npm:*, @types/d3-interpolate@npm:^3.0.1": +"@types/d3-interpolate@npm:*, @types/d3-interpolate@npm:^3.0.0, @types/d3-interpolate@npm:^3.0.1": version: 3.0.1 resolution: "@types/d3-interpolate@npm:3.0.1" dependencies: @@ -10597,15 +10590,6 @@ __metadata: languageName: node linkType: hard -"@types/d3-interpolate@npm:^1.4.0": - version: 1.4.2 - resolution: "@types/d3-interpolate@npm:1.4.2" - dependencies: - "@types/d3-color": ^1 - checksum: 3d551377c036580efb4f918171300d849d9bf7c6fe61b1ee6ba916065a2e406cded2aaef067a39d068ec6ab898053abb703413921f2d3701c9332bd201152ef9 - languageName: node - linkType: hard - "@types/d3-path@npm:*": version: 3.0.0 resolution: "@types/d3-path@npm:3.0.0" @@ -10641,20 +10625,13 @@ __metadata: languageName: node linkType: hard -"@types/d3-scale-chromatic@npm:*": +"@types/d3-scale-chromatic@npm:*, @types/d3-scale-chromatic@npm:3.0.0": version: 3.0.0 resolution: "@types/d3-scale-chromatic@npm:3.0.0" checksum: e06afffd2725570aa90cb3050eb96a94727264948d9256e56807ab582aba379168d84d1d98bcaa275bf38375148b35dfe13697e06fc7565dd17ac7e2acb11980 languageName: node linkType: hard -"@types/d3-scale-chromatic@npm:1.3.1": - version: 1.3.1 - resolution: "@types/d3-scale-chromatic@npm:1.3.1" - checksum: f3d5eafa0723e6b46fe6610c544e166b03915d91a19298183ad411dc928a0ce12827bb073dfab2bc704e5b9311ea1831a2d7b4192d96cd73891f2e6fecd80041 - languageName: node - linkType: hard - "@types/d3-scale@npm:*": version: 4.0.2 resolution: "@types/d3-scale@npm:4.0.2" @@ -17271,13 +17248,6 @@ __metadata: languageName: node linkType: hard -"d3-dispatch@npm:1 - 2": - version: 2.0.0 - resolution: "d3-dispatch@npm:2.0.0" - checksum: cf473676ae0df1915d51d056d2c6734ceec480d258611d970a01847c50e8c273c185032bf9ed491abd077696bcbeeb491dc94af53e888871f3a1a0fac7365cec - languageName: node - linkType: hard - "d3-dispatch@npm:1 - 3, d3-dispatch@npm:3": version: 3.0.1 resolution: "d3-dispatch@npm:3.0.1" @@ -17332,18 +17302,7 @@ __metadata: languageName: node linkType: hard -"d3-force@npm:2.1.1": - version: 2.1.1 - resolution: "d3-force@npm:2.1.1" - dependencies: - d3-dispatch: 1 - 2 - d3-quadtree: 1 - 2 - d3-timer: 1 - 2 - checksum: aaee5b86d753450e72dae6748765ac3e0b7b784bd420a61264b778d697b9521a343b74b5c55654be2ff7fdf9bada0953a6fcae9be69091176d0579b56df72937 - languageName: node - linkType: hard - -"d3-force@npm:3": +"d3-force@npm:3, d3-force@npm:3.0.0": version: 3.0.0 resolution: "d3-force@npm:3.0.0" dependencies: @@ -17407,13 +17366,6 @@ __metadata: languageName: node linkType: hard -"d3-quadtree@npm:1 - 2": - version: 2.0.0 - resolution: "d3-quadtree@npm:2.0.0" - checksum: e5f9cee19a636666e9f1614f9a9508dde9af47d80769ecb70b6b5033448a8c3ae96f39f1ffea0d1782442559412e3f98508fedf5dc39fe09a2f5995e6a0913bf - languageName: node - linkType: hard - "d3-quadtree@npm:1 - 3, d3-quadtree@npm:3": version: 3.0.1 resolution: "d3-quadtree@npm:3.0.1" @@ -17503,13 +17455,6 @@ __metadata: languageName: node linkType: hard -"d3-timer@npm:1 - 2": - version: 2.0.0 - resolution: "d3-timer@npm:2.0.0" - checksum: 70733c3baffe473155b712896f04f27dae32d6e94169827f57aebb203e190926ba37af12c5f56cbc7126e538a4b1cd083f2451b80dc2a5644d076b6b31982bd8 - languageName: node - linkType: hard - "d3-timer@npm:1 - 3, d3-timer@npm:3": version: 3.0.1 resolution: "d3-timer@npm:3.0.1" @@ -21712,8 +21657,8 @@ __metadata: "@types/angular-route": 1.7.2 "@types/common-tags": ^1.8.0 "@types/d3": 7.4.0 - "@types/d3-force": ^2.1.0 - "@types/d3-scale-chromatic": 1.3.1 + "@types/d3-force": ^3.0.0 + "@types/d3-scale-chromatic": 3.0.0 "@types/debounce-promise": 3.1.5 "@types/dompurify": ^2 "@types/eslint": 8.4.9 @@ -21795,7 +21740,7 @@ __metadata: css-minimizer-webpack-plugin: 4.2.2 cypress: 9.5.1 d3: 7.8.2 - d3-force: 2.1.1 + d3-force: 3.0.0 d3-scale-chromatic: 3.0.0 dangerously-set-html-content: 1.0.9 date-fns: 2.29.3 From 3447ad2602e5535a1d8496ab5404be1f115318cf Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Fri, 27 Jan 2023 11:40:12 +0100 Subject: [PATCH 016/117] AuthN: support priority for post auth and post login hooks (#62208) * AuthN: store post auth hooks in a priority list and update registration function to take a priority * AuthN: store post login hooks in a priority list and update registration function to take a priority * AuthN: Change priority for sync user --- pkg/services/authn/authn.go | 8 +++-- pkg/services/authn/authnimpl/service.go | 35 ++++++++++---------- pkg/services/authn/authnimpl/service_test.go | 12 ++++--- 3 files changed, 30 insertions(+), 25 deletions(-) diff --git a/pkg/services/authn/authn.go b/pkg/services/authn/authn.go index 6767542e2a0..a6c435259c4 100644 --- a/pkg/services/authn/authn.go +++ b/pkg/services/authn/authn.go @@ -54,12 +54,14 @@ type PostLoginHookFn func(ctx context.Context, identity *Identity, r *Request, e type Service interface { // Authenticate authenticates a request Authenticate(ctx context.Context, r *Request) (*Identity, error) - // RegisterPostAuthHook registers a hook that is called after a successful authentication. - RegisterPostAuthHook(hook PostAuthHookFn) + // RegisterPostAuthHook registers a hook with a priority that is called after a successful authentication. + // A lower number means higher priority. + RegisterPostAuthHook(hook PostAuthHookFn, priority uint) // Login authenticates a request and creates a session on successful authentication. Login(ctx context.Context, client string, r *Request) (*Identity, error) // RegisterPostLoginHook registers a hook that that is called after a login request. - RegisterPostLoginHook(hook PostLoginHookFn) + // A lower number means higher priority. + RegisterPostLoginHook(hook PostLoginHookFn, priority uint) // RedirectURL will generate url that we can use to initiate auth flow for supported clients. RedirectURL(ctx context.Context, client string, r *Request) (string, error) } diff --git a/pkg/services/authn/authnimpl/service.go b/pkg/services/authn/authnimpl/service.go index 3bddf2f45bb..cba480ee297 100644 --- a/pkg/services/authn/authnimpl/service.go +++ b/pkg/services/authn/authnimpl/service.go @@ -60,7 +60,8 @@ func ProvideService( clientQueue: newQueue[authn.ContextAwareClient](), tracer: tracer, sessionService: sessionService, - postAuthHooks: []authn.PostAuthHookFn{}, + postAuthHooks: newQueue[authn.PostAuthHookFn](), + postLoginHooks: newQueue[authn.PostLoginHookFn](), } s.RegisterClient(clients.ProvideRender(userService, renderService)) @@ -69,7 +70,7 @@ func ProvideService( if cfg.LoginCookieName != "" { sessionClient := clients.ProvideSession(sessionService, userService, cfg.LoginCookieName, cfg.LoginMaxLifetime) s.RegisterClient(sessionClient) - s.RegisterPostAuthHook(sessionClient.RefreshTokenHook) + s.RegisterPostAuthHook(sessionClient.RefreshTokenHook, 20) } if s.cfg.AnonymousEnabled { @@ -118,13 +119,13 @@ func ProvideService( // FIXME (jguer): move to User package userSyncService := sync.ProvideUserSync(userService, userProtectionService, authInfoService, quotaService) orgUserSyncService := sync.ProvideOrgSync(userService, orgService, accessControlService) - s.RegisterPostAuthHook(userSyncService.SyncUser) - s.RegisterPostAuthHook(orgUserSyncService.SyncOrgUser) - s.RegisterPostAuthHook(sync.ProvideUserLastSeenSync(userService).SyncLastSeen) - s.RegisterPostAuthHook(sync.ProvideAPIKeyLastSeenSync(apikeyService).SyncLastSeen) + s.RegisterPostAuthHook(userSyncService.SyncUser, 10) + s.RegisterPostAuthHook(orgUserSyncService.SyncOrgUser, 30) + s.RegisterPostAuthHook(sync.ProvideUserLastSeenSync(userService).SyncLastSeen, 40) + s.RegisterPostAuthHook(sync.ProvideAPIKeyLastSeenSync(apikeyService).SyncLastSeen, 50) if features.IsEnabled(featuremgmt.FlagAccessTokenExpirationCheck) { - s.RegisterPostAuthHook(sync.ProvideOauthTokenSync(oauthTokenService, sessionService).SyncOauthToken) + s.RegisterPostAuthHook(sync.ProvideOauthTokenSync(oauthTokenService, sessionService).SyncOauthToken, 60) } return s @@ -141,9 +142,9 @@ type Service struct { sessionService auth.UserTokenService // postAuthHooks are called after a successful authentication. They can modify the identity. - postAuthHooks []authn.PostAuthHookFn + postAuthHooks *queue[authn.PostAuthHookFn] // postLoginHooks are called after a login request is performed, both for failing and successful requests. - postLoginHooks []authn.PostLoginHookFn + postLoginHooks *queue[authn.PostLoginHookFn] } func (s *Service) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { @@ -182,8 +183,8 @@ func (s *Service) authenticate(ctx context.Context, c authn.Client, r *authn.Req return nil, err } - for _, hook := range s.postAuthHooks { - if err := hook(ctx, identity, r); err != nil { + for _, hook := range s.postAuthHooks.items { + if err := hook.v(ctx, identity, r); err != nil { s.log.FromContext(ctx).Warn("post auth hook failed", "error", err, "id", identity) return nil, err } @@ -196,14 +197,14 @@ func (s *Service) authenticate(ctx context.Context, c authn.Client, r *authn.Req return identity, nil } -func (s *Service) RegisterPostAuthHook(hook authn.PostAuthHookFn) { - s.postAuthHooks = append(s.postAuthHooks, hook) +func (s *Service) RegisterPostAuthHook(hook authn.PostAuthHookFn, priority uint) { + s.postAuthHooks.insert(hook, priority) } func (s *Service) Login(ctx context.Context, client string, r *authn.Request) (identity *authn.Identity, err error) { defer func() { - for _, hook := range s.postLoginHooks { - hook(ctx, identity, r, err) + for _, hook := range s.postLoginHooks.items { + hook.v(ctx, identity, r, err) } }() @@ -239,8 +240,8 @@ func (s *Service) Login(ctx context.Context, client string, r *authn.Request) (i return identity, nil } -func (s *Service) RegisterPostLoginHook(hook authn.PostLoginHookFn) { - s.postLoginHooks = append(s.postLoginHooks, hook) +func (s *Service) RegisterPostLoginHook(hook authn.PostLoginHookFn, priority uint) { + s.postLoginHooks.insert(hook, priority) } func (s *Service) RedirectURL(ctx context.Context, client string, r *authn.Request) (string, error) { diff --git a/pkg/services/authn/authnimpl/service_test.go b/pkg/services/authn/authnimpl/service_test.go index 21fb1f50d90..a778242b2c2 100644 --- a/pkg/services/authn/authnimpl/service_test.go +++ b/pkg/services/authn/authnimpl/service_test.go @@ -292,11 +292,13 @@ func setupTests(t *testing.T, opts ...func(svc *Service)) *Service { t.Helper() s := &Service{ - log: log.NewNopLogger(), - cfg: setting.NewCfg(), - clientQueue: newQueue[authn.ContextAwareClient](), - clients: map[string]authn.Client{}, - tracer: tracing.InitializeTracerForTest(), + log: log.NewNopLogger(), + cfg: setting.NewCfg(), + clients: map[string]authn.Client{}, + clientQueue: newQueue[authn.ContextAwareClient](), + tracer: tracing.InitializeTracerForTest(), + postAuthHooks: newQueue[authn.PostAuthHookFn](), + postLoginHooks: newQueue[authn.PostLoginHookFn](), } for _, o := range opts { From 6292a41b24d1ba7b1995236e89bc6b3f17fd8540 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Fri, 27 Jan 2023 11:40:49 +0100 Subject: [PATCH 017/117] Azure Monitor: Allow multi-value variables (#62238) --- .../azure_monitor_datasource.test.ts | 74 +++++++++- .../azure_monitor/azure_monitor_datasource.ts | 126 +++++++++++------- .../datasource.ts | 6 +- .../types/types.ts | 6 + 4 files changed, 158 insertions(+), 54 deletions(-) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts index 98934049c87..26275e04e74 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts @@ -5,7 +5,7 @@ import { TemplateSrv } from 'app/features/templating/template_srv'; import createMockQuery from '../__mocks__/query'; import { createTemplateVariables } from '../__mocks__/utils'; -import { singleVariable, subscriptionsVariable } from '../__mocks__/variables'; +import { multiVariable, singleVariable, subscriptionsVariable } from '../__mocks__/variables'; import AzureMonitorDatasource from '../datasource'; import { AzureDataSourceJsonData, AzureMonitorLocationsResponse, AzureQueryType } from '../types'; @@ -122,6 +122,43 @@ describe('AzureMonitorDatasource', () => { }, }); }); + + it('expand template variables in resource groups and names', () => { + const resourceGroup = '$rg'; + const resourceName = '$rn'; + templateSrv.init([ + { + id: 'rg', + name: 'rg', + current: { + value: `rg1,rg2`, + }, + }, + { + id: 'rn', + name: 'rn', + current: { + value: `rn1,rn2`, + }, + }, + ]); + const query = createMockQuery({ + azureMonitor: { + resources: [{ resourceGroup, resourceName }], + }, + }); + const templatedQuery = ctx.ds.azureMonitorDatasource.applyTemplateVariables(query, {}); + expect(templatedQuery).toMatchObject({ + azureMonitor: { + resources: [ + { resourceGroup: 'rg1', resourceName: 'rn1' }, + { resourceGroup: 'rg2', resourceName: 'rn1' }, + { resourceGroup: 'rg1', resourceName: 'rn2' }, + { resourceGroup: 'rg2', resourceName: 'rn2' }, + ], + }, + }); + }); }); describe('When performing getMetricNamespaces', () => { @@ -570,6 +607,41 @@ describe('AzureMonitorDatasource', () => { expect(results[0].value).toEqual('nodeapp'); }); }); + + it('should return multiple resources from a template variable', () => { + const tsrv = new TemplateSrv(); + tsrv.replace = jest + .fn() + .mockImplementation((value: string) => (value === `$${multiVariable.id}` ? 'foo,bar' : value)); + const ds = new AzureMonitorDatasource(ctx.instanceSettings, templateSrv); + ds.azureMonitorDatasource.templateSrv = tsrv; + ds.azureMonitorDatasource.getResource = jest + .fn() + .mockImplementationOnce((path: string) => { + expect(path).toMatch('foo'); + return Promise.resolve(response); + }) + .mockImplementationOnce((path: string) => { + expect(path).toMatch('bar'); + return Promise.resolve({ + value: [ + { + name: resourceGroup + '2', + type: metricNamespace, + }, + ], + }); + }); + return ds + .getResourceNames(subscription, `$${multiVariable.id}`, metricNamespace) + .then((results: Array<{ text: string; value: string }>) => { + expect(results.length).toEqual(2); + expect(results[0].text).toEqual('nodeapp'); + expect(results[0].value).toEqual('nodeapp'); + expect(results[1].text).toEqual('nodeapp2'); + expect(results[1].value).toEqual('nodeapp2'); + }); + }); }); describe('and the metric definition is blobServices', () => { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts index 16c37b1b2e5..4a30a99c961 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts @@ -22,6 +22,7 @@ import { AzureMonitorLocations, AzureMonitorProvidersResponse, AzureMonitorLocationsResponse, + AzureGetResourceNamesQuery, } from '../types'; import { routeNames } from '../utils/common'; import migrateQuery from '../utils/migrateQuery'; @@ -98,10 +99,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend ({ - resourceGroup: templateSrv.replace(r.resourceGroup, scopedVars), - resourceName: templateSrv.replace(r.resourceName, scopedVars), - })); + const resources = item.resources?.map((r) => this.replaceTemplateVariables(r, scopedVars)).flat(); const metricNamespace = templateSrv.replace(item.metricNamespace, scopedVars); const customNamespace = templateSrv.replace(item.customNamespace, scopedVars); const timeGrain = templateSrv.replace((item.timeGrain || '').toString(), scopedVars); @@ -165,53 +163,57 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend { - let list: Array<{ text: string; value: string }> = []; - if (startsWith(metricNamespace?.toLowerCase(), 'microsoft.storage/storageaccounts/')) { - list = ResponseParser.parseResourceNames(result, 'microsoft.storage/storageaccounts'); - for (let i = 0; i < list.length; i++) { - list[i].text += '/default'; - list[i].value += '/default'; - } - } else { - list = ResponseParser.parseResourceNames(result, metricNamespace); + async getResourceNames(query: AzureGetResourceNamesQuery, skipToken?: string) { + const promises = this.replaceTemplateVariables(query).map(({ metricNamespace, subscriptionId, resourceGroup }) => { + const validMetricNamespace = startsWith(metricNamespace?.toLowerCase(), 'microsoft.storage/storageaccounts/') + ? 'microsoft.storage/storageaccounts' + : metricNamespace; + let url = `${this.resourcePath}/subscriptions/${subscriptionId}`; + if (resourceGroup) { + url += `/resourceGroups/${resourceGroup}`; } - - if (result.nextLink) { - // If there is a nextLink, we should request more pages - const nextURL = new URL(result.nextLink); - const nextToken = nextURL.searchParams.get('$skiptoken'); - if (!nextToken) { - throw Error('unable to request the next page of resources'); - } - const nextPage = await this.getResourceNames(subscriptionId, resourceGroup, metricNamespace, nextToken); - list = list.concat(nextPage); + url += `/resources?api-version=${this.listByResourceGroupApiVersion}`; + if (validMetricNamespace) { + url += `&$filter=resourceType eq '${validMetricNamespace}'`; } + if (skipToken) { + url += `&$skiptoken=${skipToken}`; + } + return this.getResource(url).then(async (result: any) => { + let list: Array<{ text: string; value: string }> = []; + if (startsWith(metricNamespace?.toLowerCase(), 'microsoft.storage/storageaccounts/')) { + list = ResponseParser.parseResourceNames(result, 'microsoft.storage/storageaccounts'); + for (let i = 0; i < list.length; i++) { + list[i].text += '/default'; + list[i].value += '/default'; + } + } else { + list = ResponseParser.parseResourceNames(result, metricNamespace); + } - return list; + if (result.nextLink) { + // If there is a nextLink, we should request more pages + const nextURL = new URL(result.nextLink); + const nextToken = nextURL.searchParams.get('$skiptoken'); + if (!nextToken) { + throw Error('unable to request the next page of resources'); + } + const nextPage = await this.getResourceNames({ metricNamespace, subscriptionId, resourceGroup }, nextToken); + list = list.concat(nextPage); + } + + return list; + }); }); + return (await Promise.all(promises)).flat(); } getMetricNamespaces(query: GetMetricNamespacesQuery, globalRegion: boolean) { const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl( this.resourcePath, this.apiPreviewVersion, - this.replaceTemplateVariables(query), + // Only use the first query, as the metric namespaces should be the same for all queries + this.replaceSingleTemplateVariables(query), globalRegion, this.templateSrv ); @@ -246,7 +248,8 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend { @@ -259,7 +262,8 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend { @@ -293,16 +297,42 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend 0; } - private replaceTemplateVariables(query: T) { - const templateSrv = getTemplateSrv(); + private replaceSingleTemplateVariables(query: T, scopedVars?: ScopedVars) { + // This method evaluates template variables supporting multiple values but only returns the first value. + // This will work as far as the the first combination of variables is valid. + // For example if 'rg1' contains 'res1' and 'rg2' contains 'res2' then + // { resourceGroup: ['rg1', 'rg2'], resourceName: ['res1', 'res2'] } would return + // { resourceGroup: 'rg1', resourceName: 'res1' } which is valid but + // { resourceGroup: ['rg1', 'rg2'], resourceName: ['res2'] } would result in + // { resourceGroup: 'rg1', resourceName: 'res2' } which is not. + return this.replaceTemplateVariables(query, scopedVars)[0]; + } - const workingQuery: { [K in keyof T]: string } = { ...query }; + private replaceTemplateVariables(query: T, scopedVars?: ScopedVars) { + const workingQueries: Array<{ [K in keyof T]: string }> = [{ ...query }]; const keys = Object.keys(query) as Array; keys.forEach((key) => { - workingQuery[key] = templateSrv.replace(workingQuery[key]); + const replaced = this.templateSrv.replace(workingQueries[0][key], scopedVars, 'raw'); + if (replaced.includes(',')) { + const multiple = replaced.split(','); + const currentQueries = [...workingQueries]; + multiple.forEach((value, i) => { + currentQueries.forEach((q) => { + if (i === 0) { + q[key] = value; + } else { + workingQueries.push({ ...q, [key]: value }); + } + }); + }); + } else { + workingQueries.forEach((q) => { + q[key] = replaced; + }); + } }); - return workingQuery; + return workingQueries; } async getProvider(providerName: string) { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts index a3903e0bceb..aadb5583421 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts @@ -156,11 +156,7 @@ export default class Datasource extends DataSourceWithBackend Date: Fri, 27 Jan 2023 10:50:48 +0000 Subject: [PATCH 018/117] Search: Store only search value in state, not the whole selectable value (#62228) * Search: Store only search value in state, not the whole selectable value * type sort to undefined-able --- .betterer.results | 3 +-- .../search/page/components/ActionRow.tsx | 6 +++--- public/app/features/search/page/reporting.ts | 2 +- .../search/state/SearchStateManager.ts | 19 +++++++++---------- public/app/features/search/types.ts | 7 +++---- public/app/features/search/utils.test.ts | 9 --------- public/app/features/search/utils.ts | 7 +++---- 7 files changed, 20 insertions(+), 33 deletions(-) diff --git a/.betterer.results b/.betterer.results index 19cfd61c989..2a789546b7a 100644 --- a/.betterer.results +++ b/.betterer.results @@ -4585,8 +4585,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], "public/app/features/search/utils.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"] + [0, 0, 0, "Do not use any type assertions.", "0"] ], "public/app/features/serviceaccounts/ServiceAccountPage.test.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], diff --git a/public/app/features/search/page/components/ActionRow.tsx b/public/app/features/search/page/components/ActionRow.tsx index 415941bdbb0..27fe51c9ab5 100644 --- a/public/app/features/search/page/components/ActionRow.tsx +++ b/public/app/features/search/page/components/ActionRow.tsx @@ -20,7 +20,7 @@ if (config.featureToggles.dashboardPreviews) { interface Props { onLayoutChange: (layout: SearchLayout) => void; - onSortChange: (value: SelectableValue) => void; + onSortChange: (value?: string) => void; onStarredFilterChange?: (event: FormEvent) => void; onTagFilterChange: (tags: string[]) => void; getTagOptions: () => Promise; @@ -106,8 +106,8 @@ export const ActionRow: FC = ({ /> )} onSortChange(change.value)} + value={state.sort} getSortOptions={getSortOptions} placeholder={sortPlaceholder} isClearable diff --git a/public/app/features/search/page/reporting.ts b/public/app/features/search/page/reporting.ts index 083a340b49e..9c708ac069f 100644 --- a/public/app/features/search/page/reporting.ts +++ b/public/app/features/search/page/reporting.ts @@ -6,7 +6,7 @@ import { EventTrackingNamespace, SearchLayout } from '../types'; interface QueryProps { layout: SearchLayout; starred: boolean; - sortValue: string; + sortValue?: string; query: string; tagCount: number; includePanels?: boolean; diff --git a/public/app/features/search/state/SearchStateManager.ts b/public/app/features/search/state/SearchStateManager.ts index e04ae3106c7..f376021bc61 100644 --- a/public/app/features/search/state/SearchStateManager.ts +++ b/public/app/features/search/state/SearchStateManager.ts @@ -1,7 +1,6 @@ import { debounce } from 'lodash'; import { FormEvent } from 'react'; -import { SelectableValue } from '@grafana/data'; import { locationService } from '@grafana/runtime'; import { TermCount } from 'app/core/components/TagFilter/TagFilter'; import { StateManagerBase } from 'app/core/services/StateManagerBase'; @@ -21,10 +20,10 @@ import { parseRouteParams } from '../utils'; export const initialState: SearchState = { query: '', tag: [], - sort: null, starred: false, layout: SearchLayout.Folders, - prevSort: null, + sort: undefined, + prevSort: undefined, eventTrackingNamespace: 'dashboard_search', }; @@ -113,7 +112,7 @@ export class SearchStateManager extends StateManagerBase { this.setStateAndDoSearch({ starred: false }); }; - onSortChange = (sort: SelectableValue | null) => { + onSortChange = (sort: string | undefined) => { if (this.state.layout === SearchLayout.Folders) { this.setStateAndDoSearch({ sort, layout: SearchLayout.List }); } else { @@ -125,7 +124,7 @@ export class SearchStateManager extends StateManagerBase { localStorage.setItem(SEARCH_SELECTED_LAYOUT, layout); if (this.state.sort && layout === SearchLayout.Folders) { - this.setStateAndDoSearch({ layout, prevSort: this.state.sort, sort: null }); + this.setStateAndDoSearch({ layout, prevSort: this.state.sort, sort: undefined }); } else { this.setStateAndDoSearch({ layout, sort: this.state.prevSort }); } @@ -146,7 +145,7 @@ export class SearchStateManager extends StateManagerBase { tags: this.state.tag as string[], ds_uid: this.state.datasource as string, location: this.state.folderUid, // This will scope all results to the prefix - sort: this.state.sort?.value, + sort: this.state.sort, explain: this.state.explain, withAllowedActions: this.state.explain, // allowedActions are currently not used for anything on the UI and added only in `explain` mode starred: this.state.starred, @@ -179,7 +178,7 @@ export class SearchStateManager extends StateManagerBase { const trackingInfo = { layout: this.state.layout, starred: this.state.starred, - sortValue: this.state.sort?.value, + sortValue: this.state.sort, query: this.state.query, tagCount: this.state.tag?.length, includePanels: this.state.includePanels, @@ -227,13 +226,13 @@ export class SearchStateManager extends StateManagerBase { onSearchItemClicked = (e: React.MouseEvent) => { // Clear some filters only if we're not opening a search item in a new tab if (!e.altKey && !e.ctrlKey && !e.metaKey) { - this.setState({ tag: [], starred: false, sort: null, query: '', folderUid: undefined }); + this.setState({ tag: [], starred: false, sort: undefined, query: '', folderUid: undefined }); } reportSearchResultInteraction(this.state.eventTrackingNamespace, { layout: this.state.layout, starred: this.state.starred, - sortValue: this.state.sort?.value, + sortValue: this.state.sort, query: this.state.query, tagCount: this.state.tag?.length, includePanels: this.state.includePanels, @@ -247,7 +246,7 @@ export class SearchStateManager extends StateManagerBase { reportDashboardListViewed(this.state.eventTrackingNamespace, { layout: this.state.layout, starred: this.state.starred, - sortValue: this.state.sort?.value, + sortValue: this.state.sort, query: this.state.query, tagCount: this.state.tag?.length, includePanels: this.state.includePanels, diff --git a/public/app/features/search/types.ts b/public/app/features/search/types.ts index a63ef7960a5..ac8f930f094 100644 --- a/public/app/features/search/types.ts +++ b/public/app/features/search/types.ts @@ -1,6 +1,6 @@ import { Action } from 'redux'; -import { SelectableValue, WithAccessControlMetadata } from '@grafana/data'; +import { WithAccessControlMetadata } from '@grafana/data'; import { QueryResponse } from './service'; @@ -78,9 +78,8 @@ export interface SearchState { starred: boolean; explain?: boolean; // adds debug info datasource?: string; - sort: SelectableValue | null; - // Save sorting data between layouts - prevSort: SelectableValue | null; + sort?: string; + prevSort?: string; // Save sorting data between layouts layout: SearchLayout; result?: QueryResponse; loading?: boolean; diff --git a/public/app/features/search/utils.test.ts b/public/app/features/search/utils.test.ts index 209c9450855..152d5647956 100644 --- a/public/app/features/search/utils.test.ts +++ b/public/app/features/search/utils.test.ts @@ -12,7 +12,6 @@ describe('Search utils', () => { }); it('should return tag as array, if present', () => { - //@ts-ignore const params = { sort: undefined, tag: 'test', query: 'test' }; expect(parseRouteParams(params)).toEqual({ query: 'test', @@ -26,14 +25,6 @@ describe('Search utils', () => { }); }); - it('should return sort as a SelectableValue', () => { - const params: Partial = { sort: 'test' }; - - expect(parseRouteParams(params)).toEqual({ - sort: { value: 'test' }, - }); - }); - it('should prepend folder:{folder} to the query if folder is present', () => { expect(parseRouteParams({ folder: 'current' })).toEqual({ folder: 'current', diff --git a/public/app/features/search/utils.ts b/public/app/features/search/utils.ts index ea5ed3ea274..7c942ec1174 100644 --- a/public/app/features/search/utils.ts +++ b/public/app/features/search/utils.ts @@ -28,16 +28,15 @@ export const getSectionStorageKey = (title = 'General') => { * @param folder */ export const parseRouteParams = (params: UrlQueryMap) => { - const cleanedParams = Object.entries(params).reduce((obj, [key, val]) => { + const cleanedParams = Object.entries(params).reduce>((obj, [key, val]) => { if (!val) { return obj; } else if (key === 'tag' && !Array.isArray(val)) { return { ...obj, tag: [val] as string[] }; - } else if (key === 'sort') { - return { ...obj, sort: { value: val } }; } + return { ...obj, [key]: val }; - }, {} as Partial); + }, {}); if (params.folder) { const folderStr = `folder:${params.folder}`; From 1464dd4095c897eb03b72c77c8674e59db8aa754 Mon Sep 17 00:00:00 2001 From: Victor Marin <36818606+mdvictor@users.noreply.github.com> Date: Fri, 27 Jan 2023 12:52:32 +0200 Subject: [PATCH 019/117] Re-add RowHeight option to StatusHistory (#62293) --- .../statushistorypanelcfg/schema-reference.md | 1 + public/app/plugins/panel/status-history/module.tsx | 10 ++++++++++ public/app/plugins/panel/status-history/panelcfg.cue | 2 ++ .../app/plugins/panel/status-history/panelcfg.gen.ts | 5 +++++ 4 files changed, 18 insertions(+) diff --git a/docs/sources/developers/kinds/composable/statushistorypanelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/statushistorypanelcfg/schema-reference.md index 2daba7d77d2..46a8a21a125 100644 --- a/docs/sources/developers/kinds/composable/statushistorypanelcfg/schema-reference.md +++ b/docs/sources/developers/kinds/composable/statushistorypanelcfg/schema-reference.md @@ -34,6 +34,7 @@ title: StatusHistoryPanelCfg kind | Property | Type | Required | Description | |-------------|--------|----------|-----------------------------------------------------------| | `colWidth` | number | No | Controls the column width Default: `0.9`. | +| `rowHeight` | number | No | Set the height of the rows Default: `0.9`. | | `showValue` | string | No | TODO docs Possible values are: `auto`, `never`, `always`. | diff --git a/public/app/plugins/panel/status-history/module.tsx b/public/app/plugins/panel/status-history/module.tsx index 5f7a1ec915e..8a922cf43e2 100644 --- a/public/app/plugins/panel/status-history/module.tsx +++ b/public/app/plugins/panel/status-history/module.tsx @@ -56,6 +56,16 @@ export const plugin = new PanelPlugin(StatusHist }, defaultValue: VisibilityMode.Auto, }) + .addSliderInput({ + path: 'rowHeight', + name: 'Row height', + defaultValue: 0.9, + settings: { + min: 0, + max: 1, + step: 0.01, + }, + }) .addSliderInput({ path: 'colWidth', name: 'Column width', diff --git a/public/app/plugins/panel/status-history/panelcfg.cue b/public/app/plugins/panel/status-history/panelcfg.cue index 7dda14c56f4..04a7fc4c6fc 100644 --- a/public/app/plugins/panel/status-history/panelcfg.cue +++ b/public/app/plugins/panel/status-history/panelcfg.cue @@ -30,6 +30,8 @@ composableKinds: PanelCfg: { ui.OptionsWithTooltip ui.OptionsWithTimezones + //Set the height of the rows + rowHeight: float32 & >=0 & <=1 | *0.9 //Show values on the columns showValue: ui.VisibilityMode | *"auto" //Controls the column width diff --git a/public/app/plugins/panel/status-history/panelcfg.gen.ts b/public/app/plugins/panel/status-history/panelcfg.gen.ts index 49948fe6c3b..4c96a911e9c 100644 --- a/public/app/plugins/panel/status-history/panelcfg.gen.ts +++ b/public/app/plugins/panel/status-history/panelcfg.gen.ts @@ -17,6 +17,10 @@ export interface PanelOptions extends ui.OptionsWithLegend, ui.OptionsWithToolti * Controls the column width */ colWidth?: number; + /** + * Set the height of the rows + */ + rowHeight: number; /** * Show values on the columns */ @@ -25,6 +29,7 @@ export interface PanelOptions extends ui.OptionsWithLegend, ui.OptionsWithToolti export const defaultPanelOptions: Partial = { colWidth: 0.9, + rowHeight: 0.9, showValue: ui.VisibilityMode.Auto, }; From 3d4cf06246d6e43a12d1d5fbf511b9facbc7331a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Jan 2023 10:54:45 +0000 Subject: [PATCH 020/117] Update dependency rc-drawer to v6.1.2 (#62294) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-ui/package.json | 2 +- yarn.lock | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 8416aea2b52..693479c9c47 100644 --- a/package.json +++ b/package.json @@ -360,7 +360,7 @@ "prop-types": "15.8.1", "pseudoizer": "^0.1.0", "rc-cascader": "3.8.0", - "rc-drawer": "6.0.1", + "rc-drawer": "6.1.2", "rc-slider": "10.0.1", "rc-time-picker": "3.7.3", "rc-tree": "5.7.0", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index db5413276cb..37e44346188 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -81,7 +81,7 @@ "ol": "7.1.0", "prismjs": "1.29.0", "rc-cascader": "3.8.0", - "rc-drawer": "6.0.1", + "rc-drawer": "6.1.2", "rc-slider": "10.0.1", "rc-time-picker": "^3.7.3", "rc-tooltip": "5.2.2", diff --git a/yarn.lock b/yarn.lock index 05a0a703a69..668284d75c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5128,7 +5128,7 @@ __metadata: prismjs: 1.29.0 process: ^0.11.10 rc-cascader: 3.8.0 - rc-drawer: 6.0.1 + rc-drawer: 6.1.2 rc-slider: 10.0.1 rc-time-picker: ^3.7.3 rc-tooltip: 5.2.2 @@ -21822,7 +21822,7 @@ __metadata: prop-types: 15.8.1 pseudoizer: ^0.1.0 rc-cascader: 3.8.0 - rc-drawer: 6.0.1 + rc-drawer: 6.1.2 rc-slider: 10.0.1 rc-time-picker: 3.7.3 rc-tree: 5.7.0 @@ -31786,9 +31786,9 @@ __metadata: languageName: node linkType: hard -"rc-drawer@npm:6.0.1": - version: 6.0.1 - resolution: "rc-drawer@npm:6.0.1" +"rc-drawer@npm:6.1.2": + version: 6.1.2 + resolution: "rc-drawer@npm:6.1.2" dependencies: "@babel/runtime": ^7.10.1 "@rc-component/portal": ^1.0.0-6 @@ -31798,7 +31798,7 @@ __metadata: peerDependencies: react: ">=16.9.0" react-dom: ">=16.9.0" - checksum: ce4a0b2ac3a96a203a1038f1c30df079e34359f821db9bcab39a87bdc62e62dd681d3b37354d9e1c77b44e3954b4f583d3b16ed021bcd1851dea0f62b888f6a8 + checksum: 0d7f5cd56bcad80ebc11dd3d1c5b13ef620e8790fac67af62180938c9baa0e671cbd6aaf9588ebd1978de39a2791dcf4f0da56dd366797bada43d2d31ef575ad languageName: node linkType: hard From 4deb10888eea269fb5003630302f0e702d18a523 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 27 Jan 2023 11:58:18 +0100 Subject: [PATCH 021/117] Revert "Transforms: Add join by fields" (#62278) --- .../transforms/join-by-field.json | 770 +++++++++++------- .../transformers/ensureColumns.ts | 13 +- .../transformers/joinByField.test.ts | 98 +-- .../transformers/joinByField.ts | 15 +- .../transformers/joinDataFrames.test.ts | 99 --- .../transformers/joinDataFrames.ts | 21 +- .../editors/JoinByFieldTransformerEditor.tsx | 86 +- 7 files changed, 532 insertions(+), 570 deletions(-) diff --git a/devenv/dev-dashboards/transforms/join-by-field.json b/devenv/dev-dashboards/transforms/join-by-field.json index 907cee7e5d1..a86b3b89a89 100644 --- a/devenv/dev-dashboards/transforms/join-by-field.json +++ b/devenv/dev-dashboards/transforms/join-by-field.json @@ -24,6 +24,7 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, + "id": 1351, "links": [], "liveNow": false, "panels": [ @@ -37,7 +38,7 @@ }, "id": 9, "panels": [], - "title": "Input", + "title": "Join by time", "type": "row" }, { @@ -48,225 +49,37 @@ "fieldConfig": { "defaults": { "color": { - "mode": "thresholds" + "mode": "palette-classic" }, "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 8, - "x": 0, - "y": 1 - }, - "id": 11, - "options": { - "footer": { - "countRows": false, - "fields": "", - "reducer": [ - "sum" - ], - "show": false - }, - "showHeader": true - }, - "pluginVersion": "9.4.0-pre", - "targets": [ - { - "datasource": { - "type": "testdata", - "uid": "PD8C576611E62080A" - }, - "rawFrameContent": "[{\r\n \"name\": \"tags\",\r\n \"fields\": [\r\n { \"name\": \"tags__time\", \"values\": [100, 101, 200] },\r\n { \"name\": \"tags__name\", \"values\": [\"v1.2\", \"v1.2b\", \"v1.3\"] }\r\n ]\r\n}]", - "refId": "tags", - "scenarioId": "raw_frame" - } - ], - "title": "tags", - "type": "table" - }, - { - "datasource": { - "type": "testdata", - "uid": "PD8C576611E62080A" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 8, - "x": 8, - "y": 1 - }, - "id": 13, - "options": { - "footer": { - "countRows": false, - "fields": "", - "reducer": [ - "sum" - ], - "show": false - }, - "showHeader": true - }, - "pluginVersion": "9.4.0-pre", - "targets": [ - { - "datasource": { - "type": "testdata", - "uid": "PD8C576611E62080A" - }, - "rawFrameContent": "[{\r\n \"name\": \"releases\",\r\n\"fields\": [\r\n { \"name\": \"releases__time\", \"values\": [150, 250] },\r\n { \"name\": \"releases__tag\", \"values\": [\"v1.2\", \"v1.3\"] }\r\n]}]", - "refId": "releases", - "scenarioId": "raw_frame" - } - ], - "title": "releases", - "type": "table" - }, - { - "datasource": { - "type": "testdata", - "uid": "PD8C576611E62080A" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 8, - "x": 16, - "y": 1 - }, - "id": 19, - "options": { - "footer": { - "countRows": false, - "fields": "", - "reducer": [ - "sum" - ], - "show": false - }, - "showHeader": true - }, - "pluginVersion": "9.4.0-pre", - "targets": [ - { - "datasource": { - "type": "testdata", - "uid": "PD8C576611E62080A" - }, - "rawFrameContent": "[{\r\n \"name\": \"features\",\r\n\"fields\": [\r\n { \"name\": \"features__name\", \"values\": [\"A\", \"B\", \"C\", \"D\", \"E\"] },\r\n { \"name\": \"features__tag\", \"values\": [\"v1.2\", \"v1.3\", \"v1.2b\", \"v1.3\", \"v1.2\"] }\r\n]}]", - "refId": "features", - "scenarioId": "raw_frame" - } - ], - "title": "features", - "type": "table" - }, - { - "collapsed": false, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 9 - }, - "id": 21, - "panels": [], - "title": "Output", - "type": "row" - }, - { - "datasource": { - "type": "testdata", - "uid": "PD8C576611E62080A" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "inspect": false + "thresholdsStyle": { + "mode": "off" + } }, "mappings": [], "thresholds": { @@ -288,71 +101,39 @@ "h": 8, "w": 12, "x": 0, - "y": 10 + "y": 1 }, - "id": 23, + "id": 11, "options": { - "footer": { - "countRows": false, - "fields": "", - "reducer": [ - "sum" - ], - "show": false + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true }, - "showHeader": true + "tooltip": { + "mode": "single", + "sort": "none" + } }, - "pluginVersion": "9.4.0-pre", "targets": [ { "datasource": { "type": "testdata", "uid": "PD8C576611E62080A" }, - "rawFrameContent": "[{\r\n \"name\": \"tags\",\r\n \"fields\": [\r\n { \"name\": \"tags__time\", \"values\": [100, 101, 200] },\r\n { \"name\": \"tags__name\", \"values\": [\"v1.2\", \"v1.2b\", \"v1.3\"] }\r\n ]\r\n}]", - "refId": "tags", - "scenarioId": "raw_frame" - }, - { - "datasource": { - "type": "testdata", - "uid": "PD8C576611E62080A" - }, - "rawFrameContent": "[{\r\n \"name\": \"releases\",\r\n \"fields\": [\r\n { \"name\": \"releases__time\", \"values\": [150, 250] },\r\n { \"name\": \"releases__tag\", \"values\": [\"v1.2\", \"v1.3\"] }\r\n]}]", - "refId": "releases", - "scenarioId": "raw_frame" - }, - { - "datasource": { - "type": "testdata", - "uid": "PD8C576611E62080A" - }, - "rawFrameContent": "[{\r\n \"name\": \"features\",\r\n \"fields\": [\r\n { \"name\": \"features__name\", \"values\": [\"A\", \"B\", \"C\", \"D\", \"E\"] },\r\n { \"name\": \"features__tag\", \"values\": [\"v1.2\", \"v1.3\", \"v1.2b\", \"v1.3\", \"v1.2\"] }\r\n]}]", - "refId": "features", - "scenarioId": "raw_frame" + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 4 } ], - "title": "OUTER JOIN", - "transformations": [ - { - "id": "joinByField", - "options": { - "fields": { - "A": "features__name", - "features": "features__tag", - "releases": "releases__tag", - "tags": "tags__name" - }, - "mode": "outer" - } - } - ], - "type": "table" + "title": "Timeseries data", + "type": "timeseries" }, { "datasource": { - "type": "testdata", - "uid": "PD8C576611E62080A" + "type": "datasource", + "uid": "-- Dashboard --" }, "fieldConfig": { "defaults": { @@ -361,9 +142,7 @@ }, "custom": { "align": "auto", - "cellOptions": { - "type": "auto" - }, + "displayMode": "auto", "inspect": false }, "mappings": [], @@ -386,12 +165,11 @@ "h": 8, "w": 12, "x": 12, - "y": 10 + "y": 1 }, - "id": 24, + "id": 13, "options": { "footer": { - "countRows": false, "fields": "", "reducer": [ "sum" @@ -400,47 +178,430 @@ }, "showHeader": true }, - "pluginVersion": "9.4.0-pre", + "pluginVersion": "9.2.0-pre", "targets": [ { "datasource": { - "type": "testdata", - "uid": "PD8C576611E62080A" + "type": "datasource", + "uid": "-- Dashboard --" }, - "rawFrameContent": "[{\r\n \"name\": \"tags\",\r\n \"fields\": [\r\n { \"name\": \"tags__time\", \"values\": [100, 101, 200] },\r\n { \"name\": \"tags__name\", \"values\": [\"v1.2\", \"v1.2b\", \"v1.3\"] }\r\n ]\r\n}]", - "refId": "tags", - "scenarioId": "raw_frame" - }, - { - "datasource": { - "type": "testdata", - "uid": "PD8C576611E62080A" - }, - "rawFrameContent": "[{\r\n \"name\": \"releases\",\r\n \"fields\": [\r\n { \"name\": \"releases__time\", \"values\": [150, 250] },\r\n { \"name\": \"releases__tag\", \"values\": [\"v1.2\", \"v1.3\"] }\r\n]}]", - "refId": "releases", - "scenarioId": "raw_frame" - }, - { - "datasource": { - "type": "testdata", - "uid": "PD8C576611E62080A" - }, - "rawFrameContent": "[{\r\n \"name\": \"features\",\r\n \"fields\": [\r\n { \"name\": \"features__name\", \"values\": [\"A\", \"B\", \"C\", \"D\", \"E\"] },\r\n { \"name\": \"features__tag\", \"values\": [\"v1.2\", \"v1.3\", \"v1.2b\", \"v1.3\", \"v1.2\"] }\r\n]}]", - "refId": "features", - "scenarioId": "raw_frame" + "panelId": 11, + "refId": "A" } ], - "title": "INNER JOIN", + "title": "Same data (as a table)", + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 24, + "x": 0, + "y": 9 + }, + "id": 16, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "9.2.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 11, + "refId": "A" + } + ], + "title": "OUTER join on time (default)", + "transformations": [ + { + "id": "joinByField", + "options": {} + } + ], + "type": "table" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 14 + }, + "id": 5, + "panels": [], + "title": "Join by string field", + "type": "row" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 15 + }, + "id": 2, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "frameIndex": 0, + "showHeader": true + }, + "pluginVersion": "9.2.0-pre", + "targets": [ + { + "csvContent": "OrderID,CustomerID,Time\n100,A,10000\n101,B,20000\n102,C,30000", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "Orders", + "scenarioId": "csv_content" + }, + { + "csvContent": "CustomerID,Name,Country\nA,Customer A,USA\nB,Customer B,Germany\nC,Customer C,Spain\nD,Customer D,Canada", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "hide": false, + "refId": "Customers", + "scenarioId": "csv_content" + } + ], + "title": "Orders", + "transformations": [], + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 15 + }, + "id": 3, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "frameIndex": 1, + "showHeader": true + }, + "pluginVersion": "9.2.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 2, + "refId": "A" + } + ], + "title": "Customers", + "transformations": [], + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "CustomerID" + }, + "properties": [ + { + "id": "custom.width", + "value": 101 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "OrderID" + }, + "properties": [ + { + "id": "custom.width", + "value": 89 + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 23 + }, + "id": 6, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "frameIndex": 0, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "9.2.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 2, + "refId": "A" + } + ], + "title": "OUTER join on CustomerID (keeps missing values)", "transformations": [ { "id": "joinByField", "options": { - "fields": { - "A": "features__name", - "features": "features__tag", - "releases": "releases__tag", - "tags": "tags__name" + "byField": "CustomerID", + "mode": "outer" + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "CustomerID" }, + "properties": [ + { + "id": "custom.width", + "value": 101 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "OrderID" + }, + "properties": [ + { + "id": "custom.width", + "value": 89 + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 23 + }, + "id": 7, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "frameIndex": 0, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "9.2.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 2, + "refId": "A" + } + ], + "title": "INNER join on CustomerID ", + "transformations": [ + { + "id": "joinByField", + "options": { + "byField": "CustomerID", "mode": "inner" } } @@ -448,8 +609,7 @@ "type": "table" } ], - "revision": 1, - "schemaVersion": 38, + "schemaVersion": 37, "style": "dark", "tags": [ "gdev", @@ -466,6 +626,6 @@ "timezone": "", "title": "Join by field", "uid": "gw0K4rmVz", - "version": 1, + "version": 6, "weekStart": "" -} \ No newline at end of file +} diff --git a/packages/grafana-data/src/transformations/transformers/ensureColumns.ts b/packages/grafana-data/src/transformations/transformers/ensureColumns.ts index 9cca6fead37..4968336c236 100644 --- a/packages/grafana-data/src/transformations/transformers/ensureColumns.ts +++ b/packages/grafana-data/src/transformations/transformers/ensureColumns.ts @@ -20,13 +20,12 @@ export const ensureColumnsTransformer: SynchronousDataTransformerInfo = { const timeFieldName = findConsistentTimeFieldName(frames); if (frames.length > 1 && timeFieldName) { - const fields: { [key: string]: string } = {}; - for (const frame of frames) { - if (frame.refId) { - fields[frame.refId] = timeFieldName; - } - } - return joinByFieldTransformer.transformer({ fields }, ctx)(frames); + return joinByFieldTransformer.transformer( + { + byField: timeFieldName, + }, + ctx + )(frames); } return frames; }, diff --git a/packages/grafana-data/src/transformations/transformers/joinByField.test.ts b/packages/grafana-data/src/transformations/transformers/joinByField.test.ts index 5326e86da46..1a1026017b3 100644 --- a/packages/grafana-data/src/transformations/transformers/joinByField.test.ts +++ b/packages/grafana-data/src/transformations/transformers/joinByField.test.ts @@ -15,7 +15,6 @@ describe('JOIN Transformer', () => { describe('outer join', () => { const everySecondSeries = toDataFrame({ name: 'even', - refId: 'even', fields: [ { name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] }, { name: 'temperature', type: FieldType.number, values: [10.3, 10.4, 10.5, 10.6] }, @@ -25,7 +24,6 @@ describe('JOIN Transformer', () => { const everyOtherSecondSeries = toDataFrame({ name: 'odd', - refId: 'odd', fields: [ { name: 'time', type: FieldType.time, values: [1000, 3000, 5000, 7000] }, { name: 'temperature', type: FieldType.number, values: [11.1, 11.3, 11.5, 11.7] }, @@ -35,12 +33,9 @@ describe('JOIN Transformer', () => { it('joins by time field', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.joinByField, + id: DataTransformerID.seriesToColumns, options: { - fields: { - even: 'time', - odd: 'time', - }, + byField: 'time', }, }; @@ -140,12 +135,9 @@ describe('JOIN Transformer', () => { it('joins by temperature field', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.joinByField, + id: DataTransformerID.seriesToColumns, options: { - fields: { - even: 'temperature', - odd: 'temperature', - }, + byField: 'temperature', }, }; @@ -153,7 +145,6 @@ describe('JOIN Transformer', () => { (received) => { const data = received[0]; const filtered = data[0]; - expect(filtered.fields).toMatchInlineSnapshot(` [ { @@ -260,12 +251,9 @@ describe('JOIN Transformer', () => { it('joins by time field in reverse order', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.joinByField, + id: DataTransformerID.seriesToColumns, options: { - fields: { - even: 'time', - odd: 'time', - }, + byField: 'time', }, }; @@ -277,7 +265,6 @@ describe('JOIN Transformer', () => { (received) => { const data = received[0]; const filtered = data[0]; - expect(filtered.fields).toMatchInlineSnapshot(` [ { @@ -389,12 +376,9 @@ describe('JOIN Transformer', () => { it('when dataframe and field share the same name then use the field name', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.joinByField, + id: DataTransformerID.seriesToColumns, options: { - fields: { - even: 'time', - odd: 'time', - }, + byField: 'time', }, }; @@ -455,12 +439,9 @@ describe('JOIN Transformer', () => { it('joins if fields are missing', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.joinByField, + id: DataTransformerID.seriesToColumns, options: { - fields: { - even: 'time', - odd: 'time', - }, + byField: 'time', }, }; @@ -536,12 +517,9 @@ describe('JOIN Transformer', () => { it('handles duplicate field name', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.joinByField, + id: DataTransformerID.seriesToColumns, options: { - fields: { - even: 'time', - odd: 'time', - }, + byField: 'time', }, }; @@ -602,7 +580,6 @@ describe('JOIN Transformer', () => { describe('inner join', () => { const seriesA = toDataFrame({ name: 'A', - refId: 'A', fields: [ { name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] }, { name: 'temperature', type: FieldType.number, values: [10.3, 10.4, 10.5, 10.6] }, @@ -612,7 +589,6 @@ describe('JOIN Transformer', () => { const seriesB = toDataFrame({ name: 'B', - refId: 'B', fields: [ { name: 'time', type: FieldType.time, values: [1000, 3000, 5000, 7000] }, { name: 'temperature', type: FieldType.number, values: [11.1, 10.3, 10.5, 11.7] }, @@ -622,12 +598,9 @@ describe('JOIN Transformer', () => { it('inner joins by time field', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.joinByField, + id: DataTransformerID.seriesToColumns, options: { - fields: { - A: 'time', - B: 'time', - }, + byField: 'time', mode: JoinMode.inner, }, }; @@ -706,12 +679,9 @@ describe('JOIN Transformer', () => { it('inner joins by temperature field', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.joinByField, + id: DataTransformerID.seriesToColumns, options: { - fields: { - A: 'temperature', - B: 'temperature', - }, + byField: 'temperature', mode: JoinMode.inner, }, }; @@ -794,12 +764,9 @@ describe('JOIN Transformer', () => { it('inner joins by time field in reverse order', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.joinByField, + id: DataTransformerID.seriesToColumns, options: { - fields: { - A: 'time', - B: 'time', - }, + byField: 'time', mode: JoinMode.inner, }, }; @@ -885,7 +852,6 @@ describe('JOIN Transformer', () => { describe('Field names', () => { const seriesWithSameFieldAndDataFrameName = toDataFrame({ name: 'temperature', - refId: 'temperature', fields: [ { name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] }, { name: 'temperature', type: FieldType.number, values: [1, 3, 5, 7] }, @@ -894,7 +860,6 @@ describe('JOIN Transformer', () => { const seriesB = toDataFrame({ name: 'B', - refId: 'B', fields: [ { name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] }, { name: 'temperature', type: FieldType.number, values: [2, 4, 6, 8] }, @@ -903,12 +868,9 @@ describe('JOIN Transformer', () => { it('when dataframe and field share the same name then use the field name', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.joinByField, + id: DataTransformerID.seriesToColumns, options: { - fields: { - temperature: 'time', - B: 'time', - }, + byField: 'time', mode: JoinMode.inner, }, }; @@ -970,20 +932,15 @@ describe('JOIN Transformer', () => { it('joins if fields are missing', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.joinByField, + id: DataTransformerID.seriesToColumns, options: { - fields: { - A: 'time', - B: 'time', - C: 'time', - }, + byField: 'time', mode: JoinMode.inner, }, }; const frame1 = toDataFrame({ name: 'A', - refId: 'A', fields: [ { name: 'time', type: FieldType.time, values: [1, 2, 3] }, { name: 'temperature', type: FieldType.number, values: [10, 11, 12] }, @@ -992,13 +949,11 @@ describe('JOIN Transformer', () => { const frame2 = toDataFrame({ name: 'B', - refId: 'B', fields: [], }); const frame3 = toDataFrame({ name: 'C', - refId: 'C', fields: [ { name: 'time', type: FieldType.time, values: [1, 2, 3] }, { name: 'temperature', type: FieldType.number, values: [20, 22, 24] }, @@ -1056,18 +1011,14 @@ describe('JOIN Transformer', () => { it('handles duplicate field name', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.joinByField, + id: DataTransformerID.seriesToColumns, options: { - fields: { - frame1: 'time', - frame2: 'time', - }, + byField: 'time', mode: JoinMode.inner, }, }; const frame1 = toDataFrame({ - refId: 'frame1', fields: [ { name: 'time', type: FieldType.time, values: [1] }, { name: 'temperature', type: FieldType.number, values: [10] }, @@ -1075,7 +1026,6 @@ describe('JOIN Transformer', () => { }); const frame2 = toDataFrame({ - refId: 'frame2', fields: [ { name: 'time', type: FieldType.time, values: [1] }, { name: 'temperature', type: FieldType.number, values: [20] }, diff --git a/packages/grafana-data/src/transformations/transformers/joinByField.ts b/packages/grafana-data/src/transformations/transformers/joinByField.ts index 2946dd4125b..e6c1d386fa6 100644 --- a/packages/grafana-data/src/transformations/transformers/joinByField.ts +++ b/packages/grafana-data/src/transformations/transformers/joinByField.ts @@ -1,6 +1,8 @@ import { map } from 'rxjs/operators'; -import { DataFrame, SynchronousDataTransformerInfo } from '../../types'; +import { DataFrame, SynchronousDataTransformerInfo, FieldMatcher } from '../../types'; +import { fieldMatchers } from '../matchers'; +import { FieldMatcherID } from '../matchers/ids'; import { DataTransformerID } from './ids'; import { joinDataFrames } from './joinDataFrames'; @@ -11,7 +13,7 @@ export enum JoinMode { } export interface JoinByFieldOptions { - fields?: { [key: string]: string }; // empty will pick the field automatically + byField?: string; // empty will pick the field automatically mode?: JoinMode; } @@ -22,7 +24,7 @@ export const joinByFieldTransformer: SynchronousDataTransformerInfo joinByFieldTransformer.transformer(options, ctx)(data))), transformer: (options: JoinByFieldOptions) => { + let joinBy: FieldMatcher | undefined = undefined; return (data: DataFrame[]) => { if (data.length > 1) { - const joined = joinDataFrames({ frames: data, mode: options.mode, fields: options.fields }); + if (options.byField && !joinBy) { + joinBy = fieldMatchers.get(FieldMatcherID.byName).get(options.byField); + } + const joined = joinDataFrames({ frames: data, joinBy, mode: options.mode }); if (joined) { return [joined]; } } - return data; }; }, diff --git a/packages/grafana-data/src/transformations/transformers/joinDataFrames.test.ts b/packages/grafana-data/src/transformations/transformers/joinDataFrames.test.ts index 83dd9469f6b..5f4ef23427b 100644 --- a/packages/grafana-data/src/transformations/transformers/joinDataFrames.test.ts +++ b/packages/grafana-data/src/transformations/transformers/joinDataFrames.test.ts @@ -359,103 +359,4 @@ describe('align frames', () => { expect(isLikelyAscendingVector(new ArrayVector([null, 1, null]), 3)).toBeTruthy(); }); }); - - describe('should perform a join on custom fields', () => { - const tags = toDataFrame({ - refId: 'tags', - fields: [ - { name: 'tags__time', type: FieldType.time, values: [100, 101, 200] }, - { name: 'tags__name', type: FieldType.string, values: ['v1.2', 'v1.2b', 'v1.3'] }, - ], - }); - - const releases = toDataFrame({ - refId: 'releases', - fields: [ - { name: 'releases__time', type: FieldType.time, values: [150, 250] }, - { name: 'releases__tag', type: FieldType.string, values: ['v1.2', 'v1.3'] }, - ], - }); - - const features = toDataFrame({ - refId: 'features', - fields: [ - { name: 'features__name', type: FieldType.string, values: ['A', 'B', 'C', 'D', 'E'] }, - { name: 'features__tag', type: FieldType.time, values: ['v1.2', 'v1.3', 'v1.2b', 'v1.3', 'v1.2'] }, - ], - }); - - it('should perform an outer join', () => { - const out = joinDataFrames({ - frames: [tags, releases, features], - fields: { - tags: 'tags__name', - releases: 'releases__tag', - features: 'features__tag', - }, - })!; - - expect( - out.fields.map((f) => ({ - name: f.name, - values: f.values.toArray(), - })) - ).toEqual([ - { - name: 'tags__name', - values: ['v1.2', 'v1.2b', 'v1.3'], - }, - { - name: 'tags__time', - values: [100, 101, 200], - }, - { - name: 'releases__time', - values: [150, undefined, 250], - }, - { - name: 'features__name', - values: ['E', 'C', 'D'], - }, - ]); - }); - - it('should perform an inner join', () => { - const out = joinDataFrames({ - frames: [tags, releases, features], - fields: { - tags: 'tags__name', - releases: 'releases__tag', - features: 'features__tag', - }, - mode: JoinMode.inner, - })!; - - const mappedOut = out.fields.map((f) => ({ - name: f.name, - values: f.values.toArray(), - })); - - const expected = [ - { - name: 'tags__name', - values: ['v1.2', 'v1.3'], - }, - { - name: 'tags__time', - values: [100, 200], - }, - { - name: 'releases__time', - values: [150, 250], - }, - { - name: 'features__name', - values: ['E', 'D'], - }, - ]; - - expect(JSON.stringify(mappedOut)).toEqual(JSON.stringify(expected)); - }); - }); }); diff --git a/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts b/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts index dd09c373bfb..767161d2f9a 100644 --- a/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts +++ b/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts @@ -47,11 +47,6 @@ export interface JoinOptions { */ joinBy?: FieldMatcher; - /** - * The fields to join on - */ - fields?: { [key: string]: string }; - /** * Optionally filter the non-join fields */ @@ -68,16 +63,8 @@ export interface JoinOptions { mode?: JoinMode; } -function getJoinMatcher(options: JoinOptions, refId: string | undefined): FieldMatcher { - if (options.joinBy) { - return options.joinBy; - } - - if (!options.fields || !refId) { - return pickBestJoinField(options.frames); - } - - return fieldMatchers.get(FieldMatcherID.byName).get(options.fields[refId]); +function getJoinMatcher(options: JoinOptions): FieldMatcher { + return options.joinBy ?? pickBestJoinField(options.frames); } /** @@ -108,7 +95,7 @@ export function joinDataFrames(options: JoinOptions): DataFrame | undefined { let frame = options.frames[0]; let frameCopy = frame; - const joinFieldMatcher = getJoinMatcher(options, frame.refId); + const joinFieldMatcher = getJoinMatcher(options); let joinIndex = frameCopy.fields.findIndex((f) => joinFieldMatcher(f, frameCopy, options.frames)); if (options.keepOriginIndices) { @@ -165,10 +152,10 @@ export function joinDataFrames(options: JoinOptions): DataFrame | undefined { const nullModes: JoinNullMode[][] = []; const allData: AlignedData[] = []; const originalFields: Field[] = []; + const joinFieldMatcher = getJoinMatcher(options); for (let frameIndex = 0; frameIndex < options.frames.length; frameIndex++) { const frame = options.frames[frameIndex]; - const joinFieldMatcher = getJoinMatcher(options, frame.refId); if (!frame || !frame.fields?.length) { continue; // skip the frame diff --git a/public/app/features/transformers/editors/JoinByFieldTransformerEditor.tsx b/public/app/features/transformers/editors/JoinByFieldTransformerEditor.tsx index 1b0977455d6..cffa84015e0 100644 --- a/public/app/features/transformers/editors/JoinByFieldTransformerEditor.tsx +++ b/public/app/features/transformers/editors/JoinByFieldTransformerEditor.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect } from 'react'; +import React, { useCallback } from 'react'; import { DataTransformerID, @@ -6,10 +6,11 @@ import { standardTransformers, TransformerRegistryItem, TransformerUIProps, - DataFrame, } from '@grafana/data'; import { JoinByFieldOptions, JoinMode } from '@grafana/data/src/transformations/transformers/joinByField'; -import { Select, InlineFieldRow, InlineField, Checkbox, HorizontalGroup } from '@grafana/ui'; +import { Select, InlineFieldRow, InlineField } from '@grafana/ui'; + +import { useAllFieldNamesFromDataFrames } from '../utils'; const modes = [ { value: JoinMode.outer, label: 'OUTER', description: 'Keep all rows from any table with a value' }, @@ -17,44 +18,14 @@ const modes = [ ]; export function SeriesToFieldsTransformerEditor({ input, options, onChange }: TransformerUIProps) { - useEffect(() => { - if (options.fields && !Object.keys(options.fields).length && input.length && input[0].refId) { - options.fields[input[0].refId] = input[0].fields[0].name; - onChange({ ...options }); - } - }, [onChange, options, input]); - - const onToggleDataFrame = useCallback( - (dataFrame: DataFrame) => { - if (!dataFrame.refId) { - return; - } - - if (options.fields) { - if (dataFrame.refId in options.fields) { - if (Object.keys(options.fields).length === 1) { - return; - } - - delete options.fields[dataFrame.refId]; - } else { - options.fields[dataFrame.refId] = dataFrame.fields[0].name; - } - } - - onChange({ ...options }); - }, - [onChange, options] - ); + const fieldNames = useAllFieldNamesFromDataFrames(input).map((item: string) => ({ label: item, value: item })); const onSelectField = useCallback( - (queryRefId: string | undefined, fieldName: SelectableValue) => { - if (queryRefId && fieldName.value) { - onChange({ - ...options, - fields: { ...options.fields, [queryRefId]: fieldName.value }, - }); - } + (value: SelectableValue) => { + onChange({ + ...options, + byField: value?.value, + }); }, [onChange, options] ); @@ -63,7 +34,7 @@ export function SeriesToFieldsTransformerEditor({ input, options, onChange }: Tr (value: SelectableValue) => { onChange({ ...options, - mode: value?.value || JoinMode.outer, + mode: value?.value, }); }, [onChange, options] @@ -73,31 +44,20 @@ export function SeriesToFieldsTransformerEditor({ input, options, onChange }: Tr <> - + + + + + ({ label: field.name, value: field.name }))} - value={dataFrame.refId ? (options.fields || {})[dataFrame.refId] : dataFrame.fields[0].name} - onChange={(fieldName) => onSelectField(dataFrame.refId, fieldName)} - /> - - - - ))} ); } From 960307e9387d583c67b5016b77cc1f16ee5dd764 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Fri, 27 Jan 2023 11:00:28 +0000 Subject: [PATCH 022/117] Search: Remember sorting preference between visits (#62248) Search: Remember sort option between uses --- public/app/features/search/constants.ts | 1 + public/app/features/search/state/SearchStateManager.ts | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/public/app/features/search/constants.ts b/public/app/features/search/constants.ts index 615dfa35493..2580147cf36 100644 --- a/public/app/features/search/constants.ts +++ b/public/app/features/search/constants.ts @@ -11,6 +11,7 @@ export const GENERAL_FOLDER_UID = 'general'; export const GENERAL_FOLDER_TITLE = 'General'; export const SEARCH_PANELS_LOCAL_STORAGE_KEY = 'grafana.search.include.panels'; export const SEARCH_SELECTED_LAYOUT = 'grafana.search.layout'; +export const SEARCH_SELECTED_SORT = 'grafana.search.sort'; export const TYPE_KIND_MAP: { [key: string]: DashboardSearchItemType } = { dashboard: DashboardSearchItemType.DashDB, folder: DashboardSearchItemType.DashFolder, diff --git a/public/app/features/search/state/SearchStateManager.ts b/public/app/features/search/state/SearchStateManager.ts index f376021bc61..0b4d93d185c 100644 --- a/public/app/features/search/state/SearchStateManager.ts +++ b/public/app/features/search/state/SearchStateManager.ts @@ -6,7 +6,7 @@ import { TermCount } from 'app/core/components/TagFilter/TagFilter'; import { StateManagerBase } from 'app/core/services/StateManagerBase'; import store from 'app/core/store'; -import { SEARCH_PANELS_LOCAL_STORAGE_KEY, SEARCH_SELECTED_LAYOUT } from '../constants'; +import { SEARCH_PANELS_LOCAL_STORAGE_KEY, SEARCH_SELECTED_LAYOUT, SEARCH_SELECTED_SORT } from '../constants'; import { reportDashboardListViewed, reportSearchFailedQueryInteraction, @@ -113,6 +113,10 @@ export class SearchStateManager extends StateManagerBase { }; onSortChange = (sort: string | undefined) => { + if (sort) { + localStorage.setItem(SEARCH_SELECTED_SORT, sort); + } + if (this.state.layout === SearchLayout.Folders) { this.setStateAndDoSearch({ sort, layout: SearchLayout.List }); } else { @@ -260,13 +264,14 @@ export function getSearchStateManager() { if (!stateManager) { const selectedLayout = localStorage.getItem(SEARCH_SELECTED_LAYOUT) as SearchLayout; const layout = selectedLayout ?? initialState.layout; + const sort = localStorage.getItem(SEARCH_SELECTED_SORT) ?? undefined; let includePanels = store.getBool(SEARCH_PANELS_LOCAL_STORAGE_KEY, true); if (includePanels) { includePanels = false; } - stateManager = new SearchStateManager({ ...initialState, layout: layout, includePanels }); + stateManager = new SearchStateManager({ ...initialState, layout, sort, includePanels }); } return stateManager; From dab3fac01bb0a0f3ad46fe7885c32f119b8b8d51 Mon Sep 17 00:00:00 2001 From: Oscar Kilhed Date: Fri, 27 Jan 2023 12:51:10 +0100 Subject: [PATCH 023/117] Grafana DS: Fix dropzone showing upload file even if there is a file (#62200) * Grafana DS: Fix dropzone showing upload file even if there is a file --- .../FileDropzone/FileDropzone.test.tsx | 6 ++++++ .../components/FileDropzone/FileDropzone.tsx | 17 +++++++++++++---- .../grafana/components/QueryEditor.tsx | 5 +++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.test.tsx b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.test.tsx index e60f6818e2c..0389d7ffd2a 100644 --- a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.test.tsx +++ b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.test.tsx @@ -104,6 +104,12 @@ describe('The FileDropzone component', () => { expect(onDrop).toBeCalledWith([fileToUpload], [], expect.anything()); }); + it('should display the text generated by a custom primaryTextSupplier', async () => { + const customText = 'custom text from primaryTextSuplier'; + render( customText} />); + expect(await screen.findByText(customText)).toBeInTheDocument(); + }); + it('should show children inside the dropzone', () => { const component = ( diff --git a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx index 81961749565..79ad4c2f92b 100644 --- a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx +++ b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx @@ -46,6 +46,7 @@ export interface FileDropzoneProps { */ fileListRenderer?: (file: DropzoneFile, removeFile: (file: DropzoneFile) => void) => ReactNode; onFileRemove?: (file: DropzoneFile) => void; + primaryTextSupplier?: (files: DropzoneFile[], options?: BackwardsCompatibleDropzoneOptions) => string; } export interface DropzoneFile { @@ -57,7 +58,15 @@ export interface DropzoneFile { retryUpload?: () => void; } -export function FileDropzone({ options, children, readAs, onLoad, fileListRenderer, onFileRemove }: FileDropzoneProps) { +export function FileDropzone({ + options, + primaryTextSupplier = getPrimaryText, + children, + readAs, + onLoad, + fileListRenderer, + onFileRemove, +}: FileDropzoneProps) { const [files, setFiles] = useState([]); const [errorMessages, setErrorMessages] = useState([]); @@ -198,7 +207,7 @@ export function FileDropzone({ options, children, readAs, onLoad, fileListRender
- {children ?? } + {children ?? }
{errorMessages.length > 0 && getErrorMessages()} {options?.accept && ( @@ -252,11 +261,11 @@ export function FileDropzoneDefaultChildren({
); } -function getPrimaryText(files: DropzoneFile[], options?: BackwardsCompatibleDropzoneOptions) { +function getPrimaryText(files?: DropzoneFile[], options?: BackwardsCompatibleDropzoneOptions) { if (options?.multiple === undefined || options?.multiple) { return 'Upload file'; } - return files.length ? 'Replace file' : 'Upload file'; + return files && files.length ? 'Replace file' : 'Upload file'; } function getAcceptedFileTypeText(accept: string | string[] | Accept) { diff --git a/public/app/plugins/datasource/grafana/components/QueryEditor.tsx b/public/app/plugins/datasource/grafana/components/QueryEditor.tsx index 3956c836a19..23c16c1a213 100644 --- a/public/app/plugins/datasource/grafana/components/QueryEditor.tsx +++ b/public/app/plugins/datasource/grafana/components/QueryEditor.tsx @@ -68,6 +68,10 @@ export class UnthemedQueryEditor extends PureComponent { }, ]; + dropzoneTextSupplier = () => { + return this.props?.query?.file ? 'Replace file' : 'Upload file'; + }; + constructor(props: Props) { super(props); @@ -399,6 +403,7 @@ export class UnthemedQueryEditor extends PureComponent { fileListRenderer={this.fileListRenderer} options={{ onDropAccepted: this.onDropAccepted, maxSize: 200000, multiple: false }} onLoad={this.onFileDrop} + primaryTextSupplier={this.dropzoneTextSupplier} >
{file && (
From eb9ef34272c9642706e2caeb04a79eb116a9a8f5 Mon Sep 17 00:00:00 2001 From: Ieva Date: Fri, 27 Jan 2023 12:12:30 +0000 Subject: [PATCH 024/117] RBAC: Permission check performance improvements for the new search (#60729) * Add checker and update the resource filter function for new search * Add tests for checker * small fixes * handle location for panels correctly * clean up checker code and extend the tests for it * more fixes, but tests don't quite work yet * a small change to return error * cleanup * more simplification * fix tests * correct wrong argument ordering & use constant * Apply suggestions from code review Co-authored-by: Artur Wierzbicki * import * check general folder from permission checker function * handle root folder aka general folder properly * update tests * clean up * lint * add fix from main Co-authored-by: Karl Persson Co-authored-by: Artur Wierzbicki --- pkg/services/accesscontrol/checker.go | 57 +++++++++ pkg/services/accesscontrol/checker_test.go | 111 ++++++++++++++++++ pkg/services/accesscontrol/scope.go | 25 ++-- pkg/services/searchV2/auth.go | 41 ++++--- pkg/services/searchV2/bluge.go | 5 +- pkg/services/searchV2/filter.go | 37 +++--- pkg/services/searchV2/index.go | 21 +--- pkg/services/searchV2/index_test.go | 25 ++-- pkg/services/searchV2/service.go | 2 +- .../searchV2/testdata/basic-search.jsonc | 4 +- ...-dashboard-removed-on-folder-removed.jsonc | 4 +- .../multiple-tokens-beginning-lower.jsonc | 4 +- .../testdata/multiple-tokens-beginning.jsonc | 4 +- .../multiple-tokens-middle-lower.jsonc | 4 +- .../testdata/multiple-tokens-middle.jsonc | 4 +- .../testdata/ngram-camel-case-split.jsonc | 4 +- .../testdata/ngram-punctuation-split.jsonc | 4 +- .../searchV2/testdata/ngram-simple.jsonc | 8 +- .../prefix-search-beginning-lower.jsonc | 4 +- .../testdata/prefix-search-beginning.jsonc | 4 +- .../testdata/prefix-search-middle-lower.jsonc | 4 +- .../testdata/prefix-search-middle.jsonc | 4 +- .../prefix-search-ngram-exceeded.jsonc | 4 +- .../testdata/scattered-tokens-match.jsonc | 4 +- pkg/services/searchV2/testdata/sort-asc.jsonc | 8 +- .../searchV2/testdata/sort-desc.jsonc | 8 +- pkg/services/store/entity_events.go | 4 + 27 files changed, 289 insertions(+), 119 deletions(-) create mode 100644 pkg/services/accesscontrol/checker.go create mode 100644 pkg/services/accesscontrol/checker_test.go diff --git a/pkg/services/accesscontrol/checker.go b/pkg/services/accesscontrol/checker.go new file mode 100644 index 00000000000..1b0e7d48e03 --- /dev/null +++ b/pkg/services/accesscontrol/checker.go @@ -0,0 +1,57 @@ +package accesscontrol + +import ( + "github.com/grafana/grafana/pkg/services/user" +) + +func Checker(user *user.SignedInUser, action string) func(scopes ...string) bool { + if user.Permissions == nil || user.Permissions[user.OrgID] == nil { + return func(scopes ...string) bool { return false } + } + + userScopes, ok := user.Permissions[user.OrgID][action] + if !ok { + return func(scopes ...string) bool { return false } + } + + lookup := make(map[string]bool, len(userScopes)) + for i := range userScopes { + lookup[userScopes[i]] = true + } + + var checkedWildcards bool + var hasWildcard bool + + return func(scopes ...string) bool { + if !checkedWildcards { + wildcards := wildcardsFromScopes(scopes...) + for _, w := range wildcards { + if _, ok := lookup[w]; ok { + hasWildcard = true + break + } + } + checkedWildcards = true + } + + if hasWildcard { + return true + } + + for _, s := range scopes { + if lookup[s] { + return true + } + } + return false + } +} + +func wildcardsFromScopes(scopes ...string) Wildcards { + prefixes := make([]string, len(scopes)) + for _, scope := range scopes { + prefixes = append(prefixes, ScopePrefix(scope)) + } + + return WildcardsFromPrefixes(prefixes) +} diff --git a/pkg/services/accesscontrol/checker_test.go b/pkg/services/accesscontrol/checker_test.go new file mode 100644 index 00000000000..a9e5798978b --- /dev/null +++ b/pkg/services/accesscontrol/checker_test.go @@ -0,0 +1,111 @@ +package accesscontrol + +import ( + "strconv" + "testing" + + "github.com/grafana/grafana/pkg/services/user" + "github.com/stretchr/testify/assert" +) + +type testData struct { + uid string + folderUid string +} + +func (d testData) Scopes() []string { + return []string{ + "dashboards:uid:" + d.uid, + "folders:uid:" + d.folderUid, + } +} + +func generateTestData() []testData { + var data []testData + for i := 1; i < 100; i++ { + data = append(data, testData{ + uid: strconv.Itoa(i), + folderUid: strconv.Itoa(i + 100), + }) + } + return data +} + +func Test_Checker(t *testing.T) { + data := generateTestData() + type testCase struct { + desc string + user *user.SignedInUser + expectedLen int + } + tests := []testCase{ + { + desc: "should pass for every entity with dashboard wildcard scope", + user: &user.SignedInUser{ + OrgID: 1, + Permissions: map[int64]map[string][]string{1: {"dashboards:read": {"dashboards:*"}}}, + }, + expectedLen: len(data), + }, + { + desc: "should pass for every entity with folder wildcard scope", + user: &user.SignedInUser{ + OrgID: 1, + Permissions: map[int64]map[string][]string{1: {"dashboards:read": {"folders:*"}}}, + }, + expectedLen: len(data), + }, + { + desc: "should only pass for for 3 scopes", + user: &user.SignedInUser{ + OrgID: 1, + Permissions: map[int64]map[string][]string{1: {"dashboards:read": {"dashboards:uid:4", "dashboards:uid:50", "dashboards:uid:99"}}}, + }, + expectedLen: 3, + }, + { + desc: "should only pass 4 with secondary supported scope", + user: &user.SignedInUser{ + OrgID: 1, + Permissions: map[int64]map[string][]string{1: {"dashboards:read": {"folders:uid:104", "folders:uid:150", "folders:uid:154", "folders:uid:199"}}}, + }, + expectedLen: 4, + }, + { + desc: "should only pass 4 with some dashboard and some folder scopes", + user: &user.SignedInUser{ + OrgID: 1, + Permissions: map[int64]map[string][]string{1: {"dashboards:read": {"dashboards:uid:1", "dashboards:uid:2", "folders:uid:154", "folders:uid:199"}}}, + }, + expectedLen: 4, + }, + { + desc: "should only pass 2 with overlapping dashboard and folder scopes", + user: &user.SignedInUser{ + OrgID: 1, + Permissions: map[int64]map[string][]string{1: {"dashboards:read": {"dashboards:uid:101", "dashboards:uid:2", "folders:uid:101", "folders:uid:102"}}}, + }, + expectedLen: 2, + }, + { + desc: "should pass none for missing action", + user: &user.SignedInUser{ + OrgID: 1, + Permissions: map[int64]map[string][]string{1: {}}, + }, + expectedLen: 0, + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + check := Checker(tt.user, "dashboards:read") + numPasses := 0 + for _, d := range data { + if ok := check(d.Scopes()...); ok { + numPasses++ + } + } + assert.Equal(t, tt.expectedLen, numPasses) + }) + } +} diff --git a/pkg/services/accesscontrol/scope.go b/pkg/services/accesscontrol/scope.go index 8b58b9bd8d2..4805b25f2a1 100644 --- a/pkg/services/accesscontrol/scope.go +++ b/pkg/services/accesscontrol/scope.go @@ -142,19 +142,26 @@ func (s scopeProviderImpl) GetResourceAllIDScope() string { return GetResourceAllIDScope(s.root) } -// WildcardsFromPrefix generates valid wildcards from prefix -// datasource:uid: => "*", "datasource:*", "datasource:uid:*" func WildcardsFromPrefix(prefix string) Wildcards { + return WildcardsFromPrefixes([]string{prefix}) +} + +// WildcardsFromPrefixes generates valid wildcards from prefixes +// datasource:uid: => "*", "datasource:*", "datasource:uid:*" +func WildcardsFromPrefixes(prefixes []string) Wildcards { var b strings.Builder wildcards := Wildcards{"*"} - parts := strings.Split(prefix, ":") - for _, p := range parts { - if p == "" { - continue + for _, prefix := range prefixes { + parts := strings.Split(prefix, ":") + for _, p := range parts { + if p == "" { + continue + } + b.WriteString(p) + b.WriteRune(':') + wildcards = append(wildcards, b.String()+"*") } - b.WriteString(p) - b.WriteRune(':') - wildcards = append(wildcards, b.String()+"*") + b.Reset() } return wildcards } diff --git a/pkg/services/searchV2/auth.go b/pkg/services/searchV2/auth.go index 9fa87551ae2..60437f543cc 100644 --- a/pkg/services/searchV2/auth.go +++ b/pkg/services/searchV2/auth.go @@ -7,21 +7,20 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/sqlstore/permissions" - "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" "github.com/grafana/grafana/pkg/services/user" ) // ResourceFilter checks if a given a uid (resource identifier) check if we have the requested permission -type ResourceFilter func(uid string) bool +type ResourceFilter func(kind entityKind, uid, parentUID string) bool // FutureAuthService eventually implemented by the security service type FutureAuthService interface { GetDashboardReadFilter(user *user.SignedInUser) (ResourceFilter, error) } -var _ FutureAuthService = (*simpleSQLAuthService)(nil) +var _ FutureAuthService = (*simpleAuthService)(nil) -type simpleSQLAuthService struct { +type simpleAuthService struct { sql db.DB ac accesscontrol.Service } @@ -30,22 +29,26 @@ type dashIdQueryResult struct { UID string `xorm:"uid"` } -func (a *simpleSQLAuthService) getDashboardTableAuthFilter(user *user.SignedInUser) searchstore.FilterWhere { - if a.ac.IsDisabled() { - return permissions.DashboardPermissionFilter{ - OrgRole: user.OrgRole, - OrgId: user.OrgID, - Dialect: a.sql.GetDialect(), - UserId: user.UserID, - PermissionLevel: dashboards.PERMISSION_VIEW, - } +func (a *simpleAuthService) GetDashboardReadFilter(user *user.SignedInUser) (ResourceFilter, error) { + if !a.ac.IsDisabled() { + canReadDashboard, canReadFolder := accesscontrol.Checker(user, dashboards.ActionDashboardsRead), accesscontrol.Checker(user, dashboards.ActionFoldersRead) + return func(kind entityKind, uid, parent string) bool { + if kind == entityKindFolder { + return canReadFolder(dashboards.ScopeFoldersProvider.GetResourceScopeUID(uid)) + } else if kind == entityKindDashboard { + return canReadDashboard(dashboards.ScopeDashboardsProvider.GetResourceScopeUID(uid), dashboards.ScopeFoldersProvider.GetResourceScopeUID(parent)) + } + return false + }, nil } - return permissions.NewAccessControlDashboardPermissionFilter(user, dashboards.PERMISSION_VIEW, "") -} - -func (a *simpleSQLAuthService) GetDashboardReadFilter(user *user.SignedInUser) (ResourceFilter, error) { - filter := a.getDashboardTableAuthFilter(user) + filter := permissions.DashboardPermissionFilter{ + OrgRole: user.OrgRole, + OrgId: user.OrgID, + Dialect: a.sql.GetDialect(), + UserId: user.UserID, + PermissionLevel: dashboards.PERMISSION_VIEW, + } rows := make([]*dashIdQueryResult, 0) err := a.sql.WithDbSession(context.Background(), func(sess *db.Session) error { @@ -72,7 +75,7 @@ func (a *simpleSQLAuthService) GetDashboardReadFilter(user *user.SignedInUser) ( uids[rows[i].UID] = true } - return func(uid string) bool { + return func(_ entityKind, uid, _ string) bool { return uids[uid] }, err } diff --git a/pkg/services/searchV2/bluge.go b/pkg/services/searchV2/bluge.go index 04bec20680c..223e46ec966 100644 --- a/pkg/services/searchV2/bluge.go +++ b/pkg/services/searchV2/bluge.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/slugify" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/store/entity" ) @@ -70,6 +71,7 @@ func initOrgIndex(dashboards []dashboard, logger log.Logger, extendDoc ExtendDas // First index the folders to construct folderIdLookup. folderIdLookup := make(map[int64]string, 50) + folderIdLookup[0] = folder.GeneralFolderUID for _, dash := range dashboards { if !dash.isFolder { continue @@ -83,9 +85,6 @@ func initOrgIndex(dashboards []dashboard, logger log.Logger, extendDoc ExtendDas return nil, err } uid := dash.uid - if uid == "" { - uid = "general" - } folderIdLookup[dash.id] = uid } diff --git a/pkg/services/searchV2/filter.go b/pkg/services/searchV2/filter.go index b64d9c2adae..b68c79297ba 100644 --- a/pkg/services/searchV2/filter.go +++ b/pkg/services/searchV2/filter.go @@ -2,6 +2,7 @@ package searchV2 import ( "regexp" + "strings" "github.com/blugelabs/bluge" "github.com/blugelabs/bluge/search" @@ -36,7 +37,7 @@ func (r entityKind) supportsAuthzCheck() bool { } var ( - permissionFilterFields = []string{documentFieldUID, documentFieldKind} + permissionFilterFields = []string{documentFieldUID, documentFieldKind, documentFieldLocation} panelIdFieldRegex = regexp.MustCompile(`^(.*)#([0-9]{1,4})$`) panelIdFieldDashboardUidSubmatchIndex = 1 panelIdFieldPanelIdSubmatchIndex = 2 @@ -65,7 +66,7 @@ func (q *PermissionFilter) logAccessDecision(decision bool, kind interface{}, id } } -func (q *PermissionFilter) canAccess(kind entityKind, id string) bool { +func (q *PermissionFilter) canAccess(kind entityKind, id, location string) bool { if !kind.supportsAuthzCheck() { q.logAccessDecision(false, kind, id, "entityDoesNotSupportAuthz") return false @@ -74,29 +75,28 @@ func (q *PermissionFilter) canAccess(kind entityKind, id string) bool { // TODO add `kind` to the `ResourceFilter` interface so that we can move the switch out of here // switch kind { - case entityKindFolder: - if id == "" { - q.logAccessDecision(true, kind, id, "generalFolder") - return true - } - fallthrough - case entityKindDashboard: - decision := q.filter(id) + case entityKindFolder, entityKindDashboard: + decision := q.filter(kind, id, location) q.logAccessDecision(decision, kind, id, "resourceFilter") return decision case entityKindPanel: matches := panelIdFieldRegex.FindStringSubmatch(id) - submatchCount := len(matches) if submatchCount != panelIdFieldRegexExpectedSubmatchCount { q.logAccessDecision(false, kind, id, "invalidPanelIdFieldRegexSubmatchCount", "submatchCount", submatchCount, "expectedSubmatchCount", panelIdFieldRegexExpectedSubmatchCount) return false } - dashboardUid := matches[panelIdFieldDashboardUidSubmatchIndex] - decision := q.filter(dashboardUid) - q.logAccessDecision(decision, kind, id, "resourceFilter", "dashboardUid", dashboardUid, "panelId", matches[panelIdFieldPanelIdSubmatchIndex]) + // Location is / + if !strings.HasSuffix(location, "/"+dashboardUid) { + q.logAccessDecision(false, kind, id, "invalidLocation", "location", location, "dashboardUid", dashboardUid) + return false + } + folderUid := location[:len(location)-len(dashboardUid)-1] + + decision := q.filter(entityKindDashboard, dashboardUid, folderUid) + q.logAccessDecision(decision, kind, id, "resourceFilter", "folderUid", folderUid, "dashboardUid", dashboardUid, "panelId", matches[panelIdFieldPanelIdSubmatchIndex]) return decision default: q.logAccessDecision(false, kind, id, "reason", "unknownKind") @@ -111,13 +111,18 @@ func (q *PermissionFilter) Searcher(i search.Reader, options search.SearcherOpti } s, err := searcher.NewMatchAllSearcher(i, 1, similarity.ConstantScorer(1), options) + if err != nil { + return nil, err + } return searcher.NewFilteringSearcher(s, func(d *search.DocumentMatch) bool { - var kind, id string + var kind, id, location string err := dvReader.VisitDocumentValues(d.Number, func(field string, term []byte) { if field == documentFieldKind { kind = string(term) } else if field == documentFieldUID { id = string(term) + } else if field == documentFieldLocation { + location = string(term) } }) if err != nil { @@ -131,6 +136,6 @@ func (q *PermissionFilter) Searcher(i search.Reader, options search.SearcherOpti return false } - return q.canAccess(e, id) + return q.canAccess(e, id, location) }), err } diff --git a/pkg/services/searchV2/index.go b/pkg/services/searchV2/index.go index b3efdb079ee..2ea2540aa47 100644 --- a/pkg/services/searchV2/index.go +++ b/pkg/services/searchV2/index.go @@ -20,6 +20,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/store" "github.com/grafana/grafana/pkg/services/store/entity" kdash "github.com/grafana/grafana/pkg/services/store/kind/dashboard" @@ -761,7 +762,7 @@ func (i *searchIndex) updateDashboard(ctx context.Context, orgID int64, index *o var folderUID string if dash.folderID == 0 { - folderUID = "general" + folderUID = folder.GeneralFolderUID } else { var err error folderUID, err = i.folderIdLookup(ctx, dash.folderID) @@ -900,23 +901,7 @@ func (l sqlDashboardLoader) LoadDashboards(ctx context.Context, orgID int64, das limit := 1 if dashboardUID == "" { - limit = l.settings.DashboardLoadingBatchSize - dashboards = make([]dashboard, 0, limit+1) - - // Add the root folder ID (does not exist in SQL). - dashboards = append(dashboards, dashboard{ - id: 0, - uid: "", - isFolder: true, - folderID: 0, - slug: "", - created: time.Now(), - updated: time.Now(), - summary: &entity.EntitySummary{ - //ID: 0, - Name: "General", - }, - }) + dashboards = make([]dashboard, 0, l.settings.DashboardLoadingBatchSize) } loadDatasourceCtx, loadDatasourceSpan := l.tracer.Start(ctx, "sqlDashboardLoader LoadDatasourceLookup") diff --git a/pkg/services/searchV2/index_test.go b/pkg/services/searchV2/index_test.go index 431b43d6d6e..69ab523cf8f 100644 --- a/pkg/services/searchV2/index_test.go +++ b/pkg/services/searchV2/index_test.go @@ -9,14 +9,13 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/store" "github.com/grafana/grafana/pkg/services/store/entity" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/services/store" - "github.com/blugelabs/bluge" "github.com/grafana/grafana-plugin-sdk-go/experimental" "github.com/stretchr/testify/require" @@ -32,11 +31,11 @@ func (t *testDashboardLoader) LoadDashboards(_ context.Context, _ int64, _ strin var testLogger = log.New("index-test-logger") -var testAllowAllFilter = func(uid string) bool { +var testAllowAllFilter = func(kind entityKind, uid, parent string) bool { return true } -var testDisallowAllFilter = func(uid string) bool { +var testDisallowAllFilter = func(kind entityKind, uid, parent string) bool { return false } @@ -430,8 +429,8 @@ var dashboardsWithFolders = []dashboard{ summary: &entity.EntitySummary{ Name: "Dashboard in folder 1", Nested: []*entity.EntitySummary{ - newNestedPanel(1, "Panel 1"), - newNestedPanel(2, "Panel 2"), + newNestedPanel(1, 2, "Panel 1"), + newNestedPanel(2, 2, "Panel 2"), }, }, }, @@ -442,7 +441,7 @@ var dashboardsWithFolders = []dashboard{ summary: &entity.EntitySummary{ Name: "Dashboard in folder 2", Nested: []*entity.EntitySummary{ - newNestedPanel(3, "Panel 3"), + newNestedPanel(3, 3, "Panel 3"), }, }, }, @@ -452,7 +451,7 @@ var dashboardsWithFolders = []dashboard{ summary: &entity.EntitySummary{ Name: "One more dash", Nested: []*entity.EntitySummary{ - newNestedPanel(4, "Panel 4"), + newNestedPanel(4, 4, "Panel 4"), }, }, }, @@ -509,17 +508,17 @@ var dashboardsWithPanels = []dashboard{ summary: &entity.EntitySummary{ Name: "My Dash", Nested: []*entity.EntitySummary{ - newNestedPanel(1, "Panel 1"), - newNestedPanel(2, "Panel 2"), + newNestedPanel(1, 1, "Panel 1"), + newNestedPanel(2, 1, "Panel 2"), }, }, }, } -func newNestedPanel(id int64, name string) *entity.EntitySummary { +func newNestedPanel(id, dashId int64, name string) *entity.EntitySummary { summary := &entity.EntitySummary{ Kind: "panel", - UID: fmt.Sprintf("???#%d", id), + UID: fmt.Sprintf("%d#%d", dashId, id), } summary.Name = name return summary diff --git a/pkg/services/searchV2/service.go b/pkg/services/searchV2/service.go index e510709172f..3f611f51f99 100644 --- a/pkg/services/searchV2/service.go +++ b/pkg/services/searchV2/service.go @@ -91,7 +91,7 @@ func ProvideService(cfg *setting.Cfg, sql db.DB, entityEventStore store.EntityEv cfg: cfg, sql: sql, ac: ac, - auth: &simpleSQLAuthService{ + auth: &simpleAuthService{ sql: sql, ac: ac, }, diff --git a/pkg/services/searchV2/testdata/basic-search.jsonc b/pkg/services/searchV2/testdata/basic-search.jsonc index 0d1784474ed..75d20de5933 100644 --- a/pkg/services/searchV2/testdata/basic-search.jsonc +++ b/pkg/services/searchV2/testdata/basic-search.jsonc @@ -13,7 +13,7 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | // +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+ -// | dashboard | 2 | boom | | /pfix/d/2/ | null | [] | | +// | dashboard | 2 | boom | | /pfix/d/2/ | null | [] | general | // +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+ // // @@ -122,7 +122,7 @@ [] ], [ - "" + "general" ] ] } diff --git a/pkg/services/searchV2/testdata/folders-dashboard-removed-on-folder-removed.jsonc b/pkg/services/searchV2/testdata/folders-dashboard-removed-on-folder-removed.jsonc index 019e0ba2b28..29082357672 100644 --- a/pkg/services/searchV2/testdata/folders-dashboard-removed-on-folder-removed.jsonc +++ b/pkg/services/searchV2/testdata/folders-dashboard-removed-on-folder-removed.jsonc @@ -13,7 +13,7 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | // +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+ -// | dashboard | 4 | One more dash | | /pfix/d/4/ | null | [] | | +// | dashboard | 4 | One more dash | | /pfix/d/4/ | null | [] | general | // +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+ // // @@ -122,7 +122,7 @@ [] ], [ - "" + "general" ] ] } diff --git a/pkg/services/searchV2/testdata/multiple-tokens-beginning-lower.jsonc b/pkg/services/searchV2/testdata/multiple-tokens-beginning-lower.jsonc index 4536d5a36a0..3f1af3821bd 100644 --- a/pkg/services/searchV2/testdata/multiple-tokens-beginning-lower.jsonc +++ b/pkg/services/searchV2/testdata/multiple-tokens-beginning-lower.jsonc @@ -13,7 +13,7 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | // +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+ -// | dashboard | 1 | Archer Data System | | /pfix/d/1/ | null | [] | | +// | dashboard | 1 | Archer Data System | | /pfix/d/1/ | null | [] | general | // +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+ // // @@ -122,7 +122,7 @@ [] ], [ - "" + "general" ] ] } diff --git a/pkg/services/searchV2/testdata/multiple-tokens-beginning.jsonc b/pkg/services/searchV2/testdata/multiple-tokens-beginning.jsonc index 4536d5a36a0..3f1af3821bd 100644 --- a/pkg/services/searchV2/testdata/multiple-tokens-beginning.jsonc +++ b/pkg/services/searchV2/testdata/multiple-tokens-beginning.jsonc @@ -13,7 +13,7 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | // +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+ -// | dashboard | 1 | Archer Data System | | /pfix/d/1/ | null | [] | | +// | dashboard | 1 | Archer Data System | | /pfix/d/1/ | null | [] | general | // +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+ // // @@ -122,7 +122,7 @@ [] ], [ - "" + "general" ] ] } diff --git a/pkg/services/searchV2/testdata/multiple-tokens-middle-lower.jsonc b/pkg/services/searchV2/testdata/multiple-tokens-middle-lower.jsonc index 4102a2ad602..87e8e460095 100644 --- a/pkg/services/searchV2/testdata/multiple-tokens-middle-lower.jsonc +++ b/pkg/services/searchV2/testdata/multiple-tokens-middle-lower.jsonc @@ -13,7 +13,7 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | // +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+ -// | dashboard | 2 | Document Sync repo | | /pfix/d/2/ | null | [] | | +// | dashboard | 2 | Document Sync repo | | /pfix/d/2/ | null | [] | general | // +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+ // // @@ -122,7 +122,7 @@ [] ], [ - "" + "general" ] ] } diff --git a/pkg/services/searchV2/testdata/multiple-tokens-middle.jsonc b/pkg/services/searchV2/testdata/multiple-tokens-middle.jsonc index 4536d5a36a0..3f1af3821bd 100644 --- a/pkg/services/searchV2/testdata/multiple-tokens-middle.jsonc +++ b/pkg/services/searchV2/testdata/multiple-tokens-middle.jsonc @@ -13,7 +13,7 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | // +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+ -// | dashboard | 1 | Archer Data System | | /pfix/d/1/ | null | [] | | +// | dashboard | 1 | Archer Data System | | /pfix/d/1/ | null | [] | general | // +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+ // // @@ -122,7 +122,7 @@ [] ], [ - "" + "general" ] ] } diff --git a/pkg/services/searchV2/testdata/ngram-camel-case-split.jsonc b/pkg/services/searchV2/testdata/ngram-camel-case-split.jsonc index 8a43418872a..c49bf638f3c 100644 --- a/pkg/services/searchV2/testdata/ngram-camel-case-split.jsonc +++ b/pkg/services/searchV2/testdata/ngram-camel-case-split.jsonc @@ -13,7 +13,7 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | // +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+ -// | dashboard | 1 | heatTorkel | | /pfix/d/1/ | null | [] | | +// | dashboard | 1 | heatTorkel | | /pfix/d/1/ | null | [] | general | // +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+ // // @@ -122,7 +122,7 @@ [] ], [ - "" + "general" ] ] } diff --git a/pkg/services/searchV2/testdata/ngram-punctuation-split.jsonc b/pkg/services/searchV2/testdata/ngram-punctuation-split.jsonc index a6501a132b3..bf363db06c3 100644 --- a/pkg/services/searchV2/testdata/ngram-punctuation-split.jsonc +++ b/pkg/services/searchV2/testdata/ngram-punctuation-split.jsonc @@ -13,7 +13,7 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | // +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+ -// | dashboard | 1 | heat-torkel | | /pfix/d/1/ | null | [] | | +// | dashboard | 1 | heat-torkel | | /pfix/d/1/ | null | [] | general | // +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+ // // @@ -122,7 +122,7 @@ [] ], [ - "" + "general" ] ] } diff --git a/pkg/services/searchV2/testdata/ngram-simple.jsonc b/pkg/services/searchV2/testdata/ngram-simple.jsonc index 0a85bf090b1..a9b65c0e829 100644 --- a/pkg/services/searchV2/testdata/ngram-simple.jsonc +++ b/pkg/services/searchV2/testdata/ngram-simple.jsonc @@ -13,8 +13,8 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | // +----------------+----------------+------------------+------------------+----------------+--------------------------+-------------------------+----------------+ -// | dashboard | 1 | heat-torkel | | /pfix/d/1/ | null | [] | | -// | dashboard | 2 | topology heatmap | | /pfix/d/2/ | null | [] | | +// | dashboard | 1 | heat-torkel | | /pfix/d/1/ | null | [] | general | +// | dashboard | 2 | topology heatmap | | /pfix/d/2/ | null | [] | general | // +----------------+----------------+------------------+------------------+----------------+--------------------------+-------------------------+----------------+ // // @@ -130,8 +130,8 @@ [] ], [ - "", - "" + "general", + "general" ] ] } diff --git a/pkg/services/searchV2/testdata/prefix-search-beginning-lower.jsonc b/pkg/services/searchV2/testdata/prefix-search-beginning-lower.jsonc index 4536d5a36a0..3f1af3821bd 100644 --- a/pkg/services/searchV2/testdata/prefix-search-beginning-lower.jsonc +++ b/pkg/services/searchV2/testdata/prefix-search-beginning-lower.jsonc @@ -13,7 +13,7 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | // +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+ -// | dashboard | 1 | Archer Data System | | /pfix/d/1/ | null | [] | | +// | dashboard | 1 | Archer Data System | | /pfix/d/1/ | null | [] | general | // +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+ // // @@ -122,7 +122,7 @@ [] ], [ - "" + "general" ] ] } diff --git a/pkg/services/searchV2/testdata/prefix-search-beginning.jsonc b/pkg/services/searchV2/testdata/prefix-search-beginning.jsonc index 4536d5a36a0..3f1af3821bd 100644 --- a/pkg/services/searchV2/testdata/prefix-search-beginning.jsonc +++ b/pkg/services/searchV2/testdata/prefix-search-beginning.jsonc @@ -13,7 +13,7 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | // +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+ -// | dashboard | 1 | Archer Data System | | /pfix/d/1/ | null | [] | | +// | dashboard | 1 | Archer Data System | | /pfix/d/1/ | null | [] | general | // +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+ // // @@ -122,7 +122,7 @@ [] ], [ - "" + "general" ] ] } diff --git a/pkg/services/searchV2/testdata/prefix-search-middle-lower.jsonc b/pkg/services/searchV2/testdata/prefix-search-middle-lower.jsonc index 4102a2ad602..87e8e460095 100644 --- a/pkg/services/searchV2/testdata/prefix-search-middle-lower.jsonc +++ b/pkg/services/searchV2/testdata/prefix-search-middle-lower.jsonc @@ -13,7 +13,7 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | // +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+ -// | dashboard | 2 | Document Sync repo | | /pfix/d/2/ | null | [] | | +// | dashboard | 2 | Document Sync repo | | /pfix/d/2/ | null | [] | general | // +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+ // // @@ -122,7 +122,7 @@ [] ], [ - "" + "general" ] ] } diff --git a/pkg/services/searchV2/testdata/prefix-search-middle.jsonc b/pkg/services/searchV2/testdata/prefix-search-middle.jsonc index 4102a2ad602..87e8e460095 100644 --- a/pkg/services/searchV2/testdata/prefix-search-middle.jsonc +++ b/pkg/services/searchV2/testdata/prefix-search-middle.jsonc @@ -13,7 +13,7 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | // +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+ -// | dashboard | 2 | Document Sync repo | | /pfix/d/2/ | null | [] | | +// | dashboard | 2 | Document Sync repo | | /pfix/d/2/ | null | [] | general | // +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+ // // @@ -122,7 +122,7 @@ [] ], [ - "" + "general" ] ] } diff --git a/pkg/services/searchV2/testdata/prefix-search-ngram-exceeded.jsonc b/pkg/services/searchV2/testdata/prefix-search-ngram-exceeded.jsonc index 2e64a8cb02d..b09b73885d4 100644 --- a/pkg/services/searchV2/testdata/prefix-search-ngram-exceeded.jsonc +++ b/pkg/services/searchV2/testdata/prefix-search-ngram-exceeded.jsonc @@ -13,7 +13,7 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | // +----------------+----------------+--------------------------------+------------------+----------------+--------------------------+-------------------------+----------------+ -// | dashboard | 1 | Eyjafjallajökull Eruption data | | /pfix/d/1/ | null | [] | | +// | dashboard | 1 | Eyjafjallajökull Eruption data | | /pfix/d/1/ | null | [] | general | // +----------------+----------------+--------------------------------+------------------+----------------+--------------------------+-------------------------+----------------+ // // @@ -122,7 +122,7 @@ [] ], [ - "" + "general" ] ] } diff --git a/pkg/services/searchV2/testdata/scattered-tokens-match.jsonc b/pkg/services/searchV2/testdata/scattered-tokens-match.jsonc index 19404b6a481..84be3a0a752 100644 --- a/pkg/services/searchV2/testdata/scattered-tokens-match.jsonc +++ b/pkg/services/searchV2/testdata/scattered-tokens-match.jsonc @@ -13,7 +13,7 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | // +----------------+----------------+----------------------------------------------------------------------+------------------+----------------+--------------------------+-------------------------+----------------+ -// | dashboard | 1 | Three can keep a secret, if two of them are dead (Benjamin Franklin) | | /pfix/d/1/ | null | [] | | +// | dashboard | 1 | Three can keep a secret, if two of them are dead (Benjamin Franklin) | | /pfix/d/1/ | null | [] | general | // +----------------+----------------+----------------------------------------------------------------------+------------------+----------------+--------------------------+-------------------------+----------------+ // // @@ -122,7 +122,7 @@ [] ], [ - "" + "general" ] ] } diff --git a/pkg/services/searchV2/testdata/sort-asc.jsonc b/pkg/services/searchV2/testdata/sort-asc.jsonc index a284a955e7f..c53caec66f3 100644 --- a/pkg/services/searchV2/testdata/sort-asc.jsonc +++ b/pkg/services/searchV2/testdata/sort-asc.jsonc @@ -14,8 +14,8 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | Type: []float64 | // +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+-----------------+ -// | dashboard | 1 | a-test | | /pfix/d/1/ | null | [] | | 0 | -// | dashboard | 2 | z-test | | /pfix/d/2/ | null | [] | | 1 | +// | dashboard | 1 | a-test | | /pfix/d/1/ | null | [] | general | 0 | +// | dashboard | 2 | z-test | | /pfix/d/2/ | null | [] | general | 1 | // +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+-----------------+ // // @@ -139,8 +139,8 @@ [] ], [ - "", - "" + "general", + "general" ], [ 0, diff --git a/pkg/services/searchV2/testdata/sort-desc.jsonc b/pkg/services/searchV2/testdata/sort-desc.jsonc index 998c5cf254d..540d0e82b62 100644 --- a/pkg/services/searchV2/testdata/sort-desc.jsonc +++ b/pkg/services/searchV2/testdata/sort-desc.jsonc @@ -14,8 +14,8 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | Type: []float64 | // +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+-----------------+ -// | dashboard | 2 | z-test | | /pfix/d/2/ | null | [] | | 3 | -// | dashboard | 1 | a-test | | /pfix/d/1/ | null | [] | | 2 | +// | dashboard | 2 | z-test | | /pfix/d/2/ | null | [] | general | 3 | +// | dashboard | 1 | a-test | | /pfix/d/1/ | null | [] | general | 2 | // +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+-----------------+ // // @@ -139,8 +139,8 @@ [] ], [ - "", - "" + "general", + "general" ], [ 3, diff --git a/pkg/services/store/entity_events.go b/pkg/services/store/entity_events.go index 027900bd9a3..fe4ec4ff3c6 100644 --- a/pkg/services/store/entity_events.go +++ b/pkg/services/store/entity_events.go @@ -148,6 +148,10 @@ func (e *entityEventService) Run(ctx context.Context) error { type dummyEntityEventsService struct { } +func NewDummyEntityEventsService() EntityEventsService { + return dummyEntityEventsService{} +} + func (d dummyEntityEventsService) Run(ctx context.Context) error { return nil } From 1a28650aabcef67d8353dce8a7e3e6e0e123c86b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Jan 2023 12:28:09 +0000 Subject: [PATCH 025/117] Update dependency @testing-library/dom to v8.20.0 (#61677) * Update dependency @testing-library/dom to v8.20.0 * do some lockfile surgery Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Ashley Harrison --- package.json | 2 +- packages/grafana-data/package.json | 2 +- packages/grafana-runtime/package.json | 2 +- packages/grafana-ui/package.json | 2 +- yarn.lock | 26 +++++++++++++------------- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 693479c9c47..b22ef26cf72 100644 --- a/package.json +++ b/package.json @@ -115,7 +115,7 @@ "@rtsao/plugin-proposal-class-properties": "7.0.1-patch.1", "@swc/core": "1.3.11", "@swc/helpers": "0.4.12", - "@testing-library/dom": "8.19.0", + "@testing-library/dom": "8.20.0", "@testing-library/jest-dom": "5.16.5", "@testing-library/react": "12.1.4", "@testing-library/react-hooks": "8.0.1", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 90282e0cb47..27a713d6c83 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -62,7 +62,7 @@ "@rollup/plugin-commonjs": "23.0.2", "@rollup/plugin-json": "5.0.1", "@rollup/plugin-node-resolve": "15.0.1", - "@testing-library/dom": "8.19.0", + "@testing-library/dom": "8.20.0", "@testing-library/jest-dom": "5.16.5", "@testing-library/react": "12.1.4", "@testing-library/react-hooks": "8.0.1", diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index b39f8343576..41a95599839 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -52,7 +52,7 @@ "@grafana/tsconfig": "^1.2.0-rc1", "@rollup/plugin-commonjs": "23.0.2", "@rollup/plugin-node-resolve": "15.0.1", - "@testing-library/dom": "8.19.0", + "@testing-library/dom": "8.20.0", "@testing-library/react": "12.1.4", "@testing-library/react-hooks": "8.0.1", "@testing-library/user-event": "14.4.3", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 37e44346188..1e8fae1a11b 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -133,7 +133,7 @@ "@storybook/preset-scss": "1.0.3", "@storybook/react": "6.5.14", "@storybook/theming": "6.5.14", - "@testing-library/dom": "8.19.0", + "@testing-library/dom": "8.20.0", "@testing-library/jest-dom": "5.16.5", "@testing-library/react": "12.1.4", "@testing-library/react-hooks": "8.0.1", diff --git a/yarn.lock b/yarn.lock index 668284d75c9..33e29967e36 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4595,7 +4595,7 @@ __metadata: "@rollup/plugin-commonjs": 23.0.2 "@rollup/plugin-json": 5.0.1 "@rollup/plugin-node-resolve": 15.0.1 - "@testing-library/dom": 8.19.0 + "@testing-library/dom": 8.20.0 "@testing-library/jest-dom": 5.16.5 "@testing-library/react": 12.1.4 "@testing-library/react-hooks": 8.0.1 @@ -4865,7 +4865,7 @@ __metadata: "@rollup/plugin-commonjs": 23.0.2 "@rollup/plugin-node-resolve": 15.0.1 "@sentry/browser": 6.19.7 - "@testing-library/dom": 8.19.0 + "@testing-library/dom": 8.20.0 "@testing-library/react": 12.1.4 "@testing-library/react-hooks": 8.0.1 "@testing-library/user-event": 14.4.3 @@ -5071,7 +5071,7 @@ __metadata: "@storybook/preset-scss": 1.0.3 "@storybook/react": 6.5.14 "@storybook/theming": 6.5.14 - "@testing-library/dom": 8.19.0 + "@testing-library/dom": 8.20.0 "@testing-library/jest-dom": 5.16.5 "@testing-library/react": 12.1.4 "@testing-library/react-hooks": 8.0.1 @@ -10164,19 +10164,19 @@ __metadata: languageName: node linkType: hard -"@testing-library/dom@npm:8.19.0, @testing-library/dom@npm:>=7, @testing-library/dom@npm:^8.0.0": - version: 8.19.0 - resolution: "@testing-library/dom@npm:8.19.0" +"@testing-library/dom@npm:8.20.0, @testing-library/dom@npm:>=7, @testing-library/dom@npm:^8.0.0": + version: 8.20.0 + resolution: "@testing-library/dom@npm:8.20.0" dependencies: "@babel/code-frame": ^7.10.4 "@babel/runtime": ^7.12.5 - "@types/aria-query": ^4.2.0 + "@types/aria-query": ^5.0.1 aria-query: ^5.0.0 chalk: ^4.1.0 dom-accessibility-api: ^0.5.9 lz-string: ^1.4.4 pretty-format: ^27.0.2 - checksum: 6bb93fef96703b6c47cf1b7cc8f71d402a9576084a94ba4e9926f51bd7bb1287fbb4f6942d82bd03fc6f3d998ae97e60f6aea4618f3a1ce6139597d2a4ecb7b9 + checksum: 1e599129a2fe91959ce80900a0a4897232b89e2a8e22c1f5950c36d39c97629ea86b4986b60b173b5525a05de33fde1e35836ea597b03de78cc51b122835c6f0 languageName: node linkType: hard @@ -10323,10 +10323,10 @@ __metadata: languageName: node linkType: hard -"@types/aria-query@npm:^4.2.0": - version: 4.2.2 - resolution: "@types/aria-query@npm:4.2.2" - checksum: 6f2ce11d91e2d665f3873258db19da752d91d85d3679eb5efcdf9c711d14492287e1e4eb52613b28e60375841a9e428594e745b68436c963d8bad4bf72188df3 +"@types/aria-query@npm:^5.0.1": + version: 5.0.1 + resolution: "@types/aria-query@npm:5.0.1" + checksum: 69fd7cceb6113ed370591aef04b3fd0742e9a1b06dd045c43531448847b85de181495e4566f98e776b37c422a12fd71866e0a1dfd904c5ec3f84d271682901de languageName: node linkType: hard @@ -21648,7 +21648,7 @@ __metadata: "@sentry/utils": 6.19.7 "@swc/core": 1.3.11 "@swc/helpers": 0.4.12 - "@testing-library/dom": 8.19.0 + "@testing-library/dom": 8.20.0 "@testing-library/jest-dom": 5.16.5 "@testing-library/react": 12.1.4 "@testing-library/react-hooks": 8.0.1 From bfcf936c38f12ef3daf6b908bd0687f3e89ad390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 27 Jan 2023 13:30:12 +0100 Subject: [PATCH 026/117] QueryEditorRows: Remove double callback from onDataSourceChange (#62172) * QueryEditorRows: Remove double callbacked from onDataSourceChange * A few more updates * Updated --- public/app/features/explore/QueryRows.tsx | 21 +++++++++++-------- .../query/components/QueryEditorRows.tsx | 5 ----- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/public/app/features/explore/QueryRows.tsx b/public/app/features/explore/QueryRows.tsx index 1707f5ef167..da17315b973 100644 --- a/public/app/features/explore/QueryRows.tsx +++ b/public/app/features/explore/QueryRows.tsx @@ -1,7 +1,7 @@ import { createSelector } from '@reduxjs/toolkit'; import React, { useCallback, useMemo } from 'react'; -import { CoreApp, DataSourceInstanceSettings } from '@grafana/data'; +import { CoreApp } from '@grafana/data'; import { getDataSourceSrv, reportInteraction } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; import { getNextRefIdChar } from 'app/core/utils/query'; @@ -50,9 +50,19 @@ export const QueryRows = ({ exploreId }: Props) => { }, [dispatch, exploreId]); const onChange = useCallback( - (newQueries: DataQuery[]) => { + async (newQueries: DataQuery[]) => { dispatch(changeQueriesAction({ queries: newQueries, exploreId })); + for (const newQuery of newQueries) { + for (const oldQuery of queries) { + if (newQuery.refId === oldQuery.refId && newQuery.datasource?.type !== oldQuery.datasource?.type) { + const queryDatasource = await getDataSourceSrv().get(newQuery.datasource); + const targetDS = await getDataSourceSrv().get({ uid: newQuery.datasource?.uid }); + dispatch(importQueries(exploreId, queries, queryDatasource, targetDS, newQuery.refId)); + } + } + } + // if we are removing a query we want to run the remaining ones if (newQueries.length < queries.length) { onRunQueries(); @@ -68,12 +78,6 @@ export const QueryRows = ({ exploreId }: Props) => { [onChange, queries] ); - const onMixedDataSourceChange = async (ds: DataSourceInstanceSettings, query: DataQuery) => { - const queryDatasource = await getDataSourceSrv().get(query.datasource); - const targetDS = await getDataSourceSrv().get({ uid: ds.uid }); - dispatch(importQueries(exploreId, queries, queryDatasource, targetDS, query.refId)); - }; - const onQueryCopied = () => { reportInteraction('grafana_explore_query_row_copy'); }; @@ -89,7 +93,6 @@ export const QueryRows = ({ exploreId }: Props) => { return ( onMixedDataSourceChange(ds, query)} queries={queries} onQueriesChange={onChange} onAddQuery={onAddQuery} diff --git a/public/app/features/query/components/QueryEditorRows.tsx b/public/app/features/query/components/QueryEditorRows.tsx index 05eec743cb8..2cd7579f6cf 100644 --- a/public/app/features/query/components/QueryEditorRows.tsx +++ b/public/app/features/query/components/QueryEditorRows.tsx @@ -34,7 +34,6 @@ interface Props { onQueryCopied?: () => void; onQueryRemoved?: () => void; onQueryToggled?: (queryStatus?: boolean | undefined) => void; - onDatasourceChange?: (dataSource: DataSourceInstanceSettings, query: DataQuery) => void; } export class QueryEditorRows extends PureComponent { @@ -59,10 +58,6 @@ export class QueryEditorRows extends PureComponent { onDataSourceChange(dataSource: DataSourceInstanceSettings, index: number) { const { queries, onQueriesChange } = this.props; - if (this.props.onDatasourceChange) { - this.props.onDatasourceChange(dataSource, queries[index]); - } - onQueriesChange( queries.map((item, itemIndex) => { if (itemIndex !== index) { From 846acd28ff633ce3817def64f4ce9f6b76d6e957 Mon Sep 17 00:00:00 2001 From: Victor Marin <36818606+mdvictor@users.noreply.github.com> Date: Fri, 27 Jan 2023 14:30:42 +0200 Subject: [PATCH 027/117] StateTimeline: Show correct legend label when value mappings set (#62282) * StateTimeline: Show correct legend label when value mappings set * Add test dashboard for thresholds and value mappings * run stripnulls.sh --- .../timeline-thresholds-mappings.json | 761 ++++++++++++++++++ devenv/jsonnet/dev-dashboards.libsonnet | 7 + .../core/components/TimelineChart/utils.ts | 3 +- 3 files changed, 770 insertions(+), 1 deletion(-) create mode 100644 devenv/dev-dashboards/panel-timeline/timeline-thresholds-mappings.json diff --git a/devenv/dev-dashboards/panel-timeline/timeline-thresholds-mappings.json b/devenv/dev-dashboards/panel-timeline/timeline-thresholds-mappings.json new file mode 100644 index 00000000000..be7a3567bfa --- /dev/null +++ b/devenv/dev-dashboards/panel-timeline/timeline-thresholds-mappings.json @@ -0,0 +1,761 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 1263, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "fillOpacity": 70, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 0, + "y": 0 + }, + "id": 11, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0" + } + ], + "title": "default", + "type": "state-timeline" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "fillOpacity": 70, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 10 + }, + { + "color": "#EAB839", + "value": 20 + }, + { + "color": "#6ED0E0", + "value": 30 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 6, + "y": 0 + }, + "id": 2, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "5,10,20,30,40" + } + ], + "title": "default absolute thresholds", + "type": "state-timeline" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "fillOpacity": 70, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 10 + }, + { + "color": "#EAB839", + "value": 20 + }, + { + "color": "#6ED0E0", + "value": 30 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 12, + "y": 0 + }, + "id": 8, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "5,10,20,30,40" + } + ], + "title": "default percentage thresholds", + "type": "state-timeline" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "fillOpacity": 70, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "A-series" + }, + "properties": [ + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 10 + }, + { + "color": "#EAB839", + "value": 20 + }, + { + "color": "#6ED0E0", + "value": 30 + } + ] + } + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 18, + "y": 0 + }, + "id": 3, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "5,10,20,30,40" + } + ], + "title": "override thresholds", + "type": "state-timeline" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "fillOpacity": 70, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [ + { + "options": { + "from": 0, + "result": { + "color": "green", + "index": 0 + }, + "to": 9.9999 + }, + "type": "range" + }, + { + "options": { + "from": 10, + "result": { + "color": "yellow", + "index": 1 + }, + "to": 14.9999 + }, + "type": "range" + }, + { + "options": { + "from": 15, + "result": { + "color": "red", + "index": 2 + }, + "to": 24.9999 + }, + "type": "range" + }, + { + "options": { + "from": 25, + "result": { + "color": "blue", + "index": 3 + }, + "to": 100000 + }, + "type": "range" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 0, + "y": 9 + }, + "id": 6, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "5,10,20,30,40" + } + ], + "title": "default value mappings", + "type": "state-timeline" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "fillOpacity": 70, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "A-series" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "options": { + "from": 0, + "result": { + "color": "green", + "index": 0 + }, + "to": 9.9999 + }, + "type": "range" + }, + { + "options": { + "from": 10, + "result": { + "color": "yellow", + "index": 1 + }, + "to": 14.9999 + }, + "type": "range" + }, + { + "options": { + "from": 15, + "result": { + "color": "red", + "index": 2 + }, + "to": 24.9999 + }, + "type": "range" + }, + { + "options": { + "from": 25, + "result": { + "color": "blue", + "index": 3 + }, + "to": 100000 + }, + "type": "range" + } + ] + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 6, + "y": 9 + }, + "id": 7, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "5,10,20,30,40" + } + ], + "title": "override value mappings", + "type": "state-timeline" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "fillOpacity": 70, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 12, + "y": 9 + }, + "id": 5, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"A\",\n \"fields\": [\n {\n \"name\": \"time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"int64\",\n \"nullable\": true\n },\n \"config\": {\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"#EAB839\",\n \"value\": 10\n },\n {\n \"color\": \"red\",\n \"value\": 15\n },\n {\n \"color\": \"#6ED0E0\",\n \"value\": 25\n }\n ]\n }\n }\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1674732835000,\n 1674736435000,\n 1674740035000,\n 1674743635000\n ],\n [\n 5,\n 10,\n 20,\n 30\n ]\n ]\n }\n }\n]", + "refId": "A", + "scenarioId": "raw_frame" + } + ], + "title": "field thresholds from data", + "type": "state-timeline" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "fillOpacity": 70, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 18, + "y": 9 + }, + "id": 9, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "5,10,20,30,40" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "hide": false, + "max": 30, + "min": 0.01, + "noise": 30, + "refId": "B", + "scenarioId": "random_walk", + "startValue": 1 + } + ], + "title": "threshold from random walk", + "transformations": [ + { + "id": "configFromData", + "options": { + "configRefId": "B", + "mappings": [ + { + "fieldName": "B-series", + "handlerKey": "threshold1" + } + ] + } + } + ], + "type": "state-timeline" + } + ], + "refresh": false, + "revision": 1, + "schemaVersion": 38, + "style": "dark", + "tags": [ + "gdev", + "panel-tests", + "state-timeline", + "graph-ng" + ], + "templating": { + "list": [] + }, + "time": { + "from": "2023-01-26T11:33:55.000Z", + "to": "2023-01-26T14:33:55.000Z" + }, + "timepicker": {}, + "timezone": "", + "title": "StateTimeline - Thresholds & Mappings", + "uid": "Kce7z9TVz", + "version": 14, + "weekStart": "" +} diff --git a/devenv/jsonnet/dev-dashboards.libsonnet b/devenv/jsonnet/dev-dashboards.libsonnet index 60a5faa1694..a8ec282659e 100644 --- a/devenv/jsonnet/dev-dashboards.libsonnet +++ b/devenv/jsonnet/dev-dashboards.libsonnet @@ -632,6 +632,13 @@ local dashboard = grafana.dashboard; id: 0, } }, + dashboard.new('timeline-thresholds-mappings', import '../dev-dashboards/panel-timeline/timeline-thresholds-mappings.json') + + resource.addMetadata('folder', 'dev-dashboards') + + { + spec+: { + id: 0, + } + }, dashboard.new('timeseries', import '../dev-dashboards/panel-timeseries/timeseries.json') + resource.addMetadata('folder', 'dev-dashboards') + { diff --git a/public/app/core/components/TimelineChart/utils.ts b/public/app/core/components/TimelineChart/utils.ts index 7351ca00f00..d13700390e6 100644 --- a/public/app/core/components/TimelineChart/utils.ts +++ b/public/app/core/components/TimelineChart/utils.ts @@ -556,9 +556,10 @@ export function getFieldLegendItem(fields: Field[], theme: GrafanaTheme2): VizLe const items: VizLegendItem[] = []; const fieldConfig = fields[0].config; const colorMode = fieldConfig.color?.mode ?? FieldColorModeId.Fixed; + const thresholds = fieldConfig.thresholds; // If thresholds are enabled show each step in the legend - if (colorMode === FieldColorModeId.Thresholds) { + if (colorMode === FieldColorModeId.Thresholds && thresholds?.steps && thresholds.steps.length > 1) { return getThresholdItems(fieldConfig, theme); } From b6db1ed5242cc0a573ad482160d43bf7e9aa0d38 Mon Sep 17 00:00:00 2001 From: George Robinson Date: Fri, 27 Jan 2023 12:41:36 +0000 Subject: [PATCH 028/117] Revert "Alerting: Add is_paused attr to the POST alert rule group endpoint" (#62310) Revert "Alerting: Add is_paused attr to the POST alert rule group endpoint (#62253)" This reverts commit 3ccafe3a5af8fc2a1539dad16277f210b4de7219. --- pkg/services/ngalert/api/api_ruler_validation.go | 1 - pkg/services/ngalert/api/tooling/api.json | 4 +--- .../ngalert/api/tooling/definitions/cortex-ruler.go | 1 - pkg/services/ngalert/api/tooling/post.json | 8 ++------ pkg/services/ngalert/api/tooling/spec.json | 9 +++------ public/api-merged.json | 4 +--- 6 files changed, 7 insertions(+), 20 deletions(-) diff --git a/pkg/services/ngalert/api/api_ruler_validation.go b/pkg/services/ngalert/api/api_ruler_validation.go index 60c01732631..a8f86eee6ab 100644 --- a/pkg/services/ngalert/api/api_ruler_validation.go +++ b/pkg/services/ngalert/api/api_ruler_validation.go @@ -97,7 +97,6 @@ func validateRuleNode( RuleGroup: groupName, NoDataState: noDataState, ExecErrState: errorState, - IsPaused: ruleNode.GrafanaManagedAlert.IsPaused, } newAlertRule.For, err = validateForInterval(ruleNode) diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index 41a5eb5e903..86cd1cc9299 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -2005,9 +2005,6 @@ ], "type": "string" }, - "is_paused": { - "type": "boolean" - }, "no_data_state": { "enum": [ "Alerting", @@ -3498,6 +3495,7 @@ "type": "object" }, "gettableAlert": { + "description": "GettableAlert gettable alert", "properties": { "annotations": { "$ref": "#/definitions/labelSet" diff --git a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go index f93e9d4cc3b..4acab8ab44d 100644 --- a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go +++ b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go @@ -374,7 +374,6 @@ type PostableGrafanaRule struct { UID string `json:"uid" yaml:"uid"` NoDataState NoDataState `json:"no_data_state" yaml:"no_data_state"` ExecErrState ExecutionErrorState `json:"exec_err_state" yaml:"exec_err_state"` - IsPaused bool `json:"is_paused" yaml:"is_paused"` } // swagger:model diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index 0604354ec23..ce003de9465 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -2005,9 +2005,6 @@ ], "type": "string" }, - "is_paused": { - "type": "boolean" - }, "no_data_state": { "enum": [ "Alerting", @@ -3158,7 +3155,6 @@ "type": "object" }, "URL": { - "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.", "properties": { "ForceQuery": { "type": "boolean" @@ -3194,7 +3190,7 @@ "$ref": "#/definitions/Userinfo" } }, - "title": "A URL represents a parsed URL (technically, a URI reference).", + "title": "URL is a custom URL type that allows validation at configuration load time.", "type": "object" }, "Userinfo": { @@ -3500,6 +3496,7 @@ "type": "object" }, "gettableAlert": { + "description": "GettableAlert gettable alert", "properties": { "annotations": { "$ref": "#/definitions/labelSet" @@ -3609,7 +3606,6 @@ "type": "object" }, "gettableSilences": { - "description": "GettableSilences gettable silences", "items": { "$ref": "#/definitions/gettableSilence" }, diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index 4e0e3f16423..ce7a76b2117 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -4501,9 +4501,6 @@ "Error" ] }, - "is_paused": { - "type": "boolean" - }, "no_data_state": { "type": "string", "enum": [ @@ -5890,6 +5887,7 @@ "$ref": "#/definitions/alertGroup" }, "alertGroups": { + "description": "AlertGroups alert groups", "type": "array", "items": { "$ref": "#/definitions/alertGroup" @@ -5995,6 +5993,7 @@ } }, "gettableAlert": { + "description": "GettableAlert gettable alert", "type": "object", "required": [ "labels", @@ -6051,7 +6050,6 @@ "$ref": "#/definitions/gettableAlert" }, "gettableAlerts": { - "description": "GettableAlerts gettable alerts", "type": "array", "items": { "$ref": "#/definitions/gettableAlert" @@ -6059,7 +6057,6 @@ "$ref": "#/definitions/gettableAlerts" }, "gettableSilence": { - "description": "GettableSilence gettable silence", "type": "object", "required": [ "comment", @@ -6109,7 +6106,6 @@ "$ref": "#/definitions/gettableSilence" }, "gettableSilences": { - "description": "GettableSilences gettable silences", "type": "array", "items": { "$ref": "#/definitions/gettableSilence" @@ -6300,6 +6296,7 @@ "$ref": "#/definitions/postableSilence" }, "receiver": { + "description": "Receiver receiver", "type": "object", "required": [ "active", diff --git a/public/api-merged.json b/public/api-merged.json index 9192ca1c1c3..8f18508d55e 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -15561,9 +15561,6 @@ "Error" ] }, - "is_paused": { - "type": "boolean" - }, "no_data_state": { "type": "string", "enum": [ @@ -18799,6 +18796,7 @@ } }, "gettableAlert": { + "description": "GettableAlert gettable alert", "type": "object", "required": [ "labels", From 6bfd21ef0a8bf2f1ee1e9d193c3c375f64a05901 Mon Sep 17 00:00:00 2001 From: Oscar Kilhed Date: Fri, 27 Jan 2023 13:42:47 +0100 Subject: [PATCH 029/117] FileDropzone: make a nicer looking error message when file size is exceeded (#62290) * FileDropzone: make a nicer looking error message when file size is exceeded --- .../FileDropzone/FileDropzone.test.tsx | 8 ++++ .../components/FileDropzone/FileDropzone.tsx | 41 +++++++++++++------ 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.test.tsx b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.test.tsx index 0389d7ffd2a..39dd8a23361 100644 --- a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.test.tsx +++ b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.test.tsx @@ -33,6 +33,14 @@ describe('The FileDropzone component', () => { expect(screen.getByText('Accepted file type: .json')).toBeInTheDocument(); }); + it('should show an error message when the file size exceeds the max file size', async () => { + render(); + + dispatchEvt(screen.getByTestId('dropzone'), 'drop', mockData(files)); + + expect(await screen.findByText('File is larger than 1 B')).toBeInTheDocument(); + }); + it('should show the accepted file type(s) when passed in as a array of strings', () => { render(); diff --git a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx index 79ad4c2f92b..08acef7fc28 100644 --- a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx +++ b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx @@ -1,9 +1,9 @@ import { css, cx } from '@emotion/css'; import { isString, uniqueId } from 'lodash'; import React, { ReactNode, useCallback, useState } from 'react'; -import { Accept, DropEvent, DropzoneOptions, FileRejection, useDropzone } from 'react-dropzone'; +import { Accept, DropEvent, DropzoneOptions, FileError, FileRejection, useDropzone, ErrorCode } from 'react-dropzone'; -import { GrafanaTheme2 } from '@grafana/data'; +import { formattedValueToString, getValueFormat, GrafanaTheme2 } from '@grafana/data'; import { useTheme2 } from '../../themes'; import { Alert } from '../Alert/Alert'; @@ -68,7 +68,7 @@ export function FileDropzone({ onFileRemove, }: FileDropzoneProps) { const [files, setFiles] = useState([]); - const [errorMessages, setErrorMessages] = useState([]); + const [fileErrors, setErrorMessages] = useState([]); const setFileProperty = useCallback( (customFile: DropzoneFile, action: (customFileToModify: DropzoneFile) => void) => { @@ -175,11 +175,15 @@ export function FileDropzone({ }); const setErrors = (rejectedFiles: FileRejection[]) => { - let errors: string[] = []; + let errors: FileError[] = []; rejectedFiles.map((rejectedFile) => { - rejectedFile.errors.map((error) => { - if (errors.indexOf(error.message) === -1) { - errors.push(error.message); + rejectedFile.errors.map((newError) => { + if ( + errors.findIndex((presentError) => { + return presentError.code === newError.code && presentError.message === newError.message; + }) === -1 + ) { + errors.push(newError); } }); }); @@ -187,12 +191,22 @@ export function FileDropzone({ setErrorMessages(errors); }; - const getErrorMessages = () => { + const renderErrorMessages = (errors: FileError[]) => { return (
- {errorMessages.map((error) => { - return
{error}
; + {errors.map((error) => { + switch (error.code) { + case ErrorCode.FileTooLarge: + const formattedSize = getValueFormat('decbytes')(options?.maxSize!); + return ( +
+ File is larger than {formattedValueToString(formattedSize)} +
+ ); + default: + return
{error.message}
; + } })}
@@ -209,7 +223,7 @@ export function FileDropzone({ {children ?? }
- {errorMessages.length > 0 && getErrorMessages()} + {fileErrors.length > 0 && renderErrorMessages(fileErrors)} {options?.accept && ( {getAcceptedFileTypeText(options.accept)} )} @@ -261,11 +275,12 @@ export function FileDropzoneDefaultChildren({ ); } -function getPrimaryText(files?: DropzoneFile[], options?: BackwardsCompatibleDropzoneOptions) { + +function getPrimaryText(files: DropzoneFile[], options?: BackwardsCompatibleDropzoneOptions) { if (options?.multiple === undefined || options?.multiple) { return 'Upload file'; } - return files && files.length ? 'Replace file' : 'Upload file'; + return files.length ? 'Replace file' : 'Upload file'; } function getAcceptedFileTypeText(accept: string | string[] | Accept) { From 52955d88a73a3164bbbc4bd563ec610fcc43dc0c Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Fri, 27 Jan 2023 06:52:01 -0600 Subject: [PATCH 030/117] TimeSeries: Fix log y scale when min/max settings don't land on divisors (#60768) --- .../components/uPlot/config/UPlotScaleBuilder.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotScaleBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotScaleBuilder.ts index cbdfdbd4539..2011ad0d147 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotScaleBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotScaleBuilder.ts @@ -116,7 +116,7 @@ export class UPlotScaleBuilder extends PlotConfigBuilder { minMax = uPlot.rangeNum(hardMinOnly ? hardMin : dataMin, hardMaxOnly ? hardMax : dataMax, rangeConfig); } } else if (scale.distr === 3) { - minMax = uPlot.rangeLog(dataMin!, dataMax!, logBase, true); + minMax = uPlot.rangeLog(hardMin ?? dataMin!, hardMax ?? dataMax!, logBase, true); } if (decimals === 0) { @@ -154,13 +154,15 @@ export class UPlotScaleBuilder extends PlotConfigBuilder { } } - // if all we got were hard limits, treat them as static min/max - if (hardMinOnly) { - minMax[0] = hardMin!; - } + if (scale.distr === 1) { + // if all we got were hard limits, treat them as static min/max + if (hardMinOnly) { + minMax[0] = hardMin!; + } - if (hardMaxOnly) { - minMax[1] = hardMax!; + if (hardMaxOnly) { + minMax[1] = hardMax!; + } } // guard against invalid y ranges From 2f46317a2aa89cc331fcc7d554327130a8e71dfe Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Fri, 27 Jan 2023 14:06:43 +0100 Subject: [PATCH 031/117] Packages: Don't error if npm-artifacts directory already exists (#62303) fix(packages): dont error if npm-artifacts directory already exists --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b22ef26cf72..b111f35b3b2 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "packages:build": "lerna run build --ignore @grafana-plugins/input-datasource", "packages:clean": "rimraf ./npm-artifacts && lerna run clean --parallel", "packages:prepare": "lerna version --no-push --no-git-tag-version --force-publish --exact", - "packages:pack": "mkdir ./npm-artifacts && lerna exec --no-private -- yarn pack --out \"../../npm-artifacts/%s-%v.tgz\"", + "packages:pack": "mkdir -p ./npm-artifacts && lerna exec --no-private -- yarn pack --out \"../../npm-artifacts/%s-%v.tgz\"", "packages:publish": "lerna exec --no-private -- npm publish package.tgz", "packages:publishCanary": "lerna exec --no-private -- npm publish package.tgz --tag canary", "packages:publishLatest": "lerna exec --no-private -- npm publish package.tgz", From 3281eb92230021218ab35dc3e2f12f9e2c4e7cb0 Mon Sep 17 00:00:00 2001 From: Yaelle Chaudy <42030685+yaelleC@users.noreply.github.com> Date: Fri, 27 Jan 2023 14:11:17 +0100 Subject: [PATCH 032/117] [Navigation] Add user events for quick actions/dashboard actions (#62220) Add fe events for quick actions/dashboard actions --- .../AppChrome/QuickAdd/QuickAdd.test.tsx | 21 ++++++++++++++- .../AppChrome/QuickAdd/QuickAdd.tsx | 8 +++++- .../search/components/DashboardActions.tsx | 26 ++++++++++++++++--- 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/public/app/core/components/AppChrome/QuickAdd/QuickAdd.test.tsx b/public/app/core/components/AppChrome/QuickAdd/QuickAdd.test.tsx index 3aba09ac311..568ec4af0d8 100644 --- a/public/app/core/components/AppChrome/QuickAdd/QuickAdd.test.tsx +++ b/public/app/core/components/AppChrome/QuickAdd/QuickAdd.test.tsx @@ -4,10 +4,18 @@ import React from 'react'; import { Provider } from 'react-redux'; import { NavModelItem, NavSection } from '@grafana/data'; +import { reportInteraction } from '@grafana/runtime'; import { configureStore } from 'app/store/configureStore'; import { QuickAdd } from './QuickAdd'; +jest.mock('@grafana/runtime', () => { + return { + ...jest.requireActual('@grafana/runtime'), + reportInteraction: jest.fn(), + }; +}); + const setup = () => { const navBarTree: NavModelItem[] = [ { @@ -16,7 +24,7 @@ const setup = () => { id: 'section1', url: 'section1', children: [ - { text: 'New child 1', id: 'child1', url: 'section1/child1', isCreateAction: true }, + { text: 'New child 1', id: 'child1', url: '#', isCreateAction: true }, { text: 'Child2', id: 'child2', url: 'section1/child2' }, ], }, @@ -50,4 +58,15 @@ describe('QuickAdd', () => { expect(screen.getByRole('link', { name: 'New child 1' })).toBeInTheDocument(); expect(screen.getByRole('link', { name: 'New child 3' })).toBeInTheDocument(); }); + + it('reports interaction when a menu item is clicked', async () => { + setup(); + await userEvent.click(screen.getByRole('button', { name: 'New' })); + await userEvent.click(screen.getByRole('link', { name: 'New child 1' })); + + expect(reportInteraction).toHaveBeenCalledWith('grafana_menu_item_clicked', { + url: '#', + from: 'quickadd', + }); + }); }); diff --git a/public/app/core/components/AppChrome/QuickAdd/QuickAdd.tsx b/public/app/core/components/AppChrome/QuickAdd/QuickAdd.tsx index 0ed2337c7ff..ac38c048f0b 100644 --- a/public/app/core/components/AppChrome/QuickAdd/QuickAdd.tsx +++ b/public/app/core/components/AppChrome/QuickAdd/QuickAdd.tsx @@ -2,6 +2,7 @@ import { css } from '@emotion/css'; import React, { useMemo, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; +import { reportInteraction } from '@grafana/runtime'; import { Menu, Dropdown, useStyles2, useTheme2, ToolbarButton } from '@grafana/ui'; import { useMediaQueryChange } from 'app/core/hooks/useMediaQueryChange'; import { useSelector } from 'app/types'; @@ -32,7 +33,12 @@ export const QuickAdd = ({}: Props) => { return ( {createActions.map((createAction, index) => ( - + reportInteraction('grafana_menu_item_clicked', { url: createAction.url, from: 'quickadd' })} + /> ))} ); diff --git a/public/app/features/search/components/DashboardActions.tsx b/public/app/features/search/components/DashboardActions.tsx index 6ba55030d23..1828c2154bf 100644 --- a/public/app/features/search/components/DashboardActions.tsx +++ b/public/app/features/search/components/DashboardActions.tsx @@ -1,6 +1,6 @@ import React, { FC } from 'react'; -import { config } from '@grafana/runtime'; +import { config, reportInteraction } from '@grafana/runtime'; import { Menu, Dropdown, Button, Icon } from '@grafana/ui'; import { t } from 'app/core/internationalization'; @@ -30,13 +30,31 @@ export const DashboardActions: FC = ({ folderUid, canCreateFolders = fals return ( {canCreateDashboards && ( - + + reportInteraction('grafana_menu_item_clicked', { url: actionUrl('new'), from: '/dashboards' }) + } + /> )} {canCreateFolders && (config.featureToggles.nestedFolders || !folderUid) && ( - + + reportInteraction('grafana_menu_item_clicked', { url: actionUrl('new_folder'), from: '/dashboards' }) + } + /> )} {canCreateDashboards && ( - + + reportInteraction('grafana_menu_item_clicked', { url: actionUrl('import'), from: '/dashboards' }) + } + /> )} ); From 54ff88463c8e543ed25997ec98ed60537e94e0bf Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Fri, 27 Jan 2023 14:21:47 +0100 Subject: [PATCH 033/117] Alerting: remove link to Grafana University (#62318) --- public/app/features/alerting/unified/Home.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/public/app/features/alerting/unified/Home.tsx b/public/app/features/alerting/unified/Home.tsx index f8e094ebbe9..ce1052853e1 100644 --- a/public/app/features/alerting/unified/Home.tsx +++ b/public/app/features/alerting/unified/Home.tsx @@ -55,10 +55,6 @@ export default function Home() {
-
From f0a88e0609a31ea2ca949e53dfbce54315e0608a Mon Sep 17 00:00:00 2001 From: Armand Grillet <2117580+armandgrillet@users.noreply.github.com> Date: Fri, 27 Jan 2023 14:22:47 +0100 Subject: [PATCH 034/117] Add SQLite performance limitation with alerting (#62296) --- docs/sources/alerting/performance-limitations/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/alerting/performance-limitations/index.md b/docs/sources/alerting/performance-limitations/index.md index e108dd1ce0c..f8e17c8715d 100644 --- a/docs/sources/alerting/performance-limitations/index.md +++ b/docs/sources/alerting/performance-limitations/index.md @@ -23,7 +23,7 @@ The following section provides a list of alerting performance considerations. - Cardinality of the rule's result set. For example, suppose you are monitoring API response errors for every API path, on every VM in your fleet. This set has a cardinality of _n_ number of paths multiplied by _v_ number of VMs. You can reduce the cardinality of a result set - perhaps by monitoring errors-per-VM instead of for each path per VM. - Complexity of the alerting query consideration. Queries that data sources can process and respond to quickly consume fewer resources. Although this consideration is less important than the other considerations listed above, if you have reduced those as much as possible, looking at individual query performance could make a difference. -Each evaluation of an alert rule generates a set of alert instances; one for each member of the result set. The state of all the instances is written to the `alert_instance` table in Grafana's SQL database. +Each evaluation of an alert rule generates a set of alert instances; one for each member of the result set. The state of all the instances is written to the `alert_instance` table in Grafana's SQL database. This number of write-heavy operations can cause issues when using SQLite. Grafana Alerting exposes a metric, `grafana_alerting_rule_evaluations_total` that counts the number of alert rule evaluations. To get a feel for the influence of rule evaluations on your Grafana instance, you can observe the rate of evaluations and compare it with resource consumption. In a Prometheus-compatible database, you can use the query `rate(grafana_alerting_rule_evaluations_total[5m])` to compute the rate over 5 minute windows of time. It's important to remember that this isn't the full picture of rule evaluation. For example, the load will be unevenly distributed if you have some rules that evaluate every 10 seconds, and others every 30 minutes. From 847a5ab195305ad952a76c6125758c5fd8dd7f15 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Jan 2023 13:23:31 +0000 Subject: [PATCH 035/117] Update dependency rc-slider to v10.1.0 (#62302) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-ui/package.json | 2 +- yarn.lock | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index b111f35b3b2..1b21000680c 100644 --- a/package.json +++ b/package.json @@ -361,7 +361,7 @@ "pseudoizer": "^0.1.0", "rc-cascader": "3.8.0", "rc-drawer": "6.1.2", - "rc-slider": "10.0.1", + "rc-slider": "10.1.0", "rc-time-picker": "3.7.3", "rc-tree": "5.7.0", "re-resizable": "6.9.9", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 1e8fae1a11b..b03d75f6992 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -82,7 +82,7 @@ "prismjs": "1.29.0", "rc-cascader": "3.8.0", "rc-drawer": "6.1.2", - "rc-slider": "10.0.1", + "rc-slider": "10.1.0", "rc-time-picker": "^3.7.3", "rc-tooltip": "5.2.2", "react-beautiful-dnd": "13.1.1", diff --git a/yarn.lock b/yarn.lock index 33e29967e36..f6790269727 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5129,7 +5129,7 @@ __metadata: process: ^0.11.10 rc-cascader: 3.8.0 rc-drawer: 6.1.2 - rc-slider: 10.0.1 + rc-slider: 10.1.0 rc-time-picker: ^3.7.3 rc-tooltip: 5.2.2 react: 17.0.2 @@ -21823,7 +21823,7 @@ __metadata: pseudoizer: ^0.1.0 rc-cascader: 3.8.0 rc-drawer: 6.1.2 - rc-slider: 10.0.1 + rc-slider: 10.1.0 rc-time-picker: 3.7.3 rc-tree: 5.7.0 re-resizable: 6.9.9 @@ -31878,9 +31878,9 @@ __metadata: languageName: node linkType: hard -"rc-slider@npm:10.0.1": - version: 10.0.1 - resolution: "rc-slider@npm:10.0.1" +"rc-slider@npm:10.1.0": + version: 10.1.0 + resolution: "rc-slider@npm:10.1.0" dependencies: "@babel/runtime": ^7.10.1 classnames: ^2.2.5 @@ -31889,7 +31889,7 @@ __metadata: peerDependencies: react: ">=16.9.0" react-dom: ">=16.9.0" - checksum: 803f0cc39d43897c0b24549e87232a668d26ff5b0e14b528fd454aa455cdf96ebc60654832c51bb1a6c7b7594ca39017d6c96b3237662471efb863f1723e3d9c + checksum: 002662cd0a59d6e48dd82744dfe2043efffd15229fb39665001bd73d584570458c16c0babcc78dc7b0ccde57d83f86114492348b3e5674a0672c97269957319d languageName: node linkType: hard From 27f0c9c70f8dfe1e4ffb6031e4a9569dff347bb2 Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Fri, 27 Jan 2023 14:26:35 +0100 Subject: [PATCH 036/117] Auth: Doc change url for getting JWT (#62319) docs: change url for getting JWT --- devenv/docker/blocks/auth/oauth/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devenv/docker/blocks/auth/oauth/readme.md b/devenv/docker/blocks/auth/oauth/readme.md index 60c87d8d508..a935f2c8682 100644 --- a/devenv/docker/blocks/auth/oauth/readme.md +++ b/devenv/docker/blocks/auth/oauth/readme.md @@ -59,7 +59,7 @@ You can obtain a jwt token by using the following command for oauth-admin: ```sh curl --request POST \ - --url http://localhost:8087/auth/realms/grafana/protocol/openid-connect/token \ + --url http://localhost:8087/realms/grafana/protocol/openid-connect/token \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data client_id=grafana-oauth \ --data grant_type=password \ From 591501ef3f8991b9929f607954aca7fc30ca1cda Mon Sep 17 00:00:00 2001 From: Joey Tawadrous <90795735+joey-grafana@users.noreply.github.com> Date: Fri, 27 Jan 2023 13:33:27 +0000 Subject: [PATCH 037/117] Traces: Upgraded feature tracking (#62181) Upgraded feature tracking --- .../TracePageHeader/TracePageSearchBar.tsx | 4 +++- .../TraceTimelineViewer/SpanDetail/index.tsx | 3 ++- .../src/TraceTimelineViewer/SpanLinks.tsx | 5 ++++- .../VirtualizedTraceView.tsx | 3 ++- .../src/TraceTimelineViewer/index.tsx | 6 ++++- .../features/explore/NodeGraphContainer.tsx | 3 ++- .../app/features/inspector/InspectDataTab.tsx | 21 ++++++++++++++++-- .../plugins/datasource/tempo/CheatSheet.tsx | 3 ++- .../tempo/QueryEditor/QueryField.tsx | 3 ++- .../plugins/datasource/tempo/datasource.ts | 22 ++++++++++++++----- 10 files changed, 57 insertions(+), 16 deletions(-) diff --git a/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.tsx b/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.tsx index 186e0d2b074..7f6c96ec4fa 100644 --- a/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.tsx +++ b/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.tsx @@ -17,7 +17,7 @@ import cx from 'classnames'; import React, { memo, Dispatch, SetStateAction } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { reportInteraction } from '@grafana/runtime'; +import { config, reportInteraction } from '@grafana/runtime'; import { Button, useStyles2 } from '@grafana/ui'; import UiFindInput from '../common/UiFindInput'; @@ -117,6 +117,7 @@ export default memo(function TracePageSearchBar(props: TracePageSearchBarProps) const nextResult = () => { reportInteraction('grafana_traces_trace_view_find_next_prev_clicked', { datasourceType: datasourceType, + grafana_version: config.buildInfo.version, direction: 'next', }); @@ -140,6 +141,7 @@ export default memo(function TracePageSearchBar(props: TracePageSearchBarProps) const prevResult = () => { reportInteraction('grafana_traces_trace_view_find_next_prev_clicked', { datasourceType: datasourceType, + grafana_version: config.buildInfo.version, direction: 'prev', }); diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.tsx index 7cfee033a13..7842adf0929 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.tsx @@ -18,7 +18,7 @@ import React from 'react'; import IoLink from 'react-icons/lib/io/link'; import { dateTimeFormat, GrafanaTheme2, LinkModel, TimeZone } from '@grafana/data'; -import { reportInteraction } from '@grafana/runtime'; +import { config, reportInteraction } from '@grafana/runtime'; import { Button, DataLinkButton, TextArea, useStyles2 } from '@grafana/ui'; import { autoColor } from '../../Theme'; @@ -209,6 +209,7 @@ export default function SpanDetail(props: SpanDetailProps) { onClick: (event: React.MouseEvent) => { reportInteraction('grafana_traces_trace_view_span_link_clicked', { datasourceType: datasourceType, + grafana_version: config.buildInfo.version, type: 'log', location: 'spanDetails', }); diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanLinks.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanLinks.tsx index 0380157ce7e..666470f1242 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanLinks.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanLinks.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/css'; import React, { useState } from 'react'; -import { reportInteraction } from '@grafana/runtime'; +import { config, reportInteraction } from '@grafana/runtime'; import { useStyles2, MenuGroup, MenuItem, Icon, ContextMenu } from '@grafana/ui'; import { SpanLinks } from '../types/links'; @@ -30,6 +30,7 @@ const renderMenuItems = ( ? (event) => { reportInteraction('grafana_traces_trace_view_span_link_clicked', { datasourceType: datasourceType, + grafana_version: config.buildInfo.version, type: 'log', location: 'menu', }); @@ -56,6 +57,7 @@ const renderMenuItems = ( ? (event) => { reportInteraction('grafana_traces_trace_view_span_link_clicked', { datasourceType: datasourceType, + grafana_version: config.buildInfo.version, type: 'metric', location: 'menu', }); @@ -82,6 +84,7 @@ const renderMenuItems = ( ? (event) => { reportInteraction('grafana_traces_trace_view_span_link_clicked', { datasourceType: datasourceType, + grafana_version: config.buildInfo.version, type: 'trace', location: 'menu', }); diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx index b789298dae8..e98caf6460a 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx @@ -19,7 +19,7 @@ import * as React from 'react'; import { createRef, RefObject } from 'react'; import { GrafanaTheme2, LinkModel, TimeZone } from '@grafana/data'; -import { reportInteraction } from '@grafana/runtime'; +import { config, reportInteraction } from '@grafana/runtime'; import { stylesFactory, withTheme2, ToolbarButton } from '@grafana/ui'; import { Accessors } from '../ScrollManager'; @@ -541,6 +541,7 @@ export class UnthemedVirtualizedTraceView extends React.Component { }; exportTracesAsJson = () => { - const { data, panel } = this.props; + const { data, panel, app } = this.props; + if (!data) { return; } @@ -123,16 +124,19 @@ export class InspectDataTab extends PureComponent { if (df.meta?.preferredVisualisationType !== 'trace') { continue; } + let traceFormat = 'otlp'; switch (df.meta?.custom?.traceFormat) { case 'jaeger': { let res = transformToJaeger(new MutableDataFrame(df)); downloadAsJson(res, (panel ? panel.getDisplayTitle() : 'Explore') + '-traces'); + traceFormat = 'jaeger'; break; } case 'zipkin': { let res = transformToZipkin(new MutableDataFrame(df)); downloadAsJson(res, (panel ? panel.getDisplayTitle() : 'Explore') + '-traces'); + traceFormat = 'zipkin'; break; } case 'otlp': @@ -142,11 +146,24 @@ export class InspectDataTab extends PureComponent { break; } } + + reportInteraction('grafana_traces_download_traces_clicked', { + app, + grafana_version: config.buildInfo.version, + trace_format: traceFormat, + location: 'inspector', + }); } }; exportServiceGraph = () => { - const { data, panel } = this.props; + const { data, panel, app } = this.props; + reportInteraction('grafana_traces_download_service_graph_clicked', { + app, + grafana_version: config.buildInfo.version, + location: 'inspector', + }); + if (!data) { return; } diff --git a/public/app/plugins/datasource/tempo/CheatSheet.tsx b/public/app/plugins/datasource/tempo/CheatSheet.tsx index 2095a13d4a1..40afaf9cfff 100644 --- a/public/app/plugins/datasource/tempo/CheatSheet.tsx +++ b/public/app/plugins/datasource/tempo/CheatSheet.tsx @@ -1,10 +1,11 @@ import React from 'react'; -import { reportInteraction } from '@grafana/runtime'; +import { config, reportInteraction } from '@grafana/runtime'; export default function CheatSheet() { reportInteraction('grafana_traces_cheatsheet_clicked', { datasourceType: 'tempo', + grafana_version: config.buildInfo.version, }); return ( diff --git a/public/app/plugins/datasource/tempo/QueryEditor/QueryField.tsx b/public/app/plugins/datasource/tempo/QueryEditor/QueryField.tsx index 1648378760e..5597d05e5a5 100644 --- a/public/app/plugins/datasource/tempo/QueryEditor/QueryField.tsx +++ b/public/app/plugins/datasource/tempo/QueryEditor/QueryField.tsx @@ -3,7 +3,7 @@ import React from 'react'; import useAsync from 'react-use/lib/useAsync'; import { QueryEditorProps, SelectableValue } from '@grafana/data'; -import { reportInteraction } from '@grafana/runtime'; +import { config, reportInteraction } from '@grafana/runtime'; import { FileDropzone, InlineField, @@ -107,6 +107,7 @@ class TempoQueryFieldComponent extends React.PureComponent { reportInteraction('grafana_traces_query_type_changed', { datasourceType: 'tempo', app: app ?? '', + grafana_version: config.buildInfo.version, newQueryType: v, previousQueryType: query.queryType ?? '', }); diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts index 4bad7bb23ec..f79c9ed3fa1 100644 --- a/public/app/plugins/datasource/tempo/datasource.ts +++ b/public/app/plugins/datasource/tempo/datasource.ts @@ -108,7 +108,9 @@ export class TempoDatasource extends DataSourceWithBackend, datasourceUid: s reportInteraction('grafana_traces_service_graph_size', { datasourceType: 'tempo', + grafana_version: config.buildInfo.version, nodeLength, edgeLength, }); From c931b8031e28092764b288a5183ba4c68970fc2c Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Fri, 27 Jan 2023 14:40:04 +0100 Subject: [PATCH 038/117] SearchV2: Set correct batch limit when loading dashboards (#62314) SearchV2: Set correct limit --- pkg/services/searchV2/index.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/services/searchV2/index.go b/pkg/services/searchV2/index.go index 2ea2540aa47..0c15395cd55 100644 --- a/pkg/services/searchV2/index.go +++ b/pkg/services/searchV2/index.go @@ -901,7 +901,8 @@ func (l sqlDashboardLoader) LoadDashboards(ctx context.Context, orgID int64, das limit := 1 if dashboardUID == "" { - dashboards = make([]dashboard, 0, l.settings.DashboardLoadingBatchSize) + limit = l.settings.DashboardLoadingBatchSize + dashboards = make([]dashboard, 0, limit) } loadDatasourceCtx, loadDatasourceSpan := l.tracer.Start(ctx, "sqlDashboardLoader LoadDatasourceLookup") From af1e2d68dab719539f0e2758b79ad275a41480c6 Mon Sep 17 00:00:00 2001 From: Giuseppe Guerra Date: Fri, 27 Jan 2023 15:08:17 +0100 Subject: [PATCH 039/117] Plugins: Allow loading panel plugins from a CDN (#59096) * POC: Plugins CDN reverse proxy * CDN proxy POC: changed env var names * Add authorization: false for /public path in frontend plugin loader * Moved CDN settings to Cfg, add some comments * Fix error 500 in asset fetch if plugin is not using CDN * Fix EnterpriseLicensePath declared twice * Fix linter complaining about whitespaces * Plugins CDN: Skip signature verification for CDN plugins * Plugins CDN: Skip manifest and signature check for cdn plugins * Plugins: use IsValid() and IsInternal() rather than equality checks * Plugins CDN: remove comment * Plugins CDN: Fix seeker can't seek when serving plugins from local fs * Plugins CDN: add back error codes in getLocalPluginAssets * Plugins CDN: call asset.Close() rather than asset.readSeekCloser.Close() * Plugins CDN: Fix panic in JsonApiErr when errorMessageCoder wraps a nil error * Plugins CDN: Add error handling to proxyCDNPluginAsset * Plugins CDN: replace errorMessageCoder with errutil * Plugins CDN POC: expose cdn plugin paths to frontend for system.js * Plugins CDN: Fix cdn plugins showing as unsigned in frontend * WIP: Add support for formatted URL * Fix missing cdnPluginsBaseURLs in GrafanaConfig * Plugins CDN: Remove reverse proxy mode and reverse proxy references * Plugins CDN: Simplify asset serving logic * Plugins CDN: sanitize redirect path * Plugins CDN: Removed unused pluginAsset type * Plugins CDN: Removed system.js changes * Plugins CDN: Return different system.js baseURL and module for cdn plugins * Plugins CDN: Ensure CDN is disabled for non-external plugins * lint * Plugins CDN: serve images and screenshots from CDN, refactoring * Lint * Plugins CDN: Fix URLs for system.js (baseUrl and module) * Plugins CDN: Add more tests for RelativeURLForSystemJS * Plugins CDN: Iterate only on apps when preloading * Plugins CDN: Refactoring * Plugins CDN: Add comments to url_constructor.go * Plugins CDN: Update defaultHGPluginsCDNBaseURL * Plugins CDN: undo extract meta from system js config * refactor(plugins): migrate systemjs css plugin to typescript * feat(plugins): introduce systemjs cdn loader plugin * feat(plugins): add systemjs load type * Plugins CDN: Removed RelativeURLForSystemJS * Plugins CDN: Log backend redirect hits along with plugin info * Plugins CDN: Add pluginsCDNBasePath to getFrontendSettingsMap * feat(plugins): introduce cdn loading for angular plugins * refactor(plugins): move systemjs cache buster into systemjsplugins directory * Plugins CDN: Rename pluginsCDNBasePath to pluginsCDNBaseURL * refactor(plugins): introduce pluginsCDNBaseURL to the frontend * Plugins CDN: Renamed "cdn base path" to "cdn url template" in backend * Plugins CDN: lint * merge with main * Instrumentation: Add prometheus counter for backend hits, log from Info to Warn * Config: Changed key from plugins_cdn.url to plugins.plugins_cdn_base_url * CDN: Add backend tests * Lint: goimports * Default CDN URL to empty string, * Do not use CDN in setImages and module if the url template is empty * CDN: Backend: Add test for frontend settings * CDN: Do not log missing module.js warn if plugin is being loaded from CDN * CDN: Add backend test for CDN plugin loader * Removed 'cdn' signature level, switch to 'valid' * Fix pfs.TestParseTreeTestdata for cdn plugin testdata dir * Fix TestLoader_Load * Fix gocyclo complexity of loadPlugins * Plugins CDN: Moved prometheus metric to api package, removed asset_path label * Fix missing in config * Changes after review * Add pluginscdn.Service * Fix tests * Refactoring * Moved all remaining CDN checks inside pluginscdn.Service * CDN url constructor: Renamed stringURLFor to stringPath * CDN: Moved asset URL functionality to assetpath service * CDN: Renamed HasCDN() to IsEnabled() * CDN: Replace assert with require * CDN: Changes after review * Assetpath: Handle url.Parse error * Fix plugin_resource_test * CDN: Change fallback redirect from 302 to 307 * goimports * Fix tests * Switch to contextmodel.ReqContext in plugins.go Co-authored-by: Will Browne Co-authored-by: Jack Westbrook --- packages/grafana-runtime/src/config.ts | 1 + pkg/api/frontendsettings.go | 9 ++ pkg/api/frontendsettings_test.go | 58 ++++++- pkg/api/http_server.go | 4 + pkg/api/plugin_resource_test.go | 5 +- pkg/api/plugins.go | 69 ++++++-- pkg/api/plugins_test.go | 135 ++++++++++++++-- pkg/plugins/config/config.go | 3 + .../manager/loader/assetpath/assetpath.go | 70 ++++++++ .../loader/assetpath/assetpath_test.go | 88 ++++++++++ pkg/plugins/manager/loader/loader.go | 150 +++++++++--------- pkg/plugins/manager/loader/loader_test.go | 61 ++++++- .../manager/manager_integration_test.go | 7 +- pkg/plugins/manager/signature/signature.go | 6 +- .../manager/testdata/cdn/plugin/plugin.json | 40 +++++ pkg/plugins/pfs/pfs_test.go | 4 + pkg/plugins/pluginscdn/pluginscdn.go | 80 ++++++++++ pkg/plugins/pluginscdn/pluginscdn_test.go | 49 ++++++ pkg/plugins/pluginscdn/url_constructor.go | 61 +++++++ .../pluginscdn/url_constructor_test.go | 34 ++++ .../pluginsintegration/pluginsintegration.go | 4 + pkg/setting/setting.go | 2 + pkg/setting/setting_plugins.go | 4 + public/app/angular/AngularApp.ts | 8 +- .../components/plugin_component.test.ts | 18 +++ .../angular/components/plugin_component.ts | 31 +++- .../features/plugins/admin/state/actions.ts | 2 +- public/app/features/plugins/plugin_loader.ts | 21 ++- .../plugins/systemjsPlugins/pluginCDN.test.ts | 105 ++++++++++++ .../plugins/systemjsPlugins/pluginCDN.ts | 29 ++++ .../plugins/systemjsPlugins/pluginCSS.ts | 75 +++++++++ .../pluginCacheBuster.test.ts | 3 +- .../pluginCacheBuster.ts | 2 +- .../features/plugins/systemjsPlugins/types.ts | 16 ++ public/vendor/plugin-css/css.js | 73 --------- 35 files changed, 1139 insertions(+), 188 deletions(-) create mode 100644 pkg/plugins/manager/loader/assetpath/assetpath.go create mode 100644 pkg/plugins/manager/loader/assetpath/assetpath_test.go create mode 100644 pkg/plugins/manager/testdata/cdn/plugin/plugin.json create mode 100644 pkg/plugins/pluginscdn/pluginscdn.go create mode 100644 pkg/plugins/pluginscdn/pluginscdn_test.go create mode 100644 pkg/plugins/pluginscdn/url_constructor.go create mode 100644 pkg/plugins/pluginscdn/url_constructor_test.go create mode 100644 public/app/angular/components/plugin_component.test.ts create mode 100644 public/app/features/plugins/systemjsPlugins/pluginCDN.test.ts create mode 100644 public/app/features/plugins/systemjsPlugins/pluginCDN.ts create mode 100644 public/app/features/plugins/systemjsPlugins/pluginCSS.ts rename public/app/features/plugins/{tests => systemjsPlugins}/pluginCacheBuster.test.ts (98%) rename public/app/features/plugins/{ => systemjsPlugins}/pluginCacheBuster.ts (94%) create mode 100644 public/app/features/plugins/systemjsPlugins/types.ts delete mode 100644 public/vendor/plugin-css/css.js diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index 1382f8c6aba..c6614026f04 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -111,6 +111,7 @@ export class GrafanaBootConfig implements GrafanaConfig { pluginAdminEnabled = true; pluginAdminExternalManageEnabled = false; pluginCatalogHiddenPlugins: string[] = []; + pluginsCDNBaseURL = ''; expressionsEnabled = false; customTheme?: undefined; awsAllowedAuthProviders: string[] = []; diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index a92d2ff25a8..00c05cf7e3d 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -2,6 +2,7 @@ package api import ( "context" + "fmt" "net/http" "strconv" @@ -212,6 +213,14 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *contextmodel.ReqContext) (map[st "snapshotEnabled": hs.Cfg.SnapshotEnabled, } + if hs.pluginsCDNService != nil && hs.pluginsCDNService.IsEnabled() { + cdnBaseURL, err := hs.pluginsCDNService.BaseURL() + if err != nil { + return nil, fmt.Errorf("plugins cdn base url: %w", err) + } + jsonObj["pluginsCDNBaseURL"] = cdnBaseURL + } + if hs.ThumbService != nil { jsonObj["dashboardPreviews"] = hs.ThumbService.GetDashboardPreviewsSetupSettings(c) } diff --git a/pkg/api/frontendsettings_test.go b/pkg/api/frontendsettings_test.go index dc1d1446a43..5569e30d4e2 100644 --- a/pkg/api/frontendsettings_test.go +++ b/pkg/api/frontendsettings_test.go @@ -8,6 +8,8 @@ import ( "testing" "github.com/grafana/grafana/pkg/login/social" + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -58,7 +60,11 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features *featuremgmt. grafanaUpdateChecker: &updatechecker.GrafanaService{}, AccessControl: accesscontrolmock.New().WithDisabled(), PluginSettings: pluginSettings.ProvideService(sqlStore, secretsService), - SocialService: social.ProvideService(cfg, features), + pluginsCDNService: pluginscdn.ProvideService(&config.Cfg{ + PluginsCDNURLTemplate: cfg.PluginsCDNURLTemplate, + PluginSettings: cfg.PluginSettings, + }), + SocialService: social.ProvideService(cfg, features), } m := web.New() @@ -138,3 +144,53 @@ func TestHTTPServer_GetFrontendSettings_hideVersionAnonymous(t *testing.T) { }) } } + +func TestHTTPServer_GetFrontendSettings_pluginsCDNBaseURL(t *testing.T) { + type settings struct { + PluginsCDNBaseURL string `json:"pluginsCDNBaseURL"` + } + + tests := []struct { + desc string + mutateCfg func(*setting.Cfg) + expected settings + }{ + { + desc: "With CDN", + mutateCfg: func(cfg *setting.Cfg) { + cfg.PluginsCDNURLTemplate = "https://cdn.example.com/{id}/{version}/public/plugins/{id}/{assetPath}" + }, + expected: settings{PluginsCDNBaseURL: "https://cdn.example.com"}, + }, + { + desc: "Without CDN", + mutateCfg: func(cfg *setting.Cfg) { + cfg.PluginsCDNURLTemplate = "" + }, + expected: settings{PluginsCDNBaseURL: ""}, + }, + { + desc: "CDN is disabled by default", + expected: settings{PluginsCDNBaseURL: ""}, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + cfg := setting.NewCfg() + if test.mutateCfg != nil { + test.mutateCfg(cfg) + } + m, _ := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures()) + req := httptest.NewRequest(http.MethodGet, "/api/frontend/settings", nil) + + recorder := httptest.NewRecorder() + m.ServeHTTP(recorder, req) + var got settings + err := json.Unmarshal(recorder.Body.Bytes(), &got) + require.NoError(t, err) + require.Equal(t, http.StatusOK, recorder.Code) + require.EqualValues(t, test.expected, got) + }) + } +} diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index c3500501a20..b6348daf98e 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -33,6 +33,7 @@ import ( "github.com/grafana/grafana/pkg/middleware/csrf" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/plugincontext" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/registry/corekind" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/alerting" @@ -201,6 +202,7 @@ type HTTPServer struct { playlistService playlist.Service apiKeyService apikey.Service kvStore kvstore.KVStore + pluginsCDNService *pluginscdn.Service userService user.Service tempUserService tempUser.Service @@ -258,6 +260,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi annotationRepo annotations.Repository, tagService tag.Service, searchv2HTTPService searchV2.SearchHTTPService, queryLibraryHTTPService querylibrary.HTTPService, queryLibraryService querylibrary.Service, oauthTokenService oauthtoken.OAuthTokenService, statsService stats.Service, authnService authn.Service, + pluginsCDNService *pluginscdn.Service, k8saccess k8saccess.K8SAccess, // required so that the router is registered starApi *starApi.API, ) (*HTTPServer, error) { @@ -366,6 +369,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi oauthTokenService: oauthTokenService, statsService: statsService, authnService: authnService, + pluginsCDNService: pluginsCDNService, starApi: starApi, } if hs.Listener != nil { diff --git a/pkg/api/plugin_resource_test.go b/pkg/api/plugin_resource_test.go index a7cf4e9bdfe..a63e9485264 100644 --- a/pkg/api/plugin_resource_test.go +++ b/pkg/api/plugin_resource_test.go @@ -23,10 +23,12 @@ import ( pluginClient "github.com/grafana/grafana/pkg/plugins/manager/client" "github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/grafana/grafana/pkg/plugins/manager/loader" + "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/manager/store" "github.com/grafana/grafana/pkg/plugins/plugincontext" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/services/accesscontrol" datasources "github.com/grafana/grafana/pkg/services/datasources/fakes" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -56,8 +58,9 @@ func TestCallResource(t *testing.T) { nil, nil, nil, nil, testdatasource.ProvideService(cfg, featuremgmt.WithFeatures()), nil, nil, nil, nil, nil, nil) pCfg := config.ProvideConfig(setting.ProvideProvider(cfg), cfg) reg := registry.ProvideService() + cdn := pluginscdn.ProvideService(pCfg) l := loader.ProvideService(pCfg, fakes.NewFakeLicensingService(), signature.NewUnsignedAuthorizer(pCfg), - reg, provider.ProvideService(coreRegistry), fakes.NewFakeRoleRegistry()) + reg, provider.ProvideService(coreRegistry), fakes.NewFakeRoleRegistry(), cdn, assetpath.ProvideService(cdn)) ps, err := store.ProvideService(cfg, pCfg, reg, l) require.NoError(t, err) diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 423b731eff5..2658391d2cc 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -15,6 +15,8 @@ import ( "strings" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" @@ -33,6 +35,14 @@ import ( "github.com/grafana/grafana/pkg/web" ) +// pluginsCDNFallbackRedirectRequests is a metric counter keeping track of how many +// requests are received on the plugins CDN backend redirect fallback handler. +var pluginsCDNFallbackRedirectRequests = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: "grafana", + Name: "plugins_cdn_fallback_redirect_requests_total", + Help: "Number of requests to the plugins CDN backend redirect fallback handler.", +}, []string{"plugin_id", "plugin_version"}) + func (hs *HTTPServer) GetPluginList(c *contextmodel.ReqContext) response.Response { typeFilter := c.Query("type") enabledFilter := c.Query("enabled") @@ -301,6 +311,13 @@ func (hs *HTTPServer) CollectPluginMetrics(c *contextmodel.ReqContext) response. // getPluginAssets returns public plugin assets (images, JS, etc.) // +// If the plugin has cdn = false in its config (default), it will always attempt to return the asset +// from the local filesystem. +// +// If the plugin has cdn = true and hs.Cfg.PluginsCDNURLTemplate is empty, it will get the file +// from the local filesystem. If hs.Cfg.PluginsCDNURLTemplate is not empty, +// this handler returns a redirect to the plugin asset file on the specified CDN. +// // /public/plugins/:pluginId/* func (hs *HTTPServer) getPluginAssets(c *contextmodel.ReqContext) { pluginID := web.Params(c.Req)[":pluginId"] @@ -318,7 +335,19 @@ func (hs *HTTPServer) getPluginAssets(c *contextmodel.ReqContext) { return } - f, err := plugin.File(requestedFile) + if hs.pluginsCDNService.PluginSupported(pluginID) { + // Send a redirect to the client + hs.redirectCDNPluginAsset(c, plugin, requestedFile) + return + } + + // Send the actual file to the client from local filesystem + hs.serveLocalPluginAsset(c, plugin, requestedFile) +} + +// serveLocalPluginAsset returns the content of a plugin asset file from the local filesystem to the http client. +func (hs *HTTPServer) serveLocalPluginAsset(c *contextmodel.ReqContext, plugin plugins.PluginDTO, assetPath string) { + f, err := plugin.File(assetPath) if err != nil { if errors.Is(err, plugins.ErrFileNotExist) { c.JsonApiErr(404, "Plugin file not found", nil) @@ -346,15 +375,37 @@ func (hs *HTTPServer) getPluginAssets(c *contextmodel.ReqContext) { } if rs, ok := f.(io.ReadSeeker); ok { - http.ServeContent(c.Resp, c.Req, requestedFile, fi.ModTime(), rs) - } else { - b, err := io.ReadAll(f) - if err != nil { - c.JsonApiErr(500, "Plugin file exists but could not read", err) - return - } - http.ServeContent(c.Resp, c.Req, requestedFile, fi.ModTime(), bytes.NewReader(b)) + http.ServeContent(c.Resp, c.Req, assetPath, fi.ModTime(), rs) + return } + + b, err := io.ReadAll(f) + if err != nil { + c.JsonApiErr(500, "Plugin file exists but could not read", err) + return + } + http.ServeContent(c.Resp, c.Req, assetPath, fi.ModTime(), bytes.NewReader(b)) +} + +// redirectCDNPluginAsset redirects the http request to specified asset path on the configured plugins CDN. +func (hs *HTTPServer) redirectCDNPluginAsset(c *contextmodel.ReqContext, plugin plugins.PluginDTO, assetPath string) { + remoteURL, err := hs.pluginsCDNService.AssetURL(plugin.ID, plugin.Info.Version, assetPath) + if err != nil { + c.JsonApiErr(500, "Failed to get CDN plugin asset remote URL", err) + return + } + hs.log.Warn( + "plugin cdn redirect hit", + "pluginID", plugin.ID, + "pluginVersion", plugin.Info.Version, + "assetPath", assetPath, + "remoteURL", remoteURL, + ) + pluginsCDNFallbackRedirectRequests.With(prometheus.Labels{ + "plugin_id": plugin.ID, + "plugin_version": plugin.Info.Version, + }).Inc() + http.Redirect(c.Resp, c.Req, remoteURL, http.StatusTemporaryRedirect) } // CheckHealth returns the health of a plugin. diff --git a/pkg/api/plugins_test.go b/pkg/api/plugins_test.go index 1c85d632ead..fef5ceba31b 100644 --- a/pkg/api/plugins_test.go +++ b/pkg/api/plugins_test.go @@ -12,7 +12,9 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" + "github.com/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" + "github.com/stretchr/testify/require" "github.com/grafana/grafana-plugin-sdk-go/backend" @@ -21,6 +23,8 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log/logtest" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" ac "github.com/grafana/grafana/pkg/services/accesscontrol" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" @@ -139,7 +143,7 @@ func Test_PluginsInstallAndUninstall_AccessControl(t *testing.T) { req := webtest.RequestWithSignedInUser(server.NewPostRequest("/api/plugins/test/install", input), userWithPermissions(1, tc.permissions)) res, err := server.SendJSON(req) require.NoError(t, err) - assert.Equal(t, tc.expectedCode, res.StatusCode) + require.Equal(t, tc.expectedCode, res.StatusCode) require.NoError(t, res.Body.Close()) }) @@ -148,12 +152,105 @@ func Test_PluginsInstallAndUninstall_AccessControl(t *testing.T) { req := webtest.RequestWithSignedInUser(server.NewPostRequest("/api/plugins/test/uninstall", input), userWithPermissions(1, tc.permissions)) res, err := server.SendJSON(req) require.NoError(t, err) - assert.Equal(t, tc.expectedCode, res.StatusCode) + require.Equal(t, tc.expectedCode, res.StatusCode) require.NoError(t, res.Body.Close()) }) } } +func Test_GetPluginAssetCDNRedirect(t *testing.T) { + const cdnPluginID = "cdn-plugin" + const nonCDNPluginID = "non-cdn-plugin" + t.Run("Plugin CDN asset redirect", func(t *testing.T) { + cdnPlugin := &plugins.Plugin{ + JSONData: plugins.JSONData{ID: cdnPluginID, Info: plugins.Info{Version: "1.0.0"}}, + } + nonCdnPlugin := &plugins.Plugin{ + JSONData: plugins.JSONData{ID: nonCDNPluginID, Info: plugins.Info{Version: "2.0.0"}}, + } + service := &plugins.FakePluginStore{ + PluginList: []plugins.PluginDTO{ + cdnPlugin.ToDTO(), + nonCdnPlugin.ToDTO(), + }, + } + cfg := setting.NewCfg() + cfg.PluginsCDNURLTemplate = "https://cdn.example.com/{id}/{version}/public/plugins/{id}/{assetPath}" + cfg.PluginSettings = map[string]map[string]string{ + cdnPluginID: {"cdn": "true"}, + } + + const cdnFolderBaseURL = "https://cdn.example.com/cdn-plugin/1.0.0/public/plugins/cdn-plugin" + + type tc struct { + assetURL string + expRelativeURL string + } + for _, cas := range []tc{ + {"module.js", "module.js"}, + {"other/folder/file.js", "other/folder/file.js"}, + {"double////slashes/file.js", "double/slashes/file.js"}, + } { + pluginAssetScenario( + t, + "When calling GET for a CDN plugin on", + fmt.Sprintf("/public/plugins/%s/%s", cdnPluginID, cas.assetURL), + "/public/plugins/:pluginId/*", + cfg, service, func(sc *scenarioContext) { + // Get the prometheus metric (to test that the handler is instrumented correctly) + counter := pluginsCDNFallbackRedirectRequests.With(prometheus.Labels{ + "plugin_id": cdnPluginID, + "plugin_version": "1.0.0", + }) + + // Encode the prometheus metric and get its value + var m dto.Metric + require.NoError(t, counter.Write(&m)) + before := m.Counter.GetValue() + + // Call handler + callGetPluginAsset(sc) + + // Check redirect code + location + require.Equal(t, http.StatusTemporaryRedirect, sc.resp.Code, "wrong status code") + require.Equal(t, cdnFolderBaseURL+"/"+cas.expRelativeURL, sc.resp.Header().Get("Location"), "wrong location header") + + // Check metric + require.NoError(t, counter.Write(&m)) + require.Equal(t, before+1, m.Counter.GetValue(), "prometheus metric not incremented") + }, + ) + } + pluginAssetScenario( + t, + "When calling GET for a non-CDN plugin on", + fmt.Sprintf("/public/plugins/%s/%s", nonCDNPluginID, "module.js"), + "/public/plugins/:pluginId/*", + cfg, service, func(sc *scenarioContext) { + // Here the metric should not increment + var m dto.Metric + counter := pluginsCDNFallbackRedirectRequests.With(prometheus.Labels{ + "plugin_id": nonCDNPluginID, + "plugin_version": "2.0.0", + }) + require.NoError(t, counter.Write(&m)) + require.Zero(t, m.Counter.GetValue()) + + // Call handler + callGetPluginAsset(sc) + + // 404 implies access to fs + require.Equal(t, http.StatusNotFound, sc.resp.Code) + require.Empty(t, sc.resp.Header().Get("Location")) + + // Ensure the metric did not change + require.NoError(t, counter.Write(&m)) + require.Zero(t, m.Counter.GetValue()) + }, + ) + }) +} + func Test_GetPluginAssets(t *testing.T) { pluginID := "test-plugin" pluginDir := "." @@ -185,8 +282,8 @@ func Test_GetPluginAssets(t *testing.T) { } url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile) - pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service, - func(sc *scenarioContext) { + pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", + setting.NewCfg(), service, func(sc *scenarioContext) { callGetPluginAsset(sc) require.Equal(t, 200, sc.resp.Code) @@ -201,8 +298,8 @@ func Test_GetPluginAssets(t *testing.T) { } url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, tmpFileInParentDir.Name()) - pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service, - func(sc *scenarioContext) { + pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", + setting.NewCfg(), service, func(sc *scenarioContext) { callGetPluginAsset(sc) require.Equal(t, 404, sc.resp.Code) @@ -217,8 +314,8 @@ func Test_GetPluginAssets(t *testing.T) { requestedFile := "nonExistent" url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile) - pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service, - func(sc *scenarioContext) { + pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", + setting.NewCfg(), service, func(sc *scenarioContext) { callGetPluginAsset(sc) var respJson map[string]interface{} @@ -237,8 +334,8 @@ func Test_GetPluginAssets(t *testing.T) { requestedFile := "nonExistent" url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile) - pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service, - func(sc *scenarioContext) { + pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", + setting.NewCfg(), service, func(sc *scenarioContext) { callGetPluginAsset(sc) var respJson map[string]interface{} @@ -262,8 +359,8 @@ func Test_GetPluginAssets(t *testing.T) { l := &logtest.Fake{} url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile) - pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service, - func(sc *scenarioContext) { + pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", + setting.NewCfg(), service, func(sc *scenarioContext) { callGetPluginAsset(sc) require.Equal(t, 200, sc.resp.Code) @@ -383,12 +480,18 @@ func callGetPluginAsset(sc *scenarioContext) { sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() } -func pluginAssetScenario(t *testing.T, desc string, url string, urlPattern string, pluginStore plugins.Store, - fn scenarioFunc) { +func pluginAssetScenario(t *testing.T, desc string, url string, urlPattern string, + cfg *setting.Cfg, pluginStore plugins.Store, fn scenarioFunc) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { + cfg.IsFeatureToggleEnabled = func(_ string) bool { return false } hs := HTTPServer{ - Cfg: setting.NewCfg(), + Cfg: cfg, pluginStore: pluginStore, + log: log.NewNopLogger(), + pluginsCDNService: pluginscdn.ProvideService(&config.Cfg{ + PluginsCDNURLTemplate: cfg.PluginsCDNURLTemplate, + PluginSettings: cfg.PluginSettings, + }), } sc := setupScenarioContext(t, url) diff --git a/pkg/plugins/config/config.go b/pkg/plugins/config/config.go index 1c97e241819..c73b71e3b5d 100644 --- a/pkg/plugins/config/config.go +++ b/pkg/plugins/config/config.go @@ -30,6 +30,8 @@ type Cfg struct { BuildVersion string // TODO Remove LogDatasourceRequests bool + + PluginsCDNURLTemplate string } func ProvideConfig(settingProvider setting.Provider, grafanaCfg *setting.Cfg) *Cfg { @@ -63,6 +65,7 @@ func NewCfg(settingProvider setting.Provider, grafanaCfg *setting.Cfg) *Cfg { AWSAssumeRoleEnabled: aws.KeyValue("assume_role_enabled").MustBool(grafanaCfg.AWSAssumeRoleEnabled), Azure: grafanaCfg.Azure, LogDatasourceRequests: grafanaCfg.IsFeatureToggleEnabled(featuremgmt.FlagDatasourceLogger), + PluginsCDNURLTemplate: grafanaCfg.PluginsCDNURLTemplate, } } diff --git a/pkg/plugins/manager/loader/assetpath/assetpath.go b/pkg/plugins/manager/loader/assetpath/assetpath.go new file mode 100644 index 00000000000..57ebc7557da --- /dev/null +++ b/pkg/plugins/manager/loader/assetpath/assetpath.go @@ -0,0 +1,70 @@ +package assetpath + +import ( + "fmt" + "net/url" + "path" + "path/filepath" + "strings" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" +) + +// Service provides methods for constructing asset paths for plugins. +// It supports core plugins, external plugins stored on the local filesystem, and external plugins stored +// on the plugins CDN, and it will switch to the correct implementation depending on the plugin and the config. +type Service struct { + cdn *pluginscdn.Service +} + +func ProvideService(cdn *pluginscdn.Service) *Service { + return &Service{cdn: cdn} +} + +// Base returns the base path for the specified plugin. +func (s *Service) Base(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string) (string, error) { + if class == plugins.Core { + return path.Join("public/app/plugins", string(pluginJSON.Type), filepath.Base(pluginDir)), nil + } + if s.cdn.PluginSupported(pluginJSON.ID) { + return s.cdn.SystemJSAssetPath(pluginJSON.ID, pluginJSON.Info.Version, "") + } + return path.Join("public/plugins", pluginJSON.ID), nil +} + +// Module returns the module.js path for the specified plugin. +func (s *Service) Module(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string) (string, error) { + if class == plugins.Core { + return path.Join("app/plugins", string(pluginJSON.Type), filepath.Base(pluginDir), "module"), nil + } + if s.cdn.PluginSupported(pluginJSON.ID) { + return s.cdn.SystemJSAssetPath(pluginJSON.ID, pluginJSON.Info.Version, "module") + } + return path.Join("plugins", pluginJSON.ID, "module"), nil +} + +// RelativeURL returns the relative URL for an arbitrary plugin asset. +// If pathStr is an empty string, defaultStr is returned. +func (s *Service) RelativeURL(p *plugins.Plugin, pathStr, defaultStr string) (string, error) { + if pathStr == "" { + return defaultStr, nil + } + if s.cdn.PluginSupported(p.ID) { + // CDN + return s.cdn.NewCDNURLConstructor(p.ID, p.Info.Version).StringPath(pathStr) + } + // Local + u, err := url.Parse(pathStr) + if err != nil { + return "", fmt.Errorf("url parse: %w", err) + } + if u.IsAbs() { + return pathStr, nil + } + // is set as default or has already been prefixed with base path + if pathStr == defaultStr || strings.HasPrefix(pathStr, p.BaseURL) { + return pathStr, nil + } + return path.Join(p.BaseURL, pathStr), nil +} diff --git a/pkg/plugins/manager/loader/assetpath/assetpath_test.go b/pkg/plugins/manager/loader/assetpath/assetpath_test.go new file mode 100644 index 00000000000..1551aa57146 --- /dev/null +++ b/pkg/plugins/manager/loader/assetpath/assetpath_test.go @@ -0,0 +1,88 @@ +package assetpath + +import ( + "testing" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" + "github.com/stretchr/testify/require" +) + +func extPath(pluginID string) string { + return "/grafana/data/plugins/" + pluginID +} + +func TestService(t *testing.T) { + svc := ProvideService(pluginscdn.ProvideService(&config.Cfg{ + PluginsCDNURLTemplate: "https://cdn.example.com/{id}/{version}/public/plugins/{id}/{assetPath}", + PluginSettings: map[string]map[string]string{ + "one": {"cdn": "true"}, + "two": {}, + }, + })) + + const tableOldPath = "/grafana/public/app/plugins/panel/table-old" + jsonData := map[string]plugins.JSONData{ + "table-old": {ID: "table-old", Info: plugins.Info{Version: "1.0.0"}}, + + "one": {ID: "one", Info: plugins.Info{Version: "1.0.0"}}, + "two": {ID: "two", Info: plugins.Info{Version: "2.0.0"}}, + } + + t.Run("Base", func(t *testing.T) { + base, err := svc.Base(jsonData["one"], plugins.External, extPath("one")) + require.NoError(t, err) + require.Equal(t, "plugin-cdn/one/1.0.0/public/plugins/one", base) + + base, err = svc.Base(jsonData["two"], plugins.External, extPath("two")) + require.NoError(t, err) + require.Equal(t, "public/plugins/two", base) + + base, err = svc.Base(jsonData["table-old"], plugins.Core, tableOldPath) + require.NoError(t, err) + require.Equal(t, "public/app/plugins/table-old", base) + }) + + t.Run("Module", func(t *testing.T) { + module, err := svc.Module(jsonData["one"], plugins.External, extPath("one")) + require.NoError(t, err) + require.Equal(t, "plugin-cdn/one/1.0.0/public/plugins/one/module", module) + + module, err = svc.Module(jsonData["two"], plugins.External, extPath("two")) + require.NoError(t, err) + require.Equal(t, "plugins/two/module", module) + + module, err = svc.Module(jsonData["table-old"], plugins.Core, tableOldPath) + require.NoError(t, err) + require.Equal(t, "app/plugins/table-old/module", module) + }) + + t.Run("RelativeURL", func(t *testing.T) { + pluginsMap := map[string]*plugins.Plugin{ + "one": { + JSONData: plugins.JSONData{ID: "one", Info: plugins.Info{Version: "1.0.0"}}, + BaseURL: "plugin-cdn/one/1.0.0/public/pluginsMap/one", + }, + "two": { + JSONData: plugins.JSONData{ID: "two", Info: plugins.Info{Version: "2.0.0"}}, + BaseURL: "public/pluginsMap/two", + }, + } + u, err := svc.RelativeURL(pluginsMap["one"], "", "default") + require.NoError(t, err) + require.Equal(t, "default", u) + + u, err = svc.RelativeURL(pluginsMap["one"], "path/to/file.txt", "default") + require.NoError(t, err) + require.Equal(t, "https://cdn.example.com/one/1.0.0/public/plugins/one/path/to/file.txt", u) + + u, err = svc.RelativeURL(pluginsMap["two"], "path/to/file.txt", "default") + require.NoError(t, err) + require.Equal(t, "public/pluginsMap/two/path/to/file.txt", u) + + u, err = svc.RelativeURL(pluginsMap["two"], "default", "default") + require.NoError(t, err) + require.Equal(t, "default", u) + }) +} diff --git a/pkg/plugins/manager/loader/loader.go b/pkg/plugins/manager/loader/loader.go index 9c2abac8d8b..1d7d99bcbea 100644 --- a/pkg/plugins/manager/loader/loader.go +++ b/pkg/plugins/manager/loader/loader.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "net/url" "os" "path" "path/filepath" @@ -19,11 +18,13 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/logger" + "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" "github.com/grafana/grafana/pkg/plugins/manager/loader/finder" "github.com/grafana/grafana/pkg/plugins/manager/loader/initializer" "github.com/grafana/grafana/pkg/plugins/manager/process" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/signature" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/plugins/storage" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/util" @@ -44,21 +45,25 @@ type Loader struct { pluginInitializer initializer.Initializer signatureValidator signature.Validator pluginStorage storage.Manager + pluginsCDN *pluginscdn.Service + assetPath *assetpath.Service log log.Logger + cfg *config.Cfg errs map[string]*plugins.SignatureError } func ProvideService(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLoaderAuthorizer, pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider, - roleRegistry plugins.RoleRegistry) *Loader { + roleRegistry plugins.RoleRegistry, pluginsCDNService *pluginscdn.Service, assetPath *assetpath.Service) *Loader { return New(cfg, license, authorizer, pluginRegistry, backendProvider, process.NewManager(pluginRegistry), - storage.FileSystem(logger.NewLogger("loader.fs"), cfg.PluginsPath), roleRegistry) + storage.FileSystem(logger.NewLogger("loader.fs"), cfg.PluginsPath), roleRegistry, pluginsCDNService, assetPath) } func New(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLoaderAuthorizer, pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider, - processManager process.Service, pluginStorage storage.Manager, roleRegistry plugins.RoleRegistry) *Loader { + processManager process.Service, pluginStorage storage.Manager, roleRegistry plugins.RoleRegistry, + pluginsCDNService *pluginscdn.Service, assetPath *assetpath.Service) *Loader { return &Loader{ pluginFinder: finder.New(), pluginRegistry: pluginRegistry, @@ -69,6 +74,9 @@ func New(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLo errs: make(map[string]*plugins.SignatureError), log: log.New("plugin.loader"), roleRegistry: roleRegistry, + cfg: cfg, + pluginsCDN: pluginsCDNService, + assetPath: assetPath, } } @@ -81,6 +89,36 @@ func (l *Loader) Load(ctx context.Context, class plugins.Class, paths []string) return l.loadPlugins(ctx, class, pluginJSONPaths) } +func (l *Loader) createPluginsForLoading(class plugins.Class, foundPlugins foundPlugins) map[string]*plugins.Plugin { + loadedPlugins := make(map[string]*plugins.Plugin) + for pluginDir, pluginJSON := range foundPlugins { + plugin, err := l.createPluginBase(pluginJSON, class, pluginDir) + if err != nil { + l.log.Warn("Could not create plugin base", "pluginID", pluginJSON.ID, "err", err) + continue + } + + // calculate initial signature state + var sig plugins.Signature + if l.pluginsCDN.PluginSupported(plugin.ID) { + // CDN plugins have no signature checks for now. + sig = plugins.Signature{Status: plugins.SignatureValid} + } else { + sig, err = signature.Calculate(l.log, plugin) + if err != nil { + l.log.Warn("Could not calculate plugin signature state", "pluginID", plugin.ID, "err", err) + continue + } + } + plugin.Signature = sig.Status + plugin.SignatureType = sig.Type + plugin.SignatureOrg = sig.SigningOrg + + loadedPlugins[plugin.PluginDir] = plugin + } + return loadedPlugins +} + func (l *Loader) loadPlugins(ctx context.Context, class plugins.Class, pluginJSONPaths []string) ([]*plugins.Plugin, error) { var foundPlugins = foundPlugins{} @@ -113,22 +151,8 @@ func (l *Loader) loadPlugins(ctx context.Context, class plugins.Class, pluginJSO foundPlugins.stripDuplicates(registeredPlugins, l.log) - // calculate initial signature state - loadedPlugins := make(map[string]*plugins.Plugin) - for pluginDir, pluginJSON := range foundPlugins { - plugin := createPluginBase(pluginJSON, class, pluginDir) - - sig, err := signature.Calculate(l.log, plugin) - if err != nil { - l.log.Warn("Could not calculate plugin signature state", "pluginID", plugin.ID, "err", err) - continue - } - plugin.Signature = sig.Status - plugin.SignatureType = sig.Type - plugin.SignatureOrg = sig.SigningOrg - - loadedPlugins[plugin.PluginDir] = plugin - } + // create plugins structs and calculate signatures + loadedPlugins := l.createPluginsForLoading(class, foundPlugins) // wire up plugin dependencies for _, plugin := range loadedPlugins { @@ -165,12 +189,13 @@ func (l *Loader) loadPlugins(ctx context.Context, class plugins.Class, pluginJSO // clear plugin error if a pre-existing error has since been resolved delete(l.errs, plugin.ID) - // verify module.js exists for SystemJS to load + // verify module.js exists for SystemJS to load. + // CDN plugins can be loaded with plugin.json only, so do not warn for those. if !plugin.IsRenderer() && !plugin.IsCorePlugin() { module := filepath.Join(plugin.PluginDir, "module.js") if exists, err := fs.Exists(module); err != nil { return nil, err - } else if !exists { + } else if !exists && !l.pluginsCDN.PluginSupported(plugin.ID) { l.log.Warn("Plugin missing module.js", "pluginID", plugin.ID, "warning", "Missing module.js, If you loaded this plugin from git, make sure to compile it.", @@ -312,28 +337,47 @@ func (l *Loader) readPluginJSON(pluginJSONPath string) (plugins.JSONData, error) return plugin, nil } -func createPluginBase(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string) *plugins.Plugin { +func (l *Loader) createPluginBase(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string) (*plugins.Plugin, error) { + baseURL, err := l.assetPath.Base(pluginJSON, class, pluginDir) + if err != nil { + return nil, fmt.Errorf("base url: %w", err) + } + moduleURL, err := l.assetPath.Module(pluginJSON, class, pluginDir) + if err != nil { + return nil, fmt.Errorf("module url: %w", err) + } plugin := &plugins.Plugin{ JSONData: pluginJSON, PluginDir: pluginDir, - BaseURL: baseURL(pluginJSON, class, pluginDir), - Module: module(pluginJSON, class, pluginDir), + BaseURL: baseURL, + Module: moduleURL, Class: class, } plugin.SetLogger(log.New(fmt.Sprintf("plugin.%s", plugin.ID))) - setImages(plugin) + if err := l.setImages(plugin); err != nil { + return nil, err + } - return plugin + return plugin, nil } -func setImages(p *plugins.Plugin) { - p.Info.Logos.Small = pluginLogoURL(p.Type, p.Info.Logos.Small, p.BaseURL) - p.Info.Logos.Large = pluginLogoURL(p.Type, p.Info.Logos.Large, p.BaseURL) - - for i := 0; i < len(p.Info.Screenshots); i++ { - p.Info.Screenshots[i].Path = evalRelativePluginURLPath(p.Info.Screenshots[i].Path, p.BaseURL, p.Type) +func (l *Loader) setImages(p *plugins.Plugin) error { + var err error + for _, dst := range []*string{&p.Info.Logos.Small, &p.Info.Logos.Large} { + *dst, err = l.assetPath.RelativeURL(p, *dst, defaultLogoPath(p.Type)) + if err != nil { + return fmt.Errorf("logo: %w", err) + } } + for i := 0; i < len(p.Info.Screenshots); i++ { + screenshot := &p.Info.Screenshots[i] + screenshot.Path, err = l.assetPath.RelativeURL(p, screenshot.Path, "") + if err != nil { + return fmt.Errorf("screenshot %d relative url: %w", i, err) + } + } + return nil } func setDefaultNavURL(p *plugins.Plugin) { @@ -377,36 +421,10 @@ func configureAppChildPlugin(parent *plugins.Plugin, child *plugins.Plugin) { } } -func pluginLogoURL(pluginType plugins.Type, path, baseURL string) string { - if path == "" { - return defaultLogoPath(pluginType) - } - - return evalRelativePluginURLPath(path, baseURL, pluginType) -} - func defaultLogoPath(pluginType plugins.Type) string { return "public/img/icn-" + string(pluginType) + ".svg" } -func evalRelativePluginURLPath(pathStr, baseURL string, pluginType plugins.Type) string { - if pathStr == "" { - return "" - } - - u, _ := url.Parse(pathStr) - if u.IsAbs() { - return pathStr - } - - // is set as default or has already been prefixed with base path - if pathStr == defaultLogoPath(pluginType) || strings.HasPrefix(pathStr, baseURL) { - return pathStr - } - - return path.Join(baseURL, pathStr) -} - func (l *Loader) PluginErrors() []*plugins.Error { errs := make([]*plugins.Error, 0) for _, err := range l.errs { @@ -419,20 +437,6 @@ func (l *Loader) PluginErrors() []*plugins.Error { return errs } -func baseURL(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string) string { - if class == plugins.Core { - return path.Join("public/app/plugins", string(pluginJSON.Type), filepath.Base(pluginDir)) - } - return path.Join("public/plugins", pluginJSON.ID) -} - -func module(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string) string { - if class == plugins.Core { - return path.Join("app/plugins", string(pluginJSON.Type), filepath.Base(pluginDir), "module") - } - return path.Join("plugins", pluginJSON.ID, "module") -} - func validatePluginJSON(data plugins.JSONData) error { if data.ID == "" || !data.Type.IsValid() { return ErrInvalidPluginJSON diff --git a/pkg/plugins/manager/loader/loader_test.go b/pkg/plugins/manager/loader/loader_test.go index 4c0ccbbcd6c..190ae69a90e 100644 --- a/pkg/plugins/manager/loader/loader_test.go +++ b/pkg/plugins/manager/loader/loader_test.go @@ -7,6 +7,9 @@ import ( "sort" "testing" + "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" + "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/require" @@ -404,6 +407,61 @@ func TestLoader_Load(t *testing.T) { }, }, }, + { + name: "Load CDN plugin", + class: plugins.External, + cfg: &config.Cfg{ + PluginsCDNURLTemplate: "https://cdn.example.com/{id}/{version}/public/plugins/{id}/{assetPath}", + PluginSettings: setting.PluginSettings{ + "grafana-worldmap-panel": {"cdn": "true"}, + }, + }, + pluginPaths: []string{"../testdata/cdn"}, + want: []*plugins.Plugin{ + { + JSONData: plugins.JSONData{ + ID: "grafana-worldmap-panel", + Type: "panel", + Name: "Worldmap Panel", + Info: plugins.Info{ + Version: "0.3.3", + Links: []plugins.InfoLink{ + {Name: "Project site", URL: "https://github.com/grafana/worldmap-panel"}, + {Name: "MIT License", URL: "https://github.com/grafana/worldmap-panel/blob/master/LICENSE"}, + }, + Logos: plugins.Logos{ + // Path substitution + Small: "https://cdn.example.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/images/worldmap_logo.svg", + Large: "https://cdn.example.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/images/worldmap_logo.svg", + }, + Screenshots: []plugins.Screenshots{ + { + Name: "World", + Path: "https://cdn.example.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/images/worldmap-world.png", + }, + { + Name: "USA", + Path: "https://cdn.example.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/images/worldmap-usa.png", + }, + { + Name: "Light Theme", + Path: "https://cdn.example.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/images/worldmap-light-theme.png", + }, + }, + }, + Dependencies: plugins.Dependencies{ + GrafanaVersion: "3.x.x", + Plugins: []plugins.Dependency{}, + }, + }, + PluginDir: filepath.Join(parentDir, "testdata/cdn/plugin"), + Class: plugins.External, + Signature: plugins.SignatureValid, + BaseURL: "plugin-cdn/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel", + Module: "plugin-cdn/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/module", + }, + }, + }, } for _, tt := range tests { reg := fakes.NewFakePluginRegistry() @@ -1320,9 +1378,10 @@ func Test_setPathsBasedOnApp(t *testing.T) { } func newLoader(cfg *config.Cfg, cbs ...func(loader *Loader)) *Loader { + cdn := pluginscdn.ProvideService(cfg) l := New(cfg, &fakes.FakeLicensingService{}, signature.NewUnsignedAuthorizer(cfg), fakes.NewFakePluginRegistry(), fakes.NewFakeBackendProcessProvider(), fakes.NewFakeProcessManager(), fakes.NewFakePluginStorage(), - fakes.NewFakeRoleRegistry()) + fakes.NewFakeRoleRegistry(), cdn, assetpath.ProvideService(cdn)) for _, cb := range cbs { cb(l) diff --git a/pkg/plugins/manager/manager_integration_test.go b/pkg/plugins/manager/manager_integration_test.go index fe1391c862d..5fcc288e917 100644 --- a/pkg/plugins/manager/manager_integration_test.go +++ b/pkg/plugins/manager/manager_integration_test.go @@ -8,6 +8,9 @@ import ( "testing" "time" + "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" + "github.com/grafana/grafana-azure-sdk-go/azsettings" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" @@ -110,10 +113,12 @@ func TestIntegrationPluginManager(t *testing.T) { pCfg := config.ProvideConfig(setting.ProvideProvider(cfg), cfg) reg := registry.ProvideService() + cdn := pluginscdn.ProvideService(pCfg) lic := plicensing.ProvideLicensing(cfg, &licensing.OSSLicensingService{Cfg: cfg}) l := loader.ProvideService(pCfg, lic, signature.NewUnsignedAuthorizer(pCfg), - reg, provider.ProvideService(coreRegistry), fakes.NewFakeRoleRegistry()) + reg, provider.ProvideService(coreRegistry), fakes.NewFakeRoleRegistry(), + cdn, assetpath.ProvideService(cdn)) ps, err := store.ProvideService(cfg, pCfg, reg, l) require.NoError(t, err) diff --git a/pkg/plugins/manager/signature/signature.go b/pkg/plugins/manager/signature/signature.go index 2e7262d7569..3954a4850d6 100644 --- a/pkg/plugins/manager/signature/signature.go +++ b/pkg/plugins/manager/signature/signature.go @@ -18,14 +18,14 @@ func NewValidator(authorizer plugins.PluginLoaderAuthorizer) Validator { } func (s *Validator) Validate(plugin *plugins.Plugin) *plugins.SignatureError { - if plugin.Signature == plugins.SignatureValid { + if plugin.Signature.IsValid() { s.log.Debug("Plugin has valid signature", "id", plugin.ID) return nil } // If a plugin is nested within another, create links to each other to inherit signature details if plugin.Parent != nil { - if plugin.IsCorePlugin() || plugin.Signature == plugins.SignatureInternal { + if plugin.IsCorePlugin() || plugin.Signature.IsInternal() { s.log.Debug("Not setting descendant plugin's signature to that of root since it's core or internal", "plugin", plugin.ID, "signature", plugin.Signature, "isCore", plugin.IsCorePlugin()) } else { @@ -34,7 +34,7 @@ func (s *Validator) Validate(plugin *plugins.Plugin) *plugins.SignatureError { plugin.Signature = plugin.Parent.Signature plugin.SignatureType = plugin.Parent.SignatureType plugin.SignatureOrg = plugin.Parent.SignatureOrg - if plugin.Signature == plugins.SignatureValid { + if plugin.Signature.IsValid() { s.log.Debug("Plugin has valid signature (inherited from root)", "id", plugin.ID) return nil } diff --git a/pkg/plugins/manager/testdata/cdn/plugin/plugin.json b/pkg/plugins/manager/testdata/cdn/plugin/plugin.json new file mode 100644 index 00000000000..e0c88b874da --- /dev/null +++ b/pkg/plugins/manager/testdata/cdn/plugin/plugin.json @@ -0,0 +1,40 @@ +{ + "type": "panel", + "name": "Worldmap Panel", + "id": "grafana-worldmap-panel", + "info": { + "logos": { + "small": "images/worldmap_logo.svg", + "large": "images/worldmap_logo.svg" + }, + "links": [ + { + "name": "Project site", + "url": "https://github.com/grafana/worldmap-panel" + }, + { + "name": "MIT License", + "url": "https://github.com/grafana/worldmap-panel/blob/master/LICENSE" + } + ], + "screenshots": [ + { + "name": "World", + "path": "images/worldmap-world.png" + }, + { + "name": "USA", + "path": "images/worldmap-usa.png" + }, + { + "name": "Light Theme", + "path": "images/worldmap-light-theme.png" + } + ], + "version": "0.3.3" + }, + "dependencies": { + "grafanaVersion": "3.x.x", + "plugins": [] + } +} diff --git a/pkg/plugins/pfs/pfs_test.go b/pkg/plugins/pfs/pfs_test.go index 7f1dd59ecb2..bef758a5924 100644 --- a/pkg/plugins/pfs/pfs_test.go +++ b/pkg/plugins/pfs/pfs_test.go @@ -127,6 +127,10 @@ func TestParsePluginTestdata(t *testing.T) { "disallowed-cue-import": { err: ErrDisallowedCUEImport, }, + "cdn": { + rootid: "grafana-worldmap-panel", + subpath: "plugin", + }, } staticRootPath, err := filepath.Abs("../manager/testdata") diff --git a/pkg/plugins/pluginscdn/pluginscdn.go b/pkg/plugins/pluginscdn/pluginscdn.go new file mode 100644 index 00000000000..3539d9ac765 --- /dev/null +++ b/pkg/plugins/pluginscdn/pluginscdn.go @@ -0,0 +1,80 @@ +package pluginscdn + +import ( + "errors" + "fmt" + "net/url" + "path" + + "github.com/grafana/grafana/pkg/plugins/config" +) + +const ( + // systemJSCDNKeyword is the path prefix used by system.js to identify the plugins CDN. + systemJSCDNKeyword = "plugin-cdn" +) + +var ErrPluginNotCDN = errors.New("plugin is not a cdn plugin") + +// Service provides methods for the plugins CDN. +type Service struct { + cfg *config.Cfg +} + +func ProvideService(cfg *config.Cfg) *Service { + return &Service{cfg: cfg} +} + +// NewCDNURLConstructor returns a new URLConstructor for the provided plugin id and version. +// The CDN should be enabled for the plugin, otherwise the returned URLConstructor will have +// and invalid base url. +func (s *Service) NewCDNURLConstructor(pluginID, pluginVersion string) URLConstructor { + return URLConstructor{ + cdnURLTemplate: s.cfg.PluginsCDNURLTemplate, + pluginID: pluginID, + pluginVersion: pluginVersion, + } +} + +// IsEnabled returns true if the plugins cdn is enabled. +func (s *Service) IsEnabled() bool { + return s.cfg.PluginsCDNURLTemplate != "" +} + +// PluginSupported returns true if the CDN is enabled in the config and if the specified plugin ID has CDN enabled. +func (s *Service) PluginSupported(pluginID string) bool { + return s.IsEnabled() && s.cfg.PluginSettings[pluginID]["cdn"] != "" +} + +// BaseURL returns the absolute base URL of the plugins CDN. +// If the plugins CDN is disabled, it returns an empty string. +func (s *Service) BaseURL() (string, error) { + if !s.IsEnabled() { + return "", nil + } + u, err := url.Parse(s.cfg.PluginsCDNURLTemplate) + if err != nil { + return "", fmt.Errorf("url parse: %w", err) + } + return u.Scheme + "://" + u.Host, nil +} + +// SystemJSAssetPath returns a system-js path for the specified asset on the plugins CDN. +// It replaces the base path of the CDN with systemJSCDNKeyword. +// If assetPath is an empty string, the base path for the plugin is returned. +func (s *Service) SystemJSAssetPath(pluginID, pluginVersion, assetPath string) (string, error) { + u, err := s.NewCDNURLConstructor(pluginID, pluginVersion).Path(assetPath) + if err != nil { + return "", err + } + return path.Join(systemJSCDNKeyword, u.Path), nil +} + +// AssetURL returns the URL of a CDN asset for a CDN plugin. If the specified plugin is not a CDN plugin, +// it returns ErrPluginNotCDN. +func (s *Service) AssetURL(pluginID, pluginVersion, assetPath string) (string, error) { + if !s.PluginSupported(pluginID) { + return "", ErrPluginNotCDN + } + return s.NewCDNURLConstructor(pluginID, pluginVersion).StringPath(assetPath) +} diff --git a/pkg/plugins/pluginscdn/pluginscdn_test.go b/pkg/plugins/pluginscdn/pluginscdn_test.go new file mode 100644 index 00000000000..b473aeb4b54 --- /dev/null +++ b/pkg/plugins/pluginscdn/pluginscdn_test.go @@ -0,0 +1,49 @@ +package pluginscdn + +import ( + "testing" + + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/stretchr/testify/require" +) + +func TestService(t *testing.T) { + svc := ProvideService(&config.Cfg{ + PluginsCDNURLTemplate: "https://cdn.example.com/{id}/{version}/public/plugins/{id}/{assetPath}", + PluginSettings: map[string]map[string]string{ + "one": {"cdn": "true"}, + "two": {}, + }, + }) + + t.Run("IsCDNPlugin", func(t *testing.T) { + require.True(t, svc.PluginSupported("one")) + require.False(t, svc.PluginSupported("two")) + require.False(t, svc.PluginSupported("unknown")) + }) + + t.Run("CDNBaseURL", func(t *testing.T) { + for _, c := range []struct { + name string + cfgURL string + expBaseURL string + }{ + { + name: "valid", + cfgURL: "https://grafana-assets.grafana.net/plugin-cdn-test/plugin-cdn/{id}/{version}/public/plugins/{id}/{assetPath}", + expBaseURL: "https://grafana-assets.grafana.net", + }, + { + name: "empty", + cfgURL: "", + expBaseURL: "", + }, + } { + t.Run(c.name, func(t *testing.T) { + u, err := ProvideService(&config.Cfg{PluginsCDNURLTemplate: c.cfgURL}).BaseURL() + require.NoError(t, err) + require.Equal(t, c.expBaseURL, u) + }) + } + }) +} diff --git a/pkg/plugins/pluginscdn/url_constructor.go b/pkg/plugins/pluginscdn/url_constructor.go new file mode 100644 index 00000000000..c4ab35ca668 --- /dev/null +++ b/pkg/plugins/pluginscdn/url_constructor.go @@ -0,0 +1,61 @@ +package pluginscdn + +import ( + "fmt" + "net/url" + "path" + "strings" +) + +// URLConstructor is a struct that can build CDN URLs for plugins on a remote CDN. +type URLConstructor struct { + // cdnURLTemplate is absolute base url of the CDN. This string will be formatted + // according to the rules specified in the Path method. + cdnURLTemplate string + + // pluginID is the ID of the plugin. + pluginID string + + // pluginVersion is the version of the plugin. + pluginVersion string +} + +// Path returns a new *url.URL that points to an asset file for the CDN, plugin and plugin version +// specified by the current URLConstructor. +// +// c.cdnURLTemplate is used to build the string, the following substitutions are performed in it: +// +// - {id} -> plugin id +// +// - {version} -> plugin version +// +// - {assetPath} -> assetPath +// +// The asset Path is sanitized via path.Clean (double slashes are removed, "../" is resolved, etc). +// +// The returned URL will be for a file, so it won't have a trailing slash. +func (c URLConstructor) Path(assetPath string) (*url.URL, error) { + u, err := url.Parse( + strings.TrimRight( + strings.NewReplacer( + "{id}", c.pluginID, + "{version}", c.pluginVersion, + "{assetPath}", strings.Trim(path.Clean("/"+assetPath+"/"), "/"), + ).Replace(c.cdnURLTemplate), + "/", + ), + ) + if err != nil { + return nil, fmt.Errorf("url parse: %w", err) + } + return u, nil +} + +// StringPath is like Path, but it returns the absolute URL as a string rather than *url.URL. +func (c URLConstructor) StringPath(assetPath string) (string, error) { + u, err := c.Path(assetPath) + if err != nil { + return "", err + } + return u.String(), nil +} diff --git a/pkg/plugins/pluginscdn/url_constructor_test.go b/pkg/plugins/pluginscdn/url_constructor_test.go new file mode 100644 index 00000000000..aae290c432d --- /dev/null +++ b/pkg/plugins/pluginscdn/url_constructor_test.go @@ -0,0 +1,34 @@ +package pluginscdn + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestURLConstructor_StringURLFor(t *testing.T) { + uc := URLConstructor{ + cdnURLTemplate: "https://the.cdn/{id}/{version}/{assetPath}", + pluginID: "the-plugin", + pluginVersion: "0.1", + } + type tc struct { + name string + path string + exp string + } + for _, c := range []tc{ + {"simple", "file.txt", "https://the.cdn/the-plugin/0.1/file.txt"}, + {"multiple", "some/path/to/file.txt", "https://the.cdn/the-plugin/0.1/some/path/to/file.txt"}, + {"path traversal", "some/../to/file.txt", "https://the.cdn/the-plugin/0.1/to/file.txt"}, + {"above root", "../../../../../file.txt", "https://the.cdn/the-plugin/0.1/file.txt"}, + {"multiple slashes", "some/////file.txt", "https://the.cdn/the-plugin/0.1/some/file.txt"}, + {"dots", "some/././././file.txt", "https://the.cdn/the-plugin/0.1/some/file.txt"}, + } { + t.Run(c.name, func(t *testing.T) { + u, err := uc.StringPath(c.path) + require.NoError(t, err) + require.Equal(t, c.exp, u) + }) + } +} diff --git a/pkg/services/pluginsintegration/pluginsintegration.go b/pkg/services/pluginsintegration/pluginsintegration.go index f6433487d20..ba9937438ab 100644 --- a/pkg/services/pluginsintegration/pluginsintegration.go +++ b/pkg/services/pluginsintegration/pluginsintegration.go @@ -10,11 +10,13 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager" "github.com/grafana/grafana/pkg/plugins/manager/client" "github.com/grafana/grafana/pkg/plugins/manager/loader" + "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" "github.com/grafana/grafana/pkg/plugins/manager/process" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/manager/store" "github.com/grafana/grafana/pkg/plugins/plugincontext" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/plugins/repo" "github.com/grafana/grafana/pkg/services/oauthtoken" "github.com/grafana/grafana/pkg/services/pluginsintegration/clientmiddleware" @@ -34,6 +36,8 @@ var WireSet = wire.NewSet( process.ProvideService, wire.Bind(new(process.Service), new(*process.Manager)), coreplugin.ProvideCoreRegistry, + pluginscdn.ProvideService, + assetpath.ProvideService, loader.ProvideService, wire.Bind(new(loader.Service), new(*loader.Loader)), wire.Bind(new(plugins.ErrorResolver), new(*loader.Loader)), diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index e4f6d211b68..b2b1feeb842 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -276,6 +276,8 @@ type Cfg struct { PluginAdminEnabled bool PluginAdminExternalManageEnabled bool + PluginsCDNURLTemplate string + // Panels DisableSanitizeHtml bool diff --git a/pkg/setting/setting_plugins.go b/pkg/setting/setting_plugins.go index 95d5f0e7fd0..8c29a2e52ff 100644 --- a/pkg/setting/setting_plugins.go +++ b/pkg/setting/setting_plugins.go @@ -26,6 +26,7 @@ func extractPluginSettings(sections []*ini.Section) PluginSettings { func (cfg *Cfg) readPluginSettings(iniFile *ini.File) error { pluginsSection := iniFile.Section("plugins") + cfg.PluginsEnableAlpha = pluginsSection.Key("enable_alpha").MustBool(false) cfg.PluginsAppsSkipVerifyTLS = pluginsSection.Key("app_tls_skip_verify_insecure").MustBool(false) cfg.PluginSettings = extractPluginSettings(iniFile.Sections()) @@ -47,5 +48,8 @@ func (cfg *Cfg) readPluginSettings(iniFile *ini.File) error { cfg.PluginCatalogHiddenPlugins = append(cfg.PluginCatalogHiddenPlugins, plug) } + // Plugins CDN settings + cfg.PluginsCDNURLTemplate = strings.TrimRight(pluginsSection.Key("cdn_base_url").MustString(""), "/") + return nil } diff --git a/public/app/angular/AngularApp.ts b/public/app/angular/AngularApp.ts index a4fb82ec255..2d64500d54b 100644 --- a/public/app/angular/AngularApp.ts +++ b/public/app/angular/AngularApp.ts @@ -43,12 +43,14 @@ export class AngularApp { '$filterProvider', '$httpProvider', '$provide', + '$sceDelegateProvider', ( $controllerProvider: angular.IControllerProvider, $compileProvider: angular.ICompileProvider, $filterProvider: angular.IFilterProvider, $httpProvider: angular.IHttpProvider, - $provide: angular.auto.IProvideService + $provide: angular.auto.IProvideService, + $sceDelegateProvider: angular.ISCEDelegateProvider ) => { if (config.buildInfo.env !== 'development') { $compileProvider.debugInfoEnabled(false); @@ -56,6 +58,10 @@ export class AngularApp { $httpProvider.useApplyAsync(true); + if (Boolean(config.pluginsCDNBaseURL)) { + $sceDelegateProvider.trustedResourceUrlList(['self', `${config.pluginsCDNBaseURL}/**`]); + } + this.registerFunctions.controller = $controllerProvider.register; this.registerFunctions.directive = $compileProvider.directive; this.registerFunctions.factory = $provide.factory; diff --git a/public/app/angular/components/plugin_component.test.ts b/public/app/angular/components/plugin_component.test.ts new file mode 100644 index 00000000000..e5205751fc6 --- /dev/null +++ b/public/app/angular/components/plugin_component.test.ts @@ -0,0 +1,18 @@ +import { config } from '@grafana/runtime'; + +import { relativeTemplateUrlToCDN } from './plugin_component'; + +describe('Plugin Component', () => { + describe('relativeTemplateUrlToCDN()', () => { + it('should create a proper path', () => { + config.pluginsCDNBaseURL = 'http://my-host.com'; + + const templateUrl = 'partials/module.html'; + const baseUrl = 'plugin-cdn/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel'; + const expectedUrl = + 'http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/partials/module.html'; + + expect(relativeTemplateUrlToCDN(templateUrl, baseUrl)).toBe(expectedUrl); + }); + }); +}); diff --git a/public/app/angular/components/plugin_component.ts b/public/app/angular/components/plugin_component.ts index f8271c41cec..a22b122c31c 100644 --- a/public/app/angular/components/plugin_component.ts +++ b/public/app/angular/components/plugin_component.ts @@ -8,6 +8,20 @@ import config from 'app/core/config'; import { importPanelPlugin } from '../../features/plugins/importPanelPlugin'; import { importDataSourcePlugin, importAppPlugin } from '../../features/plugins/plugin_loader'; +export function relativeTemplateUrlToCDN(templateUrl: string, baseUrl: string) { + if (!templateUrl) { + return undefined; + } + + // the templateUrl may have already been updated with the hostname + if (templateUrl.startsWith(config.pluginsCDNBaseURL)) { + return templateUrl; + } + + // use the 'plugin-cdn' key to load via cdn + return `${baseUrl.replace('plugin-cdn/', `${config.pluginsCDNBaseURL}/`)}/${templateUrl}`; +} + coreModule.directive('pluginComponent', ['$compile', '$http', '$templateCache', '$location', pluginDirectiveLoader]); function pluginDirectiveLoader($compile: any, $http: any, $templateCache: any, $location: ILocationService) { @@ -31,12 +45,17 @@ function pluginDirectiveLoader($compile: any, $http: any, $templateCache: any, $ if (templateUrl.indexOf('public') === 0) { return templateUrl; } + return baseUrl + '/' + templateUrl; } function getPluginComponentDirective(options: any) { - // handle relative template urls for plugin templates - options.Component.templateUrl = relativeTemplateUrlToAbs(options.Component.templateUrl, options.baseUrl); + if (options.baseUrl.includes('plugin-cdn')) { + options.Component.templateUrl = relativeTemplateUrlToCDN(options.Component.templateUrl, options.baseUrl); + } else { + // handle relative template urls for plugin templates + options.Component.templateUrl = relativeTemplateUrlToAbs(options.Component.templateUrl, options.baseUrl); + } return () => { return { @@ -86,13 +105,17 @@ function pluginDirectiveLoader($compile: any, $http: any, $templateCache: any, $ } if (panelInfo) { - PanelCtrl.templateUrl = relativeTemplateUrlToAbs(PanelCtrl.templateUrl, panelInfo.baseUrl); + if (panelInfo.baseUrl.includes('plugin-cdn')) { + PanelCtrl.templateUrl = relativeTemplateUrlToCDN(PanelCtrl.templateUrl, panelInfo.baseUrl); + } else { + PanelCtrl.templateUrl = relativeTemplateUrlToAbs(PanelCtrl.templateUrl, panelInfo.baseUrl); + } } PanelCtrl.templatePromise = getTemplate(PanelCtrl).then((template: any) => { PanelCtrl.templateUrl = null; PanelCtrl.template = `${template}`; - return componentInfo; + return { ...componentInfo, baseUrl: panelInfo.baseUrl }; }); return PanelCtrl.templatePromise; diff --git a/public/app/features/plugins/admin/state/actions.ts b/public/app/features/plugins/admin/state/actions.ts index 3b8af6a23d2..9eb4d9003aa 100644 --- a/public/app/features/plugins/admin/state/actions.ts +++ b/public/app/features/plugins/admin/state/actions.ts @@ -5,7 +5,7 @@ import { getBackendSrv, isFetchError } from '@grafana/runtime'; import { importPanelPlugin } from 'app/features/plugins/importPanelPlugin'; import { StoreState, ThunkResult } from 'app/types'; -import { invalidatePluginInCache } from '../../pluginCacheBuster'; +import { invalidatePluginInCache } from '../../systemjsPlugins/pluginCacheBuster'; import { getRemotePlugins, getPluginErrors, diff --git a/public/app/features/plugins/plugin_loader.ts b/public/app/features/plugins/plugin_loader.ts index 63b933f302e..05a92288f8d 100644 --- a/public/app/features/plugins/plugin_loader.ts +++ b/public/app/features/plugins/plugin_loader.ts @@ -32,7 +32,9 @@ import * as ticks from 'app/core/utils/ticks'; import { GenericDataSourcePlugin } from '../datasources/types'; import builtInPlugins from './built_in_plugins'; -import { locateWithCache, registerPluginInCache } from './pluginCacheBuster'; +import { locateFromCDN, translateForCDN } from './systemjsPlugins/pluginCDN'; +import { fetchCSS, locateCSS } from './systemjsPlugins/pluginCSS'; +import { locateWithCache, registerPluginInCache } from './systemjsPlugins/pluginCacheBuster'; // Help the 6.4 to 6.5 migration // The base classes were moved from @grafana/ui to @grafana/data @@ -43,7 +45,12 @@ grafanaUI.DataSourcePlugin = grafanaData.DataSourcePlugin; grafanaUI.AppPlugin = grafanaData.AppPlugin; grafanaUI.DataSourceApi = grafanaData.DataSourceApi; +grafanaRuntime.SystemJS.registry.set('css', grafanaRuntime.SystemJS.newModule({ locate: locateCSS, fetch: fetchCSS })); grafanaRuntime.SystemJS.registry.set('plugin-loader', grafanaRuntime.SystemJS.newModule({ locate: locateWithCache })); +grafanaRuntime.SystemJS.registry.set( + 'cdn-loader', + grafanaRuntime.SystemJS.newModule({ locate: locateFromCDN, translate: translateForCDN }) +); grafanaRuntime.SystemJS.config({ baseURL: 'public', @@ -52,10 +59,12 @@ grafanaRuntime.SystemJS.config({ plugins: { defaultExtension: 'js', }, + 'plugin-cdn': { + defaultExtension: 'js', + }, }, map: { text: 'vendor/plugin-text/text.js', - css: 'vendor/plugin-css/css.js', }, meta: { '/*': { @@ -63,6 +72,14 @@ grafanaRuntime.SystemJS.config({ authorization: true, loader: 'plugin-loader', }, + '*.css': { + loader: 'css', + }, + 'plugin-cdn/*': { + esModule: true, + authorization: false, + loader: 'cdn-loader', + }, }, }); diff --git a/public/app/features/plugins/systemjsPlugins/pluginCDN.test.ts b/public/app/features/plugins/systemjsPlugins/pluginCDN.test.ts new file mode 100644 index 00000000000..6abb0699e7d --- /dev/null +++ b/public/app/features/plugins/systemjsPlugins/pluginCDN.test.ts @@ -0,0 +1,105 @@ +import { config } from '@grafana/runtime'; + +import { translateForCDN, extractPluginNameVersionFromUrl } from './pluginCDN'; +describe('Plugin CDN', () => { + describe('translateForCDN', () => { + const load = { + name: 'http://localhost:3000/public/plugin-cdn/grafana-worldmap-panel/0.3.3/grafana-worldmap-panel/module.js', + address: 'http://my-host.com/grafana-worldmap-panel/0.3.3/grafana-worldmap-panel/module.js', + source: 'public/plugins/grafana-worldmap-panel/template.html', + metadata: { + extension: '', + deps: [], + format: 'amd', + loader: 'cdn-loader', + encapsulateGlobal: false, + cjsRequireDetection: true, + cjsDeferDepsExecute: false, + esModule: true, + authorization: false, + }, + }; + config.pluginsCDNBaseURL = 'http://my-host.com'; + + it('should update the default local path to use the CDN path', () => { + const translatedLoad = translateForCDN({ + ...load, + source: 'public/plugins/grafana-worldmap-panel/template.html', + }); + expect(translatedLoad).toBe( + 'http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/template.html' + ); + }); + + it('should replace the default path in a multi-line source code', () => { + const source = ` + const a = "public/plugins/grafana-worldmap-panel/template.html"; + const img = ""; + `; + const expectedSource = ` + const a = "http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/template.html"; + const img = ""; + `; + const translatedLoad = translateForCDN({ ...load, source }); + expect(translatedLoad).toBe(expectedSource); + }); + + it('should cater for local paths starting with a slash', () => { + const source = ` + const a = "/public/plugins/grafana-worldmap-panel/template.html"; + const img = ""; + `; + const expectedSource = ` + const a = "http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/template.html"; + const img = ""; + `; + const translatedLoad = translateForCDN({ ...load, source }); + expect(translatedLoad).toBe(expectedSource); + }); + + it('should cater for a particular path', () => { + const source = ` + .getJSON( + "public/plugins/grafana-worldmap-panel/data/" + + this.panel.locationData + + ".json" + ) + `; + const expectedSource = ` + .getJSON( + "http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/data/" + + this.panel.locationData + + ".json" + ) + `; + const translatedLoad = translateForCDN({ ...load, source }); + expect(translatedLoad).toBe(expectedSource); + }); + + it('should replace sourcemap locations', () => { + const source = ` + Zn(t,e)},t.Rectangle=ui,t.rectangle=function(t,e){return new ui(t,e)},t.Map=He,t.map=function(t,e){return new He(t,e)}}(e)}])}); + //# sourceMappingURL=module.js.map + `; + const expectedSource = ` + Zn(t,e)},t.Rectangle=ui,t.rectangle=function(t,e){return new ui(t,e)},t.Map=He,t.map=function(t,e){return new He(t,e)}}(e)}])}); + //# sourceMappingURL=http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/module.js.map + `; + const translatedLoad = translateForCDN({ ...load, source }); + expect(translatedLoad).toBe(expectedSource); + }); + }); + + describe('extractPluginNameVersionFromUrl', () => { + it('should extract the plugin name and version from a path', () => { + const source = + 'http://localhost:3000/public/plugin-cdn/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/module.js'; + const expected = { + name: 'grafana-worldmap-panel', + version: '0.3.3', + }; + const expectedExtractedPluginDeets = extractPluginNameVersionFromUrl(source); + expect(expectedExtractedPluginDeets).toEqual(expected); + }); + }); +}); diff --git a/public/app/features/plugins/systemjsPlugins/pluginCDN.ts b/public/app/features/plugins/systemjsPlugins/pluginCDN.ts new file mode 100644 index 00000000000..22e33f0e1ef --- /dev/null +++ b/public/app/features/plugins/systemjsPlugins/pluginCDN.ts @@ -0,0 +1,29 @@ +import { config } from '@grafana/runtime'; + +import type { SystemJSLoad } from './types'; + +export function extractPluginNameVersionFromUrl(address: string) { + const path = new URL(address).pathname; + const match = path.split('/'); + return { name: match[3], version: match[4] }; +} + +export function locateFromCDN(load: SystemJSLoad) { + const { address } = load; + const pluginPath = address.split('/public/plugin-cdn/'); + return `${config.pluginsCDNBaseURL}/${pluginPath[1]}`; +} + +export function translateForCDN(load: SystemJSLoad) { + const { name, version } = extractPluginNameVersionFromUrl(load.name); + const baseAddress = `${config.pluginsCDNBaseURL}/${name}/${version}`; + + load.source = load.source.replace(/(\/?)(public\/plugins)/g, `${baseAddress}/$2`); + load.source = load.source.replace(/(["|'])(plugins\/.+.css)(["|'])/g, `$1${baseAddress}/public/$2$3`); + load.source = load.source.replace( + /(\/\/#\ssourceMappingURL=)(.+)\.map/g, + `$1${baseAddress}/public/plugins/${name}/$2.map` + ); + + return load.source; +} diff --git a/public/app/features/plugins/systemjsPlugins/pluginCSS.ts b/public/app/features/plugins/systemjsPlugins/pluginCSS.ts new file mode 100644 index 00000000000..a97002ab8ed --- /dev/null +++ b/public/app/features/plugins/systemjsPlugins/pluginCSS.ts @@ -0,0 +1,75 @@ +import { noop } from 'lodash'; + +import { config } from '@grafana/runtime'; + +import type { SystemJSLoad } from './types'; + +/* + Locate: Overrides the location of the plugin resource + Plugins that import css use relative paths in Systemjs.register dependency list. + Rather than attempt to resolve it in the pluginCDN systemjs plugin let SystemJS resolve it to origin + then we can replace the "baseUrl" with the "cdnHost". + */ +export function locateCSS(load: SystemJSLoad) { + if (load.metadata.loader === 'cdn-loader' && load.address.startsWith(`${location.origin}/public/plugin-cdn`)) { + load.address = load.address.replace(`${location.origin}/public/plugin-cdn`, config.pluginsCDNBaseURL); + } + return load.address; +} + +/* + Fetch: Called with second argument representing default fetch function, has full control of fetch output. + Plugins that have external CSS will use this plugin to load their custom styles +*/ +export function fetchCSS(load: SystemJSLoad) { + const links = document.getElementsByTagName('link'); + const linkHrefs: string[] = Array.from(links).map((link) => link.href); + + // dont reload styles loaded in the head + if (linkHrefs.includes(load.address)) { + return ''; + } + + return loadCSS(load.address); +} + +const bust = '?_cache=' + Date.now(); +const waitSeconds = 100; + +function loadCSS(url: string) { + return new Promise(function (resolve, reject) { + const timeout = setTimeout(function () { + reject('Unable to load CSS'); + }, waitSeconds * 1000); + const _callback = function (error?: string | Error) { + clearTimeout(timeout); + link.onload = link.onerror = noop; + setTimeout(function () { + if (error) { + reject(error); + } else { + resolve(''); + } + }, 7); + }; + const link = document.createElement('link'); + link.type = 'text/css'; + link.rel = 'stylesheet'; + link.href = url; + + // Don't cache bust plugins loaded from cdn. + if (!link.href.startsWith(config.pluginsCDNBaseURL)) { + link.href = link.href + bust; + } + + link.onload = function () { + _callback(); + }; + + link.onerror = function (event) { + _callback(event instanceof ErrorEvent ? event.message : new Error('Error loading CSS file.')); + }; + + document.head.appendChild(link); + }); +} diff --git a/public/app/features/plugins/tests/pluginCacheBuster.test.ts b/public/app/features/plugins/systemjsPlugins/pluginCacheBuster.test.ts similarity index 98% rename from public/app/features/plugins/tests/pluginCacheBuster.test.ts rename to public/app/features/plugins/systemjsPlugins/pluginCacheBuster.test.ts index f263cbd3d04..c8d046bcaa6 100644 --- a/public/app/features/plugins/tests/pluginCacheBuster.test.ts +++ b/public/app/features/plugins/systemjsPlugins/pluginCacheBuster.test.ts @@ -1,6 +1,7 @@ -import { invalidatePluginInCache, locateWithCache, registerPluginInCache } from '../pluginCacheBuster'; import * as pluginSettings from '../pluginSettings'; +import { invalidatePluginInCache, locateWithCache, registerPluginInCache } from './pluginCacheBuster'; + describe('PluginCacheBuster', () => { const now = 12345; diff --git a/public/app/features/plugins/pluginCacheBuster.ts b/public/app/features/plugins/systemjsPlugins/pluginCacheBuster.ts similarity index 94% rename from public/app/features/plugins/pluginCacheBuster.ts rename to public/app/features/plugins/systemjsPlugins/pluginCacheBuster.ts index 5df396c1562..af18eb43dd8 100644 --- a/public/app/features/plugins/pluginCacheBuster.ts +++ b/public/app/features/plugins/systemjsPlugins/pluginCacheBuster.ts @@ -1,4 +1,4 @@ -import { clearPluginSettingsCache } from './pluginSettings'; +import { clearPluginSettingsCache } from '../pluginSettings'; const cache: Record = {}; const initializedAt: number = Date.now(); diff --git a/public/app/features/plugins/systemjsPlugins/types.ts b/public/app/features/plugins/systemjsPlugins/types.ts new file mode 100644 index 00000000000..3a7356084d0 --- /dev/null +++ b/public/app/features/plugins/systemjsPlugins/types.ts @@ -0,0 +1,16 @@ +export type SystemJSLoad = { + address: string; + metadata: { + authorization: boolean; + cjsDeferDepsExecute: boolean; + cjsRequireDetection: boolean; + crossOrigin?: boolean; + encapsulateGlobal: boolean; + esModule: boolean; + integrity?: string; + loader: string; + scriptLoad?: boolean; + }; + name: string; + source: string; +}; diff --git a/public/vendor/plugin-css/css.js b/public/vendor/plugin-css/css.js deleted file mode 100644 index 09f28d23b3a..00000000000 --- a/public/vendor/plugin-css/css.js +++ /dev/null @@ -1,73 +0,0 @@ -"use strict"; - -if (typeof window !== 'undefined') { - var bust = '?_cache=' + Date.now(); - var waitSeconds = 100; - - var head = document.getElementsByTagName('head')[0]; - - // get all link tags in the page - var links = document.getElementsByTagName('link'); - var linkHrefs = []; - for (var i = 0; i < links.length; i++) { - linkHrefs.push(links[i].href); - } - - var isWebkit = !!window.navigator.userAgent.match(/AppleWebKit\/([^ ;]*)/); - var webkitLoadCheck = function (link, callback) { - setTimeout(function () { - for (var i = 0; i < document.styleSheets.length; i++) { - var sheet = document.styleSheets[i]; - if (sheet.href === link.href) { - return callback(); - } - } - webkitLoadCheck(link, callback); - }, 10); - }; - - var noop = function () { }; - - var loadCSS = function (url) { - return new Promise(function (resolve, reject) { - var timeout = setTimeout(function () { - reject('Unable to load CSS'); - }, waitSeconds * 1000); - var _callback = function (error) { - clearTimeout(timeout); - link.onload = link.onerror = noop; - setTimeout(function () { - if (error) { - reject(error); - } - else { - resolve(''); - } - }, 7); - }; - var link = document.createElement('link'); - link.type = 'text/css'; - link.rel = 'stylesheet'; - link.href = url + bust; - if (!isWebkit) { - link.onload = function () { - _callback(); - } - } else { - webkitLoadCheck(link, _callback); - } - link.onerror = function (event) { - _callback(event.error || new Error('Error loading CSS file.')); - }; - head.appendChild(link); - }); - }; - - exports.fetch = function (load) { - // dont reload styles loaded in the head - for (var i = 0; i < linkHrefs.length; i++) - if (load.address == linkHrefs[i]) - return ''; - return loadCSS(load.address); - }; -} From 7c02d9bb8a864aab35c676f1a44c6c5be9eb6c09 Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Fri, 27 Jan 2023 15:12:01 +0100 Subject: [PATCH 040/117] Logs: Add experimental support to display a datasource custom UI in LogContext (#62189) * add loki contextfilter component * add `getLogRowContextUi` support to DataSourceAPI * add `runContextQuery` to LogRowContextProvider * pass `getRowContextUi` to `LogRowContext` * adapt LogRowContext to show datasource ui * implement LogRowContextUi in Loki * add `logsContextDatasourceUi` feature flag * change state to `Alpha` * disable the feature if `logsContextDatasourceUi` is not set * don't fetch labels in the constructor * adjust to right height * remove unnecessary eslint disable * add test for LokiContextUi * move code down in datasource.ts * rename `refresh` to `runContextQuery` * update datasource tests * don't update if `updateFilter` fn changes * organized imports in datasource.test.ts * don't trigger on intialization changes * change tag to `experimental` * move `getLogRowContextUi` to props --- .betterer.results | 8 +- .../feature-toggles/index.md | 1 + .../src/types/featureToggles.gen.ts | 1 + packages/grafana-data/src/types/logs.ts | 17 ++ pkg/services/featuremgmt/registry.go | 6 + pkg/services/featuremgmt/toggles_gen.go | 4 + public/app/features/explore/Logs.tsx | 7 +- public/app/features/explore/LogsContainer.tsx | 12 ++ .../app/features/logs/components/LogRow.tsx | 26 ++- .../logs/components/LogRowContext.tsx | 120 +++++++++--- .../logs/components/LogRowContextProvider.tsx | 63 ++++--- .../logs/components/LogRowMessage.tsx | 15 +- .../app/features/logs/components/LogRows.tsx | 5 + .../loki/components/LokiContextUi.test.tsx | 116 ++++++++++++ .../loki/components/LokiContextUi.tsx | 178 ++++++++++++++++++ .../datasource/loki/datasource.test.ts | 54 ++++++ .../app/plugins/datasource/loki/datasource.ts | 104 ++++++++-- public/app/plugins/datasource/loki/types.ts | 8 + 18 files changed, 672 insertions(+), 73 deletions(-) create mode 100644 public/app/plugins/datasource/loki/components/LokiContextUi.test.tsx create mode 100644 public/app/plugins/datasource/loki/components/LokiContextUi.tsx diff --git a/.betterer.results b/.betterer.results index 2a789546b7a..ecfdfcd3111 100644 --- a/.betterer.results +++ b/.betterer.results @@ -523,7 +523,8 @@ exports[`better eslint`] = { ], "packages/grafana-data/src/types/logs.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"] + [0, 0, 0, "Do not use any type assertions.", "1"], + [0, 0, 0, "Do not use any type assertions.", "2"] ], "packages/grafana-data/src/types/logsVolume.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] @@ -6097,9 +6098,10 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "2"], [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], + [0, 0, 0, "Do not use any type assertions.", "4"], [0, 0, 0, "Unexpected any. Specify a different type.", "5"], - [0, 0, 0, "Unexpected any. Specify a different type.", "6"] + [0, 0, 0, "Unexpected any. Specify a different type.", "6"], + [0, 0, 0, "Unexpected any. Specify a different type.", "7"] ], "public/app/plugins/datasource/loki/getDerivedFields.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 783c9be869f..d67f0128b15 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -98,6 +98,7 @@ Alpha features might be changed or removed without prior notice. | `alertingBacktesting` | Rule backtesting API for alerting | | `editPanelCSVDragAndDrop` | Enables drag and drop for CSV and Excel files | | `azureMultipleResourcePicker` | Azure multiple resource picker | +| `logsContextDatasourceUi` | Allow datasource to provide custom UI for context view | ## Development feature toggles diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index aaf4f957601..83cbe0d8cc7 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -92,4 +92,5 @@ export interface FeatureToggles { alertingNoNormalState?: boolean; azureMultipleResourcePicker?: boolean; topNavCommandPalette?: boolean; + logsContextDatasourceUi?: boolean; } diff --git a/packages/grafana-data/src/types/logs.ts b/packages/grafana-data/src/types/logs.ts index 6469be8404a..6d33e89db4d 100644 --- a/packages/grafana-data/src/types/logs.ts +++ b/packages/grafana-data/src/types/logs.ts @@ -167,6 +167,13 @@ export interface DataSourceWithLogsContextSupport void): React.ReactNode; } export const hasLogsContextSupport = (datasource: unknown): datasource is DataSourceWithLogsContextSupport => { @@ -230,3 +237,13 @@ export const hasSupplementaryQuerySupport = ( withSupplementaryQueriesSupport.getSupportedSupplementaryQueryTypes().includes(type) ); }; + +export const hasLogsContextUiSupport = (datasource: unknown): datasource is DataSourceWithLogsContextSupport => { + if (!datasource) { + return false; + } + + const withLogsSupport = datasource as DataSourceWithLogsContextSupport; + + return withLogsSupport.getLogRowContextUi !== undefined; +}; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 46df1ffbd58..72602b93540 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -426,5 +426,11 @@ var ( State: FeatureStateBeta, FrontendOnly: true, }, + { + Name: "logsContextDatasourceUi", + Description: "Allow datasource to provide custom UI for context view", + State: FeatureStateAlpha, + FrontendOnly: true, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 825eba4df39..456ce5e8f3e 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -310,4 +310,8 @@ const ( // FlagTopNavCommandPalette // Launch the Command Palette from the top navigation search box FlagTopNavCommandPalette = "topNavCommandPalette" + + // FlagLogsContextDatasourceUi + // Allow datasource to provide custom UI for context view + FlagLogsContextDatasourceUi = "logsContextDatasourceUi" ) diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index c5a620d4c04..96de60d9a3e 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -25,6 +25,7 @@ import { DataHoverEvent, DataHoverClearEvent, EventBus, + DataSourceWithLogsContextSupport, } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; @@ -79,6 +80,7 @@ interface Props extends Themeable2 { onStartScanning?: () => void; onStopScanning?: () => void; getRowContext?: (row: LogRowModel, options?: RowContextOptions) => Promise; + getLogRowContextUi?: DataSourceWithLogsContextSupport['getLogRowContextUi']; getFieldLinks: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array>; addResultsToCache: () => void; clearCache: () => void; @@ -344,6 +346,8 @@ class UnthemedLogs extends PureComponent { addResultsToCache, exploreId, scrollElement, + getRowContext, + getLogRowContextUi, } = this.props; const { @@ -487,7 +491,8 @@ class UnthemedLogs extends PureComponent { logRows={logRows} deduplicatedRows={dedupedRows} dedupStrategy={dedupStrategy} - getRowContext={this.props.getRowContext} + getRowContext={getRowContext} + getLogRowContextUi={getLogRowContextUi} onClickFilterLabel={onClickFilterLabel} onClickFilterOutLabel={onClickFilterOutLabel} showContextToggle={showContextToggle} diff --git a/public/app/features/explore/LogsContainer.tsx b/public/app/features/explore/LogsContainer.tsx index c34c34e9051..2ecaa763537 100644 --- a/public/app/features/explore/LogsContainer.tsx +++ b/public/app/features/explore/LogsContainer.tsx @@ -5,6 +5,7 @@ import { AbsoluteTimeRange, Field, hasLogsContextSupport, + hasLogsContextUiSupport, LoadingState, LogRowModel, RawTimeRange, @@ -63,6 +64,16 @@ class LogsContainer extends PureComponent { return []; }; + getLogRowContextUi = (row: LogRowModel, runContextQuery?: () => void): React.ReactNode => { + const { datasourceInstance } = this.props; + + if (hasLogsContextUiSupport(datasourceInstance) && datasourceInstance.getLogRowContextUi) { + return datasourceInstance.getLogRowContextUi(row, runContextQuery); + } + + return <>; + }; + showContextToggle = (row?: LogRowModel): boolean => { const { datasourceInstance } = this.props; @@ -159,6 +170,7 @@ class LogsContainer extends PureComponent { scanRange={range.raw} showContextToggle={this.showContextToggle} getRowContext={this.getLogRowContext} + getLogRowContextUi={this.getLogRowContextUi} getFieldLinks={this.getFieldLinks} addResultsToCache={() => addResultsToCache(exploreId)} clearCache={() => clearCache(exploreId)} diff --git a/public/app/features/logs/components/LogRow.tsx b/public/app/features/logs/components/LogRow.tsx index d4b44d4c0f2..ac0813f6b6d 100644 --- a/public/app/features/logs/components/LogRow.tsx +++ b/public/app/features/logs/components/LogRow.tsx @@ -12,6 +12,7 @@ import { GrafanaTheme2, CoreApp, DataFrame, + DataSourceWithLogsContextSupport, } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; import { styleMixins, withTheme2, Themeable2, Icon, Tooltip } from '@grafana/ui'; @@ -53,6 +54,7 @@ interface Props extends Themeable2 { onClickFilterOutLabel?: (key: string, value: string) => void; onContextClick?: () => void; getRowContext: (row: LogRowModel, options?: RowContextOptions) => Promise; + getLogRowContextUi?: (row: LogRowModel) => React.ReactNode; getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array>; showContextToggle?: (row?: LogRowModel) => boolean; onClickShowField?: (key: string) => void; @@ -143,7 +145,9 @@ class UnThemedLogRow extends PureComponent { errors?: LogRowContextQueryErrors, hasMoreContextRows?: HasMoreContextRows, updateLimit?: () => void, - logsSortOrder?: LogsSortOrder | null + logsSortOrder?: LogsSortOrder | null, + getLogRowContextUi?: DataSourceWithLogsContextSupport['getLogRowContextUi'], + runContextQuery?: () => void ) { const { getRows, @@ -230,6 +234,8 @@ class UnThemedLogRow extends PureComponent { getRows={getRows} errors={errors} hasMoreContextRows={hasMoreContextRows} + getLogRowContextUi={getLogRowContextUi} + runContextQuery={runContextQuery} updateLimit={updateLimit} context={context} contextIsOpen={showContext} @@ -267,14 +273,26 @@ class UnThemedLogRow extends PureComponent { render() { const { showContext } = this.state; - const { logsSortOrder, row, getRowContext } = this.props; + const { logsSortOrder, row, getRowContext, getLogRowContextUi } = this.props; if (showContext) { return ( <> - {({ result, errors, hasMoreContextRows, updateLimit, logsSortOrder }) => { - return <>{this.renderLogRow(result, errors, hasMoreContextRows, updateLimit, logsSortOrder)}; + {({ result, errors, hasMoreContextRows, updateLimit, runContextQuery, logsSortOrder }) => { + return ( + <> + {this.renderLogRow( + result, + errors, + hasMoreContextRows, + updateLimit, + logsSortOrder, + getLogRowContextUi, + runContextQuery + )} + + ); }} diff --git a/public/app/features/logs/components/LogRowContext.tsx b/public/app/features/logs/components/LogRowContext.tsx index 68383069231..1ba24038979 100644 --- a/public/app/features/logs/components/LogRowContext.tsx +++ b/public/app/features/logs/components/LogRowContext.tsx @@ -1,8 +1,16 @@ import { css, cx } from '@emotion/css'; -import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; +import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import usePrevious from 'react-use/lib/usePrevious'; -import { DataQueryError, GrafanaTheme2, LogRowModel, LogsSortOrder, textUtil } from '@grafana/data'; +import { + DataQueryError, + GrafanaTheme2, + LogRowModel, + LogsSortOrder, + textUtil, + DataSourceWithLogsContextSupport, +} from '@grafana/data'; +import { config } from '@grafana/runtime'; import { Alert, Button, ClickOutsideWrapper, CustomScrollbar, IconButton, List, useStyles2 } from '@grafana/ui'; import { LogMessageAnsi } from './LogMessageAnsi'; @@ -22,9 +30,16 @@ interface LogRowContextProps { logsSortOrder?: LogsSortOrder | null; onOutsideClick: (method: string) => void; onLoadMoreContext: () => void; + runContextQuery?: () => void; + getLogRowContextUi?: DataSourceWithLogsContextSupport['getLogRowContextUi']; } -const getLogRowContextStyles = (theme: GrafanaTheme2, wrapLogMessage?: boolean) => { +const getLogRowContextStyles = (theme: GrafanaTheme2, wrapLogMessage?: boolean, datasourceUiHeight?: number) => { + if (config.featureToggles.logsContextDatasourceUi) { + datasourceUiHeight = datasourceUiHeight ?? 55; + } else { + datasourceUiHeight = 0; + } /** * This is workaround for displaying uncropped context when we have unwrapping log messages. * We are using margins to correctly position context. Because non-wrapped logs have always 1 line of log @@ -34,7 +49,8 @@ const getLogRowContextStyles = (theme: GrafanaTheme2, wrapLogMessage?: boolean) const headerHeight = 40; const logsHeight = 220; - const contextHeight = headerHeight + logsHeight; + const contextHeight = datasourceUiHeight + headerHeight + logsHeight; + const bottomContextHeight = headerHeight + logsHeight; const width = wrapLogMessage ? '100%' : '75%'; const afterContext = wrapLogMessage ? css` @@ -55,6 +71,9 @@ const getLogRowContextStyles = (theme: GrafanaTheme2, wrapLogMessage?: boolean) width: css` width: ${width}; `, + bottomContext: css` + height: ${bottomContextHeight}px; + `, commonStyles: css` position: absolute; height: ${contextHeight}px; @@ -73,6 +92,13 @@ const getLogRowContextStyles = (theme: GrafanaTheme2, wrapLogMessage?: boolean) align-items: center; background: ${theme.colors.background.canvas}; `, + datasourceUi: css` + height: ${datasourceUiHeight}px; + padding: ${theme.spacing(0, 1.25)}; + display: flex; + align-items: center; + background: ${theme.colors.background.canvas}; + `, top: css` border-radius: 0 0 ${theme.shape.borderRadius(2)} ${theme.shape.borderRadius(2)}; box-shadow: 0 0 ${theme.spacing(1.25)} ${theme.v1.palette.black}; @@ -132,13 +158,14 @@ interface LogRowContextGroupHeaderProps { shouldScrollToBottom?: boolean; canLoadMoreRows?: boolean; logsSortOrder?: LogsSortOrder | null; + getLogRowContextUi?: DataSourceWithLogsContextSupport['getLogRowContextUi']; + runContextQuery?: () => void; } interface LogRowContextGroupProps extends LogRowContextGroupHeaderProps { rows: Array; groupPosition: LogGroupPosition; className?: string; error?: string; - logsSortOrder?: LogsSortOrder | null; } const LogRowContextGroupHeader: React.FunctionComponent = ({ @@ -148,8 +175,16 @@ const LogRowContextGroupHeader: React.FunctionComponent { - const { header, headerButton } = useStyles2(getLogRowContextStyles); + const [height, setHeight] = useState(50); + const datasourceUiRef = React.createRef(); + const { + datasourceUi: dsUi, + header, + headerButton, + } = useStyles2((theme) => getLogRowContextStyles(theme, undefined, height)); // determine the position in time for this LogGroup by taking the ordering of // logs and position of the component itself into account. @@ -162,21 +197,56 @@ const LogRowContextGroupHeader: React.FunctionComponent + new ResizeObserver((entries) => { + for (let entry of entries) { + setHeight(entry.contentRect.height); + } + }), + [] + ); + + // eslint-disable-next-line react-hooks/rules-of-hooks + useLayoutEffect(() => { + // observe the first child of the ref, which is the datasource controlled component and varies in height + // TODO: this is a bit of a hack and we can remove this as soon as we move back from the absolute positioned context + const child = datasourceUiRef.current?.children.item(0); + if (child) { + resizeObserver.observe(child); + } + return () => { + resizeObserver.disconnect(); + }; + }, [datasourceUiRef, resizeObserver]); + } + return ( -
- - Showing {rows.length} lines {logGroupPosition} match. - - {(rows.length >= 10 || (rows.length > 10 && rows.length % 10 !== 0)) && canLoadMoreRows && ( - + <> + {config.featureToggles.logsContextDatasourceUi && getLogRowContextUi && ( +
+ {getLogRowContextUi(row, runContextQuery)} +
)} -
+
+ + Showing {rows.length} lines {logGroupPosition} match. + + {(rows.length >= 10 || (rows.length > 10 && rows.length % 10 !== 0)) && canLoadMoreRows && ( + + )} +
+ ); }; @@ -190,8 +260,10 @@ export const LogRowContextGroup: React.FunctionComponent { - const { commonStyles, logs } = useStyles2(getLogRowContextStyles); + const { commonStyles, logs, bottomContext } = useStyles2(getLogRowContextStyles); const [scrollTop, setScrollTop] = useState(0); const [scrollHeight, setScrollHeight] = useState(0); @@ -243,10 +315,12 @@ export const LogRowContextGroup: React.FunctionComponent +
{/* When displaying "after" context */} {shouldScrollToBottom && !error && }
@@ -284,9 +358,11 @@ export const LogRowContext: React.FunctionComponent = ({ errors, onOutsideClick, onLoadMoreContext, + runContextQuery: runContextQuery, hasMoreContextRows, wrapLogMessage, logsSortOrder, + getLogRowContextUi, }) => { useEffect(() => { const handleEscKeyDown = (e: KeyboardEvent): void => { @@ -321,6 +397,8 @@ export const LogRowContext: React.FunctionComponent = ({ onLoadMoreContext={onLoadMoreContext} groupPosition={LogGroupPosition.Top} logsSortOrder={logsSortOrder} + getLogRowContextUi={getLogRowContextUi} + runContextQuery={runContextQuery} /> )} diff --git a/public/app/features/logs/components/LogRowContextProvider.tsx b/public/app/features/logs/components/LogRowContextProvider.tsx index 814ffb632d4..e79d9476903 100644 --- a/public/app/features/logs/components/LogRowContextProvider.tsx +++ b/public/app/features/logs/components/LogRowContextProvider.tsx @@ -34,6 +34,7 @@ export interface HasMoreContextRows { interface ResultType { data: string[][]; errors: string[]; + doNotCheckForMore?: boolean; } interface LogRowContextProviderProps { @@ -45,6 +46,7 @@ interface LogRowContextProviderProps { errors: LogRowContextQueryErrors; hasMoreContextRows: HasMoreContextRows; updateLimit: () => void; + runContextQuery: () => void; limit: number; logsSortOrder?: LogsSortOrder | null; }) => JSX.Element; @@ -55,7 +57,7 @@ export const getRowContexts = async ( row: LogRowModel, limit: number, logsSortOrder?: LogsSortOrder | null -) => { +): Promise => { const promises = [ getRowContext(row, { limit, @@ -159,6 +161,8 @@ export const LogRowContextProvider: React.FunctionComponent(); + // React Hook that resolves two promises every time the limit prop changes // First promise fetches limit number of rows backwards in time from a specific point in time // Second promise fetches limit number of rows forwards in time from a specific point in time @@ -166,40 +170,46 @@ export const LogRowContextProvider: React.FunctionComponent { + setResults(value); + }, [value]); + // React Hook that performs a side effect every time the value (from useAsync hook) prop changes // The side effect changes the result state with the response from the useAsync hook // The side effect changes the hasMoreContextRows state if there are more context rows before or after the current result useEffect(() => { - if (value) { + if (results) { setResult((currentResult) => { - let hasMoreLogsBefore = true, - hasMoreLogsAfter = true; + if (!results.doNotCheckForMore) { + let hasMoreLogsBefore = true, + hasMoreLogsAfter = true; - const currentResultBefore = currentResult?.data[0]; - const currentResultAfter = currentResult?.data[1]; - const valueBefore = value.data[0]; - const valueAfter = value.data[1]; + const currentResultBefore = currentResult?.data[0]; + const currentResultAfter = currentResult?.data[1]; + const valueBefore = results.data[0]; + const valueAfter = results.data[1]; - // checks if there are more log rows in a given direction - // if after fetching additional rows the length of result is the same, - // we can assume there are no logs in that direction within a given time range - if (currentResult && (!valueBefore || currentResultBefore.length === valueBefore.length)) { - hasMoreLogsBefore = false; + // checks if there are more log rows in a given direction + // if after fetching additional rows the length of result is the same, + // we can assume there are no logs in that direction within a given time range + if (currentResult && (!valueBefore || currentResultBefore.length === valueBefore.length)) { + hasMoreLogsBefore = false; + } + + if (currentResult && (!valueAfter || currentResultAfter.length === valueAfter.length)) { + hasMoreLogsAfter = false; + } + + setHasMoreContextRows({ + before: hasMoreLogsBefore, + after: hasMoreLogsAfter, + }); } - if (currentResult && (!valueAfter || currentResultAfter.length === valueAfter.length)) { - hasMoreLogsAfter = false; - } - - setHasMoreContextRows({ - before: hasMoreLogsBefore, - after: hasMoreLogsAfter, - }); - - return value; + return results; }); } - }, [value]); + }, [results]); return children({ result: { @@ -221,6 +231,11 @@ export const LogRowContextProvider: React.FunctionComponent { + const results = await getRowContexts(getRowContext, row, limit, logsSortOrder); + results.doNotCheckForMore = true; + setResults(results); + }, limit, logsSortOrder, }); diff --git a/public/app/features/logs/components/LogRowMessage.tsx b/public/app/features/logs/components/LogRowMessage.tsx index e0fd1d69fb7..380e32feb05 100644 --- a/public/app/features/logs/components/LogRowMessage.tsx +++ b/public/app/features/logs/components/LogRowMessage.tsx @@ -4,7 +4,14 @@ import React, { PureComponent } from 'react'; import Highlighter from 'react-highlight-words'; import tinycolor from 'tinycolor2'; -import { LogRowModel, findHighlightChunksInText, GrafanaTheme2, LogsSortOrder, CoreApp } from '@grafana/data'; +import { + LogRowModel, + findHighlightChunksInText, + GrafanaTheme2, + LogsSortOrder, + CoreApp, + DataSourceWithLogsContextSupport, +} from '@grafana/data'; import { withTheme2, Themeable2, IconButton, Tooltip } from '@grafana/ui'; import { LogMessageAnsi } from './LogMessageAnsi'; @@ -26,9 +33,11 @@ interface Props extends Themeable2 { app?: CoreApp; scrollElement?: HTMLDivElement; showContextToggle?: (row?: LogRowModel) => boolean; + getLogRowContextUi?: DataSourceWithLogsContextSupport['getLogRowContextUi']; getRows: () => LogRowModel[]; onToggleContext: (method: string) => void; updateLimit?: () => void; + runContextQuery?: () => void; logsSortOrder?: LogsSortOrder | null; } @@ -154,6 +163,7 @@ class UnThemedLogRowMessage extends PureComponent { errors, hasMoreContextRows, updateLimit, + runContextQuery, context, contextIsOpen, showRowMenu, @@ -163,6 +173,7 @@ class UnThemedLogRowMessage extends PureComponent { app, logsSortOrder, showContextToggle, + getLogRowContextUi, } = this.props; const style = getLogRowStyles(theme, row.logLevel); @@ -191,6 +202,8 @@ class UnThemedLogRowMessage extends PureComponent { {contextIsOpen && context && ( void; onClickFilterOutLabel?: (key: string, value: string) => void; getRowContext?: (row: LogRowModel, options?: RowContextOptions) => Promise; + getLogRowContextUi?: DataSourceWithLogsContextSupport['getLogRowContextUi']; getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array>; onClickShowField?: (key: string) => void; onClickHideField?: (key: string) => void; @@ -128,6 +130,7 @@ class UnThemedLogRows extends PureComponent { onLogRowHover, app, scrollElement, + getLogRowContextUi, } = this.props; const { renderAll, contextIsOpen } = this.state; const { logsRowsTable } = getLogRowStyles(theme); @@ -156,6 +159,7 @@ class UnThemedLogRows extends PureComponent { key={row.uid} getRows={getRows} getRowContext={getRowContext} + getLogRowContextUi={getLogRowContextUi} row={row} showContextToggle={showContextToggle} showRowMenu={!contextIsOpen} @@ -187,6 +191,7 @@ class UnThemedLogRows extends PureComponent { key={row.uid} getRows={getRows} getRowContext={getRowContext} + getLogRowContextUi={getLogRowContextUi} row={row} showContextToggle={showContextToggle} showRowMenu={!contextIsOpen} diff --git a/public/app/plugins/datasource/loki/components/LokiContextUi.test.tsx b/public/app/plugins/datasource/loki/components/LokiContextUi.test.tsx new file mode 100644 index 00000000000..1e730abbfcc --- /dev/null +++ b/public/app/plugins/datasource/loki/components/LokiContextUi.test.tsx @@ -0,0 +1,116 @@ +import { act, render, screen, waitFor } from '@testing-library/react'; +import React from 'react'; +import { selectOptionInTest } from 'test/helpers/selectOptionInTest'; + +import { LogRowModel } from '@grafana/data'; + +import LokiLanguageProvider from '../LanguageProvider'; + +import { LokiContextUi, LokiContextUiProps } from './LokiContextUi'; + +// we have to mock out reportInteraction, otherwise it crashes the test. +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + reportInteraction: () => null, +})); + +describe('LokiContextUi', () => { + const setupProps = (): LokiContextUiProps => { + const mockLanguageProvider = { + start: jest.fn().mockImplementation(() => Promise.resolve()), + getLabelValues: (name: string) => { + switch (name) { + case 'label1': + return ['value1-1', 'value1-2']; + case 'label2': + return ['value2-1', 'value2-2']; + case 'label3': + return ['value3-1', 'value3-2']; + } + return []; + }, + fetchSeriesLabels: (selector: string) => { + switch (selector) { + case '{label1="value1-1"}': + return { label1: ['value1-1'], label2: ['value2-1'], label3: ['value3-1'] }; + case '{label1=~"value1-1|value1-2"}': + return { label1: ['value1-1', 'value1-2'], label2: ['value2-1'], label3: ['value3-1', 'value3-2'] }; + } + // Allow full set by default + return { + label1: ['value1-1', 'value1-2'], + label2: ['value2-1', 'value2-2'], + }; + }, + getLabelKeys: () => ['label1', 'label2'], + }; + + const defaults: LokiContextUiProps = { + languageProvider: mockLanguageProvider as unknown as LokiLanguageProvider, + updateFilter: jest.fn(), + row: { + entry: 'WARN test 1.23 on [xxx]', + labels: { + label1: 'value1', + label3: 'value3', + }, + } as unknown as LogRowModel, + }; + + return defaults; + }; + + it('renders and shows basic text', async () => { + const props = setupProps(); + render(); + + // Initial set of labels is available and not selected + expect(await screen.findByText(/Select labels to include in the context query/)).toBeInTheDocument(); + }); + + it('starts the languageProvider', async () => { + const props = setupProps(); + render(); + + await waitFor(() => { + expect(props.languageProvider.start).toHaveBeenCalled(); + }); + }); + + it('finds label1 as a real label', async () => { + const props = setupProps(); + render(); + await waitFor(() => { + expect(props.languageProvider.start).toHaveBeenCalled(); + }); + const select = await screen.findAllByRole('combobox'); + await selectOptionInTest(select[0], 'label1'); + }); + + it('finds label3 as a parsed label', async () => { + const props = setupProps(); + render(); + await waitFor(() => { + expect(props.languageProvider.start).toHaveBeenCalled(); + }); + const select = await screen.findAllByRole('combobox'); + await selectOptionInTest(select[1], 'label3'); + }); + + it('calls updateFilter when selecting a label', async () => { + jest.useFakeTimers(); + const props = setupProps(); + render(); + await waitFor(() => { + expect(props.languageProvider.start).toHaveBeenCalled(); + }); + const select = await screen.findAllByRole('combobox'); + await selectOptionInTest(select[1], 'label3'); + act(() => { + jest.runAllTimers(); + }); + expect(props.updateFilter).toHaveBeenCalled(); + + jest.useRealTimers(); + }); +}); diff --git a/public/app/plugins/datasource/loki/components/LokiContextUi.tsx b/public/app/plugins/datasource/loki/components/LokiContextUi.tsx new file mode 100644 index 00000000000..3f14392eccc --- /dev/null +++ b/public/app/plugins/datasource/loki/components/LokiContextUi.tsx @@ -0,0 +1,178 @@ +import { css } from '@emotion/css'; +import memoizeOne from 'memoize-one'; +import React, { useEffect, useState } from 'react'; +import { useAsync } from 'react-use'; + +import { GrafanaTheme2, LogRowModel, SelectableValue } from '@grafana/data'; +import { MultiSelect, Tag, Tooltip, useStyles2 } from '@grafana/ui'; + +import LokiLanguageProvider from '../LanguageProvider'; +import { ContextFilter } from '../types'; + +export interface LokiContextUiProps { + languageProvider: LokiLanguageProvider; + row: LogRowModel; + updateFilter: (value: ContextFilter[]) => void; +} + +function getStyles(theme: GrafanaTheme2) { + return { + labels: css` + display: flex; + gap: 2px; + `, + multiSelectWrapper: css` + display: flex; + flex-direction: column; + flex: 1; + margin-top: ${theme.spacing(1)}; + gap: ${theme.spacing(0.5)}; + `, + multiSelect: css` + & .scrollbar-view { + overscroll-behavior: contain; + } + `, + }; +} + +const formatOptionLabel = memoizeOne(({ label, description }: SelectableValue) => ( + + {label} + +)); + +export function LokiContextUi(props: LokiContextUiProps) { + const { row, languageProvider, updateFilter } = props; + const styles = useStyles2(getStyles); + + const [contextFilters, setContextFilters] = useState([]); + const [initialized, setInitialized] = useState(false); + const timerHandle = React.useRef(); + const previousInitialized = React.useRef(false); + useEffect(() => { + if (!initialized) { + return; + } + + // don't trigger if we initialized, this will be the same query anyways. + if (!previousInitialized.current) { + previousInitialized.current = initialized; + return; + } + + if (timerHandle.current) { + clearTimeout(timerHandle.current); + } + timerHandle.current = window.setTimeout(() => { + updateFilter(contextFilters); + }, 1500); + + return () => { + clearTimeout(timerHandle.current); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [contextFilters, initialized]); + + useAsync(async () => { + await languageProvider.start(); + const allLabels = languageProvider.getLabelKeys(); + const contextFilters: ContextFilter[] = []; + + Object.entries(row.labels).forEach(([label, value]) => { + const filter: ContextFilter = { + label, + value: label, // this looks weird in the first place, but we need to set the label as value here + enabled: allLabels.includes(label), + fromParser: !allLabels.includes(label), + description: value, + }; + contextFilters.push(filter); + }); + + setContextFilters(contextFilters); + setInitialized(true); + }); + + const realLabels = contextFilters.filter(({ fromParser }) => !fromParser); + const realLabelsEnabled = realLabels.filter(({ enabled }) => enabled); + + const parsedLabels = contextFilters.filter(({ fromParser }) => fromParser); + const parsedLabelsEnabled = parsedLabels.filter(({ enabled }) => enabled); + + return ( +
+
+ {' '} + + + {' '} + Select labels to include in the context query: +
+
+ { + return setContextFilters( + contextFilters.map((filter) => { + if (filter.fromParser) { + return filter; + } + filter.enabled = keys.some((key) => key.value === filter.value); + return filter; + }) + ); + }} + /> +
+ {parsedLabels.length > 0 && ( +
+ { + setContextFilters( + contextFilters.map((filter) => { + if (!filter.fromParser) { + return filter; + } + filter.enabled = keys.some((key) => key.value === filter.value); + return filter; + }) + ); + }} + /> +
+ )} +
+ ); +} diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index 5e183049033..f24469c5ddb 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -1168,3 +1168,57 @@ function makeAnnotationQueryRequest(options = {}): AnnotationQueryRequest { + it('returns expression with 1 label', async () => { + const ds = createLokiDatasource(templateSrvStub); + + const row: LogRowModel = { + rowIndex: 0, + dataFrame: new MutableDataFrame({ + fields: [ + { + name: 'ts', + type: FieldType.time, + values: [0], + }, + ], + }), + labels: { bar: 'baz', foo: 'uniqueParsedLabel' }, + uid: '1', + } as unknown as LogRowModel; + + jest.spyOn(ds.languageProvider, 'start').mockImplementation(() => Promise.resolve([])); + jest.spyOn(ds.languageProvider, 'getLabelKeys').mockImplementation(() => ['foo']); + + const result = await ds.prepareContextExpr(row); + + expect(result).toEqual('{foo="uniqueParsedLabel"}'); + }); + + it('returns empty expression for parsed labels', async () => { + const ds = createLokiDatasource(templateSrvStub); + + const row: LogRowModel = { + rowIndex: 0, + dataFrame: new MutableDataFrame({ + fields: [ + { + name: 'ts', + type: FieldType.time, + values: [0], + }, + ], + }), + labels: { bar: 'baz', foo: 'uniqueParsedLabel' }, + uid: '1', + } as unknown as LogRowModel; + + jest.spyOn(ds.languageProvider, 'start').mockImplementation(() => Promise.resolve([])); + jest.spyOn(ds.languageProvider, 'getLabelKeys').mockImplementation(() => []); + + const result = await ds.prepareContextExpr(row); + + expect(result).toEqual('{}'); + }); +}); diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 4d28ff4e299..be825cd50a3 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -35,6 +35,7 @@ import { toUtc, } from '@grafana/data'; import { config, DataSourceWithBackend, FetchError } from '@grafana/runtime'; +import { DataQuery } from '@grafana/schema'; import { queryLogsSample, queryLogsVolume } from 'app/core/logsModel'; import { convertToWebSocketUrl } from 'app/core/utils/explore'; import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; @@ -50,6 +51,7 @@ import LanguageProvider from './LanguageProvider'; import { LiveStreams, LokiLiveTarget } from './LiveStreams'; import { transformBackendResult } from './backendResultTransformer'; import { LokiAnnotationsQueryEditor } from './components/AnnotationsQueryEditor'; +import { LokiContextUi } from './components/LokiContextUi'; import { escapeLabelValueInExactSelector, escapeLabelValueInSelector, isRegexSelector } from './languageUtils'; import { labelNamesRegex, labelValuesRegex } from './migrations/variableQueryMigrations'; import { @@ -66,11 +68,18 @@ import { getLabelFilterPositions, } from './modifyQuery'; import { getQueryHints } from './queryHints'; -import { getLogQueryFromMetricsQuery, getNormalizedLokiQuery, isLogsQuery, isValidQuery } from './queryUtils'; +import { + getLogQueryFromMetricsQuery, + getNormalizedLokiQuery, + getParserFromQuery, + isLogsQuery, + isValidQuery, +} from './queryUtils'; import { sortDataFrameByTime } from './sortDataFrame'; import { doLokiChannelStream } from './streaming'; import { trackQuery } from './tracking'; import { + ContextFilter, LokiOptions, LokiQuery, LokiQueryDirection, @@ -594,10 +603,14 @@ export class LokiDatasource return Math.ceil(date.valueOf() * 1e6); } - getLogRowContext = async (row: LogRowModel, options?: RowContextOptions): Promise<{ data: DataFrame[] }> => { + getLogRowContext = async ( + row: LogRowModel, + options?: RowContextOptions, + origQuery?: DataQuery + ): Promise<{ data: DataFrame[] }> => { const direction = (options && options.direction) || 'BACKWARD'; const limit = (options && options.limit) || 10; - const { query, range } = await this.prepareLogRowContextQueryTarget(row, limit, direction); + const { query, range } = await this.prepareLogRowContextQueryTarget(row, limit, direction, origQuery); const processDataFrame = (frame: DataFrame): DataFrame => { // log-row-context requires specific field-names to work, so we set them here: "ts", "line", "id" @@ -663,29 +676,17 @@ export class LokiDatasource prepareLogRowContextQueryTarget = async ( row: LogRowModel, limit: number, - direction: 'BACKWARD' | 'FORWARD' + direction: 'BACKWARD' | 'FORWARD', + origQuery?: DataQuery ): Promise<{ query: LokiQuery; range: TimeRange }> => { - // need to await the languageProvider to be started to have all labels. This call is not blocking after it has been called once. - await this.languageProvider.start(); - const labels = this.languageProvider.getLabelKeys(); - const expr = Object.keys(row.labels) - .map((label: string) => { - if (labels.includes(label)) { - // escape backslashes in label as users can't escape them by themselves - return `${label}="${escapeLabelValueInExactSelector(row.labels[label])}"`; - } - return ''; - }) - // Filter empty strings - .filter((label) => !!label) - .join(','); + let expr = await this.prepareContextExpr(row, origQuery); const contextTimeBuffer = 2 * 60 * 60 * 1000; // 2h buffer const queryDirection = direction === 'FORWARD' ? LokiQueryDirection.Forward : LokiQueryDirection.Backward; const query: LokiQuery = { - expr: `{${expr}}`, + expr, queryType: LokiQueryType.Range, refId: `${REF_ID_STARTER_LOG_ROW_CONTEXT}${row.dataFrame.refId || ''}`, maxLines: limit, @@ -726,6 +727,71 @@ export class LokiDatasource }; }; + async prepareContextExpr(row: LogRowModel, origQuery?: DataQuery): Promise { + await this.languageProvider.start(); + const labels = this.languageProvider.getLabelKeys(); + const expr = Object.keys(row.labels) + .map((label: string) => { + if (labels.includes(label)) { + // escape backslashes in label as users can't escape them by themselves + return `${label}="${escapeLabelValueInExactSelector(row.labels[label])}"`; + } + return ''; + }) + .filter((label) => !!label) + .join(','); + + return `{${expr}}`; + } + + getLogRowContextUi(row: LogRowModel, runContextQuery: () => void): React.ReactNode { + return LokiContextUi({ + row, + languageProvider: this.languageProvider, + updateFilter: (contextFilters: ContextFilter[]) => { + this.prepareContextExpr = async (row: LogRowModel, origQuery?: DataQuery) => { + await this.languageProvider.start(); + const labels = this.languageProvider.getLabelKeys(); + + let expr = contextFilters + .map((filter) => { + const label = filter.value; + if (filter && !filter.fromParser && filter.enabled && labels.includes(label)) { + // escape backslashes in label as users can't escape them by themselves + return `${label}="${escapeLabelValueInExactSelector(row.labels[label])}"`; + } + return ''; + }) + // Filter empty strings + .filter((label) => !!label) + .join(','); + + expr = `{${expr}}`; + + const parserContextFilters = contextFilters.filter((filter) => filter.fromParser && filter.enabled); + if (parserContextFilters.length) { + // we should also filter for labels from parsers, let's find the right parser + if (origQuery) { + const parser = getParserFromQuery((origQuery as LokiQuery).expr); + if (parser) { + expr = addParserToQuery(expr, parser); + } + } + for (const filter of parserContextFilters) { + if (filter.enabled) { + expr = addLabelToQuery(expr, filter.label, '=', row.labels[filter.label]); + } + } + } + return expr; + }; + if (runContextQuery) { + runContextQuery(); + } + }, + }); + } + testDatasource(): Promise<{ status: string; message: string }> { // Consider only last 10 minutes otherwise request takes too long const nowMs = Date.now(); diff --git a/public/app/plugins/datasource/loki/types.ts b/public/app/plugins/datasource/loki/types.ts index ec900a654ab..1a75e42f2d6 100644 --- a/public/app/plugins/datasource/loki/types.ts +++ b/public/app/plugins/datasource/loki/types.ts @@ -153,3 +153,11 @@ export interface LokiVariableQuery extends DataQuery { label?: string; stream?: string; } + +export interface ContextFilter { + enabled: boolean; + label: string; + value: string; + fromParser: boolean; + description?: string; +} From afd39c18ba956f42a4bb71622c53c52dfb7672e4 Mon Sep 17 00:00:00 2001 From: Andre Pereira Date: Fri, 27 Jan 2023 14:13:17 +0000 Subject: [PATCH 041/117] Explore: Refactor trace view and move to core (#61938) * Move TraceView to core grafana * Remove unused code * yarn install * Remove jaeger-ui-components from CODEOWNERS and other tools * Type fixes * yarn install * Remove mock that we no longer need * Fix merge conflicts * Re-add Apache license for trace view components * Use an exclamation-circle instead of triangle to denote errors * Remove eslint disables and update betterer results instead --- .betterer.results | 148 +- .github/CODEOWNERS | 3 +- .github/renovate.json5 | 2 - LICENSING.md | 2 +- package.json | 6 +- packages/jaeger-ui-components/package.json | 58 - .../src/common/NewWindowIcon.test.tsx | 26 - .../src/constants/default-config.tsx | 86 - .../src/types/TDdgState.tsx | 37 - .../src/types/embedded.tsx | 25 - .../jaeger-ui-components/src/types/search.tsx | 54 - packages/jaeger-ui-components/tsconfig.json | 16 - .../containers/DashboardPage.test.tsx | 4 - .../explore/TraceView/TraceView.test.tsx | 7 +- .../features/explore/TraceView/TraceView.tsx | 10 +- .../explore/TraceView/TraceViewContainer.tsx | 4 +- .../TraceView/components}/LICENSE_APACHE2 | 0 .../components}/ScrollManager.test.ts | 3 +- .../TraceView/components}/ScrollManager.tsx | 0 .../explore/TraceView/components}/Theme.tsx | 0 .../SpanGraph/CanvasSpanGraph.test.tsx | 0 .../SpanGraph/CanvasSpanGraph.tsx | 0 .../SpanGraph/GraphTicks.test.tsx | 0 .../TracePageHeader/SpanGraph/GraphTicks.tsx | 0 .../SpanGraph/Scrubber.test.tsx | 0 .../TracePageHeader/SpanGraph/Scrubber.tsx | 0 .../SpanGraph/TickLabels.test.tsx | 0 .../TracePageHeader/SpanGraph/TickLabels.tsx | 0 .../SpanGraph/ViewingLayer.test.tsx | 0 .../SpanGraph/ViewingLayer.tsx | 2 +- .../TracePageHeader/SpanGraph/index.test.tsx | 0 .../TracePageHeader/SpanGraph/index.tsx | 4 +- .../SpanGraph/render-into-canvas.test.ts | 0 .../SpanGraph/render-into-canvas.tsx | 2 +- .../TracePageHeader/TracePageHeader.test.tsx | 0 .../TracePageHeader/TracePageHeader.tsx | 10 +- .../TracePageSearchBar.test.tsx | 0 .../TracePageHeader/TracePageSearchBar.tsx | 0 .../components}/TracePageHeader/index.tsx | 0 .../ListView/Positions.test.ts | 0 .../ListView/Positions.tsx | 0 .../ListView/index.test.tsx | 0 .../TraceTimelineViewer/ListView/index.tsx | 2 + .../TraceTimelineViewer/SpanBar.test.tsx | 0 .../TraceTimelineViewer/SpanBar.tsx | 3 +- .../TraceTimelineViewer/SpanBarRow.test.tsx | 4 +- .../TraceTimelineViewer/SpanBarRow.tsx | 19 +- .../SpanDetail/AccordianKeyValues.markers.tsx | 0 .../SpanDetail/AccordianKeyValues.test.tsx | 0 .../SpanDetail/AccordianKeyValues.tsx | 13 +- .../SpanDetail/AccordianLogs.test.tsx | 0 .../SpanDetail/AccordianLogs.tsx | 10 +- .../SpanDetail/AccordianReferences.test.tsx | 0 .../SpanDetail/AccordianReferences.tsx | 8 +- .../SpanDetail/AccordianText.test.tsx | 0 .../SpanDetail/AccordianText.tsx | 10 +- .../SpanDetail/DetailState.tsx | 0 .../SpanDetail/KeyValuesTable.test.tsx | 0 .../SpanDetail/KeyValuesTable.tsx | 3 +- .../SpanDetail/TextList.test.tsx | 0 .../SpanDetail/TextList.tsx | 0 .../SpanDetail/index.test.tsx | 2 +- .../TraceTimelineViewer/SpanDetail/index.tsx | 5 +- .../SpanDetailRow.test.tsx | 0 .../TraceTimelineViewer/SpanDetailRow.tsx | 0 .../TraceTimelineViewer/SpanLinks.tsx | 0 .../SpanTreeOffset.test.tsx | 2 +- .../TraceTimelineViewer/SpanTreeOffset.tsx | 10 +- .../TraceTimelineViewer/Ticks.test.tsx | 0 .../components}/TraceTimelineViewer/Ticks.tsx | 0 .../TimelineCollapser.test.tsx | 0 .../TimelineHeaderRow/TimelineCollapser.tsx | 0 .../TimelineColumnResizer.test.tsx | 0 .../TimelineColumnResizer.tsx | 1 - .../TimelineHeaderRow.test.tsx | 0 .../TimelineHeaderRow/TimelineHeaderRow.tsx | 0 .../TimelineViewingLayer.test.tsx | 0 .../TimelineViewingLayer.tsx | 2 +- .../TimelineHeaderRow/index.tsx | 0 .../TraceTimelineViewer/TimelineRow.tsx | 0 .../VirtualizedTraceView.test.tsx | 2 +- .../VirtualizedTraceView.tsx | 13 +- .../TraceTimelineViewer/index.test.tsx | 0 .../components}/TraceTimelineViewer/index.tsx | 0 .../components}/TraceTimelineViewer/types.tsx | 0 .../TraceTimelineViewer/utils.test.ts | 3 +- .../components}/TraceTimelineViewer/utils.tsx | 2 +- .../TraceView/components}/Tween.test.ts | 0 .../explore/TraceView/components}/Tween.tsx | 0 .../components}/common/BreakableText.tsx | 2 +- .../components}/common/CopyIcon.test.tsx | 9 +- .../TraceView/components}/common/CopyIcon.tsx | 3 +- .../TraceView/components}/common/Divider.tsx | 0 .../components}/common/ExternalLinks.tsx | 0 .../components}/common/LabeledList.tsx | 4 +- .../components}/common/NewWindowIcon.tsx | 5 +- .../TraceView/components}/common/Popover.tsx | 0 .../components}/common/TraceName.tsx | 0 .../components}/common/UiFindInput.test.tsx | 0 .../components}/common/UiFindInput.tsx | 5 +- .../components/constants/default-config.ts | 36 +- .../TraceView/components/constants/index.tsx | 12 +- .../components}/constants/tag-keys.tsx | 4 +- .../TraceView/components}/demo/.eslintrc | 0 .../TraceView/components}/demo/chance.d.ts | 0 .../components}/demo/trace-generators.ts | 4 +- .../explore/TraceView/components}/index.ts | 0 .../components}/keyboard-mappings.tsx | 0 .../components}/keyboard-shortcuts.tsx | 4 +- .../components}/model/ddg/PathElem.test.ts | 0 .../components}/model/ddg/PathElem.tsx | 0 .../ddg/__snapshots__/PathElem.test.ts.snap | 0 .../model/ddg/sample-paths.test.resources.ts | 0 .../TraceView/components}/model/ddg/types.tsx | 12 - .../components}/model/find-trace-name.test.ts | 2 +- .../components}/model/link-patterns.test.ts | 4 +- .../components}/model/link-patterns.tsx | 3 +- .../TraceView/components}/model/span.tsx | 2 +- .../components}/model/trace-viewer.ts | 2 +- .../model/transform-trace-data.test.ts | 2 +- .../model/transform-trace-data.tsx | 6 +- .../TraceView/components}/scroll-page.test.ts | 1 - .../TraceView/components}/scroll-page.tsx | 0 .../components}/selectors/process.test.ts | 2 +- .../components}/selectors/process.ts | 2 +- .../components}/selectors/span.test.ts | 4 +- .../TraceView/components}/selectors/span.ts | 30 +- .../components}/selectors/trace.fixture.ts | 0 .../components}/selectors/trace.test.ts | 3 +- .../TraceView/components}/selectors/trace.ts | 61 +- .../components}/settings/SpanBarSettings.tsx | 0 .../TraceView/components}/types/TNil.tsx | 0 .../components}/types/TTraceDiffState.tsx | 0 .../components}/types/TTraceTimeline.tsx | 0 .../TraceView/components}/types/config.tsx | 2 +- .../TraceView/components}/types/index.tsx | 0 .../TraceView/components}/types/links.ts | 2 +- .../TraceView/components/types/search.tsx | 20 +- .../TraceView/components}/types/trace.ts | 0 .../components}/uberUtilityStyles.ts | 0 .../components}/url/ReferenceLink.test.tsx | 0 .../components}/url/ReferenceLink.tsx | 0 .../DraggableManager/DraggableManager.test.ts | 0 .../DraggableManager/DraggableManager.tsx | 0 .../utils/DraggableManager/EUpdateTypes.tsx | 0 .../utils/DraggableManager/README.md | 2 +- .../DraggableManager/demo/DividerDemo.css | 0 .../DraggableManager/demo/DividerDemo.tsx | 2 +- .../demo/DraggableManagerDemo.css | 0 .../demo/DraggableManagerDemo.tsx | 0 .../DraggableManager/demo/RegionDemo.css | 0 .../DraggableManager/demo/RegionDemo.tsx | 2 +- .../utils/DraggableManager/demo/demo-ux.gif | Bin .../utils/DraggableManager/demo/index.tsx | 0 .../utils/DraggableManager/index.tsx | 0 .../utils/DraggableManager/types.tsx | 2 +- .../components}/utils/TreeNode.test.ts | 0 .../TraceView/components}/utils/TreeNode.ts | 0 .../components}/utils/color-generator.test.ts | 0 .../components}/utils/color-generator.tsx | 0 .../components}/utils/config/get-config.tsx | 0 .../TraceView/components}/utils/date.test.ts | 0 .../TraceView/components}/utils/date.tsx | 42 - .../components}/utils/filter-spans.test.ts | 2 +- .../components}/utils/filter-spans.tsx | 3 +- .../TraceView/components}/utils/number.tsx | 0 .../TraceView/components}/utils/sort.test.ts | 30 - .../TraceView/components}/utils/sort.ts | 14 - .../utils/span-ancestor-ids.test.ts | 2 +- .../components}/utils/span-ancestor-ids.tsx | 3 +- .../utils/test/requestAnimationFrame.ts | 0 .../explore/TraceView/createSpanLink.test.ts | 2 +- .../explore/TraceView/createSpanLink.tsx | 5 +- .../features/explore/TraceView}/custom.d.ts | 9 - .../TraceView/useChildrenState.test.ts | 3 +- .../explore/TraceView/useChildrenState.ts | 2 +- .../explore/TraceView/useDetailState.test.ts | 2 +- .../explore/TraceView/useDetailState.ts | 4 +- .../explore/TraceView/useSearch.test.ts | 3 +- .../features/explore/TraceView/useSearch.ts | 2 +- .../explore/TraceView/useViewRange.ts | 2 +- .../explore/TraceView/utils/transform.ts | 3 +- .../inspector/InspectDataTab.test.tsx | 4 - .../jaeger/components/ConfigEditor.tsx | 2 +- .../plugins/datasource/jaeger/datasource.ts | 2 +- .../datasource/jaeger/responseTransform.ts | 2 +- .../tempo/configuration/ConfigEditor.tsx | 2 +- .../plugins/datasource/tempo/datasource.ts | 2 +- .../datasource/zipkin/ConfigEditor.tsx | 2 +- .../plugins/datasource/zipkin/datasource.ts | 2 +- .../app/plugins/panel/traces/TracesPanel.tsx | 4 +- scripts/check-breaking-changes.sh | 6 +- tsconfig.json | 1 - yarn.lock | 1647 +++++++++++------ 194 files changed, 1310 insertions(+), 1378 deletions(-) delete mode 100644 packages/jaeger-ui-components/package.json delete mode 100644 packages/jaeger-ui-components/src/common/NewWindowIcon.test.tsx delete mode 100644 packages/jaeger-ui-components/src/constants/default-config.tsx delete mode 100644 packages/jaeger-ui-components/src/types/TDdgState.tsx delete mode 100644 packages/jaeger-ui-components/src/types/embedded.tsx delete mode 100644 packages/jaeger-ui-components/src/types/search.tsx delete mode 100644 packages/jaeger-ui-components/tsconfig.json rename {packages/jaeger-ui-components => public/app/features/explore/TraceView/components}/LICENSE_APACHE2 (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/ScrollManager.test.ts (99%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/ScrollManager.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/Theme.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TracePageHeader/SpanGraph/CanvasSpanGraph.test.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TracePageHeader/SpanGraph/CanvasSpanGraph.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TracePageHeader/SpanGraph/GraphTicks.test.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TracePageHeader/SpanGraph/GraphTicks.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TracePageHeader/SpanGraph/Scrubber.test.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TracePageHeader/SpanGraph/Scrubber.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TracePageHeader/SpanGraph/TickLabels.test.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TracePageHeader/SpanGraph/TickLabels.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TracePageHeader/SpanGraph/ViewingLayer.test.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TracePageHeader/SpanGraph/ViewingLayer.tsx (99%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TracePageHeader/SpanGraph/index.test.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TracePageHeader/SpanGraph/index.tsx (97%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TracePageHeader/SpanGraph/render-into-canvas.test.ts (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TracePageHeader/SpanGraph/render-into-canvas.tsx (98%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TracePageHeader/TracePageHeader.test.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TracePageHeader/TracePageHeader.tsx (97%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TracePageHeader/TracePageSearchBar.test.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TracePageHeader/TracePageSearchBar.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TracePageHeader/index.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/ListView/Positions.test.ts (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/ListView/Positions.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/ListView/index.test.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/ListView/index.tsx (99%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/SpanBar.test.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/SpanBar.tsx (98%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/SpanBarRow.test.tsx (98%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/SpanBarRow.tsx (97%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/SpanDetail/AccordianKeyValues.markers.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/SpanDetail/AccordianKeyValues.test.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/SpanDetail/AccordianKeyValues.tsx (92%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/SpanDetail/AccordianLogs.test.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/SpanDetail/AccordianLogs.tsx (93%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/SpanDetail/AccordianReferences.test.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx (96%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/SpanDetail/AccordianText.test.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/SpanDetail/AccordianText.tsx (91%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/SpanDetail/DetailState.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/SpanDetail/KeyValuesTable.test.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx (97%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/SpanDetail/TextList.test.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/SpanDetail/TextList.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/SpanDetail/index.test.tsx (99%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/SpanDetail/index.tsx (98%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/SpanDetailRow.test.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/SpanDetailRow.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/SpanLinks.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/SpanTreeOffset.test.tsx (99%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/SpanTreeOffset.tsx (94%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/Ticks.test.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/Ticks.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/TimelineHeaderRow/TimelineCollapser.test.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/TimelineHeaderRow/TimelineCollapser.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.test.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.tsx (99%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.test.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.test.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.tsx (99%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/TimelineHeaderRow/index.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/TimelineRow.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/VirtualizedTraceView.test.tsx (99%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/VirtualizedTraceView.tsx (97%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/index.test.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/index.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/types.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/utils.test.ts (99%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/TraceTimelineViewer/utils.tsx (99%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/Tween.test.ts (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/Tween.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/common/BreakableText.tsx (97%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/common/CopyIcon.test.tsx (90%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/common/CopyIcon.tsx (96%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/common/Divider.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/common/ExternalLinks.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/common/LabeledList.tsx (97%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/common/NewWindowIcon.tsx (88%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/common/Popover.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/common/TraceName.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/common/UiFindInput.test.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/common/UiFindInput.tsx (94%) rename packages/jaeger-ui-components/src/constants/index.tsx => public/app/features/explore/TraceView/components/constants/default-config.ts (50%) rename packages/jaeger-ui-components/src/types/api-error.tsx => public/app/features/explore/TraceView/components/constants/index.tsx (75%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/constants/tag-keys.tsx (77%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/demo/.eslintrc (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/demo/chance.d.ts (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/demo/trace-generators.ts (97%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/index.ts (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/keyboard-mappings.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/keyboard-shortcuts.tsx (92%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/model/ddg/PathElem.test.ts (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/model/ddg/PathElem.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/model/ddg/__snapshots__/PathElem.test.ts.snap (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/model/ddg/sample-paths.test.resources.ts (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/model/ddg/types.tsx (76%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/model/find-trace-name.test.ts (99%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/model/link-patterns.test.ts (98%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/model/link-patterns.tsx (98%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/model/span.tsx (95%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/model/trace-viewer.ts (97%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/model/transform-trace-data.test.ts (98%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/model/transform-trace-data.tsx (97%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/scroll-page.test.ts (99%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/scroll-page.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/selectors/process.test.ts (96%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/selectors/process.ts (94%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/selectors/span.test.ts (96%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/selectors/span.ts (76%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/selectors/trace.fixture.ts (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/selectors/trace.test.ts (99%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/selectors/trace.ts (81%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/settings/SpanBarSettings.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/types/TNil.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/types/TTraceDiffState.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/types/TTraceTimeline.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/types/config.tsx (97%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/types/index.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/types/links.ts (91%) rename packages/jaeger-ui-components/src/types/archive.tsx => public/app/features/explore/TraceView/components/types/search.tsx (67%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/types/trace.ts (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/uberUtilityStyles.ts (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/url/ReferenceLink.test.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/url/ReferenceLink.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/utils/DraggableManager/DraggableManager.test.ts (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/utils/DraggableManager/DraggableManager.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/utils/DraggableManager/EUpdateTypes.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/utils/DraggableManager/README.md (99%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/utils/DraggableManager/demo/DividerDemo.css (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/utils/DraggableManager/demo/DividerDemo.tsx (97%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/utils/DraggableManager/demo/DraggableManagerDemo.css (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/utils/DraggableManager/demo/DraggableManagerDemo.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/utils/DraggableManager/demo/RegionDemo.css (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/utils/DraggableManager/demo/RegionDemo.tsx (99%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/utils/DraggableManager/demo/demo-ux.gif (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/utils/DraggableManager/demo/index.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/utils/DraggableManager/index.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/utils/DraggableManager/types.tsx (95%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/utils/TreeNode.test.ts (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/utils/TreeNode.ts (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/utils/color-generator.test.ts (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/utils/color-generator.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/utils/config/get-config.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/utils/date.test.ts (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/utils/date.tsx (76%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/utils/filter-spans.test.ts (99%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/utils/filter-spans.tsx (96%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/utils/number.tsx (100%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/utils/sort.test.ts (73%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/utils/sort.ts (77%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/utils/span-ancestor-ids.test.ts (98%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/utils/span-ancestor-ids.tsx (93%) rename {packages/jaeger-ui-components/src => public/app/features/explore/TraceView/components}/utils/test/requestAnimationFrame.ts (100%) rename {packages/jaeger-ui-components/typings => public/app/features/explore/TraceView}/custom.d.ts (85%) diff --git a/.betterer.results b/.betterer.results index ecfdfcd3111..43127440a4e 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1689,97 +1689,6 @@ exports[`better eslint`] = { "packages/grafana-ui/src/utils/useAsyncDependency.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "packages/jaeger-ui-components/src/ScrollManager.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"] - ], - "packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/Scrubber.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"] - ], - "packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/render-into-canvas.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/index.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Do not use any type assertions.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Do not use any type assertions.", "4"] - ], - "packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBar.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], - "packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], - "packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "packages/jaeger-ui-components/src/TraceTimelineViewer/utils.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], - "packages/jaeger-ui-components/src/common/BreakableText.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], - "packages/jaeger-ui-components/src/common/UiFindInput.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"] - ], - "packages/jaeger-ui-components/src/constants/index.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"], - [0, 0, 0, "Do not use any type assertions.", "2"], - [0, 0, 0, "Do not use any type assertions.", "3"], - [0, 0, 0, "Do not use any type assertions.", "4"] - ], - "packages/jaeger-ui-components/src/constants/tag-keys.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"], - [0, 0, 0, "Do not use any type assertions.", "2"] - ], - "packages/jaeger-ui-components/src/keyboard-shortcuts.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"] - ], - "packages/jaeger-ui-components/src/model/link-patterns.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"], - [0, 0, 0, "Unexpected any. Specify a different type.", "6"], - [0, 0, 0, "Unexpected any. Specify a different type.", "7"], - [0, 0, 0, "Unexpected any. Specify a different type.", "8"], - [0, 0, 0, "Unexpected any. Specify a different type.", "9"], - [0, 0, 0, "Do not use any type assertions.", "10"], - [0, 0, 0, "Unexpected any. Specify a different type.", "11"], - [0, 0, 0, "Do not use any type assertions.", "12"], - [0, 0, 0, "Unexpected any. Specify a different type.", "13"] - ], - "packages/jaeger-ui-components/src/model/transform-trace-data.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"], - [0, 0, 0, "Do not use any type assertions.", "2"] - ], - "packages/jaeger-ui-components/src/types/api-error.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], - "packages/jaeger-ui-components/src/types/links.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], - "packages/jaeger-ui-components/src/types/trace.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], - "packages/jaeger-ui-components/src/utils/DraggableManager/types.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], - "packages/jaeger-ui-components/src/utils/date.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "plugins-bundled/internal/input-datasource/src/InputDatasource.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"] @@ -3883,6 +3792,63 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "4"], [0, 0, 0, "Do not use any type assertions.", "5"] ], + "public/app/features/explore/TraceView/components/ScrollManager.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"] + ], + "public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/Scrubber.tsx:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"] + ], + "public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/render-into-canvas.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], + "public/app/features/explore/TraceView/components/TraceTimelineViewer/ListView/index.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], + "public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBar.tsx:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] + ], + "public/app/features/explore/TraceView/components/TraceTimelineViewer/utils.tsx:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] + ], + "public/app/features/explore/TraceView/components/common/BreakableText.tsx:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] + ], + "public/app/features/explore/TraceView/components/constants/index.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], + "public/app/features/explore/TraceView/components/demo/trace-generators.ts:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], + "public/app/features/explore/TraceView/components/model/link-patterns.test.ts:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"] + ], + "public/app/features/explore/TraceView/components/model/link-patterns.tsx:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"], + [0, 0, 0, "Unexpected any. Specify a different type.", "3"], + [0, 0, 0, "Unexpected any. Specify a different type.", "4"], + [0, 0, 0, "Unexpected any. Specify a different type.", "5"], + [0, 0, 0, "Unexpected any. Specify a different type.", "6"], + [0, 0, 0, "Unexpected any. Specify a different type.", "7"], + [0, 0, 0, "Unexpected any. Specify a different type.", "8"], + [0, 0, 0, "Unexpected any. Specify a different type.", "9"], + [0, 0, 0, "Do not use any type assertions.", "10"], + [0, 0, 0, "Unexpected any. Specify a different type.", "11"], + [0, 0, 0, "Do not use any type assertions.", "12"], + [0, 0, 0, "Unexpected any. Specify a different type.", "13"] + ], + "public/app/features/explore/TraceView/components/model/transform-trace-data.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], + "public/app/features/explore/TraceView/components/types/trace.ts:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] + ], "public/app/features/explore/TraceView/createSpanLink.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"] diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8a118e42a87..fd06ecb38d9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -306,7 +306,6 @@ WORKFLOW.md @torkelo /packages/grafana-ui/src/components/TimeSeries/ @grafana/grafana-bi-squad /packages/grafana-ui/src/components/uPlot/ @grafana/grafana-bi-squad /packages/grafana-ui/src/utils/storybook/ @grafana/plugins-platform-frontend -/packages/jaeger-ui-components// @grafana/observability-traces-and-profiling /packages/grafana-data/src/**/*logs* @grafana/observability-logs /plugins-bundled/ @grafana/plugins-platform-frontend @@ -436,6 +435,8 @@ lerna.json @grafana/frontend-ops /public/app/features/explore/Logs.tsx @grafana/observability-logs /public/app/features/explore/LogsContainer.tsx @grafana/observability-logs +/public/app/features/explore/TraceView/ @grafana/observability-traces-and-profiling + /public/api-merged.json @grafana/backend-platform /public/api-spec.json @grafana/backend-platform /public/openapi3.json @grafana/backend-platform diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 1ed4455944f..a828336afbd 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -5,7 +5,6 @@ "enabledManagers": ["npm"], "ignoreDeps": [ "@types/systemjs", - "@types/react-icons", // jaeger-ui-components is being refactored to use @grafana/ui icons instead "commander", // we are planning to remove this, so no need to update it "execa", // we should bump this once we move to esm modules "history", // we should bump this together with react-router-dom @@ -13,7 +12,6 @@ "monaco-editor", // due to us exposing this via @grafana/ui/CodeEditor's props bumping can break plugins "node-fetch", // we should bump this once we move to esm modules "react-hook-form", // due to us exposing these hooks via @grafana/ui form components bumping can break plugins - "react-icons", // jaeger-ui-components is being refactored to use @grafana/ui icons instead "react-redux", // react-beautiful-dnd depends on react-redux 7.x, we need to update that one first "react-router-dom", // we should bump this together with history "systemjs", diff --git a/LICENSING.md b/LICENSING.md index 7b64973e012..696cf188f9b 100644 --- a/LICENSING.md +++ b/LICENSING.md @@ -15,7 +15,6 @@ packages/grafana-e2e-selectors/ packages/grafana-runtime/ packages/grafana-toolkit/ packages/grafana-ui/ -packages/jaeger-ui-components/ packaging/ kinds/ pkg/kinds/ @@ -23,6 +22,7 @@ pkg/kindsys/ pkg/registry/corekind/ grafana-mixin/ public/app/plugins/datasource/tempo +public/app/features/explore/TraceView/components public/img/icons/solid/ public/img/icons/unicons/ ``` diff --git a/package.json b/package.json index 1b21000680c..49d08cba814 100644 --- a/package.json +++ b/package.json @@ -178,6 +178,7 @@ "babel-plugin-macros": "3.1.0", "blob-polyfill": "7.0.20220408", "browserslist": "^4.21.4", + "chance": "^1.0.10", "codeowners": "^5.1.1", "copy-webpack-plugin": "9.0.1", "css-loader": "6.7.1", @@ -270,7 +271,6 @@ "@grafana/scenes": "latest", "@grafana/schema": "workspace:*", "@grafana/ui": "workspace:*", - "@jaegertracing/jaeger-ui-components": "workspace:*", "@kusto/monaco-kusto": "5.3.6", "@leeoniya/ufuzzy": "0.9.1", "@lezer/common": "1.0.1", @@ -315,6 +315,7 @@ "calculate-size": "1.1.1", "centrifuge": "3.1.0", "classnames": "2.3.2", + "combokeys": "^3.0.0", "comlink": "4.3.1", "common-tags": "1.8.2", "core-js": "3.27.1", @@ -338,12 +339,14 @@ "immutable": "4.2.2", "jquery": "3.6.1", "js-yaml": "^4.1.0", + "json-markup": "^1.1.0", "json-source-map": "0.6.1", "jsurl": "^0.1.5", "kbar": "0.1.0-beta.36", "lodash": "4.17.21", "logfmt": "^1.3.2", "lru-cache": "7.14.0", + "lru-memoize": "^1.1.0", "memoize-one": "6.0.0", "moment": "2.29.4", "moment-timezone": "0.5.38", @@ -409,6 +412,7 @@ "tether-drop": "https://github.com/torkelo/drop", "tinycolor2": "1.4.2", "tslib": "2.4.1", + "tween-functions": "^1.2.0", "uplot": "1.6.24", "uuid": "9.0.0", "vendor": "link:./public/vendor", diff --git a/packages/jaeger-ui-components/package.json b/packages/jaeger-ui-components/package.json deleted file mode 100644 index 8984f23da5d..00000000000 --- a/packages/jaeger-ui-components/package.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "name": "@jaegertracing/jaeger-ui-components", - "version": "9.4.0-pre", - "main": "src/index.ts", - "types": "src/index.ts", - "license": "Apache-2.0", - "private": true, - "scripts": { - "typecheck": "tsc --noEmit" - }, - "devDependencies": { - "@grafana/tsconfig": "^1.2.0-rc1", - "@testing-library/jest-dom": "5.16.5", - "@testing-library/react": "12.1.4", - "@testing-library/user-event": "14.4.3", - "@types/deep-freeze": "^0.1.1", - "@types/hoist-non-react-statics": "^3.3.1", - "@types/jest": "29.2.3", - "@types/lodash": "4.14.187", - "@types/prop-types": "15.7.5", - "@types/react": "17.0.42", - "@types/react-icons": "2.2.7", - "@types/sinon": "^10.0.13", - "@types/slate-react": "0.22.9", - "@types/testing-library__jest-dom": "5.14.5", - "@types/tinycolor2": "1.4.3", - "sinon": "14.0.1", - "typescript": "4.8.4" - }, - "dependencies": { - "@emotion/css": "11.10.5", - "@grafana/data": "9.4.0-pre", - "@grafana/e2e-selectors": "9.4.0-pre", - "@grafana/runtime": "9.4.0-pre", - "@grafana/ui": "9.4.0-pre", - "chance": "^1.0.10", - "classnames": "^2.2.5", - "combokeys": "^3.0.0", - "copy-to-clipboard": "^3.1.0", - "deep-freeze": "^0.0.1", - "fuzzy": "^0.1.3", - "hoist-non-react-statics": "^3.3.2", - "json-markup": "^1.1.0", - "lodash": "4.17.21", - "lru-memoize": "^1.1.0", - "memoize-one": "6.0.0", - "moment": "2.29.4", - "moment-timezone": "0.5.38", - "prop-types": "15.8.1", - "react": "17.0.2", - "react-dom": "17.0.2", - "react-icons": "2.2.7", - "reselect": "4.1.6", - "tinycolor2": "1.4.2", - "tslib": "2.4.1", - "tween-functions": "^1.2.0" - } -} diff --git a/packages/jaeger-ui-components/src/common/NewWindowIcon.test.tsx b/packages/jaeger-ui-components/src/common/NewWindowIcon.test.tsx deleted file mode 100644 index bde47016e78..00000000000 --- a/packages/jaeger-ui-components/src/common/NewWindowIcon.test.tsx +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) 2019 Uber Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { render } from '@testing-library/react'; -import React from 'react'; - -import NewWindowIcon, { getStyles } from './NewWindowIcon'; - -describe('NewWindowIcon', () => { - it('adds is-large className when props.isLarge is true', () => { - const { container } = render(); - const styles = getStyles(); - expect(container.firstChild).toHaveClass(styles.NewWindowIconLarge); - }); -}); diff --git a/packages/jaeger-ui-components/src/constants/default-config.tsx b/packages/jaeger-ui-components/src/constants/default-config.tsx deleted file mode 100644 index 5dfe3420920..00000000000 --- a/packages/jaeger-ui-components/src/constants/default-config.tsx +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) 2017 Uber Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import deepFreeze from 'deep-freeze'; - -import { FALLBACK_DAG_MAX_NUM_SERVICES } from './index'; - -export default deepFreeze( - Object.defineProperty( - { - archiveEnabled: false, - dependencies: { - dagMaxNumServices: FALLBACK_DAG_MAX_NUM_SERVICES, - menuEnabled: true, - }, - linkPatterns: [], - menu: [ - { - label: 'About Jaeger', - items: [ - { - label: 'GitHub', - url: 'https://github.com/uber/jaeger', - }, - { - label: 'Docs', - url: 'http://jaeger.readthedocs.io/en/latest/', - }, - { - label: 'Twitter', - url: 'https://twitter.com/JaegerTracing', - }, - { - label: 'Discussion Group', - url: 'https://groups.google.com/forum/#!forum/jaeger-tracing', - }, - { - label: 'Gitter.im', - url: 'https://gitter.im/jaegertracing/Lobby', - }, - { - label: 'Blog', - url: 'https://medium.com/jaegertracing/', - }, - ], - }, - ], - search: { - maxLookback: { - label: '2 Days', - value: '2d', - }, - maxLimit: 1500, - }, - tracking: { - gaID: null, - trackErrors: true, - }, - }, - // fields that should be individually merged vs wholesale replaced - '__mergeFields', - { value: ['dependencies', 'search', 'tracking'] } - ) -); - -export const deprecations = [ - { - formerKey: 'dependenciesMenuEnabled', - currentKey: 'dependencies.menuEnabled', - }, - { - formerKey: 'gaTrackingID', - currentKey: 'tracking.gaID', - }, -]; diff --git a/packages/jaeger-ui-components/src/types/TDdgState.tsx b/packages/jaeger-ui-components/src/types/TDdgState.tsx deleted file mode 100644 index f1aba58ac7c..00000000000 --- a/packages/jaeger-ui-components/src/types/TDdgState.tsx +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) 2019 Uber Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { fetchedState } from '../constants'; -import { TDdgModel } from '../model/ddg/types'; - -import { ApiError } from './api-error'; - -export type TDdgStateEntry = - | { - state: typeof fetchedState.LOADING; - } - | { - error: ApiError; - state: typeof fetchedState.ERROR; - } - | { - model: TDdgModel; - state: typeof fetchedState.DONE; - viewModifiers: Map; - }; - -type TDdgState = Record; - -// eslint-disable-next-line no-undef -export default TDdgState; diff --git a/packages/jaeger-ui-components/src/types/embedded.tsx b/packages/jaeger-ui-components/src/types/embedded.tsx deleted file mode 100644 index e7f87776df5..00000000000 --- a/packages/jaeger-ui-components/src/types/embedded.tsx +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) 2018 Uber Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -type EmbeddedStateV0 = { - version: 'v0'; - searchHideGraph: boolean; - timeline: { - collapseTitle: boolean; - hideMinimap: boolean; - hideSummary: boolean; - }; -}; - -export type EmbeddedState = EmbeddedStateV0; diff --git a/packages/jaeger-ui-components/src/types/search.tsx b/packages/jaeger-ui-components/src/types/search.tsx deleted file mode 100644 index 380d4316ddc..00000000000 --- a/packages/jaeger-ui-components/src/types/search.tsx +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) 2017 Uber Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { TNil } from '.'; - -export type SearchQuery = { - end: number | string; - limit: number | string; - lookback: string; - maxDuration: null | string; - minDuration: null | string; - operation: string | TNil; - service: string; - start: number | string; - tags: string | TNil; -}; - -/** - * Type used to summarize traces for the search page. - */ -export type TraceSummary = { - /** - * Duration of trace in milliseconds. - */ - duration: number; - /** - * Start time of trace in milliseconds. - */ - timestamp: number; - traceName: string; - traceID: string; - numberOfErredSpans: number; - numberOfSpans: number; - services: Array<{ name: string; numberOfSpans: number }>; -}; - -export type TraceSummaries = { - /** - * Duration of longest trace in `traces` in milliseconds. - */ - maxDuration: number; - traces: TraceSummary[]; -}; diff --git a/packages/jaeger-ui-components/tsconfig.json b/packages/jaeger-ui-components/tsconfig.json deleted file mode 100644 index 83f4d88d032..00000000000 --- a/packages/jaeger-ui-components/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "compilerOptions": { - "baseUrl": ".", - "allowJs": true, - "jsx": "react" - }, - "exclude": ["dist", "node_modules"], - "extends": "@grafana/tsconfig", - "include": [ - "src/**/*.ts*", - "typings", - "../../public/app/types/jquery/*.ts", - "../../public/app/types/*.d.ts", - "../grafana-ui/src/types/*.d.ts" - ] -} diff --git a/public/app/features/dashboard/containers/DashboardPage.test.tsx b/public/app/features/dashboard/containers/DashboardPage.test.tsx index 32a0e5f9bc5..f34e450863c 100644 --- a/public/app/features/dashboard/containers/DashboardPage.test.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.test.tsx @@ -63,10 +63,6 @@ jest.mock('react-virtualized-auto-sizer', () => { return ({ children }: AutoSizerProps) => children({ height: 1, width: 1 }); }); -// the mock below gets rid of this warning from recompose: -// Warning: React.createFactory() is deprecated and will be removed in a future major release. Consider using JSX or use React.createElement() directly instead. -jest.mock('@jaegertracing/jaeger-ui-components', () => ({})); - interface ScenarioContext { dashboard?: DashboardModel | null; container?: HTMLElement; diff --git a/public/app/features/explore/TraceView/TraceView.test.tsx b/public/app/features/explore/TraceView/TraceView.test.tsx index 89e1b7adc82..da19941910e 100644 --- a/public/app/features/explore/TraceView/TraceView.test.tsx +++ b/public/app/features/explore/TraceView/TraceView.test.tsx @@ -1,5 +1,3 @@ -import { TopOfViewRefType } from '@jaegertracing/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView'; -import { TraceData, TraceSpanData } from '@jaegertracing/jaeger-ui-components/src/types/trace'; import { render, prettyDOM, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React, { createRef } from 'react'; @@ -12,6 +10,8 @@ import { ExploreId } from 'app/types'; import { configureStore } from '../../../store/configureStore'; import { TraceView } from './TraceView'; +import { TopOfViewRefType } from './components/TraceTimelineViewer/VirtualizedTraceView'; +import { TraceData, TraceSpanData } from './components/types/trace'; import { transformDataFrames } from './utils/transform'; function getTraceView(frames: DataFrame[]) { @@ -23,7 +23,7 @@ function getTraceView(frames: DataFrame[]) { }; const topOfViewRef = createRef(); - const traceView = ( + return ( ); - return traceView; } function renderTraceView(frames = [frameOld]) { diff --git a/public/app/features/explore/TraceView/TraceView.tsx b/public/app/features/explore/TraceView/TraceView.tsx index b89588bd0e4..11badf9eca6 100644 --- a/public/app/features/explore/TraceView/TraceView.tsx +++ b/public/app/features/explore/TraceView/TraceView.tsx @@ -1,5 +1,4 @@ import { css } from '@emotion/css'; -import { TopOfViewRefType } from '@jaegertracing/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView'; import React, { RefObject, useCallback, useMemo, useState } from 'react'; import { @@ -17,13 +16,6 @@ import { import { getTemplateSrv } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; import { useStyles2 } from '@grafana/ui'; -import { - SpanBarOptionsData, - Trace, - TracePageHeader, - TraceTimelineViewer, - TTraceTimeline, -} from '@jaegertracing/jaeger-ui-components'; import { getTraceToLogsOptions, TraceToLogsData } from 'app/core/components/TraceToLogs/TraceToLogsSettings'; import { TraceToMetricsData } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; @@ -34,6 +26,8 @@ import { ExploreId } from 'app/types/explore'; import { changePanelState } from '../state/explorePane'; +import { SpanBarOptionsData, Trace, TracePageHeader, TraceTimelineViewer, TTraceTimeline } from './components'; +import { TopOfViewRefType } from './components/TraceTimelineViewer/VirtualizedTraceView'; import { createSpanLinkFactory } from './createSpanLink'; import { useChildrenState } from './useChildrenState'; import { useDetailState } from './useDetailState'; diff --git a/public/app/features/explore/TraceView/TraceViewContainer.tsx b/public/app/features/explore/TraceView/TraceViewContainer.tsx index 1d908a0ed64..d0fad09498e 100644 --- a/public/app/features/explore/TraceView/TraceViewContainer.tsx +++ b/public/app/features/explore/TraceView/TraceViewContainer.tsx @@ -1,5 +1,3 @@ -import TracePageSearchBar from '@jaegertracing/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar'; -import { TopOfViewRefType } from '@jaegertracing/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView'; import React, { RefObject, useMemo, useState } from 'react'; import { DataFrame, SplitOpen, PanelData } from '@grafana/data'; @@ -8,6 +6,8 @@ import { StoreState, useSelector } from 'app/types'; import { ExploreId } from 'app/types/explore'; import { TraceView } from './TraceView'; +import TracePageSearchBar from './components/TracePageHeader/TracePageSearchBar'; +import { TopOfViewRefType } from './components/TraceTimelineViewer/VirtualizedTraceView'; import { useSearch } from './useSearch'; import { transformDataFrames } from './utils/transform'; interface Props { diff --git a/packages/jaeger-ui-components/LICENSE_APACHE2 b/public/app/features/explore/TraceView/components/LICENSE_APACHE2 similarity index 100% rename from packages/jaeger-ui-components/LICENSE_APACHE2 rename to public/app/features/explore/TraceView/components/LICENSE_APACHE2 diff --git a/packages/jaeger-ui-components/src/ScrollManager.test.ts b/public/app/features/explore/TraceView/components/ScrollManager.test.ts similarity index 99% rename from packages/jaeger-ui-components/src/ScrollManager.test.ts rename to public/app/features/explore/TraceView/components/ScrollManager.test.ts index 7f7e1368279..3be8a64327e 100644 --- a/packages/jaeger-ui-components/src/ScrollManager.test.ts +++ b/public/app/features/explore/TraceView/components/ScrollManager.test.ts @@ -14,9 +14,8 @@ jest.mock('./scroll-page'); -import traceGenerator from '../src/demo/trace-generators'; - import ScrollManager, { Accessors } from './ScrollManager'; +import traceGenerator from './demo/trace-generators'; import { scrollBy, scrollTo } from './scroll-page'; import { Trace, TraceSpanData, TraceSpanReference } from './types/trace'; diff --git a/packages/jaeger-ui-components/src/ScrollManager.tsx b/public/app/features/explore/TraceView/components/ScrollManager.tsx similarity index 100% rename from packages/jaeger-ui-components/src/ScrollManager.tsx rename to public/app/features/explore/TraceView/components/ScrollManager.tsx diff --git a/packages/jaeger-ui-components/src/Theme.tsx b/public/app/features/explore/TraceView/components/Theme.tsx similarity index 100% rename from packages/jaeger-ui-components/src/Theme.tsx rename to public/app/features/explore/TraceView/components/Theme.tsx diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/CanvasSpanGraph.test.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/CanvasSpanGraph.test.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/CanvasSpanGraph.test.tsx rename to public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/CanvasSpanGraph.test.tsx diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/CanvasSpanGraph.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/CanvasSpanGraph.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/CanvasSpanGraph.tsx rename to public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/CanvasSpanGraph.tsx diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/GraphTicks.test.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/GraphTicks.test.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/GraphTicks.test.tsx rename to public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/GraphTicks.test.tsx diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/GraphTicks.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/GraphTicks.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/GraphTicks.tsx rename to public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/GraphTicks.tsx diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/Scrubber.test.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/Scrubber.test.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/Scrubber.test.tsx rename to public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/Scrubber.test.tsx diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/Scrubber.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/Scrubber.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/Scrubber.tsx rename to public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/Scrubber.tsx diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/TickLabels.test.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/TickLabels.test.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/TickLabels.test.tsx rename to public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/TickLabels.test.tsx diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/TickLabels.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/TickLabels.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/TickLabels.tsx rename to public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/TickLabels.tsx diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.test.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/ViewingLayer.test.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.test.tsx rename to public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/ViewingLayer.test.tsx diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/ViewingLayer.tsx similarity index 99% rename from packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.tsx rename to public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/ViewingLayer.tsx index eb2277c1b3d..a8a12ac1e64 100644 --- a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/ViewingLayer.tsx @@ -19,8 +19,8 @@ import * as React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { withTheme2, stylesFactory, Button } from '@grafana/ui'; -import { TUpdateViewRangeTimeFunction, ViewRange, ViewRangeTimeUpdate, TNil } from '../..'; import { autoColor } from '../../Theme'; +import { TUpdateViewRangeTimeFunction, ViewRange, ViewRangeTimeUpdate, TNil } from '../../index'; import DraggableManager, { DraggableBounds, DraggingUpdate, EUpdateTypes } from '../../utils/DraggableManager'; import GraphTicks from './GraphTicks'; diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/index.test.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/index.test.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/index.test.tsx rename to public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/index.test.tsx diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/index.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/index.tsx similarity index 97% rename from packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/index.tsx rename to public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/index.tsx index 3a1a0275e72..f7c05fa723a 100644 --- a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/index.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/index.tsx @@ -16,8 +16,8 @@ import cx from 'classnames'; import memoizeOne from 'memoize-one'; import * as React from 'react'; -import { TUpdateViewRangeTimeFunction, ViewRange, ViewRangeTimeUpdate } from '../..'; -import { TraceSpan, Trace } from '../../types/trace'; +import { TUpdateViewRangeTimeFunction, ViewRange, ViewRangeTimeUpdate } from '../../index'; +import { TraceSpan, Trace } from '../../types'; import { ubPb2, ubPx2, ubRelative } from '../../uberUtilityStyles'; import CanvasSpanGraph from './CanvasSpanGraph'; diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/render-into-canvas.test.ts b/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/render-into-canvas.test.ts similarity index 100% rename from packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/render-into-canvas.test.ts rename to public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/render-into-canvas.test.ts diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/render-into-canvas.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/render-into-canvas.tsx similarity index 98% rename from packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/render-into-canvas.tsx rename to public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/render-into-canvas.tsx index 32f1ac8afba..a6fe5f0cebb 100644 --- a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/render-into-canvas.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/render-into-canvas.tsx @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { TNil } from '../..'; +import { TNil } from '../../index'; // exported for tests export const ITEM_ALPHA = 0.8; diff --git a/packages/jaeger-ui-components/src/TracePageHeader/TracePageHeader.test.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.test.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TracePageHeader/TracePageHeader.test.tsx rename to public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.test.tsx diff --git a/packages/jaeger-ui-components/src/TracePageHeader/TracePageHeader.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx similarity index 97% rename from packages/jaeger-ui-components/src/TracePageHeader/TracePageHeader.tsx rename to public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx index a18fb38e213..1a7af88ca81 100644 --- a/packages/jaeger-ui-components/src/TracePageHeader/TracePageHeader.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx @@ -16,18 +16,17 @@ import { css } from '@emotion/css'; import cx from 'classnames'; import { get as _get, maxBy as _maxBy, values as _values } from 'lodash'; import * as React from 'react'; -import MdKeyboardArrowRight from 'react-icons/lib/md/keyboard-arrow-right'; import { dateTimeFormat, GrafanaTheme2, TimeZone } from '@grafana/data'; -import { useStyles2 } from '@grafana/ui'; +import { Icon, useStyles2 } from '@grafana/ui'; -import { autoColor, TUpdateViewRangeTimeFunction, ViewRange, ViewRangeTimeUpdate } from '..'; import ExternalLinks from '../common/ExternalLinks'; import LabeledList from '../common/LabeledList'; import TraceName from '../common/TraceName'; +import { autoColor, TUpdateViewRangeTimeFunction, ViewRange, ViewRangeTimeUpdate } from '../index'; import { getTraceLinks } from '../model/link-patterns'; import { getTraceName } from '../model/trace-viewer'; -import { Trace } from '../types/trace'; +import { Trace } from '../types'; import { uTxMuted } from '../uberUtilityStyles'; import { formatDuration } from '../utils/date'; @@ -247,7 +246,8 @@ export default function TracePageHeader(props: TracePageHeaderEmbedProps) { role="switch" aria-checked={!slimView} > - { this._htmlTopOffset = -1; this._windowScrollListenerAdded = false; // _htmlElm is only relevant if props.windowScroller is true + // eslint-disable-next-line this._htmlElm = document.documentElement as any; this._wrapperElm = undefined; this._itemHolderElm = undefined; @@ -378,6 +379,7 @@ export default class ListView extends React.Component { const nodes = this._itemHolderElm.childNodes; const max = nodes.length; for (let i = 0; i < max; i++) { + // eslint-disable-next-line const node: HTMLElement = nodes[i] as any; // use `.getAttribute(...)` instead of `.dataset` for jest / JSDOM const itemKey = node.getAttribute('data-item-key'); diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBar.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBar.test.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBar.test.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBar.test.tsx diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBar.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBar.tsx similarity index 98% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBar.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBar.tsx index aca8cee5b5f..ebf46d12f1b 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBar.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBar.tsx @@ -23,8 +23,7 @@ import { useStyles2 } from '@grafana/ui'; import { autoColor } from '../Theme'; import { Popover } from '../common/Popover'; -import { TNil } from '../types'; -import { TraceSpan } from '../types/trace'; +import { TraceSpan, TNil } from '../types'; import AccordianLogs from './SpanDetail/AccordianLogs'; import { ViewedBoundsFunctionType } from './utils'; diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.test.tsx similarity index 98% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.test.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.test.tsx index 23d684fdcd7..a1ef4b8296d 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.test.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.test.tsx @@ -15,10 +15,10 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { SpanLinks } from 'src/types/links'; -import { TraceSpan } from 'src/types/trace'; import { NONE, DURATION, TAG } from '../settings/SpanBarSettings'; +import { TraceSpan } from '../types'; +import { SpanLinks } from '../types/links'; import SpanBarRow, { SpanBarRowProps } from './SpanBarRow'; diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.tsx similarity index 97% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.tsx index ca2e19992bd..29ad671870e 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.tsx @@ -15,17 +15,14 @@ import { css, keyframes } from '@emotion/css'; import cx from 'classnames'; import * as React from 'react'; -import IoAlert from 'react-icons/lib/io/alert'; -import IoArrowRightA from 'react-icons/lib/io/arrow-right-a'; import { GrafanaTheme2, TraceKeyValuePair } from '@grafana/data'; -import { stylesFactory, withTheme2 } from '@grafana/ui'; +import { Icon, stylesFactory, withTheme2 } from '@grafana/ui'; import { autoColor } from '../Theme'; import { DURATION, NONE, TAG } from '../settings/SpanBarSettings'; -import { SpanBarOptions, SpanLinkFunc, TNil } from '../types'; +import { SpanBarOptions, SpanLinkFunc, TraceSpan, TNil } from '../types'; import { SpanLinks } from '../types/links'; -import { TraceSpan } from '../types/trace'; import SpanBar from './SpanBar'; import { SpanLinksMenu } from './SpanLinks'; @@ -43,13 +40,13 @@ const nameColumnClassName = 'nameColumn'; const getStyles = stylesFactory((theme: GrafanaTheme2) => { const animations = { + label: 'flash', flash: keyframes` - label: flash; from { background-color: ${autoColor(theme, '#68b9ff')}; } to { - background-color: default; + background-color: 'default'; } `, }; @@ -458,7 +455,8 @@ export class UnthemedSpanBarRow extends React.PureComponent { })} > {showErrorIcon && ( - { {serviceName}{' '} {rpc && ( - + {' '} + {rpc.serviceName} )} {noInstrumentedServer && ( - {' '} + {' '} {noInstrumentedServer.serviceName} diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianKeyValues.markers.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.markers.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianKeyValues.markers.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.markers.tsx diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianKeyValues.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.test.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianKeyValues.test.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.test.tsx diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianKeyValues.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.tsx similarity index 92% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianKeyValues.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.tsx index 66db2f97d76..dfc31d55f8d 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianKeyValues.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.tsx @@ -15,15 +15,12 @@ import { css } from '@emotion/css'; import cx from 'classnames'; import * as React from 'react'; -import IoIosArrowDown from 'react-icons/lib/io/ios-arrow-down'; -import IoIosArrowRight from 'react-icons/lib/io/ios-arrow-right'; import { GrafanaTheme2 } from '@grafana/data'; -import { useStyles2 } from '@grafana/ui'; +import { Icon, useStyles2 } from '@grafana/ui'; import { autoColor } from '../../Theme'; -import { TNil } from '../../types'; -import { TraceKeyValuePair, TraceLink } from '../../types/trace'; +import { TraceKeyValuePair, TraceLink, TNil } from '../../types'; import { uAlignIcon, uTxEllipsis } from '../../uberUtilityStyles'; import * as markers from './AccordianKeyValues.markers'; @@ -132,7 +129,11 @@ export default function AccordianKeyValues(props: AccordianKeyValuesProps) { let arrow: React.ReactNode | null = null; let headerProps: {} | null = null; if (interactive) { - arrow = isOpen ? : ; + arrow = isOpen ? ( + + ) : ( + + ); headerProps = { 'aria-checked': isOpen, onClick: isEmpty ? null : onToggle, diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianLogs.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.test.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianLogs.test.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.test.tsx diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianLogs.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.tsx similarity index 93% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianLogs.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.tsx index 72409092a1f..2eb8abefad3 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianLogs.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.tsx @@ -15,11 +15,9 @@ import { css } from '@emotion/css'; import { sortBy as _sortBy } from 'lodash'; import * as React from 'react'; -import IoIosArrowDown from 'react-icons/lib/io/ios-arrow-down'; -import IoIosArrowRight from 'react-icons/lib/io/ios-arrow-right'; import { GrafanaTheme2 } from '@grafana/data'; -import { useStyles2 } from '@grafana/ui'; +import { Icon, useStyles2 } from '@grafana/ui'; import { autoColor } from '../../Theme'; import { TNil } from '../../types'; @@ -77,7 +75,11 @@ export default function AccordianLogs(props: AccordianLogsProps) { let HeaderComponent: 'span' | 'a' = 'span'; let headerProps: {} | null = null; if (interactive) { - arrow = isOpen ? : ; + arrow = isOpen ? ( + + ) : ( + + ); HeaderComponent = 'a'; headerProps = { 'aria-checked': isOpen, diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianReferences.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianReferences.test.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianReferences.test.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianReferences.test.tsx diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx similarity index 96% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx index 3d4defe5cf6..24bf571ba9a 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx @@ -14,8 +14,6 @@ import { css, cx } from '@emotion/css'; import * as React from 'react'; -import IoIosArrowDown from 'react-icons/lib/io/ios-arrow-down'; -import IoIosArrowRight from 'react-icons/lib/io/ios-arrow-right'; import { Field, GrafanaTheme2, LinkModel } from '@grafana/data'; import { Icon, useStyles2 } from '@grafana/ui'; @@ -199,7 +197,11 @@ const AccordianReferences: React.FC = ({ let HeaderComponent: 'span' | 'a' = 'span'; let headerProps: {} | null = null; if (interactive) { - arrow = isOpen ? : ; + arrow = isOpen ? ( + + ) : ( + + ); HeaderComponent = 'a'; headerProps = { 'aria-checked': isOpen, diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianText.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianText.test.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianText.test.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianText.test.tsx diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianText.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianText.tsx similarity index 91% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianText.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianText.tsx index c7bbb516562..bcc2d6fa886 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianText.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianText.tsx @@ -15,11 +15,9 @@ import { css } from '@emotion/css'; import cx from 'classnames'; import * as React from 'react'; -import IoIosArrowDown from 'react-icons/lib/io/ios-arrow-down'; -import IoIosArrowRight from 'react-icons/lib/io/ios-arrow-right'; import { GrafanaTheme2 } from '@grafana/data'; -import { useStyles2 } from '@grafana/ui'; +import { Icon, useStyles2 } from '@grafana/ui'; import { autoColor } from '../../Theme'; import { TNil } from '../../types'; @@ -76,7 +74,11 @@ export default function AccordianText(props: AccordianTextProps) { let arrow: React.ReactNode | null = null; let headerProps: {} | null = null; if (interactive) { - arrow = isOpen ? : ; + arrow = isOpen ? ( + + ) : ( + + ); headerProps = { 'aria-checked': isOpen, onClick: isEmpty ? null : onToggle, diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/DetailState.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/DetailState.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/DetailState.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/DetailState.tsx diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/KeyValuesTable.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable.test.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/KeyValuesTable.test.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable.test.tsx diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx similarity index 97% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx index ec5ad19b68e..df3675a3d2c 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx @@ -22,8 +22,7 @@ import { Icon, useStyles2 } from '@grafana/ui'; import { autoColor } from '../../Theme'; import CopyIcon from '../../common/CopyIcon'; -import { TNil } from '../../types'; -import { TraceKeyValuePair, TraceLink } from '../../types/trace'; +import { TraceKeyValuePair, TraceLink, TNil } from '../../types'; import { ubInlineBlock, uWidth100 } from '../../uberUtilityStyles'; const copyIconClassName = 'copyIcon'; diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/TextList.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/TextList.test.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/TextList.test.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/TextList.test.tsx diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/TextList.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/TextList.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/TextList.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/TextList.tsx diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.test.tsx similarity index 99% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.test.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.test.tsx index 081ec6bab28..260ec358c33 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.test.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.test.tsx @@ -17,10 +17,10 @@ jest.mock('../utils'); import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { TraceSpanReference } from 'src/types/trace'; import traceGenerator from '../../demo/trace-generators'; import transformTraceData from '../../model/transform-trace-data'; +import { TraceSpanReference } from '../../types/trace'; import { formatDuration } from '../utils'; import DetailState from './DetailState'; diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx similarity index 98% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx index 7842adf0929..f11a77dc230 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx @@ -15,11 +15,10 @@ import { css } from '@emotion/css'; import cx from 'classnames'; import React from 'react'; -import IoLink from 'react-icons/lib/io/link'; import { dateTimeFormat, GrafanaTheme2, LinkModel, TimeZone } from '@grafana/data'; import { config, reportInteraction } from '@grafana/runtime'; -import { Button, DataLinkButton, TextArea, useStyles2 } from '@grafana/ui'; +import { Button, DataLinkButton, Icon, TextArea, useStyles2 } from '@grafana/ui'; import { autoColor } from '../../Theme'; import { Divider } from '../../common/Divider'; @@ -344,7 +343,7 @@ export default function SpanDetail(props: SpanDetailProps) { } }} > - + {spanID} diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetailRow.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.test.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetailRow.test.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.test.tsx diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetailRow.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetailRow.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.tsx diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanLinks.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanLinks.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/SpanLinks.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanLinks.tsx diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanTreeOffset.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanTreeOffset.test.tsx similarity index 99% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/SpanTreeOffset.test.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanTreeOffset.test.tsx index f9b91ef0899..a20f35688c6 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanTreeOffset.test.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanTreeOffset.test.tsx @@ -15,10 +15,10 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { TraceSpan } from 'src/types/trace'; import { createTheme } from '@grafana/data'; +import { TraceSpan } from '../types'; import spanAncestorIdsSpy from '../utils/span-ancestor-ids'; import SpanTreeOffset, { getStyles, TProps } from './SpanTreeOffset'; diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanTreeOffset.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanTreeOffset.tsx similarity index 94% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/SpanTreeOffset.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanTreeOffset.tsx index 149252a2f77..452389a5572 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanTreeOffset.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanTreeOffset.tsx @@ -16,14 +16,12 @@ import { css } from '@emotion/css'; import cx from 'classnames'; import { get as _get } from 'lodash'; import React from 'react'; -import IoChevronRight from 'react-icons/lib/io/chevron-right'; -import IoIosArrowDown from 'react-icons/lib/io/ios-arrow-down'; import { GrafanaTheme2 } from '@grafana/data'; -import { stylesFactory, withTheme2 } from '@grafana/ui'; +import { Icon, stylesFactory, withTheme2 } from '@grafana/ui'; import { autoColor } from '../Theme'; -import { TraceSpan } from '../types/trace'; +import { TraceSpan } from '../types'; import spanAncestorIds from '../utils/span-ancestor-ids'; export const getStyles = stylesFactory((theme: GrafanaTheme2) => { @@ -142,9 +140,9 @@ export class UnthemedSpanTreeOffset extends React.PureComponent { showChildrenIcon && hasChildren && (childrenVisible ? ( - + ) : ( - + )); const styles = getStyles(theme); return ( diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/Ticks.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/Ticks.test.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/Ticks.test.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/Ticks.test.tsx diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/Ticks.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/Ticks.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/Ticks.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/Ticks.tsx diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineCollapser.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineCollapser.test.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineCollapser.test.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineCollapser.test.tsx diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineCollapser.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineCollapser.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineCollapser.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineCollapser.tsx diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.test.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.test.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.test.tsx diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.tsx similarity index 99% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.tsx index d8be774af0f..6e58221feb4 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.tsx @@ -178,7 +178,6 @@ export default class TimelineColumnResizer extends React.PureComponent< if (this._dragManager.isDragging() && this._rootElm && dragPosition != null) { isDraggingLeft = dragPosition < position; isDraggingRight = dragPosition > position; - left = `${dragPosition * 100}%`; // Draw a highlight from the current dragged position back to the original // position, e.g. highlight the change. Draw the highlight via `left` and // `right` css styles (simpler than using `width`). diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.test.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.test.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.test.tsx diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.tsx diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.test.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.test.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.test.tsx diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.tsx similarity index 99% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.tsx index adfb8624894..273c1123ecf 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.tsx @@ -85,7 +85,7 @@ export type TimelineViewingLayerProps = { * bounds for dragging need to be recalculated. In practice, the name column * width serves fine for this. */ - boundsInvalidator: any | null | undefined; + boundsInvalidator: number | null | undefined; updateNextViewRangeTime: (update: ViewRangeTimeUpdate) => void; updateViewRangeTime: TUpdateViewRangeTimeFunction; viewRangeTime: ViewRangeTime; diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/index.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/index.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/index.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/index.tsx diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineRow.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineRow.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineRow.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineRow.tsx diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.test.tsx similarity index 99% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.test.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.test.tsx index f3dab2e1334..e387616ce86 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.test.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.test.tsx @@ -13,10 +13,10 @@ // limitations under the License. import { render, screen } from '@testing-library/react'; import React from 'react'; -import { Trace } from 'src/types/trace'; import traceGenerator from '../demo/trace-generators'; import transformTraceData from '../model/transform-trace-data'; +import { Trace } from '../types'; import SpanTreeOffset from './SpanTreeOffset'; import VirtualizedTraceView, { VirtualizedTraceViewProps } from './VirtualizedTraceView'; diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.tsx similarity index 97% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.tsx index e98caf6460a..994bc9270e0 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.tsx @@ -16,7 +16,7 @@ import { css } from '@emotion/css'; import { isEqual } from 'lodash'; import memoizeOne from 'memoize-one'; import * as React from 'react'; -import { createRef, RefObject } from 'react'; +import { RefObject } from 'react'; import { GrafanaTheme2, LinkModel, TimeZone } from '@grafana/data'; import { config, reportInteraction } from '@grafana/runtime'; @@ -196,7 +196,6 @@ const memoizedGetClipping = memoizeOne(getClipping, isEqual); // export from tests export class UnthemedVirtualizedTraceView extends React.Component { listView: ListView | TNil; - topTraceViewRef = createRef(); constructor(props: VirtualizedTraceViewProps) { super(props); @@ -210,12 +209,12 @@ export class UnthemedVirtualizedTraceView extends React.Component; - for (let i = 0; i < nextPropKeys.length; i += 1) { - if (nextProps[nextPropKeys[i]] !== this.props[nextPropKeys[i]]) { + let key: keyof VirtualizedTraceViewProps; + for (key in nextProps) { + if (nextProps[key] !== this.props[key]) { // Unless the only change was props.shouldScrollToFirstUiFindMatch changing to false. - if (nextPropKeys[i] === 'shouldScrollToFirstUiFindMatch') { - if (nextProps[nextPropKeys[i]]) { + if (key === 'shouldScrollToFirstUiFindMatch') { + if (nextProps[key]) { return true; } } else { diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/index.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/index.test.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/index.test.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/index.test.tsx diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/index.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/index.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/index.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/index.tsx diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/types.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/types.tsx similarity index 100% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/types.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/types.tsx diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/utils.test.ts b/public/app/features/explore/TraceView/components/TraceTimelineViewer/utils.test.ts similarity index 99% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/utils.test.ts rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/utils.test.ts index cbb51960592..50d87b73bc8 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/utils.test.ts +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/utils.test.ts @@ -12,9 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { TraceSpan } from 'src/types/trace'; - import traceGenerator from '../demo/trace-generators'; +import { TraceSpan } from '../types'; import { findServerChildSpan, diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/utils.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/utils.tsx similarity index 99% rename from packages/jaeger-ui-components/src/TraceTimelineViewer/utils.tsx rename to public/app/features/explore/TraceView/components/TraceTimelineViewer/utils.tsx index caf79e46cd3..bdd9a5887e8 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/utils.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/utils.tsx @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { TraceSpan } from '../types/trace'; +import { TraceSpan } from '../types'; export type ViewedBoundsFunctionType = (start: number, end: number) => { start: number; end: number }; /** diff --git a/packages/jaeger-ui-components/src/Tween.test.ts b/public/app/features/explore/TraceView/components/Tween.test.ts similarity index 100% rename from packages/jaeger-ui-components/src/Tween.test.ts rename to public/app/features/explore/TraceView/components/Tween.test.ts diff --git a/packages/jaeger-ui-components/src/Tween.tsx b/public/app/features/explore/TraceView/components/Tween.tsx similarity index 100% rename from packages/jaeger-ui-components/src/Tween.tsx rename to public/app/features/explore/TraceView/components/Tween.tsx diff --git a/packages/jaeger-ui-components/src/common/BreakableText.tsx b/public/app/features/explore/TraceView/components/common/BreakableText.tsx similarity index 97% rename from packages/jaeger-ui-components/src/common/BreakableText.tsx rename to public/app/features/explore/TraceView/components/common/BreakableText.tsx index ad8c4cecb6d..26ad069eb40 100644 --- a/packages/jaeger-ui-components/src/common/BreakableText.tsx +++ b/public/app/features/explore/TraceView/components/common/BreakableText.tsx @@ -43,7 +43,7 @@ export default function BreakableText( const { className, text, wordRegexp = WORD_RX } = props; const styles = useStyles2(getStyles); if (!text) { - return typeof text === 'string' ? text : null; + return null; } const spans = []; wordRegexp.exec(''); diff --git a/packages/jaeger-ui-components/src/common/CopyIcon.test.tsx b/public/app/features/explore/TraceView/components/common/CopyIcon.test.tsx similarity index 90% rename from packages/jaeger-ui-components/src/common/CopyIcon.test.tsx rename to public/app/features/explore/TraceView/components/common/CopyIcon.test.tsx index 9f0101501f1..9e00636459d 100644 --- a/packages/jaeger-ui-components/src/common/CopyIcon.test.tsx +++ b/public/app/features/explore/TraceView/components/common/CopyIcon.test.tsx @@ -13,12 +13,15 @@ // limitations under the License. import { render, screen } from '@testing-library/react'; -import * as copy from 'copy-to-clipboard'; import React from 'react'; import CopyIcon from './CopyIcon'; -jest.mock('copy-to-clipboard'); +Object.assign(navigator, { + clipboard: { + writeText: () => {}, + }, +}); describe('', () => { const props = { @@ -29,7 +32,7 @@ describe('', () => { let copySpy: jest.SpyInstance; beforeAll(() => { - copySpy = jest.spyOn(copy, 'default'); + copySpy = jest.spyOn(navigator.clipboard, 'writeText'); }); beforeEach(() => { diff --git a/packages/jaeger-ui-components/src/common/CopyIcon.tsx b/public/app/features/explore/TraceView/components/common/CopyIcon.tsx similarity index 96% rename from packages/jaeger-ui-components/src/common/CopyIcon.tsx rename to public/app/features/explore/TraceView/components/common/CopyIcon.tsx index 5fb166a7ada..f2d01c21c00 100644 --- a/packages/jaeger-ui-components/src/common/CopyIcon.tsx +++ b/public/app/features/explore/TraceView/components/common/CopyIcon.tsx @@ -14,7 +14,6 @@ import { css } from '@emotion/css'; import cx from 'classnames'; -import copy from 'copy-to-clipboard'; import React, { useState } from 'react'; import { Button, IconName, Tooltip, useStyles2 } from '@grafana/ui'; @@ -48,7 +47,7 @@ export default function CopyIcon(props: PropsType) { const [hasCopied, setHasCopied] = useState(false); const handleClick = () => { - copy(props.copyText); + navigator.clipboard.writeText(props.copyText); setHasCopied(true); }; diff --git a/packages/jaeger-ui-components/src/common/Divider.tsx b/public/app/features/explore/TraceView/components/common/Divider.tsx similarity index 100% rename from packages/jaeger-ui-components/src/common/Divider.tsx rename to public/app/features/explore/TraceView/components/common/Divider.tsx diff --git a/packages/jaeger-ui-components/src/common/ExternalLinks.tsx b/public/app/features/explore/TraceView/components/common/ExternalLinks.tsx similarity index 100% rename from packages/jaeger-ui-components/src/common/ExternalLinks.tsx rename to public/app/features/explore/TraceView/components/common/ExternalLinks.tsx diff --git a/packages/jaeger-ui-components/src/common/LabeledList.tsx b/public/app/features/explore/TraceView/components/common/LabeledList.tsx similarity index 97% rename from packages/jaeger-ui-components/src/common/LabeledList.tsx rename to public/app/features/explore/TraceView/components/common/LabeledList.tsx index 7ce89630e02..58df134c695 100644 --- a/packages/jaeger-ui-components/src/common/LabeledList.tsx +++ b/public/app/features/explore/TraceView/components/common/LabeledList.tsx @@ -28,7 +28,7 @@ const getStyles = (divider: boolean) => (theme: GrafanaTheme2) => { list-style: none; margin: 0; padding: 0; - ${divider === true && + ${divider && ` margin-right: -8px; display: flex; @@ -39,7 +39,7 @@ const getStyles = (divider: boolean) => (theme: GrafanaTheme2) => { LabeledListItem: css` label: LabeledListItem; display: inline-block; - ${divider === true && + ${divider && ` border-right: 1px solid ${autoColor(theme, '#ddd')}; padding: 0 8px; diff --git a/packages/jaeger-ui-components/src/common/NewWindowIcon.tsx b/public/app/features/explore/TraceView/components/common/NewWindowIcon.tsx similarity index 88% rename from packages/jaeger-ui-components/src/common/NewWindowIcon.tsx rename to public/app/features/explore/TraceView/components/common/NewWindowIcon.tsx index 1f5185d5df5..804d10fa03f 100644 --- a/packages/jaeger-ui-components/src/common/NewWindowIcon.tsx +++ b/public/app/features/explore/TraceView/components/common/NewWindowIcon.tsx @@ -15,9 +15,8 @@ import { css } from '@emotion/css'; import cx from 'classnames'; import React from 'react'; -import IoAndroidOpen from 'react-icons/lib/io/android-open'; -import { useStyles2 } from '@grafana/ui'; +import { Icon, useStyles2 } from '@grafana/ui'; export const getStyles = () => { return { @@ -37,7 +36,7 @@ export default function NewWindowIcon(props: Props) { const { isLarge, className, ...rest } = props; const styles = useStyles2(getStyles); const cls = cx({ [styles.NewWindowIconLarge]: isLarge }, className); - return ; + return ; } NewWindowIcon.defaultProps = { diff --git a/packages/jaeger-ui-components/src/common/Popover.tsx b/public/app/features/explore/TraceView/components/common/Popover.tsx similarity index 100% rename from packages/jaeger-ui-components/src/common/Popover.tsx rename to public/app/features/explore/TraceView/components/common/Popover.tsx diff --git a/packages/jaeger-ui-components/src/common/TraceName.tsx b/public/app/features/explore/TraceView/components/common/TraceName.tsx similarity index 100% rename from packages/jaeger-ui-components/src/common/TraceName.tsx rename to public/app/features/explore/TraceView/components/common/TraceName.tsx diff --git a/packages/jaeger-ui-components/src/common/UiFindInput.test.tsx b/public/app/features/explore/TraceView/components/common/UiFindInput.test.tsx similarity index 100% rename from packages/jaeger-ui-components/src/common/UiFindInput.test.tsx rename to public/app/features/explore/TraceView/components/common/UiFindInput.test.tsx diff --git a/packages/jaeger-ui-components/src/common/UiFindInput.tsx b/public/app/features/explore/TraceView/components/common/UiFindInput.tsx similarity index 94% rename from packages/jaeger-ui-components/src/common/UiFindInput.tsx rename to public/app/features/explore/TraceView/components/common/UiFindInput.tsx index b6ca9fff3fe..c98db541ae4 100644 --- a/packages/jaeger-ui-components/src/common/UiFindInput.tsx +++ b/public/app/features/explore/TraceView/components/common/UiFindInput.tsx @@ -16,13 +16,12 @@ import * as React from 'react'; import { IconButton, Input } from '@grafana/ui'; -import { TNil } from '../types/index'; +import { TNil } from '../types'; type Props = { allowClear?: boolean; - inputProps: Record; + inputProps: Record; location: Location; - match: any; trackFindFunction?: (str: string | TNil) => void; value: string | undefined; onChange: (value: string) => void; diff --git a/packages/jaeger-ui-components/src/constants/index.tsx b/public/app/features/explore/TraceView/components/constants/default-config.ts similarity index 50% rename from packages/jaeger-ui-components/src/constants/index.tsx rename to public/app/features/explore/TraceView/components/constants/default-config.ts index 3d2483e6344..7fddc8d938a 100644 --- a/packages/jaeger-ui-components/src/constants/index.tsx +++ b/public/app/features/explore/TraceView/components/constants/default-config.ts @@ -12,15 +12,29 @@ // See the License for the specific language governing permissions and // limitations under the License. -export const FALLBACK_DAG_MAX_NUM_SERVICES = 100 as 100; -export const FALLBACK_TRACE_NAME = '' as ''; +import { FALLBACK_DAG_MAX_NUM_SERVICES } from './index'; -export const FETCH_DONE = 'FETCH_DONE' as 'FETCH_DONE'; -export const FETCH_ERROR = 'FETCH_ERROR' as 'FETCH_ERROR'; -export const FETCH_LOADING = 'FETCH_LOADING' as 'FETCH_LOADING'; - -export const fetchedState = { - DONE: FETCH_DONE, - ERROR: FETCH_ERROR, - LOADING: FETCH_LOADING, -}; +export default Object.defineProperty( + { + archiveEnabled: false, + dependencies: { + dagMaxNumServices: FALLBACK_DAG_MAX_NUM_SERVICES, + menuEnabled: true, + }, + linkPatterns: [], + search: { + maxLookback: { + label: '2 Days', + value: '2d', + }, + maxLimit: 1500, + }, + tracking: { + gaID: null, + trackErrors: true, + }, + }, + // fields that should be individually merged vs wholesale replaced + '__mergeFields', + { value: ['dependencies', 'search', 'tracking'] } +); diff --git a/packages/jaeger-ui-components/src/types/api-error.tsx b/public/app/features/explore/TraceView/components/constants/index.tsx similarity index 75% rename from packages/jaeger-ui-components/src/types/api-error.tsx rename to public/app/features/explore/TraceView/components/constants/index.tsx index fd72371e34f..15726a21be1 100644 --- a/packages/jaeger-ui-components/src/types/api-error.tsx +++ b/public/app/features/explore/TraceView/components/constants/index.tsx @@ -12,13 +12,5 @@ // See the License for the specific language governing permissions and // limitations under the License. -export type ApiError = - | string - | { - message: string; - httpStatus?: any; - httpStatusText?: string; - httpUrl?: string; - httpQuery?: string; - httpBody?: string; - }; +export const FALLBACK_DAG_MAX_NUM_SERVICES = 100 as 100; +export const FALLBACK_TRACE_NAME = ''; diff --git a/packages/jaeger-ui-components/src/constants/tag-keys.tsx b/public/app/features/explore/TraceView/components/constants/tag-keys.tsx similarity index 77% rename from packages/jaeger-ui-components/src/constants/tag-keys.tsx rename to public/app/features/explore/TraceView/components/constants/tag-keys.tsx index fb4587a547d..0aec89c75a5 100644 --- a/packages/jaeger-ui-components/src/constants/tag-keys.tsx +++ b/public/app/features/explore/TraceView/components/constants/tag-keys.tsx @@ -12,6 +12,4 @@ // See the License for the specific language governing permissions and // limitations under the License. -export const HTTP_METHOD = 'http.method' as 'http.method'; -export const PEER_SERVICE = 'peer.service' as 'peer.service'; -export const SPAN_KIND = 'span.kind' as 'span.kind'; +export const PEER_SERVICE = 'peer.service'; diff --git a/packages/jaeger-ui-components/src/demo/.eslintrc b/public/app/features/explore/TraceView/components/demo/.eslintrc similarity index 100% rename from packages/jaeger-ui-components/src/demo/.eslintrc rename to public/app/features/explore/TraceView/components/demo/.eslintrc diff --git a/packages/jaeger-ui-components/src/demo/chance.d.ts b/public/app/features/explore/TraceView/components/demo/chance.d.ts similarity index 100% rename from packages/jaeger-ui-components/src/demo/chance.d.ts rename to public/app/features/explore/TraceView/components/demo/chance.d.ts diff --git a/packages/jaeger-ui-components/src/demo/trace-generators.ts b/public/app/features/explore/TraceView/components/demo/trace-generators.ts similarity index 97% rename from packages/jaeger-ui-components/src/demo/trace-generators.ts rename to public/app/features/explore/TraceView/components/demo/trace-generators.ts index 4cdd08a4609..a87889233f4 100644 --- a/packages/jaeger-ui-components/src/demo/trace-generators.ts +++ b/public/app/features/explore/TraceView/components/demo/trace-generators.ts @@ -13,7 +13,8 @@ // limitations under the License. import Chance from 'chance'; -import { TraceSpanData, TraceProcess } from 'src/types/trace'; + +import { TraceSpanData, TraceProcess } from 'app/features/explore/TraceView/components/types/trace'; import { getSpanId } from '../selectors/span'; @@ -119,7 +120,6 @@ export default chance.mixin({ }); spans = attachReferences(spans, maxDepth, spansPerLevel); if (spans.length > 1) { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions spans = setupParentSpan(spans, { startTime: timestamp, duration } as TraceSpanData); } diff --git a/packages/jaeger-ui-components/src/index.ts b/public/app/features/explore/TraceView/components/index.ts similarity index 100% rename from packages/jaeger-ui-components/src/index.ts rename to public/app/features/explore/TraceView/components/index.ts diff --git a/packages/jaeger-ui-components/src/keyboard-mappings.tsx b/public/app/features/explore/TraceView/components/keyboard-mappings.tsx similarity index 100% rename from packages/jaeger-ui-components/src/keyboard-mappings.tsx rename to public/app/features/explore/TraceView/components/keyboard-mappings.tsx diff --git a/packages/jaeger-ui-components/src/keyboard-shortcuts.tsx b/public/app/features/explore/TraceView/components/keyboard-shortcuts.tsx similarity index 92% rename from packages/jaeger-ui-components/src/keyboard-shortcuts.tsx rename to public/app/features/explore/TraceView/components/keyboard-shortcuts.tsx index 692935f7940..d038611b5d2 100644 --- a/packages/jaeger-ui-components/src/keyboard-shortcuts.tsx +++ b/public/app/features/explore/TraceView/components/keyboard-shortcuts.tsx @@ -19,8 +19,8 @@ import keyboardMappings from './keyboard-mappings'; export type CombokeysHandler = | (() => void) - | ((event: React.KeyboardEvent) => void) - | ((event: React.KeyboardEvent, s: string) => void); + | ((event: React.KeyboardEvent) => void) + | ((event: React.KeyboardEvent, s: string) => void); export type ShortcutCallbacks = { [name: string]: CombokeysHandler; diff --git a/packages/jaeger-ui-components/src/model/ddg/PathElem.test.ts b/public/app/features/explore/TraceView/components/model/ddg/PathElem.test.ts similarity index 100% rename from packages/jaeger-ui-components/src/model/ddg/PathElem.test.ts rename to public/app/features/explore/TraceView/components/model/ddg/PathElem.test.ts diff --git a/packages/jaeger-ui-components/src/model/ddg/PathElem.tsx b/public/app/features/explore/TraceView/components/model/ddg/PathElem.tsx similarity index 100% rename from packages/jaeger-ui-components/src/model/ddg/PathElem.tsx rename to public/app/features/explore/TraceView/components/model/ddg/PathElem.tsx diff --git a/packages/jaeger-ui-components/src/model/ddg/__snapshots__/PathElem.test.ts.snap b/public/app/features/explore/TraceView/components/model/ddg/__snapshots__/PathElem.test.ts.snap similarity index 100% rename from packages/jaeger-ui-components/src/model/ddg/__snapshots__/PathElem.test.ts.snap rename to public/app/features/explore/TraceView/components/model/ddg/__snapshots__/PathElem.test.ts.snap diff --git a/packages/jaeger-ui-components/src/model/ddg/sample-paths.test.resources.ts b/public/app/features/explore/TraceView/components/model/ddg/sample-paths.test.resources.ts similarity index 100% rename from packages/jaeger-ui-components/src/model/ddg/sample-paths.test.resources.ts rename to public/app/features/explore/TraceView/components/model/ddg/sample-paths.test.resources.ts diff --git a/packages/jaeger-ui-components/src/model/ddg/types.tsx b/public/app/features/explore/TraceView/components/model/ddg/types.tsx similarity index 76% rename from packages/jaeger-ui-components/src/model/ddg/types.tsx rename to public/app/features/explore/TraceView/components/model/ddg/types.tsx index f6d4f0f965a..6705c4ea0c3 100644 --- a/packages/jaeger-ui-components/src/model/ddg/types.tsx +++ b/public/app/features/explore/TraceView/components/model/ddg/types.tsx @@ -27,20 +27,8 @@ export type TDdgOperation = { service: TDdgService; }; -export type TDdgServiceMap = Map; - export type TDdgPath = { focalIdx: number; members: PathElem[]; traceIDs: string[]; }; - -export type TDdgDistanceToPathElems = Map; - -export type TDdgModel = { - distanceToPathElems: TDdgDistanceToPathElems; - hash: string; - paths: TDdgPath[]; - services: TDdgServiceMap; - visIdxToPathElem: PathElem[]; -}; diff --git a/packages/jaeger-ui-components/src/model/find-trace-name.test.ts b/public/app/features/explore/TraceView/components/model/find-trace-name.test.ts similarity index 99% rename from packages/jaeger-ui-components/src/model/find-trace-name.test.ts rename to public/app/features/explore/TraceView/components/model/find-trace-name.test.ts index bec8fefd8c3..d27cc98bdc4 100644 --- a/packages/jaeger-ui-components/src/model/find-trace-name.test.ts +++ b/public/app/features/explore/TraceView/components/model/find-trace-name.test.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { TraceSpan } from '../types/trace'; +import { TraceSpan } from '../types'; import { _getTraceNameImpl as getTraceName } from './trace-viewer'; diff --git a/packages/jaeger-ui-components/src/model/link-patterns.test.ts b/public/app/features/explore/TraceView/components/model/link-patterns.test.ts similarity index 98% rename from packages/jaeger-ui-components/src/model/link-patterns.test.ts rename to public/app/features/explore/TraceView/components/model/link-patterns.test.ts index 0a6768febd9..d31063ffc7c 100644 --- a/packages/jaeger-ui-components/src/model/link-patterns.test.ts +++ b/public/app/features/explore/TraceView/components/model/link-patterns.test.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Trace, TraceLink, TraceSpan } from '../types/trace'; +import { Trace, TraceLink, TraceSpan } from '../types'; import { processTemplate, @@ -67,7 +67,6 @@ describe('processTemplate()', () => { expect(() => processTemplate( { - /* eslint-disable @typescript-eslint/no-explicit-any */ template: (data: { [key: string]: any }) => `a${data.b}c`, }, (a) => a @@ -417,7 +416,6 @@ describe('getLinks()', () => { const span = { depth: 0, process: {}, tags: [{ key: 'mySpecialKey', value: 'valueOfMyKey' }] } as TraceSpan; - /* eslint-disable @typescript-eslint/no-explicit-any */ let cache: WeakMap; beforeEach(() => { diff --git a/packages/jaeger-ui-components/src/model/link-patterns.tsx b/public/app/features/explore/TraceView/components/model/link-patterns.tsx similarity index 98% rename from packages/jaeger-ui-components/src/model/link-patterns.tsx rename to public/app/features/explore/TraceView/components/model/link-patterns.tsx index 9f74ccb532f..696c13c690c 100644 --- a/packages/jaeger-ui-components/src/model/link-patterns.tsx +++ b/public/app/features/explore/TraceView/components/model/link-patterns.tsx @@ -15,8 +15,7 @@ import { uniq as _uniq } from 'lodash'; import memoize from 'lru-memoize'; -import { TNil } from '../types'; -import { TraceSpan, TraceLink, TraceKeyValuePair, Trace } from '../types/trace'; +import { TraceSpan, TraceLink, TraceKeyValuePair, Trace, TNil } from '../types'; import { getConfigValue } from '../utils/config/get-config'; import { getParent } from './span'; diff --git a/packages/jaeger-ui-components/src/model/span.tsx b/public/app/features/explore/TraceView/components/model/span.tsx similarity index 95% rename from packages/jaeger-ui-components/src/model/span.tsx rename to public/app/features/explore/TraceView/components/model/span.tsx index 1450c357faf..07764898a24 100644 --- a/packages/jaeger-ui-components/src/model/span.tsx +++ b/public/app/features/explore/TraceView/components/model/span.tsx @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { TraceSpan } from '../types/trace'; +import { TraceSpan } from '../types'; /** * Searches the span.references to find 'CHILD_OF' reference type or returns null. diff --git a/packages/jaeger-ui-components/src/model/trace-viewer.ts b/public/app/features/explore/TraceView/components/model/trace-viewer.ts similarity index 97% rename from packages/jaeger-ui-components/src/model/trace-viewer.ts rename to public/app/features/explore/TraceView/components/model/trace-viewer.ts index 105821920e6..857c4750587 100644 --- a/packages/jaeger-ui-components/src/model/trace-viewer.ts +++ b/public/app/features/explore/TraceView/components/model/trace-viewer.ts @@ -14,7 +14,7 @@ import { memoize } from 'lodash'; -import { TraceSpan } from '../types/trace'; +import { TraceSpan } from '../types'; export function _getTraceNameImpl(spans: TraceSpan[]) { // Use a span with no references to another span in given array diff --git a/packages/jaeger-ui-components/src/model/transform-trace-data.test.ts b/public/app/features/explore/TraceView/components/model/transform-trace-data.test.ts similarity index 98% rename from packages/jaeger-ui-components/src/model/transform-trace-data.test.ts rename to public/app/features/explore/TraceView/components/model/transform-trace-data.test.ts index 980b0bfabb9..8c3270ca88c 100644 --- a/packages/jaeger-ui-components/src/model/transform-trace-data.test.ts +++ b/public/app/features/explore/TraceView/components/model/transform-trace-data.test.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { TraceResponse } from '../types/trace'; +import { TraceResponse } from '../types'; import transformTraceData, { orderTags, deduplicateTags } from './transform-trace-data'; diff --git a/packages/jaeger-ui-components/src/model/transform-trace-data.tsx b/public/app/features/explore/TraceView/components/model/transform-trace-data.tsx similarity index 97% rename from packages/jaeger-ui-components/src/model/transform-trace-data.tsx rename to public/app/features/explore/TraceView/components/model/transform-trace-data.tsx index 49c10fac0aa..d116874f1cb 100644 --- a/packages/jaeger-ui-components/src/model/transform-trace-data.tsx +++ b/public/app/features/explore/TraceView/components/model/transform-trace-data.tsx @@ -16,7 +16,7 @@ import { isEqual as _isEqual } from 'lodash'; // @ts-ignore import { getTraceSpanIdsAsTree } from '../selectors/trace'; -import { TraceKeyValuePair, TraceSpan, Trace, TraceResponse, TraceProcess } from '../types/trace'; +import { TraceKeyValuePair, TraceSpan, Trace, TraceResponse, TraceProcess } from '../types'; // @ts-ignore import TreeNode from '../utils/TreeNode'; import { getConfigValue } from '../utils/config/get-config'; @@ -142,7 +142,7 @@ export default function transformTraceData(data: TraceResponse | undefined): Tra if (typeof spanID !== 'string') { return; } - const span = spanMap.get(spanID) as TraceSpan; + const span = spanMap.get(spanID); if (!span) { return; } @@ -159,7 +159,7 @@ export default function transformTraceData(data: TraceResponse | undefined): Tra span.tags = orderTags(tagsInfo.tags, getConfigValue('topTagPrefixes')); span.warnings = span.warnings.concat(tagsInfo.warnings); span.references.forEach((ref, index) => { - const refSpan = spanMap.get(ref.spanID) as TraceSpan; + const refSpan = spanMap.get(ref.spanID); if (refSpan) { // eslint-disable-next-line no-param-reassign ref.span = refSpan; diff --git a/packages/jaeger-ui-components/src/scroll-page.test.ts b/public/app/features/explore/TraceView/components/scroll-page.test.ts similarity index 99% rename from packages/jaeger-ui-components/src/scroll-page.test.ts rename to public/app/features/explore/TraceView/components/scroll-page.test.ts index 5c4a3e7f0c9..cff5b4084ba 100644 --- a/packages/jaeger-ui-components/src/scroll-page.test.ts +++ b/public/app/features/explore/TraceView/components/scroll-page.test.ts @@ -23,7 +23,6 @@ const tweenInstances: Tween[] = []; describe('scroll-by', () => { beforeEach(() => { - window.scrollY = 100; tweenInstances.length = 0; jest.mocked(Tween).mockClear(); jest.mocked(Tween).mockImplementation((opts) => { diff --git a/packages/jaeger-ui-components/src/scroll-page.tsx b/public/app/features/explore/TraceView/components/scroll-page.tsx similarity index 100% rename from packages/jaeger-ui-components/src/scroll-page.tsx rename to public/app/features/explore/TraceView/components/scroll-page.tsx diff --git a/packages/jaeger-ui-components/src/selectors/process.test.ts b/public/app/features/explore/TraceView/components/selectors/process.test.ts similarity index 96% rename from packages/jaeger-ui-components/src/selectors/process.test.ts rename to public/app/features/explore/TraceView/components/selectors/process.test.ts index d3928edfe43..cf6ad8caa7f 100644 --- a/packages/jaeger-ui-components/src/selectors/process.test.ts +++ b/public/app/features/explore/TraceView/components/selectors/process.test.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. import traceGenerator from '../demo/trace-generators'; -import { TraceProcess } from '../types/trace'; +import { TraceProcess } from '../types'; import * as processSelectors from './process'; diff --git a/packages/jaeger-ui-components/src/selectors/process.ts b/public/app/features/explore/TraceView/components/selectors/process.ts similarity index 94% rename from packages/jaeger-ui-components/src/selectors/process.ts rename to public/app/features/explore/TraceView/components/selectors/process.ts index d8d1a021630..91c2bcaa605 100644 --- a/packages/jaeger-ui-components/src/selectors/process.ts +++ b/public/app/features/explore/TraceView/components/selectors/process.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { TraceProcess } from '../types/trace'; +import { TraceProcess } from '../types'; export const getProcessServiceName = (proc: TraceProcess) => proc.serviceName; export const getProcessTags = (proc: TraceProcess) => proc.tags; diff --git a/packages/jaeger-ui-components/src/selectors/span.test.ts b/public/app/features/explore/TraceView/components/selectors/span.test.ts similarity index 96% rename from packages/jaeger-ui-components/src/selectors/span.test.ts rename to public/app/features/explore/TraceView/components/selectors/span.test.ts index 0771b01283a..9186366a0f1 100644 --- a/packages/jaeger-ui-components/src/selectors/span.test.ts +++ b/public/app/features/explore/TraceView/components/selectors/span.test.ts @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { TraceResponse } from 'src/types'; -import { TraceSpan, TraceSpanData } from 'src/types/trace'; +import { TraceResponse } from 'app/features/explore/TraceView/components/types'; +import { TraceSpan, TraceSpanData } from 'app/features/explore/TraceView/components/types/trace'; import traceGenerator from '../demo/trace-generators'; diff --git a/packages/jaeger-ui-components/src/selectors/span.ts b/public/app/features/explore/TraceView/components/selectors/span.ts similarity index 76% rename from packages/jaeger-ui-components/src/selectors/span.ts rename to public/app/features/explore/TraceView/components/selectors/span.ts index 179dd1b8483..9f7d10eeb26 100644 --- a/packages/jaeger-ui-components/src/selectors/span.ts +++ b/public/app/features/explore/TraceView/components/selectors/span.ts @@ -12,9 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -import fuzzy from 'fuzzy'; import { createSelector } from 'reselect'; +import { fuzzyMatch } from '@grafana/ui'; + import { TraceSpan, TraceSpanData, TraceSpanReference } from '../types/trace'; import { getProcessServiceName } from './process'; @@ -61,30 +62,5 @@ export const filterSpansForText = createSelector( ({ spans }: { spans: TraceSpan[] }) => spans, ({ text }: { text: string }) => text, (spans, text) => - fuzzy - .filter(text, spans, { - extract: (span) => `${getSpanServiceName(span)} ${getSpanName(span)}`, - }) - .map(({ original }) => original) -); - -const getTextFilteredSpansAsMap = createSelector(filterSpansForText, (matchingSpans) => - matchingSpans.reduce( - (obj, span) => ({ - ...obj, - [getSpanId(span)]: span, - }), - {} - ) -); - -// TODO: delete this function as it is not used? -export const highlightSpansForTextFilter = createSelector( - ({ spans }: { spans: TraceSpanData[] }) => spans, - getTextFilteredSpansAsMap, - (spans, textFilteredSpansMap: { [key: string]: TraceSpanData }) => - spans.map((span: TraceSpanData) => ({ - ...span, - muted: !textFilteredSpansMap[getSpanId(span)], - })) + spans.filter((span) => (span ? fuzzyMatch(`${getSpanServiceName(span)} ${getSpanName(span)}`, text).found : false)) ); diff --git a/packages/jaeger-ui-components/src/selectors/trace.fixture.ts b/public/app/features/explore/TraceView/components/selectors/trace.fixture.ts similarity index 100% rename from packages/jaeger-ui-components/src/selectors/trace.fixture.ts rename to public/app/features/explore/TraceView/components/selectors/trace.fixture.ts diff --git a/packages/jaeger-ui-components/src/selectors/trace.test.ts b/public/app/features/explore/TraceView/components/selectors/trace.test.ts similarity index 99% rename from packages/jaeger-ui-components/src/selectors/trace.test.ts rename to public/app/features/explore/TraceView/components/selectors/trace.test.ts index dfaa9d28b93..1aec2d75aa8 100644 --- a/packages/jaeger-ui-components/src/selectors/trace.test.ts +++ b/public/app/features/explore/TraceView/components/selectors/trace.test.ts @@ -13,7 +13,8 @@ // limitations under the License. import { values as _values } from 'lodash'; -import TreeNode from 'src/utils/TreeNode'; + +import TreeNode from 'app/features/explore/TraceView/components/utils/TreeNode'; import traceGenerator from '../demo/trace-generators'; import { TraceResponse, TraceSpan, TraceSpanData } from '../types/trace'; diff --git a/packages/jaeger-ui-components/src/selectors/trace.ts b/public/app/features/explore/TraceView/components/selectors/trace.ts similarity index 81% rename from packages/jaeger-ui-components/src/selectors/trace.ts rename to public/app/features/explore/TraceView/components/selectors/trace.ts index f9c0ebb85ca..288910b07e6 100644 --- a/packages/jaeger-ui-components/src/selectors/trace.ts +++ b/public/app/features/explore/TraceView/components/selectors/trace.ts @@ -14,7 +14,7 @@ import { createSelector, createStructuredSelector } from 'reselect'; -import { Trace, TraceData, TraceProcess, TraceResponse, TraceSpan, TraceSpanData } from '../types/trace'; +import { Trace, TraceData, TraceProcess, TraceResponse, TraceSpanData } from '../types/trace'; import TreeNode from '../utils/TreeNode'; import { formatMillisecondTime, formatSecondTime, ONE_SECOND } from '../utils/date'; import { numberSortComparator } from '../utils/sort'; @@ -126,12 +126,6 @@ export const getTraceDuration = createSelector(getTraceSpans, getTraceTimestamp, ) ); -export const getTraceEndTimestamp = createSelector( - getTraceTimestamp, - getTraceDuration, - (timestamp: number, duration: number) => timestamp! + duration -); - export const getParentSpan = createSelector( getTraceSpanIdsAsTree, getTraceSpansAsMap, @@ -199,13 +193,6 @@ export const getSortedSpans = createSelector( [...spans].sort((spanA, spanB) => dir * comparator(selector(spanA, trace), selector(spanB, trace))) ); -const getTraceSpansByHierarchyPosition = createSelector(getTraceSpanIdsAsTree, (tree) => { - const hierarchyPositionMap = new Map(); - let i = 0; - tree.walk((spanID: string | number | undefined) => hierarchyPositionMap.set(spanID, i++)); - return hierarchyPositionMap; -}); - export const getTreeSizeForTraceSpan = createSelector( createSelector((state: { trace: TraceResponse }) => state.trace, getTraceSpanIdsAsTree), createSelector((state: { span: TraceSpanData }) => state.span, getSpanId), @@ -218,12 +205,6 @@ export const getTreeSizeForTraceSpan = createSelector( } ); -export const getSpanHierarchySortPositionForTrace = createSelector( - createSelector(({ trace }: { trace: Trace }) => trace, getTraceSpansByHierarchyPosition), - ({ span }: { span: TraceSpan }) => span, - (hierarchyPositionMap, span) => hierarchyPositionMap.get(getSpanId(span)) -); - export const getTraceName = createSelector( createSelector( createSelector(hydrateSpansWithProcesses, getParentSpan), @@ -266,43 +247,3 @@ export const getTicksForTrace = createSelector( width, })) ); - -// TODO: delete this when the backend can ensure uniqueness -/* istanbul ignore next */ -export const enforceUniqueSpanIds = createSelector( - /* istanbul ignore next */ (trace: Trace) => trace, - getTraceSpans, - /* istanbul ignore next */ (trace, spans) => { - const map = new Map(); - - const spanArray: TraceSpanData[] = []; - - return { - ...trace, - spans: spans.reduce((result: TraceSpanData[], span: TraceSpanData) => { - const spanID = map.has(getSpanId(span)) ? `${getSpanId(span)}_${map.get(getSpanId(span))}` : getSpanId(span); - const updatedSpan = { ...span, spanID }; - - if (spanID !== getSpanId(span)) { - // eslint-disable-next-line no-console - console.warn('duplicate spanID in trace replaced', getSpanId(span), 'new:', spanID); - } - - // set the presence of the span in the map or increment the number - map.set(getSpanId(span), (map.get(getSpanId(span)) || 0) + 1); - - return result.concat([updatedSpan]); - }, spanArray), - }; - } -); - -// TODO: delete this when the backend can ensure uniqueness -export const dropEmptyStartTimeSpans = createSelector( - /* istanbul ignore next */ (trace: Trace) => trace, - getTraceSpans, - /* istanbul ignore next */ (trace, spans) => ({ - ...trace, - spans: spans.filter((span: TraceSpanData) => !!getSpanTimestamp(span)), - }) -); diff --git a/packages/jaeger-ui-components/src/settings/SpanBarSettings.tsx b/public/app/features/explore/TraceView/components/settings/SpanBarSettings.tsx similarity index 100% rename from packages/jaeger-ui-components/src/settings/SpanBarSettings.tsx rename to public/app/features/explore/TraceView/components/settings/SpanBarSettings.tsx diff --git a/packages/jaeger-ui-components/src/types/TNil.tsx b/public/app/features/explore/TraceView/components/types/TNil.tsx similarity index 100% rename from packages/jaeger-ui-components/src/types/TNil.tsx rename to public/app/features/explore/TraceView/components/types/TNil.tsx diff --git a/packages/jaeger-ui-components/src/types/TTraceDiffState.tsx b/public/app/features/explore/TraceView/components/types/TTraceDiffState.tsx similarity index 100% rename from packages/jaeger-ui-components/src/types/TTraceDiffState.tsx rename to public/app/features/explore/TraceView/components/types/TTraceDiffState.tsx diff --git a/packages/jaeger-ui-components/src/types/TTraceTimeline.tsx b/public/app/features/explore/TraceView/components/types/TTraceTimeline.tsx similarity index 100% rename from packages/jaeger-ui-components/src/types/TTraceTimeline.tsx rename to public/app/features/explore/TraceView/components/types/TTraceTimeline.tsx diff --git a/packages/jaeger-ui-components/src/types/config.tsx b/public/app/features/explore/TraceView/components/types/config.tsx similarity index 97% rename from packages/jaeger-ui-components/src/types/config.tsx rename to public/app/features/explore/TraceView/components/types/config.tsx index 6bd91da7c99..944a2a278de 100644 --- a/packages/jaeger-ui-components/src/types/config.tsx +++ b/public/app/features/explore/TraceView/components/types/config.tsx @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { TNil } from '.'; +import { TNil } from './index'; export type ConfigMenuItem = { label: string; diff --git a/packages/jaeger-ui-components/src/types/index.tsx b/public/app/features/explore/TraceView/components/types/index.tsx similarity index 100% rename from packages/jaeger-ui-components/src/types/index.tsx rename to public/app/features/explore/TraceView/components/types/index.tsx diff --git a/packages/jaeger-ui-components/src/types/links.ts b/public/app/features/explore/TraceView/components/types/links.ts similarity index 91% rename from packages/jaeger-ui-components/src/types/links.ts rename to public/app/features/explore/TraceView/components/types/links.ts index 0afb22985c4..8e375b2f0d3 100644 --- a/packages/jaeger-ui-components/src/types/links.ts +++ b/public/app/features/explore/TraceView/components/types/links.ts @@ -6,7 +6,7 @@ import { TraceSpan } from './trace'; export type SpanLinkDef = { href: string; - onClick?: (event: any) => void; + onClick?: (event: unknown) => void; content: React.ReactNode; title?: string; field: Field; diff --git a/packages/jaeger-ui-components/src/types/archive.tsx b/public/app/features/explore/TraceView/components/types/search.tsx similarity index 67% rename from packages/jaeger-ui-components/src/types/archive.tsx rename to public/app/features/explore/TraceView/components/types/search.tsx index 1006800d203..a2ce3710a93 100644 --- a/packages/jaeger-ui-components/src/types/archive.tsx +++ b/public/app/features/explore/TraceView/components/types/search.tsx @@ -12,14 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { ApiError } from './api-error'; +import { TNil } from './index'; -export type TraceArchive = { - isLoading?: boolean; - isArchived?: boolean; - isError?: boolean; - error?: ApiError; - isAcknowledged?: boolean; +export type SearchQuery = { + end: number | string; + limit: number | string; + lookback: string; + maxDuration: null | string; + minDuration: null | string; + operation: string | TNil; + service: string; + start: number | string; + tags: string | TNil; }; - -export type TracesArchive = Record; diff --git a/packages/jaeger-ui-components/src/types/trace.ts b/public/app/features/explore/TraceView/components/types/trace.ts similarity index 100% rename from packages/jaeger-ui-components/src/types/trace.ts rename to public/app/features/explore/TraceView/components/types/trace.ts diff --git a/packages/jaeger-ui-components/src/uberUtilityStyles.ts b/public/app/features/explore/TraceView/components/uberUtilityStyles.ts similarity index 100% rename from packages/jaeger-ui-components/src/uberUtilityStyles.ts rename to public/app/features/explore/TraceView/components/uberUtilityStyles.ts diff --git a/packages/jaeger-ui-components/src/url/ReferenceLink.test.tsx b/public/app/features/explore/TraceView/components/url/ReferenceLink.test.tsx similarity index 100% rename from packages/jaeger-ui-components/src/url/ReferenceLink.test.tsx rename to public/app/features/explore/TraceView/components/url/ReferenceLink.test.tsx diff --git a/packages/jaeger-ui-components/src/url/ReferenceLink.tsx b/public/app/features/explore/TraceView/components/url/ReferenceLink.tsx similarity index 100% rename from packages/jaeger-ui-components/src/url/ReferenceLink.tsx rename to public/app/features/explore/TraceView/components/url/ReferenceLink.tsx diff --git a/packages/jaeger-ui-components/src/utils/DraggableManager/DraggableManager.test.ts b/public/app/features/explore/TraceView/components/utils/DraggableManager/DraggableManager.test.ts similarity index 100% rename from packages/jaeger-ui-components/src/utils/DraggableManager/DraggableManager.test.ts rename to public/app/features/explore/TraceView/components/utils/DraggableManager/DraggableManager.test.ts diff --git a/packages/jaeger-ui-components/src/utils/DraggableManager/DraggableManager.tsx b/public/app/features/explore/TraceView/components/utils/DraggableManager/DraggableManager.tsx similarity index 100% rename from packages/jaeger-ui-components/src/utils/DraggableManager/DraggableManager.tsx rename to public/app/features/explore/TraceView/components/utils/DraggableManager/DraggableManager.tsx diff --git a/packages/jaeger-ui-components/src/utils/DraggableManager/EUpdateTypes.tsx b/public/app/features/explore/TraceView/components/utils/DraggableManager/EUpdateTypes.tsx similarity index 100% rename from packages/jaeger-ui-components/src/utils/DraggableManager/EUpdateTypes.tsx rename to public/app/features/explore/TraceView/components/utils/DraggableManager/EUpdateTypes.tsx diff --git a/packages/jaeger-ui-components/src/utils/DraggableManager/README.md b/public/app/features/explore/TraceView/components/utils/DraggableManager/README.md similarity index 99% rename from packages/jaeger-ui-components/src/utils/DraggableManager/README.md rename to public/app/features/explore/TraceView/components/utils/DraggableManager/README.md index 6f20f3126e8..ff98846477f 100644 --- a/packages/jaeger-ui-components/src/utils/DraggableManager/README.md +++ b/public/app/features/explore/TraceView/components/utils/DraggableManager/README.md @@ -142,7 +142,7 @@ This generally amounts to calling [`Element#getBoundingClientRect()`](https://de For instance, in the `DividerDemo`, the function used is `DivideDemo#_getDraggingBounds()`: -```js +```ts _getDraggingBounds = (): DraggableBounds => { if (!this._realmElm) { throw new Error('invalid state'); diff --git a/packages/jaeger-ui-components/src/utils/DraggableManager/demo/DividerDemo.css b/public/app/features/explore/TraceView/components/utils/DraggableManager/demo/DividerDemo.css similarity index 100% rename from packages/jaeger-ui-components/src/utils/DraggableManager/demo/DividerDemo.css rename to public/app/features/explore/TraceView/components/utils/DraggableManager/demo/DividerDemo.css diff --git a/packages/jaeger-ui-components/src/utils/DraggableManager/demo/DividerDemo.tsx b/public/app/features/explore/TraceView/components/utils/DraggableManager/demo/DividerDemo.tsx similarity index 97% rename from packages/jaeger-ui-components/src/utils/DraggableManager/demo/DividerDemo.tsx rename to public/app/features/explore/TraceView/components/utils/DraggableManager/demo/DividerDemo.tsx index ca8ce693956..26ac8b73b56 100644 --- a/packages/jaeger-ui-components/src/utils/DraggableManager/demo/DividerDemo.tsx +++ b/public/app/features/explore/TraceView/components/utils/DraggableManager/demo/DividerDemo.tsx @@ -14,9 +14,9 @@ import React from 'react'; -import { DraggableBounds, DraggingUpdate } from '..'; import TNil from '../../../types/TNil'; import DraggableManager from '../DraggableManager'; +import { DraggableBounds, DraggingUpdate } from '../index'; import './DividerDemo.css'; diff --git a/packages/jaeger-ui-components/src/utils/DraggableManager/demo/DraggableManagerDemo.css b/public/app/features/explore/TraceView/components/utils/DraggableManager/demo/DraggableManagerDemo.css similarity index 100% rename from packages/jaeger-ui-components/src/utils/DraggableManager/demo/DraggableManagerDemo.css rename to public/app/features/explore/TraceView/components/utils/DraggableManager/demo/DraggableManagerDemo.css diff --git a/packages/jaeger-ui-components/src/utils/DraggableManager/demo/DraggableManagerDemo.tsx b/public/app/features/explore/TraceView/components/utils/DraggableManager/demo/DraggableManagerDemo.tsx similarity index 100% rename from packages/jaeger-ui-components/src/utils/DraggableManager/demo/DraggableManagerDemo.tsx rename to public/app/features/explore/TraceView/components/utils/DraggableManager/demo/DraggableManagerDemo.tsx diff --git a/packages/jaeger-ui-components/src/utils/DraggableManager/demo/RegionDemo.css b/public/app/features/explore/TraceView/components/utils/DraggableManager/demo/RegionDemo.css similarity index 100% rename from packages/jaeger-ui-components/src/utils/DraggableManager/demo/RegionDemo.css rename to public/app/features/explore/TraceView/components/utils/DraggableManager/demo/RegionDemo.css diff --git a/packages/jaeger-ui-components/src/utils/DraggableManager/demo/RegionDemo.tsx b/public/app/features/explore/TraceView/components/utils/DraggableManager/demo/RegionDemo.tsx similarity index 99% rename from packages/jaeger-ui-components/src/utils/DraggableManager/demo/RegionDemo.tsx rename to public/app/features/explore/TraceView/components/utils/DraggableManager/demo/RegionDemo.tsx index 5338d7f7c4d..f25f9f05320 100644 --- a/packages/jaeger-ui-components/src/utils/DraggableManager/demo/RegionDemo.tsx +++ b/public/app/features/explore/TraceView/components/utils/DraggableManager/demo/RegionDemo.tsx @@ -14,8 +14,8 @@ import React from 'react'; -import DraggableManager, { DraggableBounds, DraggingUpdate } from '..'; import { TNil } from '../../../types'; +import DraggableManager, { DraggableBounds, DraggingUpdate } from '../index'; import './RegionDemo.css'; diff --git a/packages/jaeger-ui-components/src/utils/DraggableManager/demo/demo-ux.gif b/public/app/features/explore/TraceView/components/utils/DraggableManager/demo/demo-ux.gif similarity index 100% rename from packages/jaeger-ui-components/src/utils/DraggableManager/demo/demo-ux.gif rename to public/app/features/explore/TraceView/components/utils/DraggableManager/demo/demo-ux.gif diff --git a/packages/jaeger-ui-components/src/utils/DraggableManager/demo/index.tsx b/public/app/features/explore/TraceView/components/utils/DraggableManager/demo/index.tsx similarity index 100% rename from packages/jaeger-ui-components/src/utils/DraggableManager/demo/index.tsx rename to public/app/features/explore/TraceView/components/utils/DraggableManager/demo/index.tsx diff --git a/packages/jaeger-ui-components/src/utils/DraggableManager/index.tsx b/public/app/features/explore/TraceView/components/utils/DraggableManager/index.tsx similarity index 100% rename from packages/jaeger-ui-components/src/utils/DraggableManager/index.tsx rename to public/app/features/explore/TraceView/components/utils/DraggableManager/index.tsx diff --git a/packages/jaeger-ui-components/src/utils/DraggableManager/types.tsx b/public/app/features/explore/TraceView/components/utils/DraggableManager/types.tsx similarity index 95% rename from packages/jaeger-ui-components/src/utils/DraggableManager/types.tsx rename to public/app/features/explore/TraceView/components/utils/DraggableManager/types.tsx index b10c4581199..79e213f8d7e 100644 --- a/packages/jaeger-ui-components/src/utils/DraggableManager/types.tsx +++ b/public/app/features/explore/TraceView/components/utils/DraggableManager/types.tsx @@ -27,7 +27,7 @@ export type DraggableBounds = { }; export type DraggingUpdate = { - event: React.MouseEvent | MouseEvent; + event: React.MouseEvent | MouseEvent; manager: DraggableManager; tag: string | TNil; type: EUpdateTypes; diff --git a/packages/jaeger-ui-components/src/utils/TreeNode.test.ts b/public/app/features/explore/TraceView/components/utils/TreeNode.test.ts similarity index 100% rename from packages/jaeger-ui-components/src/utils/TreeNode.test.ts rename to public/app/features/explore/TraceView/components/utils/TreeNode.test.ts diff --git a/packages/jaeger-ui-components/src/utils/TreeNode.ts b/public/app/features/explore/TraceView/components/utils/TreeNode.ts similarity index 100% rename from packages/jaeger-ui-components/src/utils/TreeNode.ts rename to public/app/features/explore/TraceView/components/utils/TreeNode.ts diff --git a/packages/jaeger-ui-components/src/utils/color-generator.test.ts b/public/app/features/explore/TraceView/components/utils/color-generator.test.ts similarity index 100% rename from packages/jaeger-ui-components/src/utils/color-generator.test.ts rename to public/app/features/explore/TraceView/components/utils/color-generator.test.ts diff --git a/packages/jaeger-ui-components/src/utils/color-generator.tsx b/public/app/features/explore/TraceView/components/utils/color-generator.tsx similarity index 100% rename from packages/jaeger-ui-components/src/utils/color-generator.tsx rename to public/app/features/explore/TraceView/components/utils/color-generator.tsx diff --git a/packages/jaeger-ui-components/src/utils/config/get-config.tsx b/public/app/features/explore/TraceView/components/utils/config/get-config.tsx similarity index 100% rename from packages/jaeger-ui-components/src/utils/config/get-config.tsx rename to public/app/features/explore/TraceView/components/utils/config/get-config.tsx diff --git a/packages/jaeger-ui-components/src/utils/date.test.ts b/public/app/features/explore/TraceView/components/utils/date.test.ts similarity index 100% rename from packages/jaeger-ui-components/src/utils/date.test.ts rename to public/app/features/explore/TraceView/components/utils/date.test.ts diff --git a/packages/jaeger-ui-components/src/utils/date.tsx b/public/app/features/explore/TraceView/components/utils/date.tsx similarity index 76% rename from packages/jaeger-ui-components/src/utils/date.tsx rename to public/app/features/explore/TraceView/components/utils/date.tsx index 1a0277f189f..3c358885359 100644 --- a/packages/jaeger-ui-components/src/utils/date.tsx +++ b/public/app/features/explore/TraceView/components/utils/date.tsx @@ -17,12 +17,8 @@ import moment from 'moment-timezone'; import { toFloatPrecision } from './number'; -const TODAY = 'Today'; -const YESTERDAY = 'Yesterday'; - export const STANDARD_DATE_FORMAT = 'YYYY-MM-DD'; export const STANDARD_TIME_FORMAT = 'HH:mm'; -export const STANDARD_DATETIME_FORMAT = 'MMMM D YYYY, HH:mm:ss.SSS'; export const ONE_MILLISECOND = 1000; export const ONE_SECOND = 1000 * ONE_MILLISECOND; export const ONE_MINUTE = 60 * ONE_SECOND; @@ -39,16 +35,6 @@ const UNIT_STEPS: Array<{ unit: string; microseconds: number; ofPrevious: number { unit: 'μs', microseconds: 1, ofPrevious: 1000 }, ]; -/** - * @param {number} timestamp - * @param {number} initialTimestamp - * @param {number} totalDuration - * @returns {number} 0-100 percentage - */ -export function getPercentageOfDuration(duration: number, totalDuration: number) { - return (duration / totalDuration) * 100; -} - const quantizeDuration = (duration: number, floatPrecision: number, conversionFactor: number) => toFloatPrecision(duration / conversionFactor, floatPrecision) * conversionFactor; @@ -68,14 +54,6 @@ export function formatTime(duration: number) { return moment(duration / ONE_MILLISECOND).format(STANDARD_TIME_FORMAT); } -/** - * @param {number} duration (in microseconds) - * @returns {string} formatted, unit-labelled string with time in milliseconds - */ -export function formatDatetime(duration: number) { - return moment(duration / ONE_MILLISECOND).format(STANDARD_DATETIME_FORMAT); -} - /** * @param {number} duration (in microseconds) * @returns {string} formatted, unit-labelled string with time in milliseconds @@ -123,23 +101,3 @@ export function formatDuration(duration: number): string { const secondaryUnitString = `${secondaryValue}${secondaryUnit.unit}`; return secondaryValue === 0 ? primaryUnitString : `${primaryUnitString} ${secondaryUnitString}`; } - -export function formatRelativeDate(value: any, fullMonthName = false) { - const m = moment.isMoment(value) ? value : moment(value); - const monthFormat = fullMonthName ? 'MMMM' : 'MMM'; - const dt = new Date(); - if (dt.getFullYear() !== m.year()) { - return m.format(`${monthFormat} D, YYYY`); - } - const mMonth = m.month(); - const mDate = m.date(); - const date = dt.getDate(); - if (mMonth === dt.getMonth() && mDate === date) { - return TODAY; - } - dt.setDate(date - 1); - if (mMonth === dt.getMonth() && mDate === dt.getDate()) { - return YESTERDAY; - } - return m.format(`${monthFormat} D`); -} diff --git a/packages/jaeger-ui-components/src/utils/filter-spans.test.ts b/public/app/features/explore/TraceView/components/utils/filter-spans.test.ts similarity index 99% rename from packages/jaeger-ui-components/src/utils/filter-spans.test.ts rename to public/app/features/explore/TraceView/components/utils/filter-spans.test.ts index 48f704e4cc4..9123cd2106a 100644 --- a/packages/jaeger-ui-components/src/utils/filter-spans.test.ts +++ b/public/app/features/explore/TraceView/components/utils/filter-spans.test.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { TraceSpan } from 'src/types/trace'; +import { TraceSpan } from '../types'; import filterSpans from './filter-spans'; diff --git a/packages/jaeger-ui-components/src/utils/filter-spans.tsx b/public/app/features/explore/TraceView/components/utils/filter-spans.tsx similarity index 96% rename from packages/jaeger-ui-components/src/utils/filter-spans.tsx rename to public/app/features/explore/TraceView/components/utils/filter-spans.tsx index 699d8996487..4f93a6b42c1 100644 --- a/packages/jaeger-ui-components/src/utils/filter-spans.tsx +++ b/public/app/features/explore/TraceView/components/utils/filter-spans.tsx @@ -12,8 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { TNil } from '../types'; -import { TraceKeyValuePair, TraceSpan } from '../types/trace'; +import { TNil, TraceKeyValuePair, TraceSpan } from '../types'; export default function filterSpans(textFilter: string, spans: TraceSpan[] | TNil) { if (!spans) { diff --git a/packages/jaeger-ui-components/src/utils/number.tsx b/public/app/features/explore/TraceView/components/utils/number.tsx similarity index 100% rename from packages/jaeger-ui-components/src/utils/number.tsx rename to public/app/features/explore/TraceView/components/utils/number.tsx diff --git a/packages/jaeger-ui-components/src/utils/sort.test.ts b/public/app/features/explore/TraceView/components/utils/sort.test.ts similarity index 73% rename from packages/jaeger-ui-components/src/utils/sort.test.ts rename to public/app/features/explore/TraceView/components/utils/sort.test.ts index 43bede4d91c..1f427425072 100644 --- a/packages/jaeger-ui-components/src/utils/sort.test.ts +++ b/public/app/features/explore/TraceView/components/utils/sort.test.ts @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import sinon from 'sinon'; - import * as sortUtils from './sort'; it('localeStringComparator() provides a case-insensitive sort', () => { @@ -77,31 +75,3 @@ it('getNewSortForClick() should toggle direction if same column', () => { dir: 1, }); }); - -it('createSortClickHandler() should return a function', () => { - const column = { name: 'alpha' }; - const currentSortKey = 'alpha'; - const currentSortDir = 1; - const updateSort = sinon.spy(); - - expect(typeof sortUtils.createSortClickHandler(column, currentSortKey, currentSortDir, updateSort)).toBe('function'); -}); - -it('createSortClickHandler() should call updateSort with the new sort vals', () => { - const column = { name: 'alpha' }; - const prevSort = { key: 'alpha', dir: 1 }; - const currentSortKey = prevSort.key; - const currentSortDir = prevSort.dir; - const updateSort = sinon.spy(); - - const clickHandler = sortUtils.createSortClickHandler(column, currentSortKey, currentSortDir, updateSort); - - clickHandler(); - - expect( - updateSort.calledWith( - sortUtils.getNewSortForClick(prevSort, column).key, - sortUtils.getNewSortForClick(prevSort, column).dir - ) - ).toBeTruthy(); -}); diff --git a/packages/jaeger-ui-components/src/utils/sort.ts b/public/app/features/explore/TraceView/components/utils/sort.ts similarity index 77% rename from packages/jaeger-ui-components/src/utils/sort.ts rename to public/app/features/explore/TraceView/components/utils/sort.ts index 5d3c85872cc..5636fdc977f 100644 --- a/packages/jaeger-ui-components/src/utils/sort.ts +++ b/public/app/features/explore/TraceView/components/utils/sort.ts @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import sinon from 'sinon'; - export function localeStringComparator(itemA: string, itemB: string) { return itemA.localeCompare(itemB); } @@ -37,15 +35,3 @@ export function getNewSortForClick( dir: prevSort.key === column.name ? -1 * prevSort.dir : defaultDir, }; } - -export function createSortClickHandler( - column: { name: string }, - currentSortKey: string, - currentSortDir: number, - updateSort: sinon.SinonSpy -) { - return function onClickSortingElement() { - const { key, dir } = getNewSortForClick({ key: currentSortKey, dir: currentSortDir }, column); - updateSort(key, dir); - }; -} diff --git a/packages/jaeger-ui-components/src/utils/span-ancestor-ids.test.ts b/public/app/features/explore/TraceView/components/utils/span-ancestor-ids.test.ts similarity index 98% rename from packages/jaeger-ui-components/src/utils/span-ancestor-ids.test.ts rename to public/app/features/explore/TraceView/components/utils/span-ancestor-ids.test.ts index 76d6fc3d7b5..d8532b74151 100644 --- a/packages/jaeger-ui-components/src/utils/span-ancestor-ids.test.ts +++ b/public/app/features/explore/TraceView/components/utils/span-ancestor-ids.test.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { TraceSpan } from 'src/types/trace'; +import { TraceSpan } from '../types'; import spanAncestorIdsSpy from './span-ancestor-ids'; diff --git a/packages/jaeger-ui-components/src/utils/span-ancestor-ids.tsx b/public/app/features/explore/TraceView/components/utils/span-ancestor-ids.tsx similarity index 93% rename from packages/jaeger-ui-components/src/utils/span-ancestor-ids.tsx rename to public/app/features/explore/TraceView/components/utils/span-ancestor-ids.tsx index cb4677353d3..5ddb037aa2a 100644 --- a/packages/jaeger-ui-components/src/utils/span-ancestor-ids.tsx +++ b/public/app/features/explore/TraceView/components/utils/span-ancestor-ids.tsx @@ -14,8 +14,7 @@ import { find as _find, get as _get } from 'lodash'; -import { TNil } from '../types'; -import { TraceSpan } from '../types/trace'; +import { TNil, TraceSpan } from '../types'; function getFirstAncestor(span: TraceSpan): TraceSpan | TNil { return _get( diff --git a/packages/jaeger-ui-components/src/utils/test/requestAnimationFrame.ts b/public/app/features/explore/TraceView/components/utils/test/requestAnimationFrame.ts similarity index 100% rename from packages/jaeger-ui-components/src/utils/test/requestAnimationFrame.ts rename to public/app/features/explore/TraceView/components/utils/test/requestAnimationFrame.ts diff --git a/public/app/features/explore/TraceView/createSpanLink.test.ts b/public/app/features/explore/TraceView/createSpanLink.test.ts index ba208be0afc..f1ad205615a 100644 --- a/public/app/features/explore/TraceView/createSpanLink.test.ts +++ b/public/app/features/explore/TraceView/createSpanLink.test.ts @@ -1,6 +1,5 @@ import { DataSourceInstanceSettings, LinkModel, MutableDataFrame } from '@grafana/data'; import { DataSourceSrv, setDataSourceSrv, setTemplateSrv } from '@grafana/runtime'; -import { Trace, TraceSpan } from '@jaegertracing/jaeger-ui-components'; import { TraceToMetricsOptions } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; @@ -8,6 +7,7 @@ import { TraceToLogsOptionsV2 } from '../../../core/components/TraceToLogs/Trace import { LinkSrv, setLinkSrv } from '../../panel/panellinks/link_srv'; import { TemplateSrv } from '../../templating/template_srv'; +import { Trace, TraceSpan } from './components'; import { createSpanLinkFactory } from './createSpanLink'; const dummyTraceData = { duration: 10, traceID: 'trace1', traceName: 'test trace' } as unknown as Trace; diff --git a/public/app/features/explore/TraceView/createSpanLink.tsx b/public/app/features/explore/TraceView/createSpanLink.tsx index 95184af081f..3edbdf50e31 100644 --- a/public/app/features/explore/TraceView/createSpanLink.tsx +++ b/public/app/features/explore/TraceView/createSpanLink.tsx @@ -1,4 +1,3 @@ -import { SpanLinks } from '@jaegertracing/jaeger-ui-components/src/types/links'; import { property } from 'lodash'; import React from 'react'; @@ -19,7 +18,6 @@ import { import { getTemplateSrv } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; import { Icon } from '@grafana/ui'; -import { SpanLinkFunc, Trace, TraceSpan } from '@jaegertracing/jaeger-ui-components'; import { TraceToLogsOptionsV2 } from 'app/core/components/TraceToLogs/TraceToLogsSettings'; import { TraceToMetricQuery, TraceToMetricsOptions } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; @@ -29,6 +27,9 @@ import { LokiQuery } from '../../../plugins/datasource/loki/types'; import { variableRegex } from '../../variables/utils'; import { getFieldLinksForExplore } from '../utils/links'; +import { SpanLinkFunc, Trace, TraceSpan } from './components'; +import { SpanLinks } from './components/types/links'; + /** * This is a factory for the link creator. It returns the function mainly so it can return undefined in which case * the trace view won't create any links and to capture the datasource and split function making it easier to memoize diff --git a/packages/jaeger-ui-components/typings/custom.d.ts b/public/app/features/explore/TraceView/custom.d.ts similarity index 85% rename from packages/jaeger-ui-components/typings/custom.d.ts rename to public/app/features/explore/TraceView/custom.d.ts index dd70a67124d..9695782217f 100644 --- a/packages/jaeger-ui-components/typings/custom.d.ts +++ b/public/app/features/explore/TraceView/custom.d.ts @@ -12,15 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// For inlined envvars -declare const process: { - env: { - NODE_ENV: string; - REACT_APP_GA_DEBUG?: string; - REACT_APP_VSN_STATE?: string; - }; -}; - declare module 'combokeys' { export default class Combokeys { constructor(element: HTMLElement); diff --git a/public/app/features/explore/TraceView/useChildrenState.test.ts b/public/app/features/explore/TraceView/useChildrenState.test.ts index 1d6dca9c200..ab462c1ec48 100644 --- a/public/app/features/explore/TraceView/useChildrenState.test.ts +++ b/public/app/features/explore/TraceView/useChildrenState.test.ts @@ -1,7 +1,6 @@ import { renderHook, act } from '@testing-library/react-hooks'; -import { TraceSpan } from '@jaegertracing/jaeger-ui-components'; - +import { TraceSpan } from './components'; import { useChildrenState } from './useChildrenState'; describe('useChildrenState', () => { diff --git a/public/app/features/explore/TraceView/useChildrenState.ts b/public/app/features/explore/TraceView/useChildrenState.ts index ef8607bf9ef..1bfca3611d6 100644 --- a/public/app/features/explore/TraceView/useChildrenState.ts +++ b/public/app/features/explore/TraceView/useChildrenState.ts @@ -1,6 +1,6 @@ import { useCallback, useState } from 'react'; -import { TraceSpan } from '@jaegertracing/jaeger-ui-components'; +import { TraceSpan } from './components'; /** * Children state means whether spans are collapsed or not. Also provides some functions to manipulate that state. diff --git a/public/app/features/explore/TraceView/useDetailState.test.ts b/public/app/features/explore/TraceView/useDetailState.test.ts index ae941f924f5..e9454bd1091 100644 --- a/public/app/features/explore/TraceView/useDetailState.test.ts +++ b/public/app/features/explore/TraceView/useDetailState.test.ts @@ -1,8 +1,8 @@ -import { TraceLog } from '@jaegertracing/jaeger-ui-components/src/types/trace'; import { act, renderHook } from '@testing-library/react-hooks'; import { DataFrame } from '@grafana/data'; +import { TraceLog } from './components/types/trace'; import { useDetailState } from './useDetailState'; const sampleFrame: DataFrame = { diff --git a/public/app/features/explore/TraceView/useDetailState.ts b/public/app/features/explore/TraceView/useDetailState.ts index 63400a6f2f7..e3e4df4c75a 100644 --- a/public/app/features/explore/TraceView/useDetailState.ts +++ b/public/app/features/explore/TraceView/useDetailState.ts @@ -1,9 +1,9 @@ -import { TraceLog, TraceSpanReference } from '@jaegertracing/jaeger-ui-components/src/types/trace'; import { useCallback, useState, useEffect } from 'react'; import { DataFrame } from '@grafana/data'; -import { DetailState } from '@jaegertracing/jaeger-ui-components'; +import { DetailState } from './components'; +import { TraceLog, TraceSpanReference } from './components/types/trace'; /** * Keeps state of the span detail. This means whether span details are open but also state of each detail subitem * like logs or tags. diff --git a/public/app/features/explore/TraceView/useSearch.test.ts b/public/app/features/explore/TraceView/useSearch.test.ts index ce5ceddbeb7..a4a0f537172 100644 --- a/public/app/features/explore/TraceView/useSearch.test.ts +++ b/public/app/features/explore/TraceView/useSearch.test.ts @@ -1,7 +1,6 @@ import { act, renderHook } from '@testing-library/react-hooks'; -import { TraceSpan } from '@jaegertracing/jaeger-ui-components'; - +import { TraceSpan } from './components'; import { useSearch } from './useSearch'; describe('useSearch', () => { diff --git a/public/app/features/explore/TraceView/useSearch.ts b/public/app/features/explore/TraceView/useSearch.ts index 6574ac5659b..38550370c17 100644 --- a/public/app/features/explore/TraceView/useSearch.ts +++ b/public/app/features/explore/TraceView/useSearch.ts @@ -1,6 +1,6 @@ import { useMemo, useState } from 'react'; -import { filterSpans, TraceSpan } from '@jaegertracing/jaeger-ui-components'; +import { filterSpans, TraceSpan } from './components'; /** * Controls the state of search input that highlights spans if they match the search string. diff --git a/public/app/features/explore/TraceView/useViewRange.ts b/public/app/features/explore/TraceView/useViewRange.ts index e617f488ebe..f577beb0b38 100644 --- a/public/app/features/explore/TraceView/useViewRange.ts +++ b/public/app/features/explore/TraceView/useViewRange.ts @@ -1,6 +1,6 @@ import { useCallback, useState } from 'react'; -import { ViewRangeTimeUpdate, ViewRange } from '@jaegertracing/jaeger-ui-components'; +import { ViewRangeTimeUpdate, ViewRange } from './components'; /** * Controls state of the zoom function that can be used through minimap in header or on the timeline. ViewRange contains diff --git a/public/app/features/explore/TraceView/utils/transform.ts b/public/app/features/explore/TraceView/utils/transform.ts index 7300566121f..e7f1582af83 100644 --- a/public/app/features/explore/TraceView/utils/transform.ts +++ b/public/app/features/explore/TraceView/utils/transform.ts @@ -1,5 +1,6 @@ import { DataFrame, DataFrameView, TraceSpanRow } from '@grafana/data'; -import { Trace, TraceProcess, TraceResponse, transformTraceData } from '@jaegertracing/jaeger-ui-components'; + +import { Trace, TraceProcess, TraceResponse, transformTraceData } from '../components'; export function transformDataFrames(frame?: DataFrame): Trace | null { if (!frame) { diff --git a/public/app/features/inspector/InspectDataTab.test.tsx b/public/app/features/inspector/InspectDataTab.test.tsx index 3355a8b8cef..0dc821cb758 100644 --- a/public/app/features/inspector/InspectDataTab.test.tsx +++ b/public/app/features/inspector/InspectDataTab.test.tsx @@ -6,10 +6,6 @@ import { DataFrame, FieldType } from '@grafana/data'; import { InspectDataTab } from './InspectDataTab'; -// the mock below gets rid of this warning from recompose: -// Warning: React.createFactory() is deprecated and will be removed in a future major release. Consider using JSX or use React.createElement() directly instead. -jest.mock('@jaegertracing/jaeger-ui-components', () => ({})); - const createProps = (propsOverride?: Partial>) => { const defaultProps = { isLoading: false, diff --git a/public/app/plugins/datasource/jaeger/components/ConfigEditor.tsx b/public/app/plugins/datasource/jaeger/components/ConfigEditor.tsx index 6b20122169d..aa2df3773ed 100644 --- a/public/app/plugins/datasource/jaeger/components/ConfigEditor.tsx +++ b/public/app/plugins/datasource/jaeger/components/ConfigEditor.tsx @@ -3,10 +3,10 @@ import React from 'react'; import { DataSourcePluginOptionsEditorProps } from '@grafana/data'; import { config } from '@grafana/runtime'; import { DataSourceHttpSettings, SecureSocksProxySettings } from '@grafana/ui'; -import { SpanBarSettings } from '@jaegertracing/jaeger-ui-components'; import { NodeGraphSettings } from 'app/core/components/NodeGraphSettings'; import { TraceToLogsSettings } from 'app/core/components/TraceToLogs/TraceToLogsSettings'; import { TraceToMetricsSettings } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings'; +import { SpanBarSettings } from 'app/features/explore/TraceView/components'; export type Props = DataSourcePluginOptionsEditorProps; diff --git a/public/app/plugins/datasource/jaeger/datasource.ts b/public/app/plugins/datasource/jaeger/datasource.ts index 85e3ae1badd..f8288806604 100644 --- a/public/app/plugins/datasource/jaeger/datasource.ts +++ b/public/app/plugins/datasource/jaeger/datasource.ts @@ -15,10 +15,10 @@ import { ScopedVars, } from '@grafana/data'; import { BackendSrvRequest, getBackendSrv, getTemplateSrv, TemplateSrv } from '@grafana/runtime'; -import { SpanBarOptions } from '@jaegertracing/jaeger-ui-components'; import { NodeGraphOptions } from 'app/core/components/NodeGraphSettings'; import { serializeParams } from 'app/core/utils/fetch'; import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; +import { SpanBarOptions } from 'app/features/explore/TraceView/components'; import { ALL_OPERATIONS_KEY } from './components/SearchForm'; import { createGraphFrames } from './graphTransform'; diff --git a/public/app/plugins/datasource/jaeger/responseTransform.ts b/public/app/plugins/datasource/jaeger/responseTransform.ts index 07123123422..507e5646cfb 100644 --- a/public/app/plugins/datasource/jaeger/responseTransform.ts +++ b/public/app/plugins/datasource/jaeger/responseTransform.ts @@ -6,7 +6,7 @@ import { TraceLog, TraceSpanRow, } from '@grafana/data'; -import { transformTraceData } from '@jaegertracing/jaeger-ui-components'; +import { transformTraceData } from 'app/features/explore/TraceView/components'; import { JaegerResponse, Span, TraceProcess, TraceResponse } from './types'; diff --git a/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx b/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx index b883cc8e611..7f792be4527 100644 --- a/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx +++ b/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx @@ -3,10 +3,10 @@ import React from 'react'; import { DataSourcePluginOptionsEditorProps } from '@grafana/data'; import { config } from '@grafana/runtime'; import { DataSourceHttpSettings, SecureSocksProxySettings } from '@grafana/ui'; -import { SpanBarSettings } from '@jaegertracing/jaeger-ui-components'; import { NodeGraphSettings } from 'app/core/components/NodeGraphSettings'; import { TraceToLogsSettings } from 'app/core/components/TraceToLogs/TraceToLogsSettings'; import { TraceToMetricsSettings } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings'; +import { SpanBarSettings } from 'app/features/explore/TraceView/components'; import { LokiSearchSettings } from './LokiSearchSettings'; import { QuerySettings } from './QuerySettings'; diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts index f79c9ed3fa1..3e8c2111aa6 100644 --- a/public/app/plugins/datasource/tempo/datasource.ts +++ b/public/app/plugins/datasource/tempo/datasource.ts @@ -24,10 +24,10 @@ import { TemplateSrv, getTemplateSrv, } from '@grafana/runtime'; -import { SpanBarOptions } from '@jaegertracing/jaeger-ui-components'; import { NodeGraphOptions } from 'app/core/components/NodeGraphSettings'; import { TraceToLogsOptions } from 'app/core/components/TraceToLogs/TraceToLogsSettings'; import { serializeParams } from 'app/core/utils/fetch'; +import { SpanBarOptions } from 'app/features/explore/TraceView/components'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { LokiOptions } from '../loki/types'; diff --git a/public/app/plugins/datasource/zipkin/ConfigEditor.tsx b/public/app/plugins/datasource/zipkin/ConfigEditor.tsx index f3621bfef59..81c3dd193fc 100644 --- a/public/app/plugins/datasource/zipkin/ConfigEditor.tsx +++ b/public/app/plugins/datasource/zipkin/ConfigEditor.tsx @@ -3,10 +3,10 @@ import React from 'react'; import { DataSourcePluginOptionsEditorProps } from '@grafana/data'; import { config } from '@grafana/runtime'; import { DataSourceHttpSettings, SecureSocksProxySettings } from '@grafana/ui'; -import { SpanBarSettings } from '@jaegertracing/jaeger-ui-components'; import { NodeGraphSettings } from 'app/core/components/NodeGraphSettings'; import { TraceToLogsSettings } from 'app/core/components/TraceToLogs/TraceToLogsSettings'; import { TraceToMetricsSettings } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings'; +import { SpanBarSettings } from 'app/features/explore/TraceView/components'; export type Props = DataSourcePluginOptionsEditorProps; diff --git a/public/app/plugins/datasource/zipkin/datasource.ts b/public/app/plugins/datasource/zipkin/datasource.ts index cf9ed51c03f..0ea00fe33f0 100644 --- a/public/app/plugins/datasource/zipkin/datasource.ts +++ b/public/app/plugins/datasource/zipkin/datasource.ts @@ -12,8 +12,8 @@ import { ScopedVars, } from '@grafana/data'; import { BackendSrvRequest, FetchResponse, getBackendSrv, getTemplateSrv, TemplateSrv } from '@grafana/runtime'; -import { SpanBarOptions } from '@jaegertracing/jaeger-ui-components'; import { NodeGraphOptions } from 'app/core/components/NodeGraphSettings'; +import { SpanBarOptions } from 'app/features/explore/TraceView/components'; import { serializeParams } from '../../../core/utils/fetch'; diff --git a/public/app/plugins/panel/traces/TracesPanel.tsx b/public/app/plugins/panel/traces/TracesPanel.tsx index b39b8b1aee1..4bcfe68a295 100644 --- a/public/app/plugins/panel/traces/TracesPanel.tsx +++ b/public/app/plugins/panel/traces/TracesPanel.tsx @@ -1,12 +1,12 @@ import { css } from '@emotion/css'; -import TracePageSearchBar from '@jaegertracing/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar'; -import { TopOfViewRefType } from '@jaegertracing/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView'; import React, { useMemo, useState, createRef } from 'react'; import { useAsync } from 'react-use'; import { PanelProps } from '@grafana/data'; import { getDataSourceSrv } from '@grafana/runtime'; import { TraceView } from 'app/features/explore/TraceView/TraceView'; +import TracePageSearchBar from 'app/features/explore/TraceView/components/TracePageHeader/TracePageSearchBar'; +import { TopOfViewRefType } from 'app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView'; import { useSearch } from 'app/features/explore/TraceView/useSearch'; import { transformDataFrames } from 'app/features/explore/TraceView/utils/transform'; diff --git a/scripts/check-breaking-changes.sh b/scripts/check-breaking-changes.sh index 28e1ec57c1f..fd6ca45de20 100755 --- a/scripts/check-breaking-changes.sh +++ b/scripts/check-breaking-changes.sh @@ -16,7 +16,7 @@ while IFS=" " read -r -a package; do CURRENT="./pr/$PACKAGE_PATH" # Temporarily skipping these packages as they don't have any exposed static typing - if [[ "$PACKAGE_PATH" == 'grafana-toolkit' || "$PACKAGE_PATH" == 'jaeger-ui-components' || "$PACKAGE_PATH" == 'grafana-eslint-rules' ]]; then + if [[ "$PACKAGE_PATH" == 'grafana-toolkit' || "$PACKAGE_PATH" == 'grafana-eslint-rules' ]]; then continue fi @@ -38,11 +38,11 @@ while IFS=" " read -r -a package; do STATUS=$? # Final exit code - # (non-zero if any of the packages failed the checks) + # (non-zero if any of the packages failed the checks) if [ $STATUS -gt 0 ] then EXIT_CODE=1 - GITHUB_MESSAGE="${GITHUB_MESSAGE}**\\\`${PACKAGE_PATH}\\\`** has possible breaking changes ([more info](${GITHUB_JOB_LINK}#step:${GITHUB_STEP_NUMBER}:1))
" + GITHUB_MESSAGE="${GITHUB_MESSAGE}**\\\`${PACKAGE_PATH}\\\`** has possible breaking changes ([more info](${GITHUB_JOB_LINK}#step:${GITHUB_STEP_NUMBER}:1))
" fi done <<< "$PACKAGES" diff --git a/tsconfig.json b/tsconfig.json index 8e94aad40cb..72b9d2e6402 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,6 @@ "public/e2e-test/**/*.ts", "public/test/**/*.ts", "public/vendor/**/*.ts", - "packages/jaeger-ui-components/typings", "packages/grafana-data/typings", "packages/grafana-ui/src/types" ] diff --git a/yarn.lock b/yarn.lock index f6790269727..c7b07867ad5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -92,6 +92,13 @@ __metadata: languageName: node linkType: hard +"@babel/compat-data@npm:^7.19.3, @babel/compat-data@npm:^7.19.4": + version: 7.19.4 + resolution: "@babel/compat-data@npm:7.19.4" + checksum: 757fdaeb6756c2d323ff56f60fb8e670292108cda6abf762a56c0d40910ecc4d2c7e283dbdfbcee6bc28c74ad659144352609e1cb49d31e101ab13ea5ce90072 + languageName: node + linkType: hard + "@babel/compat-data@npm:^7.20.0, @babel/compat-data@npm:^7.20.1": version: 7.20.5 resolution: "@babel/compat-data@npm:7.20.5" @@ -146,7 +153,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:7.20.5, @babel/core@npm:^7.0.1, @babel/core@npm:^7.12.10, @babel/core@npm:^7.7.5": +"@babel/core@npm:7.20.5, @babel/core@npm:^7.0.1": version: 7.20.5 resolution: "@babel/core@npm:7.20.5" dependencies: @@ -215,6 +222,29 @@ __metadata: languageName: node linkType: hard +"@babel/core@npm:^7.12.10, @babel/core@npm:^7.7.5": + version: 7.19.6 + resolution: "@babel/core@npm:7.19.6" + dependencies: + "@ampproject/remapping": ^2.1.0 + "@babel/code-frame": ^7.18.6 + "@babel/generator": ^7.19.6 + "@babel/helper-compilation-targets": ^7.19.3 + "@babel/helper-module-transforms": ^7.19.6 + "@babel/helpers": ^7.19.4 + "@babel/parser": ^7.19.6 + "@babel/template": ^7.18.10 + "@babel/traverse": ^7.19.6 + "@babel/types": ^7.19.4 + convert-source-map: ^1.7.0 + debug: ^4.1.0 + gensync: ^1.0.0-beta.2 + json5: ^2.2.1 + semver: ^6.3.0 + checksum: 85c0bd38d0ef180aa2d23c3db6840a0baec88d2e05c30e7ffc3dfeb6b2b89d6e4864922f04997a1f4ce55f9dd469bf2e76518d5c7ae744b98516709d32769b73 + languageName: node + linkType: hard + "@babel/core@npm:^7.7.2": version: 7.16.0 resolution: "@babel/core@npm:7.16.0" @@ -261,7 +291,7 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.12.11, @babel/generator@npm:^7.19.6": +"@babel/generator@npm:^7.12.11, @babel/generator@npm:^7.12.5, @babel/generator@npm:^7.19.6": version: 7.19.6 resolution: "@babel/generator@npm:7.19.6" dependencies: @@ -272,17 +302,6 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.12.5, @babel/generator@npm:^7.17.9, @babel/generator@npm:^7.20.5": - version: 7.20.5 - resolution: "@babel/generator@npm:7.20.5" - dependencies: - "@babel/types": ^7.20.5 - "@jridgewell/gen-mapping": ^0.3.2 - jsesc: ^2.5.1 - checksum: 31c10d1e122f08cf755a24bd6f5d197f47eceba03f1133759687d00ab72d210e60ba4011da42f368b6e9fa85cbfda7dc4adb9889c2c20cc5c34bb2d57c1deab7 - languageName: node - linkType: hard - "@babel/generator@npm:^7.15.4, @babel/generator@npm:^7.15.8": version: 7.15.8 resolution: "@babel/generator@npm:7.15.8" @@ -327,6 +346,17 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.17.9, @babel/generator@npm:^7.20.5": + version: 7.20.5 + resolution: "@babel/generator@npm:7.20.5" + dependencies: + "@babel/types": ^7.20.5 + "@jridgewell/gen-mapping": ^0.3.2 + jsesc: ^2.5.1 + checksum: 31c10d1e122f08cf755a24bd6f5d197f47eceba03f1133759687d00ab72d210e60ba4011da42f368b6e9fa85cbfda7dc4adb9889c2c20cc5c34bb2d57c1deab7 + languageName: node + linkType: hard + "@babel/generator@npm:^7.18.2": version: 7.18.2 resolution: "@babel/generator@npm:7.18.2" @@ -360,6 +390,17 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.19.0": + version: 7.19.0 + resolution: "@babel/generator@npm:7.19.0" + dependencies: + "@babel/types": ^7.19.0 + "@jridgewell/gen-mapping": ^0.3.2 + jsesc: ^2.5.1 + checksum: aa3d5785cf8f8e81672dcc61aef351188efeadb20d9f66d79113d82cbcf3bbbdeb829989fa14582108572ddbc4e4027bdceb06ccaf5ec40fa93c2dda8fbcd4aa + languageName: node + linkType: hard + "@babel/helper-annotate-as-pure@npm:^7.14.5": version: 7.15.4 resolution: "@babel/helper-annotate-as-pure@npm:7.15.4" @@ -430,7 +471,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-compilation-targets@npm:^7.17.7": +"@babel/helper-compilation-targets@npm:^7.17.7, @babel/helper-compilation-targets@npm:^7.19.0": version: 7.19.0 resolution: "@babel/helper-compilation-targets@npm:7.19.0" dependencies: @@ -472,6 +513,20 @@ __metadata: languageName: node linkType: hard +"@babel/helper-compilation-targets@npm:^7.19.3": + version: 7.19.3 + resolution: "@babel/helper-compilation-targets@npm:7.19.3" + dependencies: + "@babel/compat-data": ^7.19.3 + "@babel/helper-validator-option": ^7.18.6 + browserslist: ^4.21.3 + semver: ^6.3.0 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: aafcb4490c98cddb3255fff98bfbdb881b4def85a1935fd9b1f9b1f0f8b502696839f6b387fb508ca991ea72ba82ce6913bab99f21df4ce80bda2b79e91a09f5 + languageName: node + linkType: hard + "@babel/helper-compilation-targets@npm:^7.20.0": version: 7.20.0 resolution: "@babel/helper-compilation-targets@npm:7.20.0" @@ -520,23 +575,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-create-class-features-plugin@npm:^7.20.5": - version: 7.20.5 - resolution: "@babel/helper-create-class-features-plugin@npm:7.20.5" - dependencies: - "@babel/helper-annotate-as-pure": ^7.18.6 - "@babel/helper-environment-visitor": ^7.18.9 - "@babel/helper-function-name": ^7.19.0 - "@babel/helper-member-expression-to-functions": ^7.18.9 - "@babel/helper-optimise-call-expression": ^7.18.6 - "@babel/helper-replace-supers": ^7.19.1 - "@babel/helper-split-export-declaration": ^7.18.6 - peerDependencies: - "@babel/core": ^7.0.0 - checksum: 51b0662cc44ae5fe3691ed552f97312006709ec3f5321a5e5b5a139a5743eaaf65987f30ee7c171af80ab77460fb57c1970b0b1583dd70d90b58e4433b117a1b - languageName: node - linkType: hard - "@babel/helper-create-regexp-features-plugin@npm:^7.14.5": version: 7.14.5 resolution: "@babel/helper-create-regexp-features-plugin@npm:7.14.5" @@ -888,19 +926,19 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.12.1, @babel/helper-module-transforms@npm:^7.20.2": - version: 7.20.2 - resolution: "@babel/helper-module-transforms@npm:7.20.2" +"@babel/helper-module-transforms@npm:^7.12.1, @babel/helper-module-transforms@npm:^7.19.6": + version: 7.19.6 + resolution: "@babel/helper-module-transforms@npm:7.19.6" dependencies: "@babel/helper-environment-visitor": ^7.18.9 "@babel/helper-module-imports": ^7.18.6 - "@babel/helper-simple-access": ^7.20.2 + "@babel/helper-simple-access": ^7.19.4 "@babel/helper-split-export-declaration": ^7.18.6 "@babel/helper-validator-identifier": ^7.19.1 "@babel/template": ^7.18.10 - "@babel/traverse": ^7.20.1 - "@babel/types": ^7.20.2 - checksum: 33a60ca115f6fce2c9d98e2a2e5649498aa7b23e2ae3c18745d7a021487708fc311458c33542f299387a0da168afccba94116e077f2cce49ae9e5ab83399e8a2 + "@babel/traverse": ^7.19.6 + "@babel/types": ^7.19.4 + checksum: c28692b37d4b5abacc775bcab52a74f44a493f38c58cb72b56a6c6d67a97485dd8aff6f26905abd1a924d3261a171d0214a9fb76f48d8598f1e35b8b29284792 languageName: node linkType: hard @@ -984,19 +1022,35 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.19.6": - version: 7.19.6 - resolution: "@babel/helper-module-transforms@npm:7.19.6" +"@babel/helper-module-transforms@npm:^7.19.0": + version: 7.19.0 + resolution: "@babel/helper-module-transforms@npm:7.19.0" dependencies: "@babel/helper-environment-visitor": ^7.18.9 "@babel/helper-module-imports": ^7.18.6 - "@babel/helper-simple-access": ^7.19.4 + "@babel/helper-simple-access": ^7.18.6 + "@babel/helper-split-export-declaration": ^7.18.6 + "@babel/helper-validator-identifier": ^7.18.6 + "@babel/template": ^7.18.10 + "@babel/traverse": ^7.19.0 + "@babel/types": ^7.19.0 + checksum: 4483276c66f56cf3b5b063634092ad9438c2593725de5c143ba277dda82f1501e6d73b311c1b28036f181dbe36eaeff29f24726cde37a599d4e735af294e5359 + languageName: node + linkType: hard + +"@babel/helper-module-transforms@npm:^7.20.2": + version: 7.20.2 + resolution: "@babel/helper-module-transforms@npm:7.20.2" + dependencies: + "@babel/helper-environment-visitor": ^7.18.9 + "@babel/helper-module-imports": ^7.18.6 + "@babel/helper-simple-access": ^7.20.2 "@babel/helper-split-export-declaration": ^7.18.6 "@babel/helper-validator-identifier": ^7.19.1 "@babel/template": ^7.18.10 - "@babel/traverse": ^7.19.6 - "@babel/types": ^7.19.4 - checksum: c28692b37d4b5abacc775bcab52a74f44a493f38c58cb72b56a6c6d67a97485dd8aff6f26905abd1a924d3261a171d0214a9fb76f48d8598f1e35b8b29284792 + "@babel/traverse": ^7.20.1 + "@babel/types": ^7.20.2 + checksum: 33a60ca115f6fce2c9d98e2a2e5649498aa7b23e2ae3c18745d7a021487708fc311458c33542f299387a0da168afccba94116e077f2cce49ae9e5ab83399e8a2 languageName: node linkType: hard @@ -1324,14 +1378,14 @@ __metadata: languageName: node linkType: hard -"@babel/helpers@npm:^7.12.5, @babel/helpers@npm:^7.20.5": - version: 7.20.6 - resolution: "@babel/helpers@npm:7.20.6" +"@babel/helpers@npm:^7.12.5, @babel/helpers@npm:^7.19.4": + version: 7.19.4 + resolution: "@babel/helpers@npm:7.19.4" dependencies: "@babel/template": ^7.18.10 - "@babel/traverse": ^7.20.5 - "@babel/types": ^7.20.5 - checksum: f03ec6eb2bf8dc7cdfe2569ee421fd9ba6c7bac6c862d90b608ccdd80281ebe858bc56ca175fc92b3ac50f63126b66bbd5ec86f9f361729289a20054518f1ac5 + "@babel/traverse": ^7.19.4 + "@babel/types": ^7.19.4 + checksum: e2684e9a79d45b95db05c7e14628e8dd1d91ad59433a3afd715bdf19d4683d9e9f84382bcc82316b678aa609ecfc41b07be0b9c49eed07c444f82a6b9e501186 languageName: node linkType: hard @@ -1390,6 +1444,17 @@ __metadata: languageName: node linkType: hard +"@babel/helpers@npm:^7.20.5": + version: 7.20.6 + resolution: "@babel/helpers@npm:7.20.6" + dependencies: + "@babel/template": ^7.18.10 + "@babel/traverse": ^7.20.5 + "@babel/types": ^7.20.5 + checksum: f03ec6eb2bf8dc7cdfe2569ee421fd9ba6c7bac6c862d90b608ccdd80281ebe858bc56ca175fc92b3ac50f63126b66bbd5ec86f9f361729289a20054518f1ac5 + languageName: node + linkType: hard + "@babel/highlight@npm:^7.14.5": version: 7.14.5 resolution: "@babel/highlight@npm:7.14.5" @@ -1443,7 +1508,7 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.12.11, @babel/parser@npm:^7.19.6": +"@babel/parser@npm:^7.12.11, @babel/parser@npm:^7.12.7, @babel/parser@npm:^7.19.6": version: 7.19.6 resolution: "@babel/parser@npm:7.19.6" bin: @@ -1452,7 +1517,7 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.12.7, @babel/parser@npm:^7.13.0, @babel/parser@npm:^7.20.5": +"@babel/parser@npm:^7.13.0, @babel/parser@npm:^7.20.5": version: 7.20.5 resolution: "@babel/parser@npm:7.20.5" bin: @@ -1497,7 +1562,7 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.18.10": +"@babel/parser@npm:^7.18.10, @babel/parser@npm:^7.19.0": version: 7.19.0 resolution: "@babel/parser@npm:7.19.0" bin: @@ -1562,6 +1627,20 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-proposal-async-generator-functions@npm:^7.19.1": + version: 7.19.1 + resolution: "@babel/plugin-proposal-async-generator-functions@npm:7.19.1" + dependencies: + "@babel/helper-environment-visitor": ^7.18.9 + "@babel/helper-plugin-utils": ^7.19.0 + "@babel/helper-remap-async-to-generator": ^7.18.9 + "@babel/plugin-syntax-async-generators": ^7.8.4 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: f101555b00aee6ee0107c9e40d872ad646bbd3094abdbeda56d17b107df69a0cb49e5d02dcf5f9d8753e25564e798d08429f12d811aaa1b307b6a725c0b8159c + languageName: node + linkType: hard + "@babel/plugin-proposal-async-generator-functions@npm:^7.20.1": version: 7.20.1 resolution: "@babel/plugin-proposal-async-generator-functions@npm:7.20.1" @@ -1602,17 +1681,17 @@ __metadata: linkType: hard "@babel/plugin-proposal-decorators@npm:^7.12.12": - version: 7.20.5 - resolution: "@babel/plugin-proposal-decorators@npm:7.20.5" + version: 7.19.6 + resolution: "@babel/plugin-proposal-decorators@npm:7.19.6" dependencies: - "@babel/helper-create-class-features-plugin": ^7.20.5 - "@babel/helper-plugin-utils": ^7.20.2 + "@babel/helper-create-class-features-plugin": ^7.19.0 + "@babel/helper-plugin-utils": ^7.19.0 "@babel/helper-replace-supers": ^7.19.1 "@babel/helper-split-export-declaration": ^7.18.6 "@babel/plugin-syntax-decorators": ^7.19.0 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 780696710dcd5f292a235dcc9dbb1fd6600a1b91c75b5c6efaf6d596520d54c750dabca5ebdb4592534f1572bdca3d424145741815554660335a10a4168ca19a + checksum: 69162475282507e1579232fdaae26330cfcfa7843f4a943383d76c61a5e225ea1fe08edd7c700c400694ab9b57e8b3928b757da985ac613ddfc78be5a9b61c47 languageName: node linkType: hard @@ -1728,7 +1807,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-proposal-object-rest-spread@npm:7.20.2, @babel/plugin-proposal-object-rest-spread@npm:^7.12.1, @babel/plugin-proposal-object-rest-spread@npm:^7.20.2": +"@babel/plugin-proposal-object-rest-spread@npm:7.20.2, @babel/plugin-proposal-object-rest-spread@npm:^7.20.2": version: 7.20.2 resolution: "@babel/plugin-proposal-object-rest-spread@npm:7.20.2" dependencies: @@ -1743,6 +1822,21 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-proposal-object-rest-spread@npm:^7.12.1, @babel/plugin-proposal-object-rest-spread@npm:^7.19.4": + version: 7.19.4 + resolution: "@babel/plugin-proposal-object-rest-spread@npm:7.19.4" + dependencies: + "@babel/compat-data": ^7.19.4 + "@babel/helper-compilation-targets": ^7.19.3 + "@babel/helper-plugin-utils": ^7.19.0 + "@babel/plugin-syntax-object-rest-spread": ^7.8.3 + "@babel/plugin-transform-parameters": ^7.18.8 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 90a2a59da305e6c8c83831e16079193df33d727a77a90972e286af2c8c0295fddb91b0978b88f16f63080d08a82b08ce3ee82a88b0488b3c51decc73c1d35786 + languageName: node + linkType: hard + "@babel/plugin-proposal-optional-catch-binding@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-proposal-optional-catch-binding@npm:7.18.6" @@ -1793,21 +1887,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-proposal-private-property-in-object@npm:^7.12.1": - version: 7.20.5 - resolution: "@babel/plugin-proposal-private-property-in-object@npm:7.20.5" - dependencies: - "@babel/helper-annotate-as-pure": ^7.18.6 - "@babel/helper-create-class-features-plugin": ^7.20.5 - "@babel/helper-plugin-utils": ^7.20.2 - "@babel/plugin-syntax-private-property-in-object": ^7.14.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 513b5e0e2c1b2846be5336cf680e932ae17924ef885aa1429e1a4f7924724bdd99b15f28d67187d0a006d5f18a0c4b61d96c3ecb4902fed3c8fe2f0abfc9753a - languageName: node - linkType: hard - -"@babel/plugin-proposal-private-property-in-object@npm:^7.18.6": +"@babel/plugin-proposal-private-property-in-object@npm:^7.12.1, @babel/plugin-proposal-private-property-in-object@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-proposal-private-property-in-object@npm:7.18.6" dependencies: @@ -2177,14 +2257,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-block-scoping@npm:^7.12.12, @babel/plugin-transform-block-scoping@npm:^7.20.2": - version: 7.20.5 - resolution: "@babel/plugin-transform-block-scoping@npm:7.20.5" +"@babel/plugin-transform-block-scoping@npm:^7.12.12, @babel/plugin-transform-block-scoping@npm:^7.19.4": + version: 7.19.4 + resolution: "@babel/plugin-transform-block-scoping@npm:7.19.4" dependencies: - "@babel/helper-plugin-utils": ^7.20.2 + "@babel/helper-plugin-utils": ^7.19.0 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 03606bc6710c15cd4e4d1163e1cbab08799f852a5dd55a1f7e115032e9406ac9430ddc0cb6d09a51a4095446985640411f60683c6fcea9bc1a7b202462022e1c + checksum: 86353ccbb57b4a0513ac2b1209271858f9c3f2c56b15a6225ff5f1c97ffb1c48f8984046a718a9835ecdae100cbe80ed0b9ca15a5554e33386671b56a8cd887c languageName: node linkType: hard @@ -2199,22 +2279,33 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-classes@npm:^7.12.1, @babel/plugin-transform-classes@npm:^7.20.2": - version: 7.20.2 - resolution: "@babel/plugin-transform-classes@npm:7.20.2" +"@babel/plugin-transform-block-scoping@npm:^7.20.2": + version: 7.20.5 + resolution: "@babel/plugin-transform-block-scoping@npm:7.20.5" + dependencies: + "@babel/helper-plugin-utils": ^7.20.2 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 03606bc6710c15cd4e4d1163e1cbab08799f852a5dd55a1f7e115032e9406ac9430ddc0cb6d09a51a4095446985640411f60683c6fcea9bc1a7b202462022e1c + languageName: node + linkType: hard + +"@babel/plugin-transform-classes@npm:^7.12.1, @babel/plugin-transform-classes@npm:^7.19.0": + version: 7.19.0 + resolution: "@babel/plugin-transform-classes@npm:7.19.0" dependencies: "@babel/helper-annotate-as-pure": ^7.18.6 - "@babel/helper-compilation-targets": ^7.20.0 + "@babel/helper-compilation-targets": ^7.19.0 "@babel/helper-environment-visitor": ^7.18.9 "@babel/helper-function-name": ^7.19.0 "@babel/helper-optimise-call-expression": ^7.18.6 - "@babel/helper-plugin-utils": ^7.20.2 - "@babel/helper-replace-supers": ^7.19.1 + "@babel/helper-plugin-utils": ^7.19.0 + "@babel/helper-replace-supers": ^7.18.9 "@babel/helper-split-export-declaration": ^7.18.6 globals: ^11.1.0 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 57f3467a8eb7853cdb61cda963cfb6c6568ad276d77c9de2ff5a2194650010217aa318ef3733975537c6fb906b73a019afb6ea650b01852e7d2e1fab4034361b + checksum: 5500953031fc3eae73f717c7b59ef406158a4a710d566a0f78a4944240bcf98f817f07cf1d6af0e749e21f0dfee29c36412b75d57b0a753c3ad823b70c596b79 languageName: node linkType: hard @@ -2236,6 +2327,25 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-classes@npm:^7.20.2": + version: 7.20.2 + resolution: "@babel/plugin-transform-classes@npm:7.20.2" + dependencies: + "@babel/helper-annotate-as-pure": ^7.18.6 + "@babel/helper-compilation-targets": ^7.20.0 + "@babel/helper-environment-visitor": ^7.18.9 + "@babel/helper-function-name": ^7.19.0 + "@babel/helper-optimise-call-expression": ^7.18.6 + "@babel/helper-plugin-utils": ^7.20.2 + "@babel/helper-replace-supers": ^7.19.1 + "@babel/helper-split-export-declaration": ^7.18.6 + globals: ^11.1.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 57f3467a8eb7853cdb61cda963cfb6c6568ad276d77c9de2ff5a2194650010217aa318ef3733975537c6fb906b73a019afb6ea650b01852e7d2e1fab4034361b + languageName: node + linkType: hard + "@babel/plugin-transform-computed-properties@npm:^7.18.9": version: 7.18.9 resolution: "@babel/plugin-transform-computed-properties@npm:7.18.9" @@ -2247,14 +2357,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-destructuring@npm:^7.12.1, @babel/plugin-transform-destructuring@npm:^7.20.2": - version: 7.20.2 - resolution: "@babel/plugin-transform-destructuring@npm:7.20.2" +"@babel/plugin-transform-destructuring@npm:^7.12.1, @babel/plugin-transform-destructuring@npm:^7.19.4": + version: 7.19.4 + resolution: "@babel/plugin-transform-destructuring@npm:7.19.4" dependencies: - "@babel/helper-plugin-utils": ^7.20.2 + "@babel/helper-plugin-utils": ^7.19.0 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 09033e09b28ca1b0d46a8d82f5a677b1d718a739b3c199886908c3ef1af23369317d0c429b21507d480ee82721c15892a9893be18e50ad6fc219e69312f4b097 + checksum: 0ca40f6abf7273dafefb7a1cc11fef2b9ab3edbd23188cdcff8cd5e30783b89d64e7813e44aae9efab417b90972ae80971bf6c4130eeeb112bcfb44100c72657 languageName: node linkType: hard @@ -2269,6 +2379,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-destructuring@npm:^7.20.2": + version: 7.20.2 + resolution: "@babel/plugin-transform-destructuring@npm:7.20.2" + dependencies: + "@babel/helper-plugin-utils": ^7.20.2 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 09033e09b28ca1b0d46a8d82f5a677b1d718a739b3c199886908c3ef1af23369317d0c429b21507d480ee82721c15892a9893be18e50ad6fc219e69312f4b097 + languageName: node + linkType: hard + "@babel/plugin-transform-dotall-regex@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-transform-dotall-regex@npm:7.18.6" @@ -2441,6 +2562,21 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-modules-systemjs@npm:^7.19.0": + version: 7.19.0 + resolution: "@babel/plugin-transform-modules-systemjs@npm:7.19.0" + dependencies: + "@babel/helper-hoist-variables": ^7.18.6 + "@babel/helper-module-transforms": ^7.19.0 + "@babel/helper-plugin-utils": ^7.19.0 + "@babel/helper-validator-identifier": ^7.18.6 + babel-plugin-dynamic-import-node: ^2.3.3 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: a0742deee4a076d6fc303d036c1ea2bea9b7d91af390483fe91fc415f9cb43925bb5dd930fdcb8fcdc9d4c7a22774a3cec521c67f1422a9b473debcb85ee57f9 + languageName: node + linkType: hard + "@babel/plugin-transform-modules-systemjs@npm:^7.19.6": version: 7.19.6 resolution: "@babel/plugin-transform-modules-systemjs@npm:7.19.6" @@ -2514,18 +2650,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-parameters@npm:^7.12.1, @babel/plugin-transform-parameters@npm:^7.20.1": - version: 7.20.5 - resolution: "@babel/plugin-transform-parameters@npm:7.20.5" - dependencies: - "@babel/helper-plugin-utils": ^7.20.2 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: fa588b0d8551e3e0cfde5fcb9d63a7acd38da199bee1851dd7e2abb34b3d754684defb1209a5669ecf0076d3d17ddc375b3f107da770b550a30402e4b9d7aa2f - languageName: node - linkType: hard - -"@babel/plugin-transform-parameters@npm:^7.18.8": +"@babel/plugin-transform-parameters@npm:^7.12.1, @babel/plugin-transform-parameters@npm:^7.18.8": version: 7.18.8 resolution: "@babel/plugin-transform-parameters@npm:7.18.8" dependencies: @@ -2536,6 +2661,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-parameters@npm:^7.20.1": + version: 7.20.5 + resolution: "@babel/plugin-transform-parameters@npm:7.20.5" + dependencies: + "@babel/helper-plugin-utils": ^7.20.2 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: fa588b0d8551e3e0cfde5fcb9d63a7acd38da199bee1851dd7e2abb34b3d754684defb1209a5669ecf0076d3d17ddc375b3f107da770b550a30402e4b9d7aa2f + languageName: node + linkType: hard + "@babel/plugin-transform-property-literals@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-transform-property-literals@npm:7.18.6" @@ -2924,7 +3060,7 @@ __metadata: languageName: node linkType: hard -"@babel/preset-env@npm:7.20.2, @babel/preset-env@npm:^7.12.11": +"@babel/preset-env@npm:7.20.2": version: 7.20.2 resolution: "@babel/preset-env@npm:7.20.2" dependencies: @@ -3009,6 +3145,91 @@ __metadata: languageName: node linkType: hard +"@babel/preset-env@npm:^7.12.11": + version: 7.19.4 + resolution: "@babel/preset-env@npm:7.19.4" + dependencies: + "@babel/compat-data": ^7.19.4 + "@babel/helper-compilation-targets": ^7.19.3 + "@babel/helper-plugin-utils": ^7.19.0 + "@babel/helper-validator-option": ^7.18.6 + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ^7.18.6 + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ^7.18.9 + "@babel/plugin-proposal-async-generator-functions": ^7.19.1 + "@babel/plugin-proposal-class-properties": ^7.18.6 + "@babel/plugin-proposal-class-static-block": ^7.18.6 + "@babel/plugin-proposal-dynamic-import": ^7.18.6 + "@babel/plugin-proposal-export-namespace-from": ^7.18.9 + "@babel/plugin-proposal-json-strings": ^7.18.6 + "@babel/plugin-proposal-logical-assignment-operators": ^7.18.9 + "@babel/plugin-proposal-nullish-coalescing-operator": ^7.18.6 + "@babel/plugin-proposal-numeric-separator": ^7.18.6 + "@babel/plugin-proposal-object-rest-spread": ^7.19.4 + "@babel/plugin-proposal-optional-catch-binding": ^7.18.6 + "@babel/plugin-proposal-optional-chaining": ^7.18.9 + "@babel/plugin-proposal-private-methods": ^7.18.6 + "@babel/plugin-proposal-private-property-in-object": ^7.18.6 + "@babel/plugin-proposal-unicode-property-regex": ^7.18.6 + "@babel/plugin-syntax-async-generators": ^7.8.4 + "@babel/plugin-syntax-class-properties": ^7.12.13 + "@babel/plugin-syntax-class-static-block": ^7.14.5 + "@babel/plugin-syntax-dynamic-import": ^7.8.3 + "@babel/plugin-syntax-export-namespace-from": ^7.8.3 + "@babel/plugin-syntax-import-assertions": ^7.18.6 + "@babel/plugin-syntax-json-strings": ^7.8.3 + "@babel/plugin-syntax-logical-assignment-operators": ^7.10.4 + "@babel/plugin-syntax-nullish-coalescing-operator": ^7.8.3 + "@babel/plugin-syntax-numeric-separator": ^7.10.4 + "@babel/plugin-syntax-object-rest-spread": ^7.8.3 + "@babel/plugin-syntax-optional-catch-binding": ^7.8.3 + "@babel/plugin-syntax-optional-chaining": ^7.8.3 + "@babel/plugin-syntax-private-property-in-object": ^7.14.5 + "@babel/plugin-syntax-top-level-await": ^7.14.5 + "@babel/plugin-transform-arrow-functions": ^7.18.6 + "@babel/plugin-transform-async-to-generator": ^7.18.6 + "@babel/plugin-transform-block-scoped-functions": ^7.18.6 + "@babel/plugin-transform-block-scoping": ^7.19.4 + "@babel/plugin-transform-classes": ^7.19.0 + "@babel/plugin-transform-computed-properties": ^7.18.9 + "@babel/plugin-transform-destructuring": ^7.19.4 + "@babel/plugin-transform-dotall-regex": ^7.18.6 + "@babel/plugin-transform-duplicate-keys": ^7.18.9 + "@babel/plugin-transform-exponentiation-operator": ^7.18.6 + "@babel/plugin-transform-for-of": ^7.18.8 + "@babel/plugin-transform-function-name": ^7.18.9 + "@babel/plugin-transform-literals": ^7.18.9 + "@babel/plugin-transform-member-expression-literals": ^7.18.6 + "@babel/plugin-transform-modules-amd": ^7.18.6 + "@babel/plugin-transform-modules-commonjs": ^7.18.6 + "@babel/plugin-transform-modules-systemjs": ^7.19.0 + "@babel/plugin-transform-modules-umd": ^7.18.6 + "@babel/plugin-transform-named-capturing-groups-regex": ^7.19.1 + "@babel/plugin-transform-new-target": ^7.18.6 + "@babel/plugin-transform-object-super": ^7.18.6 + "@babel/plugin-transform-parameters": ^7.18.8 + "@babel/plugin-transform-property-literals": ^7.18.6 + "@babel/plugin-transform-regenerator": ^7.18.6 + "@babel/plugin-transform-reserved-words": ^7.18.6 + "@babel/plugin-transform-shorthand-properties": ^7.18.6 + "@babel/plugin-transform-spread": ^7.19.0 + "@babel/plugin-transform-sticky-regex": ^7.18.6 + "@babel/plugin-transform-template-literals": ^7.18.9 + "@babel/plugin-transform-typeof-symbol": ^7.18.9 + "@babel/plugin-transform-unicode-escapes": ^7.18.10 + "@babel/plugin-transform-unicode-regex": ^7.18.6 + "@babel/preset-modules": ^0.1.5 + "@babel/types": ^7.19.4 + babel-plugin-polyfill-corejs2: ^0.3.3 + babel-plugin-polyfill-corejs3: ^0.6.0 + babel-plugin-polyfill-regenerator: ^0.4.1 + core-js-compat: ^3.25.1 + semver: ^6.3.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: f12af25281f3c5e7df60fa1e79ad481ddd7f6a111d4c0fabcffdabf0eaed3a01b4f8c647ae5445ed1f58df70f52083ffd283e8919ade7afa73801a49c733d22c + languageName: node + linkType: hard + "@babel/preset-flow@npm:^7.12.1": version: 7.18.6 resolution: "@babel/preset-flow@npm:7.18.6" @@ -3117,7 +3338,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.5.0, @babel/runtime@npm:^7.7.6": +"@babel/runtime@npm:^7.20.0": version: 7.20.6 resolution: "@babel/runtime@npm:7.20.6" dependencies: @@ -3127,15 +3348,15 @@ __metadata: linkType: hard "@babel/runtime@npm:^7.20.7": - version: 7.20.7 - resolution: "@babel/runtime@npm:7.20.7" + version: 7.20.13 + resolution: "@babel/runtime@npm:7.20.13" dependencies: regenerator-runtime: ^0.13.11 - checksum: 4629ce5c46f06cca9cfb9b7fc00d48003335a809888e2b91ec2069a2dcfbfef738480cff32ba81e0b7c290f8918e5c22ddcf2b710001464ee84ba62c7e32a3a3 + checksum: 09b7a97a05c80540db6c9e4ddf8c5d2ebb06cae5caf3a87e33c33f27f8c4d49d9c67a2d72f1570e796045288fad569f98a26ceba0c4f5fad2af84b6ad855c4fb languageName: node linkType: hard -"@babel/runtime@npm:^7.3.1": +"@babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.0, @babel/runtime@npm:^7.7.6": version: 7.19.4 resolution: "@babel/runtime@npm:7.19.4" dependencies: @@ -3199,7 +3420,7 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.1.6, @babel/traverse@npm:^7.19.1, @babel/traverse@npm:^7.19.6": +"@babel/traverse@npm:^7.1.6, @babel/traverse@npm:^7.12.11, @babel/traverse@npm:^7.12.9, @babel/traverse@npm:^7.19.1, @babel/traverse@npm:^7.19.4, @babel/traverse@npm:^7.19.6": version: 7.19.6 resolution: "@babel/traverse@npm:7.19.6" dependencies: @@ -3217,24 +3438,6 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.12.11, @babel/traverse@npm:^7.12.9, @babel/traverse@npm:^7.17.9, @babel/traverse@npm:^7.20.1, @babel/traverse@npm:^7.20.5": - version: 7.20.5 - resolution: "@babel/traverse@npm:7.20.5" - dependencies: - "@babel/code-frame": ^7.18.6 - "@babel/generator": ^7.20.5 - "@babel/helper-environment-visitor": ^7.18.9 - "@babel/helper-function-name": ^7.19.0 - "@babel/helper-hoist-variables": ^7.18.6 - "@babel/helper-split-export-declaration": ^7.18.6 - "@babel/parser": ^7.20.5 - "@babel/types": ^7.20.5 - debug: ^4.1.0 - globals: ^11.1.0 - checksum: c7fed468614aab1cf762dda5df26e2cfcd2b1b448c9d3321ac44786c4ee773fb0e10357e6593c3c6a648ae2e0be6d90462d855998dc10e3abae84de99291e008 - languageName: node - linkType: hard - "@babel/traverse@npm:^7.13.0, @babel/traverse@npm:^7.15.4": version: 7.15.4 resolution: "@babel/traverse@npm:7.15.4" @@ -3305,6 +3508,24 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.17.9, @babel/traverse@npm:^7.20.1, @babel/traverse@npm:^7.20.5": + version: 7.20.5 + resolution: "@babel/traverse@npm:7.20.5" + dependencies: + "@babel/code-frame": ^7.18.6 + "@babel/generator": ^7.20.5 + "@babel/helper-environment-visitor": ^7.18.9 + "@babel/helper-function-name": ^7.19.0 + "@babel/helper-hoist-variables": ^7.18.6 + "@babel/helper-split-export-declaration": ^7.18.6 + "@babel/parser": ^7.20.5 + "@babel/types": ^7.20.5 + debug: ^4.1.0 + globals: ^11.1.0 + checksum: c7fed468614aab1cf762dda5df26e2cfcd2b1b448c9d3321ac44786c4ee773fb0e10357e6593c3c6a648ae2e0be6d90462d855998dc10e3abae84de99291e008 + languageName: node + linkType: hard + "@babel/traverse@npm:^7.18.0, @babel/traverse@npm:^7.18.2": version: 7.18.2 resolution: "@babel/traverse@npm:7.18.2" @@ -3359,6 +3580,24 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.19.0": + version: 7.19.0 + resolution: "@babel/traverse@npm:7.19.0" + dependencies: + "@babel/code-frame": ^7.18.6 + "@babel/generator": ^7.19.0 + "@babel/helper-environment-visitor": ^7.18.9 + "@babel/helper-function-name": ^7.19.0 + "@babel/helper-hoist-variables": ^7.18.6 + "@babel/helper-split-export-declaration": ^7.18.6 + "@babel/parser": ^7.19.0 + "@babel/types": ^7.19.0 + debug: ^4.1.0 + globals: ^11.1.0 + checksum: dcbd1316c9f4bf3cefee45b6f5194590563aa5d123500a60d3c8d714bef279205014c8e599ebafc469967199a7622e1444cd0235c16d4243da437e3f1281771e + languageName: node + linkType: hard + "@babel/types@npm:^7.0.0, @babel/types@npm:^7.15.4, @babel/types@npm:^7.15.6, @babel/types@npm:^7.2.0, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": version: 7.15.6 resolution: "@babel/types@npm:7.15.6" @@ -3369,18 +3608,7 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.12.11, @babel/types@npm:^7.12.7, @babel/types@npm:^7.20.2, @babel/types@npm:^7.20.5": - version: 7.20.5 - resolution: "@babel/types@npm:7.20.5" - dependencies: - "@babel/helper-string-parser": ^7.19.4 - "@babel/helper-validator-identifier": ^7.19.1 - to-fast-properties: ^2.0.0 - checksum: 773f0a1ad9f6ca5c5beaf751d1d8d81b9130de87689d1321fc911d73c3b1167326d66f0ae086a27fb5bfc8b4ee3ffebf1339be50d3b4d8015719692468c31f2d - languageName: node - linkType: hard - -"@babel/types@npm:^7.14.8, @babel/types@npm:^7.19.4": +"@babel/types@npm:^7.12.11, @babel/types@npm:^7.12.7, @babel/types@npm:^7.14.8, @babel/types@npm:^7.19.4": version: 7.19.4 resolution: "@babel/types@npm:7.19.4" dependencies: @@ -3473,6 +3701,17 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.20.2, @babel/types@npm:^7.20.5": + version: 7.20.5 + resolution: "@babel/types@npm:7.20.5" + dependencies: + "@babel/helper-string-parser": ^7.19.4 + "@babel/helper-validator-identifier": ^7.19.1 + to-fast-properties: ^2.0.0 + checksum: 773f0a1ad9f6ca5c5beaf751d1d8d81b9130de87689d1321fc911d73c3b1167326d66f0ae086a27fb5bfc8b4ee3ffebf1339be50d3b4d8015719692468c31f2d + languageName: node + linkType: hard + "@base2/pretty-print-object@npm:1.0.1": version: 1.0.1 resolution: "@base2/pretty-print-object@npm:1.0.1" @@ -4281,6 +4520,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm@npm:0.15.12": + version: 0.15.12 + resolution: "@esbuild/android-arm@npm:0.15.12" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@esbuild/android-arm@npm:0.16.17": version: 0.16.17 resolution: "@esbuild/android-arm@npm:0.16.17" @@ -4344,6 +4590,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-loong64@npm:0.15.12": + version: 0.15.12 + resolution: "@esbuild/linux-loong64@npm:0.15.12" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + "@esbuild/linux-loong64@npm:0.16.17": version: 0.16.17 resolution: "@esbuild/linux-loong64@npm:0.16.17" @@ -4853,7 +5106,7 @@ __metadata: languageName: node linkType: hard -"@grafana/runtime@9.4.0-pre, @grafana/runtime@workspace:*, @grafana/runtime@workspace:packages/grafana-runtime": +"@grafana/runtime@workspace:*, @grafana/runtime@workspace:packages/grafana-runtime": version: 0.0.0-use.local resolution: "@grafana/runtime@workspace:packages/grafana-runtime" dependencies: @@ -5195,7 +5448,18 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/config-array@npm:^0.11.6, @humanwhocodes/config-array@npm:^0.11.8": +"@humanwhocodes/config-array@npm:^0.11.6": + version: 0.11.7 + resolution: "@humanwhocodes/config-array@npm:0.11.7" + dependencies: + "@humanwhocodes/object-schema": ^1.2.1 + debug: ^4.1.1 + minimatch: ^3.0.5 + checksum: cf506dc45d9488af7fbf108ea6ac2151ba1a25e6d2b94b9b4fc36d2c1e4099b89ff560296dbfa13947e44604d4ca4a90d97a4fb167370bf8dd01a6ca2b6d83ac + languageName: node + linkType: hard + +"@humanwhocodes/config-array@npm:^0.11.8": version: 0.11.8 resolution: "@humanwhocodes/config-array@npm:0.11.8" dependencies: @@ -5302,56 +5566,6 @@ __metadata: languageName: node linkType: hard -"@jaegertracing/jaeger-ui-components@workspace:*, @jaegertracing/jaeger-ui-components@workspace:packages/jaeger-ui-components": - version: 0.0.0-use.local - resolution: "@jaegertracing/jaeger-ui-components@workspace:packages/jaeger-ui-components" - dependencies: - "@emotion/css": 11.10.5 - "@grafana/data": 9.4.0-pre - "@grafana/e2e-selectors": 9.4.0-pre - "@grafana/runtime": 9.4.0-pre - "@grafana/tsconfig": ^1.2.0-rc1 - "@grafana/ui": 9.4.0-pre - "@testing-library/jest-dom": 5.16.5 - "@testing-library/react": 12.1.4 - "@testing-library/user-event": 14.4.3 - "@types/deep-freeze": ^0.1.1 - "@types/hoist-non-react-statics": ^3.3.1 - "@types/jest": 29.2.3 - "@types/lodash": 4.14.187 - "@types/prop-types": 15.7.5 - "@types/react": 17.0.42 - "@types/react-icons": 2.2.7 - "@types/sinon": ^10.0.13 - "@types/slate-react": 0.22.9 - "@types/testing-library__jest-dom": 5.14.5 - "@types/tinycolor2": 1.4.3 - chance: ^1.0.10 - classnames: ^2.2.5 - combokeys: ^3.0.0 - copy-to-clipboard: ^3.1.0 - deep-freeze: ^0.0.1 - fuzzy: ^0.1.3 - hoist-non-react-statics: ^3.3.2 - json-markup: ^1.1.0 - lodash: 4.17.21 - lru-memoize: ^1.1.0 - memoize-one: 6.0.0 - moment: 2.29.4 - moment-timezone: 0.5.38 - prop-types: 15.8.1 - react: 17.0.2 - react-dom: 17.0.2 - react-icons: 2.2.7 - reselect: 4.1.6 - sinon: 14.0.1 - tinycolor2: 1.4.2 - tslib: 2.4.1 - tween-functions: ^1.2.0 - typescript: 4.8.4 - languageName: unknown - linkType: soft - "@jest/console@npm:^27.5.1": version: 27.5.1 resolution: "@jest/console@npm:27.5.1" @@ -7709,7 +7923,7 @@ __metadata: languageName: node linkType: hard -"@pmmmwh/react-refresh-webpack-plugin@npm:0.5.8": +"@pmmmwh/react-refresh-webpack-plugin@npm:0.5.8, @pmmmwh/react-refresh-webpack-plugin@npm:^0.5.3": version: 0.5.8 resolution: "@pmmmwh/react-refresh-webpack-plugin@npm:0.5.8" dependencies: @@ -7748,45 +7962,6 @@ __metadata: languageName: node linkType: hard -"@pmmmwh/react-refresh-webpack-plugin@npm:^0.5.3": - version: 0.5.10 - resolution: "@pmmmwh/react-refresh-webpack-plugin@npm:0.5.10" - dependencies: - ansi-html-community: ^0.0.8 - common-path-prefix: ^3.0.0 - core-js-pure: ^3.23.3 - error-stack-parser: ^2.0.6 - find-up: ^5.0.0 - html-entities: ^2.1.0 - loader-utils: ^2.0.4 - schema-utils: ^3.0.0 - source-map: ^0.7.3 - peerDependencies: - "@types/webpack": 4.x || 5.x - react-refresh: ">=0.10.0 <1.0.0" - sockjs-client: ^1.4.0 - type-fest: ">=0.17.0 <4.0.0" - webpack: ">=4.43.0 <6.0.0" - webpack-dev-server: 3.x || 4.x - webpack-hot-middleware: 2.x - webpack-plugin-serve: 0.x || 1.x - peerDependenciesMeta: - "@types/webpack": - optional: true - sockjs-client: - optional: true - type-fest: - optional: true - webpack-dev-server: - optional: true - webpack-hot-middleware: - optional: true - webpack-plugin-serve: - optional: true - checksum: c45beded9c56fbbdc7213a2c36131ace5db360ed704d462cc39d6678f980173a91c9a3f691e6bd3a026f25486644cd0027e8a12a0a4eced8e8b886a0472e7d34 - languageName: node - linkType: hard - "@polka/url@npm:^1.0.0-next.20": version: 1.0.0-next.21 resolution: "@polka/url@npm:1.0.0-next.21" @@ -10776,13 +10951,6 @@ __metadata: languageName: node linkType: hard -"@types/deep-freeze@npm:^0.1.1": - version: 0.1.2 - resolution: "@types/deep-freeze@npm:0.1.2" - checksum: 16a9f73ad2753049ae3d3b6d2b2d972bb47f21e237fd4a3d2a7b36bac9f69bd2b8671209d5d47d493e92861e283b1e999ec81260b158dd08d1855f8086ac8d40 - languageName: node - linkType: hard - "@types/dompurify@npm:^2": version: 2.4.0 resolution: "@types/dompurify@npm:2.4.0" @@ -10793,12 +10961,12 @@ __metadata: linkType: hard "@types/eslint-scope@npm:^3.7.3": - version: 3.7.4 - resolution: "@types/eslint-scope@npm:3.7.4" + version: 3.7.3 + resolution: "@types/eslint-scope@npm:3.7.3" dependencies: "@types/eslint": "*" "@types/estree": "*" - checksum: ea6a9363e92f301cd3888194469f9ec9d0021fe0a397a97a6dd689e7545c75de0bd2153dfb13d3ab532853a278b6572c6f678ce846980669e41029d205653460 + checksum: 6772b05e1b92003d1f295e81bc847a61f4fbe8ddab77ffa49e84ed3f9552513bdde677eb53ef167753901282857dd1d604d9f82eddb34a233495932b2dc3dc17 languageName: node linkType: hard @@ -10991,7 +11159,7 @@ __metadata: languageName: node linkType: hard -"@types/hoist-non-react-statics@npm:3.3.1, @types/hoist-non-react-statics@npm:^3.3.0, @types/hoist-non-react-statics@npm:^3.3.1": +"@types/hoist-non-react-statics@npm:3.3.1, @types/hoist-non-react-statics@npm:^3.3.0": version: 3.3.1 resolution: "@types/hoist-non-react-statics@npm:3.3.1" dependencies: @@ -11002,9 +11170,9 @@ __metadata: linkType: hard "@types/html-minifier-terser@npm:^6.0.0": - version: 6.1.0 - resolution: "@types/html-minifier-terser@npm:6.1.0" - checksum: eb843f6a8d662d44fb18ec61041117734c6aae77aa38df1be3b4712e8e50ffaa35f1e1c92fdd0fde14a5675fecf457abcd0d15a01fae7506c91926176967f452 + version: 6.0.0 + resolution: "@types/html-minifier-terser@npm:6.0.0" + checksum: 8f602498d726c9fd30d2b895478b4e7cb1f91558d892e44f54533669dbbbfae572c5fb2b04ee4fa5cbe7f8d59982d2067bf5c2931a3aefcf8dac590e4494b103 languageName: node linkType: hard @@ -11221,9 +11389,9 @@ __metadata: linkType: hard "@types/lodash@npm:^4.14.167": - version: 4.14.191 - resolution: "@types/lodash@npm:4.14.191" - checksum: ba0d5434e10690869f32d5ea49095250157cae502f10d57de0a723fd72229ce6c6a4979576f0f13e0aa9fbe3ce2457bfb9fa7d4ec3d6daba56730a51906d1491 + version: 4.14.186 + resolution: "@types/lodash@npm:4.14.186" + checksum: ee0c1368a8100bb6efb88335107473a41928fc307ff1ef4ff1278868ccddba9c04c68c36d1ffe3a0392ef4a956e1955f7de3203ec09df4f1655dd1b88485c549 languageName: node linkType: hard @@ -11354,9 +11522,9 @@ __metadata: linkType: hard "@types/node@npm:^14.0.10 || ^16.0.0, @types/node@npm:^14.14.20 || ^16.0.0": - version: 16.18.10 - resolution: "@types/node@npm:16.18.10" - checksum: 1b138616923e9a1c6d3806edf75714b605d2ec689357cdc675bc73816c508ff11b3c68df054b02a496c76654d8ed53add2e90816af39423431c73aa6eec06f29 + version: 16.18.0 + resolution: "@types/node@npm:16.18.0" + checksum: 4eb4b88012c7d3f527c1b4989cf085479d44ce418fb047fb8d3b545601a3e1fc436de8491b9734debeda8eae241963fb802cea87a5a5698bf5f6f3d489d446a8 languageName: node linkType: hard @@ -11455,13 +11623,6 @@ __metadata: languageName: node linkType: hard -"@types/prop-types@npm:15.7.5": - version: 15.7.5 - resolution: "@types/prop-types@npm:15.7.5" - checksum: 5b43b8b15415e1f298243165f1d44390403bb2bd42e662bca3b5b5633fdd39c938e91b7fce3a9483699db0f7a715d08cef220c121f723a634972fdf596aec980 - languageName: node - linkType: hard - "@types/qs@npm:*, @types/qs@npm:^6.9.5": version: 6.9.7 resolution: "@types/qs@npm:6.9.7" @@ -11560,25 +11721,6 @@ __metadata: languageName: node linkType: hard -"@types/react-icon-base@npm:*": - version: 2.1.4 - resolution: "@types/react-icon-base@npm:2.1.4" - dependencies: - "@types/react": "*" - checksum: 67b5d22c6234946b2b2dd137e75902642762248da190fbf3110f0d4c101fc435e19cc4a19866887868e77ef9ce4e5cdc2eb899a52ae5103f143de1ab6aaef85b - languageName: node - linkType: hard - -"@types/react-icons@npm:2.2.7": - version: 2.2.7 - resolution: "@types/react-icons@npm:2.2.7" - dependencies: - "@types/react": "*" - "@types/react-icon-base": "*" - checksum: b395741c537820af5b75852eed7909d203956711a296bebb8103f6e62ced9f6b4876805b4128a5b9a15f4566d2a63e3bedd6b565be5963b940e97c57c86d4db9 - languageName: node - linkType: hard - "@types/react-redux@npm:7.1.24": version: 7.1.24 resolution: "@types/react-redux@npm:7.1.24" @@ -11828,7 +11970,7 @@ __metadata: languageName: node linkType: hard -"@types/sinon@npm:10.0.13, @types/sinon@npm:^10.0.13": +"@types/sinon@npm:10.0.13": version: 10.0.13 resolution: "@types/sinon@npm:10.0.13" dependencies: @@ -13135,7 +13277,7 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.0.0, acorn@npm:^8.1.0, acorn@npm:^8.5.0": +"acorn@npm:^8.0.0, acorn@npm:^8.1.0": version: 8.8.1 resolution: "acorn@npm:8.8.1" bin: @@ -13153,6 +13295,15 @@ __metadata: languageName: node linkType: hard +"acorn@npm:^8.5.0": + version: 8.7.0 + resolution: "acorn@npm:8.7.0" + bin: + acorn: bin/acorn + checksum: e0f79409d68923fbf1aa6d4166f3eedc47955320d25c89a20cc822e6ba7c48c5963d5bc657bc242d68f7a4ac9faf96eef033e8f73656da6c640d4219935fdfd0 + languageName: node + linkType: hard + "acorn@npm:^8.7.1": version: 8.7.1 resolution: "acorn@npm:8.7.1" @@ -13672,16 +13823,16 @@ __metadata: languageName: node linkType: hard -"array-includes@npm:^3.0.3, array-includes@npm:^3.1.6": - version: 3.1.6 - resolution: "array-includes@npm:3.1.6" +"array-includes@npm:^3.0.3, array-includes@npm:^3.1.5": + version: 3.1.5 + resolution: "array-includes@npm:3.1.5" dependencies: call-bind: ^1.0.2 define-properties: ^1.1.4 - es-abstract: ^1.20.4 - get-intrinsic: ^1.1.3 + es-abstract: ^1.19.5 + get-intrinsic: ^1.1.1 is-string: ^1.0.7 - checksum: f22f8cd8ba8a6448d91eebdc69f04e4e55085d09232b5216ee2d476dab3ef59984e8d1889e662c6a0ed939dcb1b57fd05b2c0209c3370942fc41b752c82a2ca5 + checksum: f6f24d834179604656b7bec3e047251d5cc87e9e87fab7c175c61af48e80e75acd296017abcde21fb52292ab6a2a449ab2ee37213ee48c8709f004d75983f9c5 languageName: node linkType: hard @@ -13698,16 +13849,16 @@ __metadata: languageName: node linkType: hard -"array-includes@npm:^3.1.5": - version: 3.1.5 - resolution: "array-includes@npm:3.1.5" +"array-includes@npm:^3.1.6": + version: 3.1.6 + resolution: "array-includes@npm:3.1.6" dependencies: call-bind: ^1.0.2 define-properties: ^1.1.4 - es-abstract: ^1.19.5 - get-intrinsic: ^1.1.1 + es-abstract: ^1.20.4 + get-intrinsic: ^1.1.3 is-string: ^1.0.7 - checksum: f6f24d834179604656b7bec3e047251d5cc87e9e87fab7c175c61af48e80e75acd296017abcde21fb52292ab6a2a449ab2ee37213ee48c8709f004d75983f9c5 + checksum: f22f8cd8ba8a6448d91eebdc69f04e4e55085d09232b5216ee2d476dab3ef59984e8d1889e662c6a0ed939dcb1b57fd05b2c0209c3370942fc41b752c82a2ca5 languageName: node linkType: hard @@ -13748,19 +13899,7 @@ __metadata: languageName: node linkType: hard -"array.prototype.flat@npm:^1.2.1": - version: 1.3.1 - resolution: "array.prototype.flat@npm:1.3.1" - dependencies: - call-bind: ^1.0.2 - define-properties: ^1.1.4 - es-abstract: ^1.20.4 - es-shim-unscopables: ^1.0.0 - checksum: 5a8415949df79bf6e01afd7e8839bbde5a3581300e8ad5d8449dea52639e9e59b26a467665622783697917b43bf39940a6e621877c7dd9b3d1c1f97484b9b88b - languageName: node - linkType: hard - -"array.prototype.flat@npm:^1.2.5": +"array.prototype.flat@npm:^1.2.1, array.prototype.flat@npm:^1.2.5": version: 1.3.0 resolution: "array.prototype.flat@npm:1.3.0" dependencies: @@ -13772,15 +13911,15 @@ __metadata: languageName: node linkType: hard -"array.prototype.flatmap@npm:^1.2.1, array.prototype.flatmap@npm:^1.3.1": - version: 1.3.1 - resolution: "array.prototype.flatmap@npm:1.3.1" +"array.prototype.flatmap@npm:^1.2.1, array.prototype.flatmap@npm:^1.3.0": + version: 1.3.0 + resolution: "array.prototype.flatmap@npm:1.3.0" dependencies: call-bind: ^1.0.2 - define-properties: ^1.1.4 - es-abstract: ^1.20.4 + define-properties: ^1.1.3 + es-abstract: ^1.19.2 es-shim-unscopables: ^1.0.0 - checksum: 8c1c43a4995f12cf12523436da28515184c753807b3f0bc2ca6c075f71c470b099e2090cc67dba8e5280958fea401c1d0c59e1db0143272aef6cd1103921a987 + checksum: 818538f39409c4045d874be85df0dbd195e1446b14d22f95bdcfefea44ae77db44e42dcd89a559254ec5a7c8b338cfc986cc6d641e3472f9a5326b21eb2976a2 languageName: node linkType: hard @@ -13795,41 +13934,41 @@ __metadata: languageName: node linkType: hard -"array.prototype.flatmap@npm:^1.3.0": - version: 1.3.0 - resolution: "array.prototype.flatmap@npm:1.3.0" +"array.prototype.flatmap@npm:^1.3.1": + version: 1.3.1 + resolution: "array.prototype.flatmap@npm:1.3.1" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.1.4 + es-abstract: ^1.20.4 + es-shim-unscopables: ^1.0.0 + checksum: 8c1c43a4995f12cf12523436da28515184c753807b3f0bc2ca6c075f71c470b099e2090cc67dba8e5280958fea401c1d0c59e1db0143272aef6cd1103921a987 + languageName: node + linkType: hard + +"array.prototype.map@npm:^1.0.4": + version: 1.0.4 + resolution: "array.prototype.map@npm:1.0.4" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.1.3 + es-abstract: ^1.19.0 + es-array-method-boxes-properly: ^1.0.0 + is-string: ^1.0.7 + checksum: 08c8065ae9e60585c1262e54556da2340cd140dc799d790843c1f4ad3a3f458e9866d147c8ff0308741e8316904313f682803ca15c179f65cb2f5b993fa71a82 + languageName: node + linkType: hard + +"array.prototype.reduce@npm:^1.0.4": + version: 1.0.4 + resolution: "array.prototype.reduce@npm:1.0.4" dependencies: call-bind: ^1.0.2 define-properties: ^1.1.3 es-abstract: ^1.19.2 - es-shim-unscopables: ^1.0.0 - checksum: 818538f39409c4045d874be85df0dbd195e1446b14d22f95bdcfefea44ae77db44e42dcd89a559254ec5a7c8b338cfc986cc6d641e3472f9a5326b21eb2976a2 - languageName: node - linkType: hard - -"array.prototype.map@npm:^1.0.5": - version: 1.0.5 - resolution: "array.prototype.map@npm:1.0.5" - dependencies: - call-bind: ^1.0.2 - define-properties: ^1.1.4 - es-abstract: ^1.20.4 es-array-method-boxes-properly: ^1.0.0 is-string: ^1.0.7 - checksum: 70c4ecdd39480a51cfe84d18e4839a5f05d0b5d2785fee6838cd2bd5f86a17340a734ce7bb90c16804a70cead214b6f42c3d285f92267e11ccc0abd1880fe3b5 - languageName: node - linkType: hard - -"array.prototype.reduce@npm:^1.0.5": - version: 1.0.5 - resolution: "array.prototype.reduce@npm:1.0.5" - dependencies: - call-bind: ^1.0.2 - define-properties: ^1.1.4 - es-abstract: ^1.20.4 - es-array-method-boxes-properly: ^1.0.0 - is-string: ^1.0.7 - checksum: f44691395f9202aba5ec2446468d4c27209bfa81464f342ae024b7157dbf05b164e47cca01250b8c7c2a8219953fb57651cca16aab3d16f43b85c0d92c26eef3 + checksum: 6a57a1a2d3b77a9543db139cd52211f43a5af8e8271cb3c173be802076e3a6f71204ba8f090f5937ebc0842d5876db282f0f63dffd0e86b153e6e5a45681e4a5 languageName: node linkType: hard @@ -14137,22 +14276,7 @@ __metadata: languageName: node linkType: hard -"babel-loader@npm:^8.0.0": - version: 8.3.0 - resolution: "babel-loader@npm:8.3.0" - dependencies: - find-cache-dir: ^3.3.1 - loader-utils: ^2.0.0 - make-dir: ^3.1.0 - schema-utils: ^2.6.5 - peerDependencies: - "@babel/core": ^7.0.0 - webpack: ">=2" - checksum: d48bcf9e030e598656ad3ff5fb85967db2eaaf38af5b4a4b99d25618a2057f9f100e6b231af2a46c1913206db506115ca7a8cbdf52c9c73d767070dae4352ab5 - languageName: node - linkType: hard - -"babel-loader@npm:^8.2.5": +"babel-loader@npm:^8.0.0, babel-loader@npm:^8.2.5": version: 8.2.5 resolution: "babel-loader@npm:8.2.5" dependencies: @@ -14888,7 +15012,7 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.0.0, browserslist@npm:^4.16.6, browserslist@npm:^4.17.5": +"browserslist@npm:^4.0.0, browserslist@npm:^4.14.5, browserslist@npm:^4.16.6, browserslist@npm:^4.17.5": version: 4.17.5 resolution: "browserslist@npm:4.17.5" dependencies: @@ -14903,7 +15027,7 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.12.0, browserslist@npm:^4.14.5, browserslist@npm:^4.21.3, browserslist@npm:^4.21.4": +"browserslist@npm:^4.12.0, browserslist@npm:^4.21.3, browserslist@npm:^4.21.4": version: 4.21.4 resolution: "browserslist@npm:4.21.4" dependencies: @@ -15180,9 +15304,9 @@ __metadata: linkType: hard "call-me-maybe@npm:^1.0.1": - version: 1.0.2 - resolution: "call-me-maybe@npm:1.0.2" - checksum: 42ff2d0bed5b207e3f0122589162eaaa47ba618f79ad2382fe0ba14d9e49fbf901099a6227440acc5946f86a4953e8aa2d242b330b0a5de4d090bb18f8935cae + version: 1.0.1 + resolution: "call-me-maybe@npm:1.0.1" + checksum: d19e9d6ac2c6a83fb1215718b64c5e233f688ebebb603bdfe4af59cde952df1f2b648530fab555bf290ea910d69d7d9665ebc916e871e0e194f47c2e48e4886b languageName: node linkType: hard @@ -15293,9 +15417,9 @@ __metadata: linkType: hard "caniuse-lite@npm:^1.0.30001109": - version: 1.0.30001439 - resolution: "caniuse-lite@npm:1.0.30001439" - checksum: 3912dd536c9735713ca85e47721988bbcefb881ddb4886b0b9923fa984247fd22cba032cf268e57d158af0e8a2ae2eae042ae01942a1d6d7849fa9fa5d62fb82 + version: 1.0.30001423 + resolution: "caniuse-lite@npm:1.0.30001423" + checksum: fe443f323f5dc6a858ef7d7deddb93db5e5f9a35e22970c4a65c4ef793bb696c1e2f038df572722d9edf29021e43ed16f5131faafde783563bd0d9eccf486592 languageName: node linkType: hard @@ -15442,9 +15566,9 @@ __metadata: linkType: hard "chance@npm:^1.0.10": - version: 1.1.8 - resolution: "chance@npm:1.1.8" - checksum: e733f51e1094d7b0343a9e79f38599086442e6284fc2789cf9e072c71a0070874a1b340f9fffe4e66260899733f768cf113434be466ecb0016e3afcb60add3ee + version: 1.1.9 + resolution: "chance@npm:1.1.9" + checksum: 57d09fd404ffb87f5e106705c28c38a77cb0a6d96f3828d63052947a12d62d65f40a100d74b123bea3d5ebce3b53333a93d3b81783c64d8473788a0ce6b6e8db languageName: node linkType: hard @@ -15674,12 +15798,12 @@ __metadata: languageName: node linkType: hard -"clean-css@npm:^5.2.2": - version: 5.3.1 - resolution: "clean-css@npm:5.3.1" +"clean-css@npm:^5.1.5": + version: 5.2.2 + resolution: "clean-css@npm:5.2.2" dependencies: source-map: ~0.6.0 - checksum: 860696c60503cbfec480b5f92f62729246304b55950571af7292f2687b57f86b277f2b9fefe6f64643d409008018b78383972b55c2cc859792dcc8658988fb16 + checksum: 10855820829b8b6ea94e462313fdc177b297aca5c7870a969591549d6a766824f912b5e58773bd345b2a7effae863ab492258b5a77a40029fba6d11d861cbee3 languageName: node linkType: hard @@ -15777,6 +15901,17 @@ __metadata: languageName: node linkType: hard +"cliui@npm:^8.0.1": + version: 8.0.1 + resolution: "cliui@npm:8.0.1" + dependencies: + string-width: ^4.2.0 + strip-ansi: ^6.0.1 + wrap-ansi: ^7.0.0 + checksum: 79648b3b0045f2e285b76fb2e24e207c6db44323581e421c3acbd0e86454cba1b37aea976ab50195a49e7384b871e6dfb2247ad7dec53c02454ac6497394cb56 + languageName: node + linkType: hard + "clone-buffer@npm:^1.0.0": version: 1.0.0 resolution: "clone-buffer@npm:1.0.0" @@ -16058,7 +16193,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:8.3.0, commander@npm:^8.3.0": +"commander@npm:8.3.0, commander@npm:^8.1.0, commander@npm:^8.3.0": version: 8.3.0 resolution: "commander@npm:8.3.0" checksum: 0f82321821fc27b83bd409510bb9deeebcfa799ff0bf5d102128b500b7af22872c0c92cb6a0ebc5a4cf19c6b550fba9cedfa7329d18c6442a625f851377bacf0 @@ -16093,6 +16228,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^9.4.0": + version: 9.4.0 + resolution: "commander@npm:9.4.0" + checksum: a322de584a6ccd1ea83c24f6a660e52d16ffbe2613fcfbb8d2cc68bc9dec637492456d754fe8bb5b039ad843ed8e04fb0b107e581a75f62cde9e1a0ab1546e09 + languageName: node + linkType: hard + "commander@npm:^9.4.1, commander@npm:~9.4.1": version: 9.4.1 resolution: "commander@npm:9.4.1" @@ -16413,7 +16555,7 @@ __metadata: languageName: node linkType: hard -"copy-to-clipboard@npm:^3.1.0, copy-to-clipboard@npm:^3.3.1": +"copy-to-clipboard@npm:^3.3.1": version: 3.3.1 resolution: "copy-to-clipboard@npm:3.3.1" dependencies: @@ -16485,11 +16627,11 @@ __metadata: linkType: hard "core-js-compat@npm:^3.8.1": - version: 3.26.1 - resolution: "core-js-compat@npm:3.26.1" + version: 3.26.0 + resolution: "core-js-compat@npm:3.26.0" dependencies: browserslist: ^4.21.4 - checksum: f222bce0002eae405327d68286e1d566037e8ac21906a47d7ecd15858adca7b12e82140db11dc43c8cc1fc066c5306120f3c27bfb2d7dbc2d20a72a2d90d38dc + checksum: 120780ec33d441e476810abac9bf57199c2083006b179dc23d0ab0cfea096eff2a2fc3e9cb315d245735df661cfa4b76a8b8c37f5056fd02428a3cd2ea1d9f36 languageName: node linkType: hard @@ -16515,9 +16657,9 @@ __metadata: linkType: hard "core-js@npm:^3.0.4": - version: 3.26.1 - resolution: "core-js@npm:3.26.1" - checksum: 0a01149f51ff1e9f41d1ea49cc4c9222047949ea597189ede7c4cf8cde3b097766b9c7615acc77c86fe65b4002f20b638a133dfba7b41dba830d707aeeed45ad + version: 3.26.0 + resolution: "core-js@npm:3.26.0" + checksum: 0149eb9d3909fde9c17626af3a6e625c326e8598d0bb5e6c5b48a18e5fcd4eaf48d4964d873667d8148542ff590fb98eb3f93618da114ca54999d6bc0349734b languageName: node linkType: hard @@ -17787,13 +17929,6 @@ __metadata: languageName: node linkType: hard -"deep-freeze@npm:^0.0.1": - version: 0.0.1 - resolution: "deep-freeze@npm:0.0.1" - checksum: 1e43c98e44c7849382d9f896e679d48a1b5bf40993f7cc858e3730ef4e2ba387b9b7b7fe722cac34febe7f6a564cd242c27bbc319e8df793c2a287f21e5ba038 - languageName: node - linkType: hard - "deep-is@npm:^0.1.3, deep-is@npm:~0.1.3": version: 0.1.4 resolution: "deep-is@npm:0.1.4" @@ -18617,12 +18752,22 @@ __metadata: linkType: hard "enhanced-resolve@npm:^5.10.0": - version: 5.12.0 - resolution: "enhanced-resolve@npm:5.12.0" + version: 5.10.0 + resolution: "enhanced-resolve@npm:5.10.0" dependencies: graceful-fs: ^4.2.4 tapable: ^2.2.0 - checksum: bf3f787facaf4ce3439bef59d148646344e372bef5557f0d37ea8aa02c51f50a925cd1f07b8d338f18992c29f544ec235a8c64bcdb56030196c48832a5494174 + checksum: 0bb9830704db271610f900e8d79d70a740ea16f251263362b0c91af545576d09fe50103496606c1300a05e588372d6f9780a9bc2e30ce8ef9b827ec8f44687ff + languageName: node + linkType: hard + +"enhanced-resolve@npm:^5.9.2": + version: 5.9.2 + resolution: "enhanced-resolve@npm:5.9.2" + dependencies: + graceful-fs: ^4.2.4 + tapable: ^2.2.0 + checksum: 792b7a01abb4ee4433b658c71f92d5948675938e0c03cad1732abe843b87395f15cb880ace4f819f78ead94163278283afc79b8be63c0eddca8ab45f7d8c515d languageName: node linkType: hard @@ -18649,7 +18794,7 @@ __metadata: languageName: node linkType: hard -"entities@npm:^4.2.0, entities@npm:^4.4.0": +"entities@npm:^4.2.0, entities@npm:^4.3.1, entities@npm:^4.4.0": version: 4.4.0 resolution: "entities@npm:4.4.0" checksum: 84d250329f4b56b40fa93ed067b194db21e8815e4eb9b59f43a086f0ecd342814f6bc483de8a77da5d64e0f626033192b1b4f1792232a7ea6b970ebe0f3187c2 @@ -18809,6 +18954,38 @@ __metadata: languageName: node linkType: hard +"es-abstract@npm:^1.20.1": + version: 1.20.4 + resolution: "es-abstract@npm:1.20.4" + dependencies: + call-bind: ^1.0.2 + es-to-primitive: ^1.2.1 + function-bind: ^1.1.1 + function.prototype.name: ^1.1.5 + get-intrinsic: ^1.1.3 + get-symbol-description: ^1.0.0 + has: ^1.0.3 + has-property-descriptors: ^1.0.0 + has-symbols: ^1.0.3 + internal-slot: ^1.0.3 + is-callable: ^1.2.7 + is-negative-zero: ^2.0.2 + is-regex: ^1.1.4 + is-shared-array-buffer: ^1.0.2 + is-string: ^1.0.7 + is-weakref: ^1.0.2 + object-inspect: ^1.12.2 + object-keys: ^1.1.1 + object.assign: ^4.1.4 + regexp.prototype.flags: ^1.4.3 + safe-regex-test: ^1.0.0 + string.prototype.trimend: ^1.0.5 + string.prototype.trimstart: ^1.0.5 + unbox-primitive: ^1.0.2 + checksum: 89297cc785c31aedf961a603d5a07ed16471e435d3a1b6d070b54f157cf48454b95cda2ac55e4b86ff4fe3276e835fcffd2771578e6fa634337da49b26826141 + languageName: node + linkType: hard + "es-abstract@npm:^1.20.4": version: 1.21.1 resolution: "es-abstract@npm:1.21.1" @@ -18956,7 +19133,105 @@ __metadata: languageName: node linkType: hard -"esbuild-loader@npm:2.21.0, esbuild-loader@npm:^2.10.0": +"esbuild-android-64@npm:0.15.12": + version: 0.15.12 + resolution: "esbuild-android-64@npm:0.15.12" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + +"esbuild-android-arm64@npm:0.15.12": + version: 0.15.12 + resolution: "esbuild-android-arm64@npm:0.15.12" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"esbuild-darwin-64@npm:0.15.12": + version: 0.15.12 + resolution: "esbuild-darwin-64@npm:0.15.12" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"esbuild-darwin-arm64@npm:0.15.12": + version: 0.15.12 + resolution: "esbuild-darwin-arm64@npm:0.15.12" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"esbuild-freebsd-64@npm:0.15.12": + version: 0.15.12 + resolution: "esbuild-freebsd-64@npm:0.15.12" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"esbuild-freebsd-arm64@npm:0.15.12": + version: 0.15.12 + resolution: "esbuild-freebsd-arm64@npm:0.15.12" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + +"esbuild-linux-32@npm:0.15.12": + version: 0.15.12 + resolution: "esbuild-linux-32@npm:0.15.12" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + +"esbuild-linux-64@npm:0.15.12": + version: 0.15.12 + resolution: "esbuild-linux-64@npm:0.15.12" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"esbuild-linux-arm64@npm:0.15.12": + version: 0.15.12 + resolution: "esbuild-linux-arm64@npm:0.15.12" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"esbuild-linux-arm@npm:0.15.12": + version: 0.15.12 + resolution: "esbuild-linux-arm@npm:0.15.12" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"esbuild-linux-mips64le@npm:0.15.12": + version: 0.15.12 + resolution: "esbuild-linux-mips64le@npm:0.15.12" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + +"esbuild-linux-ppc64le@npm:0.15.12": + version: 0.15.12 + resolution: "esbuild-linux-ppc64le@npm:0.15.12" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + +"esbuild-linux-riscv64@npm:0.15.12": + version: 0.15.12 + resolution: "esbuild-linux-riscv64@npm:0.15.12" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + +"esbuild-linux-s390x@npm:0.15.12": + version: 0.15.12 + resolution: "esbuild-linux-s390x@npm:0.15.12" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + +"esbuild-loader@npm:2.21.0": version: 2.21.0 resolution: "esbuild-loader@npm:2.21.0" dependencies: @@ -18972,6 +19247,36 @@ __metadata: languageName: node linkType: hard +"esbuild-loader@npm:^2.10.0": + version: 2.20.0 + resolution: "esbuild-loader@npm:2.20.0" + dependencies: + esbuild: ^0.15.6 + joycon: ^3.0.1 + json5: ^2.2.0 + loader-utils: ^2.0.0 + tapable: ^2.2.0 + webpack-sources: ^2.2.0 + peerDependencies: + webpack: ^4.40.0 || ^5.0.0 + checksum: 81faee7155b35af1fdef3dffa273a14ec83e56b9efa1efb76cb1eb64964dd738809c147a87ab9d3507de11946eed51fd1ee42d476b2c9654cbda145da0d9479b + languageName: node + linkType: hard + +"esbuild-netbsd-64@npm:0.15.12": + version: 0.15.12 + resolution: "esbuild-netbsd-64@npm:0.15.12" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + +"esbuild-openbsd-64@npm:0.15.12": + version: 0.15.12 + resolution: "esbuild-openbsd-64@npm:0.15.12" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + "esbuild-plugin-browserslist@npm:^0.6.0": version: 0.6.0 resolution: "esbuild-plugin-browserslist@npm:0.6.0" @@ -18985,6 +19290,34 @@ __metadata: languageName: node linkType: hard +"esbuild-sunos-64@npm:0.15.12": + version: 0.15.12 + resolution: "esbuild-sunos-64@npm:0.15.12" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + +"esbuild-windows-32@npm:0.15.12": + version: 0.15.12 + resolution: "esbuild-windows-32@npm:0.15.12" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"esbuild-windows-64@npm:0.15.12": + version: 0.15.12 + resolution: "esbuild-windows-64@npm:0.15.12" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"esbuild-windows-arm64@npm:0.15.12": + version: 0.15.12 + resolution: "esbuild-windows-arm64@npm:0.15.12" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "esbuild@npm:0.16.17, esbuild@npm:^0.16.17": version: 0.16.17 resolution: "esbuild@npm:0.16.17" @@ -19062,6 +19395,83 @@ __metadata: languageName: node linkType: hard +"esbuild@npm:^0.15.6": + version: 0.15.12 + resolution: "esbuild@npm:0.15.12" + dependencies: + "@esbuild/android-arm": 0.15.12 + "@esbuild/linux-loong64": 0.15.12 + esbuild-android-64: 0.15.12 + esbuild-android-arm64: 0.15.12 + esbuild-darwin-64: 0.15.12 + esbuild-darwin-arm64: 0.15.12 + esbuild-freebsd-64: 0.15.12 + esbuild-freebsd-arm64: 0.15.12 + esbuild-linux-32: 0.15.12 + esbuild-linux-64: 0.15.12 + esbuild-linux-arm: 0.15.12 + esbuild-linux-arm64: 0.15.12 + esbuild-linux-mips64le: 0.15.12 + esbuild-linux-ppc64le: 0.15.12 + esbuild-linux-riscv64: 0.15.12 + esbuild-linux-s390x: 0.15.12 + esbuild-netbsd-64: 0.15.12 + esbuild-openbsd-64: 0.15.12 + esbuild-sunos-64: 0.15.12 + esbuild-windows-32: 0.15.12 + esbuild-windows-64: 0.15.12 + esbuild-windows-arm64: 0.15.12 + dependenciesMeta: + "@esbuild/android-arm": + optional: true + "@esbuild/linux-loong64": + optional: true + esbuild-android-64: + optional: true + esbuild-android-arm64: + optional: true + esbuild-darwin-64: + optional: true + esbuild-darwin-arm64: + optional: true + esbuild-freebsd-64: + optional: true + esbuild-freebsd-arm64: + optional: true + esbuild-linux-32: + optional: true + esbuild-linux-64: + optional: true + esbuild-linux-arm: + optional: true + esbuild-linux-arm64: + optional: true + esbuild-linux-mips64le: + optional: true + esbuild-linux-ppc64le: + optional: true + esbuild-linux-riscv64: + optional: true + esbuild-linux-s390x: + optional: true + esbuild-netbsd-64: + optional: true + esbuild-openbsd-64: + optional: true + esbuild-sunos-64: + optional: true + esbuild-windows-32: + optional: true + esbuild-windows-64: + optional: true + esbuild-windows-arm64: + optional: true + bin: + esbuild: bin/esbuild + checksum: b344d52c57616917719ac2fa38a58eba7d3c9d2a295116272b3e16a4f6327dc42549274c06560d301f9235a6fe31ccb45499b31d04820dfb8527d89d9766a2ad + languageName: node + linkType: hard + "escalade@npm:^3.1.1": version: 3.1.1 resolution: "escalade@npm:3.1.1" @@ -20965,13 +21375,6 @@ __metadata: languageName: node linkType: hard -"fuzzy@npm:^0.1.3": - version: 0.1.3 - resolution: "fuzzy@npm:0.1.3" - checksum: acc09c6173e12d5dc8ae51857551ddbe834befa9ebc6be6d5581d09117265d704809d80407d220fd0652f347a9975a4d106854cacc8bd031487a0ede86982f84 - languageName: node - linkType: hard - "gauge@npm:^3.0.0": version: 3.0.2 resolution: "gauge@npm:3.0.2" @@ -21278,9 +21681,9 @@ __metadata: linkType: hard "github-slugger@npm:^1.0.0": - version: 1.5.0 - resolution: "github-slugger@npm:1.5.0" - checksum: c70988224578b3bdaa25df65973ffc8c24594a77a28550c3636e495e49d17aef5cdb04c04fa3f1744babef98c61eecc6a43299a13ea7f3cc33d680bf9053ffbe + version: 1.4.0 + resolution: "github-slugger@npm:1.4.0" + checksum: 4f52e7a21f5c6a4c5328f01fe4fe13ae8881fea78bfe31f9e72c4038f97e3e70d52fb85aa7633a52c501dc2486874474d9abd22aa61cbe9b113099a495551c6b languageName: node linkType: hard @@ -21615,7 +22018,6 @@ __metadata: "@grafana/toolkit": "workspace:*" "@grafana/tsconfig": ^1.2.0-rc1 "@grafana/ui": "workspace:*" - "@jaegertracing/jaeger-ui-components": "workspace:*" "@kusto/monaco-kusto": 5.3.6 "@leeoniya/ufuzzy": 0.9.1 "@lezer/common": 1.0.1 @@ -21730,8 +22132,10 @@ __metadata: browserslist: ^4.21.4 calculate-size: 1.1.1 centrifuge: 3.1.0 + chance: ^1.0.10 classnames: 2.3.2 codeowners: ^5.1.1 + combokeys: ^3.0.0 comlink: 4.3.1 common-tags: 1.8.2 copy-webpack-plugin: 9.0.1 @@ -21788,6 +22192,7 @@ __metadata: jest-matcher-utils: 29.3.1 jquery: 3.6.1 js-yaml: ^4.1.0 + json-markup: ^1.1.0 json-source-map: 0.6.1 jsurl: ^0.1.5 kbar: 0.1.0-beta.36 @@ -21796,6 +22201,7 @@ __metadata: lodash: 4.17.21 logfmt: ^1.3.2 lru-cache: 7.14.0 + lru-memoize: ^1.1.0 memoize-one: 6.0.0 mini-css-extract-plugin: 2.7.2 moment: 2.29.4 @@ -21889,6 +22295,7 @@ __metadata: ts-loader: 9.3.1 ts-node: 10.9.1 tslib: 2.4.1 + tween-functions: ^1.2.0 typescript: 4.8.4 uplot: 1.6.24 uuid: 9.0.0 @@ -22422,48 +22829,48 @@ __metadata: linkType: hard "html-loader@npm:^3.1.0": - version: 3.1.2 - resolution: "html-loader@npm:3.1.2" + version: 3.1.0 + resolution: "html-loader@npm:3.1.0" dependencies: html-minifier-terser: ^6.0.2 parse5: ^6.0.1 peerDependencies: webpack: ^5.0.0 - checksum: 75d665f118315056f24e248a6f0b6f6a3dbaec34593b9216af507d36eb24ca54cb8d80667a87ffc6a02b6b51c62423d59b4f64d827a745ac2e199d0c2b7c5c19 + checksum: 4c383d103c10465964924f31eeb2876df941df6515b52a7be517eb01d59ff8f5ae344a3ca428469029c999c73f5ae5dac3431701886ca16617a670a0991bd3a2 languageName: node linkType: hard "html-minifier-terser@npm:^6.0.2": - version: 6.1.0 - resolution: "html-minifier-terser@npm:6.1.0" + version: 6.0.2 + resolution: "html-minifier-terser@npm:6.0.2" dependencies: camel-case: ^4.1.2 - clean-css: ^5.2.2 - commander: ^8.3.0 + clean-css: ^5.1.5 + commander: ^8.1.0 he: ^1.2.0 param-case: ^3.0.4 relateurl: ^0.2.7 - terser: ^5.10.0 + terser: ^5.7.2 bin: html-minifier-terser: cli.js - checksum: ac52c14006476f773204c198b64838477859dc2879490040efab8979c0207424da55d59df7348153f412efa45a0840a1ca3c757bf14767d23a15e3e389d37a93 + checksum: 9c8775ea036f7b04fd5a16607cf4242efdddc64884e84fcc81e27ef56505a12b8a9e1f9ac865ca00a77a3e4c21ef4ffb194dcc6492cdf6cfdfc73bf8de6d7c2d languageName: node linkType: hard "html-minifier-terser@npm:^7.0.0": - version: 7.1.0 - resolution: "html-minifier-terser@npm:7.1.0" + version: 7.0.0 + resolution: "html-minifier-terser@npm:7.0.0" dependencies: camel-case: ^4.1.2 clean-css: 5.2.0 - commander: ^9.4.1 - entities: ^4.4.0 + commander: ^9.4.0 + entities: ^4.3.1 param-case: ^3.0.4 relateurl: ^0.2.7 - terser: ^5.15.1 + terser: ^5.14.2 bin: html-minifier-terser: cli.js - checksum: 351de28d85f142314a6a9b5222bdcaf068cef6bf2f521952ef55d99a6acdcecd0b4dbc42578da2d438d579c6e868b899ca19eac901ee6f9f0c69c223b5942099 + checksum: eabd3b4835d9663bf7c30ea0f1c57870b0d8f7dc27b9fa17852ea57bfdd5fdc0ed9b7b83a7f13edd8626ccf98d3082e347764754278a661cab291469fd37dced languageName: node linkType: hard @@ -25337,7 +25744,7 @@ __metadata: languageName: node linkType: hard -"jest-worker@npm:^27.0.2, jest-worker@npm:^27.4.5, jest-worker@npm:^27.5.1": +"jest-worker@npm:^27.0.2, jest-worker@npm:^27.5.1": version: 27.5.1 resolution: "jest-worker@npm:27.5.1" dependencies: @@ -25348,6 +25755,28 @@ __metadata: languageName: node linkType: hard +"jest-worker@npm:^27.0.6": + version: 27.3.1 + resolution: "jest-worker@npm:27.3.1" + dependencies: + "@types/node": "*" + merge-stream: ^2.0.0 + supports-color: ^8.0.0 + checksum: 125d46939d894ef8cf1ffbbf6c63cee10f28218698db3949704d5f613a353f56502da50d3425ec722927c7948c5742d0306f63ad5064a432574b8b217b9ceeba + languageName: node + linkType: hard + +"jest-worker@npm:^27.4.5": + version: 27.5.0 + resolution: "jest-worker@npm:27.5.0" + dependencies: + "@types/node": "*" + merge-stream: ^2.0.0 + supports-color: ^8.0.0 + checksum: bfd41bef36d3c217819278d8e53b7b9e02c32d90f54149ab4ec87595e389f5caca84237cc4c84050c93a435d458150876ce1812d68cd50a5a4cbb7d80286212f + languageName: node + linkType: hard + "jest-worker@npm:^28.0.2": version: 28.1.3 resolution: "jest-worker@npm:28.1.3" @@ -25637,7 +26066,7 @@ __metadata: languageName: node linkType: hard -"json-parse-better-errors@npm:^1.0.1": +"json-parse-better-errors@npm:^1.0.1, json-parse-better-errors@npm:^1.0.2": version: 1.0.2 resolution: "json-parse-better-errors@npm:1.0.2" checksum: ff2b5ba2a70e88fd97a3cb28c1840144c5ce8fae9cbeeddba15afa333a5c407cf0e42300cd0a2885dbb055227fe68d405070faad941beeffbfde9cf3b2c78c5d @@ -25729,16 +26158,7 @@ __metadata: languageName: node linkType: hard -"json5@npm:^2.1.3": - version: 2.2.2 - resolution: "json5@npm:2.2.2" - bin: - json5: lib/cli.js - checksum: 9a878d66b72157b073cf0017f3e5d93ec209fa5943abcb38d37a54b208917c166bd473c26a24695e67a016ce65759aeb89946592991f8f9174fb96c8e2492683 - languageName: node - linkType: hard - -"json5@npm:^2.2.0, json5@npm:^2.2.1": +"json5@npm:^2.1.3, json5@npm:^2.2.0, json5@npm:^2.2.1": version: 2.2.1 resolution: "json5@npm:2.2.1" bin: @@ -26315,9 +26735,9 @@ __metadata: linkType: hard "loader-runner@npm:^4.2.0": - version: 4.3.0 - resolution: "loader-runner@npm:4.3.0" - checksum: a90e00dee9a16be118ea43fec3192d0b491fe03a32ed48a4132eb61d498f5536a03a1315531c19d284392a8726a4ecad71d82044c28d7f22ef62e029bf761569 + version: 4.2.0 + resolution: "loader-runner@npm:4.2.0" + checksum: e61aea8b6904b8af53d9de6f0484da86c462c0001f4511bedc837cec63deb9475cea813db62f702cd7930420ccb0e75c78112270ca5c8b61b374294f53c0cb3a languageName: node linkType: hard @@ -27025,11 +27445,11 @@ __metadata: linkType: hard "memfs@npm:^3.2.2": - version: 3.4.12 - resolution: "memfs@npm:3.4.12" + version: 3.4.7 + resolution: "memfs@npm:3.4.7" dependencies: fs-monkey: ^1.0.3 - checksum: dab8dec1ae0b2a92e4d563ac86846047cd7aeb17cde4ad51da85cff6e580c32d12b886354527788e36eb75f733dd8edbaf174476b7cea73fed9c5a0e45a6b428 + checksum: fab88266dc576dc4999e38bdf531d703fb798affac2e0dd3fc17470878486844027b2766008ba80c0103b443f52cf9068a5c00f4e1ecf04106f4b29c11855822 languageName: node linkType: hard @@ -27587,7 +28007,7 @@ __metadata: languageName: node linkType: hard -"mime-types@npm:^2.1.12, mime-types@npm:~2.1.19, mime-types@npm:~2.1.24": +"mime-types@npm:^2.1.12, mime-types@npm:^2.1.27, mime-types@npm:~2.1.19, mime-types@npm:~2.1.24": version: 2.1.33 resolution: "mime-types@npm:2.1.33" dependencies: @@ -27596,7 +28016,7 @@ __metadata: languageName: node linkType: hard -"mime-types@npm:^2.1.27, mime-types@npm:^2.1.30, mime-types@npm:^2.1.31, mime-types@npm:~2.1.17, mime-types@npm:~2.1.34": +"mime-types@npm:^2.1.30, mime-types@npm:^2.1.31, mime-types@npm:~2.1.17, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" dependencies: @@ -28934,18 +29354,7 @@ __metadata: languageName: node linkType: hard -"object.entries@npm:^1.1.0, object.entries@npm:^1.1.6": - version: 1.1.6 - resolution: "object.entries@npm:1.1.6" - dependencies: - call-bind: ^1.0.2 - define-properties: ^1.1.4 - es-abstract: ^1.20.4 - checksum: 0f8c47517e6a9a980241eafe3b73de11e59511883173c2b93d67424a008e47e11b77c80e431ad1d8a806f6108b225a1cab9223e53e555776c612a24297117d28 - languageName: node - linkType: hard - -"object.entries@npm:^1.1.5": +"object.entries@npm:^1.1.0, object.entries@npm:^1.1.5": version: 1.1.5 resolution: "object.entries@npm:1.1.5" dependencies: @@ -28956,6 +29365,17 @@ __metadata: languageName: node linkType: hard +"object.entries@npm:^1.1.6": + version: 1.1.6 + resolution: "object.entries@npm:1.1.6" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.1.4 + es-abstract: ^1.20.4 + checksum: 0f8c47517e6a9a980241eafe3b73de11e59511883173c2b93d67424a008e47e11b77c80e431ad1d8a806f6108b225a1cab9223e53e555776c612a24297117d28 + languageName: node + linkType: hard + "object.fromentries@npm:^2.0.0 || ^1.0.0, object.fromentries@npm:^2.0.5": version: 2.0.5 resolution: "object.fromentries@npm:2.0.5" @@ -28979,14 +29399,14 @@ __metadata: linkType: hard "object.getownpropertydescriptors@npm:^2.0.3, object.getownpropertydescriptors@npm:^2.1.2": - version: 2.1.5 - resolution: "object.getownpropertydescriptors@npm:2.1.5" + version: 2.1.4 + resolution: "object.getownpropertydescriptors@npm:2.1.4" dependencies: - array.prototype.reduce: ^1.0.5 + array.prototype.reduce: ^1.0.4 call-bind: ^1.0.2 define-properties: ^1.1.4 - es-abstract: ^1.20.4 - checksum: 7883e1aac1f9cd4cd85e2bb8c7aab6a60940a7cfe07b788356f301844d4967482fc81058e7bda24e1b3909cbb4879387ea9407329b78704f8937bc0b97dec58b + es-abstract: ^1.20.1 + checksum: 988c466fe49fc4f19a28d2d1d894c95c6abfe33c94674ec0b14d96eed71f453c7ad16873d430dc2acbb1760de6d3d2affac4b81237a306012cc4dc49f7539e7f languageName: node linkType: hard @@ -29019,18 +29439,7 @@ __metadata: languageName: node linkType: hard -"object.values@npm:^1.1.0": - version: 1.1.6 - resolution: "object.values@npm:1.1.6" - dependencies: - call-bind: ^1.0.2 - define-properties: ^1.1.4 - es-abstract: ^1.20.4 - checksum: f6fff9fd817c24cfd8107f50fb33061d81cd11bacc4e3dbb3852e9ff7692fde4dbce823d4333ea27cd9637ef1b6690df5fbb61f1ed314fa2959598dc3ae23d8e - languageName: node - linkType: hard - -"object.values@npm:^1.1.5": +"object.values@npm:^1.1.0, object.values@npm:^1.1.5": version: 1.1.5 resolution: "object.values@npm:1.1.5" dependencies: @@ -30939,7 +31348,7 @@ __metadata: languageName: node linkType: hard -"postcss@npm:8.4.18, postcss@npm:^8.4.17": +"postcss@npm:8.4.18, postcss@npm:^8.2.15, postcss@npm:^8.4.17": version: 8.4.18 resolution: "postcss@npm:8.4.18" dependencies: @@ -30960,17 +31369,6 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.2.15": - version: 8.4.20 - resolution: "postcss@npm:8.4.20" - dependencies: - nanoid: ^3.3.4 - picocolors: ^1.0.0 - source-map-js: ^1.0.2 - checksum: 1a5609ea1c1b204f9c2974a0019ae9eef2d99bf645c2c9aac675166c4cb1005be7b5e2ba196160bc771f5d9ac896ed883f236f888c891e835e59d28fff6651aa - languageName: node - linkType: hard - "postcss@npm:^8.3.11, postcss@npm:^8.3.5": version: 8.3.11 resolution: "postcss@npm:8.3.11" @@ -31327,27 +31725,27 @@ __metadata: linkType: hard "promise.allsettled@npm:^1.0.0": - version: 1.0.6 - resolution: "promise.allsettled@npm:1.0.6" + version: 1.0.5 + resolution: "promise.allsettled@npm:1.0.5" dependencies: - array.prototype.map: ^1.0.5 + array.prototype.map: ^1.0.4 call-bind: ^1.0.2 - define-properties: ^1.1.4 - es-abstract: ^1.20.4 - get-intrinsic: ^1.1.3 + define-properties: ^1.1.3 + es-abstract: ^1.19.1 + get-intrinsic: ^1.1.1 iterate-value: ^1.0.2 - checksum: 5de80c33f41b23387be49229e47ade2fbeb86ad9b2066e5e093c21dbd5a3e7a8e4eb8e420cbf58386e2af976cc4677950092f855b677b16771191599f493d035 + checksum: 92775552d3a3487ed924852e5de00a217a202cefc833e8cc169283fe4f7dbe09953505b0c7471b2681e09aa7d064bdbd07b978d44ff536f712e4dcd7c9faba35 languageName: node linkType: hard "promise.prototype.finally@npm:^3.1.0": - version: 3.1.4 - resolution: "promise.prototype.finally@npm:3.1.4" + version: 3.1.3 + resolution: "promise.prototype.finally@npm:3.1.3" dependencies: call-bind: ^1.0.2 - define-properties: ^1.1.4 - es-abstract: ^1.20.4 - checksum: 116556f16e5af74a1be0faf0b76e05fc6592bf74e66c6babbba7094f89887b771691f13236d2ffcf0f8d28ee1048808ccee8f70754c4cb5b3736314fbfadc32b + define-properties: ^1.1.3 + es-abstract: ^1.19.1 + checksum: aba8af6ae8d076e2c344d2674409b44c8f98b3aba98b78619739aeb4a74ebac80dbba5f9338da7cf0108a34384799d3996c46697d2e21c6e998c04d68041213c languageName: node linkType: hard @@ -31580,18 +31978,18 @@ __metadata: linkType: hard "qs@npm:^6.4.0": - version: 6.10.3 - resolution: "qs@npm:6.10.3" + version: 6.10.1 + resolution: "qs@npm:6.10.1" dependencies: side-channel: ^1.0.4 - checksum: 0fac5e6c7191d0295a96d0e83c851aeb015df7e990e4d3b093897d3ac6c94e555dbd0a599739c84d7fa46d7fee282d94ba76943983935cf33bba6769539b8019 + checksum: 00e390dbf98eff4d8ff121b61ab2fe32106852290de99ecd0e40fc76651c4101f43fc6cc8313cb69423563876fc532951b11dda55d2917def05f292258263480 languageName: node linkType: hard "qs@npm:~6.5.2": - version: 6.5.3 - resolution: "qs@npm:6.5.3" - checksum: 6f20bf08cabd90c458e50855559539a28d00b2f2e7dddcb66082b16a43188418cb3cb77cbd09268bcef6022935650f0534357b8af9eeb29bf0f27ccb17655692 + version: 6.5.2 + resolution: "qs@npm:6.5.2" + checksum: 24af7b9928ba2141233fba2912876ff100403dba1b08b20c3b490da9ea6c636760445ea2211a079e7dfa882a5cf8f738337b3748c8bdd0f93358fa8881d2db8f languageName: node linkType: hard @@ -32466,29 +32864,6 @@ __metadata: languageName: node linkType: hard -"react-icon-base@npm:2.1.0": - version: 2.1.0 - resolution: "react-icon-base@npm:2.1.0" - peerDependencies: - prop-types: "*" - react: "*" - react-dom: "*" - checksum: 62b6bfe48649974d3ce2d9d1d1ebadc3523ae85181a2ecde858667c216abe1b3e95b68a0bf800335c6b9aa7804081d0fcbdb6647be04c00e1e5f537846788b00 - languageName: node - linkType: hard - -"react-icons@npm:2.2.7": - version: 2.2.7 - resolution: "react-icons@npm:2.2.7" - dependencies: - react-icon-base: 2.1.0 - peerDependencies: - react: ^0.14.0 || ^15.0.0 || ^16.0.0 - react-dom: ^0.14.0 || ^15.0.0 || ^16.0.0 - checksum: 74e692fdd3d3e2be7cc7a549e7d9ac00eadf21d0b6825681d55c497d5c8683d39bcb56633037c428672140c8f024f062ee34b6f51cab74467ac4d5ce0e4fd98b - languageName: node - linkType: hard - "react-immutable-proptypes@npm:^2.1.0": version: 2.2.0 resolution: "react-immutable-proptypes@npm:2.2.0" @@ -35208,7 +35583,7 @@ __metadata: languageName: node linkType: hard -"source-map-support@npm:^0.5.16, source-map-support@npm:~0.5.20": +"source-map-support@npm:^0.5.16": version: 0.5.21 resolution: "source-map-support@npm:0.5.21" dependencies: @@ -35218,7 +35593,7 @@ __metadata: languageName: node linkType: hard -"source-map-support@npm:^0.5.17, source-map-support@npm:^0.5.6": +"source-map-support@npm:^0.5.17, source-map-support@npm:^0.5.6, source-map-support@npm:~0.5.20": version: 0.5.20 resolution: "source-map-support@npm:0.5.20" dependencies: @@ -35256,7 +35631,7 @@ __metadata: languageName: node linkType: hard -"source-map@npm:^0.7.0": +"source-map@npm:^0.7.0, source-map@npm:~0.7.2": version: 0.7.4 resolution: "source-map@npm:0.7.4" checksum: 01cc5a74b1f0e1d626a58d36ad6898ea820567e87f18dfc9d24a9843a351aaa2ec09b87422589906d6ff1deed29693e176194dc88bcae7c9a852dc74b311dbf5 @@ -35714,19 +36089,19 @@ __metadata: languageName: node linkType: hard -"string.prototype.matchall@npm:^4.0.0 || ^3.0.1": - version: 4.0.8 - resolution: "string.prototype.matchall@npm:4.0.8" +"string.prototype.matchall@npm:^4.0.0 || ^3.0.1, string.prototype.matchall@npm:^4.0.7": + version: 4.0.7 + resolution: "string.prototype.matchall@npm:4.0.7" dependencies: call-bind: ^1.0.2 - define-properties: ^1.1.4 - es-abstract: ^1.20.4 - get-intrinsic: ^1.1.3 + define-properties: ^1.1.3 + es-abstract: ^1.19.1 + get-intrinsic: ^1.1.1 has-symbols: ^1.0.3 internal-slot: ^1.0.3 - regexp.prototype.flags: ^1.4.3 + regexp.prototype.flags: ^1.4.1 side-channel: ^1.0.4 - checksum: 952da3a818de42ad1c10b576140a5e05b4de7b34b8d9dbf00c3ac8c1293e9c0f533613a39c5cda53e0a8221f2e710bc2150e730b1c2278d60004a8a35726efb6 + checksum: fc09f3ccbfb325de0472bcc87a6be0598a7499e0b4a31db5789676155b15754a4cc4bb83924f15fc9ed48934dac7366ee52c8b9bd160bed6fd072c93b489e75c languageName: node linkType: hard @@ -35746,41 +36121,25 @@ __metadata: languageName: node linkType: hard -"string.prototype.matchall@npm:^4.0.7": - version: 4.0.7 - resolution: "string.prototype.matchall@npm:4.0.7" +"string.prototype.padend@npm:^3.0.0": + version: 3.1.3 + resolution: "string.prototype.padend@npm:3.1.3" dependencies: call-bind: ^1.0.2 define-properties: ^1.1.3 es-abstract: ^1.19.1 - get-intrinsic: ^1.1.1 - has-symbols: ^1.0.3 - internal-slot: ^1.0.3 - regexp.prototype.flags: ^1.4.1 - side-channel: ^1.0.4 - checksum: fc09f3ccbfb325de0472bcc87a6be0598a7499e0b4a31db5789676155b15754a4cc4bb83924f15fc9ed48934dac7366ee52c8b9bd160bed6fd072c93b489e75c - languageName: node - linkType: hard - -"string.prototype.padend@npm:^3.0.0": - version: 3.1.4 - resolution: "string.prototype.padend@npm:3.1.4" - dependencies: - call-bind: ^1.0.2 - define-properties: ^1.1.4 - es-abstract: ^1.20.4 - checksum: 76e07238fe31dc12177428f0436b7ed6985f6a7ba97470fd53e4f0a6d9860bfee127d81957f3073cc879b434233df143825d140581e1340278053ad993c92f6c + checksum: ef9ee0542c17975629bc6d21497e8faaa142d873e9f07fb65de2a955df402a1eac45cbed375045a759501e9d4ef80e589e11f0e12103c20df0770e47f6b59bc7 languageName: node linkType: hard "string.prototype.padstart@npm:^3.0.0": - version: 3.1.4 - resolution: "string.prototype.padstart@npm:3.1.4" + version: 3.1.3 + resolution: "string.prototype.padstart@npm:3.1.3" dependencies: call-bind: ^1.0.2 - define-properties: ^1.1.4 - es-abstract: ^1.20.4 - checksum: a8517d83fd4fc5832b85cd9621188156094392494983fa41f6e6e727ab6af20f6bf8b2aac43b97ffad94e21fa52f1bb21342e2f87b79965707fe174cff5b8b2b + define-properties: ^1.1.3 + es-abstract: ^1.19.1 + checksum: 8bf8bc1d25edc79c4db285aa8dfd5d269dac4024631e8ae13202c2126348a07e00b153d6bf7b858c5bd716e44675a7fbb50baedd3e8970e1034bb86be22c9475 languageName: node linkType: hard @@ -36416,7 +36775,7 @@ __metadata: languageName: node linkType: hard -"terser-webpack-plugin@npm:5.3.6, terser-webpack-plugin@npm:^5.0.3, terser-webpack-plugin@npm:^5.1.3, terser-webpack-plugin@npm:^5.3.1": +"terser-webpack-plugin@npm:5.3.6, terser-webpack-plugin@npm:^5.0.3": version: 5.3.6 resolution: "terser-webpack-plugin@npm:5.3.6" dependencies: @@ -36457,9 +36816,67 @@ __metadata: languageName: node linkType: hard -"terser@npm:^5.0.0, terser@npm:^5.10.0, terser@npm:^5.14.1, terser@npm:^5.15.1, terser@npm:^5.3.4": - version: 5.16.1 - resolution: "terser@npm:5.16.1" +"terser-webpack-plugin@npm:^5.1.3": + version: 5.2.4 + resolution: "terser-webpack-plugin@npm:5.2.4" + dependencies: + jest-worker: ^27.0.6 + p-limit: ^3.1.0 + schema-utils: ^3.1.1 + serialize-javascript: ^6.0.0 + source-map: ^0.6.1 + terser: ^5.7.2 + peerDependencies: + webpack: ^5.1.0 + peerDependenciesMeta: + "@swc/core": + optional: true + esbuild: + optional: true + uglify-js: + optional: true + checksum: ddbcdd28f9620ecacc9b50ff31776485ad012c7f1cbef53825e4fc334a78d82e2344346e5595751916494951bc64717004c07b03ad88deeb3df4a5f76c559cc9 + languageName: node + linkType: hard + +"terser-webpack-plugin@npm:^5.3.1": + version: 5.3.1 + resolution: "terser-webpack-plugin@npm:5.3.1" + dependencies: + jest-worker: ^27.4.5 + schema-utils: ^3.1.1 + serialize-javascript: ^6.0.0 + source-map: ^0.6.1 + terser: ^5.7.2 + peerDependencies: + webpack: ^5.1.0 + peerDependenciesMeta: + "@swc/core": + optional: true + esbuild: + optional: true + uglify-js: + optional: true + checksum: 1b808fd4f58ce0b532baacc50b9a850fc69ce0077a0e9e5076d4156c52fab3d40b02d5d9148a3eba64630cf7f40057de54f6a5a87fac1849b1f11d6bfdb42072 + languageName: node + linkType: hard + +"terser@npm:^5.0.0, terser@npm:^5.7.2": + version: 5.9.0 + resolution: "terser@npm:5.9.0" + dependencies: + commander: ^2.20.0 + source-map: ~0.7.2 + source-map-support: ~0.5.20 + bin: + terser: bin/terser + checksum: 11c1246b1991015a8881742878af779e3863fad42f626ffda957dbf28c94bf51e7994cffb9ffbec86ff3c23ab45ffa6d79d453c15e664306e35fc7b2c4eee5f4 + languageName: node + linkType: hard + +"terser@npm:^5.14.1, terser@npm:^5.3.4": + version: 5.15.1 + resolution: "terser@npm:5.15.1" dependencies: "@jridgewell/source-map": ^0.3.2 acorn: ^8.5.0 @@ -36467,7 +36884,21 @@ __metadata: source-map-support: ~0.5.20 bin: terser: bin/terser - checksum: cb524123504a2f0d9140c1e1a8628c83bba9cacc404c6aca79e2493a38dfdf21275617ba75b91006b3f1ff586e401ab31121160cd253699f334c6340ea2756f5 + checksum: 9880a1e0956983a1ce5de204ea35121c0009fa41d582a6904ae850e1953a1a2cc021168439565280c5a8eee67c85a874175627e24989b046c7a72589b81c3979 + languageName: node + linkType: hard + +"terser@npm:^5.14.2": + version: 5.15.0 + resolution: "terser@npm:5.15.0" + dependencies: + "@jridgewell/source-map": ^0.3.2 + acorn: ^8.5.0 + commander: ^2.20.0 + source-map-support: ~0.5.20 + bin: + terser: bin/terser + checksum: b2358c989fcb76b4a1c265f60e175c950d3f776e5f619a9f58f54e8d2d792cd6b4cca86071834075f3b9943556d695357bafdd4ee2390de2fc9fd96ba3efa8c8 languageName: node linkType: hard @@ -37154,14 +37585,14 @@ __metadata: languageName: node linkType: hard -"tslib@npm:2.4.0, tslib@npm:^2.4.0": +"tslib@npm:2.4.0, tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.4.0": version: 2.4.0 resolution: "tslib@npm:2.4.0" checksum: 8c4aa6a3c5a754bf76aefc38026134180c053b7bd2f81338cb5e5ebf96fefa0f417bff221592bf801077f5bf990562f6264fecbc42cd3309b33872cb6fc3b113 languageName: node linkType: hard -"tslib@npm:2.4.1, tslib@npm:^2.0.0, tslib@npm:^2.0.1": +"tslib@npm:2.4.1": version: 2.4.1 resolution: "tslib@npm:2.4.1" checksum: 19480d6e0313292bd6505d4efe096a6b31c70e21cf08b5febf4da62e95c265c8f571f7b36fcc3d1a17e068032f59c269fab3459d6cd3ed6949eafecf64315fca @@ -38437,6 +38868,16 @@ __metadata: languageName: node linkType: hard +"watchpack@npm:^2.3.1": + version: 2.3.1 + resolution: "watchpack@npm:2.3.1" + dependencies: + glob-to-regexp: ^0.4.1 + graceful-fs: ^4.1.2 + checksum: 70a34f92842d94b5d842980f866d568d7a467de667c96ae5759c759f46587e49265863171f4650bdbafc5f3870a28f2b4453e9e847098ec4b718b38926d47d22 + languageName: node + linkType: hard + "wbuf@npm:^1.1.0, wbuf@npm:^1.7.3": version: 1.7.3 resolution: "wbuf@npm:1.7.3" @@ -38669,13 +39110,13 @@ __metadata: linkType: hard "webpack-hot-middleware@npm:^2.25.1": - version: 2.25.3 - resolution: "webpack-hot-middleware@npm:2.25.3" + version: 2.25.2 + resolution: "webpack-hot-middleware@npm:2.25.2" dependencies: ansi-html-community: 0.0.8 html-entities: ^2.1.0 strip-ansi: ^6.0.0 - checksum: 74fe5d15f3120742cf0f88a4af7e72f3678f2d05905676e37ab4e85c559f2c21d8aa72b0efe7c262993370bfc83fbe5a8d42561bcd10b370fac88640f87c463a + checksum: 9bbcb4a3109d5efc3fedc41ab84209745e47770a205897324adff9126196d9cd086237288161d71cd7273a0154e09046d025a3c30c6938bd04e58d3b379fdcca languageName: node linkType: hard @@ -38747,21 +39188,14 @@ __metadata: languageName: node linkType: hard -"webpack-virtual-modules@npm:^0.4.1": - version: 0.4.6 - resolution: "webpack-virtual-modules@npm:0.4.6" - checksum: cb056ba8c50b35436ae43149554b051b80065b0cf79f2d528ca692ddf344a422ac71c415adb9da83dc3acc6e7e58f518388cc1cd11cb4fa29dc04f2c4494afe3 - languageName: node - linkType: hard - -"webpack-virtual-modules@npm:^0.4.4": +"webpack-virtual-modules@npm:^0.4.1, webpack-virtual-modules@npm:^0.4.4": version: 0.4.5 resolution: "webpack-virtual-modules@npm:0.4.5" checksum: 0ae9a8b50d0cb1e43da5ff8acaa7b99c34a42f0d6cc83a82908fb6e131e574a949d19948df4fdd3de0dbfdbadb2b93ceb4a740c55727a4236eb3b2bbc8f785a6 languageName: node linkType: hard -"webpack@npm:5.74.0": +"webpack@npm:5.74.0, webpack@npm:>=4.43.0 <6.0.0": version: 5.74.0 resolution: "webpack@npm:5.74.0" dependencies: @@ -38798,40 +39232,40 @@ __metadata: languageName: node linkType: hard -"webpack@npm:>=4.43.0 <6.0.0, webpack@npm:^5.72.0": - version: 5.75.0 - resolution: "webpack@npm:5.75.0" +"webpack@npm:^5.72.0": + version: 5.72.0 + resolution: "webpack@npm:5.72.0" dependencies: "@types/eslint-scope": ^3.7.3 "@types/estree": ^0.0.51 "@webassemblyjs/ast": 1.11.1 "@webassemblyjs/wasm-edit": 1.11.1 "@webassemblyjs/wasm-parser": 1.11.1 - acorn: ^8.7.1 + acorn: ^8.4.1 acorn-import-assertions: ^1.7.6 browserslist: ^4.14.5 chrome-trace-event: ^1.0.2 - enhanced-resolve: ^5.10.0 + enhanced-resolve: ^5.9.2 es-module-lexer: ^0.9.0 eslint-scope: 5.1.1 events: ^3.2.0 glob-to-regexp: ^0.4.1 graceful-fs: ^4.2.9 - json-parse-even-better-errors: ^2.3.1 + json-parse-better-errors: ^1.0.2 loader-runner: ^4.2.0 mime-types: ^2.1.27 neo-async: ^2.6.2 schema-utils: ^3.1.0 tapable: ^2.1.1 terser-webpack-plugin: ^5.1.3 - watchpack: ^2.4.0 + watchpack: ^2.3.1 webpack-sources: ^3.2.3 peerDependenciesMeta: webpack-cli: optional: true bin: webpack: bin/webpack.js - checksum: 2bcc5f3c195f375944e8af2f00bf2feea39cb9fda5f763b0d1b00077f1c51783db25c94d3fae96a07dead9fa085e6ae7474417e5ab31719c9776ea5969ceb83a + checksum: 8365f1466d0f7adbf80ebc9b780f263a28eeeabcd5fb515249bfd9a56ab7fe8d29ea53df3d9364d0732ab39ae774445eb28abce694ed375b13882a6b2fe93ffc languageName: node linkType: hard @@ -39194,9 +39628,9 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.2.3, ws@npm:^8.9.0": - version: 8.11.0 - resolution: "ws@npm:8.11.0" +"ws@npm:^8.2.3": + version: 8.7.0 + resolution: "ws@npm:8.7.0" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ^5.0.2 @@ -39205,7 +39639,7 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 316b33aba32f317cd217df66dbfc5b281a2f09ff36815de222bc859e3424d83766d9eb2bd4d667de658b6ab7be151f258318fb1da812416b30be13103e5b5c67 + checksum: 078fa2dbc06b31a45e0057b19e2930d26c222622e355955afe019c9b9b25f62eb2a8eff7cceabdad04910ecd2bd6ef4fa48e6f3673f2fdddff02a6e4c2459584 languageName: node linkType: hard @@ -39224,6 +39658,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:^8.9.0": + version: 8.11.0 + resolution: "ws@npm:8.11.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 316b33aba32f317cd217df66dbfc5b281a2f09ff36815de222bc859e3424d83766d9eb2bd4d667de658b6ab7be151f258318fb1da812416b30be13103e5b5c67 + languageName: node + linkType: hard + "x-default-browser@npm:^0.4.0": version: 0.4.0 resolution: "x-default-browser@npm:0.4.0" @@ -39357,6 +39806,13 @@ __metadata: languageName: node linkType: hard +"yargs-parser@npm:^21.1.1": + version: 21.1.1 + resolution: "yargs-parser@npm:21.1.1" + checksum: ed2d96a616a9e3e1cc7d204c62ecc61f7aaab633dcbfab2c6df50f7f87b393993fe6640d017759fe112d0cb1e0119f2b4150a87305cc873fd90831c6a58ccf1c + languageName: node + linkType: hard + "yargs-unparser@npm:2.0.0": version: 2.0.0 resolution: "yargs-unparser@npm:2.0.0" @@ -39384,7 +39840,7 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^17.3.1, yargs@npm:^17.4.0, yargs@npm:^17.5.1": +"yargs@npm:^17.3.1, yargs@npm:^17.4.0": version: 17.5.1 resolution: "yargs@npm:17.5.1" dependencies: @@ -39399,6 +39855,21 @@ __metadata: languageName: node linkType: hard +"yargs@npm:^17.5.1": + version: 17.6.2 + resolution: "yargs@npm:17.6.2" + dependencies: + cliui: ^8.0.1 + escalade: ^3.1.1 + get-caller-file: ^2.0.5 + require-directory: ^2.1.1 + string-width: ^4.2.3 + y18n: ^5.0.5 + yargs-parser: ^21.1.1 + checksum: 47da1b0d854fa16d45a3ded57b716b013b2179022352a5f7467409da5a04a1eef5b3b3d97a2dfc13e8bbe5f2ffc0afe3bc6a4a72f8254e60f5a4bd7947138643 + languageName: node + linkType: hard + "yauzl@npm:^2.10.0": version: 2.10.0 resolution: "yauzl@npm:2.10.0" From 89ef62f163e2a821f843fb6cbb35fa0a0ecf082c Mon Sep 17 00:00:00 2001 From: Sriram <153843+yesoreyeram@users.noreply.github.com> Date: Fri, 27 Jan 2023 14:19:42 +0000 Subject: [PATCH 042/117] Chore: Removed unused grafana-plugin-ci images (#62219) removed unused grafana-plugin-ci images --- .../grafana-plugin-ci-alpine/Dockerfile | 10 -- .../docker/grafana-plugin-ci-alpine/README.md | 71 -------- .../docker/grafana-plugin-ci-alpine/build.sh | 22 --- .../docker/grafana-plugin-ci-alpine/common.sh | 8 - .../grafana-plugin-ci-alpine/install/bin/cp | 7 - .../install/bin/ginstall | 73 --------- .../install/bin/githubRelease.js | 154 ------------------ .../scripts/deploy-common.sh | 52 ------ .../scripts/deploy-user.sh | 3 - .../scripts/deploy.sh | 80 --------- .../test/docker-compose.yml | 18 -- .../grafana-plugin-ci-alpine/test/start.sh | 14 -- .../docker/grafana-plugin-ci-e2e/Dockerfile | 9 - .../docker/grafana-plugin-ci-e2e/README.md | 67 -------- .../docker/grafana-plugin-ci-e2e/build.sh | 24 --- .../docker/grafana-plugin-ci-e2e/common.sh | 10 -- .../grafana-plugin-ci-e2e/install/ginstall | 73 --------- .../scripts/deploy-common.sh | 37 ----- .../scripts/deploy-slim.sh | 27 --- .../scripts/deploy-user.sh | 3 - .../grafana-plugin-ci-e2e/scripts/deploy.sh | 65 -------- .../test/docker-compose.yml | 18 -- .../grafana-plugin-ci-e2e/test/start.sh | 14 -- .../docker/grafana-plugin-ci/Dockerfile | 8 - .../docker/grafana-plugin-ci/README.md | 71 -------- .../docker/grafana-plugin-ci/build.sh | 8 - .../docker/grafana-plugin-ci/common.sh | 8 - .../docker/grafana-plugin-ci/install/gget | 63 ------- .../scripts/deploy-common.sh | 38 ----- .../grafana-plugin-ci/scripts/deploy-user.sh | 3 - .../grafana-plugin-ci/scripts/deploy.sh | 57 ------- .../grafana-plugin-ci/test/docker-compose.yml | 10 -- .../docker/grafana-plugin-ci/test/start.sh | 10 -- 33 files changed, 1135 deletions(-) delete mode 100644 packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/Dockerfile delete mode 100644 packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/README.md delete mode 100755 packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/build.sh delete mode 100755 packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/common.sh delete mode 100755 packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/install/bin/cp delete mode 100755 packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/install/bin/ginstall delete mode 100644 packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/install/bin/githubRelease.js delete mode 100755 packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/scripts/deploy-common.sh delete mode 100755 packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/scripts/deploy-user.sh delete mode 100755 packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/scripts/deploy.sh delete mode 100644 packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/test/docker-compose.yml delete mode 100755 packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/test/start.sh delete mode 100644 packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/Dockerfile delete mode 100644 packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/README.md delete mode 100755 packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/build.sh delete mode 100755 packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/common.sh delete mode 100755 packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/install/ginstall delete mode 100755 packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/scripts/deploy-common.sh delete mode 100755 packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/scripts/deploy-slim.sh delete mode 100755 packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/scripts/deploy-user.sh delete mode 100755 packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/scripts/deploy.sh delete mode 100644 packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/test/docker-compose.yml delete mode 100755 packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/test/start.sh delete mode 100644 packages/grafana-toolkit/docker/grafana-plugin-ci/Dockerfile delete mode 100644 packages/grafana-toolkit/docker/grafana-plugin-ci/README.md delete mode 100755 packages/grafana-toolkit/docker/grafana-plugin-ci/build.sh delete mode 100755 packages/grafana-toolkit/docker/grafana-plugin-ci/common.sh delete mode 100755 packages/grafana-toolkit/docker/grafana-plugin-ci/install/gget delete mode 100755 packages/grafana-toolkit/docker/grafana-plugin-ci/scripts/deploy-common.sh delete mode 100755 packages/grafana-toolkit/docker/grafana-plugin-ci/scripts/deploy-user.sh delete mode 100755 packages/grafana-toolkit/docker/grafana-plugin-ci/scripts/deploy.sh delete mode 100644 packages/grafana-toolkit/docker/grafana-plugin-ci/test/docker-compose.yml delete mode 100755 packages/grafana-toolkit/docker/grafana-plugin-ci/test/start.sh diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/Dockerfile b/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/Dockerfile deleted file mode 100644 index 3907fe9c05a..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -FROM alpine:3.15.6 - -USER root - -COPY scripts scripts -COPY install /usr/local - -WORKDIR scripts - -RUN ./deploy.sh diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/README.md b/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/README.md deleted file mode 100644 index 94b42aefd3d..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/README.md +++ /dev/null @@ -1,71 +0,0 @@ -# Using this docker image - -Uploaded to dockerhub as grafana/grafana-plugin-ci:latest-alpine - -Based off of `circleci/node:12-browsers` - -## User - -The user will be `circleci` -The home directory will be `/home/circleci` - -## Node - -- node 12 is installed -- yarn is installed globally -- npm is installed globally - -## Go - -- Go is installed in `/usr/local/bin/go` -- golangci-lint is installed in `/usr/local/bin/golangci-lint` -- mage is installed in `/home/circleci/go/bin/mage` - -All of the above directories are in the path, so there is no need to specify fully qualified paths. - -## Grafana - -- Installed in `/home/circleci/src/grafana` -- `yarn install` has been run - -## Integration/Release Testing - -There are 4 previous versions pre-downloaded to /usr/local/grafana. These versions are: - -1. 6.6.2 -2. 6.5.3 -3. 6.4.5 -4. 6.3.7 - -To test, your CircleCI config will need a run section with something similar to the following - -``` -- run: - name: Setup Grafana (local install) - command: | - sudo dpkg -i /usr/local/grafana/deb/grafana_6.6.2_amd64.deb - sudo cp ci/grafana-test-env/custom.ini /usr/share/grafana/conf/custom.ini - sudo cp ci/grafana-test-env/custom.ini /etc/grafana/grafana.ini - sudo service grafana-server start - grafana-cli --version -``` - -# Building - -To build, cd to `/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine` - -``` -./build.sh -``` - -# Developing/Testing - -To test, you should have docker-compose installed. - -``` -cd test -./start.sh -``` - -You will be in /home/circleci/test with the buildscripts installed to the local directory. -Do your edits/run tests. When saving, your edits will be available in the container immediately. diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/build.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/build.sh deleted file mode 100755 index f024e617055..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/build.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -set -eo pipefail - -source ./common.sh - -# -# No longer required, but useful to keep just in case we want to deploy -# changes in toolkit directly to the docker image -# -if [ -n "$INCLUDE_TOOLKIT" ]; then - /bin/rm -rfv install/grafana-toolkit - mkdir -pv install/grafana-toolkit - cp -rv ../../bin install/grafana-toolkit - cp -rv ../../src install/grafana-toolkit - cp -v ../../package.json install/grafana-toolkit - cp -v ../../tsconfig.json install/grafana-toolkit -fi - -docker build -t ${DOCKER_IMAGE_NAME} . -docker push $DOCKER_IMAGE_NAME - -[ -n "$INCLUDE_TOOLKIT" ] && /bin/rm -rfv install/grafana-toolkit diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/common.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/common.sh deleted file mode 100755 index 8a99e1e649b..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/common.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -## -## Common variable declarations -## Find the latest tags on https://hub.docker.com/r/grafana/grafana-plugin-ci/tags?page=1&name=alpine -## - -DOCKER_IMAGE_NAME="grafana/grafana-plugin-ci:1.6.1-alpine" diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/install/bin/cp b/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/install/bin/cp deleted file mode 100755 index fd23c3db871..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/install/bin/cp +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - -if [ "$1" == "-rn" ]; then - false | busybox cp -i -r "$2" "$3" 2>/dev/null -else - busybox cp $* -fi diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/install/bin/ginstall b/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/install/bin/ginstall deleted file mode 100755 index 537305a1f72..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/install/bin/ginstall +++ /dev/null @@ -1,73 +0,0 @@ -#!/bin/sh -## -# gget -# A script to get and install grafana versions -# for usage information see "show_help" below. -# - -latest=$(wget -O - 'https://raw.githubusercontent.com/grafana/grafana/main/latest.json' | jq -r '.stable') -canary=$(wget -O - "https://grafana.com/api/grafana/versions" | jq ".items[0].version" | tr -d '"') - -show_help() { - echo "Usage: gget " - echo "" - echo "where can be:" - echo " 1) A version from https://grafana.com/grafana/download (ex x.y.z)" - echo " 2) latest (currently $latest)" - echo " 3) canary (currently $canary)" - echo "" - echo " -h, --help: Display this help message" - echo "" - exit 0 -} - -opts=$(getopt -o h --long help -n 'gget' -- "$@") -[ $? -eq 0 ] || { - show_help -} - -eval set -- "$opts" -while true; do - case "$1" in - -h | --help) - show_help - ;; - --) - shift - break - ;; - *) - break - ;; - esac - shift -done - -[ -z "$1" ] && show_help - -# Make sure the script is being run as root -if [ $EUID -ne 0 ]; then - echo "This script must be run as root" - exit 1 -fi - -## -# MAIN -# -# Enough setup, let's actually do something -# -version=$1 -if [ "$version" == "latest" ]; then - version="$latest" - wget -O - "https://dl.grafana.com/oss/release/grafana-${version}.linux-amd64.tar.gz" | tar -C /opt -zxf - -elif [ "$version" == "canary" ]; then - version="$canary" - wget -O - "https://dl.grafana.com/oss/main/grafana-${version}.linux-amd64.tar.gz" | tar -C /opt -zxf - -else - wget -O - "https://dl.grafana.com/oss/release/grafana-${version}.linux-amd64.tar.gz" | tar -C /opt -zxf - -fi - -/bin/rm -rf /opt/grafana > /dev/null 2>&1 || true -ln -s /opt/grafana-${version} /opt/grafana - -# nohup /opt/grafana/bin/grafana-server -config /opt/grafana/conf/defaults.ini -homepath /opt/grafana >/dev/null 2>&1 & diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/install/bin/githubRelease.js b/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/install/bin/githubRelease.js deleted file mode 100644 index 849836da380..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/install/bin/githubRelease.js +++ /dev/null @@ -1,154 +0,0 @@ -'use strict'; -Object.defineProperty(exports, '__esModule', { value: true }); -const fs = require('fs'); -const path = require('path'); -const tslib_1 = require('tslib'); - -const getPluginId_1 = require('../../config/utils/getPluginId'); -const pluginValidation_1 = require('../../config/utils/pluginValidation'); -const env_1 = require('../../plugins/env'); -// @ts-ignore -// import execa = require('execa'); -const githubClient_1 = tslib_1.__importDefault(require('./githubClient')); -const resolveContentType = function (extension) { - if (extension.startsWith('.')) { - extension = extension.slice(1); - } - switch (extension) { - case 'zip': - return 'application/zip'; - case 'json': - return 'application/json'; - case 'sha1': - return 'text/plain'; - default: - return 'application/octet-stream'; - } -}; -const GitHubRelease = /** @class */ (function () { - function GitHubRelease(token, username, repository, releaseNotes, commitHash) { - this.token = token; - this.username = username; - this.repository = repository; - this.releaseNotes = releaseNotes; - this.commitHash = commitHash; - this.git = new githubClient_1.default({ - required: true, - repo: repository, - }); - } - GitHubRelease.prototype.publishAssets = function (srcLocation, destUrl) { - const _this = this; - // Add the assets. Loop through files in the ci/dist folder and upload each asset. - const files = fs.readdirSync(srcLocation); - return files.map(function (file) { - return tslib_1.__awaiter(_this, void 0, void 0, function () { - let fileStat, fileData; - return tslib_1.__generator(this, function (_a) { - fileStat = fs.statSync(srcLocation + '/' + file); - fileData = fs.readFileSync(srcLocation + '/' + file); - return [ - 2 /*return*/, - this.git.client.post(destUrl + '?name=' + file, fileData, { - headers: { - 'Content-Type': resolveContentType(path.extname(file)), - 'Content-Length': fileStat.size, - }, - maxContentLength: fileStat.size * 2 * 1024 * 1024, - }), - ]; - }); - }); - }); - }; - GitHubRelease.prototype.release = function () { - let _a, _b, _c, _d; - return tslib_1.__awaiter(this, void 0, void 0, function () { - let ciDir, - distDir, - distContentDir, - pluginJsonFile, - pluginInfo, - PUBLISH_DIR, - commitHash, - latestRelease, - reason_1, - newReleaseResponse, - publishPromises, - reason_2; - return tslib_1.__generator(this, function (_e) { - switch (_e.label) { - case 0: - ciDir = env_1.getCiFolder(); - distDir = path.resolve(ciDir, 'dist'); - distContentDir = path.resolve(distDir, getPluginId_1.getPluginId()); - pluginJsonFile = path.resolve(distContentDir, 'plugin.json'); - pluginInfo = pluginValidation_1.getPluginJson(pluginJsonFile).info; - PUBLISH_DIR = path.resolve(env_1.getCiFolder(), 'packages'); - commitHash = this.commitHash || ((_a = pluginInfo.build) === null || _a === void 0 ? void 0 : _a.hash); - _e.label = 1; - case 1: - _e.trys.push([1, 5, , 6]); - return [4 /*yield*/, this.git.client.get('releases/tags/v' + pluginInfo.version)]; - case 2: - latestRelease = _e.sent(); - if (!(latestRelease.data.tag_name === 'v' + pluginInfo.version)) { - return [3 /*break*/, 4]; - } - return [4 /*yield*/, this.git.client.delete('releases/' + latestRelease.data.id)]; - case 3: - _e.sent(); - _e.label = 4; - case 4: - return [3 /*break*/, 6]; - case 5: - reason_1 = _e.sent(); - if (reason_1.response.status !== 404) { - // 404 just means no release found. Not an error. Anything else though, re throw the error - throw reason_1; - } - return [3 /*break*/, 6]; - case 6: - _e.trys.push([6, 9, , 10]); - return [ - 4 /*yield*/, - this.git.client.post('releases', { - tag_name: 'v' + pluginInfo.version, - target_commitish: commitHash, - name: 'v' + pluginInfo.version, - body: this.releaseNotes, - draft: false, - prerelease: false, - }), - ]; - case 7: - newReleaseResponse = _e.sent(); - publishPromises = this.publishAssets( - PUBLISH_DIR, - 'https://uploads.github.com/repos/' + - this.username + - '/' + - this.repository + - '/releases/' + - newReleaseResponse.data.id + - '/assets' - ); - return [4 /*yield*/, Promise.all(publishPromises)]; - case 8: - _e.sent(); - return [3 /*break*/, 10]; - case 9: - reason_2 = _e.sent(); - console.log(reason_2); - // Rethrow the error so that we can trigger a non-zero exit code to circle-ci - throw reason_2; - case 10: - return [2 /*return*/]; - } - }); - }); - }; - return GitHubRelease; -})(); -exports.GitHubRelease = GitHubRelease; -//# sourceMappingURL=githubRelease.js.map7027e10521e9 diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/scripts/deploy-common.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/scripts/deploy-common.sh deleted file mode 100755 index 9263691386f..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/scripts/deploy-common.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/bin/sh - -## -# Script to deploy a docker image. Must return exit code 0 -# -do_exit() { - message="$1" - exit_code="$2" - - echo "$message" - exit $exit_code -} - - -## -# Get file, get's a file, validates the SHA -# @param filename -# @param expected sha value -# @returns 0 if successful, -1 of checksum validation failed. -# -get_file () { - [ -n "$1" ] && url=$1 || do_exit "url required" 1 - [ -n "$2" ] && dest=$2 || do_exit "destination required" 2 - sha=$3 - file=$(basename $dest) - - curl -fL "${url}" -o "$dest" - if [ -n "$sha" ]; then - echo "$sha $dest" | sha256sum || do_exit "Checksum validation failed for $file. Exiting" 1 - fi -} - -untar_file () { - [ -n "$1" ] && src=$1 || do_exit "src required" 1 - [ -n "$2" ] && dest=$2 || dest="/usr/local" - - tar -C "$dest" -xf "$src" && /bin/rm -rf "$src" -} - -## -# WIP: Just started this and not finished. -# The intent it to download a release from a git repo, -# compile, and install -get_latest_release () { - tarsrc=$(curl -sL "https://api.github.com/repos/$1/$2/releases/latest" | jq ".tarball_url" | tr -d '"') - curl -fL -o /tmp/autoretrieved.tar.gz "$tarsrc" - origdir=$PWD - reponame=$(tar zxvf autoretrieved.tar.gz | tail -1 | awk -F / '{print $1}') - cd "/tmp/$reponame" - #perform compile - cd $origdir -} diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/scripts/deploy-user.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/scripts/deploy-user.sh deleted file mode 100755 index f8926ec4678..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/scripts/deploy-user.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -source "./deploy-common.sh" - diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/scripts/deploy.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/scripts/deploy.sh deleted file mode 100755 index 94345950078..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/scripts/deploy.sh +++ /dev/null @@ -1,80 +0,0 @@ -#!/bin/sh -set -eo pipefail - -source "./deploy-common.sh" - -# Make libgcc compatible -mkdir /lib64 && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2 - -# Replace cp with something that mocks the one that ci-package needs -rm /bin/cp -mv /usr/local/bin/cp /bin/cp - -apk add --no-cache curl npm yarn build-base openssh git-lfs perl-utils coreutils python3 - -# -# Only relevant for testing, but cypress does not work with musl/alpine. -# -# apk add --no-cache xvfb glib nss nspr gdk-pixbuf "gtk+3.0" pango atk cairo dbus-libs libxcomposite libxrender libxi libxtst libxrandr libxscrnsaver alsa-lib at-spi2-atk at-spi2-core cups-libs gcompat libc6-compat - -# Install Go -filename="go1.19.4.linux-amd64.tar.gz" -get_file "https://dl.google.com/go/$filename" "/tmp/$filename" "c9c08f783325c4cf840a94333159cc937f05f75d36a8b307951d5bd959cf2ab8" -untar_file "/tmp/$filename" - -# Install golangci-lint -GOLANGCILINT_VERSION=1.50.0 -filename="golangci-lint-${GOLANGCILINT_VERSION}-linux-amd64" -get_file "https://github.com/golangci/golangci-lint/releases/download/v${GOLANGCILINT_VERSION}/$filename.tar.gz" \ - "/tmp/$filename.tar.gz" \ - "b4b329efcd913082c87d0e9606711ecb57415b5e6ddf233fde9e76c69d9b4e8b" -untar_file "/tmp/$filename.tar.gz" -ln -s /usr/local/${filename}/golangci-lint /usr/local/bin/golangci-lint -ln -s /usr/local/go/bin/go /usr/local/bin/go -ln -s /usr/local/go/bin/gofmt /usr/local/bin/gofmt -chmod 755 /usr/local/bin/golangci-lint - -# Install dependencies -apk add --no-cache fontconfig zip jq - -# Install code climate -get_file "https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64" \ - "/usr/local/bin/cc-test-reporter" \ - "20d1d4e2b399d0287d91e65faeee8ffbef08e3262b0be5eda7def7b3c2799ddd" -chmod 755 /usr/local/bin/cc-test-reporter - -curl -fL -o /usr/local/bin/grabpl "https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl" - -apk add --no-cache git -# Install Mage -mkdir -pv /tmp/mage $HOME/go/bin -git clone https://github.com/magefile/mage.git /tmp/mage -cd /tmp/mage && go run bootstrap.go -mv $HOME/go/bin/mage /usr/local/bin - -wget -O - -q https://raw.githubusercontent.com/securego/gosec/master/install.sh | sh -s -- -b /usr/local/bin v2.14.0 - -source "/etc/profile" -sh -l -c "go get -u github.com/mgechev/revive" -for file in $(ls $HOME/go/bin); do - mv -v $HOME/go/bin/$file /usr/local/bin/$file -done - -# Install grafana-toolkit deps -current_dir=$PWD -cd /usr/local/grafana-toolkit && yarn install && cd $current_dir -ln -s /usr/local/grafana-toolkit/bin/grafana-toolkit.js /usr/local/bin/grafana-toolkit - -GOOGLE_SDK_VERSION=365.0.1 -GOOGLE_SDK_CHECKSUM=17003cdba67a868c2518ac16efa60dc6175533b7a9fb87304459784308e30fb0 - -curl -fLO https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-${GOOGLE_SDK_VERSION}-linux-x86_64.tar.gz -echo "${GOOGLE_SDK_CHECKSUM} google-cloud-sdk-${GOOGLE_SDK_VERSION}-linux-x86_64.tar.gz" | sha256sum --check --status -tar xvzf google-cloud-sdk-${GOOGLE_SDK_VERSION}-linux-x86_64.tar.gz -C /opt -rm google-cloud-sdk-${GOOGLE_SDK_VERSION}-linux-x86_64.tar.gz -ln -s /opt/google-cloud-sdk/bin/gsutil /usr/bin/gsutil -ln -s /opt/google-cloud-sdk/bin/gcloud /usr/bin/gcloud - -# Cleanup after yourself -/bin/rm -rf /tmp/mage -/bin/rm -rf $HOME/go diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/test/docker-compose.yml b/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/test/docker-compose.yml deleted file mode 100644 index 9a3e53f2cc3..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/test/docker-compose.yml +++ /dev/null @@ -1,18 +0,0 @@ -version: '3' -services: - citest: - image: "amd64/alpine" - user: root - volumes: - - ../scripts:/home/circleci/scripts - - ../install:/home/circleci/install - - ${HOME}/.ssh:/root/.ssh - - ../../..:/home/circleci/grafana-toolkit - cibuilt: - image: "grafana/grafana-plugin-ci:latest-alpine" - user: root - volumes: - - ../scripts:/home/circleci/scripts - - ../install:/home/circleci/install - - ${HOME}/.ssh:/root/.ssh - - ../../..:/home/circleci/grafana-toolkit diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/test/start.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/test/start.sh deleted file mode 100755 index f5551eb3b6e..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/test/start.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -function finish { - echo "Exiting and cleaning up docker image" - docker-compose down -} -trap finish EXIT - -# Enter the docker container -if [ "$1" = "built" ]; then - docker-compose run cibuilt sh -c "cd /home/circleci; exec sh --login -i" -else - docker-compose run citest sh -c "cd /home/circleci; exec sh --login -i" -fi diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/Dockerfile b/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/Dockerfile deleted file mode 100644 index 6aa2682c27b..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM debian:buster-slim - -ENV DEBIAN_FRONTEND=noninteractive - -COPY scripts scripts -COPY install /usr/local - -RUN cd scripts && ./deploy.sh -ENV DEBIAN_FRONTEND=newt diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/README.md b/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/README.md deleted file mode 100644 index 1b37557a0b3..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# Using this docker image - -## User - -The user will be `circleci` -The home directory will be `/home/circleci` - -## Node - -- node 14 is installed -- yarn is installed globally -- npm is installed globally - -## Go - -- Go is installed in `/usr/local/bin/go` -- golangci-lint is installed in `/usr/local/bin/golangci-lint` -- mage is installed in `/usr/local/bin/mage` - -All of the above directories are in the path, so there is no need to specify fully qualified paths. - -## Grafana - -- Installed in `/home/circleci/src/grafana` -- `yarn install` has been run - -## Integration/Release Testing - -There are 4 previous versions pre-downloaded to /usr/local/grafana. These versions are: - -1. 6.6.2 -2. 6.5.3 -3. 6.4.5 -4. 6.3.7 - -To test, your CircleCI config will need a run section with something similar to the following - -``` -- run: - name: Setup Grafana (local install) - command: | - sudo dpkg -i /usr/local/grafana/deb/grafana_6.6.2_amd64.deb - sudo cp ci/grafana-test-env/custom.ini /usr/share/grafana/conf/custom.ini - sudo cp ci/grafana-test-env/custom.ini /etc/grafana/grafana.ini - sudo service grafana-server start - grafana-cli --version -``` - -# Building - -To build, cd to `/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e` - -``` -./build.sh -``` - -# Developing/Testing - -To test, you should have docker-compose installed. - -``` -cd test -./start.sh -``` - -You will be in /home/circleci/test with the buildscripts installed to the local directory. -Do your edits/run tests. When saving, your edits will be available in the container immediately. diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/build.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/build.sh deleted file mode 100755 index 14ca02a1dc9..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/build.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -set -eo pipefail - -source ./common.sh - -# -# No longer required, but useful to keep just in case we want to deploy -# changes in toolkit directly to the docker image -# -if [ -n "$INCLUDE_TOOLKIT" ]; then - /bin/rm -rfv install/grafana-toolkit - mkdir -pv install/grafana-toolkit - cp -rv ../../bin install/grafana-toolkit - cp -rv ../../src install/grafana-toolkit - cp -v ../../package.json install/grafana-toolkit - cp -v ../../tsconfig.json install/grafana-toolkit -fi - -docker build -t ${DOCKER_IMAGE_NAME} . -docker push $DOCKER_IMAGE_NAME -docker tag ${DOCKER_IMAGE_NAME} ${DOCKER_IMAGE_BASE_NAME}:latest -docker push ${DOCKER_IMAGE_BASE_NAME}:latest - -[ -n "$INCLUDE_TOOLKIT" ] && /bin/rm -rfv install/grafana-toolkit diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/common.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/common.sh deleted file mode 100755 index 29714cd5a84..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/common.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -## -## Common variable declarations -## Find the latest tag on https://hub.docker.com/r/grafana/grafana-plugin-ci-e2e/tags -## - -DOCKER_IMAGE_BASE_NAME="grafana/grafana-plugin-ci-e2e" -DOCKER_IMAGE_VERSION="1.6.1" -DOCKER_IMAGE_NAME="${DOCKER_IMAGE_BASE_NAME}:${DOCKER_IMAGE_VERSION}" diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/install/ginstall b/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/install/ginstall deleted file mode 100755 index a71019bdbef..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/install/ginstall +++ /dev/null @@ -1,73 +0,0 @@ -#!/bin/bash -## -# gget -# A script to get and install grafana versions -# for usage information see "show_help" below. -# - -latest=$(wget -O - 'https://raw.githubusercontent.com/grafana/grafana/main/latest.json' | jq -r '.stable') -canary=$(wget -O - "https://grafana.com/api/grafana-enterprise/versions" | jq ".items[0].version" | tr -d '"') - -show_help() { - echo "Usage: gget " - echo "" - echo "where can be:" - echo " 1) A version from https://grafana.com/grafana/download (ex x.y.z)" - echo " 2) latest (currently $latest)" - echo " 3) canary (currently $canary)" - echo "" - echo " -h, --help: Display this help message" - echo "" - exit 0 -} - -opts=$(getopt -o h --long help -n 'gget' -- "$@") -[ $? -eq 0 ] || { - show_help -} - -eval set -- "$opts" -while true; do - case "$1" in - -h | --help) - show_help - ;; - --) - shift - break - ;; - *) - break - ;; - esac - shift -done - -[ -z "$1" ] && show_help - -# Make sure the script is being run as root -if [ $EUID -ne 0 ]; then - echo "This script must be run as root" - exit 1 -fi - -## -# MAIN -# -# Enough setup, let's actually do something -# -version=$1 -if [ "$version" == "latest" ]; then - version="$latest" - wget -O - "https://dl.grafana.com/enterprise/release/grafana-enterprise-${version}.linux-amd64.tar.gz" | tar -C /opt -zxf - -elif [ "$version" == "canary" ]; then - version="$canary" - wget -O - "https://dl.grafana.com/enterprise/main/grafana-enterprise-${version}.linux-amd64.tar.gz" | tar -C /opt -zxf - -else - wget -O - "https://dl.grafana.com/enterprise/release/grafana-enterprise-${version}.linux-amd64.tar.gz" | tar -C /opt -zxf - -fi - -/bin/rm -rf /opt/grafana > /dev/null 2>&1 || true -ln -s /opt/grafana-${version} /opt/grafana - -# nohup /opt/grafana/bin/grafana-server -config /opt/grafana/conf/defaults.ini -homepath /opt/grafana >/dev/null 2>&1 & diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/scripts/deploy-common.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/scripts/deploy-common.sh deleted file mode 100755 index af585d2eb19..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/scripts/deploy-common.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash - -## -# Script to deploy a docker image. Must return exit code 0 -# -do_exit() { - message="$1" - exit_code="$2" - - echo "$message" - exit $exit_code -} - -## -# Get file, get's a file, validates the SHA -# @param filename -# @param expected sha value -# @returns 0 if successful, -1 of checksum validation failed. -# -get_file () { - [ -n "$1" ] && url=$1 || do_exit "url required" -1 - [ -n "$2" ] && dest=$2 || do_exit "destination required" -2 - sha=$3 - file=$(basename $dest) - - wget "$url" -O "$dest" - if [ -n "$sha" ]; then - echo "$sha $dest" | sha256sum --check --status || do_exit "Checksum validation failed for $file. Exiting" -1 - fi -} - -untar_file () { - [ -n "$1" ] && src=$1 || do_exit "src required" -1 - [ -n "$2" ] && dest=$2 || dest="/usr/local" - - tar -C "$dest" -xf "$src" && /bin/rm -rf "$src" -} diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/scripts/deploy-slim.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/scripts/deploy-slim.sh deleted file mode 100755 index 9c3c0a31e2d..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/scripts/deploy-slim.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash -source "/etc/profile" - -apt-get --allow-insecure-repositories update -apt-get install --allow-unauthenticated -y \ - build-essential \ - wget git sudo adduser \ - libfontconfig1 \ - locate \ - libnss3 libnspr4 \ - libgdk-pixbuf2.0-0 \ - libgtk-3-0 \ - libpangocairo-1.0-0 \ - libpango-1.0-0 \ - libatk1.0-0 \ - libcairo2 \ - libdbus-1-3 \ - libxcomposite1 libxrender1 libxcursor1 libxi6 libxtst6 libxrandr2 libxss1 xauth xvfb \ - libasound2 \ - libatk-bridge2.0-0 \ - libatspi2.0-0 \ - libcups2 \ - jq net-tools git-lfs unzip pkg-config zip \ - libaio1 libaio-dev \ - netcat \ - libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb - diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/scripts/deploy-user.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/scripts/deploy-user.sh deleted file mode 100755 index 08f2b7b950a..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/scripts/deploy-user.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -source "./deploy-common.sh" - diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/scripts/deploy.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/scripts/deploy.sh deleted file mode 100755 index 99e711aee87..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/scripts/deploy.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/bash -set -eo pipefail - -source "/etc/profile" -source "./deploy-slim.sh" -source "./deploy-common.sh" - -NODEVER="v16.13.2" -# Install Node -wget -O - "https://nodejs.org/dist/${NODEVER}/node-${NODEVER}-linux-x64.tar.xz" | tar Jvxf - -C "/tmp" - -# Move node to /usr/local so it's in the path -pushd /tmp/node-${NODEVER}-linux-x64 -/bin/rm -f CHANGELOG.md README.md LICENSE -/bin/cp -r * /usr/local -popd -/bin/rm -rf /tmp/node-${NODEVER} - -# Resource the profile so we know our path is being honoured -source "/etc/profile" -# Install Yarn. Not in the path yet so fully qualified -npm i -g yarn - -# Install Go -filename="go1.19.4.linux-amd64.tar.gz" -get_file "https://dl.google.com/go/$filename" "/tmp/$filename" "c9c08f783325c4cf840a94333159cc937f05f75d36a8b307951d5bd959cf2ab8" -untar_file "/tmp/$filename" - -# Install golangci-lint -GOLANGCILINT_VERSION=1.50.0 -filename="golangci-lint-${GOLANGCILINT_VERSION}-linux-amd64" -get_file "https://github.com/golangci/golangci-lint/releases/download/v${GOLANGCILINT_VERSION}/$filename.tar.gz" \ - "/tmp/$filename.tar.gz" \ - "b4b329efcd913082c87d0e9606711ecb57415b5e6ddf233fde9e76c69d9b4e8b" -untar_file "/tmp/$filename.tar.gz" -ln -s /usr/local/${filename}/golangci-lint /usr/local/bin/golangci-lint -ln -s /usr/local/go/bin/go /usr/local/bin/go -ln -s /usr/local/go/bin/gofmt /usr/local/bin/gofmt -chmod 755 /usr/local/bin/golangci-lint - -# Install code climate -get_file "https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64" \ - "/usr/local/bin/cc-test-reporter" \ - "20d1d4e2b399d0287d91e65faeee8ffbef08e3262b0be5eda7def7b3c2799ddd" -chmod 755 /usr/local/bin/cc-test-reporter - -wget -O /usr/local/bin/grabpl "https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl" -chmod +x /usr/local/bin/grabpl - -# Install Mage -mkdir -pv /tmp/mage $HOME/go/bin -git clone https://github.com/magefile/mage.git /tmp/mage -pushd /tmp/mage && go run bootstrap.go && popd -mv $HOME/go/bin/mage /usr/local/bin -# Cleanup after yourself -/bin/rm -rf /tmp/mage -/bin/rm -rf $HOME/go - -# add cypress -yarn global add cypress -# verify cypress install -cypress verify - -# Get the size down -/bin/rm -rf /var/lib/apt/lists diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/test/docker-compose.yml b/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/test/docker-compose.yml deleted file mode 100644 index 5d6297c4833..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/test/docker-compose.yml +++ /dev/null @@ -1,18 +0,0 @@ -version: '3' -services: - citest: - image: "debian:buster-slim" - user: root - volumes: - - ../scripts:/root/scripts - - ../install:/root/install - - ${HOME}/.ssh:/root/.ssh - - ../../..:/root/grafana-toolkit - cibuilt: - image: "grafana/grafana-plugin-ci-e2e" - user: root - volumes: - - ../scripts:/root/scripts - - ../install:/root/install - - ${HOME}/.ssh:/root/.ssh - - ../../..:/root/grafana-toolkit diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/test/start.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/test/start.sh deleted file mode 100755 index 2552c964a85..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/test/start.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -function finish { - echo "Exiting and cleaning up docker image" - docker-compose down -} -trap finish EXIT - -# Enter the docker container -if [ "$1" = "built" ]; then - docker-compose run cibuilt sh -c "cd /root; exec bash --login -i" -else - docker-compose run citest sh -c "cd /root; exec bash --login -i" -fi diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci/Dockerfile b/packages/grafana-toolkit/docker/grafana-plugin-ci/Dockerfile deleted file mode 100644 index 46228193273..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM debian:testing-20210111-slim -USER root -COPY scripts scripts -WORKDIR scripts -RUN apt-get update && \ - apt-get install -y wget && \ - ./deploy.sh -COPY install/gget /usr/local/bin/gget diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci/README.md b/packages/grafana-toolkit/docker/grafana-plugin-ci/README.md deleted file mode 100644 index 18bc1aec592..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci/README.md +++ /dev/null @@ -1,71 +0,0 @@ -# Using this docker image - -Currently tagged and uploaded to dockerhub as srclosson/integrations-ci-build - -Based off of `circleci/node:12-browsers` - -## User - -The user will be `circleci` -The home directory will be `/home/circleci` - -## Node - -- node 12 is installed -- yarn is installed globally -- npm is installed globally - -## Go - -- Go is installed in `/usr/local/bin/go` -- golangci-lint is installed in `/usr/local/bin/golangci-lint` -- mage is installed in `/home/circleci/go/bin/mage` - -All of the above directories are in the path, so there is no need to specify fully qualified paths. - -## Grafana - -- Installed in `/home/circleci/src/grafana` -- `yarn install` has been run - -## Integration/Release Testing - -There are 4 previous versions pre-downloaded to /usr/local/grafana. These versions are: - -1. 6.6.2 -2. 6.5.3 -3. 6.4.5 -4. 6.3.7 - -To test, your CircleCI config will need a run section with something similar to the following - -``` -- run: - name: Setup Grafana (local install) - command: | - sudo dpkg -i /usr/local/grafana/deb/grafana_6.6.2_amd64.deb - sudo cp ci/grafana-test-env/custom.ini /usr/share/grafana/conf/custom.ini - sudo cp ci/grafana-test-env/custom.ini /etc/grafana/grafana.ini - sudo service grafana-server start - grafana-cli --version -``` - -# Building - -To build, cd to `/packages/grafana-toolkit/docker/grafana-plugin-ci` - -``` -./build.sh -``` - -# Developing/Testing - -To test, you should have docker-compose installed. - -``` -cd test -./start.sh -``` - -You will be in /home/circleci/test with the buildscripts installed to the local directory. -Do your edits/run tests. When saving, your edits will be available in the container immediately. diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci/build.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci/build.sh deleted file mode 100755 index 3d8d3d7110a..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci/build.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -set -eo pipefail - -source ./common.sh - -docker build -t ${DOCKER_IMAGE_NAME} . -docker push $DOCKER_IMAGE_NAME - diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci/common.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci/common.sh deleted file mode 100755 index 47d2b068d8d..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci/common.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -## -## Common variable declarations -## Find the latest tags on https://hub.docker.com/r/grafana/grafana-plugin-ci/tags -## - -DOCKER_IMAGE_NAME="grafana/grafana-plugin-ci:1.6.1" diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci/install/gget b/packages/grafana-toolkit/docker/grafana-plugin-ci/install/gget deleted file mode 100755 index 3e38e2d5911..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci/install/gget +++ /dev/null @@ -1,63 +0,0 @@ -#!/bin/bash -## -# gget -# A script to get and install grafana versions -# for usage information see "show_help" below. -# - -latest=$(curl -s 'https://raw.githubusercontent.com/grafana/grafana/main/latest.json' | jq -r '.stable') -canary=$(curl -s "https://grafana.com/api/grafana/versions" | jq ".items[0].version" | tr -d '"') - -show_help() { - echo "Usage: gget " - echo "" - echo "where can be:" - echo " 1) A version from https://grafana.com/grafana/download (ex x.y.z)" - echo " 2) latest (currently $latest)" - echo " 3) canary (currently $canary)" - echo "" - echo " -h, --help: Display this help message" - echo "" - exit 0 -} - -opts=$(getopt -o h --long help -n 'gget' -- "$@") -[ $? -eq 0 ] || { - show_help -} - -eval set -- "$opts" -while true; do - case "$1" in - -h | --help) - show_help - ;; - --) - shift - break - ;; - *) - break - ;; - esac - shift -done - -[ -z "$1" ] && show_help - -# Make sure the script is being run as root -if [ $EUID -ne 0 ]; then - echo "This script must be run as root" - exit 1 -fi - -## -# MAIN -# -# Enough setup, let's actually do something -# -version=$1 -[ "$version" == "latest" ] && version="$latest" -[ "$version" == "canary" ] && version="$canary" -wget "https://dl.grafana.com/oss/release/grafana_${version}_amd64.deb" -O "/tmp/grafana_${version}_amd64.deb" -dpkg -i "/tmp/grafana_${version}_amd64.deb" && /bin/rm -rfv "/tmp/grafana_${version}_amd64.deb" diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci/scripts/deploy-common.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci/scripts/deploy-common.sh deleted file mode 100755 index 524bb5e4ec6..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci/scripts/deploy-common.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash - -## -# Script to deploy a docker image. Must return exit code 0 -# -do_exit() { - message="$1" - exit_code="$2" - - echo "$message" - exit $exit_code -} - - -## -# Get file, get's a file, validates the SHA -# @param filename -# @param expected sha value -# @returns 0 if successful, -1 of checksum validation failed. -# -get_file () { - [ -n "$1" ] && url=$1 || do_exit "url required" -1 - [ -n "$2" ] && dest=$2 || do_exit "destination required" -2 - sha=$3 - file=$(basename $dest) - - wget "$url" -O "$dest" - if [ -n "$sha" ]; then - echo "$sha $dest" | sha256sum --check --status || do_exit "Checksum validation failed for $file. Exiting" -1 - fi -} - -untar_file () { - [ -n "$1" ] && src=$1 || do_exit "src required" -1 - [ -n "$2" ] && dest=$2 || dest="/usr/local" - - tar -C "$dest" -xf "$src" && /bin/rm -rf "$src" -} \ No newline at end of file diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci/scripts/deploy-user.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci/scripts/deploy-user.sh deleted file mode 100755 index 08f2b7b950a..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci/scripts/deploy-user.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -source "./deploy-common.sh" - diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci/scripts/deploy.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci/scripts/deploy.sh deleted file mode 100755 index 3ee0bff1e16..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci/scripts/deploy.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/bash -source "./deploy-common.sh" - -# Install Go -filename="go1.19.4.linux-amd64.tar.gz" -get_file "https://dl.google.com/go/$filename" "/tmp/$filename" "c9c08f783325c4cf840a94333159cc937f05f75d36a8b307951d5bd959cf2ab8" -untar_file "/tmp/$filename" - -# Install golangci-lint -GOLANGCILINT_VERSION=1.50.0 -filename="golangci-lint-${GOLANGCILINT_VERSION}-linux-amd64" -get_file "https://github.com/golangci/golangci-lint/releases/download/v${GOLANGCILINT_VERSION}/$filename.tar.gz" \ - "/tmp/$filename.tar.gz" \ - "b4b329efcd913082c87d0e9606711ecb57415b5e6ddf233fde9e76c69d9b4e8b" -untar_file "/tmp/$filename.tar.gz" -ln -s /usr/local/${filename}/golangci-lint /usr/local/bin/golangci-lint -ln -s /usr/local/go/bin/go /usr/local/bin/go -ln -s /usr/local/go/bin/gofmt /usr/local/bin/gofmt -chmod 755 /usr/local/bin/golangci-lint - -# Install dependencies -apt-get update -y && apt-get install -y adduser libfontconfig1 locate && /bin/rm -rf /var/lib/apt/lists/* - -# Install code climate -get_file "https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64" \ - "/usr/local/bin/cc-test-reporter" \ - "20d1d4e2b399d0287d91e65faeee8ffbef08e3262b0be5eda7def7b3c2799ddd" -chmod 755 /usr/local/bin/cc-test-reporter - -wget -O /usr/local/bin/grabpl "https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl" -chmod +x /usr/local/bin/grabpl - -# Install Mage -mkdir -pv /tmp/mage $HOME/go/bin -git clone https://github.com/magefile/mage.git /tmp/mage -pushd /tmp/mage && go run bootstrap.go && popd -mv $HOME/go/bin/mage /usr/local/bin - -GOOGLE_SDK_VERSION=365.0.1 -GOOGLE_SDK_CHECKSUM=17003cdba67a868c2518ac16efa60dc6175533b7a9fb87304459784308e30fb0 - -curl -fLO https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-${GOOGLE_SDK_VERSION}-linux-x86_64.tar.gz -echo "${GOOGLE_SDK_CHECKSUM} google-cloud-sdk-${GOOGLE_SDK_VERSION}-linux-x86_64.tar.gz" | sha256sum --check --status -tar xvzf google-cloud-sdk-${GOOGLE_SDK_VERSION}-linux-x86_64.tar.gz -C /opt -rm google-cloud-sdk-${GOOGLE_SDK_VERSION}-linux-x86_64.tar.gz -ln -s /opt/google-cloud-sdk/bin/gsutil /usr/bin/gsutil -ln -s /opt/google-cloud-sdk/bin/gcloud /usr/bin/gcloud - -# Cleanup after yourself -/bin/rm -rf /tmp/mage -/bin/rm -rf $HOME/go - -# Perform user specific initialization -sudo -u circleci ./deploy-user.sh - -# Get the size down -/bin/rm -rf /var/lib/apt/lists diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci/test/docker-compose.yml b/packages/grafana-toolkit/docker/grafana-plugin-ci/test/docker-compose.yml deleted file mode 100644 index d734b3a38e6..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci/test/docker-compose.yml +++ /dev/null @@ -1,10 +0,0 @@ -version: '3' -services: - citest: - image: "circleci/node:12-browsers" - user: root - volumes: - - ../scripts:/home/circleci/scripts - - ../install:/home/circleci/install - - ${HOME}/.ssh:/root/.ssh - - ../../..:/home/circleci/grafana-toolkit diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci/test/start.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci/test/start.sh deleted file mode 100755 index af3d91cb4a0..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci/test/start.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -function finish { - echo "Exiting and cleaning up docker image" - docker-compose down -} -trap finish EXIT - -# Enter the docker container -docker-compose run citest bash -c "cd /home/circleci; exec bash --login -i" From a1289444717efec6ec5c96405710b5f391513756 Mon Sep 17 00:00:00 2001 From: Ezequiel Victorero Date: Fri, 27 Jan 2023 11:20:22 -0300 Subject: [PATCH 043/117] PublicDashboards: moved tokens service and new repository method (#61806) --- pkg/services/publicdashboards/api/api.go | 12 ++--- .../publicdashboards/api/middleware.go | 6 +-- .../publicdashboards/api/middleware_test.go | 4 +- pkg/services/publicdashboards/api/query.go | 8 +-- .../database/database_test.go | 4 +- .../internal/tokens/tokens.go | 29 ---------- .../internal/tokens/tokens_test.go | 54 ------------------- .../public_dashboard_service_mock.go | 25 ++++++++- .../publicdashboards/publicdashboard.go | 1 + .../publicdashboards/service/service.go | 22 +++++++- .../publicdashboards/service/service_test.go | 18 ++++++- .../publicdashboards/validation/validation.go | 14 +++++ .../validation/validation_test.go | 33 ++++++++++++ 13 files changed, 125 insertions(+), 105 deletions(-) delete mode 100644 pkg/services/publicdashboards/internal/tokens/tokens.go delete mode 100644 pkg/services/publicdashboards/internal/tokens/tokens_test.go diff --git a/pkg/services/publicdashboards/api/api.go b/pkg/services/publicdashboards/api/api.go index a40d8776009..8ac11743ed6 100644 --- a/pkg/services/publicdashboards/api/api.go +++ b/pkg/services/publicdashboards/api/api.go @@ -13,8 +13,8 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/publicdashboards" - "github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" + "github.com/grafana/grafana/pkg/services/publicdashboards/validation" "github.com/grafana/grafana/pkg/web" ) @@ -102,7 +102,7 @@ func (api *Api) ListPublicDashboards(c *contextmodel.ReqContext) response.Respon func (api *Api) GetPublicDashboard(c *contextmodel.ReqContext) response.Response { // exit if we don't have a valid dashboardUid dashboardUid := web.Params(c.Req)[":dashboardUid"] - if !tokens.IsValidShortUID(dashboardUid) { + if !validation.IsValidShortUID(dashboardUid) { return response.Err(ErrPublicDashboardIdentifierNotSet.Errorf("GetPublicDashboard: no dashboard Uid for public dashboard specified")) } @@ -123,7 +123,7 @@ func (api *Api) GetPublicDashboard(c *contextmodel.ReqContext) response.Response func (api *Api) CreatePublicDashboard(c *contextmodel.ReqContext) response.Response { // exit if we don't have a valid dashboardUid dashboardUid := web.Params(c.Req)[":dashboardUid"] - if !tokens.IsValidShortUID(dashboardUid) { + if !validation.IsValidShortUID(dashboardUid) { return response.Err(ErrInvalidUid.Errorf("CreatePublicDashboard: invalid Uid %s", dashboardUid)) } @@ -155,12 +155,12 @@ func (api *Api) CreatePublicDashboard(c *contextmodel.ReqContext) response.Respo func (api *Api) UpdatePublicDashboard(c *contextmodel.ReqContext) response.Response { // exit if we don't have a valid dashboardUid dashboardUid := web.Params(c.Req)[":dashboardUid"] - if !tokens.IsValidShortUID(dashboardUid) { + if !validation.IsValidShortUID(dashboardUid) { return response.Err(ErrInvalidUid.Errorf("UpdatePublicDashboard: invalid dashboard Uid %s", dashboardUid)) } uid := web.Params(c.Req)[":uid"] - if !tokens.IsValidShortUID(uid) { + if !validation.IsValidShortUID(uid) { return response.Err(ErrInvalidUid.Errorf("UpdatePublicDashboard: invalid Uid %s", uid)) } @@ -192,7 +192,7 @@ func (api *Api) UpdatePublicDashboard(c *contextmodel.ReqContext) response.Respo // DELETE /api/dashboards/uid/:dashboardUid/public-dashboards/:uid func (api *Api) DeletePublicDashboard(c *contextmodel.ReqContext) response.Response { uid := web.Params(c.Req)[":uid"] - if !tokens.IsValidShortUID(uid) { + if !validation.IsValidShortUID(uid) { return response.Err(ErrInvalidUid.Errorf("UpdatePublicDashboard: invalid Uid %s", uid)) } diff --git a/pkg/services/publicdashboards/api/middleware.go b/pkg/services/publicdashboards/api/middleware.go index 78a42c5cf23..1805ae9c358 100644 --- a/pkg/services/publicdashboards/api/middleware.go +++ b/pkg/services/publicdashboards/api/middleware.go @@ -6,7 +6,7 @@ import ( "github.com/grafana/grafana/pkg/infra/metrics" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/publicdashboards" - "github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens" + "github.com/grafana/grafana/pkg/services/publicdashboards/validation" "github.com/grafana/grafana/pkg/web" ) @@ -14,7 +14,7 @@ import ( func SetPublicDashboardOrgIdOnContext(publicDashboardService publicdashboards.Service) func(c *contextmodel.ReqContext) { return func(c *contextmodel.ReqContext) { accessToken, ok := web.Params(c.Req)[":accessToken"] - if !ok || !tokens.IsValidAccessToken(accessToken) { + if !ok || !validation.IsValidAccessToken(accessToken) { return } @@ -45,7 +45,7 @@ func RequiresExistingAccessToken(publicDashboardService publicdashboards.Service return } - if !tokens.IsValidAccessToken(accessToken) { + if !validation.IsValidAccessToken(accessToken) { c.JsonApiErr(http.StatusBadRequest, "Invalid access token", nil) } diff --git a/pkg/services/publicdashboards/api/middleware_test.go b/pkg/services/publicdashboards/api/middleware_test.go index b0462107cf4..3e6f9811705 100644 --- a/pkg/services/publicdashboards/api/middleware_test.go +++ b/pkg/services/publicdashboards/api/middleware_test.go @@ -10,7 +10,7 @@ import ( contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/publicdashboards" - "github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens" + "github.com/grafana/grafana/pkg/services/publicdashboards/service" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/web" "github.com/stretchr/testify/assert" @@ -18,7 +18,7 @@ import ( "github.com/stretchr/testify/require" ) -var validAccessToken, _ = tokens.GenerateAccessToken() +var validAccessToken, _ = service.GenerateAccessToken() func TestRequiresExistingAccessToken(t *testing.T) { tests := []struct { diff --git a/pkg/services/publicdashboards/api/query.go b/pkg/services/publicdashboards/api/query.go index 98a3172c760..4f4583d621f 100644 --- a/pkg/services/publicdashboards/api/query.go +++ b/pkg/services/publicdashboards/api/query.go @@ -8,8 +8,8 @@ import ( "github.com/grafana/grafana/pkg/api/response" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" - "github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" + "github.com/grafana/grafana/pkg/services/publicdashboards/validation" "github.com/grafana/grafana/pkg/web" ) @@ -17,7 +17,7 @@ import ( // GET /api/public/dashboards/:accessToken func (api *Api) ViewPublicDashboard(c *contextmodel.ReqContext) response.Response { accessToken := web.Params(c.Req)[":accessToken"] - if !tokens.IsValidAccessToken(accessToken) { + if !validation.IsValidAccessToken(accessToken) { return response.Err(ErrInvalidAccessToken.Errorf("ViewPublicDashboard: invalid access token")) } @@ -55,7 +55,7 @@ func (api *Api) ViewPublicDashboard(c *contextmodel.ReqContext) response.Respons // POST /api/public/dashboard/:accessToken/panels/:panelId/query func (api *Api) QueryPublicDashboard(c *contextmodel.ReqContext) response.Response { accessToken := web.Params(c.Req)[":accessToken"] - if !tokens.IsValidAccessToken(accessToken) { + if !validation.IsValidAccessToken(accessToken) { return response.Err(ErrInvalidAccessToken.Errorf("QueryPublicDashboard: invalid access token")) } @@ -81,7 +81,7 @@ func (api *Api) QueryPublicDashboard(c *contextmodel.ReqContext) response.Respon // GET /api/public/dashboards/:accessToken/annotations func (api *Api) GetAnnotations(c *contextmodel.ReqContext) response.Response { accessToken := web.Params(c.Req)[":accessToken"] - if !tokens.IsValidAccessToken(accessToken) { + if !validation.IsValidAccessToken(accessToken) { return response.Err(ErrInvalidAccessToken.Errorf("GetAnnotations: invalid access token")) } diff --git a/pkg/services/publicdashboards/database/database_test.go b/pkg/services/publicdashboards/database/database_test.go index ba4a6eab5ad..d8da2b866f7 100644 --- a/pkg/services/publicdashboards/database/database_test.go +++ b/pkg/services/publicdashboards/database/database_test.go @@ -10,8 +10,8 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" dashboardsDB "github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" + "github.com/grafana/grafana/pkg/services/publicdashboards/service" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/setting" @@ -693,7 +693,7 @@ func insertPublicDashboard(t *testing.T, publicdashboardStore *PublicDashboardSt uid := util.GenerateShortUID() - accessToken, err := tokens.GenerateAccessToken() + accessToken, err := service.GenerateAccessToken() require.NoError(t, err) cmd := SavePublicDashboardCommand{ diff --git a/pkg/services/publicdashboards/internal/tokens/tokens.go b/pkg/services/publicdashboards/internal/tokens/tokens.go deleted file mode 100644 index 4d4f3accf0d..00000000000 --- a/pkg/services/publicdashboards/internal/tokens/tokens.go +++ /dev/null @@ -1,29 +0,0 @@ -package tokens - -import ( - "fmt" - - "github.com/google/uuid" - "github.com/grafana/grafana/pkg/util" -) - -// GenerateAccessToken generates an uuid formatted without dashes to use as access token -func GenerateAccessToken() (string, error) { - token, err := uuid.NewRandom() - if err != nil { - return "", err - } - return fmt.Sprintf("%x", token[:]), nil -} - -// IsValidAccessToken asserts that an accessToken is a valid uuid -func IsValidAccessToken(token string) bool { - _, err := uuid.Parse(token) - return err == nil -} - -// IsValidShortUID checks that the uid is not blank and contains valid -// characters. Wraps utils.IsValidShortUID -func IsValidShortUID(uid string) bool { - return uid != "" && util.IsValidShortUID(uid) -} diff --git a/pkg/services/publicdashboards/internal/tokens/tokens_test.go b/pkg/services/publicdashboards/internal/tokens/tokens_test.go deleted file mode 100644 index b04ba221f10..00000000000 --- a/pkg/services/publicdashboards/internal/tokens/tokens_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package tokens - -import ( - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGenerateAccessToken(t *testing.T) { - accessToken, err := GenerateAccessToken() - - t.Run("length", func(t *testing.T) { - require.NoError(t, err) - assert.Equal(t, 32, len(accessToken)) - }) - - t.Run("no - ", func(t *testing.T) { - assert.False(t, strings.Contains("-", accessToken)) - }) -} - -func TestValidAccessToken(t *testing.T) { - t.Run("true", func(t *testing.T) { - uuid, _ := GenerateAccessToken() - assert.True(t, IsValidAccessToken(uuid)) - }) - - t.Run("false when blank", func(t *testing.T) { - assert.False(t, IsValidAccessToken("")) - }) - - t.Run("false when can't be parsed by uuid lib", func(t *testing.T) { - // too long - assert.False(t, IsValidAccessToken("0123456789012345678901234567890123456789")) - }) -} - -// we just check base cases since this wraps utils.IsValidShortUID which has -// test coverage -func TestValidUid(t *testing.T) { - t.Run("true", func(t *testing.T) { - assert.True(t, IsValidShortUID("afqrz7jZZ")) - }) - - t.Run("false when blank", func(t *testing.T) { - assert.False(t, IsValidShortUID("")) - }) - - t.Run("false when invalid chars", func(t *testing.T) { - assert.False(t, IsValidShortUID("afqrz7j%%")) - }) -} diff --git a/pkg/services/publicdashboards/public_dashboard_service_mock.go b/pkg/services/publicdashboards/public_dashboard_service_mock.go index 883c51e1b80..3fcf2bf2f87 100644 --- a/pkg/services/publicdashboards/public_dashboard_service_mock.go +++ b/pkg/services/publicdashboards/public_dashboard_service_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.16.0. DO NOT EDIT. +// Code generated by mockery v2.14.0. DO NOT EDIT. package publicdashboards @@ -102,6 +102,29 @@ func (_m *FakePublicDashboardService) ExistsEnabledByDashboardUid(ctx context.Co return r0, r1 } +// Find provides a mock function with given fields: ctx, uid +func (_m *FakePublicDashboardService) Find(ctx context.Context, uid string) (*models.PublicDashboard, error) { + ret := _m.Called(ctx, uid) + + var r0 *models.PublicDashboard + if rf, ok := ret.Get(0).(func(context.Context, string) *models.PublicDashboard); ok { + r0 = rf(ctx, uid) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.PublicDashboard) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, uid) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // FindAll provides a mock function with given fields: ctx, u, orgId func (_m *FakePublicDashboardService) FindAll(ctx context.Context, u *user.SignedInUser, orgId int64) ([]models.PublicDashboardListResponse, error) { ret := _m.Called(ctx, u, orgId) diff --git a/pkg/services/publicdashboards/publicdashboard.go b/pkg/services/publicdashboards/publicdashboard.go index b301e9be8ad..017c37f4152 100644 --- a/pkg/services/publicdashboards/publicdashboard.go +++ b/pkg/services/publicdashboards/publicdashboard.go @@ -21,6 +21,7 @@ type Service interface { FindAnnotations(ctx context.Context, reqDTO AnnotationsQueryDTO, accessToken string) ([]AnnotationEvent, error) FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*dashboards.Dashboard, error) FindAll(ctx context.Context, u *user.SignedInUser, orgId int64) ([]PublicDashboardListResponse, error) + Find(ctx context.Context, uid string) (*PublicDashboard, error) Create(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardDTO) (*PublicDashboard, error) Update(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardDTO) (*PublicDashboard, error) Delete(ctx context.Context, orgId int64, uid string) error diff --git a/pkg/services/publicdashboards/service/service.go b/pkg/services/publicdashboards/service/service.go index 812257d6850..2d557248e4e 100644 --- a/pkg/services/publicdashboards/service/service.go +++ b/pkg/services/publicdashboards/service/service.go @@ -3,15 +3,16 @@ package service import ( "context" "errors" + "fmt" "time" + "github.com/google/uuid" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/publicdashboards" - "github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/grafana/grafana/pkg/services/publicdashboards/validation" "github.com/grafana/grafana/pkg/services/query" @@ -60,6 +61,14 @@ func ProvideService( } } +func (pd *PublicDashboardServiceImpl) Find(ctx context.Context, uid string) (*PublicDashboard, error) { + pubdash, err := pd.store.Find(ctx, uid) + if err != nil { + return nil, ErrInternalServerError.Errorf("Find: failed to find public dashboard%w", err) + } + return pubdash, nil +} + // FindDashboard Gets a dashboard by Uid func (pd *PublicDashboardServiceImpl) FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*dashboards.Dashboard, error) { dash, err := pd.store.FindDashboard(ctx, orgId, dashboardUid) @@ -281,7 +290,7 @@ func (pd *PublicDashboardServiceImpl) NewPublicDashboardAccessToken(ctx context. var accessToken string for i := 0; i < 3; i++ { var err error - accessToken, err = tokens.GenerateAccessToken() + accessToken, err = GenerateAccessToken() if err != nil { continue } @@ -396,3 +405,12 @@ func publicDashboardIsEnabledChanged(existingPubdash *PublicDashboard, newPubdas isEnabledChanged := existingPubdash != nil && newPubdash.IsEnabled != existingPubdash.IsEnabled return newDashCreated || isEnabledChanged } + +// GenerateAccessToken generates an uuid formatted without dashes to use as access token +func GenerateAccessToken() (string, error) { + token, err := uuid.NewRandom() + if err != nil { + return "", err + } + return fmt.Sprintf("%x", token[:]), nil +} diff --git a/pkg/services/publicdashboards/service/service_test.go b/pkg/services/publicdashboards/service/service_test.go index ebc16b51d80..8a1acd1389b 100644 --- a/pkg/services/publicdashboards/service/service_test.go +++ b/pkg/services/publicdashboards/service/service_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "strings" "testing" "time" @@ -18,8 +19,8 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" . "github.com/grafana/grafana/pkg/services/publicdashboards" "github.com/grafana/grafana/pkg/services/publicdashboards/database" - "github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" + "github.com/grafana/grafana/pkg/services/publicdashboards/validation" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/serviceaccounts/tests" "github.com/grafana/grafana/pkg/services/tag/tagimpl" @@ -914,7 +915,7 @@ func TestPublicDashboardServiceImpl_NewPublicDashboardAccessToken(t *testing.T) if err == nil { assert.NotEqual(t, got, tt.want, "NewPublicDashboardAccessToken(%v)", tt.args.ctx) - assert.True(t, tokens.IsValidAccessToken(got), "NewPublicDashboardAccessToken(%v)", tt.args.ctx) + assert.True(t, validation.IsValidAccessToken(got), "NewPublicDashboardAccessToken(%v)", tt.args.ctx) store.AssertNumberOfCalls(t, "FindByAccessToken", 1) } else { store.AssertNumberOfCalls(t, "FindByAccessToken", 3) @@ -1028,3 +1029,16 @@ func insertTestDashboard(t *testing.T, dashboardStore *dashboardsDB.DashboardSto dash.Data.Set("uid", dash.UID) return dash } + +func TestGenerateAccessToken(t *testing.T) { + accessToken, err := GenerateAccessToken() + + t.Run("length", func(t *testing.T) { + require.NoError(t, err) + assert.Equal(t, 32, len(accessToken)) + }) + + t.Run("no - ", func(t *testing.T) { + assert.False(t, strings.Contains("-", accessToken)) + }) +} diff --git a/pkg/services/publicdashboards/validation/validation.go b/pkg/services/publicdashboards/validation/validation.go index 6f2c57c83dd..2094cf3545d 100644 --- a/pkg/services/publicdashboards/validation/validation.go +++ b/pkg/services/publicdashboards/validation/validation.go @@ -1,9 +1,11 @@ package validation import ( + "github.com/google/uuid" "github.com/grafana/grafana/pkg/services/dashboards" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/grafana/grafana/pkg/tsdb/legacydata" + "github.com/grafana/grafana/pkg/util" ) func ValidatePublicDashboard(dto *SavePublicDashboardDTO, dashboard *dashboards.Dashboard) error { @@ -44,3 +46,15 @@ func ValidateQueryPublicDashboardRequest(req PublicDashboardQueryDTO, pd *Public return nil } + +// IsValidAccessToken asserts that an accessToken is a valid uuid +func IsValidAccessToken(token string) bool { + _, err := uuid.Parse(token) + return err == nil +} + +// IsValidShortUID checks that the uid is not blank and contains valid +// characters. Wraps utils.IsValidShortUID +func IsValidShortUID(uid string) bool { + return uid != "" && util.IsValidShortUID(uid) +} diff --git a/pkg/services/publicdashboards/validation/validation_test.go b/pkg/services/publicdashboards/validation/validation_test.go index 4ee281e20fb..5ba9dcd8c30 100644 --- a/pkg/services/publicdashboards/validation/validation_test.go +++ b/pkg/services/publicdashboards/validation/validation_test.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/dashboards" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -157,3 +158,35 @@ func TestValidateQueryPublicDashboardRequest(t *testing.T) { }) } } + +func TestValidAccessToken(t *testing.T) { + t.Run("true", func(t *testing.T) { + uuid := "da82510c2aa64d78a2e87fef36c58e89" + assert.True(t, IsValidAccessToken(uuid)) + }) + + t.Run("false when blank", func(t *testing.T) { + assert.False(t, IsValidAccessToken("")) + }) + + t.Run("false when can't be parsed by uuid lib", func(t *testing.T) { + // too long + assert.False(t, IsValidAccessToken("0123456789012345678901234567890123456789")) + }) +} + +// we just check base cases since this wraps utils.IsValidShortUID which has +// test coverage +func TestValidUid(t *testing.T) { + t.Run("true", func(t *testing.T) { + assert.True(t, IsValidShortUID("afqrz7jZZ")) + }) + + t.Run("false when blank", func(t *testing.T) { + assert.False(t, IsValidShortUID("")) + }) + + t.Run("false when invalid chars", func(t *testing.T) { + assert.False(t, IsValidShortUID("afqrz7j%%")) + }) +} From 5bdfbd93a50af34623375455e6a92194449e4fb1 Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Fri, 27 Jan 2023 14:33:48 +0000 Subject: [PATCH 044/117] Bump code coverage version (#62322) * Bump code coverage version * Update codeowners to include aws-plugins * Bump version further --- .github/CODEOWNERS | 2 +- .github/workflows/cloud-data-sources-code-coverage.yml | 2 +- .github/workflows/ox-code-coverage.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index fd06ecb38d9..ae1b22f9be3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -571,7 +571,7 @@ embed.go @grafana/grafana-as-code /.github/workflows/backport.yml @grafana/grafana-release-eng /.github/workflows/bump-version.yml @grafana/grafana-release-eng /.github/workflows/close-milestone.yml @grafana/grafana-release-eng -/.github/workflows/cloud-data-sources-code-coverage.yml @grafana/partner-plugins +/.github/workflows/cloud-data-sources-code-coverage.yml @grafana/partner-plugins @grafana/aws-plugins /.github/workflows/codeowners-validator.yml @tolzhabayev /.github/workflows/codeql-analysis.yml @DanCech /.github/workflows/commands.yml @torkelo diff --git a/.github/workflows/cloud-data-sources-code-coverage.yml b/.github/workflows/cloud-data-sources-code-coverage.yml index a69cb01601a..bb27cb086de 100644 --- a/.github/workflows/cloud-data-sources-code-coverage.yml +++ b/.github/workflows/cloud-data-sources-code-coverage.yml @@ -14,7 +14,7 @@ on: jobs: workflow-call: - uses: grafana/code-coverage/.github/workflows/code-coverage.yml@v0.1.15 + uses: grafana/code-coverage/.github/workflows/code-coverage.yml@v0.1.17 with: frontend-path-regexp: public\/app\/plugins\/datasource\/(grafana-azure-monitor-datasource|cloud-monitoring|cloudwatch) backend-path-regexp: pkg\/tsdb\/(azuremonitor|cloudmonitoring|cloudwatch) diff --git a/.github/workflows/ox-code-coverage.yml b/.github/workflows/ox-code-coverage.yml index e852c06570b..7413350f646 100644 --- a/.github/workflows/ox-code-coverage.yml +++ b/.github/workflows/ox-code-coverage.yml @@ -15,7 +15,7 @@ on: jobs: workflow-call: - uses: grafana/code-coverage/.github/workflows/code-coverage.yml@v0.1.15 + uses: grafana/code-coverage/.github/workflows/code-coverage.yml@v0.1.17 with: frontend-path-regexp: public\/app\/features\/(explore|correlations)|public\/app\/plugins\/datasource\/(loki|elasticsearch) backend-path-regexp: pkg\/services\/(queryhistory)|pkg\/tsdb\/(loki|elasticsearch) From df2db3bb2305109e7ead8e011f335b941a893622 Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Fri, 27 Jan 2023 15:41:16 +0100 Subject: [PATCH 045/117] LogContext: Fix setting wrong height for ElasticSearch (#62330) fix wrong height for elastic --- public/app/features/logs/components/LogRowContext.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/public/app/features/logs/components/LogRowContext.tsx b/public/app/features/logs/components/LogRowContext.tsx index 1ba24038979..9c3e76283fa 100644 --- a/public/app/features/logs/components/LogRowContext.tsx +++ b/public/app/features/logs/components/LogRowContext.tsx @@ -35,9 +35,7 @@ interface LogRowContextProps { } const getLogRowContextStyles = (theme: GrafanaTheme2, wrapLogMessage?: boolean, datasourceUiHeight?: number) => { - if (config.featureToggles.logsContextDatasourceUi) { - datasourceUiHeight = datasourceUiHeight ?? 55; - } else { + if (!config.featureToggles.logsContextDatasourceUi || !datasourceUiHeight) { datasourceUiHeight = 0; } /** @@ -178,7 +176,7 @@ const LogRowContextGroupHeader: React.FunctionComponent { - const [height, setHeight] = useState(50); + const [height, setHeight] = useState(0); const datasourceUiRef = React.createRef(); const { datasourceUi: dsUi, From 5531e22f4627d4c02f081690d25974bda42e47b7 Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Fri, 27 Jan 2023 16:05:25 +0100 Subject: [PATCH 046/117] Auth: Add disable of team sync for JWT Authentication (#62191) * fix: disable team sync for JWT Authentication * add: comment to test * change test to conform to new expected behavior * fix: spelling * formatting --- pkg/models/user_auth.go | 1 + pkg/services/authn/clients/jwt.go | 5 +-- pkg/services/authn/clients/jwt_test.go | 2 +- pkg/services/contexthandler/auth_jwt.go | 3 ++ .../login/loginservice/loginservice.go | 3 +- .../login/loginservice/loginservice_test.go | 35 +++++++++++++++---- 6 files changed, 39 insertions(+), 10 deletions(-) diff --git a/pkg/models/user_auth.go b/pkg/models/user_auth.go index bebdfdccf61..7b2697778dc 100644 --- a/pkg/models/user_auth.go +++ b/pkg/models/user_auth.go @@ -37,6 +37,7 @@ type ExternalUserInfo struct { OrgRoles map[int64]org.RoleType IsGrafanaAdmin *bool // This is a pointer to know if we should sync this or not (nil = ignore sync) IsDisabled bool + SkipTeamSync bool } func (e *ExternalUserInfo) String() string { diff --git a/pkg/services/authn/clients/jwt.go b/pkg/services/authn/clients/jwt.go index 79259943af1..8ad47e3105e 100644 --- a/pkg/services/authn/clients/jwt.go +++ b/pkg/services/authn/clients/jwt.go @@ -69,8 +69,9 @@ func (s *JWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identi AuthID: sub, OrgRoles: map[int64]org.RoleType{}, ClientParams: authn.ClientParams{ - SyncUser: true, - SyncTeamMembers: true, + SyncUser: true, + // We do not allow team member sync from JWT Authentication + SyncTeamMembers: false, AllowSignUp: s.cfg.JWTAuthAutoSignUp, EnableDisabledUsers: false, }} diff --git a/pkg/services/authn/clients/jwt_test.go b/pkg/services/authn/clients/jwt_test.go index 80df9289092..d95876fd4f6 100644 --- a/pkg/services/authn/clients/jwt_test.go +++ b/pkg/services/authn/clients/jwt_test.go @@ -49,9 +49,9 @@ func TestAuthenticateJWT(t *testing.T) { IsDisabled: false, HelpFlags1: 0, ClientParams: authn.ClientParams{ + SyncTeamMembers: false, SyncUser: true, AllowSignUp: true, - SyncTeamMembers: true, LookUpParams: models.UserLookupParams{ UserID: nil, Email: stringPtr("eai.doe@cor.po"), diff --git a/pkg/services/contexthandler/auth_jwt.go b/pkg/services/contexthandler/auth_jwt.go index 68558418a4d..24f7a2c0a22 100644 --- a/pkg/services/contexthandler/auth_jwt.go +++ b/pkg/services/contexthandler/auth_jwt.go @@ -61,10 +61,13 @@ func (h *ContextHandler) initContextWithJWT(ctx *contextmodel.ReqContext, orgId ctx.JsonApiErr(http.StatusUnauthorized, InvalidJWT, err) return true } + extUser := &models.ExternalUserInfo{ AuthModule: "jwt", AuthId: sub, OrgRoles: map[int64]org.RoleType{}, + // we do not want to sync team memberships from JWT authentication see - https://github.com/grafana/grafana/issues/62175 + SkipTeamSync: true, } if key := h.Cfg.JWTAuthUsernameClaim; key != "" { diff --git a/pkg/services/login/loginservice/loginservice.go b/pkg/services/login/loginservice/loginservice.go index 37f38af1373..89cc5351dae 100644 --- a/pkg/services/login/loginservice/loginservice.go +++ b/pkg/services/login/loginservice/loginservice.go @@ -154,7 +154,8 @@ func (ls *Implementation) UpsertUser(ctx context.Context, cmd *models.UpsertUser } } - if ls.TeamSync != nil { + // There are external providers where we want to completely skip team synchronization see - https://github.com/grafana/grafana/issues/62175 + if ls.TeamSync != nil && !extUser.SkipTeamSync { if errTeamSync := ls.TeamSync(cmd.Result, extUser); errTeamSync != nil { return errTeamSync } diff --git a/pkg/services/login/loginservice/loginservice_test.go b/pkg/services/login/loginservice/loginservice_test.go index 0798d744045..c7d5a4762bc 100644 --- a/pkg/services/login/loginservice/loginservice_test.go +++ b/pkg/services/login/loginservice/loginservice_test.go @@ -71,7 +71,7 @@ func Test_teamSync(t *testing.T) { } email := "test_user@example.org" - upserCmd := &models.UpsertUserCommand{ExternalUser: &models.ExternalUserInfo{Email: email}, + upsertCmd := &models.UpsertUserCommand{ExternalUser: &models.ExternalUserInfo{Email: email}, UserLookupParams: models.UserLookupParams{Email: &email}} expectedUser := &user.User{ ID: 1, @@ -84,8 +84,8 @@ func Test_teamSync(t *testing.T) { var actualUser *user.User var actualExternalUser *models.ExternalUserInfo - t.Run("login.TeamSync should not be called when nil", func(t *testing.T) { - err := login.UpsertUser(context.Background(), upserCmd) + t.Run("login.TeamSync should not be called when nil", func(t *testing.T) { + err := login.UpsertUser(context.Background(), upsertCmd) require.Nil(t, err) assert.Nil(t, actualUser) assert.Nil(t, actualExternalUser) @@ -97,10 +97,33 @@ func Test_teamSync(t *testing.T) { return nil } login.TeamSync = teamSyncFunc - err := login.UpsertUser(context.Background(), upserCmd) + err := login.UpsertUser(context.Background(), upsertCmd) require.Nil(t, err) assert.Equal(t, actualUser, expectedUser) - assert.Equal(t, actualExternalUser, upserCmd.ExternalUser) + assert.Equal(t, actualExternalUser, upsertCmd.ExternalUser) + }) + + t.Run("login.TeamSync should not be called when not nil and skipTeamSync is set for externalUserInfo", func(t *testing.T) { + var actualUser *user.User + var actualExternalUser *models.ExternalUserInfo + upsertCmdSkipTeamSync := &models.UpsertUserCommand{ + ExternalUser: &models.ExternalUserInfo{ + Email: email, + // sending in ExternalUserInfo with SkipTeamSync yields no team sync + SkipTeamSync: true, + }, + UserLookupParams: models.UserLookupParams{Email: &email}, + } + teamSyncFunc := func(user *user.User, externalUser *models.ExternalUserInfo) error { + actualUser = user + actualExternalUser = externalUser + return nil + } + login.TeamSync = teamSyncFunc + err := login.UpsertUser(context.Background(), upsertCmdSkipTeamSync) + require.Nil(t, err) + assert.Nil(t, actualUser) + assert.Nil(t, actualExternalUser) }) t.Run("login.TeamSync should propagate its errors to the caller", func(t *testing.T) { @@ -108,7 +131,7 @@ func Test_teamSync(t *testing.T) { return errors.New("teamsync test error") } login.TeamSync = teamSyncFunc - err := login.UpsertUser(context.Background(), upserCmd) + err := login.UpsertUser(context.Background(), upsertCmd) require.Error(t, err) }) }) From 83199c4bf51e953db7de0d702b00962eaf6ad9f4 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Fri, 27 Jan 2023 07:13:33 -0800 Subject: [PATCH 047/117] Dashboard schema: Review and mature timezone property (#62090) * Review and mature timezone property of Dashboard kind * Update timezone type --- .../kinds/core/dashboard/schema-reference.md | 2 +- kinds/dashboard/dashboard_kind.cue | 4 ++-- .../src/raw/dashboard/x/dashboard_types.gen.ts | 6 +++--- pkg/kinds/dashboard/dashboard_types_gen.go | 16 ++-------------- pkg/kindsys/report.json | 2 +- .../containers/PublicDashboardPage.test.tsx | 1 + .../state/__fixtures__/dashboardFixtures.ts | 1 + 7 files changed, 11 insertions(+), 21 deletions(-) diff --git a/docs/sources/developers/kinds/core/dashboard/schema-reference.md b/docs/sources/developers/kinds/core/dashboard/schema-reference.md index 5d91eef9451..5b4ba009ad0 100644 --- a/docs/sources/developers/kinds/core/dashboard/schema-reference.md +++ b/docs/sources/developers/kinds/core/dashboard/schema-reference.md @@ -34,7 +34,7 @@ title: Dashboard kind | `templating` | [object](#templating) | No | TODO docs | | `time` | [object](#time) | No | Time range for dashboard, e.g. last 6 hours, last 7 days, etc | | `timepicker` | [object](#timepicker) | No | TODO docs
TODO this appears to be spread all over in the frontend. Concepts will likely need tidying in tandem with schema changes | -| `timezone` | string | No | Timezone of dashboard, Possible values are: `browser`, `utc`, ``. Default: `browser`. | +| `timezone` | string | No | Timezone of dashboard. Accepts IANA TZDB zone ID or "browser" or "utc". Default: ``. | | `title` | string | No | Title of dashboard. | | `uid` | string | No | Unique dashboard identifier that can be generated by anyone. string (8-40) | | `version` | integer | No | Version of the dashboard, incremented each time the dashboard is updated. | diff --git a/kinds/dashboard/dashboard_kind.cue b/kinds/dashboard/dashboard_kind.cue index 8498d08b175..db1402c7476 100644 --- a/kinds/dashboard/dashboard_kind.cue +++ b/kinds/dashboard/dashboard_kind.cue @@ -30,8 +30,8 @@ lineage: seqs: [ tags?: [...string] @grafanamaturity(NeedsExpertReview) // Theme of dashboard. style: "light" | *"dark" @grafanamaturity(NeedsExpertReview) - // Timezone of dashboard, - timezone?: *"browser" | "utc" | "" @grafanamaturity(NeedsExpertReview) + // Timezone of dashboard. Accepts IANA TZDB zone ID or "browser" or "utc". + timezone?: string | *"" // Whether a dashboard is editable or not. editable: bool | *true // Configuration of dashboard cursor sync behavior. diff --git a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts index bbaaee013f0..ec61957cea2 100644 --- a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts +++ b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts @@ -777,9 +777,9 @@ export interface Dashboard { time_options: Array; }; /** - * Timezone of dashboard, + * Timezone of dashboard. Accepts IANA TZDB zone ID or "browser" or "utc". */ - timezone?: ('browser' | 'utc' | ''); + timezone?: string; /** * Title of dashboard. */ @@ -808,5 +808,5 @@ export const defaultDashboard: Partial = { schemaVersion: 36, style: 'dark', tags: [], - timezone: 'browser', + timezone: '', }; diff --git a/pkg/kinds/dashboard/dashboard_types_gen.go b/pkg/kinds/dashboard/dashboard_types_gen.go index 6a34f274e31..7ed218331cc 100644 --- a/pkg/kinds/dashboard/dashboard_types_gen.go +++ b/pkg/kinds/dashboard/dashboard_types_gen.go @@ -17,15 +17,6 @@ const ( StyleLight Style = "light" ) -// Defines values for Timezone. -const ( - TimezoneBrowser Timezone = "browser" - - TimezoneEmpty Timezone = "" - - TimezoneUtc Timezone = "utc" -) - // Defines values for CursorSync. const ( CursorSyncN0 CursorSync = 0 @@ -290,8 +281,8 @@ type Dashboard struct { TimeOptions []string `json:"time_options"` } `json:"timepicker,omitempty"` - // Timezone of dashboard, - Timezone *Timezone `json:"timezone,omitempty"` + // Timezone of dashboard. Accepts IANA TZDB zone ID or "browser" or "utc". + Timezone *string `json:"timezone,omitempty"` // Title of dashboard. Title *string `json:"title,omitempty"` @@ -309,9 +300,6 @@ type Dashboard struct { // Theme of dashboard. type Style string -// Timezone of dashboard, -type Timezone string - // 0 for no shared crosshair or tooltip (default). // 1 for shared crosshair. // 2 for shared crosshair AND shared tooltip. diff --git a/pkg/kindsys/report.json b/pkg/kindsys/report.json index d9fd1cf56d6..d6f4b88460e 100644 --- a/pkg/kindsys/report.json +++ b/pkg/kindsys/report.json @@ -283,7 +283,7 @@ 0, 0 ], - "grafanaMaturityCount": 140, + "grafanaMaturityCount": 139, "lineageIsGroup": false, "links": { "docs": "https://grafana.com/docs/grafana/next/developers/kinds/core/dashboard/schema-reference", diff --git a/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx b/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx index d517a64c403..f012e8886af 100644 --- a/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx +++ b/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx @@ -93,6 +93,7 @@ const getTestDashboard = (overrides?: Partial, metaOverrides?: Partia schemaVersion: 1, style: 'dark', timepicker: { hidden: true }, + timezone: '', panels: [ { id: 1, diff --git a/public/app/features/dashboard/state/__fixtures__/dashboardFixtures.ts b/public/app/features/dashboard/state/__fixtures__/dashboardFixtures.ts index 147f51aa03d..20cc4af46e1 100644 --- a/public/app/features/dashboard/state/__fixtures__/dashboardFixtures.ts +++ b/public/app/features/dashboard/state/__fixtures__/dashboardFixtures.ts @@ -24,6 +24,7 @@ export function createDashboardModelFixture( schemaVersion: 1, revision: 1, style: 'dark', + timezone: '', ...dashboardInput, }; From d5433a488a4bc0af81a5853a83e3eb0ed3002bf7 Mon Sep 17 00:00:00 2001 From: Ezequiel Victorero Date: Fri, 27 Jan 2023 12:17:18 -0300 Subject: [PATCH 048/117] Chore: Update code owners of public dashboards (#62332) --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ae1b22f9be3..84dec49745d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -112,7 +112,7 @@ WORKFLOW.md @torkelo /pkg/services/pluginsettings/ @grafana/backend-platform /pkg/services/preference/ @grafana/backend-platform /pkg/services/provisioning/ @grafana/backend-platform -/pkg/services/publicdashboards/ @grafana/backend-platform +/pkg/services/publicdashboards/ @grafana/dashboards-squad /pkg/services/query/ @grafana/backend-platform /pkg/services/queryhistory/ @grafana/backend-platform /pkg/services/quota/ @grafana/backend-platform From 88119ad6c32a781ac21737db92dc8734f143daa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Fri, 27 Jan 2023 16:18:36 +0100 Subject: [PATCH 049/117] Elasticsearch: Support nested aggregation (#62301) * Add nested query support * Add nested support for alerts * update nested aggregation * cleanup types * Add nested integration test * Move aggdef to nested * fixed merge conflict * fixed lint warning * mark nested-mode experimental --------- Co-authored-by: Ethan Gallant Co-authored-by: Ethan J. Gallant --- pkg/tsdb/elasticsearch/client/models.go | 5 +++++ .../elasticsearch/client/search_request.go | 21 +++++++++++++++++++ pkg/tsdb/elasticsearch/response_parser.go | 8 +++++++ pkg/tsdb/elasticsearch/time_series_query.go | 10 +++++++++ .../elasticsearch/ElasticResponse.ts | 5 +++++ .../elasticsearch/QueryBuilder.test.ts | 17 +++++++++++++++ .../datasource/elasticsearch/QueryBuilder.ts | 4 ++++ .../BucketAggregationsEditor/aggregations.ts | 10 +++++++-- .../BucketAggregationsEditor/utils.ts | 5 +++++ 9 files changed, 83 insertions(+), 2 deletions(-) diff --git a/pkg/tsdb/elasticsearch/client/models.go b/pkg/tsdb/elasticsearch/client/models.go index 8ac01ea8620..b2e212ba720 100644 --- a/pkg/tsdb/elasticsearch/client/models.go +++ b/pkg/tsdb/elasticsearch/client/models.go @@ -236,6 +236,11 @@ type TermsAggregation struct { Missing *string `json:"missing,omitempty"` } +// NestedAggregation represents a nested aggregation +type NestedAggregation struct { + Path string `json:"path"` +} + // ExtendedBounds represents extended bounds type ExtendedBounds struct { Min int64 `json:"min"` diff --git a/pkg/tsdb/elasticsearch/client/search_request.go b/pkg/tsdb/elasticsearch/client/search_request.go index f4a4ce97755..e3122e5974b 100644 --- a/pkg/tsdb/elasticsearch/client/search_request.go +++ b/pkg/tsdb/elasticsearch/client/search_request.go @@ -270,6 +270,7 @@ type AggBuilder interface { Histogram(key, field string, fn func(a *HistogramAgg, b AggBuilder)) AggBuilder DateHistogram(key, field string, fn func(a *DateHistogramAgg, b AggBuilder)) AggBuilder Terms(key, field string, fn func(a *TermsAggregation, b AggBuilder)) AggBuilder + Nested(key, path string, fn func(a *NestedAggregation, b AggBuilder)) AggBuilder Filters(key string, fn func(a *FiltersAggregation, b AggBuilder)) AggBuilder GeoHashGrid(key, field string, fn func(a *GeoHashGridAggregation, b AggBuilder)) AggBuilder Metric(key, metricType, field string, fn func(a *MetricAggregation)) AggBuilder @@ -382,6 +383,26 @@ func (b *aggBuilderImpl) Terms(key, field string, fn func(a *TermsAggregation, b return b } +func (b *aggBuilderImpl) Nested(key, field string, fn func(a *NestedAggregation, b AggBuilder)) AggBuilder { + innerAgg := &NestedAggregation{ + Path: field, + } + aggDef := newAggDef(key, &aggContainer{ + Type: "nested", + Aggregation: innerAgg, + }) + + if fn != nil { + builder := newAggBuilder() + aggDef.builders = append(aggDef.builders, builder) + fn(innerAgg, builder) + } + + b.aggDefs = append(b.aggDefs, aggDef) + + return b +} + func (b *aggBuilderImpl) Filters(key string, fn func(a *FiltersAggregation, b AggBuilder)) AggBuilder { innerAgg := &FiltersAggregation{ Filters: make(map[string]interface{}), diff --git a/pkg/tsdb/elasticsearch/response_parser.go b/pkg/tsdb/elasticsearch/response_parser.go index 50b6adcd822..5c154f9c09c 100644 --- a/pkg/tsdb/elasticsearch/response_parser.go +++ b/pkg/tsdb/elasticsearch/response_parser.go @@ -22,6 +22,7 @@ const ( topMetricsType = "top_metrics" // Bucket types dateHistType = "date_histogram" + nestedType = "nested" histogramType = "histogram" filtersType = "filters" termsType = "terms" @@ -84,6 +85,13 @@ func processBuckets(aggs map[string]interface{}, target *Query, if aggDef == nil { continue } + if aggDef.Type == nestedType { + err = processBuckets(esAgg.MustMap(), target, queryResult, props, depth+1) + if err != nil { + return err + } + continue + } if depth == maxDepth { if aggDef.Type == dateHistType { diff --git a/pkg/tsdb/elasticsearch/time_series_query.go b/pkg/tsdb/elasticsearch/time_series_query.go index d1e50e5f169..2e88a92e3b9 100644 --- a/pkg/tsdb/elasticsearch/time_series_query.go +++ b/pkg/tsdb/elasticsearch/time_series_query.go @@ -244,6 +244,14 @@ func addTermsAgg(aggBuilder es.AggBuilder, bucketAgg *BucketAgg, metrics []*Metr return aggBuilder } +func addNestedAgg(aggBuilder es.AggBuilder, bucketAgg *BucketAgg) es.AggBuilder { + aggBuilder.Nested(bucketAgg.ID, bucketAgg.Field, func(a *es.NestedAggregation, b es.AggBuilder) { + aggBuilder = b + }) + + return aggBuilder +} + func addFiltersAgg(aggBuilder es.AggBuilder, bucketAgg *BucketAgg) es.AggBuilder { filters := make(map[string]interface{}) for _, filter := range bucketAgg.Settings.Get("filters").MustArray() { @@ -361,6 +369,8 @@ func processTimeSeriesQuery(q *Query, b *es.SearchRequestBuilder, from, to int64 aggBuilder = addTermsAgg(aggBuilder, bucketAgg, q.Metrics) case geohashGridType: aggBuilder = addGeoHashGridAgg(aggBuilder, bucketAgg) + case nestedType: + aggBuilder = addNestedAgg(aggBuilder, bucketAgg) } } diff --git a/public/app/plugins/datasource/elasticsearch/ElasticResponse.ts b/public/app/plugins/datasource/elasticsearch/ElasticResponse.ts index 665d5ab3c33..51cfb3c0883 100644 --- a/public/app/plugins/datasource/elasticsearch/ElasticResponse.ts +++ b/public/app/plugins/datasource/elasticsearch/ElasticResponse.ts @@ -288,6 +288,11 @@ export class ElasticResponse { continue; } + if (aggDef.type === 'nested') { + this.processBuckets(esAgg, target, seriesList, table, props, depth + 1); + continue; + } + if (depth === maxDepth) { if (aggDef.type === 'date_histogram') { this.processMetrics(esAgg, target, seriesList, props); diff --git a/public/app/plugins/datasource/elasticsearch/QueryBuilder.test.ts b/public/app/plugins/datasource/elasticsearch/QueryBuilder.test.ts index b091f5a9c00..c9f8f06ae32 100644 --- a/public/app/plugins/datasource/elasticsearch/QueryBuilder.test.ts +++ b/public/app/plugins/datasource/elasticsearch/QueryBuilder.test.ts @@ -587,6 +587,23 @@ describe('ElasticQueryBuilder', () => { expect(firstLevel.histogram.min_doc_count).toBe('2'); }); + it('with nested', () => { + const query = builder.build({ + refId: 'A', + metrics: [{ id: '1', type: 'count' }], + bucketAggs: [ + { + type: 'nested', + field: 'nested_field', + id: '3', + }, + ], + }); + + const firstLevel = query.aggs['3']; + expect(firstLevel.nested.path).toBe('nested_field'); + }); + // This test wasn't migrated, as adhoc variables are going to be interpolated before // Or we need to add this to backend query builder (TBD) it('with adhoc filters', () => { diff --git a/public/app/plugins/datasource/elasticsearch/QueryBuilder.ts b/public/app/plugins/datasource/elasticsearch/QueryBuilder.ts index c79653169bc..ee236a425ca 100644 --- a/public/app/plugins/datasource/elasticsearch/QueryBuilder.ts +++ b/public/app/plugins/datasource/elasticsearch/QueryBuilder.ts @@ -283,6 +283,10 @@ export class ElasticQueryBuilder { }; break; } + case 'nested': { + esAgg['nested'] = { path: aggDef.field }; + break; + } } nestedAggs.aggs = nestedAggs.aggs || {}; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/aggregations.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/aggregations.ts index 5e230c26b19..a23bd5b58eb 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/aggregations.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/aggregations.ts @@ -1,6 +1,6 @@ import { bucketAggregationConfig } from './utils'; -export type BucketAggregationType = 'terms' | 'filters' | 'geohash_grid' | 'date_histogram' | 'histogram'; +export type BucketAggregationType = 'terms' | 'filters' | 'geohash_grid' | 'date_histogram' | 'histogram' | 'nested'; interface BaseBucketAggregation { id: string; @@ -62,7 +62,12 @@ interface GeoHashGrid extends BucketAggregationWithField { }; } -export type BucketAggregation = DateHistogram | Histogram | Terms | Filters | GeoHashGrid; +interface Nested extends BucketAggregationWithField { + type: 'nested'; + settings?: {}; +} + +export type BucketAggregation = DateHistogram | Histogram | Terms | Filters | GeoHashGrid | Nested; export const isBucketAggregationWithField = ( bucketAgg: BucketAggregation | BucketAggregationWithField @@ -74,6 +79,7 @@ export const BUCKET_AGGREGATION_TYPES: BucketAggregationType[] = [ 'terms', 'filters', 'geohash_grid', + 'nested', ]; export const isBucketAggregationType = (s: BucketAggregationType | string): s is BucketAggregationType => diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/utils.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/utils.ts index d37876d090e..60ecba4d87e 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/utils.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/utils.ts @@ -47,6 +47,11 @@ export const bucketAggregationConfig: BucketsConfiguration = { min_doc_count: '0', }, }, + nested: { + label: 'Nested (experimental)', + requiresField: true, + defaultSettings: {}, + }, }; export const orderByOptions: Array> = [ From 317ef1de86bc0d86975e15180067e99fce0e508c Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Fri, 27 Jan 2023 16:29:05 +0100 Subject: [PATCH 050/117] Azure Monitor: Add variable function to list regions (#62297) --- .../__mocks__/query.ts | 1 + .../azure_monitor_datasource.test.ts | 24 +++++++++++++++++++ .../azure_monitor/azure_monitor_datasource.ts | 1 + .../VariableEditor/VariableEditor.test.tsx | 16 +++++++++++++ .../VariableEditor/VariableEditor.tsx | 4 ++++ .../types/query.ts | 1 + .../variables.ts | 11 +++++++++ 7 files changed, 58 insertions(+) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/query.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/query.ts index 4b8d43f11a8..8cf4faa3f7b 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/query.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/query.ts @@ -41,6 +41,7 @@ export default function createMockQuery(overrides?: Partial): alias: '', // timeGrains: [], top: '10', + region: '', ...overrides?.azureMonitor, }, }; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts index 26275e04e74..8d5cdf15074 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts @@ -159,6 +159,30 @@ describe('AzureMonitorDatasource', () => { }, }); }); + + it('expand template variables for a region', () => { + const region = '$reg'; + templateSrv.init([ + { + id: 'reg', + name: 'reg', + current: { + value: `eastus`, + }, + }, + ]); + const query = createMockQuery({ + azureMonitor: { + region, + }, + }); + const templatedQuery = ctx.ds.azureMonitorDatasource.applyTemplateVariables(query, {}); + expect(templatedQuery).toMatchObject({ + azureMonitor: { + region: 'eastus', + }, + }); + }); }); describe('When performing getMetricNamespaces', () => { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts index 4a30a99c961..84d8b3bebf9 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts @@ -125,6 +125,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend { }) ); }); + + it('should run the query if requesting regions', async () => { + const onChange = jest.fn(); + const { rerender } = render(); + // wait for initial load + await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument()); + await selectAndRerender('select query type', 'Regions', onChange, rerender); + await selectAndRerender('select subscription', 'Primary Subscription', onChange, rerender); + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ + queryType: AzureQueryType.LocationsQuery, + subscription: 'sub', + refId: 'A', + }) + ); + }); }); }); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.tsx index 3b5feed71d0..44c77300584 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.tsx @@ -30,6 +30,7 @@ const VariableEditor = (props: Props) => { { label: 'Subscriptions', value: AzureQueryType.SubscriptionsQuery }, { label: 'Resource Groups', value: AzureQueryType.ResourceGroupsQuery }, { label: 'Namespaces', value: AzureQueryType.NamespacesQuery }, + { label: 'Regions', value: AzureQueryType.LocationsQuery }, { label: 'Resource Names', value: AzureQueryType.ResourceNamesQuery }, { label: 'Metric Names', value: AzureQueryType.MetricNamesQuery }, { label: 'Workspaces', value: AzureQueryType.WorkspacesQuery }, @@ -93,6 +94,9 @@ const VariableEditor = (props: Props) => { setRequireNamespace(true); setRequireResource(true); break; + case AzureQueryType.LocationsQuery: + setRequireSubscription(true); + break; } }, [queryType]); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/query.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/query.ts index 316091d5aa6..d0bd3547b77 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/query.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/query.ts @@ -12,6 +12,7 @@ export enum AzureQueryType { ResourceNamesQuery = 'Azure Resource Names', MetricNamesQuery = 'Azure Metric Names', WorkspacesQuery = 'Azure Workspaces', + LocationsQuery = 'Azure Locations', /** Deprecated */ GrafanaTemplateVariableFn = 'Grafana Template Variable Function', } diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/variables.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/variables.ts index 0fad891a40a..6acec60c9f8 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/variables.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/variables.ts @@ -108,6 +108,17 @@ export class VariableSupport extends CustomVariableSupport = []; + locationMap.forEach((loc) => { + res.push({ text: loc.displayName, value: loc.name }); + }); + return { + data: res?.length ? [toDataFrame(res)] : [], + }; + } default: request.targets[0] = queryObj; const queryResp = await lastValueFrom(this.datasource.query(request)); From 9453bec81999e95a82ab8d2cf7cee05b972c5f4c Mon Sep 17 00:00:00 2001 From: Levente Balogh Date: Fri, 27 Jan 2023 16:40:54 +0100 Subject: [PATCH 051/117] FileDropzone: Revert introducing a new prop (#62324) --- .../components/FileDropzone/FileDropzone.test.tsx | 6 ------ .../src/components/FileDropzone/FileDropzone.tsx | 13 ++----------- .../datasource/grafana/components/QueryEditor.tsx | 10 ++++------ 3 files changed, 6 insertions(+), 23 deletions(-) diff --git a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.test.tsx b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.test.tsx index 39dd8a23361..42ac0652b90 100644 --- a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.test.tsx +++ b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.test.tsx @@ -112,12 +112,6 @@ describe('The FileDropzone component', () => { expect(onDrop).toBeCalledWith([fileToUpload], [], expect.anything()); }); - it('should display the text generated by a custom primaryTextSupplier', async () => { - const customText = 'custom text from primaryTextSuplier'; - render( customText} />); - expect(await screen.findByText(customText)).toBeInTheDocument(); - }); - it('should show children inside the dropzone', () => { const component = ( diff --git a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx index 08acef7fc28..76f3ed99f74 100644 --- a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx +++ b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx @@ -46,7 +46,6 @@ export interface FileDropzoneProps { */ fileListRenderer?: (file: DropzoneFile, removeFile: (file: DropzoneFile) => void) => ReactNode; onFileRemove?: (file: DropzoneFile) => void; - primaryTextSupplier?: (files: DropzoneFile[], options?: BackwardsCompatibleDropzoneOptions) => string; } export interface DropzoneFile { @@ -58,15 +57,7 @@ export interface DropzoneFile { retryUpload?: () => void; } -export function FileDropzone({ - options, - primaryTextSupplier = getPrimaryText, - children, - readAs, - onLoad, - fileListRenderer, - onFileRemove, -}: FileDropzoneProps) { +export function FileDropzone({ options, children, readAs, onLoad, fileListRenderer, onFileRemove }: FileDropzoneProps) { const [files, setFiles] = useState([]); const [fileErrors, setErrorMessages] = useState([]); @@ -221,7 +212,7 @@ export function FileDropzone({
- {children ?? } + {children ?? }
{fileErrors.length > 0 && renderErrorMessages(fileErrors)} {options?.accept && ( diff --git a/public/app/plugins/datasource/grafana/components/QueryEditor.tsx b/public/app/plugins/datasource/grafana/components/QueryEditor.tsx index 23c16c1a213..edc9f18abb2 100644 --- a/public/app/plugins/datasource/grafana/components/QueryEditor.tsx +++ b/public/app/plugins/datasource/grafana/components/QueryEditor.tsx @@ -24,6 +24,7 @@ import { InlineFieldRow, InlineLabel, FileDropzone, + FileDropzoneDefaultChildren, DropzoneFile, Themeable2, withTheme2, @@ -68,10 +69,6 @@ export class UnthemedQueryEditor extends PureComponent { }, ]; - dropzoneTextSupplier = () => { - return this.props?.query?.file ? 'Replace file' : 'Upload file'; - }; - constructor(props: Props) { super(props); @@ -403,8 +400,9 @@ export class UnthemedQueryEditor extends PureComponent { fileListRenderer={this.fileListRenderer} options={{ onDropAccepted: this.onDropAccepted, maxSize: 200000, multiple: false }} onLoad={this.onFileDrop} - primaryTextSupplier={this.dropzoneTextSupplier} - > + > + + {file && (
{file?.name} From d0e95f8c9509eb6fe681283af63f938b51e78473 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Fri, 27 Jan 2023 16:41:40 +0100 Subject: [PATCH 052/117] Loki: Add X-Query-Tags header for logs sample and data sample (#62333) --- pkg/tsdb/loki/api.go | 22 +++++++++- pkg/tsdb/loki/api_test.go | 28 ++++++++++++- pkg/tsdb/loki/loki.go | 18 ++++----- pkg/tsdb/loki/parse_query.go | 40 ++++++++++++++----- pkg/tsdb/loki/types.go | 28 ++++++++----- .../datasource/loki/datasource.test.ts | 4 +- .../app/plugins/datasource/loki/datasource.ts | 3 +- public/app/plugins/datasource/loki/types.ts | 10 ++++- 8 files changed, 115 insertions(+), 38 deletions(-) diff --git a/pkg/tsdb/loki/api.go b/pkg/tsdb/loki/api.go index a8835da56af..e18752383d2 100644 --- a/pkg/tsdb/loki/api.go +++ b/pkg/tsdb/loki/api.go @@ -85,8 +85,11 @@ func makeDataRequest(ctx context.Context, lokiDsUrl string, query lokiQuery) (*h return nil, err } - if query.VolumeQuery { - req.Header.Set("X-Query-Tags", "Source=logvolhist") + if query.SupportingQueryType != SupportingQueryNone { + value := getSupportingQueryHeaderValue(req, query.SupportingQueryType) + if value != "" { + req.Header.Set("X-Query-Tags", "Source="+value) + } } return req, nil @@ -223,3 +226,18 @@ func (api *LokiAPI) RawQuery(ctx context.Context, resourcePath string) (RawLokiR return encodedBytes, nil } + +func getSupportingQueryHeaderValue(req *http.Request, supportingQueryType SupportingQueryType) string { + value := "" + switch supportingQueryType { + case SupportingQueryLogsVolume: + value = "logvolhist" + case SupportingQueryLogsSample: + value = "logsample" + case SupportingQueryDataSample: + value = "datasample" + default: //ignore + } + + return value +} diff --git a/pkg/tsdb/loki/api_test.go b/pkg/tsdb/loki/api_test.go index 2b64e383bf4..b28d159d895 100644 --- a/pkg/tsdb/loki/api_test.go +++ b/pkg/tsdb/loki/api_test.go @@ -28,7 +28,31 @@ func TestApiLogVolume(t *testing.T) { require.Equal(t, "Source=logvolhist", req.Header.Get("X-Query-Tags")) }) - _, err := api.DataQuery(context.Background(), lokiQuery{Expr: "", VolumeQuery: true, QueryType: QueryTypeRange}) + _, err := api.DataQuery(context.Background(), lokiQuery{Expr: "", SupportingQueryType: SupportingQueryLogsVolume, QueryType: QueryTypeRange}) + require.NoError(t, err) + require.True(t, called) + }) + + t.Run("logs sample queries should set logs sample http header", func(t *testing.T) { + called := false + api := makeMockedAPI(200, "application/json", response, func(req *http.Request) { + called = true + require.Equal(t, "Source=logsample", req.Header.Get("X-Query-Tags")) + }) + + _, err := api.DataQuery(context.Background(), lokiQuery{Expr: "", SupportingQueryType: SupportingQueryLogsSample, QueryType: QueryTypeRange}) + require.NoError(t, err) + require.True(t, called) + }) + + t.Run("data sample queries should set data sample http header", func(t *testing.T) { + called := false + api := makeMockedAPI(200, "application/json", response, func(req *http.Request) { + called = true + require.Equal(t, "Source=datasample", req.Header.Get("X-Query-Tags")) + }) + + _, err := api.DataQuery(context.Background(), lokiQuery{Expr: "", SupportingQueryType: SupportingQueryDataSample, QueryType: QueryTypeRange}) require.NoError(t, err) require.True(t, called) }) @@ -40,7 +64,7 @@ func TestApiLogVolume(t *testing.T) { require.Equal(t, "", req.Header.Get("X-Query-Tags")) }) - _, err := api.DataQuery(context.Background(), lokiQuery{Expr: "", VolumeQuery: false, QueryType: QueryTypeRange}) + _, err := api.DataQuery(context.Background(), lokiQuery{Expr: "", SupportingQueryType: SupportingQueryNone, QueryType: QueryTypeRange}) require.NoError(t, err) require.True(t, called) }) diff --git a/pkg/tsdb/loki/loki.go b/pkg/tsdb/loki/loki.go index 6925a91c25b..bb0147318a1 100644 --- a/pkg/tsdb/loki/loki.go +++ b/pkg/tsdb/loki/loki.go @@ -57,15 +57,15 @@ type datasourceInfo struct { } type QueryJSONModel struct { - QueryType string `json:"queryType"` - Expr string `json:"expr"` - Direction string `json:"direction"` - LegendFormat string `json:"legendFormat"` - Interval string `json:"interval"` - IntervalMS int `json:"intervalMS"` - Resolution int64 `json:"resolution"` - MaxLines int `json:"maxLines"` - VolumeQuery bool `json:"volumeQuery"` + QueryType string `json:"queryType"` + Expr string `json:"expr"` + Direction string `json:"direction"` + LegendFormat string `json:"legendFormat"` + Interval string `json:"interval"` + IntervalMS int `json:"intervalMS"` + Resolution int64 `json:"resolution"` + MaxLines int `json:"maxLines"` + SupportingQueryType string `json:"supportingQueryType"` } func parseQueryModel(raw json.RawMessage) (*QueryJSONModel, error) { diff --git a/pkg/tsdb/loki/parse_query.go b/pkg/tsdb/loki/parse_query.go index 80f0c0cbc27..5e4434bfce0 100644 --- a/pkg/tsdb/loki/parse_query.go +++ b/pkg/tsdb/loki/parse_query.go @@ -82,6 +82,21 @@ func parseDirection(jsonValue string) (Direction, error) { } } +func parseSupportingQueryType(jsonValue string) (SupportingQueryType, error) { + switch jsonValue { + case "logsVolume": + return SupportingQueryLogsVolume, nil + case "logsSample": + return SupportingQueryLogsSample, nil + case "dataSample": + return SupportingQueryDataSample, nil + case "": + return SupportingQueryNone, nil + default: + return SupportingQueryNone, fmt.Errorf("invalid supportingQueryType: %s", jsonValue) + } +} + func parseQuery(queryContext *backend.QueryDataRequest) ([]*lokiQuery, error) { qs := []*lokiQuery{} for _, query := range queryContext.Queries { @@ -115,17 +130,22 @@ func parseQuery(queryContext *backend.QueryDataRequest) ([]*lokiQuery, error) { return nil, err } + supportingQueryType, err := parseSupportingQueryType(model.SupportingQueryType) + if err != nil { + return nil, err + } + qs = append(qs, &lokiQuery{ - Expr: expr, - QueryType: queryType, - Direction: direction, - Step: step, - MaxLines: model.MaxLines, - LegendFormat: model.LegendFormat, - Start: start, - End: end, - RefID: query.RefID, - VolumeQuery: model.VolumeQuery, + Expr: expr, + QueryType: queryType, + Direction: direction, + Step: step, + MaxLines: model.MaxLines, + LegendFormat: model.LegendFormat, + Start: start, + End: end, + RefID: query.RefID, + SupportingQueryType: supportingQueryType, }) } diff --git a/pkg/tsdb/loki/types.go b/pkg/tsdb/loki/types.go index 9d5fcead26d..c2f1b130fb5 100644 --- a/pkg/tsdb/loki/types.go +++ b/pkg/tsdb/loki/types.go @@ -3,12 +3,20 @@ package loki import "time" type QueryType string +type SupportingQueryType string const ( QueryTypeRange QueryType = "range" QueryTypeInstant QueryType = "instant" ) +const ( + SupportingQueryLogsVolume SupportingQueryType = "logsVolume" + SupportingQueryLogsSample SupportingQueryType = "logsSample" + SupportingQueryDataSample SupportingQueryType = "dataSample" + SupportingQueryNone SupportingQueryType = "none" +) + type Direction string const ( @@ -17,14 +25,14 @@ const ( ) type lokiQuery struct { - Expr string - QueryType QueryType - Direction Direction - Step time.Duration - MaxLines int - LegendFormat string - Start time.Time - End time.Time - RefID string - VolumeQuery bool + Expr string + QueryType QueryType + Direction Direction + Step time.Duration + MaxLines int + LegendFormat string + Start time.Time + End time.Time + RefID string + SupportingQueryType SupportingQueryType } diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index f24469c5ddb..1b19a7acb6d 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -33,7 +33,7 @@ import { CustomVariableModel } from '../../../features/variables/types'; import { LokiDatasource, REF_ID_DATA_SAMPLES } from './datasource'; import { createLokiDatasource, createMetadataRequest } from './mocks'; import { parseToNodeNamesArray } from './queryUtils'; -import { LokiOptions, LokiQuery, LokiQueryType, LokiVariableQueryType } from './types'; +import { LokiOptions, LokiQuery, LokiQueryType, LokiVariableQueryType, SupportingQueryType } from './types'; import { LokiVariableSupport } from './variables'; jest.mock('@grafana/runtime', () => { @@ -981,7 +981,7 @@ describe('LokiDatasource', () => { instant: false, queryType: 'range', refId: 'log-volume-A', - volumeQuery: true, + supportingQueryType: SupportingQueryType.LogsVolume, }); }); diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index be825cd50a3..0d1ec4e37e6 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -86,6 +86,7 @@ import { LokiQueryType, LokiVariableQuery, LokiVariableQueryType, + SupportingQueryType, } from './types'; import { LokiVariableSupport } from './variables'; @@ -191,7 +192,7 @@ export class LokiDatasource ...normalizedQuery, refId: `${REF_ID_STARTER_LOG_VOLUME}${normalizedQuery.refId}`, instant: false, - volumeQuery: true, + supportingQueryType: SupportingQueryType.LogsVolume, expr: `sum by (level) (count_over_time(${expr}[$__interval]))`, }; diff --git a/public/app/plugins/datasource/loki/types.ts b/public/app/plugins/datasource/loki/types.ts index 1a75e42f2d6..d1339f659dc 100644 --- a/public/app/plugins/datasource/loki/types.ts +++ b/public/app/plugins/datasource/loki/types.ts @@ -42,8 +42,8 @@ export interface LokiQuery extends DataQuery { legendFormat?: string; maxLines?: number; resolution?: number; - /** Used in range queries */ - volumeQuery?: boolean; + /** Used only to identify supporting queries, e.g. logs volume, logs sample and data sample */ + supportingQueryType?: SupportingQueryType; /* @deprecated now use queryType */ range?: boolean; /* @deprecated now use queryType */ @@ -154,6 +154,12 @@ export interface LokiVariableQuery extends DataQuery { stream?: string; } +export enum SupportingQueryType { + LogsVolume = 'logsVolume', + LogsSample = 'logsSample', + DataSample = 'dataSample', +} + export interface ContextFilter { enabled: boolean; label: string; From 1865205d6823a5b7b39ec576e0ec7b16efadda44 Mon Sep 17 00:00:00 2001 From: Ieva Date: Fri, 27 Jan 2023 15:42:08 +0000 Subject: [PATCH 053/117] Benchmarks for searchv2 (#60730) * bench-test * cleanup * more simplification * fix tests * correct wrong argument ordering & use constant * fix issues with tests setup * add benchmark results * reuse Gabriel's concurrent setup code * correct error logs for ac benchmarks --- .../acimpl/service_bench_test.go | 68 +----- pkg/services/accesscontrol/actest/common.go | 61 +++++ pkg/services/searchV2/service_bench_test.go | 211 ++++++++++++++++++ 3 files changed, 277 insertions(+), 63 deletions(-) create mode 100644 pkg/services/accesscontrol/actest/common.go create mode 100644 pkg/services/searchV2/service_bench_test.go diff --git a/pkg/services/accesscontrol/acimpl/service_bench_test.go b/pkg/services/accesscontrol/acimpl/service_bench_test.go index 3d864a3aa28..2a12199eda2 100644 --- a/pkg/services/accesscontrol/acimpl/service_bench_test.go +++ b/pkg/services/accesscontrol/acimpl/service_bench_test.go @@ -3,13 +3,13 @@ package acimpl import ( "context" "fmt" - "sync" "testing" "time" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/actest" "github.com/grafana/grafana/pkg/services/accesscontrol/database" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" @@ -17,64 +17,6 @@ import ( "github.com/stretchr/testify/require" ) -const concurrency = 10 -const batchSize = 1000 - -type bounds struct { - start, end int -} - -// concurrentBatch spawns the requested amount of workers then ask them to run eachFn on chunks of the requested size -func concurrentBatch(workers, count, size int, eachFn func(start, end int) error) error { - var wg sync.WaitGroup - alldone := make(chan bool) // Indicates that all workers have finished working - chunk := make(chan bounds) // Gives the workers the bounds they should work with - ret := make(chan error) // Allow workers to notify in case of errors - defer close(ret) - - // Launch all workers - for x := 0; x < workers; x++ { - wg.Add(1) - go func() { - defer wg.Done() - for ck := range chunk { - if err := eachFn(ck.start, ck.end); err != nil { - ret <- err - return - } - } - }() - } - - go func() { - // Tell the workers the chunks they have to work on - for i := 0; i < count; { - end := i + size - if end > count { - end = count - } - - chunk <- bounds{start: i, end: end} - - i = end - } - close(chunk) - - // Wait for the workers - wg.Wait() - close(alldone) - }() - - // wait for an error or for all workers to be done - select { - case err := <-ret: - return err - case <-alldone: - break - } - return nil -} - // setupBenchEnv will create userCount users, userCount managed roles with resourceCount managed permission each // Example: setupBenchEnv(b, 2, 3): // - will create 2 users and assign them 2 managed roles @@ -102,7 +44,7 @@ func setupBenchEnv(b *testing.B, usersCount, resourceCount int) (accesscontrol.S require.NoError(b, err) // Populate users, roles and assignments - if errInsert := concurrentBatch(concurrency, usersCount, batchSize, func(start, end int) error { + if errInsert := actest.ConcurrentBatch(actest.Concurrency, usersCount, actest.BatchSize, func(start, end int) error { n := end - start users := make([]user.User, 0, n) orgUsers := make([]org.OrgUser, 0, n) @@ -157,13 +99,13 @@ func setupBenchEnv(b *testing.B, usersCount, resourceCount int) (accesscontrol.S }) return err }); errInsert != nil { - require.NoError(b, err, "could not insert users and roles") + require.NoError(b, errInsert, "could not insert users and roles") return nil, nil } // Populate permissions action2 := "resources:action2" - if errInsert := concurrentBatch(concurrency, resourceCount*usersCount, batchSize, func(start, end int) error { + if errInsert := actest.ConcurrentBatch(actest.Concurrency, resourceCount*usersCount, actest.BatchSize, func(start, end int) error { permissions := make([]accesscontrol.Permission, 0, end-start) for i := start; i < end; i++ { permissions = append(permissions, accesscontrol.Permission{ @@ -180,7 +122,7 @@ func setupBenchEnv(b *testing.B, usersCount, resourceCount int) (accesscontrol.S return err }) }); errInsert != nil { - require.NoError(b, err, "could not insert permissions") + require.NoError(b, errInsert, "could not insert permissions") return nil, nil } diff --git a/pkg/services/accesscontrol/actest/common.go b/pkg/services/accesscontrol/actest/common.go new file mode 100644 index 00000000000..5aa71efe08c --- /dev/null +++ b/pkg/services/accesscontrol/actest/common.go @@ -0,0 +1,61 @@ +package actest + +import "sync" + +const Concurrency = 10 +const BatchSize = 1000 + +type bounds struct { + start, end int +} + +// ConcurrentBatch spawns the requested amount of workers then ask them to run eachFn on chunks of the requested size +func ConcurrentBatch(workers, count, size int, eachFn func(start, end int) error) error { + var wg sync.WaitGroup + alldone := make(chan bool) // Indicates that all workers have finished working + chunk := make(chan bounds) // Gives the workers the bounds they should work with + ret := make(chan error) // Allow workers to notify in case of errors + defer close(ret) + + // Launch all workers + for x := 0; x < workers; x++ { + wg.Add(1) + go func() { + defer wg.Done() + for ck := range chunk { + if err := eachFn(ck.start, ck.end); err != nil { + ret <- err + return + } + } + }() + } + + go func() { + // Tell the workers the chunks they have to work on + for i := 0; i < count; { + end := i + size + if end > count { + end = count + } + + chunk <- bounds{start: i, end: end} + + i = end + } + close(chunk) + + // Wait for the workers + wg.Wait() + close(alldone) + }() + + // wait for an error or for all workers to be done + select { + case err := <-ret: + return err + case <-alldone: + break + } + return nil +} diff --git a/pkg/services/searchV2/service_bench_test.go b/pkg/services/searchV2/service_bench_test.go new file mode 100644 index 00000000000..9fc9fbfa573 --- /dev/null +++ b/pkg/services/searchV2/service_bench_test.go @@ -0,0 +1,211 @@ +package searchV2 + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/services/accesscontrol/actest" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/org/orgtest" + "github.com/grafana/grafana/pkg/services/querylibrary/querylibraryimpl" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/store" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" + + "github.com/stretchr/testify/require" +) + +// setupBenchEnv will set up a database with folderCount folders and dashboardsPerFolder dashboards per folder +// It will also set up and run the search service +// and create a signed in user object with explicit permissions on each dashboard and folder. +func setupBenchEnv(b *testing.B, folderCount, dashboardsPerFolder int) (*StandardSearchService, *user.SignedInUser, error) { + sqlStore := db.InitTestDB(b) + err := populateDB(folderCount, dashboardsPerFolder, sqlStore) + require.NoError(b, err, "error when populating the database") + + // load all dashboards and folders + dbLoadingBatchSize := (dashboardsPerFolder + 1) * folderCount + cfg := &setting.Cfg{Search: setting.SearchSettings{DashboardLoadingBatchSize: dbLoadingBatchSize}} + features := featuremgmt.WithFeatures() + orgSvc := &orgtest.FakeOrgService{ + ExpectedOrgs: []*org.OrgDTO{{ID: 1}}, + } + querySvc := querylibraryimpl.ProvideService(cfg, features) + searchService, ok := ProvideService(cfg, sqlStore, store.NewDummyEntityEventsService(), actest.FakeService{}, + tracing.InitializeTracerForTest(), features, orgSvc, nil, querySvc).(*StandardSearchService) + require.True(b, ok) + + err = runSearchService(searchService) + require.NoError(b, err, "error when running search service") + + user := getSignedInUser(folderCount, dashboardsPerFolder) + + return searchService, user, nil +} + +// Returns a signed in user object with permissions on all dashboards and folders +func getSignedInUser(folderCount, dashboardsPerFolder int) *user.SignedInUser { + folderScopes := make([]string, folderCount) + for i := 1; i <= folderCount; i++ { + folderScopes[i-1] = dashboards.ScopeFoldersProvider.GetResourceScopeUID(fmt.Sprintf("folder%d", i)) + } + + dashScopes := make([]string, folderCount*dashboardsPerFolder) + for i := folderCount + 1; i <= (folderCount * (dashboardsPerFolder + 1)); i++ { + dashScopes[i-(folderCount+1)] = dashboards.ScopeDashboardsProvider.GetResourceScopeUID(fmt.Sprintf("dashboard%d", i)) + } + + user := &user.SignedInUser{ + UserID: 1, + OrgID: 1, + Permissions: map[int64]map[string][]string{ + 1: { + dashboards.ActionDashboardsRead: dashScopes, + dashboards.ActionFoldersRead: folderScopes, + }, + }, + } + + return user +} + +// Runs initial indexing of search service +func runSearchService(searchService *StandardSearchService) error { + if err := searchService.dashboardIndex.buildInitialIndexes(context.Background(), []int64{int64(1)}); err != nil { + return err + } + searchService.dashboardIndex.initialIndexingComplete = true + + // Required for sync that is called during dashboard search + go func() { + for { + doneCh := <-searchService.dashboardIndex.syncCh + close(doneCh) + } + }() + + return nil +} + +// Populates database with dashboards and folders +func populateDB(folderCount, dashboardsPerFolder int, sqlStore *sqlstore.SQLStore) error { + // Insert folders + offset := 1 + if errInsert := actest.ConcurrentBatch(actest.Concurrency, folderCount, actest.BatchSize, func(start, end int) error { + n := end - start + folders := make([]dashboards.Dashboard, 0, n) + now := time.Now() + + for u := start; u < end; u++ { + folderID := int64(u + offset) + folders = append(folders, dashboards.Dashboard{ + ID: folderID, + UID: fmt.Sprintf("folder%v", folderID), + Title: fmt.Sprintf("folder%v", folderID), + IsFolder: true, + OrgID: 1, + Created: now, + Updated: now, + }) + } + + err := sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { + if _, err := sess.Insert(folders); err != nil { + return err + } + return nil + }) + return err + }); errInsert != nil { + return errInsert + } + + // Insert dashboards + offset += folderCount + if errInsert := actest.ConcurrentBatch(actest.Concurrency, dashboardsPerFolder*folderCount, actest.BatchSize, func(start, end int) error { + n := end - start + dbs := make([]dashboards.Dashboard, 0, n) + now := time.Now() + + for u := start; u < end; u++ { + dashID := int64(u + offset) + folderID := int64((u+offset)%folderCount + 1) + dbs = append(dbs, dashboards.Dashboard{ + ID: dashID, + UID: fmt.Sprintf("dashboard%v", dashID), + Title: fmt.Sprintf("dashboard%v", dashID), + IsFolder: false, + FolderID: folderID, + OrgID: 1, + Created: now, + Updated: now, + }) + } + + err := sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { + if _, err := sess.Insert(dbs); err != nil { + return err + } + return nil + }) + return err + }); errInsert != nil { + return errInsert + } + + return nil +} + +func benchSearchV2(b *testing.B, folderCount, dashboardsPerFolder int) { + searchService, testUser, err := setupBenchEnv(b, folderCount, dashboardsPerFolder) + require.NoError(b, err) + + b.ResetTimer() + + expectedResultCount := (dashboardsPerFolder + 1) * folderCount + for n := 0; n < b.N; n++ { + result := searchService.doDashboardQuery(context.Background(), testUser, 1, DashboardQuery{Limit: expectedResultCount}) + require.NoError(b, result.Error) + require.NotZero(b, len(result.Frames)) + for _, field := range result.Frames[0].Fields { + if field.Name == "uid" { + require.Equal(b, expectedResultCount, field.Len()) + break + } + } + } +} + +// Test with some dashboards and some folders +func BenchmarkSearchV2_10_10(b *testing.B) { + benchSearchV2(b, 10, 10) +} // ~0.0002 s/op +func BenchmarkSearchV2_10_100(b *testing.B) { + benchSearchV2(b, 10, 100) +} // ~0.002 s/op + +// Test with many dashboards and only one folder +func BenchmarkSearchV2_1_1k(b *testing.B) { + benchSearchV2(b, 1, 1000) +} // ~0.002 s/op +func BenchmarkSearchV2_1_10k(b *testing.B) { + benchSearchV2(b, 1, 10000) +} // ~0.019 s/op + +// Test with a large number of dashboards and folders +func BenchmarkSearchV2_100_100(b *testing.B) { + benchSearchV2(b, 100, 100) +} // ~0.02 s/op +func BenchmarkSearchV2_100_1k(b *testing.B) { + benchSearchV2(b, 100, 1000) +} // ~0.22 s/op +func BenchmarkSearchV2_1k_100(b *testing.B) { + benchSearchV2(b, 1000, 100) +} // ~0.22 s/op From d4015602cafd7139c6628ca6990dba4c326a453e Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Fri, 27 Jan 2023 07:43:13 -0800 Subject: [PATCH 054/117] Dashboard schema: Update default value for timezone (#62340) --- .../sources/developers/kinds/core/dashboard/schema-reference.md | 2 +- kinds/dashboard/dashboard_kind.cue | 2 +- .../grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/sources/developers/kinds/core/dashboard/schema-reference.md b/docs/sources/developers/kinds/core/dashboard/schema-reference.md index 5b4ba009ad0..3672213e9d0 100644 --- a/docs/sources/developers/kinds/core/dashboard/schema-reference.md +++ b/docs/sources/developers/kinds/core/dashboard/schema-reference.md @@ -34,7 +34,7 @@ title: Dashboard kind | `templating` | [object](#templating) | No | TODO docs | | `time` | [object](#time) | No | Time range for dashboard, e.g. last 6 hours, last 7 days, etc | | `timepicker` | [object](#timepicker) | No | TODO docs
TODO this appears to be spread all over in the frontend. Concepts will likely need tidying in tandem with schema changes | -| `timezone` | string | No | Timezone of dashboard. Accepts IANA TZDB zone ID or "browser" or "utc". Default: ``. | +| `timezone` | string | No | Timezone of dashboard. Accepts IANA TZDB zone ID or "browser" or "utc". Default: `browser`. | | `title` | string | No | Title of dashboard. | | `uid` | string | No | Unique dashboard identifier that can be generated by anyone. string (8-40) | | `version` | integer | No | Version of the dashboard, incremented each time the dashboard is updated. | diff --git a/kinds/dashboard/dashboard_kind.cue b/kinds/dashboard/dashboard_kind.cue index db1402c7476..2aa725ad809 100644 --- a/kinds/dashboard/dashboard_kind.cue +++ b/kinds/dashboard/dashboard_kind.cue @@ -31,7 +31,7 @@ lineage: seqs: [ // Theme of dashboard. style: "light" | *"dark" @grafanamaturity(NeedsExpertReview) // Timezone of dashboard. Accepts IANA TZDB zone ID or "browser" or "utc". - timezone?: string | *"" + timezone?: string | *"browser" // Whether a dashboard is editable or not. editable: bool | *true // Configuration of dashboard cursor sync behavior. diff --git a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts index ec61957cea2..706d6522ba9 100644 --- a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts +++ b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts @@ -808,5 +808,5 @@ export const defaultDashboard: Partial = { schemaVersion: 36, style: 'dark', tags: [], - timezone: '', + timezone: 'browser', }; From 7f5ed9f59d8b6fe6dbaa29513db6083139c6beec Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Jan 2023 15:48:06 +0000 Subject: [PATCH 055/117] Update dependency rc-tooltip to v5.3.1 (#62341) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- packages/grafana-ui/package.json | 2 +- yarn.lock | 44 ++++++++++++++++---------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index b03d75f6992..73d828f5be1 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -84,7 +84,7 @@ "rc-drawer": "6.1.2", "rc-slider": "10.1.0", "rc-time-picker": "^3.7.3", - "rc-tooltip": "5.2.2", + "rc-tooltip": "5.3.1", "react-beautiful-dnd": "13.1.1", "react-calendar": "3.9.0", "react-colorful": "5.6.1", diff --git a/yarn.lock b/yarn.lock index c7b07867ad5..9896d6fe216 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5384,7 +5384,7 @@ __metadata: rc-drawer: 6.1.2 rc-slider: 10.1.0 rc-time-picker: ^3.7.3 - rc-tooltip: 5.2.2 + rc-tooltip: 5.3.1 react: 17.0.2 react-beautiful-dnd: 13.1.1 react-calendar: 3.9.0 @@ -32305,17 +32305,17 @@ __metadata: languageName: node linkType: hard -"rc-tooltip@npm:5.2.2": - version: 5.2.2 - resolution: "rc-tooltip@npm:5.2.2" +"rc-tooltip@npm:5.3.1": + version: 5.3.1 + resolution: "rc-tooltip@npm:5.3.1" dependencies: "@babel/runtime": ^7.11.2 classnames: ^2.3.1 - rc-trigger: ^5.0.0 + rc-trigger: ^5.3.1 peerDependencies: react: ">=16.9.0" react-dom: ">=16.9.0" - checksum: df6a59096876becf930df0347cfe6379cde9647f338a333dd0aae99039bf45e72db866f03ab6b5fd5ce616b074ec888f50e61ebe0f8d2a135c2617595dbf0583 + checksum: 93a99dd8f83ca6187cae7d09e498156e660331837f7ff16d6c50165e5cbc810d566552535d8c92c6fb3093f45cadfa0b62a03b9f78ba22e8b6123eda27333cf4 languageName: node linkType: hard @@ -32350,22 +32350,6 @@ __metadata: languageName: node linkType: hard -"rc-trigger@npm:^5.0.0": - version: 5.3.1 - resolution: "rc-trigger@npm:5.3.1" - dependencies: - "@babel/runtime": ^7.18.3 - classnames: ^2.2.6 - rc-align: ^4.0.0 - rc-motion: ^2.0.0 - rc-util: ^5.19.2 - peerDependencies: - react: ">=16.9.0" - react-dom: ">=16.9.0" - checksum: 29302e9a0a66eb26cef5ce0b291ada9bb3e284f19980bf02da6863a5306d5de863aa87fbbf30c176a4453c13db160b69789ac50aae09af8e5c53942ac0368a3b - languageName: node - linkType: hard - "rc-trigger@npm:^5.0.4": version: 5.2.10 resolution: "rc-trigger@npm:5.2.10" @@ -32382,6 +32366,22 @@ __metadata: languageName: node linkType: hard +"rc-trigger@npm:^5.3.1": + version: 5.3.4 + resolution: "rc-trigger@npm:5.3.4" + dependencies: + "@babel/runtime": ^7.18.3 + classnames: ^2.2.6 + rc-align: ^4.0.0 + rc-motion: ^2.0.0 + rc-util: ^5.19.2 + peerDependencies: + react: ">=16.9.0" + react-dom: ">=16.9.0" + checksum: 6ca7694a4cf064040b5e0fd9b4629b0e0a19ebb29c4eb5614ee9eb22b4193e21909171fd95e48be73a94e44f249cb9616d7670b696164620b722d3de6f280017 + languageName: node + linkType: hard + "rc-util@npm:^4.0.4, rc-util@npm:^4.15.3, rc-util@npm:^4.4.0": version: 4.21.1 resolution: "rc-util@npm:4.21.1" From 305209f4be61837f47dfb15fcfb2893d6933a03d Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Fri, 27 Jan 2023 16:10:05 +0000 Subject: [PATCH 056/117] CommandPalette: Render dashboard + nav actions as links (#62315) * CommandPalette: Render links as links! * Update public/app/features/commandPalette/KBarResults.tsx Co-authored-by: Ashley Harrison * Apply suggestions from code review Co-authored-by: Ashley Harrison * fix ellipsis showing --------- Co-authored-by: Ashley Harrison --- package.json | 1 + .../commandPalette/CommandPalette.tsx | 2 +- .../features/commandPalette/KBarResults.tsx | 226 ++++++++++++++++++ .../features/commandPalette/ResultItem.tsx | 5 +- .../actions/dashboardActions.ts | 9 +- .../commandPalette/actions/staticActions.ts | 2 +- public/app/features/commandPalette/types.ts | 3 +- yarn.lock | 12 + 8 files changed, 249 insertions(+), 11 deletions(-) create mode 100644 public/app/features/commandPalette/KBarResults.tsx diff --git a/package.json b/package.json index 49d08cba814..75bb1dbfeeb 100644 --- a/package.json +++ b/package.json @@ -391,6 +391,7 @@ "react-table": "7.8.0", "react-transition-group": "4.4.5", "react-use": "17.4.0", + "react-virtual": "2.8.2", "react-virtualized-auto-sizer": "1.0.7", "react-window": "1.8.8", "react-window-infinite-loader": "1.0.8", diff --git a/public/app/features/commandPalette/CommandPalette.tsx b/public/app/features/commandPalette/CommandPalette.tsx index 73fb7bf5778..4636c8e543a 100644 --- a/public/app/features/commandPalette/CommandPalette.tsx +++ b/public/app/features/commandPalette/CommandPalette.tsx @@ -6,7 +6,6 @@ import { KBarAnimator, KBarPortal, KBarPositioner, - KBarResults, KBarSearch, VisualState, useRegisterActions, @@ -20,6 +19,7 @@ import { config, reportInteraction } from '@grafana/runtime'; import { Icon, Spinner, useStyles2 } from '@grafana/ui'; import { t } from 'app/core/internationalization'; +import { KBarResults } from './KBarResults'; import { ResultItem } from './ResultItem'; import { useDashboardResults } from './actions/dashboardActions'; import useActions from './actions/useActions'; diff --git a/public/app/features/commandPalette/KBarResults.tsx b/public/app/features/commandPalette/KBarResults.tsx new file mode 100644 index 00000000000..76ff919b1f1 --- /dev/null +++ b/public/app/features/commandPalette/KBarResults.tsx @@ -0,0 +1,226 @@ +import { ActionImpl, getListboxItemId, KBAR_LISTBOX, useKBar } from 'kbar'; +import { usePointerMovedSinceMount } from 'kbar/lib/utils'; +import * as React from 'react'; +import { useVirtual } from 'react-virtual'; + +// From https://github.com/timc1/kbar/blob/main/src/KBarResults.tsx +// TODO: Go back to KBarResults from kbar when https://github.com/timc1/kbar/issues/281 is fixed +// Remember to remove dependency on react-virtual when removing this file + +const START_INDEX = 0; + +interface RenderParams { + item: T; + active: boolean; +} + +interface KBarResultsProps { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + items: any[]; + onRender: (params: RenderParams) => React.ReactElement; + maxHeight?: number; +} + +export const KBarResults: React.FC = (props) => { + const activeRef = React.useRef(null); + const parentRef = React.useRef(null); + + // store a ref to all items so we do not have to pass + // them as a dependency when setting up event listeners. + const itemsRef = React.useRef(props.items); + itemsRef.current = props.items; + + const rowVirtualizer = useVirtual({ + size: itemsRef.current.length, + parentRef, + }); + + const { query, search, currentRootActionId, activeIndex, options } = useKBar((state) => ({ + search: state.searchQuery, + currentRootActionId: state.currentRootActionId, + activeIndex: state.activeIndex, + })); + + React.useEffect(() => { + const handler = (event: KeyboardEvent) => { + if (event.key === 'ArrowUp' || (event.ctrlKey && event.key === 'p')) { + event.preventDefault(); + query.setActiveIndex((index) => { + let nextIndex = index > START_INDEX ? index - 1 : index; + // avoid setting active index on a group + if (typeof itemsRef.current[nextIndex] === 'string') { + if (nextIndex === 0) { + return index; + } + nextIndex -= 1; + } + return nextIndex; + }); + } else if (event.key === 'ArrowDown' || (event.ctrlKey && event.key === 'n')) { + event.preventDefault(); + query.setActiveIndex((index) => { + let nextIndex = index < itemsRef.current.length - 1 ? index + 1 : index; + // avoid setting active index on a group + if (typeof itemsRef.current[nextIndex] === 'string') { + if (nextIndex === itemsRef.current.length - 1) { + return index; + } + nextIndex += 1; + } + return nextIndex; + }); + } else if (event.key === 'Enter') { + event.preventDefault(); + // storing the active dom element in a ref prevents us from + // having to calculate the current action to perform based + // on the `activeIndex`, which we would have needed to add + // as part of the dependencies array. + activeRef.current?.click(); + } + }; + window.addEventListener('keydown', handler); + return () => window.removeEventListener('keydown', handler); + }, [query]); + + // destructuring here to prevent linter warning to pass + // entire rowVirtualizer in the dependencies array. + const { scrollToIndex } = rowVirtualizer; + React.useEffect(() => { + scrollToIndex(activeIndex, { + // ensure that if the first item in the list is a group + // name and we are focused on the second item, to not + // scroll past that group, hiding it. + align: activeIndex <= 1 ? 'end' : 'auto', + }); + }, [activeIndex, scrollToIndex]); + + React.useEffect(() => { + // TODO(tim): fix scenario where async actions load in + // and active index is reset to the first item. i.e. when + // users register actions and bust the `useRegisterActions` + // cache, we won't want to reset their active index as they + // are navigating the list. + query.setActiveIndex( + // avoid setting active index on a group + typeof props.items[START_INDEX] === 'string' ? START_INDEX + 1 : START_INDEX + ); + }, [search, currentRootActionId, props.items, query]); + + const execute = React.useCallback( + (ev: React.MouseEvent, item: RenderParams['item']) => { + if (typeof item === 'string') { + return; + } + + // ActionImpl constructor copies all properties from action onto ActionImpl + // so our url property is secretly there, but completely untyped + // Preferably this change is upstreamed and ActionImpl has this + // eslint-disable-next-line + const url = (item as ActionImpl & { url?: string }).url; + + if (item.command) { + item.command.perform(item); + query.toggle(); + } else if (url) { + if (!(ev.ctrlKey || ev.metaKey || ev.shiftKey)) { + query.toggle(); + } + } else { + query.setSearch(''); + query.setCurrentRootAction(item.id); + } + + options.callbacks?.onSelectAction?.(item); + }, + [query, options] + ); + + const pointerMoved = usePointerMovedSinceMount(); + + return ( +
+
+ {rowVirtualizer.virtualItems.map((virtualRow) => { + const item = itemsRef.current[virtualRow.index]; + + // ActionImpl constructor copies all properties from action onto ActionImpl + // so our url property is secretly there, but completely untyped + // Preferably this change is upstreamed and ActionImpl has this + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const url = (item as ActionImpl & { url?: string }).url; + + const handlers = typeof item !== 'string' && { + onPointerMove: () => + pointerMoved && activeIndex !== virtualRow.index && query.setActiveIndex(virtualRow.index), + onPointerDown: () => query.setActiveIndex(virtualRow.index), + onClick: (ev: React.MouseEvent) => execute(ev, item), + }; + const active = virtualRow.index === activeIndex; + + const childProps = { + id: getListboxItemId(virtualRow.index), + role: 'option', + 'aria-selected': active, + style: { + position: 'absolute', + top: 0, + left: 0, + width: '100%', + transform: `translateY(${virtualRow.start}px)`, + } as const, + ...handlers, + }; + + const renderedItem = React.cloneElement( + props.onRender({ + item, + active, + }), + { + ref: virtualRow.measureRef, + } + ); + + if (url) { + return ( + ) : null} + {...childProps} + > + {renderedItem} + + ); + } + + return ( +
) : null} + {...childProps} + > + {renderedItem} +
+ ); + })} +
+
+ ); +}; diff --git a/public/app/features/commandPalette/ResultItem.tsx b/public/app/features/commandPalette/ResultItem.tsx index 464276f10e2..1102f5aa929 100644 --- a/public/app/features/commandPalette/ResultItem.tsx +++ b/public/app/features/commandPalette/ResultItem.tsx @@ -35,8 +35,11 @@ export const ResultItem = React.forwardRef( let name = action.name; + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const hasAction = Boolean(action.command?.perform || (action as ActionImpl & { url?: string }).url); + // TODO: does this needs adjusting for i18n? - if (action.children && !action.command?.perform && !name.endsWith('...')) { + if (action.children.length && !hasAction && !name.endsWith('...')) { name += '...'; } diff --git a/public/app/features/commandPalette/actions/dashboardActions.ts b/public/app/features/commandPalette/actions/dashboardActions.ts index f9f4c24ba69..76ff3e60bb1 100644 --- a/public/app/features/commandPalette/actions/dashboardActions.ts +++ b/public/app/features/commandPalette/actions/dashboardActions.ts @@ -2,7 +2,6 @@ import debounce from 'debounce-promise'; import { useEffect, useState } from 'react'; import { locationUtil } from '@grafana/data'; -import { locationService } from '@grafana/runtime'; import { t } from 'app/core/internationalization'; import impressionSrv from 'app/core/services/impression_srv'; import { getGrafanaSearcher } from 'app/features/search/service'; @@ -38,9 +37,7 @@ export async function getRecentDashboardActions(): Promise { - locationService.push(locationUtil.stripBaseFromUrl(url)); - }, + url: locationUtil.stripBaseFromUrl(url), }; }); @@ -66,9 +63,7 @@ export async function getDashboardSearchResultActions(searchQuery: string): Prom name: `${name}`, section: t('command-palette.section.dashboard-search-results', 'Dashboards'), priority: SEARCH_RESULTS_PRORITY, - perform: () => { - locationService.push(locationUtil.stripBaseFromUrl(url)); - }, + url: locationUtil.stripBaseFromUrl(url), }; }); diff --git a/public/app/features/commandPalette/actions/staticActions.ts b/public/app/features/commandPalette/actions/staticActions.ts index 893adf713e5..0914c5d4435 100644 --- a/public/app/features/commandPalette/actions/staticActions.ts +++ b/public/app/features/commandPalette/actions/staticActions.ts @@ -31,7 +31,7 @@ function navTreeToActions(navTree: NavModelItem[], parent?: NavModelItem): Comma id: idForNavItem(navItem), name: text, // TODO: translate section: section, - perform: url ? () => locationService.push(locationUtil.stripBaseFromUrl(url)) : undefined, + url: url && locationUtil.stripBaseFromUrl(url), parent: parent && idForNavItem(parent), priority: DEFAULT_PRIORITY, }; diff --git a/public/app/features/commandPalette/types.ts b/public/app/features/commandPalette/types.ts index 01c1690025a..d44879d69cd 100644 --- a/public/app/features/commandPalette/types.ts +++ b/public/app/features/commandPalette/types.ts @@ -9,10 +9,11 @@ export type CommandPaletteAction = RootCommandPaletteAction | ChildCommandPalett type RootCommandPaletteAction = Omit & { section: NotNullable; priority: NotNullable; + url?: string; }; type ChildCommandPaletteAction = Action & { parent: NotNullable; - priority: NotNullable; + url?: string; }; diff --git a/yarn.lock b/yarn.lock index 9896d6fe216..b97a5f19210 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22260,6 +22260,7 @@ __metadata: react-test-renderer: 17.0.2 react-transition-group: 4.4.5 react-use: 17.4.0 + react-virtual: 2.8.2 react-virtualized-auto-sizer: 1.0.7 react-window: 1.8.8 react-window-infinite-loader: 1.0.8 @@ -33301,6 +33302,17 @@ __metadata: languageName: node linkType: hard +"react-virtual@npm:2.8.2": + version: 2.8.2 + resolution: "react-virtual@npm:2.8.2" + dependencies: + "@reach/observe-rect": ^1.1.0 + peerDependencies: + react: ^16.6.3 || ^17.0.0 + checksum: 3c95c7ea951d33d6da8d5461ea28b39dea7bd536b06ccae58ac808907761bc2425dcb469be5618c95a6f9f021f70b8019f386d21d33c64540d051f11e3f10e4a + languageName: node + linkType: hard + "react-virtual@npm:^2.8.2": version: 2.10.4 resolution: "react-virtual@npm:2.10.4" From 1ef7cfda3f9cc86ea5d6a8cfa10e011f4df349ab Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 27 Jan 2023 16:21:48 +0000 Subject: [PATCH 057/117] Navigation: more command palette tweaks (#62286) * update placeholder text, only create search keybindings if topNavCommandPalette is disabled * make search input wider --- public/app/core/components/AppChrome/TopSearchBar.tsx | 2 +- .../AppChrome/TopSearchBarCommandPaletteTrigger.tsx | 4 ++-- public/app/core/services/keybindingSrv.ts | 6 ++++-- public/app/features/commandPalette/CommandPalette.tsx | 2 +- public/locales/de-DE/grafana.json | 3 ++- public/locales/en-US/grafana.json | 3 ++- public/locales/es-ES/grafana.json | 3 ++- public/locales/fr-FR/grafana.json | 3 ++- public/locales/pseudo-LOCALE/grafana.json | 3 ++- public/locales/zh-Hans/grafana.json | 3 ++- 10 files changed, 20 insertions(+), 12 deletions(-) diff --git a/public/app/core/components/AppChrome/TopSearchBar.tsx b/public/app/core/components/AppChrome/TopSearchBar.tsx index b6246da47bd..726171f4ddc 100644 --- a/public/app/core/components/AppChrome/TopSearchBar.tsx +++ b/public/app/core/components/AppChrome/TopSearchBar.tsx @@ -79,7 +79,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ justifyContent: 'space-between', [theme.breakpoints.up('sm')]: { - gridTemplateColumns: '1.5fr minmax(200px, 1fr) 1.5fr', // search should not be smaller than 200px + gridTemplateColumns: '1.5fr minmax(240px, 1fr) 1.5fr', // search should not be smaller than 240px display: 'grid', justifyContent: 'flex-start', diff --git a/public/app/core/components/AppChrome/TopSearchBarCommandPaletteTrigger.tsx b/public/app/core/components/AppChrome/TopSearchBarCommandPaletteTrigger.tsx index c67660acaae..08a5c701c51 100644 --- a/public/app/core/components/AppChrome/TopSearchBarCommandPaletteTrigger.tsx +++ b/public/app/core/components/AppChrome/TopSearchBarCommandPaletteTrigger.tsx @@ -36,7 +36,7 @@ export function TopSearchBarCommandPaletteTrigger() { ); @@ -65,7 +65,7 @@ function PretendTextInput({ onClick }: PretendTextInputProps) {
diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index b48bbbe1650..51b7518e9d2 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -39,9 +39,11 @@ export class KeybindingSrv { this.bind('g a', this.openAlerting); this.bind('g p', this.goToProfile); this.bind('g e', this.goToExplore); - this.bind('s o', this.openSearch); + if (!config.featureToggles.topNavCommandPalette) { + this.bind('s o', this.openSearch); + this.bind('f', this.openSearch); + } this.bind('t a', this.makeAbsoluteTime); - this.bind('f', this.openSearch); this.bind('esc', this.exit); this.bindGlobalEsc(); } diff --git a/public/app/features/commandPalette/CommandPalette.tsx b/public/app/features/commandPalette/CommandPalette.tsx index 4636c8e543a..fa6671affa3 100644 --- a/public/app/features/commandPalette/CommandPalette.tsx +++ b/public/app/features/commandPalette/CommandPalette.tsx @@ -59,7 +59,7 @@ export const CommandPalette = () => {
{isFetchingDashboardResults ? : }
diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index dd79858a00e..7938d1ed771 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -292,7 +292,8 @@ "title": "Szenen" }, "search": { - "placeholder": "Grafana durchsuchen" + "placeholder": "Grafana durchsuchen", + "placeholderCommandPalette": "" }, "search-dashboards": { "title": "Dashboards durchsuchen" diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 1f9efec846a..4450f5977a4 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -292,7 +292,8 @@ "title": "Scenes" }, "search": { - "placeholder": "Search Grafana" + "placeholder": "Search Grafana", + "placeholderCommandPalette": "Search or jump to..." }, "search-dashboards": { "title": "Search dashboards" diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 24d17509b06..a921571d27b 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -292,7 +292,8 @@ "title": "Escenas" }, "search": { - "placeholder": "Buscar Grafana" + "placeholder": "Buscar Grafana", + "placeholderCommandPalette": "" }, "search-dashboards": { "title": "Buscar paneles de control" diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 76f0cf8e44c..753dcced8d3 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -292,7 +292,8 @@ "title": "Scènes" }, "search": { - "placeholder": "Rechercher dans Grafana" + "placeholder": "Rechercher dans Grafana", + "placeholderCommandPalette": "" }, "search-dashboards": { "title": "Rechercher dans les tableaux de bord" diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 266e11b36c8..9b900ef8d2f 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -292,7 +292,8 @@ "title": "Ŝčęʼnęş" }, "search": { - "placeholder": "Ŝęäřčĥ Ğřäƒäʼnä" + "placeholder": "Ŝęäřčĥ Ğřäƒäʼnä", + "placeholderCommandPalette": "Ŝęäřčĥ őř ĵūmp ŧő..." }, "search-dashboards": { "title": "Ŝęäřčĥ đäşĥþőäřđş" diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 756b9b2c3b5..cd6416ae13f 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -292,7 +292,8 @@ "title": "场景" }, "search": { - "placeholder": "搜索 Grafana" + "placeholder": "搜索 Grafana", + "placeholderCommandPalette": "" }, "search-dashboards": { "title": "" From d5294eb8fa71ba62546d00ebce7b9376ab6f2b1a Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Fri, 27 Jan 2023 17:30:25 +0100 Subject: [PATCH 058/117] Explore: Implement feature toggle for logs sample (#62291) * Explore: Implement feature toggle for logs sample * Run pkg/services/featuremgmt/toggles_gen_test.go * Remove boolean * Update copy --- .../configure-grafana/feature-toggles/index.md | 1 + packages/grafana-data/src/types/featureToggles.gen.ts | 1 + pkg/services/featuremgmt/registry.go | 8 ++++++++ pkg/services/featuremgmt/toggles_gen.go | 4 ++++ public/app/features/explore/Explore.tsx | 4 +++- 5 files changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index d67f0128b15..bcec59423e3 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -30,6 +30,7 @@ Some stable features are enabled by default. You can disable a stable feature by | `internationalization` | Enables internationalization | Yes | | `accessTokenExpirationCheck` | Enable OAuth access_token expiration check and token refresh using the refresh_token | | | `disablePrometheusExemplarSampling` | Disable Prometheus examplar sampling | | +| `logsSampleInExplore` | Enables access to the logs sample feature in Explore | Yes | ## Beta feature toggles diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 83cbe0d8cc7..467ad96208f 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -92,5 +92,6 @@ export interface FeatureToggles { alertingNoNormalState?: boolean; azureMultipleResourcePicker?: boolean; topNavCommandPalette?: boolean; + logsSampleInExplore?: boolean; logsContextDatasourceUi?: boolean; } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 72602b93540..6db9cb8d4bc 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -426,6 +426,14 @@ var ( State: FeatureStateBeta, FrontendOnly: true, }, + { + + Name: "logsSampleInExplore", + Description: "Enables access to the logs sample feature in Explore", + State: FeatureStateStable, + Expression: "true", //turned on by default + FrontendOnly: true, + }, { Name: "logsContextDatasourceUi", Description: "Allow datasource to provide custom UI for context view", diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 456ce5e8f3e..617186f858d 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -311,6 +311,10 @@ const ( // Launch the Command Palette from the top navigation search box FlagTopNavCommandPalette = "topNavCommandPalette" + // FlagLogsSampleInExplore + // Enables access to the logs sample feature in Explore + FlagLogsSampleInExplore = "logsSampleInExplore" + // FlagLogsContextDatasourceUi // Allow datasource to provide custom UI for context view FlagLogsContextDatasourceUi = "logsContextDatasourceUi" diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index b99eba23dc6..f4acf60a14d 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -514,7 +514,9 @@ export class Explore extends React.PureComponent { {this.renderFlameGraphPanel()} )} {showTrace && {this.renderTraceViewPanel()}} - {showLogsSample && {this.renderLogsSamplePanel()}} + {config.featureToggles.logsSampleInExplore && showLogsSample && ( + {this.renderLogsSamplePanel()} + )} {showNoData && {this.renderNoData()}} )} From c006df375a3a6056f9cbeeaf2f28f87ce86823a2 Mon Sep 17 00:00:00 2001 From: Matthew Jacobson Date: Fri, 27 Jan 2023 11:39:16 -0500 Subject: [PATCH 059/117] Alerting: Create endpoints for exporting in provisioning file format (#58623) This adds provisioning endpoints for downloading alert rules and alert rule groups in a format that is compatible with file provisioning. Each endpoint supports both json and yaml response types via Accept header as well as a query parameter download=true/false that will set Content-Disposition to recommend initiating a download or inline display. This also makes some package changes to keep structs with potential to drift closer together. Eventually, other alerting file structs should also move into this new file package, but the rest require some refactoring that is out of scope for this PR. --- .../file-provisioning/index.md | 8 +- .../http_api/alerting_provisioning.md | 851 +++++++++++------- pkg/api/response/response.go | 28 +- pkg/services/ngalert/api/api_provisioning.go | 89 ++ .../ngalert/api/api_provisioning_test.go | 478 +++++++++- pkg/services/ngalert/api/authorization.go | 5 +- .../ngalert/api/authorization_test.go | 2 +- .../api/generated_base_api_provisioning.go | 47 + pkg/services/ngalert/api/provisioning.go | 12 + pkg/services/ngalert/api/tooling/api.json | 244 ++++- .../definitions/provisioning_alert_rules.go | 55 +- pkg/services/ngalert/api/tooling/post.json | 240 ++++- pkg/services/ngalert/api/tooling/spec.json | 247 ++++- pkg/services/ngalert/ngalert.go | 2 +- .../ngalert/provisioning/alert_rules.go | 150 ++- .../alerting/{ => file}/rules_types.go | 161 +++- .../alerting/{ => file}/rules_types_test.go | 9 +- .../alerting/rules_provisioner.go | 16 +- pkg/services/provisioning/alerting/types.go | 11 +- pkg/services/provisioning/provisioning.go | 1 + public/api-merged.json | 243 ++++- 21 files changed, 2507 insertions(+), 392 deletions(-) rename pkg/services/provisioning/alerting/{ => file}/rules_types.go (50%) rename pkg/services/provisioning/alerting/{ => file}/rules_types_test.go (98%) diff --git a/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md b/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md index 1aec13bc0e4..82b1c05ae20 100644 --- a/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md +++ b/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md @@ -22,20 +22,20 @@ Details on how to set up the files and which fields are required for each object **Note:** -Provisioning takes place during the initial set up of your Grafana system, but you can re-run it at any time using the [Grafana Alerting provisioning API](https://grafana.com/docs/grafana/latest/developers/http_api/admin/#reload-provisioning-configurations). +Provisioning takes place during the initial set up of your Grafana system, but you can re-run it at any time using the [Grafana Admin API](https://grafana.com/docs/grafana/latest/developers/http_api/admin/#reload-provisioning-configurations). ### Provision alert rules Create or delete alert rules in your Grafana instance(s). -1. Create an alert rule in Grafana. -1. Use the [Alerting provisioning API](https://grafana.com/docs/grafana/latest/developers/http_api/alerting_provisioning/#route-get-alert-rule) to extract the alert rule. +1. Create alert rules in Grafana. +1. Use the [Alerting provisioning API](https://grafana.com/docs/grafana/latest/developers/http_api/alerting_provisioning/#route-get-alert-rule-export) export endpoints to download a provisioning file for your alert rules. 1. Copy the contents into a YAML or JSON configuration file in the default provisioning directory or in your configured directory. Example configuration files can be found below. 1. Ensure that your files are in the right directory on the node running the Grafana server, so that they deploy alongside your Grafana instance(s). -1. Delete the alert rule in Grafana. +1. Delete the alert rules in Grafana that will be provisioned. **Note:** diff --git a/docs/sources/developers/http_api/alerting_provisioning.md b/docs/sources/developers/http_api/alerting_provisioning.md index 1198c17eead..98ff5686667 100644 --- a/docs/sources/developers/http_api/alerting_provisioning.md +++ b/docs/sources/developers/http_api/alerting_provisioning.md @@ -18,7 +18,7 @@ title: 'Alerting Provisioning HTTP API ' ### Version -1.0.0 +1.1.0 ## Content negotiation @@ -29,53 +29,61 @@ title: 'Alerting Provisioning HTTP API ' ### Produces - application/json +- text/yaml +- application/yaml ## All endpoints ### Alert rules -| Method | URI | Name | Summary | -| ------ | ----------------------------------------------------------- | --------------------------------------------------------- | ------------------------------------ | -| GET | /api/v1/provisioning/alert-rules/{UID} | [route get alert rule](#route-get-alert-rule) | Get a specific alert rule by UID. | -| POST | /api/v1/provisioning/alert-rules | [route post alert rule](#route-post-alert-rule) | Create a new alert rule. | -| PUT | /api/v1/provisioning/alert-rules/{UID} | [route put alert rule](#route-put-alert-rule) | Update an existing alert rule. | -| PUT | /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group} | [route put alert rule group](#route-put-alert-rule-group) | Update the interval of a rule group. | -| DELETE | /api/v1/provisioning/alert-rules/{UID} | [route delete alert rule](#route-delete-alert-rule) | Delete a specific alert rule by UID. | +| Method | URI | Name | Summary | +| ------ | ------------------------------------------------------------------ | ----------------------------------------------------------------------- | ------------------------------------------------------- | +| DELETE | /api/v1/provisioning/alert-rules/{UID} | [route delete alert rule](#route-delete-alert-rule) | Delete a specific alert rule by UID. | +| GET | /api/v1/provisioning/alert-rules/{UID} | [route get alert rule](#route-get-alert-rule) | Get a specific alert rule by UID. | +| GET | /api/v1/provisioning/alert-rules/{UID}/export | [route get alert rule export](#route-get-alert-rule-export) | Export an alert rule in provisioning file format. | +| GET | /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group} | [route get alert rule group](#route-get-alert-rule-group) | Get a rule group. | +| GET | /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export | [route get alert rule group export](#route-get-alert-rule-group-export) | Export an alert rule group in provisioning file format. | +| GET | /api/v1/provisioning/alert-rules | [route get alert rules](#route-get-alert-rules) | Get all the alert rules. | +| GET | /api/v1/provisioning/alert-rules/export | [route get alert rules export](#route-get-alert-rules-export) | Export all alert rules in provisioning file format. | +| POST | /api/v1/provisioning/alert-rules | [route post alert rule](#route-post-alert-rule) | Create a new alert rule. | +| PUT | /api/v1/provisioning/alert-rules/{UID} | [route put alert rule](#route-put-alert-rule) | Update an existing alert rule. | +| PUT | /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group} | [route put alert rule group](#route-put-alert-rule-group) | Update the interval of a rule group. | ### Contact points | Method | URI | Name | Summary | | ------ | ----------------------------------------- | --------------------------------------------------------- | --------------------------------- | +| DELETE | /api/v1/provisioning/contact-points/{UID} | [route delete contactpoints](#route-delete-contactpoints) | Delete a contact point. | | GET | /api/v1/provisioning/contact-points | [route get contactpoints](#route-get-contactpoints) | Get all the contact points. | | POST | /api/v1/provisioning/contact-points | [route post contactpoints](#route-post-contactpoints) | Create a contact point. | | PUT | /api/v1/provisioning/contact-points/{UID} | [route put contactpoint](#route-put-contactpoint) | Update an existing contact point. | -| DELETE | /api/v1/provisioning/contact-points/{UID} | [route delete contactpoints](#route-delete-contactpoints) | Delete a contact point. | ### Notification policies -| Method | URI | Name | Summary | -| ------ | ----------------------------- | ----------------------------------------------- | ---------------------------------- | -| GET | /api/v1/provisioning/policies | [route get policy tree](#route-get-policy-tree) | Get the notification policy tree. | -| PUT | /api/v1/provisioning/policies | [route put policy tree](#route-put-policy-tree) | Sets the notification policy tree. | +| Method | URI | Name | Summary | +| ------ | ----------------------------- | --------------------------------------------------- | ------------------------------------ | +| DELETE | /api/v1/provisioning/policies | [route reset policy tree](#route-reset-policy-tree) | Clears the notification policy tree. | +| GET | /api/v1/provisioning/policies | [route get policy tree](#route-get-policy-tree) | Get the notification policy tree. | +| PUT | /api/v1/provisioning/policies | [route put policy tree](#route-put-policy-tree) | Sets the notification policy tree. | ### Mute timings | Method | URI | Name | Summary | | ------ | ---------------------------------------- | ----------------------------------------------------- | -------------------------------- | -| GET | /api/v1/provisioning/mute-timings | [route get mute timings](#route-get-mute-timings) | Get all the mute timings. | +| DELETE | /api/v1/provisioning/mute-timings/{name} | [route delete mute timing](#route-delete-mute-timing) | Delete a mute timing. | | GET | /api/v1/provisioning/mute-timings/{name} | [route get mute timing](#route-get-mute-timing) | Get a mute timing. | +| GET | /api/v1/provisioning/mute-timings | [route get mute timings](#route-get-mute-timings) | Get all the mute timings. | | POST | /api/v1/provisioning/mute-timings | [route post mute timing](#route-post-mute-timing) | Create a new mute timing. | | PUT | /api/v1/provisioning/mute-timings/{name} | [route put mute timing](#route-put-mute-timing) | Replace an existing mute timing. | -| DELETE | /api/v1/provisioning/mute-timings/{name} | [route delete mute timing](#route-delete-mute-timing) | Delete a mute timing. | ### Templates -| Method | URI | Name | Summary | -| ------ | ------------------------------------- | ----------------------------------------------- | ------------------------------- | -| GET | /api/v1/provisioning/templates | [route get templates](#route-get-templates) | Get all notification templates. | -| GET | /api/v1/provisioning/templates/{name} | [route get template](#route-get-template) | Get a notification template. | -| PUT | /api/v1/provisioning/templates/{name} | [route put template](#route-put-template) | Creates or updates a template. | -| DELETE | /api/v1/provisioning/templates/{name} | [route delete template](#route-delete-template) | Delete a template. | +| Method | URI | Name | Summary | +| ------ | ------------------------------------- | ----------------------------------------------- | ------------------------------------------ | +| DELETE | /api/v1/provisioning/templates/{name} | [route delete template](#route-delete-template) | Delete a template. | +| GET | /api/v1/provisioning/templates/{name} | [route get template](#route-get-template) | Get a notification template. | +| GET | /api/v1/provisioning/templates | [route get templates](#route-get-templates) | Get all notification templates. | +| PUT | /api/v1/provisioning/templates/{name} | [route put template](#route-put-template) | Updates an existing notification template. | ## Paths @@ -87,16 +95,15 @@ DELETE /api/v1/provisioning/alert-rules/{UID} #### Parameters -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ----------- | -| UID | `path` | string | `string` | | ✓ | | | +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------ | ------ | -------- | --------- | :------: | ------- | -------------- | +| UID | `path` | string | `string` | | ✓ | | Alert rule UID | #### All responses -| Code | Status | Description | Has headers | Schema | -| ----------------------------------- | ----------- | ---------------------------------------- | :---------: | --------------------------------------------- | -| [204](#route-delete-alert-rule-204) | No Content | The alert rule was deleted successfully. | | [schema](#route-delete-alert-rule-204-schema) | -| [400](#route-delete-alert-rule-400) | Bad Request | ValidationError | | [schema](#route-delete-alert-rule-400-schema) | +| Code | Status | Description | Has headers | Schema | +| ----------------------------------- | ---------- | ---------------------------------------- | :---------: | --------------------------------------------- | +| [204](#route-delete-alert-rule-204) | No Content | The alert rule was deleted successfully. | | [schema](#route-delete-alert-rule-204-schema) | #### Responses @@ -106,14 +113,6 @@ Status: No Content ###### Schema -##### 400 - ValidationError - -Status: Bad Request - -###### Schema - -[ValidationError](#validation-error) - ### Delete a contact point. (_RouteDeleteContactpoints_) ``` @@ -126,34 +125,23 @@ DELETE /api/v1/provisioning/contact-points/{UID} #### Parameters -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ------------------------------------------------- | -| UID | `path` | string | `string` | | ✓ | | UID should be the contact point unique identifier | +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ------------------------------------------ | +| UID | `path` | string | `string` | | ✓ | | UID is the contact point unique identifier | #### All responses -| Code | Status | Description | Has headers | Schema | -| -------------------------------------- | ----------- | --------------- | :---------: | ------------------------------------------------ | -| [202](#route-delete-contactpoints-202) | Accepted | Ack | | [schema](#route-delete-contactpoints-202-schema) | -| [400](#route-delete-contactpoints-400) | Bad Request | ValidationError | | [schema](#route-delete-contactpoints-400-schema) | +| Code | Status | Description | Has headers | Schema | +| -------------------------------------- | ---------- | ------------------------------------------- | :---------: | ------------------------------------------------ | +| [204](#route-delete-contactpoints-204) | No Content | The contact point was deleted successfully. | | [schema](#route-delete-contactpoints-204-schema) | #### Responses -##### 202 - Ack +##### 204 - The contact point was deleted successfully. -Status: Accepted +Status: No Content -###### Schema - -[Ack](#ack) - -##### 400 - ValidationError - -Status: Bad Request - -###### Schema - -[ValidationError](#validation-error) +###### Schema ### Delete a mute timing. (_RouteDeleteMuteTiming_) @@ -163,26 +151,24 @@ DELETE /api/v1/provisioning/mute-timings/{name} #### Parameters -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ------------- | -| name | `path` | string | `string` | | ✓ | | Template Name | +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ---------------- | +| name | `path` | string | `string` | | ✓ | | Mute timing name | #### All responses -| Code | Status | Description | Has headers | Schema | -| ------------------------------------ | ---------- | ----------- | :---------: | ---------------------------------------------- | -| [204](#route-delete-mute-timing-204) | No Content | Ack | | [schema](#route-delete-mute-timing-204-schema) | +| Code | Status | Description | Has headers | Schema | +| ------------------------------------ | ---------- | ----------------------------------------- | :---------: | ---------------------------------------------- | +| [204](#route-delete-mute-timing-204) | No Content | The mute timing was deleted successfully. | | [schema](#route-delete-mute-timing-204-schema) | #### Responses -##### 204 - Ack +##### 204 - The mute timing was deleted successfully. Status: No Content ###### Schema -[Ack](#ack) - ### Delete a template. (_RouteDeleteTemplate_) ``` @@ -197,20 +183,18 @@ DELETE /api/v1/provisioning/templates/{name} #### All responses -| Code | Status | Description | Has headers | Schema | -| --------------------------------- | ---------- | ----------- | :---------: | ------------------------------------------- | -| [204](#route-delete-template-204) | No Content | Ack | | [schema](#route-delete-template-204-schema) | +| Code | Status | Description | Has headers | Schema | +| --------------------------------- | ---------- | -------------------------------------- | :---------: | ------------------------------------------- | +| [204](#route-delete-template-204) | No Content | The template was deleted successfully. | | [schema](#route-delete-template-204-schema) | #### Responses -##### 204 - Ack +##### 204 - The template was deleted successfully. Status: No Content ###### Schema -[Ack](#ack) - ### Get a specific alert rule by UID. (_RouteGetAlertRule_) ``` @@ -219,34 +203,210 @@ GET /api/v1/provisioning/alert-rules/{UID} #### Parameters -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ----------- | -| UID | `path` | string | `string` | | ✓ | | | +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------ | ------ | -------- | --------- | :------: | ------- | -------------- | +| UID | `path` | string | `string` | | ✓ | | Alert rule UID | #### All responses -| Code | Status | Description | Has headers | Schema | -| -------------------------------- | ----------- | --------------- | :---------: | ------------------------------------------ | -| [200](#route-get-alert-rule-200) | OK | AlertRule | | [schema](#route-get-alert-rule-200-schema) | -| [400](#route-get-alert-rule-400) | Bad Request | ValidationError | | [schema](#route-get-alert-rule-400-schema) | +| Code | Status | Description | Has headers | Schema | +| -------------------------------- | --------- | -------------------- | :---------: | ------------------------------------------ | +| [200](#route-get-alert-rule-200) | OK | ProvisionedAlertRule | | [schema](#route-get-alert-rule-200-schema) | +| [404](#route-get-alert-rule-404) | Not Found | Not found. | | [schema](#route-get-alert-rule-404-schema) | #### Responses -##### 200 - AlertRule +##### 200 - ProvisionedAlertRule Status: OK ###### Schema -[AlertRule](#alert-rule) +[ProvisionedAlertRule](#provisioned-alert-rule) -##### 400 - ValidationError +##### 404 - Not found. -Status: Bad Request +Status: Not Found -###### Schema +###### Schema -[ValidationError](#validation-error) +### Export an alert rule in provisioning file format. (_RouteGetAlertRuleExport_) + +``` +GET /api/v1/provisioning/alert-rules/{UID}/export +``` + +#### Produces + +- application/json +- application/yaml +- text/yaml + +#### Parameters + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| -------- | ------- | ------- | -------- | --------- | :------: | ------- | -------------------------------------------------- | +| UID | `path` | string | `string` | | ✓ | | Alert rule UID | +| download | `query` | boolean | `bool` | | | | Whether to initiate a download of the file or not. | + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| --------------------------------------- | --------- | ------------------ | :---------: | ------------------------------------------------- | +| [200](#route-get-alert-rule-export-200) | OK | AlertingFileExport | | [schema](#route-get-alert-rule-export-200-schema) | +| [404](#route-get-alert-rule-export-404) | Not Found | Not found. | | [schema](#route-get-alert-rule-export-404-schema) | + +#### Responses + +##### 200 - AlertingFileExport + +Status: OK + +###### Schema + +[AlertingFileExport](#alerting-file-export) + +##### 404 - Not found. + +Status: Not Found + +###### Schema + +### Get a rule group. (_RouteGetAlertRuleGroup_) + +``` +GET /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group} +``` + +#### Parameters + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| --------- | ------ | ------ | -------- | --------- | :------: | ------- | ----------- | +| FolderUID | `path` | string | `string` | | ✓ | | | +| Group | `path` | string | `string` | | ✓ | | | + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| -------------------------------------- | --------- | -------------- | :---------: | ------------------------------------------------ | +| [200](#route-get-alert-rule-group-200) | OK | AlertRuleGroup | | [schema](#route-get-alert-rule-group-200-schema) | +| [404](#route-get-alert-rule-group-404) | Not Found | Not found. | | [schema](#route-get-alert-rule-group-404-schema) | + +#### Responses + +##### 200 - AlertRuleGroup + +Status: OK + +###### Schema + +[AlertRuleGroup](#alert-rule-group) + +##### 404 - Not found. + +Status: Not Found + +###### Schema + +### Export an alert rule group in provisioning file format. (_RouteGetAlertRuleGroupExport_) + +``` +GET /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export +``` + +#### Produces + +- application/json +- application/yaml +- text/yaml + +#### Parameters + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| --------- | ------- | ------- | -------- | --------- | :------: | ------- | -------------------------------------------------- | +| FolderUID | `path` | string | `string` | | ✓ | | | +| Group | `path` | string | `string` | | ✓ | | | +| download | `query` | boolean | `bool` | | | | Whether to initiate a download of the file or not. | + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| --------------------------------------------- | --------- | ------------------ | :---------: | ------------------------------------------------------- | +| [200](#route-get-alert-rule-group-export-200) | OK | AlertingFileExport | | [schema](#route-get-alert-rule-group-export-200-schema) | +| [404](#route-get-alert-rule-group-export-404) | Not Found | Not found. | | [schema](#route-get-alert-rule-group-export-404-schema) | + +#### Responses + +##### 200 - AlertingFileExport + +Status: OK + +###### Schema + +[AlertingFileExport](#alerting-file-export) + +##### 404 - Not found. + +Status: Not Found + +###### Schema + +### Get all the alert rules. (_RouteGetAlertRules_) + +``` +GET /api/v1/provisioning/alert-rules +``` + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| --------------------------------- | ------ | --------------------- | :---------: | ------------------------------------------- | +| [200](#route-get-alert-rules-200) | OK | ProvisionedAlertRules | | [schema](#route-get-alert-rules-200-schema) | + +#### Responses + +##### 200 - ProvisionedAlertRules + +Status: OK + +###### Schema + +[ProvisionedAlertRules](#provisioned-alert-rules) + +### Export all alert rules in provisioning file format. (_RouteGetAlertRulesExport_) + +``` +GET /api/v1/provisioning/alert-rules/export +``` + +#### Parameters + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| -------- | ------- | ------- | ------- | --------- | :------: | ------- | -------------------------------------------------- | +| download | `query` | boolean | `bool` | | | | Whether to initiate a download of the file or not. | + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| ---------------------------------------- | --------- | ------------------ | :---------: | -------------------------------------------------- | +| [200](#route-get-alert-rules-export-200) | OK | AlertingFileExport | | [schema](#route-get-alert-rules-export-200-schema) | +| [404](#route-get-alert-rules-export-404) | Not Found | Not found. | | [schema](#route-get-alert-rules-export-404-schema) | + +#### Responses + +##### 200 - AlertingFileExport + +Status: OK + +###### Schema + +[AlertingFileExport](#alerting-file-export) + +##### 404 - Not found. + +Status: Not Found + +###### Schema ### Get all the contact points. (_RouteGetContactpoints_) @@ -254,30 +414,27 @@ Status: Bad Request GET /api/v1/provisioning/contact-points ``` +#### Parameters + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------- | ------ | -------- | --------- | :------: | ------- | -------------- | +| name | `query` | string | `string` | | | | Filter by name | + #### All responses -| Code | Status | Description | Has headers | Schema | -| ----------------------------------- | ----------- | --------------- | :---------: | --------------------------------------------- | -| [200](#route-get-contactpoints-200) | OK | Route | | [schema](#route-get-contactpoints-200-schema) | -| [400](#route-get-contactpoints-400) | Bad Request | ValidationError | | [schema](#route-get-contactpoints-400-schema) | +| Code | Status | Description | Has headers | Schema | +| ----------------------------------- | ------ | ------------- | :---------: | --------------------------------------------- | +| [200](#route-get-contactpoints-200) | OK | ContactPoints | | [schema](#route-get-contactpoints-200-schema) | #### Responses -##### 200 - Route +##### 200 - ContactPoints Status: OK ###### Schema -[Route](#route) - -##### 400 - ValidationError - -Status: Bad Request - -###### Schema - -[ValidationError](#validation-error) +[ContactPoints](#contact-points) ### Get a mute timing. (_RouteGetMuteTiming_) @@ -287,16 +444,16 @@ GET /api/v1/provisioning/mute-timings/{name} #### Parameters -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ------------- | -| name | `path` | string | `string` | | ✓ | | Template Name | +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ---------------- | +| name | `path` | string | `string` | | ✓ | | Mute timing name | #### All responses -| Code | Status | Description | Has headers | Schema | -| --------------------------------- | ----------- | ---------------- | :---------: | ------------------------------------------- | -| [200](#route-get-mute-timing-200) | OK | MuteTimeInterval | | [schema](#route-get-mute-timing-200-schema) | -| [400](#route-get-mute-timing-400) | Bad Request | ValidationError | | [schema](#route-get-mute-timing-400-schema) | +| Code | Status | Description | Has headers | Schema | +| --------------------------------- | --------- | ---------------- | :---------: | ------------------------------------------- | +| [200](#route-get-mute-timing-200) | OK | MuteTimeInterval | | [schema](#route-get-mute-timing-200-schema) | +| [404](#route-get-mute-timing-404) | Not Found | Not found. | | [schema](#route-get-mute-timing-404-schema) | #### Responses @@ -308,13 +465,11 @@ Status: OK [MuteTimeInterval](#mute-time-interval) -##### 400 - ValidationError +##### 404 - Not found. -Status: Bad Request +Status: Not Found -###### Schema - -[ValidationError](#validation-error) +###### Schema ### Get all the mute timings. (_RouteGetMuteTimings_) @@ -324,10 +479,9 @@ GET /api/v1/provisioning/mute-timings #### All responses -| Code | Status | Description | Has headers | Schema | -| ---------------------------------- | ----------- | --------------- | :---------: | -------------------------------------------- | -| [200](#route-get-mute-timings-200) | OK | MuteTimings | | [schema](#route-get-mute-timings-200-schema) | -| [400](#route-get-mute-timings-400) | Bad Request | ValidationError | | [schema](#route-get-mute-timings-400-schema) | +| Code | Status | Description | Has headers | Schema | +| ---------------------------------- | ------ | ----------- | :---------: | -------------------------------------------- | +| [200](#route-get-mute-timings-200) | OK | MuteTimings | | [schema](#route-get-mute-timings-200-schema) | #### Responses @@ -339,14 +493,6 @@ Status: OK [MuteTimings](#mute-timings) -##### 400 - ValidationError - -Status: Bad Request - -###### Schema - -[ValidationError](#validation-error) - ### Get the notification policy tree. (_RouteGetPolicyTree_) ``` @@ -355,10 +501,9 @@ GET /api/v1/provisioning/policies #### All responses -| Code | Status | Description | Has headers | Schema | -| --------------------------------- | ----------- | --------------- | :---------: | ------------------------------------------- | -| [200](#route-get-policy-tree-200) | OK | Route | | [schema](#route-get-policy-tree-200-schema) | -| [400](#route-get-policy-tree-400) | Bad Request | ValidationError | | [schema](#route-get-policy-tree-400-schema) | +| Code | Status | Description | Has headers | Schema | +| --------------------------------- | ------ | ----------- | :---------: | ------------------------------------------- | +| [200](#route-get-policy-tree-200) | OK | Route | | [schema](#route-get-policy-tree-200-schema) | #### Responses @@ -370,14 +515,6 @@ Status: OK [Route](#route) -##### 400 - ValidationError - -Status: Bad Request - -###### Schema - -[ValidationError](#validation-error) - ### Get a notification template. (_RouteGetTemplate_) ``` @@ -395,7 +532,7 @@ GET /api/v1/provisioning/templates/{name} | Code | Status | Description | Has headers | Schema | | ------------------------------ | --------- | -------------------- | :---------: | ---------------------------------------- | | [200](#route-get-template-200) | OK | NotificationTemplate | | [schema](#route-get-template-200-schema) | -| [404](#route-get-template-404) | Not Found | NotFound | | [schema](#route-get-template-404-schema) | +| [404](#route-get-template-404) | Not Found | Not found. | | [schema](#route-get-template-404-schema) | #### Responses @@ -405,16 +542,14 @@ Status: OK ###### Schema -[NotificationTemplate](#message-template) +[NotificationTemplate](#notification-template) -##### 404 - NotFound +##### 404 - Not found. Status: Not Found ###### Schema -[NotFound](#not-found) - ### Get all notification templates. (_RouteGetTemplates_) ``` @@ -423,28 +558,26 @@ GET /api/v1/provisioning/templates #### All responses -| Code | Status | Description | Has headers | Schema | -| ------------------------------- | ----------- | -------------------- | :---------: | ----------------------------------------- | -| [200](#route-get-templates-200) | OK | NotificationTemplate | | [schema](#route-get-templates-200-schema) | -| [400](#route-get-templates-400) | Bad Request | ValidationError | | [schema](#route-get-templates-400-schema) | +| Code | Status | Description | Has headers | Schema | +| ------------------------------- | --------- | --------------------- | :---------: | ----------------------------------------- | +| [200](#route-get-templates-200) | OK | NotificationTemplates | | [schema](#route-get-templates-200-schema) | +| [404](#route-get-templates-404) | Not Found | Not found. | | [schema](#route-get-templates-404-schema) | #### Responses -##### 200 - NotificationTemplate +##### 200 - NotificationTemplates Status: OK ###### Schema -[NotificationTemplate](#message-template) +[NotificationTemplates](#notification-templates) -##### 400 - ValidationError +##### 404 - Not found. -Status: Bad Request +Status: Not Found -###### Schema - -[ValidationError](#validation-error) +###### Schema ### Create a new alert rule. (_RoutePostAlertRule_) @@ -452,28 +585,33 @@ Status: Bad Request POST /api/v1/provisioning/alert-rules ``` +#### Consumes + +- application/json + #### Parameters -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | ------------------------ | ------------------ | --------- | :------: | ------- | ----------- | -| Body | `body` | [AlertRule](#alert-rule) | `models.AlertRule` | | | | | +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| -------------------- | -------- | ----------------------------------------------- | ----------------------------- | --------- | :------: | ------- | ----------- | +| X-Disable-Provenance | `header` | string | `string` | | | | | +| Body | `body` | [ProvisionedAlertRule](#provisioned-alert-rule) | `models.ProvisionedAlertRule` | | | | | #### All responses -| Code | Status | Description | Has headers | Schema | -| --------------------------------- | ----------- | --------------- | :---------: | ------------------------------------------- | -| [201](#route-post-alert-rule-201) | Created | AlertRule | | [schema](#route-post-alert-rule-201-schema) | -| [400](#route-post-alert-rule-400) | Bad Request | ValidationError | | [schema](#route-post-alert-rule-400-schema) | +| Code | Status | Description | Has headers | Schema | +| --------------------------------- | ----------- | -------------------- | :---------: | ------------------------------------------- | +| [201](#route-post-alert-rule-201) | Created | ProvisionedAlertRule | | [schema](#route-post-alert-rule-201-schema) | +| [400](#route-post-alert-rule-400) | Bad Request | ValidationError | | [schema](#route-post-alert-rule-400-schema) | #### Responses -##### 201 - AlertRule +##### 201 - ProvisionedAlertRule Status: Created ###### Schema -[AlertRule](#alert-rule) +[ProvisionedAlertRule](#provisioned-alert-rule) ##### 400 - ValidationError @@ -501,20 +639,20 @@ POST /api/v1/provisioning/contact-points #### All responses -| Code | Status | Description | Has headers | Schema | -| ------------------------------------ | ----------- | --------------- | :---------: | ---------------------------------------------- | -| [202](#route-post-contactpoints-202) | Accepted | Ack | | [schema](#route-post-contactpoints-202-schema) | -| [400](#route-post-contactpoints-400) | Bad Request | ValidationError | | [schema](#route-post-contactpoints-400-schema) | +| Code | Status | Description | Has headers | Schema | +| ------------------------------------ | ----------- | -------------------- | :---------: | ---------------------------------------------- | +| [202](#route-post-contactpoints-202) | Accepted | EmbeddedContactPoint | | [schema](#route-post-contactpoints-202-schema) | +| [400](#route-post-contactpoints-400) | Bad Request | ValidationError | | [schema](#route-post-contactpoints-400-schema) | #### Responses -##### 202 - Ack +##### 202 - EmbeddedContactPoint Status: Accepted ###### Schema -[Ack](#ack) +[EmbeddedContactPoint](#embedded-contact-point) ##### 400 - ValidationError @@ -577,27 +715,28 @@ PUT /api/v1/provisioning/alert-rules/{UID} #### Parameters -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | ------------------------ | ------------------ | --------- | :------: | ------- | ----------- | -| UID | `path` | string | `string` | | ✓ | | | -| Body | `body` | [AlertRule](#alert-rule) | `models.AlertRule` | | | | | +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| -------------------- | -------- | ----------------------------------------------- | ----------------------------- | --------- | :------: | ------- | -------------- | +| UID | `path` | string | `string` | | ✓ | | Alert rule UID | +| X-Disable-Provenance | `header` | string | `string` | | | | | +| Body | `body` | [ProvisionedAlertRule](#provisioned-alert-rule) | `models.ProvisionedAlertRule` | | | | | #### All responses -| Code | Status | Description | Has headers | Schema | -| -------------------------------- | ----------- | --------------- | :---------: | ------------------------------------------ | -| [200](#route-put-alert-rule-200) | OK | AlertRule | | [schema](#route-put-alert-rule-200-schema) | -| [400](#route-put-alert-rule-400) | Bad Request | ValidationError | | [schema](#route-put-alert-rule-400-schema) | +| Code | Status | Description | Has headers | Schema | +| -------------------------------- | ----------- | -------------------- | :---------: | ------------------------------------------ | +| [200](#route-put-alert-rule-200) | OK | ProvisionedAlertRule | | [schema](#route-put-alert-rule-200-schema) | +| [400](#route-put-alert-rule-400) | Bad Request | ValidationError | | [schema](#route-put-alert-rule-400-schema) | #### Responses -##### 200 - AlertRule +##### 200 - ProvisionedAlertRule Status: OK ###### Schema -[AlertRule](#alert-rule) +[ProvisionedAlertRule](#provisioned-alert-rule) ##### 400 - ValidationError @@ -662,10 +801,10 @@ PUT /api/v1/provisioning/contact-points/{UID} #### Parameters -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | ----------------------------------------------- | ----------------------------- | --------- | :------: | ------- | ------------------------------------------------- | -| UID | `path` | string | `string` | | ✓ | | UID should be the contact point unique identifier | -| Body | `body` | [EmbeddedContactPoint](#embedded-contact-point) | `models.EmbeddedContactPoint` | | | | | +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------ | ----------------------------------------------- | ----------------------------- | --------- | :------: | ------- | ------------------------------------------ | +| UID | `path` | string | `string` | | ✓ | | UID is the contact point unique identifier | +| Body | `body` | [EmbeddedContactPoint](#embedded-contact-point) | `models.EmbeddedContactPoint` | | | | | #### All responses @@ -704,10 +843,10 @@ PUT /api/v1/provisioning/mute-timings/{name} #### Parameters -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | --------------------------------------- | ------------------------- | --------- | :------: | ------- | ------------- | -| name | `path` | string | `string` | | ✓ | | Template Name | -| Body | `body` | [MuteTimeInterval](#mute-time-interval) | `models.MuteTimeInterval` | | | | | +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------ | --------------------------------------- | ------------------------- | --------- | :------: | ------- | ---------------- | +| name | `path` | string | `string` | | ✓ | | Mute timing name | +| Body | `body` | [MuteTimeInterval](#mute-time-interval) | `models.MuteTimeInterval` | | | | | #### All responses @@ -746,9 +885,9 @@ PUT /api/v1/provisioning/policies #### Parameters -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | --------------- | -------------- | --------- | :------: | ------- | ----------- | -| Body | `body` | [Route](#route) | `models.Route` | | | | | +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------ | --------------- | -------------- | --------- | :------: | ------- | ---------------------------------------- | +| Body | `body` | [Route](#route) | `models.Route` | | | | The new notification routing tree to use | #### All responses @@ -775,7 +914,7 @@ Status: Bad Request [ValidationError](#validation-error) -### Updates an existing template. (_RoutePutTemplate_) +### Updates an existing notification template. (_RoutePutTemplate_) ``` PUT /api/v1/provisioning/templates/{name} @@ -787,27 +926,27 @@ PUT /api/v1/provisioning/templates/{name} #### Parameters -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | -------------------------------------------------------- | ------------------------------------ | --------- | :------: | ------- | ------------- | -| name | `path` | string | `string` | | ✓ | | Template Name | -| Body | `body` | [NotificationTemplateContent](#message-template-content) | `models.NotificationTemplateContent` | | | | | +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------ | ------------------------------------------------------------- | ------------------------------------ | --------- | :------: | ------- | ------------- | +| name | `path` | string | `string` | | ✓ | | Template Name | +| Body | `body` | [NotificationTemplateContent](#notification-template-content) | `models.NotificationTemplateContent` | | | | | #### All responses -| Code | Status | Description | Has headers | Schema | -| ------------------------------ | ----------- | --------------- | :---------: | ---------------------------------------- | -| [202](#route-put-template-202) | Accepted | Ack | | [schema](#route-put-template-202-schema) | -| [400](#route-put-template-400) | Bad Request | ValidationError | | [schema](#route-put-template-400-schema) | +| Code | Status | Description | Has headers | Schema | +| ------------------------------ | ----------- | -------------------- | :---------: | ---------------------------------------- | +| [202](#route-put-template-202) | Accepted | NotificationTemplate | | [schema](#route-put-template-202-schema) | +| [400](#route-put-template-400) | Bad Request | ValidationError | | [schema](#route-put-template-400-schema) | #### Responses -##### 202 - Ack +##### 202 - NotificationTemplate Status: Accepted ###### Schema -[Ack](#ack) +[NotificationTemplate](#notification-template) ##### 400 - ValidationError @@ -817,57 +956,116 @@ Status: Bad Request [ValidationError](#validation-error) +### Clears the notification policy tree. (_RouteResetPolicyTree_) + +``` +DELETE /api/v1/provisioning/policies +``` + +#### Consumes + +- application/json + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| ----------------------------------- | -------- | ----------- | :---------: | --------------------------------------------- | +| [202](#route-reset-policy-tree-202) | Accepted | Ack | | [schema](#route-reset-policy-tree-202-schema) | + +#### Responses + +##### 202 - Ack + +Status: Accepted + +###### Schema + +[Ack](#ack) + +## Models + +### Ack + +[interface{}](#interface) + ### AlertQuery **Properties** | Name | Type | Go type | Required | Default | Description | Example | | --------------------------------------------------------- | ----------------------------------------- | ------------------- | :------: | ------- | -------------------------------------------------------------------------------------------------- | ------- | -| DatasourceUID | string | `string` | | | Grafana data source unique identifier; it should be '-100' for a Server Side Expression operation. | | -| Model | object | `interface{}` | | | JSON is the raw JSON query and includes the above properties as well as custom properties. | | -| QueryType | string | `string` | | | QueryType is an optional identifier for the type of query. | +| datasourceUid | string | `string` | | | Grafana data source unique identifier; it should be '-100' for a Server Side Expression operation. | | +| model | [interface{}](#interface) | `interface{}` | | | JSON is the raw JSON query and includes the above properties as well as custom properties. | | +| queryType | string | `string` | | | QueryType is an optional identifier for the type of query. | | It can be used to distinguish different types of queries. | | -| RefID | string | `string` | | | RefID is the unique identifier of the query, set by the frontend call. | | +| refId | string | `string` | | | RefID is the unique identifier of the query, set by the frontend call. | | | relativeTimeRange | [RelativeTimeRange](#relative-time-range) | `RelativeTimeRange` | | | | | -### AlertRule +### AlertQueryExport **Properties** -| Name | Type | Go type | Required | Default | Description | Example | -| ------------ | ---------------------------- | ------------------- | :------: | ------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Annotations | map of string | `map[string]string` | | | | `{"runbook_url":"https://supercoolrunbook.com/page/13"}` | -| Condition | string | `string` | ✓ | | | `A` | -| Data | [][alertquery](#alert-query) | `[]*AlertQuery` | ✓ | | | `[{"datasourceUid":"-100","model":{"conditions":[{"evaluator":{"params":[0,0],"type":"gt"},"operator":{"type":"and"},"query":{"params":[]},"reducer":{"params":[],"type":"avg"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1 == 1","hide":false,"intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"},"queryType":"","refId":"A","relativeTimeRange":{"from":0,"to":0}}]` | -| ExecErrState | string | `string` | ✓ | | Allowed values: "OK", "Alerting", "Error" | | -| FolderUID | string | `string` | ✓ | | | `project_x` | -| ID | int64 (formatted integer) | `int64` | | | | | -| Labels | map of string | `map[string]string` | | | | `{"team":"sre-team-1"}` | -| NoDataState | string | `string` | ✓ | | Allowed values: "OK", "NoData", "Error" | | -| OrgID | int64 (formatted integer) | `int64` | ✓ | | | | -| RuleGroup | string | `string` | ✓ | | | `eval_group_1` | -| Title | string | `string` | ✓ | | | `Always firing` | -| UID | string | `string` | | | | | -| Updated | date-time (formatted string) | `strfmt.DateTime` | | | | | -| for | [Duration](#duration) | `Duration` | ✓ | | | | -| provenance | string | `Provenance` | | | | | +| Name | Type | Go type | Required | Default | Description | Example | +| ----------------- | ----------------------------------------- | ------------------- | :------: | ------- | ----------- | ------- | +| datasourceUid | string | `string` | | | | | +| model | [interface{}](#interface) | `interface{}` | | | | | +| queryType | string | `string` | | | | | +| refId | string | `string` | | | | | +| relativeTimeRange | [RelativeTimeRange](#relative-time-range) | `RelativeTimeRange` | | | | | + +### AlertRuleExport + +**Properties** + +| Name | Type | Go type | Required | Default | Description | Example | +| ------------ | ----------------------------------------- | --------------------- | :------: | ------- | ----------- | ------- | +| annotations | map of string | `map[string]string` | | | | | +| condition | string | `string` | | | | | +| dasboardUid | string | `string` | | | | | +| data | [][alertqueryexport](#alert-query-export) | `[]*AlertQueryExport` | | | | | +| execErrState | string | `string` | | | | | +| for | [Duration](#duration) | `Duration` | | | | | +| labels | map of string | `map[string]string` | | | | | +| noDataState | string | `string` | | | | | +| panelId | int64 (formatted integer) | `int64` | | | | | +| title | string | `string` | | | | | +| uid | string | `string` | | | | | ### AlertRuleGroup **Properties** -| Name | Type | Go type | Required | Default | Description | Example | -| -------- | ------------------------- | ------- | :------: | ------- | ----------- | ------- | -| Interval | int64 (formatted integer) | `int64` | | | | | +| Name | Type | Go type | Required | Default | Description | Example | +| --------- | ------------------------------------------------- | ------------------------- | :------: | ------- | ----------- | ------- | +| folderUid | string | `string` | | | | | +| interval | int64 (formatted integer) | `int64` | | | | | +| rules | [][provisionedalertrule](#provisioned-alert-rule) | `[]*ProvisionedAlertRule` | | | | | +| title | string | `string` | | | | | -### DayOfMonthRange +### AlertRuleGroupExport **Properties** -| Name | Type | Go type | Required | Default | Description | Example | -| ----- | ------------------------- | ------- | :------: | ------- | ----------- | ------- | -| Begin | int64 (formatted integer) | `int64` | | | | | -| End | int64 (formatted integer) | `int64` | | | | | +| Name | Type | Go type | Required | Default | Description | Example | +| -------- | --------------------------------------- | -------------------- | :------: | ------- | ----------- | ------- | +| folder | string | `string` | | | | | +| interval | [Duration](#duration) | `Duration` | | | | | +| name | string | `string` | | | | | +| orgId | int64 (formatted integer) | `int64` | | | | | +| rules | [][alertruleexport](#alert-rule-export) | `[]*AlertRuleExport` | | | | | + +### AlertingFileExport + +**Properties** + +| Name | Type | Go type | Required | Default | Description | Example | +| ---------- | -------------------------------------------------- | ------------------------- | :------: | ------- | ----------- | ------- | +| apiVersion | int64 (formatted integer) | `int64` | | | | | +| groups | [][alertrulegroupexport](#alert-rule-group-export) | `[]*AlertRuleGroupExport` | | | | | + +### ContactPoints + +[][embeddedcontactpoint](#embedded-contact-point) ### Duration @@ -877,25 +1075,35 @@ Status: Bad Request ### EmbeddedContactPoint -> EmbeddedContactPoint is the contact point integration that is used +> EmbeddedContactPoint is the contact point type that is used > by grafanas embedded alertmanager implementation. **Properties** -| Name | Type | Go type | Required | Default | Description | Example | -| --------------------- | ------- | -------- | :------: | ------- | ---------------------------------------------------------------------------------------------------- | ----------------------- | -| DisableResolveMessage | boolean | `bool` | | | | `false` | -| Name | string | `string` | ✓ | | Name is used as grouping key in the UI. Contact points with the same name will be grouped in the UI. | `webhook_1` | -| Provenance | string | `string` | | | | | -| Type | string | `string` | ✓ | | | `webhook` | -| UID | string | `string` | | | UID is the unique identifier of the contact point. The UID can be set by the user. | `my_external_reference` | -| settings | object | `JSON` | ✓ | | | | +| Name | Type | Go type | Required | Default | Description | Example | +| ------------------------------------ | ----------------------- | -------- | :------: | ------- | ----------------------------------------------------------------- | --------- | +| disableResolveMessage | boolean | `bool` | | | | `false` | +| name | string | `string` | | | Name is used as grouping key in the UI. Contact points with the | +| same name will be grouped in the UI. | `webhook_1` | +| provenance | string | `string` | | | | | +| settings | [JSON](#json) | `JSON` | ✓ | | | | +| type | string | `string` | ✓ | | | `webhook` | +| uid | string | `string` | | | UID is the unique identifier of the contact point. The UID can be | +| set by the user. | `my_external_reference` | + +### Json + +[interface{}](#interface) + +### MatchRegexps + +[MatchRegexps](#match-regexps) ### MatchType -| Name | Type | Go type | Default | Description | Example | -| --------- | ------------------------- | ------- | ------- | ---------------------------------------------------------------------- | ------- | -| MatchType | int64 (formatted integer) | int64 | | 0 = MatchEqual, 1 = MatchNotEqual, 2 = MatchRegexp, 3 = MatchNotRegexp | | +| Name | Type | Go type | Default | Description | Example | +| --------- | ------------------------- | ------- | ------- | ----------- | ------- | +| MatchType | int64 (formatted integer) | int64 | | | | ### Matcher @@ -915,49 +1123,40 @@ Status: Bad Request [][matcher](#matcher) -### NotificationTemplate - -**Properties** - -| Name | Type | Go type | Required | Default | Description | Example | -| ---------- | ------ | ------------ | :------: | ------- | ----------- | ------- | -| Name | string | `string` | | | | | -| Template | string | `string` | | | | | -| provenance | string | `Provenance` | | | | | - -### NotificationTemplateContent - -**Properties** - -| Name | Type | Go type | Required | Default | Description | Example | -| -------- | ------ | -------- | :------: | ------- | ----------- | ------- | -| Template | string | `string` | | | | | - -### MonthRange - -**Properties** - -| Name | Type | Go type | Required | Default | Description | Example | -| ----- | ------------------------- | ------- | :------: | ------- | ----------- | ------- | -| Begin | int64 (formatted integer) | `int64` | | | | | -| End | int64 (formatted integer) | `int64` | | | | | - ### MuteTimeInterval **Properties** -| Name | Type | Go type | Required | Default | Description | Example | -| ------------- | -------------------------------- | ----------------- | :------: | ------- | ----------- | ------- | -| Name | string | `string` | | | | | -| TimeIntervals | [][timeinterval](#time-interval) | `[]*TimeInterval` | | | | | +| Name | Type | Go type | Required | Default | Description | Example | +| -------------- | -------------------------------- | ----------------- | :------: | ------- | ----------- | ------- | +| name | string | `string` | | | | | +| time_intervals | [][timeinterval](#time-interval) | `[]*TimeInterval` | | | | | ### MuteTimings [][mutetimeinterval](#mute-time-interval) -### NotFound +### NotificationTemplate -[interface{}](#interface) +**Properties** + +| Name | Type | Go type | Required | Default | Description | Example | +| ---------- | ------------------------- | ------------ | :------: | ------- | ----------- | ------- | +| name | string | `string` | | | | | +| provenance | [Provenance](#provenance) | `Provenance` | | | | | +| template | string | `string` | | | | | + +### NotificationTemplateContent + +**Properties** + +| Name | Type | Go type | Required | Default | Description | Example | +| -------- | ------ | -------- | :------: | ------- | ----------- | ------- | +| template | string | `string` | | | | | + +### NotificationTemplates + +[][notificationtemplate](#notification-template) ### ObjectMatchers @@ -965,6 +1164,45 @@ Status: Bad Request #### Inlined models +### Provenance + +| Name | Type | Go type | Default | Description | Example | +| ---------- | ------ | ------- | ------- | ----------- | ------- | +| Provenance | string | string | | | | + +### ProvisionedAlertRule + +**Properties** + +| Name | Type | Go type | Required | Default | Description | Example | +| ------------ | ---------------------------- | ------------------- | :------: | ------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| annotations | map of string | `map[string]string` | | | | `{"runbook_url":"https://supercoolrunbook.com/page/13"}` | +| condition | string | `string` | ✓ | | | `A` | +| data | [][alertquery](#alert-query) | `[]*AlertQuery` | ✓ | | | `[{"datasourceUid":"-100","model":{"conditions":[{"evaluator":{"params":[0,0],"type":"gt"},"operator":{"type":"and"},"query":{"params":[]},"reducer":{"params":[],"type":"avg"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1 == 1","hide":false,"intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"},"queryType":"","refId":"A","relativeTimeRange":{"from":0,"to":0}}]` | +| execErrState | string | `string` | ✓ | | | | +| folderUID | string | `string` | ✓ | | | `project_x` | +| for | [Duration](#duration) | `Duration` | ✓ | | | | +| id | int64 (formatted integer) | `int64` | | | | | +| labels | map of string | `map[string]string` | | | | `{"team":"sre-team-1"}` | +| noDataState | string | `string` | ✓ | | | | +| orgID | int64 (formatted integer) | `int64` | ✓ | | | | +| provenance | [Provenance](#provenance) | `Provenance` | | | | | +| ruleGroup | string | `string` | ✓ | | | `eval_group_1` | +| title | string | `string` | ✓ | | | `Always firing` | +| uid | string | `string` | | | | | +| updated | date-time (formatted string) | `strfmt.DateTime` | | | | | + +### ProvisionedAlertRules + +[][provisionedalertrule](#provisioned-alert-rule) + +### Regexp + +> A Regexp is safe for concurrent use by multiple goroutines, +> except for configuration methods, such as Longest. + +[interface{}](#interface) + ### RelativeTimeRange > RelativeTimeRange is the per query start and end time @@ -979,22 +1217,26 @@ Status: Bad Request ### Route -> A Route is a node that contains definitions of how to handle alerts. +> A Route is a node that contains definitions of how to handle alerts. This is modified +> from the upstream alertmanager in that it adds the ObjectMatchers property. **Properties** -| Name | Type | Go type | Required | Default | Description | Example | -| ----------------- | ---------------------------------- | ---------------- | :------: | ------- | ----------- | ------- | -| Continue | boolean | `bool` | | | | | -| GroupByStr | []string | `[]string` | | | | | -| MuteTimeIntervals | []string | `[]string` | | | | | -| Receiver | string | `string` | | | | | -| Routes | [][route](#route) | `[]*Route` | | | | | -| group_interval | [Duration](#duration) | `Duration` | | | | | -| group_wait | [Duration](#duration) | `Duration` | | | | | -| object_matchers | [ObjectMatchers](#object-matchers) | `ObjectMatchers` | | | | | -| provenance | string | `Provenance` | | | | | -| repeat_interval | [Duration](#duration) | `Duration` | | | | | +| Name | Type | Go type | Required | Default | Description | Example | +| ------------------- | ---------------------------------- | ------------------- | :------: | ------- | --------------------------------------- | ------- | +| continue | boolean | `bool` | | | | | +| group_by | []string | `[]string` | | | | | +| group_interval | string | `string` | | | | | +| group_wait | string | `string` | | | | | +| match | map of string | `map[string]string` | | | Deprecated. Remove before v1.0 release. | | +| match_re | [MatchRegexps](#match-regexps) | `MatchRegexps` | | | | | +| matchers | [Matchers](#matchers) | `Matchers` | | | | | +| mute_time_intervals | []string | `[]string` | | | | | +| object_matchers | [ObjectMatchers](#object-matchers) | `ObjectMatchers` | | | | | +| provenance | [Provenance](#provenance) | `Provenance` | | | | | +| receiver | string | `string` | | | | | +| repeat_interval | string | `string` | | | | | +| routes | [][route](#route) | `[]*Route` | | | | | ### TimeInterval @@ -1003,13 +1245,14 @@ Status: Bad Request **Properties** -| Name | Type | Go type | Required | Default | Description | Example | -| ----------- | ---------------------------------------- | -------------------- | :------: | ------- | ----------- | ------- | -| DaysOfMonth | [][dayofmonthrange](#day-of-month-range) | `[]*DayOfMonthRange` | | | | | -| Months | [][monthrange](#month-range) | `[]*MonthRange` | | | | | -| Times | [][timerange](#time-range) | `[]*TimeRange` | | | | | -| Weekdays | [][weekdayrange](#weekday-range) | `[]*WeekdayRange` | | | | | -| Years | [][yearrange](#year-range) | `[]*YearRange` | | | | | +| Name | Type | Go type | Required | Default | Description | Example | +| ------------- | -------------------------- | -------------- | :------: | ------- | ----------- | ------- | +| days_of_month | []string | `[]string` | | | | | +| location | string | `string` | | | | | +| months | []string | `[]string` | | | | | +| times | [][timerange](#time-range) | `[]*TimeRange` | | | | | +| weekdays | []string | `[]string` | | | | | +| years | []string | `[]string` | | | | | ### TimeRange @@ -1026,24 +1269,6 @@ Status: Bad Request **Properties** -| Name | Type | Go type | Required | Default | Description | Example | -| ---- | ------ | -------- | :------: | ------- | ----------- | ------- | -| Msg | string | `string` | | | | | - -### WeekdayRange - -**Properties** - -| Name | Type | Go type | Required | Default | Description | Example | -| ----- | ------------------------- | ------- | :------: | ------- | ----------- | ------- | -| Begin | int64 (formatted integer) | `int64` | | | | | -| End | int64 (formatted integer) | `int64` | | | | | - -### YearRange - -**Properties** - -| Name | Type | Go type | Required | Default | Description | Example | -| ----- | ------------------------- | ------- | :------: | ------- | ----------- | ------- | -| Begin | int64 (formatted integer) | `int64` | | | | | -| End | int64 (formatted integer) | `int64` | | | | | +| Name | Type | Go type | Required | Default | Description | Example | +| ---- | ------ | -------- | :------: | ------- | ----------- | --------------- | +| msg | string | `string` | | | | `error message` | diff --git a/pkg/api/response/response.go b/pkg/api/response/response.go index d131bff1caa..aa8839d169f 100644 --- a/pkg/api/response/response.go +++ b/pkg/api/response/response.go @@ -9,6 +9,7 @@ import ( "reflect" jsoniter "github.com/json-iterator/go" + "gopkg.in/yaml.v3" "github.com/grafana/grafana/pkg/infra/tracing" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" @@ -173,7 +174,8 @@ func (r *RedirectResponse) Body() []byte { // JSON creates a JSON response. func JSON(status int, body interface{}) *NormalResponse { - return Respond(status, body).SetHeader("Content-Type", "application/json") + return Respond(status, body). + SetHeader("Content-Type", "application/json") } // JSONStreaming creates a streaming JSON response. @@ -187,6 +189,30 @@ func JSONStreaming(status int, body interface{}) StreamingResponse { } } +// JSONDownload creates a JSON response indicating that it should be downloaded. +func JSONDownload(status int, body interface{}, filename string) *NormalResponse { + return JSON(status, body). + SetHeader("Content-Disposition", fmt.Sprintf(`attachment;filename="%s"`, filename)) +} + +// YAML creates a YAML response. +func YAML(status int, body interface{}) *NormalResponse { + b, err := yaml.Marshal(body) + if err != nil { + return Error(http.StatusInternalServerError, "body yaml marshal", err) + } + // As of now, application/yaml is downloaded by default in chrome regardless of Content-Disposition, so we use text/yaml instead. + return Respond(status, b). + SetHeader("Content-Type", "text/yaml") +} + +// YAMLDownload creates a YAML response indicating that it should be downloaded. +func YAMLDownload(status int, body interface{}, filename string) *NormalResponse { + return YAML(status, body). + SetHeader("Content-Type", "application/yaml"). + SetHeader("Content-Disposition", fmt.Sprintf(`attachment;filename="%s"`, filename)) +} + // Success create a successful response func Success(message string) *NormalResponse { resp := make(map[string]interface{}) diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index 35f2e8c99bf..93c3e3696bf 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -3,7 +3,9 @@ package api import ( "context" "errors" + "fmt" "net/http" + "strings" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" @@ -12,6 +14,7 @@ import ( alerting_models "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/provisioning" "github.com/grafana/grafana/pkg/services/ngalert/store" + "github.com/grafana/grafana/pkg/services/provisioning/alerting/file" "github.com/grafana/grafana/pkg/util" ) @@ -60,6 +63,9 @@ type AlertRuleService interface { DeleteAlertRule(ctx context.Context, orgID int64, ruleUID string, provenance alerting_models.Provenance) error GetRuleGroup(ctx context.Context, orgID int64, folder, group string) (alerting_models.AlertRuleGroup, error) ReplaceRuleGroup(ctx context.Context, orgID int64, group alerting_models.AlertRuleGroup, userID int64, provenance alerting_models.Provenance) error + GetAlertRuleWithFolderTitle(ctx context.Context, orgID int64, ruleUID string) (provisioning.AlertRuleWithFolderTitle, error) + GetAlertRuleGroupWithFolderTitle(ctx context.Context, orgID int64, folder, group string) (file.AlertRuleGroupWithFolderTitle, error) + GetAlertGroupsWithFolderTitle(ctx context.Context, orgID int64) ([]file.AlertRuleGroupWithFolderTitle, error) } func (srv *ProvisioningSrv) RouteGetPolicyTree(c *contextmodel.ReqContext) response.Response { @@ -334,6 +340,66 @@ func (srv *ProvisioningSrv) RouteGetAlertRuleGroup(c *contextmodel.ReqContext, f return response.JSON(http.StatusOK, definitions.NewAlertRuleGroupFromModel(g)) } +// RouteGetAlertRulesExport retrieves all alert rules in a format compatible with file provisioning. +func (srv *ProvisioningSrv) RouteGetAlertRulesExport(c *contextmodel.ReqContext) response.Response { + groupsWithTitle, err := srv.alertRules.GetAlertGroupsWithFolderTitle(c.Req.Context(), c.OrgID) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "failed to get alert rules") + } + + e, err := file.NewAlertingFileExport(groupsWithTitle) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "failed to create alerting file export") + } + + return exportResponse(c, e) +} + +// RouteGetAlertRuleGroupExport retrieves the given alert rule group in a format compatible with file provisioning. +func (srv *ProvisioningSrv) RouteGetAlertRuleGroupExport(c *contextmodel.ReqContext, folder string, group string) response.Response { + g, err := srv.alertRules.GetAlertRuleGroupWithFolderTitle(c.Req.Context(), c.OrgID, folder, group) + if err != nil { + if errors.Is(err, store.ErrAlertRuleGroupNotFound) { + return ErrResp(http.StatusNotFound, err, "") + } + return ErrResp(http.StatusInternalServerError, err, "failed to get alert rule group") + } + + e, err := file.NewAlertingFileExport([]file.AlertRuleGroupWithFolderTitle{g}) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "failed to create alerting file export") + } + + return exportResponse(c, e) +} + +// RouteGetAlertRuleExport retrieves the given alert rule in a format compatible with file provisioning. +func (srv *ProvisioningSrv) RouteGetAlertRuleExport(c *contextmodel.ReqContext, UID string) response.Response { + rule, err := srv.alertRules.GetAlertRuleWithFolderTitle(c.Req.Context(), c.OrgID, UID) + if err != nil { + if errors.Is(err, alerting_models.ErrAlertRuleNotFound) { + return ErrResp(http.StatusNotFound, err, "") + } + return ErrResp(http.StatusInternalServerError, err, "") + } + + e, err := file.NewAlertingFileExport([]file.AlertRuleGroupWithFolderTitle{{ + AlertRuleGroup: &alerting_models.AlertRuleGroup{ + Title: rule.AlertRule.RuleGroup, + FolderUID: rule.AlertRule.NamespaceUID, + Interval: rule.AlertRule.IntervalSeconds, + Rules: []alerting_models.AlertRule{rule.AlertRule}, + }, + OrgID: c.OrgID, + FolderTitle: rule.FolderTitle, + }}) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "failed to create alerting file export") + } + + return exportResponse(c, e) +} + func (srv *ProvisioningSrv) RoutePutAlertRuleGroup(c *contextmodel.ReqContext, ag definitions.AlertRuleGroup, folderUID string, group string) response.Response { ag.FolderUID = folderUID ag.Title = group @@ -360,3 +426,26 @@ func determineProvenance(ctx *contextmodel.ReqContext) alerting_models.Provenanc } return alerting_models.ProvenanceAPI } + +func exportResponse(c *contextmodel.ReqContext, body any) response.Response { + format := "json" + acceptHeader := c.Req.Header.Get("Accept") + if strings.Contains(acceptHeader, "yaml") && !strings.Contains(acceptHeader, "json") { + format = "yaml" + } + + download := c.QueryBoolWithDefault("download", false) + if download { + r := response.JSONDownload + if format == "yaml" { + r = response.YAMLDownload + } + return r(http.StatusOK, body, fmt.Sprintf("export.%s", format)) + } + + r := response.JSON + if format == "yaml" { + r = response.YAML + } + return r(http.StatusOK, body) +} diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index 3408e0a34c5..183bf0c8d91 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -5,18 +5,22 @@ import ( "encoding/json" "fmt" "net/http" + "net/http/httptest" + "net/url" "testing" "time" prometheus "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/timeinterval" "github.com/prometheus/common/model" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/provisioning" @@ -370,17 +374,394 @@ func TestProvisioningApi(t *testing.T) { }) }) }) + + t.Run("exports", func(t *testing.T) { + t.Run("alert rule group", func(t *testing.T) { + t.Run("are present, GET returns 200", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule", 1)) + + response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group") + + require.Equal(t, 200, response.Status()) + }) + + t.Run("are missing, GET returns 404", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule", 1)) + + response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "does not exist") + + require.Equal(t, 404, response.Status()) + }) + + t.Run("accept header contains yaml, GET returns text yaml", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule", 1)) + + rc.Context.Req.Header.Add("Accept", "application/yaml") + response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group") + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "text/yaml", rc.Context.Resp.Header().Get("Content-Type")) + }) + + t.Run("accept header contains json, GET returns json", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule", 1)) + + rc.Context.Req.Header.Add("Accept", "application/json") + response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group") + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "application/json", rc.Context.Resp.Header().Get("Content-Type")) + }) + + t.Run("accept header contains json and yaml, GET returns json", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule", 1)) + + rc.Context.Req.Header.Add("Accept", "application/json, application/yaml") + response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group") + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "application/json", rc.Context.Resp.Header().Get("Content-Type")) + }) + + t.Run("query param download=true, GET returns content disposition attachment", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule", 1)) + + rc.Context.Req.Form.Set("download", "true") + response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group") + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Contains(t, rc.Context.Resp.Header().Get("Content-Disposition"), "attachment") + }) + + t.Run("query param download=false, GET returns empty content disposition", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule", 1)) + + rc.Context.Req.Form.Set("download", "false") + response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group") + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "", rc.Context.Resp.Header().Get("Content-Disposition")) + }) + + t.Run("query param download not set, GET returns empty content disposition", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule", 1)) + + response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group") + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "", rc.Context.Resp.Header().Get("Content-Disposition")) + }) + + t.Run("json body content is as expected", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule1", 1)) + insertRule(t, sut, createTestAlertRule("rule2", 1)) + + expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"my-cool-group","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"-100"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s"},{"uid":"rule2","title":"rule2","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"-100"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s"}]}]}` + + response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group") + + require.Equal(t, 200, response.Status()) + require.Equal(t, expectedResponse, string(response.Body())) + }) + + t.Run("yaml body content is as expected", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule1", 1)) + insertRule(t, sut, createTestAlertRule("rule2", 1)) + + rc.Context.Req.Header.Add("Accept", "application/yaml") + expectedResponse := "apiVersion: 1\ngroups:\n - orgId: 1\n name: my-cool-group\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule1\n title: rule1\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: \"-100\"\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n - uid: rule2\n title: rule2\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: \"-100\"\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n" + + response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group") + + require.Equal(t, 200, response.Status()) + require.Equal(t, expectedResponse, string(response.Body())) + }) + }) + + t.Run("alert rule", func(t *testing.T) { + t.Run("are present, GET returns 200", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule1", 1)) + + response := sut.RouteGetAlertRuleExport(&rc, "rule1") + + require.Equal(t, 200, response.Status()) + }) + + t.Run("are missing, GET returns 404", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule1", 1)) + + response := sut.RouteGetAlertRuleExport(&rc, "rule404") + + require.Equal(t, 404, response.Status()) + }) + + t.Run("accept header contains yaml, GET returns text yaml", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule1", 1)) + + rc.Context.Req.Header.Add("Accept", "application/yaml") + response := sut.RouteGetAlertRuleExport(&rc, "rule1") + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "text/yaml", rc.Context.Resp.Header().Get("Content-Type")) + }) + + t.Run("accept header contains json, GET returns json", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule1", 1)) + + rc.Context.Req.Header.Add("Accept", "application/json") + response := sut.RouteGetAlertRuleExport(&rc, "rule1") + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "application/json", rc.Context.Resp.Header().Get("Content-Type")) + }) + + t.Run("accept header contains json and yaml, GET returns json", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule1", 1)) + + rc.Context.Req.Header.Add("Accept", "application/json, application/yaml") + response := sut.RouteGetAlertRuleExport(&rc, "rule1") + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "application/json", rc.Context.Resp.Header().Get("Content-Type")) + }) + + t.Run("query param download=true, GET returns content disposition attachment", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule1", 1)) + + rc.Context.Req.Form.Set("download", "true") + response := sut.RouteGetAlertRuleExport(&rc, "rule1") + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Contains(t, rc.Context.Resp.Header().Get("Content-Disposition"), "attachment") + }) + + t.Run("query param download=false, GET returns empty content disposition", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule1", 1)) + + rc.Context.Req.Form.Set("download", "false") + response := sut.RouteGetAlertRuleExport(&rc, "rule1") + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "", rc.Context.Resp.Header().Get("Content-Disposition")) + }) + + t.Run("query param download not set, GET returns empty content disposition", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule1", 1)) + + response := sut.RouteGetAlertRuleExport(&rc, "rule1") + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "", rc.Context.Resp.Header().Get("Content-Disposition")) + }) + + t.Run("json body content is as expected", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule1", 1)) + + expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"my-cool-group","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"-100"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s"}]}]}` + + response := sut.RouteGetAlertRuleExport(&rc, "rule1") + + require.Equal(t, 200, response.Status()) + require.Equal(t, expectedResponse, string(response.Body())) + }) + + t.Run("yaml body content is as expected", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule1", 1)) + + rc.Context.Req.Header.Add("Accept", "application/yaml") + expectedResponse := "apiVersion: 1\ngroups:\n - orgId: 1\n name: my-cool-group\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule1\n title: rule1\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: \"-100\"\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n" + + response := sut.RouteGetAlertRuleExport(&rc, "rule1") + + require.Equal(t, 200, response.Status()) + require.Equal(t, expectedResponse, string(response.Body())) + }) + }) + + t.Run("all alert rules", func(t *testing.T) { + t.Run("are present, GET returns 200", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule", 1)) + + response := sut.RouteGetAlertRulesExport(&rc) + + require.Equal(t, 200, response.Status()) + }) + + t.Run("accept header contains yaml, GET returns text yaml", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule", 1)) + + rc.Context.Req.Header.Add("Accept", "application/yaml") + response := sut.RouteGetAlertRulesExport(&rc) + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "text/yaml", rc.Context.Resp.Header().Get("Content-Type")) + }) + + t.Run("accept header contains json, GET returns json", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule", 1)) + + rc.Context.Req.Header.Add("Accept", "application/json") + response := sut.RouteGetAlertRulesExport(&rc) + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "application/json", rc.Context.Resp.Header().Get("Content-Type")) + }) + + t.Run("accept header contains json and yaml, GET returns json", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule", 1)) + + rc.Context.Req.Header.Add("Accept", "application/json, application/yaml") + response := sut.RouteGetAlertRulesExport(&rc) + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "application/json", rc.Context.Resp.Header().Get("Content-Type")) + }) + + t.Run("query param download=true, GET returns content disposition attachment", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule", 1)) + + rc.Context.Req.Form.Set("download", "true") + response := sut.RouteGetAlertRulesExport(&rc) + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Contains(t, rc.Context.Resp.Header().Get("Content-Disposition"), "attachment") + }) + + t.Run("query param download=false, GET returns empty content disposition", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule", 1)) + + rc.Context.Req.Form.Set("download", "false") + response := sut.RouteGetAlertRulesExport(&rc) + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "", rc.Context.Resp.Header().Get("Content-Disposition")) + }) + + t.Run("query param download not set, GET returns empty content disposition", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule", 1)) + + response := sut.RouteGetAlertRulesExport(&rc) + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "", rc.Context.Resp.Header().Get("Content-Disposition")) + }) + + t.Run("json body content is as expected", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRuleWithFolderAndGroup("rule1", 1, "folder-uid", "groupa")) + insertRule(t, sut, createTestAlertRuleWithFolderAndGroup("rule2", 1, "folder-uid", "groupb")) + insertRule(t, sut, createTestAlertRuleWithFolderAndGroup("rule3", 1, "folder-uid2", "groupb")) + + expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"groupa","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"-100"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s"}]},{"orgId":1,"name":"groupb","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule2","title":"rule2","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"-100"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s"}]},{"orgId":1,"name":"groupb","folder":"Folder Title2","interval":"1m","rules":[{"uid":"rule3","title":"rule3","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"-100"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s"}]}]}` + + response := sut.RouteGetAlertRulesExport(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, expectedResponse, string(response.Body())) + }) + + t.Run("yaml body content is as expected", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRuleWithFolderAndGroup("rule1", 1, "folder-uid", "groupa")) + insertRule(t, sut, createTestAlertRuleWithFolderAndGroup("rule2", 1, "folder-uid", "groupb")) + insertRule(t, sut, createTestAlertRuleWithFolderAndGroup("rule3", 1, "folder-uid2", "groupb")) + + rc.Context.Req.Header.Add("Accept", "application/yaml") + expectedResponse := "apiVersion: 1\ngroups:\n - orgId: 1\n name: groupa\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule1\n title: rule1\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: \"-100\"\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n - orgId: 1\n name: groupb\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule2\n title: rule2\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: \"-100\"\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n - orgId: 1\n name: groupb\n folder: Folder Title2\n interval: 1m\n rules:\n - uid: rule3\n title: rule3\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: \"-100\"\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n" + + response := sut.RouteGetAlertRulesExport(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, expectedResponse, string(response.Body())) + }) + }) + }) } // testEnvironment binds together common dependencies for testing alerting APIs. type testEnvironment struct { - secrets secrets.Service - log log.Logger - store store.DBstore - configs provisioning.AMConfigStore - xact provisioning.TransactionManager - quotas provisioning.QuotaChecker - prov provisioning.ProvisioningStore + secrets secrets.Service + log log.Logger + store store.DBstore + dashboardService dashboards.DashboardService + configs provisioning.AMConfigStore + xact provisioning.TransactionManager + quotas provisioning.QuotaChecker + prov provisioning.ProvisioningStore } func createTestEnv(t *testing.T) testEnvironment { @@ -407,14 +788,29 @@ func createTestEnv(t *testing.T) testEnvironment { prov.EXPECT().SaveSucceeds() prov.EXPECT().GetReturns(models.ProvenanceNone) + dashboardService := dashboards.NewFakeDashboardService(t) + dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(&dashboards.Dashboard{ + UID: "folder-uid", + Title: "Folder Title", + }, nil).Maybe() + dashboardService.On("GetDashboards", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardsQuery")).Return([]*dashboards.Dashboard{{ + UID: "folder-uid", + Title: "Folder Title", + }, + { + UID: "folder-uid2", + Title: "Folder Title2", + }}, nil).Maybe() + return testEnvironment{ - secrets: secrets, - log: log, - configs: configs, - store: store, - xact: xact, - prov: prov, - quotas: quotas, + secrets: secrets, + log: log, + configs: configs, + store: store, + dashboardService: dashboardService, + xact: xact, + prov: prov, + quotas: quotas, } } @@ -434,14 +830,18 @@ func createProvisioningSrvSutFromEnv(t *testing.T, env *testEnvironment) Provisi contactPointService: provisioning.NewContactPointService(env.configs, env.secrets, env.prov, env.xact, env.log), templates: provisioning.NewTemplateService(env.configs, env.prov, env.xact, env.log), muteTimings: provisioning.NewMuteTimingService(env.configs, env.prov, env.xact, env.log), - alertRules: provisioning.NewAlertRuleService(env.store, env.prov, env.quotas, env.xact, 60, 10, env.log), + alertRules: provisioning.NewAlertRuleService(env.store, env.prov, env.dashboardService, env.quotas, env.xact, 60, 10, env.log), } } func createTestRequestCtx() contextmodel.ReqContext { return contextmodel.ReqContext{ Context: &web.Context{ - Req: &http.Request{}, + Req: &http.Request{ + Header: make(http.Header), + Form: make(url.Values), + }, + Resp: web.NewResponseWriter("GET", httptest.NewRecorder()), }, SignedInUser: &user.SignedInUser{ OrgID: 1, @@ -555,15 +955,23 @@ func createInvalidAlertRuleGroup() definitions.AlertRuleGroup { } } +func createTestAlertRuleWithFolderAndGroup(title string, orgID int64, folderUid string, group string) definitions.ProvisionedAlertRule { + rule := createTestAlertRule(title, orgID) + rule.FolderUID = folderUid + rule.RuleGroup = group + return rule +} + func createTestAlertRule(title string, orgID int64) definitions.ProvisionedAlertRule { return definitions.ProvisionedAlertRule{ + UID: title, OrgID: orgID, Title: title, Condition: "A", Data: []models.AlertQuery{ { RefID: "A", - Model: json.RawMessage("{}"), + Model: json.RawMessage(testModel), RelativeTimeRange: models.RelativeTimeRange{ From: models.Duration(60), To: models.Duration(0), @@ -600,6 +1008,42 @@ func deserializeRule(t *testing.T, data []byte) definitions.ProvisionedAlertRule return rule } +var testModel = ` +{ + "conditions": [ + { + "evaluator": { + "params": [ + 3 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "A" + ] + }, + "reducer": { + "type": "last" + }, + "type": "query" + } + ], + "datasource": { + "type": "__expr__", + "uid": "-100" + }, + "expression": "1==0", + "intervalMs": 1000, + "maxDataPoints": 43200, + "refId": "A", + "type": "math" +} +` + var testConfig = ` { "template_files": { diff --git a/pkg/services/ngalert/api/authorization.go b/pkg/services/ngalert/api/authorization.go index 4dfc8a85ab3..0a6c2a4d005 100644 --- a/pkg/services/ngalert/api/authorization.go +++ b/pkg/services/ngalert/api/authorization.go @@ -201,7 +201,10 @@ func (api *API) authorize(method, path string) web.Handler { http.MethodGet + "/api/v1/provisioning/mute-timings/{name}", http.MethodGet + "/api/v1/provisioning/alert-rules", http.MethodGet + "/api/v1/provisioning/alert-rules/{UID}", - http.MethodGet + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}": + http.MethodGet + "/api/v1/provisioning/alert-rules/export", + http.MethodGet + "/api/v1/provisioning/alert-rules/{UID}/export", + http.MethodGet + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}", + http.MethodGet + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export": fallback = middleware.ReqOrgAdmin eval = ac.EvalPermission(ac.ActionAlertingProvisioningRead) // organization scope diff --git a/pkg/services/ngalert/api/authorization_test.go b/pkg/services/ngalert/api/authorization_test.go index b4da61566fd..2957979a26d 100644 --- a/pkg/services/ngalert/api/authorization_test.go +++ b/pkg/services/ngalert/api/authorization_test.go @@ -49,7 +49,7 @@ func TestAuthorize(t *testing.T) { } paths[p] = methods } - require.Len(t, paths, 41) + require.Len(t, paths, 44) ac := acmock.New() api := &API{AccessControl: ac} diff --git a/pkg/services/ngalert/api/generated_base_api_provisioning.go b/pkg/services/ngalert/api/generated_base_api_provisioning.go index aa72ebe5efa..f3e6ed66c32 100644 --- a/pkg/services/ngalert/api/generated_base_api_provisioning.go +++ b/pkg/services/ngalert/api/generated_base_api_provisioning.go @@ -24,8 +24,11 @@ type ProvisioningApi interface { RouteDeleteMuteTiming(*contextmodel.ReqContext) response.Response RouteDeleteTemplate(*contextmodel.ReqContext) response.Response RouteGetAlertRule(*contextmodel.ReqContext) response.Response + RouteGetAlertRuleExport(*contextmodel.ReqContext) response.Response RouteGetAlertRuleGroup(*contextmodel.ReqContext) response.Response + RouteGetAlertRuleGroupExport(*contextmodel.ReqContext) response.Response RouteGetAlertRules(*contextmodel.ReqContext) response.Response + RouteGetAlertRulesExport(*contextmodel.ReqContext) response.Response RouteGetContactpoints(*contextmodel.ReqContext) response.Response RouteGetMuteTiming(*contextmodel.ReqContext) response.Response RouteGetMuteTimings(*contextmodel.ReqContext) response.Response @@ -69,15 +72,29 @@ func (f *ProvisioningApiHandler) RouteGetAlertRule(ctx *contextmodel.ReqContext) uIDParam := web.Params(ctx.Req)[":UID"] return f.handleRouteGetAlertRule(ctx, uIDParam) } +func (f *ProvisioningApiHandler) RouteGetAlertRuleExport(ctx *contextmodel.ReqContext) response.Response { + // Parse Path Parameters + uIDParam := web.Params(ctx.Req)[":UID"] + return f.handleRouteGetAlertRuleExport(ctx, uIDParam) +} func (f *ProvisioningApiHandler) RouteGetAlertRuleGroup(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters folderUIDParam := web.Params(ctx.Req)[":FolderUID"] groupParam := web.Params(ctx.Req)[":Group"] return f.handleRouteGetAlertRuleGroup(ctx, folderUIDParam, groupParam) } +func (f *ProvisioningApiHandler) RouteGetAlertRuleGroupExport(ctx *contextmodel.ReqContext) response.Response { + // Parse Path Parameters + folderUIDParam := web.Params(ctx.Req)[":FolderUID"] + groupParam := web.Params(ctx.Req)[":Group"] + return f.handleRouteGetAlertRuleGroupExport(ctx, folderUIDParam, groupParam) +} func (f *ProvisioningApiHandler) RouteGetAlertRules(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetAlertRules(ctx) } +func (f *ProvisioningApiHandler) RouteGetAlertRulesExport(ctx *contextmodel.ReqContext) response.Response { + return f.handleRouteGetAlertRulesExport(ctx) +} func (f *ProvisioningApiHandler) RouteGetContactpoints(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetContactpoints(ctx) } @@ -239,6 +256,16 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApi, m *metrics m, ), ) + group.Get( + toMacaronPath("/api/v1/provisioning/alert-rules/{UID}/export"), + api.authorize(http.MethodGet, "/api/v1/provisioning/alert-rules/{UID}/export"), + metrics.Instrument( + http.MethodGet, + "/api/v1/provisioning/alert-rules/{UID}/export", + srv.RouteGetAlertRuleExport, + m, + ), + ) group.Get( toMacaronPath("/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}"), api.authorize(http.MethodGet, "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}"), @@ -249,6 +276,16 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApi, m *metrics m, ), ) + group.Get( + toMacaronPath("/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export"), + api.authorize(http.MethodGet, "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export"), + metrics.Instrument( + http.MethodGet, + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export", + srv.RouteGetAlertRuleGroupExport, + m, + ), + ) group.Get( toMacaronPath("/api/v1/provisioning/alert-rules"), api.authorize(http.MethodGet, "/api/v1/provisioning/alert-rules"), @@ -259,6 +296,16 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApi, m *metrics m, ), ) + group.Get( + toMacaronPath("/api/v1/provisioning/alert-rules/export"), + api.authorize(http.MethodGet, "/api/v1/provisioning/alert-rules/export"), + metrics.Instrument( + http.MethodGet, + "/api/v1/provisioning/alert-rules/export", + srv.RouteGetAlertRulesExport, + m, + ), + ) group.Get( toMacaronPath("/api/v1/provisioning/contact-points"), api.authorize(http.MethodGet, "/api/v1/provisioning/contact-points"), diff --git a/pkg/services/ngalert/api/provisioning.go b/pkg/services/ngalert/api/provisioning.go index 23387e36c11..d26b6bb5e75 100644 --- a/pkg/services/ngalert/api/provisioning.go +++ b/pkg/services/ngalert/api/provisioning.go @@ -84,6 +84,14 @@ func (f *ProvisioningApiHandler) handleRouteGetAlertRule(ctx *contextmodel.ReqCo return f.svc.RouteRouteGetAlertRule(ctx, UID) } +func (f *ProvisioningApiHandler) handleRouteGetAlertRuleExport(ctx *contextmodel.ReqContext, UID string) response.Response { + return f.svc.RouteGetAlertRuleExport(ctx, UID) +} + +func (f *ProvisioningApiHandler) handleRouteGetAlertRulesExport(ctx *contextmodel.ReqContext) response.Response { + return f.svc.RouteGetAlertRulesExport(ctx) +} + func (f *ProvisioningApiHandler) handleRoutePostAlertRule(ctx *contextmodel.ReqContext, ar apimodels.ProvisionedAlertRule) response.Response { return f.svc.RoutePostAlertRule(ctx, ar) } @@ -104,6 +112,10 @@ func (f *ProvisioningApiHandler) handleRouteGetAlertRuleGroup(ctx *contextmodel. return f.svc.RouteGetAlertRuleGroup(ctx, folder, group) } +func (f *ProvisioningApiHandler) handleRouteGetAlertRuleGroupExport(ctx *contextmodel.ReqContext, folder, group string) response.Response { + return f.svc.RouteGetAlertRuleGroupExport(ctx, folder, group) +} + func (f *ProvisioningApiHandler) handleRoutePutAlertRuleGroup(ctx *contextmodel.ReqContext, ag apimodels.AlertRuleGroup, folder, group string) response.Response { return f.svc.RoutePutAlertRuleGroup(ctx, ag, folder, group) } diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index 86cd1cc9299..5c39db61c9e 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -121,6 +121,28 @@ "title": "AlertQuery represents a single query associated with an alert definition.", "type": "object" }, + "AlertQueryExport": { + "properties": { + "datasourceUid": { + "type": "string" + }, + "model": { + "additionalProperties": {}, + "type": "object" + }, + "queryType": { + "type": "string" + }, + "refId": { + "type": "string" + }, + "relativeTimeRange": { + "$ref": "#/definitions/RelativeTimeRange" + } + }, + "title": "AlertQueryExport is the provisioned export of models.AlertQuery.", + "type": "object" + }, "AlertResponse": { "properties": { "data": { @@ -141,6 +163,65 @@ ], "type": "object" }, + "AlertRuleExport": { + "properties": { + "annotations": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "condition": { + "type": "string" + }, + "dasboardUid": { + "type": "string" + }, + "data": { + "items": { + "$ref": "#/definitions/AlertQueryExport" + }, + "type": "array" + }, + "execErrState": { + "enum": [ + "Alerting", + "Error", + "OK" + ], + "type": "string" + }, + "for": { + "$ref": "#/definitions/Duration" + }, + "labels": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "noDataState": { + "enum": [ + "Alerting", + "NoData", + "OK" + ], + "type": "string" + }, + "panelId": { + "format": "int64", + "type": "integer" + }, + "title": { + "type": "string" + }, + "uid": { + "type": "string" + } + }, + "title": "AlertRuleExport is the provisioned file export of models.AlertRule.", + "type": "object" + }, "AlertRuleGroup": { "properties": { "folderUid": { @@ -162,6 +243,31 @@ }, "type": "object" }, + "AlertRuleGroupExport": { + "properties": { + "folder": { + "type": "string" + }, + "interval": { + "$ref": "#/definitions/Duration" + }, + "name": { + "type": "string" + }, + "orgId": { + "format": "int64", + "type": "integer" + }, + "rules": { + "items": { + "$ref": "#/definitions/AlertRuleExport" + }, + "type": "array" + } + }, + "title": "AlertRuleGroupExport is the provisioned file export of AlertRuleGroupV1.", + "type": "object" + }, "AlertRuleGroupMetadata": { "properties": { "interval": { @@ -171,6 +277,22 @@ }, "type": "object" }, + "AlertingFileExport": { + "properties": { + "apiVersion": { + "format": "int64", + "type": "integer" + }, + "groups": { + "items": { + "$ref": "#/definitions/AlertRuleGroupExport" + }, + "type": "array" + } + }, + "title": "AlertingFileExport is the full provisioned file export.", + "type": "object" + }, "AlertingRule": { "description": "adapted from cortex", "properties": { @@ -3155,7 +3277,6 @@ "type": "object" }, "URL": { - "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.", "properties": { "ForceQuery": { "type": "boolean" @@ -3191,7 +3312,7 @@ "$ref": "#/definitions/Userinfo" } }, - "title": "A URL represents a parsed URL (technically, a URI reference).", + "title": "URL is a custom URL type that allows validation at configuration load time.", "type": "object" }, "Userinfo": { @@ -3551,6 +3672,7 @@ "type": "object" }, "gettableAlerts": { + "description": "GettableAlerts gettable alerts", "items": { "$ref": "#/definitions/gettableAlert" }, @@ -3611,7 +3733,6 @@ "type": "array" }, "integration": { - "description": "Integration integration", "properties": { "lastNotifyAttempt": { "description": "A timestamp indicating the last attempt to deliver a notification regardless of the outcome.\nFormat: date-time", @@ -3793,6 +3914,7 @@ "type": "object" }, "receiver": { + "description": "Receiver receiver", "properties": { "active": { "description": "active", @@ -3967,6 +4089,35 @@ ] } }, + "/api/v1/provisioning/alert-rules/export": { + "get": { + "operationId": "RouteGetAlertRulesExport", + "parameters": [ + { + "default": false, + "description": "Whether to initiate a download of the file or not.", + "in": "query", + "name": "download", + "type": "boolean" + } + ], + "responses": { + "200": { + "description": "AlertingFileExport", + "schema": { + "$ref": "#/definitions/AlertingFileExport" + } + }, + "404": { + "description": " Not found." + } + }, + "summary": "Export all alert rules in provisioning file format.", + "tags": [ + "provisioning" + ] + } + }, "/api/v1/provisioning/alert-rules/{UID}": { "delete": { "operationId": "RouteDeleteAlertRule", @@ -4062,6 +4213,47 @@ ] } }, + "/api/v1/provisioning/alert-rules/{UID}/export": { + "get": { + "operationId": "RouteGetAlertRuleExport", + "parameters": [ + { + "description": "Alert rule UID", + "in": "path", + "name": "UID", + "required": true, + "type": "string" + }, + { + "default": false, + "description": "Whether to initiate a download of the file or not.", + "in": "query", + "name": "download", + "type": "boolean" + } + ], + "produces": [ + "application/json", + "application/yaml", + "text/yaml" + ], + "responses": { + "200": { + "description": "AlertingFileExport", + "schema": { + "$ref": "#/definitions/AlertingFileExport" + } + }, + "404": { + "description": " Not found." + } + }, + "summary": "Export an alert rule in provisioning file format.", + "tags": [ + "provisioning" + ] + } + }, "/api/v1/provisioning/contact-points": { "get": { "operationId": "RouteGetContactpoints", @@ -4265,6 +4457,52 @@ ] } }, + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export": { + "get": { + "operationId": "RouteGetAlertRuleGroupExport", + "parameters": [ + { + "in": "path", + "name": "FolderUID", + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "Group", + "required": true, + "type": "string" + }, + { + "default": false, + "description": "Whether to initiate a download of the file or not.", + "in": "query", + "name": "download", + "type": "boolean" + } + ], + "produces": [ + "application/json", + "application/yaml", + "text/yaml" + ], + "responses": { + "200": { + "description": "AlertingFileExport", + "schema": { + "$ref": "#/definitions/AlertingFileExport" + } + }, + "404": { + "description": " Not found." + } + }, + "summary": "Export an alert rule group in provisioning file format.", + "tags": [ + "provisioning" + ] + } + }, "/api/v1/provisioning/mute-timings": { "get": { "operationId": "RouteGetMuteTimings", diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go index 8bf8c656a5c..0e51a514bbf 100644 --- a/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go @@ -4,6 +4,8 @@ import ( "time" "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/provisioning/alerting/file" + "github.com/prometheus/common/model" ) @@ -14,6 +16,14 @@ import ( // Responses: // 200: ProvisionedAlertRules +// swagger:route GET /api/v1/provisioning/alert-rules/export provisioning stable RouteGetAlertRulesExport +// +// Export all alert rules in provisioning file format. +// +// Responses: +// 200: AlertingFileExport +// 404: description: Not found. + // swagger:route GET /api/v1/provisioning/alert-rules/{UID} provisioning stable RouteGetAlertRule // // Get a specific alert rule by UID. @@ -22,6 +32,19 @@ import ( // 200: ProvisionedAlertRule // 404: description: Not found. +// swagger:route GET /api/v1/provisioning/alert-rules/{UID}/export provisioning stable RouteGetAlertRuleExport +// +// Export an alert rule in provisioning file format. +// +// Produces: +// - application/json +// - application/yaml +// - text/yaml +// +// Responses: +// 200: AlertingFileExport +// 404: description: Not found. + // swagger:route POST /api/v1/provisioning/alert-rules provisioning stable RoutePostAlertRule // // Create a new alert rule. @@ -51,7 +74,7 @@ import ( // Responses: // 204: description: The alert rule was deleted successfully. -// swagger:parameters RouteGetAlertRule RoutePutAlertRule RouteDeleteAlertRule +// swagger:parameters RouteGetAlertRule RoutePutAlertRule RouteDeleteAlertRule RouteGetAlertRuleExport type AlertRuleUIDReference struct { // Alert rule UID // in:path @@ -168,6 +191,19 @@ func NewAlertRules(rules []*models.AlertRule) ProvisionedAlertRules { // 200: AlertRuleGroup // 404: description: Not found. +// swagger:route GET /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export provisioning stable RouteGetAlertRuleGroupExport +// +// Export an alert rule group in provisioning file format. +// +// Produces: +// - application/json +// - application/yaml +// - text/yaml +// +// Responses: +// 200: AlertingFileExport +// 404: description: Not found. + // swagger:route PUT /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group} provisioning stable RoutePutAlertRuleGroup // // Update the interval of a rule group. @@ -179,13 +215,13 @@ func NewAlertRules(rules []*models.AlertRule) ProvisionedAlertRules { // 200: AlertRuleGroup // 400: ValidationError -// swagger:parameters RouteGetAlertRuleGroup RoutePutAlertRuleGroup +// swagger:parameters RouteGetAlertRuleGroup RoutePutAlertRuleGroup RouteGetAlertRuleGroupExport type FolderUIDPathParam struct { // in:path FolderUID string `json:"FolderUID"` } -// swagger:parameters RouteGetAlertRuleGroup RoutePutAlertRuleGroup +// swagger:parameters RouteGetAlertRuleGroup RoutePutAlertRuleGroup RouteGetAlertRuleGroupExport type RuleGroupPathParam struct { // in:path Group string `json:"Group"` @@ -202,6 +238,15 @@ type AlertRuleGroupMetadata struct { Interval int64 `json:"interval"` } +// swagger:parameters RouteGetAlertRuleGroupExport RouteGetAlertRuleExport RouteGetAlertRulesExport +type ExportQueryParams struct { + // Whether to initiate a download of the file or not. + // in: query + // required: false + // default: false + Download bool `json:"download"` +} + // swagger:model type AlertRuleGroup struct { Title string `json:"title"` @@ -210,6 +255,10 @@ type AlertRuleGroup struct { Rules []ProvisionedAlertRule `json:"rules"` } +// AlertingFileExport is the full provisioned file export. +// swagger:model +type AlertingFileExport = file.AlertingFileExport + func (a *AlertRuleGroup) ToModel() (models.AlertRuleGroup, error) { ruleGroup := models.AlertRuleGroup{ Title: a.Title, diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index ce003de9465..b30fb5961d6 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -121,6 +121,28 @@ "title": "AlertQuery represents a single query associated with an alert definition.", "type": "object" }, + "AlertQueryExport": { + "properties": { + "datasourceUid": { + "type": "string" + }, + "model": { + "additionalProperties": {}, + "type": "object" + }, + "queryType": { + "type": "string" + }, + "refId": { + "type": "string" + }, + "relativeTimeRange": { + "$ref": "#/definitions/RelativeTimeRange" + } + }, + "title": "AlertQueryExport is the provisioned export of models.AlertQuery.", + "type": "object" + }, "AlertResponse": { "properties": { "data": { @@ -141,6 +163,77 @@ ], "type": "object" }, + "AlertRuleExport": { + "properties": { + "annotations": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "condition": { + "type": "string" + }, + "dasboardUid": { + "type": "string" + }, + "data": { + "items": { + "$ref": "#/definitions/AlertQueryExport" + }, + "type": "array" + }, + "execErrState": { + "enum": [ + "Alerting", + "Error", + "OK" + ], + "type": "string" + }, + "for": { + "$ref": "#/definitions/Duration" + }, + "labels": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "noDataState": { + "enum": [ + "Alerting", + "NoData", + "OK" + ], + "type": "string" + }, + "panelId": { + "format": "int64", + "type": "integer" + }, + "title": { + "type": "string" + }, + "uid": { + "type": "string" + } + }, + "title": "AlertRuleExport is the provisioned export of models.AlertRule.", + "type": "object" + }, + "AlertRuleFileExport": { + "properties": { + "groups": { + "items": { + "$ref": "#/definitions/AlertRuleGroupExport" + }, + "type": "array" + } + }, + "title": "AlertRuleFileExport is the provisioned export of multiple models.AlertRuleGroup.", + "type": "object" + }, "AlertRuleGroup": { "properties": { "folderUid": { @@ -162,6 +255,31 @@ }, "type": "object" }, + "AlertRuleGroupExport": { + "properties": { + "folder": { + "type": "string" + }, + "interval": { + "$ref": "#/definitions/Duration" + }, + "name": { + "type": "string" + }, + "orgId": { + "format": "int64", + "type": "integer" + }, + "rules": { + "items": { + "$ref": "#/definitions/AlertRuleExport" + }, + "type": "array" + } + }, + "title": "AlertRuleGroupExport is the provisioned export of models.AlertRuleGroup.", + "type": "object" + }, "AlertRuleGroupMetadata": { "properties": { "interval": { @@ -3155,6 +3273,7 @@ "type": "object" }, "URL": { + "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.", "properties": { "ForceQuery": { "type": "boolean" @@ -3190,7 +3309,7 @@ "$ref": "#/definitions/Userinfo" } }, - "title": "URL is a custom URL type that allows validation at configuration load time.", + "title": "A URL represents a parsed URL (technically, a URI reference).", "type": "object" }, "Userinfo": { @@ -3391,7 +3510,6 @@ "type": "object" }, "alertGroups": { - "description": "AlertGroups alert groups", "items": { "$ref": "#/definitions/alertGroup" }, @@ -3496,7 +3614,6 @@ "type": "object" }, "gettableAlert": { - "description": "GettableAlert gettable alert", "properties": { "annotations": { "$ref": "#/definitions/labelSet" @@ -3558,6 +3675,7 @@ "type": "array" }, "gettableSilence": { + "description": "GettableSilence gettable silence", "properties": { "comment": { "description": "comment", @@ -5640,6 +5758,35 @@ ] } }, + "/api/v1/provisioning/alert-rules/export": { + "get": { + "operationId": "RouteGetAlertRulesExport", + "parameters": [ + { + "default": false, + "description": "Whether to initiate a download of the file or not.", + "in": "query", + "name": "download", + "type": "boolean" + } + ], + "responses": { + "200": { + "description": "AlertRuleFileExport", + "schema": { + "$ref": "#/definitions/AlertRuleFileExport" + } + }, + "404": { + "description": " Not found." + } + }, + "summary": "Export all alert rules in provisioning file format.", + "tags": [ + "provisioning" + ] + } + }, "/api/v1/provisioning/alert-rules/{UID}": { "delete": { "operationId": "RouteDeleteAlertRule", @@ -5735,6 +5882,47 @@ ] } }, + "/api/v1/provisioning/alert-rules/{UID}/export": { + "get": { + "operationId": "RouteGetAlertRuleExport", + "parameters": [ + { + "description": "Alert rule UID", + "in": "path", + "name": "UID", + "required": true, + "type": "string" + }, + { + "default": false, + "description": "Whether to initiate a download of the file or not.", + "in": "query", + "name": "download", + "type": "boolean" + } + ], + "produces": [ + "application/json", + "application/yaml", + "text/yaml" + ], + "responses": { + "200": { + "description": "AlertRuleExport", + "schema": { + "$ref": "#/definitions/AlertRuleExport" + } + }, + "404": { + "description": " Not found." + } + }, + "summary": "Export an alert rule in provisioning file format.", + "tags": [ + "provisioning" + ] + } + }, "/api/v1/provisioning/contact-points": { "get": { "operationId": "RouteGetContactpoints", @@ -5938,6 +6126,52 @@ ] } }, + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export": { + "get": { + "operationId": "RouteGetAlertRuleGroupExport", + "parameters": [ + { + "in": "path", + "name": "FolderUID", + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "Group", + "required": true, + "type": "string" + }, + { + "default": false, + "description": "Whether to initiate a download of the file or not.", + "in": "query", + "name": "download", + "type": "boolean" + } + ], + "produces": [ + "application/json", + "application/yaml", + "text/yaml" + ], + "responses": { + "200": { + "description": "AlertRuleGroupExport", + "schema": { + "$ref": "#/definitions/AlertRuleGroupExport" + } + }, + "404": { + "description": " Not found." + } + }, + "summary": "Export an alert rule group in provisioning file format.", + "tags": [ + "provisioning" + ] + } + }, "/api/v1/provisioning/mute-timings": { "get": { "operationId": "RouteGetMuteTimings", diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index ce7a76b2117..fcae8f39245 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -1746,6 +1746,36 @@ } } }, + "/api/v1/provisioning/alert-rules/export": { + "get": { + "tags": [ + "provisioning", + "stable" + ], + "summary": "Export all alert rules in provisioning file format.", + "operationId": "RouteGetAlertRulesExport", + "parameters": [ + { + "type": "boolean", + "default": false, + "description": "Whether to initiate a download of the file or not.", + "name": "download", + "in": "query" + } + ], + "responses": { + "200": { + "description": "AlertingFileExport", + "schema": { + "$ref": "#/definitions/AlertingFileExport" + } + }, + "404": { + "description": " Not found." + } + } + } + }, "/api/v1/provisioning/alert-rules/{UID}": { "get": { "tags": [ @@ -1844,6 +1874,48 @@ } } }, + "/api/v1/provisioning/alert-rules/{UID}/export": { + "get": { + "produces": [ + "application/json", + "application/yaml", + "text/yaml" + ], + "tags": [ + "provisioning", + "stable" + ], + "summary": "Export an alert rule in provisioning file format.", + "operationId": "RouteGetAlertRuleExport", + "parameters": [ + { + "type": "string", + "description": "Alert rule UID", + "name": "UID", + "in": "path", + "required": true + }, + { + "type": "boolean", + "default": false, + "description": "Whether to initiate a download of the file or not.", + "name": "download", + "in": "query" + } + ], + "responses": { + "200": { + "description": "AlertingFileExport", + "schema": { + "$ref": "#/definitions/AlertingFileExport" + } + }, + "404": { + "description": " Not found." + } + } + } + }, "/api/v1/provisioning/contact-points": { "get": { "tags": [ @@ -2053,6 +2125,53 @@ } } }, + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export": { + "get": { + "produces": [ + "application/json", + "application/yaml", + "text/yaml" + ], + "tags": [ + "provisioning", + "stable" + ], + "summary": "Export an alert rule group in provisioning file format.", + "operationId": "RouteGetAlertRuleGroupExport", + "parameters": [ + { + "type": "string", + "name": "FolderUID", + "in": "path", + "required": true + }, + { + "type": "string", + "name": "Group", + "in": "path", + "required": true + }, + { + "type": "boolean", + "default": false, + "description": "Whether to initiate a download of the file or not.", + "name": "download", + "in": "query" + } + ], + "responses": { + "200": { + "description": "AlertingFileExport", + "schema": { + "$ref": "#/definitions/AlertingFileExport" + } + }, + "404": { + "description": " Not found." + } + } + } + }, "/api/v1/provisioning/mute-timings": { "get": { "tags": [ @@ -2612,6 +2731,28 @@ } } }, + "AlertQueryExport": { + "type": "object", + "title": "AlertQueryExport is the provisioned export of models.AlertQuery.", + "properties": { + "datasourceUid": { + "type": "string" + }, + "model": { + "type": "object", + "additionalProperties": {} + }, + "queryType": { + "type": "string" + }, + "refId": { + "type": "string" + }, + "relativeTimeRange": { + "$ref": "#/definitions/RelativeTimeRange" + } + } + }, "AlertResponse": { "type": "object", "required": [ @@ -2632,6 +2773,65 @@ } } }, + "AlertRuleExport": { + "type": "object", + "title": "AlertRuleExport is the provisioned file export of models.AlertRule.", + "properties": { + "annotations": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "condition": { + "type": "string" + }, + "dasboardUid": { + "type": "string" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/AlertQueryExport" + } + }, + "execErrState": { + "type": "string", + "enum": [ + "Alerting", + "Error", + "OK" + ] + }, + "for": { + "$ref": "#/definitions/Duration" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "noDataState": { + "type": "string", + "enum": [ + "Alerting", + "NoData", + "OK" + ] + }, + "panelId": { + "type": "integer", + "format": "int64" + }, + "title": { + "type": "string" + }, + "uid": { + "type": "string" + } + } + }, "AlertRuleGroup": { "type": "object", "properties": { @@ -2653,6 +2853,31 @@ } } }, + "AlertRuleGroupExport": { + "type": "object", + "title": "AlertRuleGroupExport is the provisioned file export of AlertRuleGroupV1.", + "properties": { + "folder": { + "type": "string" + }, + "interval": { + "$ref": "#/definitions/Duration" + }, + "name": { + "type": "string" + }, + "orgId": { + "type": "integer", + "format": "int64" + }, + "rules": { + "type": "array", + "items": { + "$ref": "#/definitions/AlertRuleExport" + } + } + } + }, "AlertRuleGroupMetadata": { "type": "object", "properties": { @@ -2662,6 +2887,23 @@ } } }, + "AlertingFileExport": { + "type": "object", + "title": "AlertingFileExport is the full provisioned file export.", + "properties": { + "apiVersion": { + "type": "integer", + "format": "int64" + }, + "groups": { + "type": "array", + "items": { + "$ref": "#/definitions/AlertRuleGroupExport" + } + } + }, + "$ref": "#/definitions/AlertingFileExport" + }, "AlertingRule": { "description": "adapted from cortex", "type": "object", @@ -5887,7 +6129,6 @@ "$ref": "#/definitions/alertGroup" }, "alertGroups": { - "description": "AlertGroups alert groups", "type": "array", "items": { "$ref": "#/definitions/alertGroup" @@ -5993,7 +6234,6 @@ } }, "gettableAlert": { - "description": "GettableAlert gettable alert", "type": "object", "required": [ "labels", @@ -6057,6 +6297,7 @@ "$ref": "#/definitions/gettableAlerts" }, "gettableSilence": { + "description": "GettableSilence gettable silence", "type": "object", "required": [ "comment", @@ -6113,7 +6354,6 @@ "$ref": "#/definitions/gettableSilences" }, "integration": { - "description": "Integration integration", "type": "object", "required": [ "name", @@ -6296,7 +6536,6 @@ "$ref": "#/definitions/postableSilence" }, "receiver": { - "description": "Receiver receiver", "type": "object", "required": [ "active", diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index cc92ef97891..3908c308cd0 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -239,7 +239,7 @@ func (ng *AlertNG) init() error { contactPointService := provisioning.NewContactPointService(store, ng.SecretsService, store, store, ng.Log) templateService := provisioning.NewTemplateService(store, store, store, ng.Log) muteTimingService := provisioning.NewMuteTimingService(store, store, store, ng.Log) - alertRuleService := provisioning.NewAlertRuleService(store, store, ng.QuotaService, store, + alertRuleService := provisioning.NewAlertRuleService(store, store, ng.dashboardService, ng.QuotaService, store, int64(ng.Cfg.UnifiedAlerting.DefaultRuleEvaluationInterval.Seconds()), int64(ng.Cfg.UnifiedAlerting.BaseInterval.Seconds()), ng.Log) diff --git a/pkg/services/ngalert/provisioning/alert_rules.go b/pkg/services/ngalert/provisioning/alert_rules.go index 27d6b531946..18b2ba3f361 100644 --- a/pkg/services/ngalert/provisioning/alert_rules.go +++ b/pkg/services/ngalert/provisioning/alert_rules.go @@ -4,11 +4,14 @@ import ( "context" "errors" "fmt" + "sort" "time" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/store" + "github.com/grafana/grafana/pkg/services/provisioning/alerting/file" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/util" ) @@ -18,6 +21,7 @@ type AlertRuleService struct { baseIntervalSeconds int64 ruleStore RuleStore provenanceStore ProvisioningStore + dashboardService dashboards.DashboardService quotas QuotaChecker xact TransactionManager log log.Logger @@ -25,6 +29,7 @@ type AlertRuleService struct { func NewAlertRuleService(ruleStore RuleStore, provenanceStore ProvisioningStore, + dashboardService dashboards.DashboardService, quotas QuotaChecker, xact TransactionManager, defaultIntervalSeconds int64, @@ -35,6 +40,7 @@ func NewAlertRuleService(ruleStore RuleStore, baseIntervalSeconds: baseIntervalSeconds, ruleStore: ruleStore, provenanceStore: provenanceStore, + dashboardService: dashboardService, quotas: quotas, xact: xact, log: log, @@ -69,6 +75,38 @@ func (service *AlertRuleService) GetAlertRule(ctx context.Context, orgID int64, return *query.Result, provenance, nil } +type AlertRuleWithFolderTitle struct { + AlertRule models.AlertRule + FolderTitle string +} + +// GetAlertRuleWithFolderTitle returns a single alert rule with its folder title. +func (service *AlertRuleService) GetAlertRuleWithFolderTitle(ctx context.Context, orgID int64, ruleUID string) (AlertRuleWithFolderTitle, error) { + query := &models.GetAlertRuleByUIDQuery{ + OrgID: orgID, + UID: ruleUID, + } + err := service.ruleStore.GetAlertRuleByUID(ctx, query) + if err != nil { + return AlertRuleWithFolderTitle{}, err + } + + dq := dashboards.GetDashboardQuery{ + OrgID: orgID, + UID: query.Result.NamespaceUID, + } + + dash, err := service.dashboardService.GetDashboard(ctx, &dq) + if err != nil { + return AlertRuleWithFolderTitle{}, err + } + + return AlertRuleWithFolderTitle{ + AlertRule: *query.Result, + FolderTitle: dash.Title, + }, nil +} + // CreateAlertRule creates a new alert rule. This function will ignore any // interval that is set in the rule struct and use the already existing group // interval or the default one. @@ -114,10 +152,10 @@ func (service *AlertRuleService) CreateAlertRule(ctx context.Context, rule model return rule, nil } -func (service *AlertRuleService) GetRuleGroup(ctx context.Context, orgID int64, folder, group string) (models.AlertRuleGroup, error) { +func (service *AlertRuleService) GetRuleGroup(ctx context.Context, orgID int64, namespaceUID, group string) (models.AlertRuleGroup, error) { q := models.ListAlertRulesQuery{ OrgID: orgID, - NamespaceUIDs: []string{folder}, + NamespaceUIDs: []string{namespaceUID}, RuleGroup: group, } if err := service.ruleStore.ListAlertRules(ctx, &q); err != nil { @@ -366,6 +404,114 @@ func (service *AlertRuleService) deleteRules(ctx context.Context, orgID int64, t return nil } +// GetAlertRuleGroupWithFolderTitle returns the alert rule group with folder title. +func (service *AlertRuleService) GetAlertRuleGroupWithFolderTitle(ctx context.Context, orgID int64, namespaceUID, group string) (file.AlertRuleGroupWithFolderTitle, error) { + q := models.ListAlertRulesQuery{ + OrgID: orgID, + NamespaceUIDs: []string{namespaceUID}, + RuleGroup: group, + } + if err := service.ruleStore.ListAlertRules(ctx, &q); err != nil { + return file.AlertRuleGroupWithFolderTitle{}, err + } + if len(q.Result) == 0 { + return file.AlertRuleGroupWithFolderTitle{}, store.ErrAlertRuleGroupNotFound + } + + dq := dashboards.GetDashboardQuery{ + OrgID: orgID, + UID: namespaceUID, + } + dash, err := service.dashboardService.GetDashboard(ctx, &dq) + if err != nil { + return file.AlertRuleGroupWithFolderTitle{}, err + } + + res := file.AlertRuleGroupWithFolderTitle{ + AlertRuleGroup: &models.AlertRuleGroup{ + Title: q.Result[0].RuleGroup, + FolderUID: q.Result[0].NamespaceUID, + Interval: q.Result[0].IntervalSeconds, + Rules: []models.AlertRule{}, + }, + OrgID: orgID, + FolderTitle: dash.Title, + } + for _, r := range q.Result { + if r != nil { + res.AlertRuleGroup.Rules = append(res.AlertRuleGroup.Rules, *r) + } + } + return res, nil +} + +// GetAlertGroupsWithFolderTitle returns all groups with folder title that have at least one alert. +func (service *AlertRuleService) GetAlertGroupsWithFolderTitle(ctx context.Context, orgID int64) ([]file.AlertRuleGroupWithFolderTitle, error) { + q := models.ListAlertRulesQuery{ + OrgID: orgID, + } + + if err := service.ruleStore.ListAlertRules(ctx, &q); err != nil { + return nil, err + } + + groups := make(map[models.AlertRuleGroupKey][]models.AlertRule) + namespaces := make(map[string][]*models.AlertRuleGroupKey) + for _, r := range q.Result { + groupKey := r.GetGroupKey() + group := groups[groupKey] + group = append(group, *r) + groups[groupKey] = group + + namespaces[r.NamespaceUID] = append(namespaces[r.NamespaceUID], &groupKey) + } + + dq := dashboards.GetDashboardsQuery{ + DashboardUIDs: nil, + } + for uid := range namespaces { + dq.DashboardUIDs = append(dq.DashboardUIDs, uid) + } + + // We need folder titles for the provisioning file format. We do it this way instead of using GetUserVisibleNamespaces to avoid folder:read permissions that should not apply to those with alert.provisioning:read. + dashes, err := service.dashboardService.GetDashboards(ctx, &dq) + if err != nil { + return nil, err + } + folderUidToTitle := make(map[string]string) + for _, dash := range dashes { + folderUidToTitle[dash.UID] = dash.Title + } + + result := make([]file.AlertRuleGroupWithFolderTitle, 0) + for groupKey, rules := range groups { + title, ok := folderUidToTitle[groupKey.NamespaceUID] + if !ok { + return nil, fmt.Errorf("cannot find title for folder with uid '%s'", groupKey.NamespaceUID) + } + result = append(result, file.AlertRuleGroupWithFolderTitle{ + AlertRuleGroup: &models.AlertRuleGroup{ + Title: rules[0].RuleGroup, + FolderUID: rules[0].NamespaceUID, + Interval: rules[0].IntervalSeconds, + Rules: rules, + }, + OrgID: orgID, + FolderTitle: title, + }) + } + + // Return results in a stable manner. + sort.SliceStable(result, func(i, j int) bool { + if result[i].AlertRuleGroup.FolderUID == result[j].AlertRuleGroup.FolderUID { + return result[i].AlertRuleGroup.Title < result[j].AlertRuleGroup.Title + } + return result[i].AlertRuleGroup.FolderUID < result[j].AlertRuleGroup.FolderUID + }) + + return result, nil +} + // syncRuleGroupFields synchronizes calculated fields across multiple rules in a group. func syncGroupRuleFields(group *models.AlertRuleGroup, orgID int64) *models.AlertRuleGroup { for i := range group.Rules { diff --git a/pkg/services/provisioning/alerting/rules_types.go b/pkg/services/provisioning/alerting/file/rules_types.go similarity index 50% rename from pkg/services/provisioning/alerting/rules_types.go rename to pkg/services/provisioning/alerting/file/rules_types.go index b300d918c46..3c726dffc41 100644 --- a/pkg/services/provisioning/alerting/rules_types.go +++ b/pkg/services/provisioning/alerting/file/rules_types.go @@ -1,4 +1,4 @@ -package alerting +package file import ( "encoding/json" @@ -31,11 +31,11 @@ type AlertRuleGroupV1 struct { Rules []AlertRuleV1 `json:"rules" yaml:"rules"` } -func (ruleGroupV1 *AlertRuleGroupV1) MapToModel() (AlertRuleGroup, error) { - ruleGroup := AlertRuleGroup{} - ruleGroup.Name = ruleGroupV1.Name.Value() - if strings.TrimSpace(ruleGroup.Name) == "" { - return AlertRuleGroup{}, errors.New("rule group has no name set") +func (ruleGroupV1 *AlertRuleGroupV1) MapToModel() (AlertRuleGroupWithFolderTitle, error) { + ruleGroup := AlertRuleGroupWithFolderTitle{AlertRuleGroup: &models.AlertRuleGroup{}} + ruleGroup.Title = ruleGroupV1.Name.Value() + if strings.TrimSpace(ruleGroup.Title) == "" { + return AlertRuleGroupWithFolderTitle{}, errors.New("rule group has no name set") } ruleGroup.OrgID = ruleGroupV1.OrgID.Value() if ruleGroup.OrgID < 1 { @@ -43,29 +43,27 @@ func (ruleGroupV1 *AlertRuleGroupV1) MapToModel() (AlertRuleGroup, error) { } interval, err := model.ParseDuration(ruleGroupV1.Interval.Value()) if err != nil { - return AlertRuleGroup{}, err + return AlertRuleGroupWithFolderTitle{}, err } - ruleGroup.Interval = time.Duration(interval) - ruleGroup.Folder = ruleGroupV1.Folder.Value() - if strings.TrimSpace(ruleGroup.Folder) == "" { - return AlertRuleGroup{}, errors.New("rule group has no folder set") + ruleGroup.Interval = int64(time.Duration(interval).Seconds()) + ruleGroup.FolderTitle = ruleGroupV1.Folder.Value() + if strings.TrimSpace(ruleGroup.FolderTitle) == "" { + return AlertRuleGroupWithFolderTitle{}, errors.New("rule group has no folder set") } for _, ruleV1 := range ruleGroupV1.Rules { rule, err := ruleV1.mapToModel(ruleGroup.OrgID) if err != nil { - return AlertRuleGroup{}, err + return AlertRuleGroupWithFolderTitle{}, err } ruleGroup.Rules = append(ruleGroup.Rules, rule) } return ruleGroup, nil } -type AlertRuleGroup struct { - OrgID int64 - Name string - Folder string - Interval time.Duration - Rules []models.AlertRule +type AlertRuleGroupWithFolderTitle struct { + *models.AlertRuleGroup + OrgID int64 + FolderTitle string } type AlertRuleV1 struct { @@ -175,3 +173,130 @@ func (queryV1 *QueryV1) mapToModel() (models.AlertQuery, error) { Model: rawMessage, }, nil } + +// Response structs + +// AlertingFileExport is the full provisioned file export. +// swagger:model +type AlertingFileExport struct { + APIVersion int64 `json:"apiVersion" yaml:"apiVersion"` + Groups []AlertRuleGroupExport `json:"groups" yaml:"groups"` +} + +// AlertRuleGroupExport is the provisioned file export of AlertRuleGroupV1. +type AlertRuleGroupExport struct { + OrgID int64 `json:"orgId" yaml:"orgId"` + Name string `json:"name" yaml:"name"` + Folder string `json:"folder" yaml:"folder"` + Interval model.Duration `json:"interval" yaml:"interval"` + Rules []AlertRuleExport `json:"rules" yaml:"rules"` +} + +// AlertRuleExport is the provisioned file export of models.AlertRule. +type AlertRuleExport struct { + UID string `json:"uid" yaml:"uid"` + Title string `json:"title" yaml:"title"` + Condition string `json:"condition" yaml:"condition"` + Data []AlertQueryExport `json:"data" yaml:"data"` + DashboardUID string `json:"dasboardUid,omitempty" yaml:"dashboardUid,omitempty"` + PanelID int64 `json:"panelId,omitempty" yaml:"panelId,omitempty"` + NoDataState models.NoDataState `json:"noDataState" yaml:"noDataState"` + ExecErrState models.ExecutionErrorState `json:"execErrState" yaml:"execErrState"` + For model.Duration `json:"for" yaml:"for"` + Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"` + Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` +} + +// AlertQueryExport is the provisioned export of models.AlertQuery. +type AlertQueryExport struct { + RefID string `json:"refId" yaml:"refId"` + QueryType string `json:"queryType,omitempty" yaml:"queryType,omitempty"` + RelativeTimeRange models.RelativeTimeRange `json:"relativeTimeRange,omitempty" yaml:"relativeTimeRange,omitempty"` + DatasourceUID string `json:"datasourceUid" yaml:"datasourceUid"` + Model map[string]interface{} `json:"model" yaml:"model"` +} + +// NewAlertingFileExport creates an AlertingFileExport DTO from []AlertRuleGroupWithFolderTitle. +func NewAlertingFileExport(groups []AlertRuleGroupWithFolderTitle) (AlertingFileExport, error) { + f := AlertingFileExport{APIVersion: 1} + for _, group := range groups { + export, err := newAlertRuleGroupExport(group) + if err != nil { + return AlertingFileExport{}, err + } + f.Groups = append(f.Groups, export) + } + return f, nil +} + +// newAlertRuleGroupExport creates a AlertRuleGroupExport DTO from models.AlertRuleGroup. +func newAlertRuleGroupExport(d AlertRuleGroupWithFolderTitle) (AlertRuleGroupExport, error) { + rules := make([]AlertRuleExport, 0, len(d.Rules)) + for i := range d.Rules { + alert, err := newAlertRuleExport(d.Rules[i]) + if err != nil { + return AlertRuleGroupExport{}, err + } + rules = append(rules, alert) + } + return AlertRuleGroupExport{ + OrgID: d.OrgID, + Name: d.Title, + Folder: d.FolderTitle, + Interval: model.Duration(time.Duration(d.Interval) * time.Second), + Rules: rules, + }, nil +} + +// newAlertRuleExport creates a AlertRuleExport DTO from models.AlertRule. +func newAlertRuleExport(rule models.AlertRule) (AlertRuleExport, error) { + data := make([]AlertQueryExport, 0, len(rule.Data)) + for i := range rule.Data { + query, err := newAlertQueryExport(rule.Data[i]) + if err != nil { + return AlertRuleExport{}, err + } + data = append(data, query) + } + + var dashboardUID string + if rule.DashboardUID != nil { + dashboardUID = *rule.DashboardUID + } + + var panelID int64 + if rule.PanelID != nil { + panelID = *rule.PanelID + } + + return AlertRuleExport{ + UID: rule.UID, + Title: rule.Title, + For: model.Duration(rule.For), + Condition: rule.Condition, + Data: data, + DashboardUID: dashboardUID, + PanelID: panelID, + NoDataState: rule.NoDataState, + ExecErrState: rule.ExecErrState, + Annotations: rule.Annotations, + Labels: rule.Labels, + }, nil +} + +// newAlertQueryExport creates a AlertQueryExport DTO from models.AlertQuery. +func newAlertQueryExport(query models.AlertQuery) (AlertQueryExport, error) { + // We unmarshal the json.RawMessage model into a map in order to facilitate yaml marshalling. + var mdl map[string]interface{} + err := json.Unmarshal(query.Model, &mdl) + if err != nil { + return AlertQueryExport{}, err + } + return AlertQueryExport{ + RefID: query.RefID, + QueryType: query.QueryType, + RelativeTimeRange: query.RelativeTimeRange, + DatasourceUID: query.DatasourceUID, + Model: mdl, + }, nil +} diff --git a/pkg/services/provisioning/alerting/rules_types_test.go b/pkg/services/provisioning/alerting/file/rules_types_test.go similarity index 98% rename from pkg/services/provisioning/alerting/rules_types_test.go rename to pkg/services/provisioning/alerting/file/rules_types_test.go index 0eac93e2404..abc2836f01a 100644 --- a/pkg/services/provisioning/alerting/rules_types_test.go +++ b/pkg/services/provisioning/alerting/file/rules_types_test.go @@ -1,13 +1,14 @@ -package alerting +package file import ( "testing" "time" - "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/services/provisioning/values" "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" + + "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/provisioning/values" ) func TestRuleGroup(t *testing.T) { @@ -60,7 +61,7 @@ func TestRuleGroup(t *testing.T) { rg.Interval = interval rgMapped, err := rg.MapToModel() require.NoError(t, err) - require.Equal(t, 48*time.Hour, rgMapped.Interval) + require.Equal(t, int64(48*time.Hour/time.Second), rgMapped.Interval) }) t.Run("a rule group with an empty org id should default to 1", func(t *testing.T) { rg := validRuleGroupV1(t) diff --git a/pkg/services/provisioning/alerting/rules_provisioner.go b/pkg/services/provisioning/alerting/rules_provisioner.go index 55750ff0421..36fdbef76ea 100644 --- a/pkg/services/provisioning/alerting/rules_provisioner.go +++ b/pkg/services/provisioning/alerting/rules_provisioner.go @@ -41,24 +41,24 @@ func (prov *defaultAlertRuleProvisioner) Provision(ctx context.Context, files []*AlertingFile) error { for _, file := range files { for _, group := range file.Groups { - folderUID, err := prov.getOrCreateFolderUID(ctx, group.Folder, group.OrgID) + folderUID, err := prov.getOrCreateFolderUID(ctx, group.FolderTitle, group.OrgID) if err != nil { return err } prov.logger.Debug("provisioning alert rule group", "org", group.OrgID, - "folder", group.Folder, + "folder", group.FolderTitle, "folderUID", folderUID, - "name", group.Name) + "name", group.Title) for _, rule := range group.Rules { rule.NamespaceUID = folderUID - rule.RuleGroup = group.Name - err = prov.provisionRule(ctx, group.OrgID, rule, group.Folder, folderUID) + rule.RuleGroup = group.Title + err = prov.provisionRule(ctx, group.OrgID, rule) if err != nil { return err } } - err = prov.ruleService.UpdateRuleGroup(ctx, group.OrgID, folderUID, group.Name, int64(group.Interval.Seconds())) + err = prov.ruleService.UpdateRuleGroup(ctx, group.OrgID, folderUID, group.Title, group.Interval) if err != nil { return err } @@ -77,9 +77,7 @@ func (prov *defaultAlertRuleProvisioner) Provision(ctx context.Context, func (prov *defaultAlertRuleProvisioner) provisionRule( ctx context.Context, orgID int64, - rule alert_models.AlertRule, - folder, - folderUID string) error { + rule alert_models.AlertRule) error { prov.logger.Debug("provisioning alert rule", "uid", rule.UID, "org", rule.OrgID) _, _, err := prov.ruleService.GetAlertRule(ctx, orgID, rule.UID) if err != nil && !errors.Is(err, alert_models.ErrAlertRuleNotFound) { diff --git a/pkg/services/provisioning/alerting/types.go b/pkg/services/provisioning/alerting/types.go index 6b500c85dd1..6a3331c095b 100644 --- a/pkg/services/provisioning/alerting/types.go +++ b/pkg/services/provisioning/alerting/types.go @@ -3,6 +3,7 @@ package alerting import ( "fmt" + "github.com/grafana/grafana/pkg/services/provisioning/alerting/file" "github.com/grafana/grafana/pkg/services/provisioning/values" ) @@ -15,8 +16,8 @@ type OrgID int64 type AlertingFile struct { configVersion Filename string - Groups []AlertRuleGroup - DeleteRules []RuleDelete + Groups []file.AlertRuleGroupWithFolderTitle + DeleteRules []file.RuleDelete ContactPoints []ContactPoint DeleteContactPoints []DeleteContactPoint Policies []NotificiationPolicy @@ -30,8 +31,8 @@ type AlertingFile struct { type AlertingFileV1 struct { configVersion Filename string - Groups []AlertRuleGroupV1 `json:"groups" yaml:"groups"` - DeleteRules []RuleDeleteV1 `json:"deleteRules" yaml:"deleteRules"` + Groups []file.AlertRuleGroupV1 `json:"groups" yaml:"groups"` + DeleteRules []file.RuleDeleteV1 `json:"deleteRules" yaml:"deleteRules"` ContactPoints []ContactPointV1 `json:"contactPoints" yaml:"contactPoints"` DeleteContactPoints []DeleteContactPointV1 `json:"deleteContactPoints" yaml:"deleteContactPoints"` Policies []NotificiationPolicyV1 `json:"policies" yaml:"policies"` @@ -132,7 +133,7 @@ func (fileV1 *AlertingFileV1) mapRules(alertingFile *AlertingFile) error { if orgID < 1 { orgID = 1 } - ruleDelete := RuleDelete{ + ruleDelete := file.RuleDelete{ UID: ruleDeleteV1.UID.Value(), OrgID: orgID, } diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go index fe6f7c727fa..9f7bff61917 100644 --- a/pkg/services/provisioning/provisioning.go +++ b/pkg/services/provisioning/provisioning.go @@ -269,6 +269,7 @@ func (ps *ProvisioningServiceImpl) ProvisionAlerting(ctx context.Context) error ruleService := provisioning.NewAlertRuleService( st, st, + ps.dashboardService, ps.quotaService, ps.SQLStore, int64(ps.Cfg.UnifiedAlerting.DefaultRuleEvaluationInterval.Seconds()), diff --git a/public/api-merged.json b/public/api-merged.json index 8f18508d55e..2505ee36660 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -2496,6 +2496,35 @@ } } }, + "/api/v1/provisioning/alert-rules/export": { + "get": { + "tags": [ + "provisioning" + ], + "summary": "Export all alert rules in provisioning file format.", + "operationId": "RouteGetAlertRulesExport", + "parameters": [ + { + "type": "boolean", + "default": false, + "description": "Whether to initiate a download of the file or not.", + "name": "download", + "in": "query" + } + ], + "responses": { + "200": { + "description": "AlertingFileExport", + "schema": { + "$ref": "#/definitions/AlertingFileExport" + } + }, + "404": { + "description": " Not found." + } + } + } + }, "/api/v1/provisioning/alert-rules/{UID}": { "get": { "tags": [ @@ -2591,6 +2620,47 @@ } } }, + "/api/v1/provisioning/alert-rules/{UID}/export": { + "get": { + "produces": [ + "application/json", + "application/yaml", + "text/yaml" + ], + "tags": [ + "provisioning" + ], + "summary": "Export an alert rule in provisioning file format.", + "operationId": "RouteGetAlertRuleExport", + "parameters": [ + { + "type": "string", + "description": "Alert rule UID", + "name": "UID", + "in": "path", + "required": true + }, + { + "type": "boolean", + "default": false, + "description": "Whether to initiate a download of the file or not.", + "name": "download", + "in": "query" + } + ], + "responses": { + "200": { + "description": "AlertingFileExport", + "schema": { + "$ref": "#/definitions/AlertingFileExport" + } + }, + "404": { + "description": " Not found." + } + } + } + }, "/api/v1/provisioning/contact-points": { "get": { "tags": [ @@ -2794,6 +2864,52 @@ } } }, + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export": { + "get": { + "produces": [ + "application/json", + "application/yaml", + "text/yaml" + ], + "tags": [ + "provisioning" + ], + "summary": "Export an alert rule group in provisioning file format.", + "operationId": "RouteGetAlertRuleGroupExport", + "parameters": [ + { + "type": "string", + "name": "FolderUID", + "in": "path", + "required": true + }, + { + "type": "string", + "name": "Group", + "in": "path", + "required": true + }, + { + "type": "boolean", + "default": false, + "description": "Whether to initiate a download of the file or not.", + "name": "download", + "in": "query" + } + ], + "responses": { + "200": { + "description": "AlertingFileExport", + "schema": { + "$ref": "#/definitions/AlertingFileExport" + } + }, + "404": { + "description": " Not found." + } + } + } + }, "/api/v1/provisioning/mute-timings": { "get": { "tags": [ @@ -11044,6 +11160,28 @@ } } }, + "AlertQueryExport": { + "type": "object", + "title": "AlertQueryExport is the provisioned export of models.AlertQuery.", + "properties": { + "datasourceUid": { + "type": "string" + }, + "model": { + "type": "object", + "additionalProperties": false + }, + "queryType": { + "type": "string" + }, + "refId": { + "type": "string" + }, + "relativeTimeRange": { + "$ref": "#/definitions/RelativeTimeRange" + } + } + }, "AlertResponse": { "type": "object", "required": [ @@ -11064,6 +11202,65 @@ } } }, + "AlertRuleExport": { + "type": "object", + "title": "AlertRuleExport is the provisioned file export of models.AlertRule.", + "properties": { + "annotations": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "condition": { + "type": "string" + }, + "dasboardUid": { + "type": "string" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/AlertQueryExport" + } + }, + "execErrState": { + "type": "string", + "enum": [ + "Alerting", + "Error", + "OK" + ] + }, + "for": { + "$ref": "#/definitions/Duration" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "noDataState": { + "type": "string", + "enum": [ + "Alerting", + "NoData", + "OK" + ] + }, + "panelId": { + "type": "integer", + "format": "int64" + }, + "title": { + "type": "string" + }, + "uid": { + "type": "string" + } + } + }, "AlertRuleGroup": { "type": "object", "properties": { @@ -11085,6 +11282,31 @@ } } }, + "AlertRuleGroupExport": { + "type": "object", + "title": "AlertRuleGroupExport is the provisioned file export of AlertRuleGroupV1.", + "properties": { + "folder": { + "type": "string" + }, + "interval": { + "$ref": "#/definitions/Duration" + }, + "name": { + "type": "string" + }, + "orgId": { + "type": "integer", + "format": "int64" + }, + "rules": { + "type": "array", + "items": { + "$ref": "#/definitions/AlertRuleExport" + } + } + } + }, "AlertRuleGroupMetadata": { "type": "object", "properties": { @@ -11174,6 +11396,22 @@ } } }, + "AlertingFileExport": { + "type": "object", + "title": "AlertingFileExport is the full provisioned file export.", + "properties": { + "apiVersion": { + "type": "integer", + "format": "int64" + }, + "groups": { + "type": "array", + "items": { + "$ref": "#/definitions/AlertRuleGroupExport" + } + } + } + }, "AlertingRule": { "description": "adapted from cortex", "type": "object", @@ -17824,9 +18062,8 @@ "type": "string" }, "URL": { - "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.", "type": "object", - "title": "A URL represents a parsed URL (technically, a URI reference).", + "title": "URL is a custom URL type that allows validation at configuration load time.", "properties": { "ForceQuery": { "type": "boolean" @@ -18852,6 +19089,7 @@ } }, "gettableAlerts": { + "description": "GettableAlerts gettable alerts", "type": "array", "items": { "$ref": "#/definitions/gettableAlert" @@ -18912,7 +19150,6 @@ } }, "integration": { - "description": "Integration integration", "type": "object", "required": [ "name", From 5ad7cca9d49ad22996ce1949902164f21e1d78e1 Mon Sep 17 00:00:00 2001 From: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com> Date: Fri, 27 Jan 2023 10:40:12 -0600 Subject: [PATCH 060/117] Prometheus: Fix "-Instant" string showing up in prometheus instant query UI (#62265) Add string constant and make sure to remove from name before outputting to frontend --- .../explore/PrometheusListView/ItemLabels.tsx | 12 +++++++++++- .../app/plugins/datasource/prometheus/datasource.tsx | 4 +++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/public/app/features/explore/PrometheusListView/ItemLabels.tsx b/public/app/features/explore/PrometheusListView/ItemLabels.tsx index 40fb7ba9712..b5db12afe93 100644 --- a/public/app/features/explore/PrometheusListView/ItemLabels.tsx +++ b/public/app/features/explore/PrometheusListView/ItemLabels.tsx @@ -4,6 +4,8 @@ import React from 'react'; import { Field, GrafanaTheme2 } from '@grafana/data/'; import { useStyles2 } from '@grafana/ui/'; +import { InstantQueryRefIdIndex } from '../../../plugins/datasource/prometheus/datasource'; + import { rawListItemColumnWidth } from './RawListItem'; const getItemLabelsStyles = (theme: GrafanaTheme2, expanded: boolean) => { @@ -22,14 +24,22 @@ const getItemLabelsStyles = (theme: GrafanaTheme2, expanded: boolean) => { }; }; +const formatValueName = (name: string): string => { + if (name.includes(InstantQueryRefIdIndex)) { + return name.replace(InstantQueryRefIdIndex, ''); + } + return name; +}; + export const ItemLabels = ({ valueLabels, expanded }: { valueLabels: Field[]; expanded: boolean }) => { const styles = useStyles2((theme) => getItemLabelsStyles(theme, expanded)); + return (
{valueLabels.map((value, index) => ( - {value.name} + {formatValueName(value.name)} ))}
diff --git a/public/app/plugins/datasource/prometheus/datasource.tsx b/public/app/plugins/datasource/prometheus/datasource.tsx index 06e1721fd18..afc3faf44ab 100644 --- a/public/app/plugins/datasource/prometheus/datasource.tsx +++ b/public/app/plugins/datasource/prometheus/datasource.tsx @@ -70,6 +70,8 @@ import { PrometheusVariableSupport } from './variables'; const ANNOTATION_QUERY_STEP_DEFAULT = '60s'; const GET_AND_POST_METADATA_ENDPOINTS = ['api/v1/query', 'api/v1/query_range', 'api/v1/series', 'api/v1/labels']; +export const InstantQueryRefIdIndex = '-Instant'; + export class PrometheusDatasource extends DataSourceWithBackend implements DataSourceWithQueryImportSupport, DataSourceWithQueryExportSupport @@ -430,7 +432,7 @@ export class PrometheusDatasource }, { ...processedTarget, - refId: processedTarget.refId + '-Instant', + refId: processedTarget.refId + InstantQueryRefIdIndex, range: false, } ); From b2c8126e6ef9301ab4cef5d8f7ad4f9a4befbf65 Mon Sep 17 00:00:00 2001 From: Gareth Dawson Date: Fri, 27 Jan 2023 17:32:53 +0000 Subject: [PATCH 061/117] Loki: Show query size approximation (#62109) * feat: make api request to /loki/api/v1/index/stats * fix: add /index/stats to callResource valid urls * feat: make call to getQueryStats when the query changes * feat: render user tooltip displaying the estimated value for processed data * fix: add new props to component tests * test: add tests for query size estimation * fix: disable error message on request failure * refactor: add suggestions from code review * refactor: only pass required query string --- pkg/tsdb/loki/loki.go | 3 +- .../loki/components/LokiQueryEditor.tsx | 1 + .../app/plugins/datasource/loki/datasource.ts | 38 +++- .../plugins/datasource/loki/modifyQuery.ts | 2 +- .../app/plugins/datasource/loki/queryUtils.ts | 11 ++ .../LokiQueryBuilderOptions.test.tsx | 2 + .../components/LokiQueryBuilderOptions.tsx | 167 +++++++++++------- public/app/plugins/datasource/loki/types.ts | 7 + .../shared/QueryOptionGroup.test.tsx | 48 +++++ .../querybuilder/shared/QueryOptionGroup.tsx | 55 ++++-- 10 files changed, 243 insertions(+), 91 deletions(-) create mode 100644 public/app/plugins/datasource/prometheus/querybuilder/shared/QueryOptionGroup.test.tsx diff --git a/pkg/tsdb/loki/loki.go b/pkg/tsdb/loki/loki.go index bb0147318a1..06c57a90924 100644 --- a/pkg/tsdb/loki/loki.go +++ b/pkg/tsdb/loki/loki.go @@ -112,7 +112,8 @@ func callResource(ctx context.Context, req *backend.CallResourceRequest, sender } if (!strings.HasPrefix(url, "labels?")) && (!strings.HasPrefix(url, "label/")) && // the `/label/$label_name/values` form - (!strings.HasPrefix(url, "series?")) { + (!strings.HasPrefix(url, "series?")) && + (!strings.HasPrefix(url, "index/stats?")) { return fmt.Errorf("invalid resource URL: %s", url) } lokiURL := fmt.Sprintf("/loki/api/v1/%s", url) diff --git a/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx b/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx index 85a9bd97902..deb39ffe250 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx @@ -171,6 +171,7 @@ export const LokiQueryEditor = React.memo((props) => { onRunQuery={onRunQuery} app={app} maxLines={datasource.maxLines} + datasource={datasource} /> diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 0d1ec4e37e6..a96fa406940 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -34,7 +34,7 @@ import { TimeRange, toUtc, } from '@grafana/data'; -import { config, DataSourceWithBackend, FetchError } from '@grafana/runtime'; +import { BackendSrvRequest, config, DataSourceWithBackend, FetchError } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; import { queryLogsSample, queryLogsVolume } from 'app/core/logsModel'; import { convertToWebSocketUrl } from 'app/core/utils/explore'; @@ -71,6 +71,7 @@ import { getQueryHints } from './queryHints'; import { getLogQueryFromMetricsQuery, getNormalizedLokiQuery, + getStreamSelectorsFromQuery, getParserFromQuery, isLogsQuery, isValidQuery, @@ -86,6 +87,7 @@ import { LokiQueryType, LokiVariableQuery, LokiVariableQueryType, + QueryStats, SupportingQueryType, } from './types'; import { LokiVariableSupport } from './variables'; @@ -401,15 +403,43 @@ export class LokiDatasource return queries.map((query) => this.languageProvider.exportToAbstractQuery(query)); } - async metadataRequest(url: string, params?: Record) { + async metadataRequest(url: string, params?: Record, options?: Partial) { // url must not start with a `/`, otherwise the AJAX-request // going from the browser will contain `//`, which can cause problems. if (url.startsWith('/')) { throw new Error(`invalid metadata request url: ${url}`); } - const res = await this.getResource(url, params); - return res.data || []; + const res = await this.getResource(url, params, options); + return res.data ?? (res || []); + } + + async getQueryStats(query: LokiQuery): Promise { + const { start, end } = this.getTimeRangeParams(); + const labelMatchers = getStreamSelectorsFromQuery(query.expr); + + let statsForAll: QueryStats = { streams: 0, chunks: 0, bytes: 0, entries: 0 }; + + for (const labelMatcher of labelMatchers) { + try { + const data = await this.metadataRequest( + 'index/stats', + { query: labelMatcher, start, end }, + { showErrorAlert: false } + ); + + statsForAll = { + streams: statsForAll.streams + data.streams, + chunks: statsForAll.chunks + data.chunks, + bytes: statsForAll.bytes + data.bytes, + entries: statsForAll.entries + data.entries, + }; + } catch (e) { + break; + } + } + + return statsForAll; } async metricFindQuery(query: LokiVariableQuery | string) { diff --git a/public/app/plugins/datasource/loki/modifyQuery.ts b/public/app/plugins/datasource/loki/modifyQuery.ts index 610c9b56a22..bc124eed541 100644 --- a/public/app/plugins/datasource/loki/modifyQuery.ts +++ b/public/app/plugins/datasource/loki/modifyQuery.ts @@ -139,7 +139,7 @@ export function removeCommentsFromQuery(query: string): string { * selector. * @param query */ -function getStreamSelectorPositions(query: string): Position[] { +export function getStreamSelectorPositions(query: string): Position[] { const tree = parser.parse(query); const positions: Position[] = []; tree.iterate({ diff --git a/public/app/plugins/datasource/loki/queryUtils.ts b/public/app/plugins/datasource/loki/queryUtils.ts index a5a1444b86e..779215d1c19 100644 --- a/public/app/plugins/datasource/loki/queryUtils.ts +++ b/public/app/plugins/datasource/loki/queryUtils.ts @@ -21,6 +21,7 @@ import { import { ErrorId } from '../prometheus/querybuilder/shared/parsingUtils'; +import { getStreamSelectorPositions } from './modifyQuery'; import { LokiQuery, LokiQueryType } from './types'; export function formatQuery(selector: string | undefined): string { @@ -284,3 +285,13 @@ export function isQueryWithLineFilter(query: string): boolean { return queryWithLineFilter; } + +export function getStreamSelectorsFromQuery(query: string): string[] { + const labelMatcherPositions = getStreamSelectorPositions(query); + + const labelMatchers = labelMatcherPositions.map((labelMatcher) => { + return query.slice(labelMatcher.from, labelMatcher.to); + }); + + return labelMatchers; +} diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.test.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.test.tsx index 3f0c9e3eb59..ca098f1e3e1 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.test.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.test.tsx @@ -2,6 +2,7 @@ import { fireEvent, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; +import { createLokiDatasource } from '../../mocks'; import { LokiQuery, LokiQueryType } from '../../types'; import { LokiQueryBuilderOptions } from './LokiQueryBuilderOptions'; @@ -47,6 +48,7 @@ function setup(queryOverrides: Partial = {}) { onRunQuery: jest.fn(), onChange: jest.fn(), maxLines: 20, + datasource: createLokiDatasource(), }; const { container } = render(); diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.tsx index cc46e9631ce..b9a80ca4df3 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.tsx @@ -1,4 +1,5 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; +import { usePrevious } from 'react-use'; import { CoreApp, SelectableValue } from '@grafana/data'; import { EditorField, EditorRow } from '@grafana/experimental'; @@ -7,8 +8,9 @@ import { RadioButtonGroup, Select, AutoSizeInput } from '@grafana/ui'; import { QueryOptionGroup } from 'app/plugins/datasource/prometheus/querybuilder/shared/QueryOptionGroup'; import { preprocessMaxLines, queryTypeOptions, RESOLUTION_OPTIONS } from '../../components/LokiOptionFields'; +import { LokiDatasource } from '../../datasource'; import { isLogsQuery } from '../../queryUtils'; -import { LokiQuery, LokiQueryType } from '../../types'; +import { LokiQuery, LokiQueryType, QueryStats } from '../../types'; export interface Props { query: LokiQuery; @@ -16,83 +18,112 @@ export interface Props { onRunQuery: () => void; maxLines: number; app?: CoreApp; + datasource: LokiDatasource; } -export const LokiQueryBuilderOptions = React.memo(({ app, query, onChange, onRunQuery, maxLines }) => { - const onQueryTypeChange = (value: LokiQueryType) => { - onChange({ ...query, queryType: value }); - onRunQuery(); - }; +export const LokiQueryBuilderOptions = React.memo( + ({ app, query, onChange, onRunQuery, maxLines, datasource }) => { + const [queryStats, setQueryStats] = useState(); + const prevQuery = usePrevious(query); - const onResolutionChange = (option: SelectableValue) => { - reportInteraction('grafana_loki_resolution_clicked', { - app, - resolution: option.value, - }); - onChange({ ...query, resolution: option.value }); - onRunQuery(); - }; - - const onLegendFormatChanged = (evt: React.FormEvent) => { - onChange({ ...query, legendFormat: evt.currentTarget.value }); - onRunQuery(); - }; - - function onMaxLinesChange(e: React.SyntheticEvent) { - const newMaxLines = preprocessMaxLines(e.currentTarget.value); - if (query.maxLines !== newMaxLines) { - onChange({ ...query, maxLines: newMaxLines }); + const onQueryTypeChange = (value: LokiQueryType) => { + onChange({ ...query, queryType: value }); onRunQuery(); + }; + + const onResolutionChange = (option: SelectableValue) => { + reportInteraction('grafana_loki_resolution_clicked', { + app, + resolution: option.value, + }); + onChange({ ...query, resolution: option.value }); + onRunQuery(); + }; + + const onLegendFormatChanged = (evt: React.FormEvent) => { + onChange({ ...query, legendFormat: evt.currentTarget.value }); + onRunQuery(); + }; + + function onMaxLinesChange(e: React.SyntheticEvent) { + const newMaxLines = preprocessMaxLines(e.currentTarget.value); + if (query.maxLines !== newMaxLines) { + onChange({ ...query, maxLines: newMaxLines }); + onRunQuery(); + } } - } - let queryType = query.queryType ?? (query.instant ? LokiQueryType.Instant : LokiQueryType.Range); - let showMaxLines = isLogsQuery(query.expr); + useEffect(() => { + if (query.expr === prevQuery?.expr) { + return; + } - return ( - - - { + const res = await datasource.getQueryStats(query); + + // this filters out the case where the user has not configured loki to use tsdb, in that case all keys in the query stats will be 0 + Object.values(res).every((v) => v === 0) ? setQueryStats(undefined) : setQueryStats(res); + }; + makeAsyncRequest(); + }, [query, prevQuery, datasource]); + + let queryType = query.queryType ?? (query.instant ? LokiQueryType.Instant : LokiQueryType.Range); + let showMaxLines = isLogsQuery(query.expr); + + return ( + + - - - - - - {showMaxLines && ( - + - )} - - + + + + ); + } +); function getCollapsedInfo( query: LokiQuery, diff --git a/public/app/plugins/datasource/loki/types.ts b/public/app/plugins/datasource/loki/types.ts index d1339f659dc..ec4650b12b7 100644 --- a/public/app/plugins/datasource/loki/types.ts +++ b/public/app/plugins/datasource/loki/types.ts @@ -154,6 +154,13 @@ export interface LokiVariableQuery extends DataQuery { stream?: string; } +export interface QueryStats { + streams: number; + chunks: number; + bytes: number; + entries: number; +} + export enum SupportingQueryType { LogsVolume = 'logsVolume', LogsSample = 'logsSample', diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/QueryOptionGroup.test.tsx b/public/app/plugins/datasource/prometheus/querybuilder/shared/QueryOptionGroup.test.tsx new file mode 100644 index 00000000000..2daf2794646 --- /dev/null +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/QueryOptionGroup.test.tsx @@ -0,0 +1,48 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { QueryOptionGroup, Props } from './QueryOptionGroup'; + +describe('Query size approximation', () => { + const _1KiB = 1024; // size of 1 KiB in bytes + const _1GiB = 1073741824; // ... + const _1PiB = 1125899906842624; + + it('renders the correct data value given 1 KiB', async () => { + const props = createProps(_1KiB); + render(); + expect(screen.getByText(/This query will process approximately 1.0 KiB/)).toBeInTheDocument(); + }); + + it('renders the correct data value given 1 GiB', async () => { + const props = createProps(_1GiB); + render(); + expect(screen.getByText(/This query will process approximately 1.0 GiB/)).toBeInTheDocument(); + }); + + it('renders the correct data value given 1 PiB', async () => { + const props = createProps(_1PiB); + render(); + expect(screen.getByText(/This query will process approximately 1.0 PiB/)).toBeInTheDocument(); + }); + + it('updates the data value on data change', async () => { + const props1 = createProps(_1KiB); + const props2 = createProps(_1PiB); + + const { rerender } = render(); + expect(screen.getByText(/This query will process approximately 1.0 KiB/)).toBeInTheDocument(); + + rerender(); + expect(screen.getByText(/This query will process approximately 1.0 PiB/)).toBeInTheDocument(); + }); +}); + +function createProps(bytes?: number): Props { + return { + title: 'Options', + collapsedInfo: ['Type: Range', 'Line limit: 1000'], + children:
, + queryStats: { streams: 0, chunks: 0, bytes: bytes ?? 0, entries: 0 }, + }; +} diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/QueryOptionGroup.tsx b/public/app/plugins/datasource/prometheus/querybuilder/shared/QueryOptionGroup.tsx index d3dc219ecd8..9e093588bf0 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/shared/QueryOptionGroup.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/QueryOptionGroup.tsx @@ -2,42 +2,58 @@ import { css } from '@emotion/css'; import React from 'react'; import { useToggle } from 'react-use'; -import { GrafanaTheme2 } from '@grafana/data'; +import { getValueFormat, GrafanaTheme2 } from '@grafana/data'; import { Stack } from '@grafana/experimental'; import { Icon, useStyles2 } from '@grafana/ui'; +import { QueryStats } from 'app/plugins/datasource/loki/types'; export interface Props { title: string; collapsedInfo: string[]; + queryStats?: QueryStats; children: React.ReactNode; } -export function QueryOptionGroup({ title, children, collapsedInfo }: Props) { +export function QueryOptionGroup({ title, children, collapsedInfo, queryStats }: Props) { const [isOpen, toggleOpen] = useToggle(false); const styles = useStyles2(getStyles); + const convertUnits = (): string => { + const { text, suffix } = getValueFormat('bytes')(queryStats!.bytes, 1); + return text + suffix; + }; + return ( - -
-
- -
-
{title}
- {!isOpen && ( -
- {collapsedInfo.map((x, i) => ( - {x} - ))} +
+ +
+
+
- )} -
- {isOpen &&
{children}
} -
+
{title}
+ {!isOpen && ( +
+ {collapsedInfo.map((x, i) => ( + {x} + ))} +
+ )} +
+ {isOpen &&
{children}
} + + {queryStats &&

This query will process approximately {convertUnits()}.

} +
); } const getStyles = (theme: GrafanaTheme2) => { return { + wrapper: css({ + width: '100%', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'baseline', + }), switchLabel: css({ color: theme.colors.text.secondary, cursor: 'pointer', @@ -79,5 +95,10 @@ const getStyles = (theme: GrafanaTheme2) => { color: theme.colors.text.secondary, marginRight: `${theme.spacing(1)}`, }), + stats: css({ + margin: '0px', + color: theme.colors.text.secondary, + fontSize: theme.typography.bodySmall.fontSize, + }), }; }; From 4563111180d12f656faf87f0626c07914f7ff782 Mon Sep 17 00:00:00 2001 From: Jack Baldry Date: Fri, 27 Jan 2023 17:53:50 +0000 Subject: [PATCH 062/117] Move Grafana tutorials from grafana/tutorials repository (#62309) * Add Grafana tutorials originally from tutorials repository Signed-off-by: Jack Baldry * Replace tutorials/step shortcode with ordinary headings Signed-off-by: Jack Baldry * Fix typos reported by codespell Signed-off-by: Jack Baldry * Fix doc-validator linting and run prettier Signed-off-by: Jack Baldry * Specify version in tutorials lookup as non-rendered pages do not have a relative permalink used to infer the version Signed-off-by: Jack Baldry * Use latest version Ensures CI passes and only breaks one website build as the backport to v9.3.x will solve the missing "latest" pages on publishing. Signed-off-by: Jack Baldry --------- Signed-off-by: Jack Baldry --- .../sources/shared/tutorials/create-plugin.md | 40 ++ .../shared/tutorials/plugin-anatomy.md | 29 ++ .../shared/tutorials/publish-your-plugin.md | 77 ++++ .../shared/tutorials/set-up-environment.md | 34 ++ docs/sources/tutorials/_index.md | 9 + .../index.md | 180 +++++++++ .../build-a-data-source-plugin/index.md | 372 ++++++++++++++++++ .../build-a-panel-plugin-with-d3/index.md | 235 +++++++++++ .../tutorials/build-a-panel-plugin/index.md | 259 ++++++++++++ .../index.md | 164 ++++++++ .../tutorials/build-an-app-plugin/index.md | 208 ++++++++++ .../create-alerts-from-flux-queries/index.md | 331 ++++++++++++++++ .../tutorials/create-users-and-teams/index.md | 236 +++++++++++ .../tutorials/grafana-fundamentals/index.md | 354 +++++++++++++++++ docs/sources/tutorials/iis/index.md | 146 +++++++ .../install-grafana-on-raspberry-pi/index.md | 147 +++++++ .../tutorials/integrate-hubot/index.md | 118 ++++++ .../index.md | 260 ++++++++++++ .../run-grafana-behind-a-proxy/index.md | 222 +++++++++++ .../index.md | 101 +++++ 20 files changed, 3522 insertions(+) create mode 100755 docs/sources/shared/tutorials/create-plugin.md create mode 100644 docs/sources/shared/tutorials/plugin-anatomy.md create mode 100644 docs/sources/shared/tutorials/publish-your-plugin.md create mode 100644 docs/sources/shared/tutorials/set-up-environment.md create mode 100644 docs/sources/tutorials/_index.md create mode 100644 docs/sources/tutorials/build-a-data-source-backend-plugin/index.md create mode 100644 docs/sources/tutorials/build-a-data-source-plugin/index.md create mode 100644 docs/sources/tutorials/build-a-panel-plugin-with-d3/index.md create mode 100644 docs/sources/tutorials/build-a-panel-plugin/index.md create mode 100644 docs/sources/tutorials/build-a-streaming-data-source-plugin/index.md create mode 100644 docs/sources/tutorials/build-an-app-plugin/index.md create mode 100644 docs/sources/tutorials/create-alerts-from-flux-queries/index.md create mode 100644 docs/sources/tutorials/create-users-and-teams/index.md create mode 100644 docs/sources/tutorials/grafana-fundamentals/index.md create mode 100644 docs/sources/tutorials/iis/index.md create mode 100644 docs/sources/tutorials/install-grafana-on-raspberry-pi/index.md create mode 100644 docs/sources/tutorials/integrate-hubot/index.md create mode 100644 docs/sources/tutorials/provision-dashboards-and-data-sources/index.md create mode 100644 docs/sources/tutorials/run-grafana-behind-a-proxy/index.md create mode 100644 docs/sources/tutorials/stream-metrics-from-telegraf-to-grafana/index.md diff --git a/docs/sources/shared/tutorials/create-plugin.md b/docs/sources/shared/tutorials/create-plugin.md new file mode 100755 index 00000000000..656bce3cf9e --- /dev/null +++ b/docs/sources/shared/tutorials/create-plugin.md @@ -0,0 +1,40 @@ +--- +title: Create Plugin +--- + +Tooling for modern web development can be tricky to wrap your head around. While you certainly can write your own webpack configuration, for this guide, you'll be using grafana create-plugin tool + +Grafana [create-plugin tool](https://www.npmjs.com/package/@grafana/create-plugin) is a CLI application that simplifies Grafana plugin development, so that you can focus on code. The tool scaffolds a starter plugin and all the required configuration for you. + +1. In the plugin directory, create a plugin from template using create-plugin: + + ``` + npx @grafana/create-plugin + ``` + +1. Change directory to your newly created plugin: + + ``` + cd my-plugin + ``` + +1. Install the dependencies: + + ``` + yarn install + ``` + +1. Build the plugin: + + ``` + yarn dev + ``` + +1. Restart the Grafana server for Grafana to discover your plugin. +1. Open Grafana and go to **Configuration** -> **Plugins**. Make sure that your plugin is there. + +By default, Grafana logs whenever it discovers a plugin: + +``` +INFO[01-01|12:00:00] Registering plugin logger=plugins name=my-plugin +``` diff --git a/docs/sources/shared/tutorials/plugin-anatomy.md b/docs/sources/shared/tutorials/plugin-anatomy.md new file mode 100644 index 00000000000..4d6e2c4ead0 --- /dev/null +++ b/docs/sources/shared/tutorials/plugin-anatomy.md @@ -0,0 +1,29 @@ +--- +title: Plugin Anatomy +--- + +Plugins come in different shapes and sizes. Before we dive deeper, let's look at some of the properties that are shared by all of them. + +Every plugin you create will require at least two files: `plugin.json` and `module.ts`. + +### plugin.json + +When Grafana starts, it scans the plugin directory for any subdirectory that contains a `plugin.json` file. The `plugin.json` file contains information about your plugin, and tells Grafana about what capabilities and dependencies your plugin needs. + +While certain plugin types can have specific configuration options, let's look at the mandatory ones: + +- `type` tells Grafana what type of plugin to expect. Grafana supports three types of plugins: `panel`, `datasource`, and `app`. +- `name` is what users will see in the list of plugins. If you're creating a data source, this is typically the name of the database it connects to, such as Prometheus, PostgreSQL, or Stackdriver. +- `id` uniquely identifies your plugin, and should start with your Grafana username, to avoid clashing with other plugins. [Sign up for a Grafana account](/signup/) to claim your username. + +To see all the available configuration settings for the `plugin.json`, refer to the [plugin.json Schema](/docs/grafana/latest/plugins/developing/plugin.json/). + +### module.ts + +After discovering your plugin, Grafana loads the `module.ts` file, the entrypoint for your plugin. `module.ts` exposes the implementation of your plugin, which depends on the type of plugin you're building. + +Specifically, `module.ts` needs to expose an object that extends [GrafanaPlugin](https://github.com/grafana/grafana/blob/08bf2a54523526a7f59f7c6a8dafaace79ab87db/packages/grafana-data/src/types/plugin.ts#L124), and can be any of the following: + +- [PanelPlugin](https://github.com/grafana/grafana/blob/08bf2a54523526a7f59f7c6a8dafaace79ab87db/packages/grafana-data/src/types/panel.ts#L73) +- [DataSourcePlugin](https://github.com/grafana/grafana/blob/08bf2a54523526a7f59f7c6a8dafaace79ab87db/packages/grafana-data/src/types/datasource.ts#L33) +- [AppPlugin](https://github.com/grafana/grafana/blob/45b7de1910819ad0faa7a8aeac2481e675870ad9/packages/grafana-data/src/types/app.ts#L27) diff --git a/docs/sources/shared/tutorials/publish-your-plugin.md b/docs/sources/shared/tutorials/publish-your-plugin.md new file mode 100644 index 00000000000..097f2417608 --- /dev/null +++ b/docs/sources/shared/tutorials/publish-your-plugin.md @@ -0,0 +1,77 @@ +--- +title: Package your plugin +--- + +Once you're happy with your plugin, it's time to package it, and submit to the plugin repository. + +For users to be able to use the plugin without building it themselves, you need to make a production build of the plugin, and commit to a release branch in your repository. + +To submit a plugin to the plugin repository, you need to create a release of your plugin. While we recommend following the branching strategy outlined below, you're free to use one that makes more sense to you. + +#### Create a plugin release + +Let's create version 0.1.0 of our plugin. + +1. Create a branch called `release-0.1.x`. + + ``` + git checkout -b release-0.1.x + ``` + +1. Do a production build. + + ``` + yarn build + ``` + +1. Add the `dist` directory. + + ``` + git add -f dist + ``` + +1. Create the release commit. + + ``` + git commit -m "Release v0.1.0" + ``` + +1. Create a release tag. + + ``` + git tag -a v0.1.0 -m "Create release tag v0.1.0" + ``` + +1. Push to GitHub. `follow-tags` tells Git to push the release tag along with our release branch. + ``` + git push --set-upstream origin release-0.1.x --follow-tags + ``` + +#### Submit the plugin + +For a plugin to be published on [Grafana Plugins](/grafana/plugins/), it needs to be added to the [grafana-plugin-repository](https://github.com/grafana/grafana-plugin-repository). + +1. Fork the [grafana-plugin-repository](https://github.com/grafana/grafana-plugin-repository) + +1. Add your plugin to the `repo.json` file in the project root directory: + + ```json + { + "id": "", + "type": "", + "url": "https://github.com//my-plugin", + "versions": [ + { + "version": "", + "commit": "", + "url": "https://github.com//my-plugin" + } + ] + } + ``` + +1. [Create a pull request](https://github.com/grafana/grafana-plugin-repository/pull/new/master). + +Once your plugin has been accepted, it'll be published on [Grafana Plugin](/grafana/plugins/), available for anyone to [install](/docs/grafana/latest/plugins/installation/)! + +> We're auditing every plugin that's added to make sure it's ready to be published. This means that it might take some time before your plugin is accepted. We're working on adding more automated tests to improve this process. diff --git a/docs/sources/shared/tutorials/set-up-environment.md b/docs/sources/shared/tutorials/set-up-environment.md new file mode 100644 index 00000000000..07eb372727a --- /dev/null +++ b/docs/sources/shared/tutorials/set-up-environment.md @@ -0,0 +1,34 @@ +--- +title: Set up Environment +--- + +Before you can get started building plugins, you need to set up your environment for plugin development. + +To discover plugins, Grafana scans a _plugin directory_, the location of which depends on your operating system. + +1. Create a directory called `grafana-plugins` in your preferred workspace. + +1. Find the `plugins` property in the Grafana configuration file and set the `plugins` property to the path of your `grafana-plugins` directory. Refer to the [Grafana configuration documentation](/docs/grafana/latest/installation/configuration/#plugins) for more information. + + ```ini + [paths] + plugins = "/path/to/grafana-plugins" + ``` + +1. Restart Grafana if it's already running, to load the new configuration. + +### Alternative method: Docker + +If you don't want to install Grafana on your local machine, you can use [Docker](https://www.docker.com). + +To set up Grafana for plugin development using Docker, run the following command: + +``` +docker run -d -p 3000:3000 -v "$(pwd)"/grafana-plugins:/var/lib/grafana/plugins --name=grafana grafana/grafana:7.0.0 +``` + +Since Grafana only loads plugins on start-up, you need to restart the container whenever you add or remove a plugin. + +``` +docker restart grafana +``` diff --git a/docs/sources/tutorials/_index.md b/docs/sources/tutorials/_index.md new file mode 100644 index 00000000000..cfe6c7633df --- /dev/null +++ b/docs/sources/tutorials/_index.md @@ -0,0 +1,9 @@ +--- +title: 'Tutorials' +menuTitle: 'Tutorials' +description: 'Grafana tutorials' +--- + +# Tutorials + +{{< section >}} diff --git a/docs/sources/tutorials/build-a-data-source-backend-plugin/index.md b/docs/sources/tutorials/build-a-data-source-backend-plugin/index.md new file mode 100644 index 00000000000..ebc6c160926 --- /dev/null +++ b/docs/sources/tutorials/build-a-data-source-backend-plugin/index.md @@ -0,0 +1,180 @@ +--- +title: Build a data source backend plugin +summary: Create a backend for your data source plugin. +description: Create a backend for your data source plugin. +id: build-a-data-source-backend-plugin +categories: ['plugins'] +tags: ['beginner'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +weight: 75 +--- + +## Introduction + +Grafana supports a wide range of data sources, including Prometheus, MySQL, and even Datadog. There's a good chance you can already visualize metrics from the systems you have set up. In some cases, though, you already have an in-house metrics solution that you’d like to add to your Grafana dashboards. This tutorial teaches you to build a support for your data source. + +For more information about backend plugins, refer to the documentation on [Backend plugins](/docs/grafana/latest/developers/plugins/backend/). + +In this tutorial, you'll: + +- Build a backend for your data source +- Implement a health check for your data source +- Enable Grafana Alerting for your data source + +{{% class "prerequisite-section" %}} + +#### Prerequisites + +- Knowledge about how data sources are implemented in the frontend. +- Grafana 7.0 +- Go ([Version](https://github.com/grafana/plugin-tools/blob/main/packages/create-plugin/templates/backend/go.mod#L3)) +- [Mage](https://magefile.org/) +- NodeJS ([Version](https://github.com/grafana/plugin-tools/blob/main/packages/create-plugin/templates/common/package.json#L66)) +- yarn + {{% /class %}} + +## Set up your environment + +{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" version="latest" >}} + +## Create a new plugin + +To build a backend for your data source plugin, Grafana requires a binary that it can execute when it loads the plugin during start-up. In this guide, we will build a binary using the [Grafana plugin SDK for Go](/docs/grafana/latest/developers/plugins/backend/grafana-plugin-sdk-for-go/). + +The easiest way to get started is to use the Grafana [create-plugin tool](https://www.npmjs.com/package/@grafana/create-plugin). Navigate to the plugin folder that you configured in step 1 and type: + +``` +npx @grafana/create-plugin +``` + +Follow the steps and select **datasource** as your plugin type and answer **yes** when prompted to create a backend for your plugin. + +```bash +cd my-plugin +``` + +Install frontend dependencies and build frontend parts of the plugin to _dist_ directory: + +```bash +yarn install +yarn build +``` + +Run the following to update [Grafana plugin SDK for Go](/docs/grafana/latest/developers/plugins/backend/grafana-plugin-sdk-for-go/) dependency to the latest minor version: + +```bash +go get -u github.com/grafana/grafana-plugin-sdk-go +go mod tidy +``` + +Build backend plugin binaries for Linux, Windows and Darwin to _dist_ directory: + +```bash +mage -v +``` + +Now, let's verify that the plugin you've built so far can be used in Grafana when creating a new data source: + +1. Restart your Grafana instance. +1. Open Grafana in your web browser. +1. Navigate via the side-menu to **Configuration** -> **Data Sources**. +1. Click **Add data source**. +1. Find your newly created plugin and select it. +1. Enter a name and then click **Save & Test** (ignore any errors reported for now). + +You now have a new data source instance of your plugin that is ready to use in a dashboard: + +1. Navigate via the side-menu to **Create** -> **Dashboard**. +1. Click **Add new panel**. +1. In the query tab, select the data source you just created. +1. A line graph is rendered with one series consisting of two data points. +1. Save the dashboard. + +### Troubleshooting + +#### Grafana doesn't load my plugin + +By default, Grafana requires backend plugins to be signed. To load unsigned backend plugins, you need to +configure Grafana to [allow unsigned plugins](/docs/grafana/latest/plugins/plugin-signature-verification/#allow-unsigned-plugins). +For more information, refer to [Plugin signature verification](/docs/grafana/latest/plugins/plugin-signature-verification/#backend-plugins). + +## Anatomy of a backend plugin + +The folders and files used to build the backend for the data source are: + +| file/folder | description | +| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Magefile.go` | It’s not a requirement to use mage build files, but we strongly recommend using it so that you can use the build targets provided by the plugin SDK. | +| `/go.mod ` | Go modules dependencies, [reference](https://golang.org/cmd/go/#hdr-The_go_mod_file) | +| `/src/plugin.json` | A JSON file describing the backend plugin | +| `/pkg/main.go` | Starting point of the plugin binary. | + +#### plugin.json + +The [plugin.json](/docs/grafana/latest/developers/plugins/metadata/) file is required for all plugins. When building a backend plugin these properties are important: + +| property | description | +| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| backend | Should be set to `true` for backend plugins. This tells Grafana that it should start a binary when loading the plugin. | +| executable | This is the name of the executable that Grafana expects to start, see [plugin.json reference](/docs/grafana/latest/developers/plugins/metadata/) for details. | +| alerting | Should be set to `true` if your backend datasource supports alerting. | + +In the next step we will look at the query endpoint! + +## Implement data queries + +We begin by opening the file `/pkg/plugin/plugin.go`. In this file you will see the `SampleDatasource` struct which implements the [backend.QueryDataHandler](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/backend?tab=doc#QueryDataHandler) interface. The `QueryData` method on this struct is where the data fetching happens for a data source plugin. + +Each request contains multiple queries to reduce traffic between Grafana and plugins. So you need to loop over the slice of queries, process each query, and then return the results of all queries. + +In the tutorial we have extracted a method named `query` to take care of each query model. Since each plugin has their own unique query model, Grafana sends it to the backend plugin as JSON. Therefore the plugin needs to `Unmarshal` the query model into something easier to work with. + +As you can see the sample only returns static numbers. Try to extend the plugin to return other types of data. + +You can read more about how to [build data frames in our docs](/docs/grafana/latest/developers/plugins/data-frames/). + +## Add support for health checks + +Implementing the health check handler allows Grafana to verify that a data source has been configured correctly. + +When editing a data source in Grafana's UI, you can **Save & Test** to verify that it works as expected. + +In this sample data source, there is a 50% chance that the health check will be successful. Make sure to return appropriate error messages to the users, informing them about what is misconfigured in the data source. + +Open `/pkg/plugin/plugin.go`. In this file you'll see that the `SampleDatasource` struct also implements the [backend.CheckHealthHandler](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/backend?tab=doc#CheckHealthHandler) interface. Navigate to the `CheckHealth` method to see how the health check for this sample plugin is implemented. + +## Enable Grafana Alerting + +1. Open _src/plugin.json_. +1. Add the top level `backend` property with a value of `true` to specify that your plugin supports Grafana Alerting, e.g. + ```json + { + ... + "backend": true, + "executable": "gpx_simple_datasource_backend", + "alerting": true, + "info": { + ... + } + ``` +1. Rebuild frontend parts of the plugin to _dist_ directory: + +```bash +yarn build +``` + +1. Restart your Grafana instance. +1. Open Grafana in your web browser. +1. Open the dashboard you created earlier in the _Create a new plugin_ step. +1. Edit the existing panel. +1. Click on the _Alert_ tab. +1. Click on _Create Alert_ button. +1. Edit condition and specify _IS ABOVE 10_. Change _Evaluate every_ to _10s_ and clear the _For_ field to make the alert rule evaluate quickly. +1. Save the dashboard. +1. After some time the alert rule evaluates and transitions into _Alerting_ state. + +## Summary + +In this tutorial you created a backend for your data source plugin. diff --git a/docs/sources/tutorials/build-a-data-source-plugin/index.md b/docs/sources/tutorials/build-a-data-source-plugin/index.md new file mode 100644 index 00000000000..8cb9af04281 --- /dev/null +++ b/docs/sources/tutorials/build-a-data-source-plugin/index.md @@ -0,0 +1,372 @@ +--- +title: Build a data source plugin +summary: Create a plugin to add support for your own data sources. +description: Create a plugin to add support for your own data sources. +id: build-a-data-source-plugin +categories: ['plugins'] +tags: ['beginner'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +weight: 70 +--- + +## Introduction + +Grafana supports a wide range of data sources, including Prometheus, MySQL, and even Datadog. There's a good chance you can already visualize metrics from the systems you have set up. In some cases, though, you already have an in-house metrics solution that you’d like to add to your Grafana dashboards. This tutorial teaches you to build a support for your data source. + +In this tutorial, you'll: + +- Build a data source to visualize a sine wave +- Construct queries using the query editor +- Configure your data source using the config editor + +{{% class "prerequisite-section" %}} + +### Prerequisites + +- Grafana >=7.0 +- NodeJS >=14 +- yarn + {{% /class %}} + +## Set up your environment + +{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" version="latest" >}} + +## Create a new plugin + +{{< docs/shared lookup="tutorials/create-plugin.md" source="grafana" version="latest" >}} + +## Anatomy of a plugin + +{{< docs/shared lookup="tutorials/plugin-anatomy.md" source="grafana" version="latest" >}} + +## Data source plugins + +A data source in Grafana must extend the `DataSourceApi` interface, which requires you to defines two methods: `query` and `testDatasource`. + +### The `query` method + +The `query` method is the heart of any data source plugin. It accepts a query from the user, retrieves the data from an external database, and returns the data in a format that Grafana recognizes. + +``` +async query(options: DataQueryRequest): Promise +``` + +The `options` object contains the queries, or _targets_, that the user made, along with context information, like the current time interval. Use this information to query an external database. + +> The term _target_ originates from Graphite, and the earlier days of Grafana when Graphite was the only supported data source. As Grafana gained support for more data sources, the term "target" became synonymous with any type of query. + +### Test your data source + +`testDatasource` implements a health check for your data source. For example, Grafana calls this method whenever the user clicks the **Save & Test** button, after changing the connection settings. + +``` +async testDatasource() +``` + +## Data frames + +Nowadays there are countless of different databases, each with their own ways of querying data. To be able to support all the different data formats, Grafana consolidates the data into a unified data structure called _data frames_. + +Let's see how to create and return a data frame from the `query` method. In this step, you'll change the code in the starter plugin to return a [sine wave](https://en.wikipedia.org/wiki/Sine_wave). + +1. In the current `query` method, remove the code inside the `map` function. + + The `query` method now look like this: + + ```ts + async query(options: DataQueryRequest): Promise { + const { range } = options; + const from = range!.from.valueOf(); + const to = range!.to.valueOf(); + + const data = options.targets.map(target => { + // Your code goes here. + }); + + return { data }; + } + ``` + +1. In the `map` function, use the `lodash/defaults` package to set default values for query properties that haven't been set: + + ```ts + const query = defaults(target, defaultQuery); + ``` + +1. Create a data frame with a time field and a number field: + + ```ts + const frame = new MutableDataFrame({ + refId: query.refId, + fields: [ + { name: 'time', type: FieldType.time }, + { name: 'value', type: FieldType.number }, + ], + }); + ``` + + `refId` needs to be set to tell Grafana which query that generated this date frame. + +Next, we'll add the actual values to the data frame. Don't worry about the math used to calculate the values. + +1. Create a couple of helper variables: + + ```ts + // duration of the time range, in milliseconds. + const duration = to - from; + + // step determines how close in time (ms) the points will be to each other. + const step = duration / 1000; + ``` + +1. Add the values to the data frame: + + ```ts + for (let t = 0; t < duration; t += step) { + frame.add({ time: from + t, value: Math.sin((2 * Math.PI * t) / duration) }); + } + ``` + + The `frame.add()` accepts an object where the keys corresponds to the name of each field in the data frame. + +1. Return the data frame: + + ```ts + return frame; + ``` + +1. Rebuild the plugin and try it out. + +Your data source is now sending data frames that Grafana can visualize. Next, we'll look at how you can control the frequency of the sine wave by defining a _query_. + +> In this example, we're generating timestamps from the current time range. This means that you'll get the same graph no matter what time range you're using. In practice, you'd instead use the timestamps returned by your database. + +## Define a query + +Most data sources offer a way to query specific data. MySQL and PostgreSQL use SQL, while Prometheus has its own query language, called _PromQL_. No matter what query language your databases are using, Grafana lets you build support for it. + +Add support for custom queries to your data source, by implementing your own _query editor_, a React component that enables users to build their own queries, through a user-friendly graphical interface. + +A query editor can be as simple as a text field where the user edits the raw query text, or it can provide a more user-friendly form with drop-down menus and switches, that later gets converted into the raw query text before it gets sent off to the database. + +### Define the query model + +The first step in designing your query editor is to define its _query model_. The query model defines the user input to your data source. + +We want to be able to control the frequency of the sine wave, so let's add another property. + +1. Add a new number property called `frequency` to the query model: + + **src/types.ts** + + ```ts + export interface MyQuery extends DataQuery { + queryText?: string; + constant: number; + frequency: number; + } + ``` + +1. Set a default value to the new `frequency` property: + + ```ts + export const defaultQuery: Partial = { + constant: 6.5, + frequency: 1.0, + }; + ``` + +### Bind the model to a form + +Now that you've defined the query model you wish to support, the next step is to bind the model to a form. The `FormField` is a text field component from `grafana/ui` that lets you register a listener which will be invoked whenever the form field value changes. + +1. Add a new form field to the query editor to control the new frequency property. + + **QueryEditor.tsx** + + ```ts + const { queryText, constant, frequency } = query; + ``` + + ```ts + + ``` + +1. Add a event listener for the new property. + + ```ts + onFrequencyChange = (event: ChangeEvent) => { + const { onChange, query, onRunQuery } = this.props; + onChange({ ...query, frequency: parseFloat(event.target.value) }); + // executes the query + onRunQuery(); + }; + ``` + + The registered listener, `onFrequencyChange`, calls `onChange` to update the current query with the value from the form field. + + `onRunQuery();` tells Grafana to run the query after each change. For fast queries, this is recommended to provide a more responsive experience. + +### Use the property + +The new query model is now ready to use in our `query` method. + +1. In the `query` method, use the `frequency` property to adjust our equation. + + ```ts + frame.add({ time: from + t, value: Math.sin((2 * Math.PI * query.frequency * t) / duration) }); + ``` + +## Configure your data source + +To access a specific data source, you often need to configure things like hostname, credentials, or authentication method. A _config editor_ lets your users configure your data source plugin to fit their needs. + +The config editor looks similar to the query editor, in that it defines a model and binds it to a form. + +Since we're not actually connecting to an external database in our sine wave example, we don't really need many options. To show you how you can add an option however, we're going to add the _wave resolution_ as an option. + +The resolution controls how close in time the data points are to each other. A higher resolution means more points closer together, at the cost of more data being processed. + +### Define the options model + +1. Add a new number property called `resolution` to the options model. + + **types.ts** + + ```ts + export interface MyDataSourceOptions extends DataSourceJsonData { + path?: string; + resolution?: number; + } + ``` + +### Bind the model to a form + +Just like query editor, the form field in the config editor calls the registered listener whenever the value changes. + +1. Add a new form field to the query editor to control the new resolution option. + + **ConfigEditor.tsx** + + ```ts +
+ +
+ ``` + +1. Add a event listener for the new option. + + ```ts + onResolutionChange = (event: ChangeEvent) => { + const { onOptionsChange, options } = this.props; + const jsonData = { + ...options.jsonData, + resolution: parseFloat(event.target.value), + }; + onOptionsChange({ ...options, jsonData }); + }; + ``` + + The `onResolutionChange` listener calls `onOptionsChange` to update the current options with the value from the form field. + +### Use the option + +1. Create a property called `resolution` to the `DataSource` class. + + ```ts + export class DataSource extends DataSourceApi { + resolution: number; + + constructor(instanceSettings: DataSourceInstanceSettings) { + super(instanceSettings); + + this.resolution = instanceSettings.jsonData.resolution || 1000.0; + } + + // ... + ``` + +1. In the `query` method, use the `resolution` property to calculate the step size. + + **src/DataSource.ts** + + ```ts + const step = duration / this.resolution; + ``` + +## Get data from an external API + +So far, you've generated the data returned by the data source. A more realistic use case would be to fetch data from an external API. + +While you can use something like [axios](https://github.com/axios/axios) or the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to make requests, we recommend using the [`getBackendSrv`](/docs/grafana/latest/packages_api/runtime/getbackendsrv/) function from the [grafana/runtime](/docs/grafana/latest/packages_api/runtime/) package. + +The main advantage of `getBackendSrv` is that it proxies requests through the Grafana server rather making the request from the browser. This is strongly recommended when making authenticated requests to an external API. For more information on authenticating external requests, refer to [Add authentication for data source plugins](/docs/grafana/latest/developers/plugins/add-authentication-for-data-source-plugins/). + +1. Import `getBackendSrv`. + + **src/DataSource.ts** + + ```ts + import { getBackendSrv } from '@grafana/runtime'; + ``` + +1. Create a helper method `doRequest` and use the `datasourceRequest` method to make a request to your API. Replace `https://api.example.com/metrics` to point to your own API endpoint. + + ```ts + async doRequest(query: MyQuery) { + const result = await getBackendSrv().datasourceRequest({ + method: "GET", + url: "https://api.example.com/metrics", + params: query, + }) + + return result; + } + ``` + +1. Make a request for each query. `Promises.all` waits for all requests to finish before returning the data. + + ```ts + async query(options: DataQueryRequest): Promise { + const promises = options.targets.map((query) => + this.doRequest(query).then((response) => { + const frame = new MutableDataFrame({ + refId: query.refId, + fields: [ + { name: "Time", type: FieldType.time }, + { name: "Value", type: FieldType.number }, + ], + }); + + response.data.forEach((point: any) => { + frame.appendRow([point.time, point.value]); + }); + + return frame; + }) + ); + + return Promise.all(promises).then((data) => ({ data })); + } + ``` + +## Summary + +In this tutorial you built a complete data source plugin for Grafana that uses a query editor to control what data to visualize. You've added a data source option, commonly used to set connection options and more. + +### Learn more + +Learn how you can improve your plugin even further, by reading our advanced guides: + +- [Add support for variables](/docs/grafana/latest/developers/plugins/add-support-for-variables/) +- [Add support for annotations](/docs/grafana/latest/developers/plugins/add-support-for-annotations/) +- [Add support for Explore queries](/docs/grafana/latest/developers/plugins/add-support-for-explore-queries/) +- [Build a logs data source](/docs/grafana/latest/developers/plugins/build-a-logs-data-source-plugin/) diff --git a/docs/sources/tutorials/build-a-panel-plugin-with-d3/index.md b/docs/sources/tutorials/build-a-panel-plugin-with-d3/index.md new file mode 100644 index 00000000000..3638df56e06 --- /dev/null +++ b/docs/sources/tutorials/build-a-panel-plugin-with-d3/index.md @@ -0,0 +1,235 @@ +--- +title: Build a panel plugin with D3.js +summary: Learn how to use D3.js in your panel plugins. +description: how to use D3.js in your panel plugins. +id: build-a-panel-plugin-with-d3 +categories: ['plugins'] +tags: ['beginner'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +weight: 60 +--- + +## Introduction + +Panels are the building blocks of Grafana, and allow you to visualize data in different ways. This tutorial gives you a hands-on walkthrough of creating your own panel using [D3.js](https://d3js.org/). + +For more information about panels, refer to the documentation on [Panels](/docs/grafana/latest/features/panels/panels/). + +In this tutorial, you'll: + +- Build a simple panel plugin to visualize a bar chart. +- Learn how to use D3.js to build a panel using data-driven transformations. + +{{% class "prerequisite-section" %}} + +### Prerequisites + +- Grafana 7.0 +- NodeJS 12.x +- yarn + {{% /class %}} + +## Set up your environment + +{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" version="latest" >}} + +## Create a new plugin + +{{< docs/shared lookup="tutorials/create-plugin.md" source="grafana" version="latest" >}} + +## Data-driven documents + +[D3.js](https://d3js.org/) is a JavaScript library for manipulating documents based on data. It lets you transform arbitrary data into HTML, and is commonly used for creating visualizations. + +Wait a minute. Manipulating documents based on data? That's sounds an awful lot like React. In fact, much of what you can accomplish with D3 you can already do with React. So before we start looking at D3, let's see how you can create an SVG from data, using only React. + +In **SimplePanel.tsx**, change `SimplePanel` to return an `svg` with a `rect` element. + +```ts +export const SimplePanel: React.FC = ({ options, data, width, height }) => { + const theme = useTheme(); + + return ( + + + + ); +}; +``` + +One single rectangle might not be very exciting, so let's see how you can create rectangles from data. + +1. Create some data that we can visualize. + + ```ts + const values = [4, 8, 15, 16, 23, 42]; + ``` + +1. Calculate the height of each bar based on the height of the panel. + + ```ts + const barHeight = height / values.length; + ``` + +1. Inside a SVG group, `g`, create a `rect` element for every value in the dataset. Each rectangle uses the value as its width. + + ```ts + return ( + + + {values.map((value, i) => ( + + ))} + + + ); + ``` + +1. Rebuild the plugin and reload your browser to see the changes you've made. + +As you can see, React is perfectly capable of dynamically creating HTML elements. In fact, creating elements using React is often faster than creating them using D3. + +So why would you use even use D3? In the next step, we'll see how you can take advantage of D3's data transformations. + +## Transform data using D3.js + +In this step, you'll see how you can transform data using D3 before rendering it using React. + +D3 is already bundled with Grafana, and you can access it by importing the `d3` package. However, we're going to need the type definitions while developing. + +1. Install the D3 type definitions: + + ```bash + yarn add --dev @types/d3 + ``` + +1. Import `d3` in **SimplePanel.tsx**. + + ```ts + import * as d3 from 'd3'; + ``` + +In the previous step, we had to define the width of each bar in pixels. Instead, let's use _scales_ from the D3 library to make the width of each bar depend on the width of the panel. + +Scales are functions that map a range of values to another range of values. In this case, we want to map the values in our datasets to a position within our panel. + +1. Create a scale to map a value between 0 and the maximum value in the dataset, to a value between 0 and the width of the panel. We'll be using this to calculate the width of the bar. + + ```ts + const scale = d3 + .scaleLinear() + .domain([0, d3.max(values) || 0.0]) + .range([0, width]); + ``` + +1. Pass the value to the scale function to calculate the width of the bar in pixels. + + ```ts + return ( + + + {values.map((value, i) => ( + + ))} + + + ); + ``` + +As you can see, even if we're using React to render the actual elements, the D3 library contains useful tools that you can use to transform your data before rendering it. + +## Add an axis + +Another useful tool in the D3 toolbox is the ability to generate _axes_. Adding axes to our chart makes it easier for the user to understand the differences between each bar. + +Let's see how you can use D3 to add a horizontal axis to your bar chart. + +1. Create a D3 axis. Notice that by using the same scale as before, we make sure that the bar width aligns with the ticks on the axis. + + ```ts + const axis = d3.axisBottom(scale); + ``` + +1. Generate the axis. While D3 needs to generate the elements for the axis, we can encapsulate it by generating them within an anonymous function which we pass as a `ref` to a group element `g`. + + ```ts + { + d3.select(node).call(axis as any); + }} + /> + ``` + +By default, the axis renders at the top of the SVG element. We'd like to move it to the bottom, but to do that, we first need to make room for it by decreasing the height of each bar. + +1. Calculate the new bar height based on the padded height. + + ```ts + const padding = 20; + const chartHeight = height - padding; + const barHeight = chartHeight / values.length; + ``` + +1. Translate the axis by adding a transform to the `g` element. + + ```ts + { + d3.select(node).call(axis as any); + }} + /> + ``` + +Congrats! You've created a simple and responsive bar chart. + +## Complete example + +```ts +import React from 'react'; +import { PanelProps } from '@grafana/data'; +import { SimpleOptions } from 'types'; +import { useTheme } from '@grafana/ui'; +import * as d3 from 'd3'; + +interface Props extends PanelProps {} + +export const SimplePanel: React.FC = ({ options, data, width, height }) => { + const theme = useTheme(); + + const values = [4, 8, 15, 16, 23, 42]; + + const scale = d3 + .scaleLinear() + .domain([0, d3.max(values) || 0.0]) + .range([0, width]); + + const axis = d3.axisBottom(scale); + + const padding = 20; + const chartHeight = height - padding; + const barHeight = chartHeight / values.length; + + return ( + + + {values.map((value, i) => ( + + ))} + + { + d3.select(node).call(axis as any); + }} + /> + + ); +}; +``` + +## Summary + +In this tutorial you built a panel plugin with D3.js. diff --git a/docs/sources/tutorials/build-a-panel-plugin/index.md b/docs/sources/tutorials/build-a-panel-plugin/index.md new file mode 100644 index 00000000000..4f5f101d000 --- /dev/null +++ b/docs/sources/tutorials/build-a-panel-plugin/index.md @@ -0,0 +1,259 @@ +--- +title: Build a panel plugin +summary: Learn how to create a custom visualization for your dashboards. +description: Learn how to create a custom visualization for your dashboards. +id: build-a-panel-plugin +categories: ['plugins'] +tags: ['beginner'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +weight: 50 +--- + +## Introduction + +Panels are the building blocks of Grafana. They allow you to visualize data in different ways. While Grafana has several types of panels already built-in, you can also build your own panel, to add support for other visualizations. + +For more information about panels, refer to the documentation on [Panels](/docs/grafana/latest/panels/). + +{{% class "prerequisite-section" %}} + +### Prerequisites + +- Grafana >=7.0 +- NodeJS >=14 +- yarn + {{% /class %}} + +## Set up your environment + +{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" version="latest" >}} + +## Create a new plugin + +{{< docs/shared lookup="tutorials/create-plugin.md" source="grafana" version="latest" >}} + +## Anatomy of a plugin + +{{< docs/shared lookup="tutorials/plugin-anatomy.md" source="grafana" version="latest" >}} + +## Panel plugins + +Since Grafana 6.x, panels are [ReactJS components](https://reactjs.org/docs/components-and-props.html). + +Prior to Grafana 6.0, plugins were written in [AngularJS](https://angular.io/). Even though we still support plugins written in AngularJS, we highly recommend that you write new plugins using ReactJS. + +### Panel properties + +The [PanelProps](https://github.com/grafana/grafana/blob/747b546c260f9a448e2cb56319f796d0301f4bb9/packages/grafana-data/src/types/panel.ts#L27-L40) interface exposes runtime information about the panel, such as panel dimensions, and the current time range. + +You can access the panel properties through `props`, as seen in your plugin. + +**src/SimplePanel.tsx** + +```js +const { options, data, width, height } = props; +``` + +### Development workflow + +Next, you'll learn the basic workflow of making a change to your panel, building it, and reloading Grafana to reflect the changes you made. + +First, you need to add your panel to a dashboard: + +1. Open Grafana in your browser. +1. Create a new dashboard, and add a new panel. +1. Select your panel from the list of visualization types. +1. Save the dashboard. + +Now that you can view your panel, try making a change to the panel plugin: + +1. In `SimplePanel.tsx`, change the fill color of the circle. +1. Run `yarn dev` to build the plugin. +1. In the browser, reload Grafana with the new changes. + +## Add panel options + +Sometimes you want to offer the users of your panel an option to configure the behavior of your plugin. By configuring _panel options_ for your plugin, your panel will be able to accept user input. + +In the previous step, you changed the fill color of the circle in the code. Let's change the code so that the plugin user can configure the color from the panel editor. + +#### Add an option + +Panel options are defined in a _panel options object_. `SimpleOptions` is an interface that describes the options object. + +1. In `types.ts`, add a `CircleColor` type to hold the colors the users can choose from: + + ``` + type CircleColor = 'red' | 'green' | 'blue'; + ``` + +1. In the `SimpleOptions` interface, add a new option called `color`: + + ``` + color: CircleColor; + ``` + +Here's the updated options definition: + +**src/types.ts** + +```ts +type SeriesSize = 'sm' | 'md' | 'lg'; +type CircleColor = 'red' | 'green' | 'blue'; + +// interface defining panel options type +export interface SimpleOptions { + text: string; + showSeriesCount: boolean; + seriesCountSize: SeriesSize; + color: CircleColor; +} +``` + +#### Add an option control + +To change the option from the panel editor, you need to bind the `color` option to an _option control_. + +Grafana supports a range of option controls, such as text inputs, switches, and radio groups. + +Let's create a radio control and bind it to the `color` option. + +1. In `src/module.ts`, add the control at the end of the builder: + + ```ts + .addRadio({ + path: 'color', + name: 'Circle color', + defaultValue: 'red', + settings: { + options: [ + { + value: 'red', + label: 'Red', + }, + { + value: 'green', + label: 'Green', + }, + { + value: 'blue', + label: 'Blue', + }, + ], + } + }); + ``` + + The `path` is used to bind the control to an option. You can bind a control to nested option by specifying the full path within a options object, for example `colors.background`. + +Grafana builds an options editor for you and displays it in the panel editor sidebar in the **Display** section. + +#### Use the new option + +You're almost done. You've added a new option and a corresponding control to change the value. But the plugin isn't using the option yet. Let's change that. + +1. To convert option value to the colors used by the current theme, add a `switch` statement right before the `return` statement in `SimplePanel.tsx`. + + **src/SimplePanel.tsx** + + ```ts + let color: string; + switch (options.color) { + case 'red': + color = theme.palette.redBase; + break; + case 'green': + color = theme.palette.greenBase; + break; + case 'blue': + color = theme.palette.blue95; + break; + } + ``` + +1. Configure the circle to use the color. + + ```ts + + + + ``` + +Now, when you change the color in the panel editor, the fill color of the circle changes as well. + +## Create dynamic panels using data frames + +Most panels visualize dynamic data from a Grafana data source. In this step, you'll create one circle per series, each with a radius equal to the last value in the series. + +> To use data from queries in your panel, you need to set up a data source. If you don't have one available, you can use the [TestData DB](/docs/grafana/latest/features/datasources/testdata) data source while developing. + +The results from a data source query within your panel are available in the `data` property inside your panel component. + +```ts +const { data } = props; +``` + +`data.series` contains the series returned from a data source query. Each series is represented as a data structure called _data frame_. A data frame resembles a table, where data is stored by columns, or _fields_, instead of rows. Every value in a field share the same data type, such as string, number, or time. + +Here's an example of a data frame with a time field, `Time`, and a number field, `Value`: + +| Time | Value | +| ------------- | ----- | +| 1589189388597 | 32.4 | +| 1589189406480 | 27.2 | +| 1589189513721 | 15.0 | + +Let's see how you can retrieve data from a data frame and use it in your visualization. + +1. Get the last value of each field of type `number`, by adding the following to `SimplePanel.tsx`, before the `return` statement: + + ```ts + const radii = data.series + .map((series) => series.fields.find((field) => field.type === 'number')) + .map((field) => field?.values.get(field.values.length - 1)); + ``` + + `radii` will contain the last values in each of the series that are returned from a data source query. You'll use these to set the radius for each circle. + +1. Change the `svg` element to the following: + + ```ts + + + {radii.map((radius, index) => { + const step = width / radii.length; + return ; + })} + + + ``` + + Note how we're creating a `` element for each value in `radii`: + + ```ts + { + radii.map((radius, index) => { + const step = width / radii.length; + return ; + }); + } + ``` + + We use the `transform` here to distribute the circle horizontally within the panel. + +1. Rebuild your plugin and try it out by adding multiple queries to the panel. Refresh the dashboard. + +If you want to know more about data frames, check out our introduction to [Data frames](/docs/grafana/latest/developers/plugins/data-frames/). + +## Summary + +In this tutorial you learned how to create a custom visualization for your dashboards. diff --git a/docs/sources/tutorials/build-a-streaming-data-source-plugin/index.md b/docs/sources/tutorials/build-a-streaming-data-source-plugin/index.md new file mode 100644 index 00000000000..7978f6e3629 --- /dev/null +++ b/docs/sources/tutorials/build-a-streaming-data-source-plugin/index.md @@ -0,0 +1,164 @@ +--- +title: Build a streaming data source backend plugin +summary: Create a backend for your data source plugin with streaming capabilities. +description: Create a backend for your data source plugin with streaming capabilities. +id: build-a-streaming-data-source-backend-plugin +categories: ['plugins'] +tags: ['beginner'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +weight: 75 +--- + +## Introduction + +Grafana supports a wide range of data sources, including Prometheus, MySQL, and even Datadog. In previous tutorials we have shown how to extend Grafana capabilities to query custom data sources by [building a backend datasource plugin](/tutorials/build-a-data-source-backend-plugin/). In this tutorial we take a step further and add streaming capabilities to the backend datasource plugin. Streaming allows plugins to push data to Grafana panels as soon as data appears (without periodic polling from UI side). + +For more information about backend plugins, refer to the documentation on [Backend plugins](/docs/grafana/latest/developers/plugins/backend/). + +In this tutorial, you'll: + +- Extend a backend plugin with streaming capabilities + +{{% class "prerequisite-section" %}} + +#### Prerequisites + +- Knowledge about how data sources are implemented in the frontend. +- Knowledge about [backend datasource anatomy](/tutorials/build-a-data-source-backend-plugin/) +- Grafana 8.0+ +- Go ([Version](https://github.com/grafana/plugin-tools/blob/main/packages/create-plugin/templates/backend/go.mod#L3)) +- [Mage](https://magefile.org/) +- NodeJS ([Version](https://github.com/grafana/plugin-tools/blob/main/packages/create-plugin/templates/common/package.json#L66)) +- yarn + {{% /class %}} + +## Set up your environment + +{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" version="latest" >}} + +## Create a new plugin + +To build a backend for your data source plugin, Grafana requires a binary that it can execute when it loads the plugin during start-up. In this guide, we will build a binary using the [Grafana plugin SDK for Go](/docs/grafana/latest/developers/plugins/backend/grafana-plugin-sdk-for-go/). + +The easiest way to get started is to use the Grafana [create-plugin tool](https://www.npmjs.com/package/@grafana/create-plugin). Navigate to the plugin folder that you configured in step 1 and type: + +``` +npx @grafana/create-plugin +``` + +Follow the steps and select **datasource** as your plugin type and answer **yes** when prompted to create a backend for your plugin. + +```bash +cd my-plugin +``` + +Install frontend dependencies and build frontend parts of the plugin to _dist_ directory: + +```bash +yarn install +yarn build +``` + +Run the following to update [Grafana plugin SDK for Go](/docs/grafana/latest/developers/plugins/backend/grafana-plugin-sdk-for-go/) dependency to the latest minor version: + +```bash +go get -u github.com/grafana/grafana-plugin-sdk-go +go mod tidy +``` + +Build backend plugin binaries for Linux, Windows and Darwin to _dist_ directory: + +```bash +mage -v +``` + +Now, let's verify that the plugin you've built can be used in Grafana when creating a new data source: + +1. Restart your Grafana instance. +1. Open Grafana in your web browser. +1. Navigate via the side-menu to **Configuration** -> **Data Sources**. +1. Click **Add data source**. +1. Find your newly created plugin and select it. +1. Enter a name and then click **Save & Test** (ignore any errors reported for now). + +You now have a new data source instance of your plugin that is ready to use in a dashboard. To confirm, follow these steps: + +1. Navigate via the side-menu to **Create** -> **Dashboard**. +1. Click **Add new panel**. +1. In the query tab, select the data source you just created. +1. A line graph is rendered with one series consisting of two data points. +1. Save the dashboard. + +### Troubleshooting + +#### Grafana doesn't load my plugin + +By default, Grafana requires backend plugins to be signed. To load unsigned backend plugins, you need to +configure Grafana to [allow unsigned plugins](/docs/grafana/latest/plugins/plugin-signature-verification/#allow-unsigned-plugins). +For more information, refer to [Plugin signature verification](/docs/grafana/latest/plugins/plugin-signature-verification/#backend-plugins). + +## Anatomy of a backend plugin + +As you may notice till this moment we did the same steps described in [build a backend datasource plugin tutorial](/tutorials/build-a-data-source-backend-plugin/). At this point, you should be familiar with backend plugin structure and a way how data querying and health check capabilities could be implemented. Let's take the next step and discuss how datasource plugin can handle data streaming. + +## Add streaming capabilities + +What we want to achieve here is to issue a query to load initial data from a datasource plugin and then switching to data streaming mode where the plugin will push data frames to Grafana time-series panel. + +In short – implementing a streaming plugin means implementing a `backend.StreamHandler` interface which contains `SubscribeStream`, `RunStream`, and `PublishStream` methods. + +`SubscribeStream` is a method where the plugin has a chance to authorize user subscription requests to a channel. Users on the frontend side subscribe to different channels to consume real-time data. + +When returning a `data.Frame` with initial data we can return a special field `Channel` to let the frontend know that we are going to stream data frames after initial data load. When the frontend receives a frame with a `Channel` set it automatically issues a subscription request to that channel. + +Channel is a string identifier of topic to which clients can subscribe in Grafana Live. See a documentation of Grafana Live for [details about channel structure](/docs/grafana/latest/live/live-channel/). + +As said in docs in Grafana Live channel consists of 3 parts delimited by `/`: + +- Scope +- Namespace +- Path + +For datasource plugin channels Grafana uses `ds` scope. Namespace in the case of datasource channels is a datasource unique ID (UID) which is issued by Grafana at the moment of datasource creation. The path is a custom string that plugin authors free to choose themselves (just make sure it consists of allowed symbols). I.e. datasource channel looks like `ds//`. + +So to let the frontend know that we are going to stream data we set a `Channel` field into frame metadata inside `QueryData` implementation. In our tutorial it's a `ds//stream`. The frontend will issue a subscription request to this channel. + +Inside `SubscribeStream` implementation we check whether a user allowed to subscribe on a channel path. If yes – we return an OK status code to tell Grafana user can join a channel: + +```go +status := backend.SubscribeStreamStatusPermissionDenied +if req.Path == "stream" { + // Allow subscribing only on expected path. + status = backend.SubscribeStreamStatusOK +} +return &backend.SubscribeStreamResponse{ + Status: status, +}, nil +``` + +As soon as the first subscriber joins a channel Grafana opens a unidirectional stream to consume streaming frames from a plugin. To handle this and to push data towards clients we implement a `RunStream` method which provides a way to push JSON data into a channel. So we can push data frame like this (error handling skipped): + +```go +// Send frame to stream including both frame schema and data frame parts. +_ = sender.SendFrame(frame, data.IncludeAll) +``` + +Open example datasource query editor and make sure `With Streaming` toggle is on. After doing this you should see data displayed and then periodically updated by streaming frames coming periodically from `RunStream` method. + +The important thing to note is that Grafana opens a unidirectional stream only once per channel upon the first subscriber joined. Every other subscription request will be still authorized by `SubscribeStream` method but the new `RunStream` won't be issued. I.e. you can have many active subscribers but only one running stream. At this moment this guarantee works for a single Grafana instance, we are planning to support this for highly-available Grafana setup (many Grafana instances behind load-balancer) in future releases. + +The stream will be automatically closed as soon as all subscriber users left. + +For the tutorial use case, we only need to properly implement `SubscribeStream` and `RunStream` - we don't need to handle publications to a channel from users. But we still need to write `PublishStream` method to fully implement `backend.StreamHandler` interface. Inside `PublishStream` we just do not allow any publications from users since we are pushing data from a backend: + +```go +return &backend.PublishStreamResponse{ + Status: backend.PublishStreamStatusPermissionDenied, +}, nil +``` + +## Summary + +In this tutorial you created a backend for your data source plugin with streaming capabilities. diff --git a/docs/sources/tutorials/build-an-app-plugin/index.md b/docs/sources/tutorials/build-an-app-plugin/index.md new file mode 100644 index 00000000000..0649b0fd596 --- /dev/null +++ b/docs/sources/tutorials/build-an-app-plugin/index.md @@ -0,0 +1,208 @@ +--- +title: Build an app plugin +summary: Learn at how to create an app for Grafana. +description: Learn at how to create an app for Grafana. +id: build-an-app-plugin +categories: ['plugins'] +tags: ['beginner'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +weight: 50 +draft: true +--- + +## Introduction + +App plugins are Grafana plugins that can bundle data source and panel plugins within one package. They also let you create _custom pages_ within Grafana. Custom pages enable the plugin author to include things like documentation, sign-up forms, or to control other services over HTTP. + +Data source and panel plugins will show up like normal plugins. The app pages will be available in the main menu. + +{{% class "prerequisite-section" %}} + +### Prerequisites + +- Grafana 7.0 +- NodeJS 12.x +- yarn + {{% /class %}} + +## Set up your environment + +{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" version="latest" >}} + +## Create a new plugin + +{{< docs/shared lookup="tutorials/create-plugin.md" source="grafana" version="latest" >}} + +## Anatomy of a plugin + +{{< docs/shared lookup="tutorials/plugin-anatomy.md" source="grafana" version="latest" >}} + +## App plugins + +App plugins let you bundle resources such as dashboards, panels, and data sources into a single plugin. + +Any resource you want to include needs to be added to the `includes` property in the `plugin.json` file. To add a resource to your app plugin, you need to include it to the `plugin.json`. + +Plugins that are included in an app plugin are available like any other plugin. + +Dashboards and pages can be added to the app menu by setting `addToNav` to `true`. + +By setting `"defaultNav": true`, users can navigate to the dashboard by clicking the app icon in the side menu. + +## Add a custom page + +App plugins let you extend the Grafana user interface through the use of _custom pages_. + +Any requests sent to `/a/`, e.g. `/a/myorgid-simple-app/`, are routed to the _root page_ of the app plugin. The root page is a React component that returns the content for a given route. + +While you're free to implement your own routing, in this tutorial you'll use a tab-based navigation page that you can use by calling `onNavChange`. + +Let's add a tab for managing server instances. + +1. In the `src/pages` directory, add a new file called `Instances.tsx`. This component contains the content for the new tab. + + ```ts + import { AppRootProps } from '@grafana/data'; + import React, { FC } from 'react'; + + export const Instances: FC = ({ query, path, meta }) => { + return

Hello

; + }; + ``` + +1. Register the page by adding it to the `pages` array in `src/pages/index.ts`. + + **index.ts** + + ```ts + import { Instances } from './Instances'; + ``` + + ```ts + { + component: Instances, + icon: 'file-alt', + id: 'instances', + text: 'Instances', + } + ``` + +1. Add the page to the app menu, by including it in `plugin.json`. This will be the main view of the app, so we'll set `defaultNav` to let users quickly get to it by clicking the app icon in the side menu. + + **plugin.json** + + ```json + "includes": [ + { + "type": "page", + "name": "Instances", + "path": "/a/myorgid-simple-app?tab=instances", + "role": "Viewer", + "addToNav": true, + "defaultNav": true + } + ] + ``` + +> **Note:** While `page` includes typically reference pages created by the app, you can set `path` to any URL, internal or external. Try setting `path` to `https://grafana.com`. + +## Configure the app + +Let's add a new configuration page where users are able to configure default zone and regions for any instances they create. + +1. In `module.ts`, add new configuration page using the `addConfigPage` method. `body` is the React component that renders the page content. + + **module.ts** + + ```ts + .addConfigPage({ + title: 'Defaults', + icon: 'fa fa-info', + body: DefaultsConfigPage, + id: 'defaults', + }) + ``` + +## Add a dashboard + +#### Include a dashboard in your app + +1. In `src/`, create a new directory called `dashboards`. +1. Create a file called `overview.json` in the `dashboards` directory. +1. Copy the JSON definition for the dashboard you want to include and paste it into `overview.json`. If you don't have one available, you can find a sample dashboard at the end of this step. +1. In `plugin.json`, add the following object to the `includes` property. + + - The `name` of the dashboard needs to be the same as the `title` in the dashboard JSON model. + - `path` points out the file that contains the dashboard definition, relative to the `plugin.json` file. + + ```json + "includes": [ + { + "type": "dashboard", + "name": "System overview", + "path": "dashboards/overview.json", + "addToNav": true + } + ] + ``` + +1. Save and restart Grafana to load the new changes. + +## Bundle a plugin + +An app plugin can contain panel and data source plugins that get installed along with the app plugin. + +In this step, you'll add a data source to your app plugin. You can add panel plugins the same way by changing `datasource` to `panel`. + +1. In `src/`, create a new directory called `datasources`. +1. Create a new data source using Grafana create-plugin tool in a temporary directory. + + ```bash + mkdir tmp + cd tmp + npx @grafana/create-plugin + ``` + +1. Move the `src` directory in the data source plugin to `src/datasources`, and rename it to `my-datasource`. + + ```bash + mv ./my-datasource/src ../src/datasources/my-datasource + ``` + +Any bundled plugins are built along with the app plugin. Grafana looks for any subdirectory containing a `plugin.json` file and attempts to load a plugin in that directory. + +To let users know that your plugin bundles other plugins, you can optionally display it on the plugin configuration page. This is not done automatically, so you need to add it to the `plugin.json`. + +1. Include the data source in the `plugin.json`. The `name` property is only used for displaying in the Grafana UI. + + ```json + "includes": [ + { + "type": "datasource", + "name": "My data source" + } + ] + ``` + +#### Include external plugins + +If you want to let users know that your app requires an existing plugin, you can add it as a dependency in `plugin.json`. Note that they'll still need to install it themselves. + +```json +"dependencies": { + "plugins": [ + { + "type": "panel", + "name": "Worldmap Panel", + "id": "grafana-worldmap-panel", + "version": "^0.3.2" + } + ] +} +``` + +## Summary + +In this tutorial you learned how to create an app plugin. diff --git a/docs/sources/tutorials/create-alerts-from-flux-queries/index.md b/docs/sources/tutorials/create-alerts-from-flux-queries/index.md new file mode 100644 index 00000000000..405b4f861cd --- /dev/null +++ b/docs/sources/tutorials/create-alerts-from-flux-queries/index.md @@ -0,0 +1,331 @@ +--- +title: How to create Grafana alerts with InfluxDB and the Flux query language +summary: Create complex alerts from Flux queries in the new Grafana Alerting +description: Create complex alerts from Flux queries in the new Grafana Alerting +id: grafana-alerts-flux-queries +categories: ['alerting'] +tags: ['advanced'] +status: published +authors: ['grant_pinkos'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +weight: 70 +--- + +# How to create Grafana alerts with InfluxDB and the Flux query language + +[Grafana Alerting](/docs/grafana/latest/alerting/) represents a powerful new approach to systems observability and incident response management. While the alerting platform is perhaps best known for its strong integrations with Prometheus, the system works with numerous popular data sources including InfluxDB. In this tutorial we will learn how to create Grafana alerts using InfluxDB and the newer Flux query language. We will cover five common scenarios from the most basic to the most complex. Together, these five scenarios will provide an excellent guide for almost any type of alerting query that you wish to create using Grafana and Flux. + +Before we dive into our alerting scenarios, it is worth considering the development of InfluxDB's two popular query languages: InfluxQL and Flux. Originally, InfluxDB used [InfluxQL](https://docs.influxdata.com/influxdb/v2.5/reference/syntax/influxql/spec/) as their query language, which uses a SQL-like syntax. But beginning with InfluxDB v1.8, the company introduced [Flux](https://docs.influxdata.com/flux/v0.x/), "an open source functional data scripting language designed for querying, analyzing, and acting on data." "Flux," its official documentation goes on to state, "unifies code for querying, processing, writing, and acting on data into a single syntax. The language is designed to be usable, readable, flexible, composable, testable, contributable, and shareable." + +In the following five examples we will see just how powerful and flexible the new Flux query language can be. We will also see just how well Flux pairs with Grafana Alerting. + +## Example 1: Create an alert when a value is above or below a set threshold + +Our first example uses a common real-world scenario for InfluxDB and Grafana Alerting. Popular with IoT and edge applications, InfluxDB excels at on-site, real-time observability. In this example, and in fact for many of the following examples, we will consider the hypothetical scenario where we are monitoring a number of fluid tanks in a manufacturing plant. This scenario, [based on an actual application of InfluxDB and Alerting](/go/grafanaconline/2021/plant-efficiency-grafana-cloud/), will allow us to work through Grafana's various alerting setups, progressing from the simplest to the most complex. + +For Example 1, let's consider the following scenario: we are monitoring one tank, `A5`, for which we are storing real-time temperature data. We need to make sure that the temperature in this tank is always greater than 30 °C and less than 60 °C. + +We want to write a Grafana alert that will trigger whenever the temperature in tank `A5` crosses the lower threshold of 30 °C or the upper threshold of 60 °C. + +To do this, we'll: 1. create a Grafana alert rule. 1. add a Flux query. 1. add expressions to the alert rule. + +### Create a Grafana Alert rule + +1. Open the Grafana alerting menu and select **Alert rules**. +1. Click **New alert rule**. +1. Give your alert rule a name and then select **Grafana managed alert**. + For InfluxDB, you will always create a [Grafana managed rule](/docs/grafana/latest/alerting/alerting-rules/create-grafana-managed-rule/#add-grafana-managed-rule). + +### Add an initial Flux query to the alert rule + +Still in the **Step 2** section of the Alert rule page, you will see three boxes: a query editor (`A`), and then two sections labelled `B` and `C`. You will use these three sections to construct your rule. Let's move through them one by one. + +First, we want to query the data in our imaginary InfluxDB instance to obtain a time series graph of the temperature of tank A5. For this you would choose your InfluxDB data source from the dropdown and then write a query like this: + + ``` + from(bucket: "RetroEncabulator") + |> range(start: v.timeRangeStart, stop: v.timeRangeStop) + |> filter(fn: (r) => r["_measurement"] == "TemperatureData") + |> filter(fn: (r) => r["Tank"] == "A5") + |> filter(fn: (r) => r["_field"] == "Temperature") + |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) + |> yield(name: "mean") + ``` + +This is a fairly typical Flux query. Let's go through it function by function. We begin using [the `from()` function](https://docs.influxdata.com/flux/v0.x/stdlib/influxdata/influxdb/from/) to choose the correct bucket where our tank data resides. Then we use [a `range()` function](https://docs.influxdata.com/flux/v0.x/stdlib/universe/range/) to filter our rows based on time constraints. Then we pass our data through three [`filter()` functions](https://docs.influxdata.com/flux/v0.x/stdlib/universe/filter/) to narrow our results. We choose a specific [`measurement` (a special keyword in InfluxDB)](https://docs.influxdata.com/influxdb/v1.8/concepts/glossary/#measurement), then our tank in question (`A5`), and then a specific [`field` (another special keyword in InfluxDB)](https://docs.influxdata.com/influxdb/v1.8/concepts/glossary/#field). After this we pass the data into [an `aggregateWindow()` function](https://docs.influxdata.com/flux/v0.x/stdlib/universe/aggregatewindow/), which downsamples our data into specific periods of time, and then finally [a `yield()` function](https://docs.influxdata.com/flux/v0.x/stdlib/universe/yield/), which specifies which final result we want: `mean`. + +This Flux query will yield a time-series graph like this: + +![grafana alerts from flux queries](/media/tutorials/screenshot-flux-timeseries-graph.png) + +### Add expressions to your Grafana Alert rule + +With data now appearing in our rule setup, our next step is to create an [expression](/docs/grafana/v9.0/panels/query-a-data-source/use-expressions-to-manipulate-data/about-expressions/#using-expressions). Move to section `B`. For this scenario, we want to create a Reduce expression that will reduce the above to a single value. In this image, you can see that we have chosen to reduce our time-series data the `Last` value from input `A`. In this case, it returns a value 53 degrees celsius for Tank A5: + +![grafana alerts from flux queries](/media/tutorials/screenshot-flux-reduce-expression.png) + +Finally, we need to create a math expression that Grafana will alert on. In our case we will write an expression with two conditions separated by the OR `||` operator. We want to trigger an alert any time our result in section `B` is less than 30 or more than 60. This looks like `$B < 30 || $B > 60`: + +![grafana alerts from flux queries](/media/tutorials/screenshot-flux-math-expression.png) + +Set the alert condition to `C - expression`. We can now preview our alert. Here is a preview of this alert when the state is `Normal`: + +![grafana alerts from flux queries](/media/tutorials/screenshot-flux-alert-preview-state-normal.png) + +And here is a preview of this alert when the state is `Alerting`: + +![grafana alerts from flux queries](/media/tutorials/screenshot-flux-alert-alert-preview-state-alerting.png) + +Note that the Reduce expression above is needed. Without it, when previewing the results, Grafana would display `invalid format of evaluation results for the alert definition B: looks like time series data, only reduced data can be alerted on`. + +💡Tip: In case your locale is still stubbornly using Fahrenheit, we can modify the above Flux query by adding (before the aggregateWindow statement) a map() function to to convert (or map) the values from °C to °F. Note that we are not creating a new field. We are simply remapping the existing value. + +```flux +|> map(fn: (r) => ({r with _value: r._value * 1.8 + 32.0})) +``` + +### Conclusion + +Using these three steps you can create a Flux-based Grafana Alert that will trigger on either of two thresholds from a single data source. But what if you need to trigger an alert based on **multiple conditions and from multiple time-series**? In example two we will cover this very scenario. + +## Example 2: how to create a Grafana alert from two queries and two conditions + +Let's mix things up a bit for example two and leave our imaginary manufacturing plant. Imagine you're an assistant to the great Dr. Emmett Brown from Back to the Future, and Doc has tasked you with the following challenge: "I want an alert sent to me every time both conditions for time travel are met: when the velocity of a vehicle reaches 88 miles per hour and an object generates 1.21 jigowatts of electricity." + +Let's assume we are tracking this data in InfluxDB and Grafana. Let's also assume that each of the above data sources comes from different buckets. How do we alert on this? How do we use Grafana and Flux to alert on two distinct conditions originating from two distinct data sources? + +### Add two Flux queries to your Grafana Alert rule + +Like we did in example 1, let's first mock up our queries. Our query for our vehicle data is very similar to our last query. We use a `from()`, `range()`, and a sequence of `filter()` functions. We then use `AggregateWindow()` and `yield()` to narrow our data even more. In this case, the result is a time series tracking the velocity of our 1983 DeLorean: + +```flux +from(bucket: "vehicles") +|> range(start: v.timeRangeStart, stop: v.timeRangeStop) +|> filter(fn: (r) => r["_measurement"] == "VehicleData") +|> filter(fn: (r) => r["VehicleType"] == "DeLorean") +|> filter(fn: (r) => r["VehicleYear"] == "1983") +|> filter(fn: (r) => r["_field"] == "velocity") +|> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) +|> yield(name: "mean") +``` + +Our second query will trigger an alert whenever our electricity resource (the lightning strike on the Hill Valley clocktower) reaches the needed 1.21 jigowatts. A query like this would look very similar to our vehicle velocity query: + +```flux +from(bucket: "HillValley") +|> range(start: v.timeRangeStart, stop: v.timeRangeStop) +|> filter(fn: (r) => r["_measurement"] == "ElectricityData") +|> filter(fn: (r) => r["Location"] == "clocktower") +|> filter(fn: (r) => r["Source"] == "lightning") +|> filter(fn: (r) => r["_field"] == "power") +|> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) +|> yield(name: "mean") +``` + +We are now ready to modify this data using expressions. + +### Add expressions to your Grafana Alert rule + +1. Let's now use the same steps to reduce each query to the last (most recent) value. Reducing Query `A` to a single value might look like this: + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-additional-queries-reduce-expression-A.png) + +1. And here we are reducing query `B`: + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-additional-queries-reduce-expression-B.png) + +1. Now, in section `C` we need to create a math expression to be alerted on. In this case we will use the AND `&&` operator to specify that two conditions must be met: the value of `C` (the reduced value from query `A`) must be greater than 88.0 while the value of `D` (the reduced value from query `B`) must be greater than 1.21. We write this as `$C > 88.0 && $D > 1.21` + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-additional-queries-math-expression.png) + +And here is a preview of our alerts: + +![grafana alerts from flux queries](https://raw.githubusercontent.com/grafana/tutorials/master/content/tutorials/assets/flux-additional-queries-alert-preview.png) + +💡Tip: If your data in InfluxDB happens to have an unnecessarily large number of digits to the right of the decimal (such as 1.2104705741732575 shown above), and you want your Grafana alerts to be more legible, try using {{ printf "%.2f" $values.D.Value }}. For example, in the annotation Summary, we could write the following: + +``` +{{ $values.D.Labels.Source }} at the {{ $values.D.Labels.Location }} has generated {{ printf "%.2f" $values.D.Value }} jigowatts.` +``` + +This will display as follows: +![grafana alerts from flux queries](/media/tutorials/screenshot-flux-tip-significant-figures.png)) + +You can reference our documentation on [alert message templating](/docs/grafana/latest/alerting/contact-points/message-templating/) to learn more about this powerful feature. + +### Conclusion + +In this example we showed how to create a Flux-based alert that uses two distinct conditions from two distinct queries that use data from two distinct data sources. For example three we will switch gears and tackle another popular alerting scenario: how to create an alert based on an aggregated (per day) value. + +## Example 3: how to create a Grafana Alert based on an aggregated (per-day) value + +One of the most common requests in [Grafana's community forum](https://community.grafana.com) involves graphing daily electrical consumption and production. This sort of data is very often stored in InfluxDB. In this example we will see how to aggregate time series data into a per-day value and then alert on it. + +Let’s assume our electricity meter sends a reading to InfluxDB once per hour and contains the total kWh used for that hour. We want to write a query that will aggregate these per-hour values into a per-day value, then create an alert that triggers when the power consumption (kWh) exceeds 5,000 kWh per day. + +### Add an initial Flux query to your Grafana Alert rule + +1. Let's begin by examining a typical query and the resulting time graph for our hourly data across a 7-day period. A query like this is shown below: + + ```flux + from(bucket: "RetroEncabulator") + |> range(start: v.timeRangeStart, stop: v.timeRangeStop) + |> filter(fn: (r) => r["_measurement"] == "ElectricityData") + |> filter(fn: (r) => r["Location"] == "PlantD5") + |> filter(fn: (r) => r["_field"] == "power_consumed") + |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) + |> yield(name: "power") + ``` + + We can see the same pattern of Flux functions here that we say in examples 1 and 2. A query like this would produce a graph similar to the following: + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-aggregatewindow-timeseries-graph.png) + +1. Now let's adjust our query to calculate daily usage. With many datasources, this can be a rather complex operation. But with Flux, by simply changing the aggregateWindow function parameters we can calculate the daily usage over the same 7-day period: + + ```flux + from(bucket: "RetroEncabulator") + |> range(start: v.timeRangeStart, stop: v.timeRangeStop) + |> filter(fn: (r) => r["_measurement"] == "ElectricityData") + |> filter(fn: (r) => r["Location"] == "PlantD5") + |> filter(fn: (r) => r["_field"] == "power_consumed") + |> aggregateWindow(every: 1d, fn: sum) + |> yield(name: "power") + ``` + + Note how we've adjusted our `aggregateWindow()` function to `aggregateWindow(every: 1d, fn: sum)`. This results in a graph like so: + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-aggregatewindow-aggregated.png) + +1. Add expressions to your Grafana Alert rule. + + Now that we have our per-day query correct, we can continue using the same pattern as before, adding expressions to reduce and perform math on our results. + + As before, let's reduce our query to a single value: + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-aggregatewindow-reduce-expression.png) + + Now create a math expression to be alerted on and set the evaluation behavior. In this case we want to write `$B > 5000`: + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-aggregatewindow-math-expression.png) + + And now we are alerting on our daily electricity consumption whenever we exceed 5000 kWh. Here is preview of our alert: + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-aggregatewindow-alert-preview.png) + +### Conclusion + +Plotting and aggregating electrical consumption is a common use case for combining InfluxDB and Grafana. Using Flux, we saw just how easy it can be to group our data by day and then alert on that daily value. In our next two examples we will examine the more complex form of Grafana Alert: multidimensional alerts. + +## Example 4: create a dynamic (multidimensional) Grafana Alert using Flux + +Let’s return to our fluid tanks from example 1, but this time let’s assume we have 5 tanks (A5, B4, C3, D2, and E1). We are now tracking the temperature in five tanks: A5, B4, C3, D2, and E1. + +We want to create one multidimensional alert that will notify us whenever the temperature in any tank is less than 30 °C or greater than 60 °C. + +### Add an initial Flux query to your Grafana Alert rule + +We begin, as always, by writing our initial query. This is very similar to our query in example 1, but note how our third `filter()` function captures the data from all five tanks and not just `A5`: + +```flux +from(bucket: "HyperEncabulator") +|> range(start: v.timeRangeStart, stop: v.timeRangeStop) +|> filter(fn: (r) => r["_measurement"] == "TemperatureData") +|> filter(fn: (r) => r["MeasType"] == "actual") +|> filter(fn: (r) => r["Tank"] == "A5" or r["Tank"] == "B4" or r["Tank"] == "C3" or r["Tank"] == "D2" or r["Tank"] == "E1") +|> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) +|> yield(name: "mean") +``` + +💡Tip: If the tanks were shut down every night from 23:00 to 07:00, they would possibly fall below the 30 °C threshold. If one did not want to receive alerts during those hours, one can use the Flux function hourSelection() which filters rows by time values in a specified hour range. + +```flux +|> hourSelection(start: 7, stop: 23)` +``` + +A query like the one above will produce a time series graph like this: + +![grafana alerts from flux queries](/media/tutorials/screenshot-flux-multidimensional-timeseries-graph.png) + +### Add expressions to your Grafana Alert rule + +1. We create a Reduce expression that will reduce the time series for each tank to a single value. This gives us five distinct temperatures: + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-multidimensional-reduce-expression.png)) + +1. Create a math expression to be alerted on. This is the exact same expression from example 1, `$B < 30 || $B > 60`: + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-multidimensional-math-expression.png) + +As we can see three tanks are within the acceptable thresholds while two tanks have crossed the upper boundary. This would trigger an alert for tanks `D2` and `E1`. + +### Conclusion + +With multidimensional alerts we can avoid repeating ourselves. But what if the scenario were even more complex? In the next and final example, we will examine how to use multidimensional alerts to create the most dynamic alerts possible. + +## Example 5: how to create a dynamic (multidimensional) Grafana Alert using multiple queries and multiple thresholds with Flux + +For this final example let's continue with our five fluid tanks and their five datasets.Let’s assume again that each tank has a temperature controller with a setpoint value that is stored in InfluxDB. Let’s mix things up and assume that each tank has a _different_ setpoint, where we always need to be within 3 degrees of the setpoint. + +We want to create one multidimensional alert that will cover each unique scenario for each tank, triggering an alert whenever any tank's temperature moves beyond its unique allowable range. + +To better visualize this challenge, here is a table representing our five tanks, their temperature setpoints, and their allowable range: + +| Tank | Setpoint | Allowable Range (±3) | +| ---- | -------- | -------------------- | +| A5 | 45 | 42 to 48 | +| B4 | 55 | 52 to 58 | +| C3 | 60 | 57 to 63 | +| D2 | 72 | 69 to 75 | +| E1 | 80 | 77 to 83 | + +With Grafana Alerting, we can create a single multidimensional rule to cover all 5 tanks, and we can use Flux to compare the setpoint and actual value for each tank. In other words, one multidimensional alert can monitor 5 separate tanks, each with different setpoints and actual values, but all with one common "allowable threshold" (i.e. a temperature difference of ±3 degrees). + +### Add an initial Flux query to your Grafana Alert rule + +Let's begin with our data query. It is similar to our past queries, only now more complex. We must add extra functions to get our data into the proper format, including a `pivot()`, `map()`, `rename()`, `keep()`, and `drop()` function: + +```flux +from(bucket: "HyperEncabulator") + |> range(start: v.timeRangeStart, stop: v.timeRangeStop) + |> filter(fn: (r) => r["_measurement"] == "TemperatureData") + |> filter(fn: (r) => r["MeasType"] == "actual" or r["MeasType"] == "setpoint") + |> filter(fn: (r) => r["Tank"] == "A5" or r["Tank"] == "B4" or r["Tank"] == "C3" or r["Tank"] == "D2" or r["Tank"] == "E1") + |> filter(fn: (r) => r["_field"] == "Temperature") + |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) + |> pivot(rowKey:["_time"], columnKey: ["MeasType"], valueColumn: "_value") + |> map(fn: (r) => ({ r with _value: (r.setpoint - r.actual)})) + |> rename(columns: {_value: "difference"}) + |> keep(columns: ["_time", "difference", "Tank"]) + |> drop(columns: ["actual", "setpoint"]) + |> yield(name: "mean") +``` + +Note in the above that we are calculating the difference between the actual and the setpoint. The way Grafana parses the result from InfluxDB is that if a \_value column is found, it is assumed to be a time-series. The quick workaround is to add the following `rename()` function: + +```flux + |> rename(columns: {_value: "something"}) +``` + +The above query results in this time series: + +![grafana alerts from flux queries](/media/tutorials/screenshot-flux-complex-query-timeseries-graph.png) + +### Add expressions to your Grafana Alert rule + +1. Again, we create a Reduce expression for the above query to reduce each of the above to a single value. This value represents the temperature differential between each tank's setpoint and its actual real-time temperature: + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-complex-query-reduce-expression.png) + +1. Now we create a math expression to be alerted on. This time we will create a condition that checks if the absolute value of our reduce calculation is greater than 3, `abs($(B))>3.0`: + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-complex-query-math-expression.png) + +We can now see that two tanks, `D2` and `E1`, are evaluating to true. When we preview the alert we can see that those two tanks will trigger a notification and change their state from `Normal` to `Alerting`: + +![grafana alerts from flux queries](/media/tutorials/screenshot-flux-complex-query-alert-preview-state-normal.png) +![grafana alerts from flux queries](/media/tutorials/screenshot-flux-complex-query-alert-preview-state-alerting.png) + +### Conclusion + +Flux queries and Grafana Unified Alerting are a powerful combination to identify practically any alertable conditions in your dataset, or across your entire system. For more information on Grafana Alerting, [visit the documentation here](/docs/grafana/latest/alerting/). For more information on the Flux query language, [you can visit that documentation as well](https://docs.influxdata.com/flux/v0.x/). diff --git a/docs/sources/tutorials/create-users-and-teams/index.md b/docs/sources/tutorials/create-users-and-teams/index.md new file mode 100644 index 00000000000..23b02d1d663 --- /dev/null +++ b/docs/sources/tutorials/create-users-and-teams/index.md @@ -0,0 +1,236 @@ +--- +title: Create users and teams +summary: Learn how to set up teams and users. +description: Learn how to set up teams and users. +id: create-users-and-teams +categories: ['administration'] +tags: ['beginner'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +weight: 20 +--- + +## Introduction + +This tutorial is for admins or anyone that wants to learn how to manage +users in Grafana. You'll add multiple local users, organize them into teams, +and make sure they're only able to access the resources they need. + +### Scenario + +_Graphona_, a fictional telemarketing company, has asked you to configure Grafana +for their teams. + +In this scenario, you'll: + +- Create users and organize them into teams. +- Manage resource access for each user and team through roles and folders. + +{{% class "prerequisite-section" %}} + +### Prerequisites + +- Grafana 7.0 or newer, this tutorial was tested with Grafana 8.5. +- A user with the Admin or Server Admin role. + {{% /class %}} + +## Add users + +In Grafana, all users are granted an _organization role_ that determines what +resources they can access. + +There are three types of organization roles in Grafana. The **Grafana Admin** is +a global role, the default `admin` user has this role. + +- **Grafana Admin -** Manage organizations, users, and view server-wide settings. +- **Organization Administrator -** Manage data sources, teams, and users within an organization. +- **Editor -** Create and edit dashboards. +- **Viewer -** View dashboards. + +> **Note**: You can also configure Grafana to allow [anonymous access](/docs/grafana/latest/auth/overview/#anonymous-authentication), to make dashboards available even to those who don't have a Grafana user account. That's how Grafana Labs made https://play.grafana.org publicly available. + +### Exercise + +Graphona has asked you to add a group of early adopters that work in the Marketing and Engineering teams. They'll need to be able to edit their own team's dashboards, but want to have view access to dashboards that belong to the other team. + +| Name | Email | Username | +| ----------------- | ----------------------------- | ----------------- | +| Almaz Russom | almaz.russom@example.com | almaz.russom | +| Brenda Tilman | brenda.tilman@example.com | brenda.tilman | +| Mada Rawdha Tahan | mada.rawdha.tahan@example.com | mada.rawdha.tahan | +| Yuan Yang | yuan.yang@example.com | yuan.yang | + +#### Add users + +Repeat the following steps for each of the employees in the table above to create the new user accounts: + +1. Log in as a user that has the **Server Admin** role. +1. On the sidebar, click the **Server Admin** (shield) icon. +1. Choose **Users** from the menu drop-down, then click **New User**. +1. Enter the **Name**, **Email**, **Username**, and **Password** from the table above. +1. Click the **Create User** button to create the account. + +When you create a user they are granted the Viewer role by default, which means that they won't be able to make any changes to any of the resources in Grafana. That's ok for now, you'll grant more user permissions by adding users to _teams_ in the next step. + +## Assign users to teams + +Teams let you grant permissions to a group of users, instead of granting permissions to individual users one at a time. + +Teams are useful when onboarding new colleagues. When you add a user to a team, they get access to all resources assigned to that team. + +### Exercise + +In this step, you'll create two teams and assign users to them. + +| Username | Team | +| ----------------- | ----------- | +| brenda.tilman | Marketing | +| mada.rawdha.tahan | Marketing | +| almaz.russom | Engineering | +| yuan.yang | Engineering | + +#### Create a team + +Create the _Marketing_ and _Engineering_ teams. + +1. In the sidebar, hover your mouse over the **Configuration** (gear) icon and + then click **Teams**. +1. Click **New team**. +1. In **Name**, enter the name of the team: either _Marketing_ or _Engineering_. + You do not need to enter an email. +1. Click **Create**. +1. Click on the **Teams** link at the top of the page to return to teams page and create the second team. + +#### Add a user to a team + +Repeat these steps for each user to assign them to their team. Refer to the table above for team assignments. + +1. Click the team name _Marketing_ or _Engineering_ to add members to that team. +1. Click **Add member**. +1. In the **Add team member** box, click the drop-down arrow to choose the user you want to add to the team . +1. Click **Add to team**. + +When you're done, you'll have two teams with two users assigned to each. + +## Manage resource access with folders + +It's a good practice to use folders to organize collections of related dashboards. You can assign permissions at the folder level to individual users or teams. + +### Exercise + +The Marketing team is going to use Grafana for analytics, while the Engineering team wants to monitor the application they're building. + +You'll create two folders, _Analytics_ and _Application_, where each team can add their own dashboards. The teams still want to be able to view each other's dashboards. + +| Folder | Team | Permissions | +| ----------- | ----------- | ----------- | +| Analytics | Marketing | Edit | +| | Engineering | View | +| Application | Marketing | View | +| | Engineering | Edit | + +Repeat the following steps for each folder. You'll move through all three steps for each folder before moving on to the next one. + +#### Add a folder for each team + +1. In the sidebar, hover your cursor over the **Dashboards** (four squares) icon and then click **Browse**. +1. To create a folder, click **New Folder**. +1. In **Name**, enter the folder name. +1. Click **Create**. +1. Stay in the folder view and move on to the next sections to edit permissions for this folder. + +#### Remove the viewer role from folder permissions + +By default, when you create a folder, all users with the Viewer role are granted permission to view the folder. + +In this example, Graphona wants to explicitly grant teams access to folders. To support this, you need to remove the Viewer role from the list of permissions: + +1. Go to the **Permissions** tab. +1. Remove the Viewer role from the list, by clicking the red button on the right. +1. Stay in the permissions tab and move on to the next section to grant folder permissions for each team. + +#### Grant folder permissions to a team: + +1. Click **Add Permission**. +1. In the **Add Permission For** dialog, make sure "Team" is selected in the first box. +1. In the second box, select the team to grant access to. +1. In the third box, select the access you want to grant. +1. Click **Save**. +1. Repeat for the other team. +1. Click the **Dashboards** link at the top of the page to return to the dashboard list. + +When you're finished, you'll have two empty folders, the contents of which can only be viewed by members of the Marketing or Engineering teams. Only Marketing team members can edit the contents of the Analytics folder, only Engineering team members can edit the contents of the Application folder. + +## Define granular permissions + +By using folders and teams, you avoid having to manage permissions for individual users. + +However, there are times when you need to configure permissions on a more granular level. For these cases, Grafana allows you to override permissions for specific dashboards. + +### Exercise + +Graphona has hired a consultant to assist the Marketing team. The consultant should only be able to access the SEO dashboard in the Analytics folder. + +| Name | Email | Username | +| ---------- | -------------------------------- | ---------- | +| Luc Masson | luc.masson@exampleconsulting.com | luc.masson | + +#### Add a new user + +1. In the sidebar, click the **Server Admin** (shield) icon. +1. In the Users tab, click **New user**. +1. In **Name**, enter the name of the user. +1. In **E-mail**, enter the email of the user. +1. In **Username**, enter the username that the user will use to log in. +1. In **Password**, enter a password. The user can change their password once they log in. +1. Click **Create user** to create the user account. + +#### Create a dashboard + +1. In the sidebar, click the **Create** (plus) icon to create a new dashboard. +1. In the top right corner, click the cog icon to go to **Dashboard settings**. +1. In **Name**, enter **SEO**. +1. Click **Save Dashboard**. +1. In the **Save dashboard as...** pop-up, choose the **Analytics** folder from the drop-down and click **Save**. + +#### Grant a user permission to view dashboard + +1. In the top right corner of your dashboard, click the cog icon to go to **Dashboard settings**. +1. Go to the **Permissions** tab, and click **Add Permission**. +1. In the **Add Permission For** dialog, select **User** in the first box. +1. In the second box, select the user to grant access to: Luc Masson. +1. In the third box, select **View**. +1. Click **Save**. +1. Click **Save dashboard**. +1. Add a note about giving Luc Masson Viewer permission for the dashboard and then click **Save**. + +You've created a new user and given them unique permissions to view a single dashboard within a folder. + +#### Check your work + +You can repeat these steps to log in as the other users you've created see the differences in the viewer and editor roles. + +For this example, you can log in as the user `luc.masson` to see that they can only access the SEO dashboard. + +1. Click the profile (avatar) button in the bottom left corner, choose **Sign out**. +1. Enter `luc.masson` as the username. +1. Enter the password you created for Luc. +1. Click **Log in**. +1. In the sidebar, hover your cursor over the **Dashboards** (four squares) icon and then click **Browse**. +1. You'll notice that you won't see the **Analytics** folder in the folder view because we did not give Luc folder permission. +1. Click on the list icon (3 lines) to see the dashboard list. +1. Click on the **SEO dashboard**, there shouldn't be any editing permissions since we assigned Luc the viewer role. + +## Summary + +In this tutorial, you've configured Grafana for an organization: + +- You added users to your organization. +- You created teams to manage permissions for groups of users. +- You configured permissions for folders and dashboard. + +### Learn more + +- [Organization Roles](/docs/grafana/next/administration/manage-users-and-permissions/about-users-and-permissions/#organization-roles) +- [Permissions Overview](/docs/grafana/latest/administration/manage-users-and-permissions/about-users-and-permissions/#about-users-and-permissions) diff --git a/docs/sources/tutorials/grafana-fundamentals/index.md b/docs/sources/tutorials/grafana-fundamentals/index.md new file mode 100644 index 00000000000..5374ac91ae0 --- /dev/null +++ b/docs/sources/tutorials/grafana-fundamentals/index.md @@ -0,0 +1,354 @@ +--- +title: Grafana fundamentals +summary: Get familiar with Grafana +description: Get familiar with Grafana +id: grafana-fundamentals +categories: ['fundamentals'] +tags: ['beginner'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +weight: 10 +--- + +## Introduction + +In this tutorial, you'll learn how to use Grafana to set up a monitoring solution for your application. + +In this tutorial, you'll: + +- Explore metrics and logs +- Build dashboards +- Annotate dashboards +- Set up alerts + +{{% class "prerequisite-section" %}} + +### Prerequisites + +- [Docker](https://docs.docker.com/install/) +- [Docker Compose](https://docs.docker.com/compose/) (included in Docker for Desktop for macOS and Windows) +- [Git](https://git-scm.com/) + {{% /class %}} + +## Set up the sample application + +This tutorial uses a sample application to demonstrate some of the features in Grafana. To complete the exercises in this tutorial, you need to download the files to your local machine. + +In this step, you'll set up the sample application, as well as supporting services, such as [Prometheus](https://prometheus.io/) and [Loki](/oss/loki/). + +1. Clone the [github.com/grafana/tutorial-environment](https://github.com/grafana/tutorial-environment) repository. + + ``` + git clone https://github.com/grafana/tutorial-environment.git + ``` + +1. Change to the directory where you cloned this repository: + + ``` + cd tutorial-environment + ``` + +1. Make sure Docker is running: + + ``` + docker ps + ``` + + No errors means it is running. If you get an error, then start Docker and then run the command again. + +1. Start the sample application: + + ``` + docker-compose up -d + ``` + + The first time you run `docker-compose up -d`, Docker downloads all the necessary resources for the tutorial. This might take a few minutes, depending on your internet connection. + + > **Note:** If you already have Grafana, Loki, or Prometheus running on your system, then you might see errors because the Docker image is trying to use ports that your local installations are already using. Stop the services, then run the command again. + +1. Ensure all services are up-and-running: + + ``` + docker-compose ps + ``` + + In the **State** column, it should say `Up` for all services. + +1. Browse to the sample application on [localhost:8081](http://localhost:8081). + +### Grafana News + +The sample application, Grafana News, lets you post links and vote for the ones you like. + +To add a link: + +1. In **Title**, enter **Example**. +1. In **URL**, enter **https://example.com**. +1. Click **Submit** to add the link. + + The link appears in the list under the Grafana News heading. + +To vote for a link, click the triangle icon next to the name of the link. + +## Log in to Grafana + +Grafana is an open-source platform for monitoring and observability that lets you visualize and explore the state of your systems. + +1. Open a new tab. +1. Browse to [localhost:3000](http://localhost:3000). +1. In **email or username**, enter **admin**. +1. In **password**, enter **admin**. +1. Click **Log In**. + + The first time you log in, you're asked to change your password: + +1. In **New password**, enter your new password. +1. In **Confirm new password**, enter the same password. +1. Click **Save**. + +The first thing you see is the Home dashboard, which helps you get started. + +To the far left you can see the _sidebar_, a set of quick access icons for navigating Grafana. + +## Add a metrics data source + +The sample application exposes metrics which are stored in [Prometheus](https://prometheus.io/), a popular time series database (TSDB). + +To be able to visualize the metrics from Prometheus, you first need to add it as a data source in Grafana. + +1. In the sidebar, hover your cursor over the **Configuration** (gear) icon, and then click **Data sources**. +1. Click **Add data source**. +1. In the list of data sources, click **Prometheus**. +1. In the URL box, enter **http\://prometheus:9090**. +1. Click **Save & test**. + + Prometheus is now available as a data source in Grafana. + +## Explore your metrics + +Grafana Explore is a workflow for troubleshooting and data exploration. In this step, you'll be using Explore to create ad-hoc queries to understand the metrics exposed by the sample application. + +> Ad-hoc queries are queries that are made interactively, with the purpose of exploring data. An ad-hoc query is commonly followed by another, more specific query. + +1. In the sidebar, click the **Explore** (compass) icon. +1. In the **Query editor**, where it says _Enter a PromQL query…_, enter `tns_request_duration_seconds_count` and then press Shift + Enter. + A graph appears. +1. In the top right corner, click the dropdown arrow on the **Run Query** button, and then select **5s**. Grafana runs your query and updates the graph every 5 seconds. + + You just made your first _PromQL_ query! [PromQL](https://prometheus.io/docs/prometheus/latest/querying/basics/) is a powerful query language that lets you select and aggregate time series data stored in Prometheus. + + `tns_request_duration_seconds_count` is a _counter_, a type of metric whose value only ever increases. Rather than visualizing the actual value, you can use counters to calculate the _rate of change_, i.e. how fast the value increases. + +1. Add the [`rate`](https://prometheus.io/docs/prometheus/latest/querying/functions/#rate) function to your query to visualize the rate of requests per second. Enter the following in the **Query editor** and then press Shift + Enter. + + ``` + rate(tns_request_duration_seconds_count[5m]) + ``` + + Immediately below the graph there's an area where each time series is listed with a colored icon next to it. This area is called the _legend_. + + PromQL lets you group the time series by their labels, using the [`sum`](https://prometheus.io/docs/prometheus/latest/querying/operators/#aggregation-operators) aggregation operator. + +1. Add the `sum` aggregation operator to your query to group time series by route: + + ``` + sum(rate(tns_request_duration_seconds_count[5m])) by(route) + ``` + +1. Go back to the [sample application](http://localhost:8081) and generate some traffic by adding new links, voting, or just refresh the browser. + +1. In the upper-right corner, click the _time picker_, and select **Last 5 minutes**. By zooming in on the last few minutes, it's easier to see when you receive new data. + +Depending on your use case, you might want to group on other labels. Try grouping by other labels, such as `status_code`, by changing the `by(route)` part of the query. + +## Add a logging data source + +Grafana supports log data sources, like [Loki](/oss/loki/). Just like for metrics, you first need to add your data source to Grafana. + +1. In the sidebar, hover your cursor over the **Configuration** (gear) icon, and then click **Data Sources**. +1. Click **Add data source**. +1. In the list of data sources, click **Loki**. +1. In the URL box, enter [http://loki:3100](http://loki:3100). +1. Click **Save & Test** to save your changes. + +Loki is now available as a data source in Grafana. + +## Explore your logs + +Grafana Explore not only lets you make ad-hoc queries for metrics, but lets you explore your logs as well. + +1. In the sidebar, click the **Explore** (compass) icon. +1. In the data source list at the top, select the **Loki** data source. +1. In the **Query editor**, enter: + + ``` + {filename="/var/log/tns-app.log"} + ``` + +1. Grafana displays all logs within the log file of the sample application. The height of each bar in the graph encodes the number of logs that were generated at that time. + +1. Click and drag across the bars in the graph to filter logs based on time. + +Not only does Loki let you filter logs based on labels, but on specific occurrences. + +Let's generate an error, and analyze it with Explore. + +1. In the [sample application](http://localhost:8081), post a new link without a URL to generate an error in your browser that says `empty url`. +1. Go back to Grafana and enter the following query to filter log lines based on a substring: + + ``` + {filename="/var/log/tns-app.log"} |= "error" + ``` + +1. Click on the log line that says `level=error msg="empty url"` to see more information about the error. + + > **Note:** If you're in Live mode, clicking logs will not show more information about the error. Instead, stop and exit the live stream, then click the log line there. + +Logs are helpful for understanding what went wrong. Later in this tutorial, you'll see how you can correlate logs with metrics from Prometheus to better understand the context of the error. + +## Build a dashboard + +A _dashboard_ gives you an at-a-glance view of your data and lets you track metrics through different visualizations. + +Dashboards consist of _panels_, each representing a part of the story you want your dashboard to tell. + +Every panel consists of a _query_ and a _visualization_. The query defines _what_ data you want to display, whereas the visualization defines _how_ the data is displayed. + +1. In the sidebar, hover your cursor over the **Create** (plus sign) icon and then click **Dashboard**. +1. Click **Add a new panel**. +1. In the **Query editor** below the graph, enter the query from earlier and then press Shift + Enter: + + ``` + sum(rate(tns_request_duration_seconds_count[5m])) by(route) + ``` + +1. In the **Legend** field, enter **{{route}}** to rename the time series in the legend. The graph legend updates when you click outside the field. +1. In the Panel editor on the right, under **Settings**, change the panel title to "Traffic". +1. Click **Apply** in the top-right corner to save the panel and go back to the dashboard view. +1. Click the **Save dashboard** (disk) icon at the top of the dashboard to save your dashboard. +1. Enter a name in the **Dashboard name** field and then click **Save**. + +## Annotate events + +When things go bad, it often helps if you understand the context in which the failure occurred. Time of last deploy, system changes, or database migration can offer insight into what might have caused an outage. Annotations allow you to represent such events directly on your graphs. + +In the next part of the tutorial, we will simulate some common use cases that someone would add annotations for. + +1. To manually add an annotation, click anywhere in your graph, then click **Add annotation**. +1. In **Description**, enter **Migrated user database**. +1. Click **Save**. + + Grafana adds your annotation to the graph. Hover your mouse over the base of the annotation to read the text. + +Grafana also lets you annotate a time interval, with _region annotations_. + +Add a region annotation: + +1. Press Ctrl (or Cmd on macOS), then click and drag across the graph to select an area. +1. In **Description**, enter **Performed load tests**. +1. In **Tags**, enter **testing**. + +Manually annotating your dashboard is fine for those single events. For regularly occurring events, such as deploying a new release, Grafana supports querying annotations from one of your data sources. Let's create an annotation using the Loki data source we added earlier. + +1. At the top of the dashboard, click the **Dashboard settings** (gear) icon. +1. Go to **Annotations** and click **Add annotation query**. +1. In **Name**, enter **Errors**. +1. In **Data source**, select **Loki**. +1. In **Query**, enter the following query: + + ``` + {filename="/var/log/tns-app.log"} |= "error" + ``` + + + +1. Click **Add**. Grafana displays the Annotations list, with your new annotation. +1. Click the **Go back** arrow to return to your dashboard. +1. At the top of your dashboard, there is now a toggle to display the results of the newly created annotation query. Press it so that it's enabled. + +The log lines returned by your query are now displayed as annotations in the graph. + +Being able to combine data from multiple data sources in one graph allows you to correlate information from both Prometheus and Loki. + +Annotations also work very well alongside alerts. In the next and final section, we will set up an alert for our app `grafana.news` and then we will trigger it. This will provide a quick intro to our new Alerting platform. + +## Create a Grafana Managed Alert + +Alerts allow you to identify problems in your system moments after they occur. By quickly identifying unintended changes in your system, you can minimize disruptions to your services. + +Grafana's new alerting platform debuted with Grafana 8. A year later, with Grafana 9, it became the default alerting method. In this step we will create a Grafana Managed Alert. Then we will trigger our new alert and send a test message to a dummy endpoint. + +The most basic alert consists of two parts: + +1. A _Contact Point_ - A Contact point defines how Grafana delivers an alert. When the conditions of an _alert rule_ are met, Grafana notifies the contact points, or channels, configured for that alert. Some popular channels include email, webhooks, Slack notifications, and PagerDuty notifications. +1. An _Alert rule_ - An Alert rule defines one or more _conditions_ that Grafana regularly evaluates. When these evaluations meet the rule's criteria, the alert is triggered. + +To begin, let's set up a webhook Contact Point. Once we have a usable endpoint, we'll write an alert rule and trigger a notification. + +### Create a Contact Point for Grafana Managed Alerts + +In this step, we'll set up a new Contact Point. This contact point will use the _webhooks_ channel. In order to make this work, we also need an endpoint for our webhook channel to receive the alert. We will use [requestbin.com](https://requestbin.com) to quickly set up that test endpoint. This way we can make sure that our alert is actually sending a notification somewhere. + +1. Browse to [requestbin.com](https://requestbin.com). +1. Under the **Create Request Bin** button, click the **public bin** link. + +Your request bin is now waiting for the first request. + +1. Copy the endpoint URL. + +Next, let's configure a Contact Point in Grafana's Alerting UI to send notifications to our Request Bin. + +1. Return to Grafana. In Grafana's sidebar, hover your cursor over the **Alerting** (bell) icon and then click **Contact points**. +1. Click **+ New contact point**. +1. In **Name**, write **RequestBin**. +1. In **Contact point type**, choose **Webhook**. +1. In **Url**, paste the endpoint to your request bin. +1. Click **Test** to send a test alert to your request bin. +1. Navigate back to the request bin you created earlier. On the left side, there's now a `POST /` entry. Click it to see what information Grafana sent. +1. Return to Grafana and click **Save contact point**. + +We have now created a dummy webhook endpoint and created a new Alerting Contact Point in Grafana. Now we can create an alert rule and link it to this new channel. + +### Add an Alert Rule to Grafana + +Now that Grafana knows how to notify us, it's time to set up an alert rule: + +1. In Grafana's sidebar, hover the cursor over the **Alerting** (bell) icon and then click **Alert rules**. +1. Click **+ New alert rule**. +1. For **Section 1**, name the rule `fundamentals-test`, and set **Rule type** to **Grafana Managed Alert**. For **Folder** type `fundamentals` and in the box that appears, press **Create: fundamentals**. +1. For **Section 2**, find the **query A** box. Choose your Prometheus datasource and enter the same query that we used in our earlier panel: `sum(rate(tns_request_duration_seconds_count[5m])) by(route)`. Press **Run queries**. You should see some data in the graph. +1. Now scroll down to the **query B** box. For **Operation** choose `Classic condition`. [You can read more about classic and multi-dimensional conditions here](/docs/grafana/latest/alerting/unified-alerting/alerting-rules/create-grafana-managed-rule/#single-and-multi-dimensional-rule). For conditions enter the following: `WHEN last() OF A IS ABOVE 0.2` +1. In **Section 3**, enter `30s` for the **Evaluate every** field. For the purposes of this tutorial, the evaluation interval is intentionally short. This makes it easier to test. In the **for** field, enter `0m`. This setting makes Grafana wait until an alert has fired for a given time before Grafana sends the notification. +1. In **Section 4**, you can add some sample text to your summary message. [Read more about message templating here](/docs/grafana/latest/alerting/unified-alerting/message-templating/). +1. Click **Save and exit** at the top of the page. +1. In Grafana's sidebar, hover the cursor over the **Alerting** (bell) icon and then click **Notification policies**. +1. Under **Root policy**, press **Edit** and change the **Default contact point** to **RequestBin**. As a system grows, admins can use the **Notification policies** setting to organize and match alert rules to specific contact points. + +### Trigger a Grafana Managed Alert + +We have now configured an alert rule and a contact point. Now let's see if we can trigger a Grafana Managed Alert by generating some traffic on our sample application. + +1. Browse to [localhost:8081](http://localhost:8081). +1. Repeatedly click the vote button or refresh the page to generate a traffic spike. + +Once the query `sum(rate(tns_request_duration_seconds_count[5m])) by(route)` returns a value greater than `0.2` Grafana will trigger our alert. Browse to the Request Bin we created earlier and find the sent Grafana alert notification with details and metadata. + +## Summary + +In this tutorial you learned about fundamental features of Grafana. To do so, we ran several Docker containers on your local machine. When you are ready to clean up this local tutorial environment, run the following command: + +``` +docker-compose down -v +``` + +### Learn more + +Check out the links below to continue your learning journey with Grafana's LGTM stack. + +- [Prometheus](/docs/grafana/latest/features/datasources/prometheus/) +- [Loki](/docs/grafana/latest/features/datasources/loki/) +- [Explore](/docs/grafana/latest/features/explore/) +- [Alerting Overview](/docs/grafana/latest/alerting/) +- [Alert rules](/docs/grafana/latest/alerting/create-alerts/) +- [Contact Points](/docs/grafana/latest/alerting/notifications/) diff --git a/docs/sources/tutorials/iis/index.md b/docs/sources/tutorials/iis/index.md new file mode 100644 index 00000000000..ec4a190a128 --- /dev/null +++ b/docs/sources/tutorials/iis/index.md @@ -0,0 +1,146 @@ +--- +title: Use IIS with URL Rewrite as a reverse proxy +summary: Learn how to set up Grafana behind IIS with URL Rewrite. +description: Learn how to set up Grafana behind IIS with URL Rewrite. +id: iis +categories: ['administration'] +tags: ['advanced'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +aliases: ['/docs/grafana/latest/tutorials/iis/'] +--- + +# Use IIS with URL Rewrite as a reverse proxy + +If you want Grafana to be a subpath/subfolder under a website in IIS then the Application Request Routing (ARR) and URL Rewrite modules for ISS can be used to support this. + +Example: + +- Parent site: http://yourdomain.com:8080 +- Grafana: http://localhost:3000 + +Grafana as a subpath: http://yourdomain.com:8080/grafana + +Other Examples: + +- If the application is only served on the local server, the parent site can also look like http://localhost:8080. +- If your domain is served using https on port 443, and thus the port is not normally entered in the address of your site, then the need to specify a port for the parent site in the configuration steps below can be eliminated. + +## Setup + +Install the URL Rewrite module for IIS. + +- Download and install the URL Rewrite module for IIS: https://www.iis.net/downloads/microsoft/url-rewrite + +You will also need the Application Request Routing (ARR) module for IIS for proxy forwarding + +- Download and install ARR module for IIS: https://www.iis.net/downloads/microsoft/application-request-routing + +## Grafana Config + +The Grafana config can be set by creating a file named/editing the existing file named `custom.ini` in the `conf` subdirectory of your Grafana installation. See the [installation instructions](http://docs.grafana.org/installation/windows/#configure) for more details. + +Using the example from above, if the subpath is `grafana` (you can set this to whatever is required) and the parent site is `yourdomain.com:8080`, then you would add this to the `custom.ini` config file: + +```bash +[server] +domain = yourdomain.com:8080 +root_url = %(protocol)s://%(domain)s/grafana/ +``` + +Restart the Grafana server after changing the config file. + +Configured address to serve Grafana: http://yourdomain.com:8080/grafana + +--- + +If you already have a subpath on your domain, configure it as follows: + +- Your Parent Site Address: http://yourdomain.com/existingsubpath + +```bash +[server] +domain = yourdomain.com/existingsubpath +root_url = %(protocol)s://%(domain)s/grafana/ +``` + +Restart the Grafana server after changing the config file. + +Configured address to serve Grafana: http://yourdomain.com/existingsubpath/grafana + +## IIS Config + +### Step 1: Forward Proxy + +1. Open the IIS Manager and click on the server +2. In the admin console for the server, double click on the Application Request Routing option: +3. Click the `Server Proxy Settings` action on the right-hand pane +4. Select the `Enable proxy` checkbox so that it is enabled +5. Click `Apply` and proceed with the URL Rewriting configuration + +**Note:** If you don't enable the Forward Proxy, you will most likely get 404 Not Found if you only apply the URL Rewrite rule + +### Step 2: URL Rewriting + +1. In the IIS Manager, click on the website that grafana will run under. For example, select the website that is bound to the http://yourdomain.com domain. +2. In the admin console for this website, double click on the URL Rewrite option: + +{{< figure src="/static/img/docs/tutorials/IIS_admin_console.png" max-width="800px" >}} + +3. Click on the `Add Rule(s)...` action +4. Choose the Blank Rule template for an Inbound Rule + +{{< figure src="/static/img/docs/tutorials/IIS_add_inbound_rule.png" max-width="800px" >}} + +5. Create an Inbound Rule for the website with the following settings: + +- pattern: `grafana(/)?(.*)` (if you have customised the subpath that will be used, use that instead of `grafana`) +- check the `Ignore case` checkbox +- rewrite URL set to `http://localhost:3000/{R:2}` +- check the `Append query string` checkbox +- check the `Stop processing of subsequent rules` checkbox + +{{< figure src="/static/img/docs/tutorials/IIS_url_rewrite.png" max-width="800px" >}} + +6. If your version of Grafana is greater than 8.3.5, you also need to configure the reverse proxy to preserve host headers. + +- This can be achieved by configuring the IIS config file by running this in a cmd prompt + `%windir%\system32\inetsrv\appcmd.exe set config -section:system.webServer/proxy -preserveHostHeader:true /commit:apphost` +- More information here https://github.com/grafana/grafana/issues/45261 + +Finally, navigate to `http://yourdomain.com:8080/grafana` and you should come to the Grafana login page. + +## Troubleshooting + +### 404 error + +When navigating to the Grafana URL (`http://yourdomain.com:8080/grafana`) and a `HTTP Error 404.0 - Not Found` error is returned, then either: + +- The pattern for the Inbound Rule is incorrect. Edit the rule, click on the `Test pattern...` button, test the part of the URL after `http://yourdomain.com:8080/` and make sure it matches. For `grafana/login` the test should return 3 capture groups: {R:0}: `grafana` {R:1}: `/` and {R:2}: `login`. +- The `root_url` setting in the Grafana config file does not match the parent URL with subpath. + +### Grafana Website only shows text with no images or css + +{{< figure src="/static/img/docs/tutorials/IIS_proxy_error.png" max-width="800px" >}} + +1. The `root_url` setting in the Grafana config file does not match the parent URL with subpath. This could happen if the root_url is commented out by mistake (`;` is used for commenting out a line in .ini files): + + `; root_url = %(protocol)s://%(domain)s/grafana/` + +2. or if the subpath in the `root_url` setting does not match the subpath used in the pattern in the Inbound Rule in IIS: + + `root_url = %(protocol)s://%(domain)s/grafana/` + + pattern in Inbound Rule: `wrongsubpath(/)?(.*)` + +3. or if the Rewrite URL in the Inbound Rule is incorrect. + + The Rewrite URL should not include the subpath. + + The Rewrite URL should contain the capture group from the pattern matching that returns the part of the URL after the subpath. The pattern used above returns three capture groups and the third one {R:2} returns the part of the URL after `http://yourdomain.com:8080/grafana/`. + +### You see an 'Error updating options: origin not allowed' error + +- Ensure you have undertaken step 6 above, to configure IIS to preserve host headers by edit IIS config by running this in cmd prompt: + `%windir%\system32\inetsrv\appcmd.exe set config -section:system.webServer/proxy -preserveHostHeader:true /commit:apphost` diff --git a/docs/sources/tutorials/install-grafana-on-raspberry-pi/index.md b/docs/sources/tutorials/install-grafana-on-raspberry-pi/index.md new file mode 100644 index 00000000000..16870ac0f92 --- /dev/null +++ b/docs/sources/tutorials/install-grafana-on-raspberry-pi/index.md @@ -0,0 +1,147 @@ +--- +title: Install Grafana on Raspberry Pi +summary: Get Grafana set up on your Raspberry Pi. +description: Get Grafana set up on your Raspberry Pi. +id: install-grafana-on-raspberry-pi +categories: ['administration'] +tags: ['beginner'] +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +--- + +## Introduction + +The Raspberry Pi is a tiny, affordable, yet capable computer that can run a range of different applications. Even Grafana! + +Many people are running Grafana on Raspberry Pi as a way to monitor their home, for things like indoor temperature, humidity, or energy usage. + +In this tutorial, you'll: + +- Set up a Raspberry Pi using a version of Raspberry Pi OS (previously called "Raspbian") that does not require you to connect a keyboard or monitor (this is often called "headless"). +- Install Grafana on your Raspberry Pi. + +{{% class "prerequisite-section" %}} + +### Prerequisites + +- Raspberry Pi +- SD card + {{% /class %}} + +## Set up your Raspberry Pi + +Before we can install Grafana, you first need to set up your Raspberry Pi. + +For this tutorial, you'll configure your Raspberry Pi to be _headless_. This means you don't need to connect a monitor, keyboard, or mouse to your Raspberry Pi. All configuration is done from your regular computer. + +#### Download and install Raspberry Pi Imager + +Before we get started, you need to download and install the [Raspberry Pi Imager](https://www.raspberrypi.org/software/). + +We'll use the Raspberry Pi Imager to flash the operating system image to the SD card. You download the imager directly from the official Raspberry Pi website and it's available for Ubuntu Linux, macOS, and Windows. + +Follow the directions on the website to download and install the imager. + +#### Install Raspberry Pi OS + +Now it is time to install Raspberry Pi OS. + +1. Insert the SD card into your regular computer from which you plan to install Raspberry Pi OS. +1. Run the Raspberry Pi Imager that you downloaded and installed. +1. To select an operating system, click **Choose OS** in the imager. You will be shown a list of available options. +1. From the list, select **Raspberry Pi OS (other)** and then select **Raspberry Pi OS Lite**, which is a Debian-based operating system for the Raspberry Pi. Since you're going to run a headless Raspberry Pi, you won't need the desktop dependencies. +1. To select where you want to put the operating system image, click **Choose Storage** in the imager and then select the SD card you already inserted into your computer. +1. The final step in the imager to click **Write**. When you do, the imager will write the Raspberry Pi OS Lite image to the SD card and verify that it has been written correctly. +1. Eject the SD card from your computer, and insert it again. + +While you _could_ fire up the Raspberry Pi now, we don't yet have any way of accessing it. + +1. Create an empty file called `ssh` in the boot directory. This enables SSH so that you can log in remotely. + + The next step is only required if you want the Raspberry Pi to connect to your wireless network. Otherwise, connect the it to your network by using a network cable. + +1. **(Optional)** Create a file called `wpa_supplicant.conf` in the boot directory: + + ``` + ctrl_interface=/var/run/wpa_supplicant + update_config=1 + country= + + network={ + ssid="" + psk="" + } + ``` + +All the necessary files are now on the SD card. Let's start up the Raspberry Pi. + +1. Eject the SD card and insert it into the SD card slot on the Raspberry Pi. +1. Connect the power cable and make sure the LED lights are on. +1. Find the IP address of the Raspberry Pi. Usually you can find the address in the control panel for your WiFi router. + +#### Connect remotely via SSH + +1. Open up your terminal and enter the following command: + ``` + ssh pi@ + ``` +1. SSH warns you that the authenticity of the host can't be established. Type "yes" to continue connecting. +1. When asked for a password, enter the default password: `raspberry`. +1. Once you're logged in, change the default password: + ``` + passwd + ``` + +Congratulations! You've now got a tiny Linux machine running that you can hide in a closet and access from your normal workstation. + +## Install Grafana + +Now that you've got the Raspberry Pi up and running, the next step is to install Grafana. + +1. Add the APT key used to authenticate packages: + + ``` + wget -q -O - https://packages.grafana.com/gpg.key | sudo apt-key add - + ``` + +1. Add the Grafana APT repository: + + ``` + echo "deb https://packages.grafana.com/oss/deb stable main" | sudo tee -a /etc/apt/sources.list.d/grafana.list + ``` + +1. Install Grafana: + ``` + sudo apt-get update + sudo apt-get install -y grafana + ``` + +Grafana is now installed, but not yet running. To make sure Grafana starts up even if the Raspberry Pi is restarted, we need to enable and start the Grafana Systemctl service. + +1. Enable the Grafana server: + + ``` + sudo /bin/systemctl enable grafana-server + ``` + +1. Start the Grafana server: + + ``` + sudo /bin/systemctl start grafana-server + ``` + + Grafana is now running on the machine and is accessible from any device on the local network. + +1. Open a browser and go to `http://:3000`, where the IP address is the address that you used to connect to the Raspberry Pi earlier. You're greeted with the Grafana login page. +1. Log in to Grafana with the default username `admin`, and the default password `admin`. +1. Change the password for the admin user when asked. + +Congratulations! Grafana is now running on your Raspberry Pi. If the Raspberry Pi is ever restarted or turned off, Grafana will start up whenever the machine regains power. + +## Summary + +If you want to use Grafana without having to go through a full installation process, check out [Grafana Cloud](/products/cloud/), which is designed to get users up and running quickly and easily. Grafana Cloud offers a forever free plan that is genuinely useful for hobbyists, testing, and small teams. + +### Learn more + +- [Raspberry Pi Documentation](https://www.raspberrypi.org/documentation/) diff --git a/docs/sources/tutorials/integrate-hubot/index.md b/docs/sources/tutorials/integrate-hubot/index.md new file mode 100644 index 00000000000..0d0af2f3821 --- /dev/null +++ b/docs/sources/tutorials/integrate-hubot/index.md @@ -0,0 +1,118 @@ +--- +title: Integrate Hubot with Grafana +summary: Learn how to integrate Hubot with Grafana +description: Learn how to integrate Hubot with Grafana +id: integrate-hubot +categories: ['administration'] +tags: ['advanced'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +aliases: ['/docs/grafana/latest/tutorials/hubot_howto/'] +--- + +# Integrate Hubot with Grafana + +Grafana 2.0 shipped with a great feature that enables it to render any graph or panel to a PNG image. + +No matter what data source you are using, the PNG image of the Graph will look the same as it does in your browser. + +This guide will show you how to install and configure the [Hubot-Grafana](https://github.com/stephenyeargin/hubot-grafana) plugin. This plugin allows you to tell hubot to render any dashboard or graph right from a channel in Slack, Hipchat or Basecamp. The bot will respond with an image of the graph and a link that will take you to the graph. + +> _Amazon S3 Required_: The hubot-grafana script will upload the rendered graphs to Amazon S3. This +> is so Hipchat and Slack can show them reliably (they require the image to be publicly available). + +{{< figure src="/static/img/docs/tutorials/hubot_grafana.png" max-width="800px" >}} + +## What is Hubot? + +[Hubot](https://hubot.github.com/) is an universal and extensible chat bot that can be used with many chat services and has a huge library of third party plugins that allow you to automate anything from your chat rooms. + +## Install Hubot + +Hubot is very easy to install and host. If you do not already have a bot up and running please read the official [Getting Started With Hubot](https://hubot.github.com/docs/) guide. + +## Install Hubot-Grafana script + +In your Hubot project repo install the Grafana plugin using `npm`: + +```bash +npm install hubot-grafana --save +``` + +Edit the file external-scripts.json, and add hubot-grafana to the list of plugins. + +```json +["hubot-pugme", "hubot-shipit", "hubot-grafana"] +``` + +## Configure + +The `hubot-grafana` plugin requires a number of environment variables to be set in order to work properly. + +```bash +export HUBOT_GRAFANA_HOST=https://play.grafana.org +export HUBOT_GRAFANA_API_KEY=abcd01234deadbeef01234 +export HUBOT_GRAFANA_S3_BUCKET=mybucket +export HUBOT_GRAFANA_S3_ACCESS_KEY_ID=ABCDEF123456XYZ +export HUBOT_GRAFANA_S3_SECRET_ACCESS_KEY=aBcD01234dEaDbEef01234 +export HUBOT_GRAFANA_S3_PREFIX=graphs +export HUBOT_GRAFANA_S3_REGION=us-standard +``` + +### Grafana server side rendering + +The hubot plugin will take advantage of the Grafana server side rendering feature that can render any panel on the server using phantomjs. Grafana ships with a phantomjs binary (Linux only). + +To verify that this feature works try the `Direct link to rendered image` link in the panel share dialog. If you do not get an image when opening this link verify that the required font packages are installed for phantomjs to work. + +### Grafana API Key + +{{< figure src="/static/img/docs/v2/orgdropdown_api_keys.png" max-width="150px" class="docs-image--right">}} + +You need to set the environment variable `HUBOT_GRAFANA_API_KEY` to a Grafana API Key. You can add these from the API Keys page which you find in the Organization dropdown. + +### Amazon S3 + +The `S3` options are optional but for the images to work properly in services like Slack and Hipchat they need to publicly available. By specifying the `S3` options the hubot-grafana script will publish the rendered panel to `S3` and it will use that URL when it posts to Slack or Hipchat. + +## Hubot commands + +- `hubot graf list` + - Lists the available dashboards +- `hubot graf db graphite-carbon-metrics` + - Graph all panels in the dashboard +- `hubot graf db graphite-carbon-metrics:3` + - Graph only panel with id 3 of a particular dashboard +- `hubot graf db graphite-carbon-metrics:cpu` + - Graph only the panels containing "cpu" (case insensitive) in the title +- `hubot graf db graphite-carbon-metrics now-12hr` + - Get a dashboard with a window of 12 hours ago to now +- `hubot graf db graphite-carbon-metrics now-24hr now-12hr` + - Get a dashboard with a window of 24 hours ago to 12 hours ago +- `hubot graf db graphite-carbon-metrics:3 now-8d now-1d` + - Get only the third panel of a particular dashboard with a window of 8 days ago to yesterday +- `hubot graf db graphite-carbon-metrics host=carbon-a` + - Get a templated dashboard with the `$host` parameter set to `carbon-a` + +## Aliases + +Some of the hubot commands above can lengthy and you might have to remember the dashboard slug (url id). If you have a few favorite graphs you want to be able check up on often (let's say from your mobile) you can create hubot command aliases with the hubot script `hubot-alias`. + +Install it: + +```bash +npm i --save hubot-alias +``` + +Now add `hubot-alias` to the list of plugins in `external-scripts.json` and restart hubot. + +Now you can add an alias like this: + +- `hubot alias graf-lb=graf db loadbalancers:2 now-20m` + +{{< figure src="/static/img/docs/tutorials/hubot_grafana2.png" max-width="800px" >}} + +## Summary + +Grafana is going to ship with integrated Slack and Hipchat features some day but you do not have to wait for that. Grafana 2 shipped with a very clever server side rendering feature that can render any panel to a png using phantomjs. The hubot plugin for Grafana is something you can install and use today! diff --git a/docs/sources/tutorials/provision-dashboards-and-data-sources/index.md b/docs/sources/tutorials/provision-dashboards-and-data-sources/index.md new file mode 100644 index 00000000000..4554b67073f --- /dev/null +++ b/docs/sources/tutorials/provision-dashboards-and-data-sources/index.md @@ -0,0 +1,260 @@ +--- +title: Provision dashboards and data sources +summary: Treat your configuration as code. +description: Treat your configuration as code. +id: provision-dashboards-and-data-sources +categories: ['administration'] +tags: ['intermediate'] +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +weight: 40 +--- + +## Introduction + +Learn how you can reuse dashboards and data sources across multiple teams by provisioning Grafana from version-controlled configuration files. + +In this tutorial, you'll: + +- Provision dashboards. +- Provision data sources. + +{{% class "prerequisite-section" %}} + +### Prerequisites + +- Grafana 7.0 +- Administrator privileges on the system you are doing the tutorial on + {{% /class %}} + +## Configuration as code + +Configuration as code is the practice of storing the configuration of your system as a set of version controlled, human-readable configuration files, rather than in a database. These configuration files can be reused across environments to avoid duplicated resources. + +As the number of dashboards and data sources grows within your organization, manually managing changes can become tedious and error-prone. Encouraging reuse becomes important to avoid multiple teams redesigning the same dashboards. + +Grafana supports configuration as code through _provisioning_. The resources that currently supports provisioning are: + +- [Dashboards](/docs/grafana/latest/administration/provisioning/#dashboards) +- [Data sources](/docs/grafana/latest/administration/provisioning/#datasources) +- [Alert notification channels](/docs/grafana/latest/administration/provisioning/#alert-notification-channels) + +## Set the provisioning directory + +Before you can start provisioning resources, Grafana needs to know where to find the _provisioning directory_. The provisioning directory contains configuration files that are applied whenever Grafana starts and continuously updated while running. + +By default, Grafana looks for a provisioning directory in the configuration directory (grafana > conf) on the system where Grafana is installed. However, if you are a Grafana Administrator, then you might want to place the config files in a shared resource like a network folder, so you would need to change the path to the provisioning directory. + +You can set a different path by setting the `paths.provisioning` property in the main config file: + +```ini +[paths] +provisioning = +``` + +For more information about configuration files, refer to [Configuration](/docs/grafana/latest/installation/configuration/) in the [Grafana documentation](/docs/grafana/latest/). + +The provisioning directory assumes the following structure: + +``` +provisioning/ + datasources/ + + dashboards/ + + notifiers/ + +``` + +Next, we'll look at how to provision a data source. + +## Provision a data source + +Each data source provisioning config file contains a _manifest_ that specifies the desired state of a set of provisioned data sources. + +At startup, Grafana loads the configuration files and provisions the data sources listed in the manifests. + +Let's configure a [TestData DB](/docs/grafana/latest/features/datasources/testdata/) data source that you can use for your dashboards. + +#### Create a data source manifest + +1. In the `provisioning/datasources/` directory, create a file called `default.yaml` with the following content: + + ```yaml + apiVersion: 1 + + datasources: + - name: TestData DB + type: testdata + ``` + +1. Restart Grafana to load the new changes. +1. In the sidebar, hover the cursor over the **Configuration** (gear) icon and click **Data Sources**. The TestData DB appears in the list of data sources. + +> The configuration options can vary between different types of data sources. For more information on how to configure a specific data source, refer to [Data sources](/docs/grafana/latest/administration/provisioning/#datasources). + +## Provision a dashboard + +Each dashboard config file contains a manifest that specifies the desired state of a set of _dashboard providers_. + +A dashboard provider tells Grafana where to find the dashboard definitions and where to put them. + +Grafana regularly checks for changes to the dashboard definitions (by default every 10 seconds). + +Let's define a dashboard provider so that Grafana knows where to find the dashboards we want to provision. + +#### Define a dashboard provider + +In the `provisioning/dashboards/` directory, create a file called `default.yaml` with the following content: + +```yaml +apiVersion: 1 + +providers: + - name: Default # A uniquely identifiable name for the provider + folder: Services # The folder where to place the dashboards + type: file + options: + path: + + # Default path for Windows: C:/Program Files/GrafanaLabs/grafana/public/dashboards + # Default path for Linux is: /var/lib/grafana/dashboards +``` + +For more information on how to configure dashboard providers, refer to [Dashboards](/docs/grafana/latest/administration/provisioning/#dashboards). + +#### Create a dashboard definition + +1. In the dashboard definitions directory you specified in the dashboard provider, i.e. `options.path`, create a file called `cluster.json` with the following content: + + ```json + { + "__inputs": [], + "__requires": [], + "annotations": { + "list": [] + }, + "editable": false, + "gnetId": null, + "graphTooltip": 0, + "hideControls": false, + "id": null, + "links": [], + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "TestData DB", + "fill": 1, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 2, + "legend": { + "alignAsTable": false, + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "repeat": null, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "CPU Usage", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + } + ], + "refresh": "", + "rows": [], + "schemaVersion": 16, + "style": "dark", + "tags": ["kubernetes"], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], + "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + }, + "timezone": "browser", + "title": "Cluster", + "version": 0 + } + ``` + +1. Restart Grafana to provision the new dashboard or wait 10 seconds for Grafana to automatically create the dashboard. +1. In the sidebar, hover the cursor over **Dashboards** (squares) icon, and then click **Manage**. The dashboard appears in a **Services** folder. + +> If you don't specify an `id` in the dashboard definition, then Grafana assigns one during provisioning. You can set the `id` yourself if you want to reference the dashboard from other dashboards. Be careful to not use the same `id` for multiple dashboards, as this will cause a conflict. + +## Summary + +In this tutorial you learned how you to reuse dashboards and data sources across multiple teams by provisioning Grafana from version-controlled configuration files. + +Dashboard definitions can get unwieldy as more panels and configurations are added to them. There are a number of open source tools available to make it easier to manage dashboard definitions: + +- [grafana-dash-gen](https://github.com/uber/grafana-dash-gen) (Javascript) +- [grafanalib](https://github.com/weaveworks/grafanalib) (Python) +- [grafonnet-lib](https://github.com/grafana/grafonnet-lib) (Jsonnet) +- [grafyaml](https://docs.openstack.org/infra/grafyaml/) (YAML) + +### Learn more + +- [Provisioning Grafana](/docs/grafana/latest/administration/provisioning/) diff --git a/docs/sources/tutorials/run-grafana-behind-a-proxy/index.md b/docs/sources/tutorials/run-grafana-behind-a-proxy/index.md new file mode 100644 index 00000000000..10be2dee35f --- /dev/null +++ b/docs/sources/tutorials/run-grafana-behind-a-proxy/index.md @@ -0,0 +1,222 @@ +--- +title: Run Grafana behind a reverse proxy +summary: Learn how to run Grafana behind a reverse proxy +description: Learn how to run Grafana behind a reverse proxy +id: run-grafana-behind-a-proxy +categories: ['administration'] +tags: ['advanced'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +aliases: ['/docs/grafana/latest/installation/behind_proxy/'] +--- + +## Introduction + +In this tutorial, you'll configure Grafana to run behind a reverse proxy. + +When running Grafana behind a proxy, you need to configure the domain name to let Grafana know how to render links and redirects correctly. + +- In the Grafana configuration file, change `server.domain` to the domain name you'll be using: + +```bash +[server] +domain = example.com +``` + +- Restart Grafana for the new changes to take effect. + +You can also serve Grafana behind a _sub path_, such as `http://example.com/grafana`. + +To serve Grafana behind a sub path: + +- Include the sub path at the end of the `root_url`. +- Set `serve_from_sub_path` to `true`. + +```bash +[server] +domain = example.com +root_url = %(protocol)s://%(domain)s:%(http_port)s/grafana/ +serve_from_sub_path = true +``` + +Next, you need to configure your reverse proxy. + +## Configure NGINX + +[NGINX](https://www.nginx.com) is a high performance load balancer, web server, and reverse proxy. + +- In your NGINX configuration file inside `http` section, add the following: + +```nginx +# this is required to proxy Grafana Live WebSocket connections. +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +upstream grafana { + server localhost:3000; +} + +server { + listen 80; + root /usr/share/nginx/html; + index index.html index.htm; + + location / { + proxy_set_header Host $http_host; + proxy_pass http://grafana; + } + + # Proxy Grafana Live WebSocket connections. + location /api/live/ { + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $http_host; + proxy_pass http://grafana; + } +} +``` + +- Reload the NGINX configuration. +- Navigate to port 80 on the machine NGINX is running on. You're greeted by the Grafana login page. + +For Grafana Live which uses WebSocket connections you may have to raise Nginx [worker_connections](https://nginx.org/en/docs/ngx_core_module.html#worker_connections) option which is 512 by default – which limits the number of possible concurrent connections with Grafana Live. + +Also, be aware that the above configuration will work only when the `proxy_pass` value for `location /` is a literal string. If you are using a variable here, [read this GitHub issue](https://github.com/grafana/grafana/issues/18299). You will need to add [an appropriate NGINX rewrite rule](https://www.nginx.com/blog/creating-nginx-rewrite-rules/). + +To configure NGINX to serve Grafana under a _sub path_, update the `location` block: + +```nginx +# this is required to proxy Grafana Live WebSocket connections. +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +upstream grafana { + server localhost:3000; +} + +server { + listen 80; + root /usr/share/nginx/www; + index index.html index.htm; + + location /grafana/ { + rewrite ^/grafana/(.*) /$1 break; + proxy_set_header Host $http_host; + proxy_pass http://grafana; + } + + # Proxy Grafana Live WebSocket connections. + location /grafana/api/live/ { + rewrite ^/grafana/(.*) /$1 break; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $http_host; + proxy_pass http://grafana; + } +} +``` + +## Configure HAProxy + +To configure HAProxy to serve Grafana under a _sub path_: + +```bash +frontend http-in + bind *:80 + use_backend grafana_backend if { path /grafana } or { path_beg /grafana/ } + +backend grafana_backend + # Requires haproxy >= 1.6 + http-request set-path %[path,regsub(^/grafana/?,/)] + + # Works for haproxy < 1.6 + # reqrep ^([^\ ]*\ /)grafana[/]?(.*) \1\2 + + server grafana localhost:3000 +``` + +## Configure IIS + +> IIS requires that the URL Rewrite module is installed. + +To configure IIS to serve Grafana under a _sub path_, create an Inbound Rule for the parent website in IIS Manager with the following settings: + +- pattern: `grafana(/)?(.*)` +- check the `Ignore case` checkbox +- rewrite URL set to `http://localhost:3000/{R:2}` +- check the `Append query string` checkbox +- check the `Stop processing of subsequent rules` checkbox + +This is the rewrite rule that is generated in the `web.config`: + +```xml + + + + + + + + +``` + +See the [tutorial on IIS URL Rewrites](/tutorials/iis/) for more in-depth instructions. + +## Configure Traefik + +[Traefik](https://traefik.io/traefik/) Cloud Native Reverse Proxy / Load Balancer / Edge Router + +Using the docker provider the following labels will configure the router and service for a domain or subdomain routing. + +```yaml +labels: + traefik.http.routers.grafana.rule: Host(`grafana.example.com`) + traefik.http.services.grafana.loadbalancer.server.port: 3000 +``` + +To deploy on a _sub path_ + +```yaml +labels: + traefik.http.routers.grafana.rule: Host(`example.com`) && PathPrefix(`/grafana`) + traefik.http.services.grafana.loadbalancer.server.port: 3000 +``` + +Examples using the file provider. + +```yaml +http: + routers: + grafana: + rule: Host(`grafana.example.com`) + service: grafana + services: + grafana: + loadBalancer: + servers: + - url: http://192.168.30.10:3000 +``` + +```yaml +http: + routers: + grafana: + rule: Host(`example.com`) && PathPrefix(`/grafana`) + service: grafana + services: + grafana: + loadBalancer: + servers: + - url: http://192.168.30.10:3000 +``` + +## Summary + +In this tutorial you learned how to run Grafana behind a reverse proxy. diff --git a/docs/sources/tutorials/stream-metrics-from-telegraf-to-grafana/index.md b/docs/sources/tutorials/stream-metrics-from-telegraf-to-grafana/index.md new file mode 100644 index 00000000000..822006b2fcf --- /dev/null +++ b/docs/sources/tutorials/stream-metrics-from-telegraf-to-grafana/index.md @@ -0,0 +1,101 @@ +--- +title: Stream metrics from Telegraf to Grafana +summary: Use Telegraf to stream live metrics to Grafana. +description: Use Telegraf to stream live metrics to Grafana. +id: stream-metrics-from-telegraf-to-grafana +categories: ['administration'] +tags: ['beginner'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +weight: 75 +--- + +## Introduction + +Grafana v8 introduced streaming capabilities – a way to push data to UI panels in near real-time. In this tutorial we show how Grafana real-time streaming capabilities can be used together with Telegraf to instantly display system measurements. + +In this tutorial, you'll: + +- Setup Telegraf and output measurements directly to Grafana time-series panel in near real-time + +{{% class "prerequisite-section" %}} + +#### Prerequisites + +- Grafana 8.0+ +- Telegraf + {{% /class %}} + +## Run Grafana and create admin token + +1. Run Grafana following [installation instructions](/docs/grafana/latest/installation/) for your operating system +1. Log in and go to Configuration -> API Keys +1. Press "Add API key" button and create a new API token with **Admin** role + +## Configure and run Telegraf + +Telegraf is a plugin-driven server agent for collecting and sending metrics and events from databases, systems, and IoT sensors. + +You can install it following [official installation instructions](https://docs.influxdata.com/telegraf/latest/introduction/installation/). + +In this tutorial we will be using Telegraf HTTP output plugin to send metrics in Influx format to Grafana. We can use a configuration like this: + +``` +[agent] + interval = "1s" + flush_interval = "1s" + +[[inputs.cpu]] + percpu = false + totalcpu = true + +[[outputs.http]] + url = "http://localhost:3000/api/live/push/custom_stream_id" + data_format = "influx" + [outputs.http.headers] + Authorization = "Bearer " +``` + +Make sure to replace `` placeholder with your actual API key created in the previous step. Save this config into `telegraf.conf` file and run Telegraf pointing to this config file. Telegraf will periodically (once in a second) report the state of total CPU usage on a host to Grafana (which is supposed to be running on `http://localhost:3000`). Of course you can replace `custom_stream_id` to something more meaningful for your use case. + +Inside Grafana Influx data is converted to Grafana data frames and then frames are published to Grafana Live channels. In this case, the channel where CPU data will be published is `stream/custom_stream_id/cpu`. The `stream` scope is constant, the `custom_stream_id` namespace is the last part of API URL set in Telegraf configuration (`http://localhost:3000/api/live/push/telegraf`) and the path is `cpu` - the name of a measurement. + +The only thing left here is to create a dashboard with streaming data. + +## Create dashboard with streaming data + +1. Create new dashboard +1. Press Add empty panel +1. Select `-- Grafana --` datasource +1. Select `Live Measurements` query type +1. Find and select `stream/custom_stream_id/cpu` measurement for Channel field +1. Save dashboard changes + +After making these steps Grafana UI should subscribe to the channel `stream/custom_stream_id/cpu` and you should see CPU data updates coming from Telegraf in near real-time. + +## Stream using WebSocket endpoint + +If you aim for a high-frequency update sending then you may want to use the WebSocket output plugin of Telegraf (introduced in Telegraf v1.19.0) instead of the HTTP output plugin we used above. Configure WebSocket output plugin like this: + +``` +[agent] + interval = "500ms" + flush_interval = "500ms" + +[[inputs.cpu]] + percpu = false + totalcpu = true + +[[outputs.websocket]] + url = "ws://localhost:3000/api/live/push/custom_stream_id" + data_format = "influx" + [outputs.websocket.headers] + Authorization = "Bearer " +``` + +WebSocket avoids running all Grafana HTTP middleware on each request from Telegraf thus reducing Grafana backend CPU usage significantly. + +## Summary + +In this tutorial you learned how to use Telegraf to stream live metrics to Grafana. From 9256a520a41c72e1b2635b2a99b74069112776d5 Mon Sep 17 00:00:00 2001 From: Kristin Laemmert Date: Fri, 27 Jan 2023 13:36:54 -0500 Subject: [PATCH 063/117] chore: move user_auth models to (mostly) login service (#62269) * chore: move user_auth models to (mostly) login service --- pkg/api/admin_users.go | 5 +- pkg/api/ldap_debug.go | 23 ++- pkg/api/ldap_debug_test.go | 28 ++-- pkg/api/login.go | 13 +- pkg/api/login_oauth.go | 18 +-- pkg/api/login_test.go | 22 +-- pkg/api/org_users.go | 3 +- pkg/api/password.go | 2 +- pkg/api/pluginproxy/ds_proxy_test.go | 8 +- pkg/api/user.go | 6 +- pkg/api/user_test.go | 12 +- pkg/login/auth.go | 5 +- pkg/login/auth_test.go | 16 +- pkg/login/grafana_login.go | 4 +- pkg/login/grafana_login_test.go | 6 +- pkg/login/ldap_login.go | 7 +- pkg/login/ldap_login_test.go | 19 +-- pkg/middleware/middleware_test.go | 16 +- pkg/models/user_auth.go | 138 ------------------ pkg/services/auth/authtest/testing.go | 13 +- pkg/services/authn/authn.go | 8 +- .../authnimpl/sync/oauth_token_sync_test.go | 20 +-- .../authn/authnimpl/sync/org_sync_test.go | 9 +- .../authn/authnimpl/sync/user_sync.go | 11 +- .../authn/authnimpl/sync/user_sync_test.go | 40 ++--- pkg/services/authn/clients/grafana_test.go | 9 +- pkg/services/authn/clients/jwt_test.go | 4 +- pkg/services/authn/clients/ldap.go | 16 +- pkg/services/authn/clients/ldap_test.go | 22 +-- pkg/services/contexthandler/auth_jwt.go | 9 +- .../contexthandler/auth_proxy_test.go | 4 +- .../contexthandler/authproxy/authproxy.go | 11 +- pkg/services/contexthandler/contexthandler.go | 5 +- pkg/services/hooks/hooks.go | 6 +- pkg/services/ldap/ldap.go | 23 ++- pkg/services/ldap/ldap_groups.go | 6 +- pkg/services/ldap/ldap_login_test.go | 4 +- pkg/services/ldap/ldap_private_test.go | 8 +- pkg/services/ldap/model.go | 7 + pkg/services/login/authinfo.go | 13 +- .../authinfoservice/database/database.go | 29 ++-- pkg/services/login/authinfoservice/service.go | 29 ++-- .../login/authinfoservice/user_auth_test.go | 113 +++++++------- pkg/services/login/login.go | 5 +- .../login/loginservice/loginservice.go | 17 +-- .../login/loginservice/loginservice_mock.go | 5 +- .../login/loginservice/loginservice_test.go | 46 +++--- pkg/services/login/logintest/logintest.go | 25 ++-- pkg/services/login/model.go | 126 ++++++++++++++++ pkg/services/login/userprotection.go | 15 +- pkg/services/multildap/multidap_mock.go | 14 +- pkg/services/multildap/multildap.go | 20 +-- pkg/services/multildap/multildap_test.go | 36 ++--- pkg/services/oauthtoken/oauth_token.go | 27 ++-- pkg/services/oauthtoken/oauth_token_test.go | 50 +++---- .../oauthtoken/oauthtokentest/mock.go | 19 +-- .../oauthtokentest/oauthtokentest.go | 11 +- 57 files changed, 583 insertions(+), 603 deletions(-) delete mode 100644 pkg/models/user_auth.go create mode 100644 pkg/services/ldap/model.go diff --git a/pkg/api/admin_users.go b/pkg/api/admin_users.go index af03d961188..0a437904f9c 100644 --- a/pkg/api/admin_users.go +++ b/pkg/api/admin_users.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/auth" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util" @@ -293,7 +294,7 @@ func (hs *HTTPServer) AdminDisableUser(c *contextmodel.ReqContext) response.Resp } // External users shouldn't be disabled from API - authInfoQuery := &models.GetAuthInfoQuery{UserId: userID} + authInfoQuery := &login.GetAuthInfoQuery{UserId: userID} if err := hs.authInfoService.GetAuthInfo(c.Req.Context(), authInfoQuery); !errors.Is(err, user.ErrUserNotFound) { return response.Error(500, "Could not disable external user", nil) } @@ -336,7 +337,7 @@ func (hs *HTTPServer) AdminEnableUser(c *contextmodel.ReqContext) response.Respo } // External users shouldn't be disabled from API - authInfoQuery := &models.GetAuthInfoQuery{UserId: userID} + authInfoQuery := &login.GetAuthInfoQuery{UserId: userID} if err := hs.authInfoService.GetAuthInfo(c.Req.Context(), authInfoQuery); !errors.Is(err, user.ErrUserNotFound) { return response.Error(500, "Could not enable external user", nil) } diff --git a/pkg/api/ldap_debug.go b/pkg/api/ldap_debug.go index 501b303ca84..a6907ad7b93 100644 --- a/pkg/api/ldap_debug.go +++ b/pkg/api/ldap_debug.go @@ -10,7 +10,6 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/login" @@ -48,14 +47,14 @@ type LDAPRoleDTO struct { // LDAPUserDTO is a serializer for users mapped from LDAP type LDAPUserDTO struct { - Name *LDAPAttribute `json:"name"` - Surname *LDAPAttribute `json:"surname"` - Email *LDAPAttribute `json:"email"` - Username *LDAPAttribute `json:"login"` - IsGrafanaAdmin *bool `json:"isGrafanaAdmin"` - IsDisabled bool `json:"isDisabled"` - OrgRoles []LDAPRoleDTO `json:"roles"` - Teams []models.TeamOrgGroupDTO `json:"teams"` + Name *LDAPAttribute `json:"name"` + Surname *LDAPAttribute `json:"surname"` + Email *LDAPAttribute `json:"email"` + Username *LDAPAttribute `json:"login"` + IsGrafanaAdmin *bool `json:"isGrafanaAdmin"` + IsDisabled bool `json:"isDisabled"` + OrgRoles []LDAPRoleDTO `json:"roles"` + Teams []ldap.TeamOrgGroupDTO `json:"teams"` } // LDAPServerDTO is a serializer for LDAP server statuses @@ -223,7 +222,7 @@ func (hs *HTTPServer) PostSyncUserWithLDAP(c *contextmodel.ReqContext) response. return response.Error(500, "Failed to get user", err) } - authModuleQuery := &models.GetAuthInfoQuery{UserId: usr.ID, AuthModule: login.LDAPAuthModule} + authModuleQuery := &login.GetAuthInfoQuery{UserId: usr.ID, AuthModule: login.LDAPAuthModule} if err := hs.authInfoService.GetAuthInfo(c.Req.Context(), authModuleQuery); err != nil { // validate the userId comes from LDAP if errors.Is(err, user.ErrUserNotFound) { return response.Error(404, user.ErrUserNotFound.Error(), nil) @@ -260,11 +259,11 @@ func (hs *HTTPServer) PostSyncUserWithLDAP(c *contextmodel.ReqContext) response. return response.Error(http.StatusBadRequest, "Something went wrong while finding the user in LDAP", err) } - upsertCmd := &models.UpsertUserCommand{ + upsertCmd := &login.UpsertUserCommand{ ReqContext: c, ExternalUser: userInfo, SignupAllowed: hs.Cfg.LDAPAllowSignup, - UserLookupParams: models.UserLookupParams{ + UserLookupParams: login.UserLookupParams{ UserID: &usr.ID, // Upsert by ID only Email: nil, Login: nil, diff --git a/pkg/api/ldap_debug_test.go b/pkg/api/ldap_debug_test.go index cf969dc9822..5e4e51fd750 100644 --- a/pkg/api/ldap_debug_test.go +++ b/pkg/api/ldap_debug_test.go @@ -14,11 +14,11 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/db/dbtest" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/auth/authtest" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/ldap" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/login/loginservice" "github.com/grafana/grafana/pkg/services/login/logintest" "github.com/grafana/grafana/pkg/services/multildap" @@ -31,10 +31,10 @@ import ( ) type LDAPMock struct { - Results []*models.ExternalUserInfo + Results []*login.ExternalUserInfo } -var userSearchResult *models.ExternalUserInfo +var userSearchResult *login.ExternalUserInfo var userSearchConfig ldap.ServerConfig var userSearchError error var pingResult []*multildap.ServerStatus @@ -44,16 +44,16 @@ func (m *LDAPMock) Ping() ([]*multildap.ServerStatus, error) { return pingResult, pingError } -func (m *LDAPMock) Login(query *models.LoginUserQuery) (*models.ExternalUserInfo, error) { - return &models.ExternalUserInfo{}, nil +func (m *LDAPMock) Login(query *login.LoginUserQuery) (*login.ExternalUserInfo, error) { + return &login.ExternalUserInfo{}, nil } -func (m *LDAPMock) Users(logins []string) ([]*models.ExternalUserInfo, error) { - s := []*models.ExternalUserInfo{} +func (m *LDAPMock) Users(logins []string) ([]*login.ExternalUserInfo, error) { + s := []*login.ExternalUserInfo{} return s, nil } -func (m *LDAPMock) User(login string) (*models.ExternalUserInfo, ldap.ServerConfig, error) { +func (m *LDAPMock) User(login string) (*login.ExternalUserInfo, ldap.ServerConfig, error) { return userSearchResult, userSearchConfig, userSearchError } @@ -106,7 +106,7 @@ func TestGetUserFromLDAPAPIEndpoint_UserNotFound(t *testing.T) { func TestGetUserFromLDAPAPIEndpoint_OrgNotfound(t *testing.T) { isAdmin := true - userSearchResult = &models.ExternalUserInfo{ + userSearchResult = &login.ExternalUserInfo{ Name: "John Doe", Email: "john.doe@example.com", Login: "johndoe", @@ -161,7 +161,7 @@ func TestGetUserFromLDAPAPIEndpoint_OrgNotfound(t *testing.T) { func TestGetUserFromLDAPAPIEndpoint(t *testing.T) { isAdmin := true - userSearchResult = &models.ExternalUserInfo{ + userSearchResult = &login.ExternalUserInfo{ Name: "John Doe", Email: "john.doe@example.com", Login: "johndoe", @@ -236,7 +236,7 @@ func TestGetUserFromLDAPAPIEndpoint(t *testing.T) { func TestGetUserFromLDAPAPIEndpoint_WithTeamHandler(t *testing.T) { isAdmin := true - userSearchResult = &models.ExternalUserInfo{ + userSearchResult = &login.ExternalUserInfo{ Name: "John Doe", Email: "john.doe@example.com", Login: "johndoe", @@ -418,7 +418,7 @@ func TestPostSyncUserWithLDAPAPIEndpoint_Success(t *testing.T) { return &LDAPMock{} } - userSearchResult = &models.ExternalUserInfo{ + userSearchResult = &login.ExternalUserInfo{ Login: "ldap-daniel", } }, userServiceMock) @@ -487,7 +487,7 @@ func TestPostSyncUserWithLDAPAPIEndpoint_WhenUserNotInLDAP(t *testing.T) { userServiceMock := usertest.NewUserServiceFake() userServiceMock.ExpectedUser = &user.User{Login: "ldap-daniel", ID: 34} sc := postSyncUserWithLDAPContext(t, "/api/admin/ldap/sync/34", func(t *testing.T, sc *scenarioContext) { - sc.authInfoService.ExpectedExternalUser = &models.ExternalUserInfo{IsDisabled: true, UserId: 34} + sc.authInfoService.ExpectedExternalUser = &login.ExternalUserInfo{IsDisabled: true, UserId: 34} getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) { return &ldap.Config{}, nil } @@ -625,7 +625,7 @@ func TestLDAP_AccessControl(t *testing.T) { hs.authInfoService = &logintest.AuthInfoServiceFake{} }) // Add minimal setup to pass handler - userSearchResult = &models.ExternalUserInfo{} + userSearchResult = &login.ExternalUserInfo{} userSearchError = nil newLDAP = func(_ []*ldap.ServerConfig) multildap.IMultiLDAP { return &LDAPMock{} diff --git a/pkg/api/login.go b/pkg/api/login.go index 429f080339f..8d041941ff4 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -15,12 +15,11 @@ import ( "github.com/grafana/grafana/pkg/infra/network" "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/middleware/cookies" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/authn" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/featuremgmt" - loginService "github.com/grafana/grafana/pkg/services/login" + loginservice "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -236,7 +235,7 @@ func (hs *HTTPServer) LoginPost(c *contextmodel.ReqContext) response.Response { if err == nil && resp.ErrMessage() != "" { err = errors.New(resp.ErrMessage()) } - hs.HooksService.RunLoginHook(&models.LoginInfo{ + hs.HooksService.RunLoginHook(&loginservice.LoginInfo{ AuthModule: authModule, User: usr, LoginUsername: cmd.User, @@ -250,7 +249,7 @@ func (hs *HTTPServer) LoginPost(c *contextmodel.ReqContext) response.Response { return resp } - authQuery := &models.LoginUserQuery{ + authQuery := &loginservice.LoginUserQuery{ ReqContext: c, Username: cmd.User, Password: cmd.Password, @@ -327,7 +326,7 @@ func (hs *HTTPServer) loginUserWithUser(user *user.User, c *contextmodel.ReqCont } hs.log.Debug("Got IP address from client address", "addr", addr, "ip", ip) - ctx := context.WithValue(c.Req.Context(), models.RequestURIKey{}, c.Req.RequestURI) + ctx := context.WithValue(c.Req.Context(), loginservice.RequestURIKey{}, c.Req.RequestURI) userToken, err := hs.AuthTokenService.CreateToken(ctx, user, ip, c.Req.UserAgent()) if err != nil { return fmt.Errorf("%v: %w", "failed to create auth token", err) @@ -342,9 +341,9 @@ func (hs *HTTPServer) loginUserWithUser(user *user.User, c *contextmodel.ReqCont func (hs *HTTPServer) Logout(c *contextmodel.ReqContext) { // If SAML is enabled and this is a SAML user use saml logout if hs.samlSingleLogoutEnabled() { - getAuthQuery := models.GetAuthInfoQuery{UserId: c.UserID} + getAuthQuery := loginservice.GetAuthInfoQuery{UserId: c.UserID} if err := hs.authInfoService.GetAuthInfo(c.Req.Context(), &getAuthQuery); err == nil { - if getAuthQuery.Result.AuthModule == loginService.SAMLAuthModule { + if getAuthQuery.Result.AuthModule == loginservice.SAMLAuthModule { c.Redirect(hs.Cfg.AppSubURL + "/logout/saml") return } diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index 35fe2b156a5..f444615081b 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -17,8 +17,8 @@ import ( "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/middleware/cookies" - "github.com/grafana/grafana/pkg/models" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + loginservice "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -70,7 +70,7 @@ func genPKCECode() (string, string, error) { } func (hs *HTTPServer) OAuthLogin(ctx *contextmodel.ReqContext) { - loginInfo := models.LoginInfo{ + loginInfo := loginservice.LoginInfo{ AuthModule: "oauth", } name := web.Params(ctx.Req)[":name"] @@ -271,10 +271,10 @@ func (hs *HTTPServer) OAuthLogin(ctx *contextmodel.ReqContext) { } // buildExternalUserInfo returns a ExternalUserInfo struct from OAuth user profile -func (hs *HTTPServer) buildExternalUserInfo(token *oauth2.Token, userInfo *social.BasicUserInfo, name string) *models.ExternalUserInfo { +func (hs *HTTPServer) buildExternalUserInfo(token *oauth2.Token, userInfo *social.BasicUserInfo, name string) *loginservice.ExternalUserInfo { oauthLogger.Debug("Building external user info from OAuth user info") - extUser := &models.ExternalUserInfo{ + extUser := &loginservice.ExternalUserInfo{ AuthModule: fmt.Sprintf("oauth_%s", name), OAuthToken: token, AuthId: userInfo.Id, @@ -310,16 +310,16 @@ func (hs *HTTPServer) buildExternalUserInfo(token *oauth2.Token, userInfo *socia // SyncUser syncs a Grafana user profile with the corresponding OAuth profile. func (hs *HTTPServer) SyncUser( ctx *contextmodel.ReqContext, - extUser *models.ExternalUserInfo, + extUser *loginservice.ExternalUserInfo, connect social.SocialConnector, ) (*user.User, error) { oauthLogger.Debug("Syncing Grafana user with corresponding OAuth profile") // add/update user in Grafana - cmd := &models.UpsertUserCommand{ + cmd := &loginservice.UpsertUserCommand{ ReqContext: ctx, ExternalUser: extUser, SignupAllowed: connect.IsSignupAllowed(), - UserLookupParams: models.UserLookupParams{ + UserLookupParams: loginservice.UserLookupParams{ Email: &extUser.Email, UserID: nil, Login: nil, @@ -351,7 +351,7 @@ type LoginError struct { Err error } -func (hs *HTTPServer) handleOAuthLoginError(ctx *contextmodel.ReqContext, info models.LoginInfo, err LoginError) { +func (hs *HTTPServer) handleOAuthLoginError(ctx *contextmodel.ReqContext, info loginservice.LoginInfo, err LoginError) { ctx.Handle(hs.Cfg, err.HttpStatus, err.PublicMessage, err.Err) info.Error = err.Err @@ -363,7 +363,7 @@ func (hs *HTTPServer) handleOAuthLoginError(ctx *contextmodel.ReqContext, info m hs.HooksService.RunLoginHook(&info, ctx) } -func (hs *HTTPServer) handleOAuthLoginErrorWithRedirect(ctx *contextmodel.ReqContext, info models.LoginInfo, err error, v ...interface{}) { +func (hs *HTTPServer) handleOAuthLoginErrorWithRedirect(ctx *contextmodel.ReqContext, info loginservice.LoginInfo, err error, v ...interface{}) { hs.redirectWithError(ctx, err, v...) info.Error = err diff --git a/pkg/api/login_test.go b/pkg/api/login_test.go index 45fd7cef0ea..402d9d77b7e 100644 --- a/pkg/api/login_test.go +++ b/pkg/api/login_test.go @@ -12,6 +12,9 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" @@ -19,7 +22,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/login/social" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth/authtest" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -32,8 +34,6 @@ import ( secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func fakeSetIndexViewData(t *testing.T) { @@ -590,10 +590,10 @@ func setupAuthProxyLoginTest(t *testing.T, enableLoginToken bool) *scenarioConte } type loginHookTest struct { - info *models.LoginInfo + info *loginservice.LoginInfo } -func (r *loginHookTest) LoginHook(loginInfo *models.LoginInfo, req *contextmodel.ReqContext) { +func (r *loginHookTest) LoginHook(loginInfo *loginservice.LoginInfo, req *contextmodel.ReqContext) { r.info = loginInfo } @@ -629,12 +629,12 @@ func TestLoginPostRunLokingHook(t *testing.T) { authUser *user.User authModule string authErr error - info models.LoginInfo + info loginservice.LoginInfo }{ { desc: "invalid credentials", authErr: login.ErrInvalidCredentials, - info: models.LoginInfo{ + info: loginservice.LoginInfo{ AuthModule: "", HTTPStatus: 401, Error: login.ErrInvalidCredentials, @@ -643,7 +643,7 @@ func TestLoginPostRunLokingHook(t *testing.T) { { desc: "user disabled", authErr: login.ErrUserDisabled, - info: models.LoginInfo{ + info: loginservice.LoginInfo{ AuthModule: "", HTTPStatus: 401, Error: login.ErrUserDisabled, @@ -653,7 +653,7 @@ func TestLoginPostRunLokingHook(t *testing.T) { desc: "valid Grafana user", authUser: testUser, authModule: "grafana", - info: models.LoginInfo{ + info: loginservice.LoginInfo{ AuthModule: "grafana", User: testUser, HTTPStatus: 200, @@ -663,7 +663,7 @@ func TestLoginPostRunLokingHook(t *testing.T) { desc: "valid LDAP user", authUser: testUser, authModule: loginservice.LDAPAuthModule, - info: models.LoginInfo{ + info: loginservice.LoginInfo{ AuthModule: loginservice.LDAPAuthModule, User: testUser, HTTPStatus: 200, @@ -726,7 +726,7 @@ type fakeAuthenticator struct { ExpectedError error } -func (fa *fakeAuthenticator) AuthenticateUser(c context.Context, query *models.LoginUserQuery) error { +func (fa *fakeAuthenticator) AuthenticateUser(c context.Context, query *loginservice.LoginUserQuery) error { query.User = fa.ExpectedUser query.AuthModule = fa.ExpectedAuthModule return fa.ExpectedError diff --git a/pkg/api/org_users.go b/pkg/api/org_users.go index 264c833d77b..b0c7c807cc0 100644 --- a/pkg/api/org_users.go +++ b/pkg/api/org_users.go @@ -9,7 +9,6 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/login" @@ -300,7 +299,7 @@ func (hs *HTTPServer) searchOrgUsersHelper(c *contextmodel.ReqContext, query *or filteredUsers = append(filteredUsers, user) } - modules, err := hs.authInfoService.GetUserLabels(c.Req.Context(), models.GetUserLabelsQuery{ + modules, err := hs.authInfoService.GetUserLabels(c.Req.Context(), login.GetUserLabelsQuery{ UserIDs: authLabelsUserIDs, }) diff --git a/pkg/api/password.go b/pkg/api/password.go index 585c11f2909..80753bad9c7 100644 --- a/pkg/api/password.go +++ b/pkg/api/password.go @@ -39,7 +39,7 @@ func (hs *HTTPServer) SendResetPasswordEmail(c *contextmodel.ReqContext) respons return response.Error(http.StatusOK, "Email sent", nil) } - getAuthQuery := models.GetAuthInfoQuery{UserId: usr.ID} + getAuthQuery := login.GetAuthInfoQuery{UserId: usr.ID} if err := hs.authInfoService.GetAuthInfo(c.Req.Context(), &getAuthQuery); err == nil { authModule := getAuthQuery.Result.AuthModule if authModule == login.LDAPAuthModule || authModule == login.AuthProxyAuthModule { diff --git a/pkg/api/pluginproxy/ds_proxy_test.go b/pkg/api/pluginproxy/ds_proxy_test.go index ff085671da7..bf9fa018d77 100644 --- a/pkg/api/pluginproxy/ds_proxy_test.go +++ b/pkg/api/pluginproxy/ds_proxy_test.go @@ -25,13 +25,13 @@ import ( "github.com/grafana/grafana/pkg/infra/httpclient" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" datasourceservice "github.com/grafana/grafana/pkg/services/datasources/service" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/oauthtoken" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/quota/quotatest" @@ -1126,14 +1126,14 @@ func (m *mockOAuthTokenService) IsOAuthPassThruEnabled(ds *datasources.DataSourc return m.oAuthEnabled } -func (m *mockOAuthTokenService) HasOAuthEntry(context.Context, *user.SignedInUser) (*models.UserAuth, bool, error) { +func (m *mockOAuthTokenService) HasOAuthEntry(context.Context, *user.SignedInUser) (*login.UserAuth, bool, error) { return nil, false, nil } -func (m *mockOAuthTokenService) TryTokenRefresh(context.Context, *models.UserAuth) error { +func (m *mockOAuthTokenService) TryTokenRefresh(context.Context, *login.UserAuth) error { return nil } -func (m *mockOAuthTokenService) InvalidateOAuthTokens(context.Context, *models.UserAuth) error { +func (m *mockOAuthTokenService) InvalidateOAuthTokens(context.Context, *login.UserAuth) error { return nil } diff --git a/pkg/api/user.go b/pkg/api/user.go index 90f5bb152af..997b86881fa 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -63,7 +63,7 @@ func (hs *HTTPServer) getUserUserProfile(c *contextmodel.ReqContext, userID int6 return response.Error(500, "Failed to get user", err) } - getAuthQuery := models.GetAuthInfoQuery{UserId: userID} + getAuthQuery := login.GetAuthInfoQuery{UserId: userID} userProfile.AuthLabels = []string{} if err := hs.authInfoService.GetAuthInfo(c.Req.Context(), &getAuthQuery); err == nil { authLabel := login.GetAuthProviderLabel(getAuthQuery.Result.AuthModule) @@ -224,7 +224,7 @@ func (hs *HTTPServer) handleUpdateUser(ctx context.Context, cmd user.UpdateUserC } func (hs *HTTPServer) isExternalUser(ctx context.Context, userID int64) (bool, error) { - getAuthQuery := models.GetAuthInfoQuery{UserId: userID} + getAuthQuery := login.GetAuthInfoQuery{UserId: userID} var err error if err = hs.authInfoService.GetAuthInfo(ctx, &getAuthQuery); err == nil { return true, nil @@ -434,7 +434,7 @@ func (hs *HTTPServer) ChangeUserPassword(c *contextmodel.ReqContext) response.Re return response.Error(500, "Could not read user from database", err) } - getAuthQuery := models.GetAuthInfoQuery{UserId: user.ID} + getAuthQuery := login.GetAuthInfoQuery{UserId: user.ID} if err := hs.authInfoService.GetAuthInfo(c.Req.Context(), &getAuthQuery); err == nil { authModule := getAuthQuery.Result.AuthModule if authModule == login.LDAPAuthModule || authModule == login.AuthProxyAuthModule { diff --git a/pkg/api/user_test.go b/pkg/api/user_test.go index 8ef586ccf21..50be2a76ed1 100644 --- a/pkg/api/user_test.go +++ b/pkg/api/user_test.go @@ -19,9 +19,9 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/usagestats" - "github.com/grafana/grafana/pkg/models" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/login/authinfoservice" authinfostore "github.com/grafana/grafana/pkg/services/login/authinfoservice/database" "github.com/grafana/grafana/pkg/services/login/logintest" @@ -90,9 +90,9 @@ func TestUserAPIEndpoint_userLoggedIn(t *testing.T) { } idToken := "testidtoken" token = token.WithExtra(map[string]interface{}{"id_token": idToken}) - login := "loginuser" - query := &models.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test", UserLookupParams: models.UserLookupParams{Login: &login}} - cmd := &models.UpdateAuthInfoCommand{ + userlogin := "loginuser" + query := &login.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test", UserLookupParams: login.UserLookupParams{Login: &userlogin}} + cmd := &login.UpdateAuthInfoCommand{ UserId: usr.ID, AuthId: query.AuthId, AuthModule: query.AuthModule, @@ -234,7 +234,7 @@ func TestHTTPServer_UpdateUser(t *testing.T) { routePattern: "/api/users/:id", cmd: updateUserCommand, fn: func(sc *scenarioContext) { - sc.authInfoService.ExpectedUserAuth = &models.UserAuth{} + sc.authInfoService.ExpectedUserAuth = &login.UserAuth{} sc.fakeReqWithParams("PUT", sc.url, map[string]string{"id": "1"}).exec() assert.Equal(t, 403, sc.resp.Code) }, @@ -295,7 +295,7 @@ func TestHTTPServer_UpdateSignedInUser(t *testing.T) { routePattern: "/api/users/", cmd: updateUserCommand, fn: func(sc *scenarioContext) { - sc.authInfoService.ExpectedUserAuth = &models.UserAuth{} + sc.authInfoService.ExpectedUserAuth = &login.UserAuth{} sc.fakeReqWithParams("PUT", sc.url, map[string]string{"id": "1"}).exec() assert.Equal(t, 403, sc.resp.Code) }, diff --git a/pkg/login/auth.go b/pkg/login/auth.go index 382b4b4f288..0a22319f0f9 100644 --- a/pkg/login/auth.go +++ b/pkg/login/auth.go @@ -6,7 +6,6 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/loginattempt" @@ -30,7 +29,7 @@ var ( var loginLogger = log.New("login") type Authenticator interface { - AuthenticateUser(context.Context, *models.LoginUserQuery) error + AuthenticateUser(context.Context, *login.LoginUserQuery) error } type AuthenticatorService struct { @@ -49,7 +48,7 @@ func ProvideService(store db.DB, loginService login.Service, loginAttemptService } // AuthenticateUser authenticates the user via username & password -func (a *AuthenticatorService) AuthenticateUser(ctx context.Context, query *models.LoginUserQuery) error { +func (a *AuthenticatorService) AuthenticateUser(ctx context.Context, query *login.LoginUserQuery) error { ok, err := a.loginAttemptService.Validate(ctx, query.Username) if err != nil { return err diff --git a/pkg/login/auth_test.go b/pkg/login/auth_test.go index edc8f4ed3d8..8720c6e0ea4 100644 --- a/pkg/login/auth_test.go +++ b/pkg/login/auth_test.go @@ -5,15 +5,15 @@ import ( "errors" "testing" - "github.com/grafana/grafana/pkg/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/login/logintest" "github.com/grafana/grafana/pkg/services/loginattempt/loginattempttest" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestAuthenticateUser(t *testing.T) { @@ -23,7 +23,7 @@ func TestAuthenticateUser(t *testing.T) { loginAttemptService := &loginattempttest.FakeLoginAttemptService{ExpectedValid: true} a := AuthenticatorService{loginAttemptService: loginAttemptService, loginService: &logintest.LoginServiceFake{}} - err := a.AuthenticateUser(context.Background(), &models.LoginUserQuery{ + err := a.AuthenticateUser(context.Background(), &login.LoginUserQuery{ Username: "user", Password: "", }) @@ -180,7 +180,7 @@ func TestAuthenticateUser(t *testing.T) { } type authScenarioContext struct { - loginUserQuery *models.LoginUserQuery + loginUserQuery *login.LoginUserQuery grafanaLoginWasCalled bool ldapLoginWasCalled bool } @@ -188,14 +188,14 @@ type authScenarioContext struct { type authScenarioFunc func(sc *authScenarioContext) func mockLoginUsingGrafanaDB(err error, sc *authScenarioContext) { - loginUsingGrafanaDB = func(ctx context.Context, query *models.LoginUserQuery, _ user.Service) error { + loginUsingGrafanaDB = func(ctx context.Context, query *login.LoginUserQuery, _ user.Service) error { sc.grafanaLoginWasCalled = true return err } } func mockLoginUsingLDAP(enabled bool, err error, sc *authScenarioContext) { - loginUsingLDAP = func(ctx context.Context, query *models.LoginUserQuery, _ login.Service) (bool, error) { + loginUsingLDAP = func(ctx context.Context, query *login.LoginUserQuery, _ login.Service) (bool, error) { sc.ldapLoginWasCalled = true return enabled, err } @@ -209,7 +209,7 @@ func authScenario(t *testing.T, desc string, fn authScenarioFunc) { origLoginUsingLDAP := loginUsingLDAP cfg := setting.Cfg{DisableLogin: false} sc := &authScenarioContext{ - loginUserQuery: &models.LoginUserQuery{ + loginUserQuery: &login.LoginUserQuery{ Username: "user", Password: "pwd", IpAddress: "192.168.1.1:56433", diff --git a/pkg/login/grafana_login.go b/pkg/login/grafana_login.go index 64bc19362e1..3ef14a39f79 100644 --- a/pkg/login/grafana_login.go +++ b/pkg/login/grafana_login.go @@ -4,7 +4,7 @@ import ( "context" "crypto/subtle" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util" ) @@ -21,7 +21,7 @@ var validatePassword = func(providedPassword string, userPassword string, userSa return nil } -var loginUsingGrafanaDB = func(ctx context.Context, query *models.LoginUserQuery, userService user.Service) error { +var loginUsingGrafanaDB = func(ctx context.Context, query *login.LoginUserQuery, userService user.Service) error { userQuery := user.GetUserByLoginQuery{LoginOrEmail: query.Username} user, err := userService.GetByLogin(ctx, &userQuery) diff --git a/pkg/login/grafana_login_test.go b/pkg/login/grafana_login_test.go index 531f6a1dcc3..cd68019de97 100644 --- a/pkg/login/grafana_login_test.go +++ b/pkg/login/grafana_login_test.go @@ -9,7 +9,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db/dbtest" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/usertest" ) @@ -59,7 +59,7 @@ func TestLoginUsingGrafanaDB(t *testing.T) { type grafanaLoginScenarioContext struct { store db.DB userService *usertest.FakeUserService - loginUserQuery *models.LoginUserQuery + loginUserQuery *login.LoginUserQuery validatePasswordCalled bool } @@ -73,7 +73,7 @@ func grafanaLoginScenario(t *testing.T, desc string, fn grafanaLoginScenarioFunc sc := &grafanaLoginScenarioContext{ store: dbtest.NewFakeDB(), - loginUserQuery: &models.LoginUserQuery{ + loginUserQuery: &login.LoginUserQuery{ Username: "user", Password: "pwd", IpAddress: "192.168.1.1:56433", diff --git a/pkg/login/ldap_login.go b/pkg/login/ldap_login.go index bf2b23a9e24..a2f0b4f3321 100644 --- a/pkg/login/ldap_login.go +++ b/pkg/login/ldap_login.go @@ -6,7 +6,6 @@ import ( "fmt" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/multildap" @@ -27,7 +26,7 @@ var ldapLogger = log.New("login.ldap") // loginUsingLDAP logs in user using LDAP. It returns whether LDAP is enabled and optional error and query arg will be // populated with the logged in user if successful. -var loginUsingLDAP = func(ctx context.Context, query *models.LoginUserQuery, loginService login.Service) (bool, error) { +var loginUsingLDAP = func(ctx context.Context, query *login.LoginUserQuery, loginService login.Service) (bool, error) { enabled := isLDAPEnabled() if !enabled { @@ -54,11 +53,11 @@ var loginUsingLDAP = func(ctx context.Context, query *models.LoginUserQuery, log return true, err } - upsert := &models.UpsertUserCommand{ + upsert := &login.UpsertUserCommand{ ReqContext: query.ReqContext, ExternalUser: externalUser, SignupAllowed: setting.LDAPAllowSignup, - UserLookupParams: models.UserLookupParams{ + UserLookupParams: login.UserLookupParams{ Login: &externalUser.Login, Email: &externalUser.Email, UserID: nil, diff --git a/pkg/login/ldap_login_test.go b/pkg/login/ldap_login_test.go index 4503b272e92..ac4dc47205c 100644 --- a/pkg/login/ldap_login_test.go +++ b/pkg/login/ldap_login_test.go @@ -5,13 +5,14 @@ import ( "errors" "testing" - "github.com/grafana/grafana/pkg/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/ldap" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/login/logintest" "github.com/grafana/grafana/pkg/services/multildap" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) var errTest = errors.New("test error") @@ -62,8 +63,8 @@ func (auth *mockAuth) Ping() ([]*multildap.ServerStatus, error) { return nil, nil } -func (auth *mockAuth) Login(query *models.LoginUserQuery) ( - *models.ExternalUserInfo, +func (auth *mockAuth) Login(query *login.LoginUserQuery) ( + *login.ExternalUserInfo, error, ) { auth.loginCalled = true @@ -76,14 +77,14 @@ func (auth *mockAuth) Login(query *models.LoginUserQuery) ( } func (auth *mockAuth) Users(logins []string) ( - []*models.ExternalUserInfo, + []*login.ExternalUserInfo, error, ) { return nil, nil } func (auth *mockAuth) User(login string) ( - *models.ExternalUserInfo, + *login.ExternalUserInfo, ldap.ServerConfig, error, ) { @@ -111,7 +112,7 @@ func mockLDAPAuthenticator(valid bool) *mockAuth { } type LDAPLoginScenarioContext struct { - loginUserQuery *models.LoginUserQuery + loginUserQuery *login.LoginUserQuery LDAPAuthenticatorMock *mockAuth } @@ -124,7 +125,7 @@ func LDAPLoginScenario(t *testing.T, desc string, fn LDAPLoginScenarioFunc) { mock := &mockAuth{} sc := &LDAPLoginScenarioContext{ - loginUserQuery: &models.LoginUserQuery{ + loginUserQuery: &login.LoginUserQuery{ Username: "user", Password: "pwd", IpAddress: "192.168.1.1:56433", diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 5aeda6a8a83..ac6d530749d 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -24,7 +24,6 @@ import ( "github.com/grafana/grafana/pkg/infra/remotecache" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/login" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/apikey/apikeytest" "github.com/grafana/grafana/pkg/services/auth" @@ -35,6 +34,7 @@ import ( "github.com/grafana/grafana/pkg/services/contexthandler/authproxy" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/featuremgmt" + loginsvc "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/login/loginservice" "github.com/grafana/grafana/pkg/services/login/logintest" "github.com/grafana/grafana/pkg/services/navtree" @@ -437,7 +437,7 @@ func TestMiddlewareContext(t *testing.T) { sc.withTokenSessionCookie("token") sc.userService.ExpectedSignedInUser = &user.SignedInUser{OrgID: 2, UserID: userID} - sc.oauthTokenService.ExpectedAuthUser = &models.UserAuth{UserId: userID, OAuthExpiry: fakeGetTime()().Add(11 * time.Second)} + sc.oauthTokenService.ExpectedAuthUser = &loginsvc.UserAuth{UserId: userID, OAuthExpiry: fakeGetTime()().Add(11 * time.Second)} sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*auth.UserToken, error) { return &auth.UserToken{ @@ -465,7 +465,7 @@ func TestMiddlewareContext(t *testing.T) { sc.withTokenSessionCookie("token") signedInUser := &user.SignedInUser{OrgID: 2, UserID: userID} sc.userService.ExpectedSignedInUser = signedInUser - sc.oauthTokenService.ExpectedAuthUser = &models.UserAuth{ + sc.oauthTokenService.ExpectedAuthUser = &loginsvc.UserAuth{ UserId: userID, OAuthExpiry: fakeGetTime()().Add(-1 * time.Second), OAuthAccessToken: "access_token", @@ -500,7 +500,7 @@ func TestMiddlewareContext(t *testing.T) { sc.withTokenSessionCookie("token") sc.userService.ExpectedSignedInUser = &user.SignedInUser{OrgID: 2, UserID: userID} - sc.oauthTokenService.ExpectedAuthUser = &models.UserAuth{UserId: userID, OAuthExpiry: fakeGetTime()().Add(-5 * time.Second), OAuthRefreshToken: "refreshtoken"} + sc.oauthTokenService.ExpectedAuthUser = &loginsvc.UserAuth{UserId: userID, OAuthExpiry: fakeGetTime()().Add(-5 * time.Second), OAuthRefreshToken: "refreshtoken"} sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*auth.UserToken, error) { return &auth.UserToken{ @@ -527,7 +527,7 @@ func TestMiddlewareContext(t *testing.T) { sc.withTokenSessionCookie("token") sc.userService.ExpectedSignedInUser = &user.SignedInUser{OrgID: 2, UserID: userID} - sc.oauthTokenService.ExpectedAuthUser = &models.UserAuth{UserId: userID} + sc.oauthTokenService.ExpectedAuthUser = &loginsvc.UserAuth{UserId: userID} sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*auth.UserToken, error) { return &auth.UserToken{ @@ -610,7 +610,7 @@ func TestMiddlewareContext(t *testing.T) { middlewareScenario(t, "Should respect auto signup option", func(t *testing.T, sc *scenarioContext) { var actualAuthProxyAutoSignUp *bool = nil - sc.loginService.ExpectedUserFunc = func(cmd *models.UpsertUserCommand) *user.User { + sc.loginService.ExpectedUserFunc = func(cmd *loginsvc.UpsertUserCommand) *user.User { actualAuthProxyAutoSignUp = &cmd.SignupAllowed return nil } @@ -652,7 +652,7 @@ func TestMiddlewareContext(t *testing.T) { middlewareScenario(t, "Should assign role from header to default org", func(t *testing.T, sc *scenarioContext) { var storedRoleInfo map[int64]org.RoleType = nil - sc.loginService.ExpectedUserFunc = func(cmd *models.UpsertUserCommand) *user.User { + sc.loginService.ExpectedUserFunc = func(cmd *loginsvc.UpsertUserCommand) *user.User { storedRoleInfo = cmd.ExternalUser.OrgRoles sc.userService.ExpectedSignedInUser = &user.SignedInUser{OrgID: defaultOrgId, UserID: userID, OrgRole: storedRoleInfo[defaultOrgId]} return &user.User{ID: userID} @@ -675,7 +675,7 @@ func TestMiddlewareContext(t *testing.T) { middlewareScenario(t, "Should NOT assign role from header to non-default org", func(t *testing.T, sc *scenarioContext) { var storedRoleInfo map[int64]org.RoleType = nil - sc.loginService.ExpectedUserFunc = func(cmd *models.UpsertUserCommand) *user.User { + sc.loginService.ExpectedUserFunc = func(cmd *loginsvc.UpsertUserCommand) *user.User { storedRoleInfo = cmd.ExternalUser.OrgRoles sc.userService.ExpectedSignedInUser = &user.SignedInUser{OrgID: orgID, UserID: userID, OrgRole: storedRoleInfo[orgID]} return &user.User{ID: userID} diff --git a/pkg/models/user_auth.go b/pkg/models/user_auth.go deleted file mode 100644 index 7b2697778dc..00000000000 --- a/pkg/models/user_auth.go +++ /dev/null @@ -1,138 +0,0 @@ -package models - -import ( - "fmt" - "time" - - contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" - "github.com/grafana/grafana/pkg/services/org" - "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/setting" - - "golang.org/x/oauth2" -) - -type UserAuth struct { - Id int64 - UserId int64 - AuthModule string - AuthId string - Created time.Time - OAuthAccessToken string - OAuthRefreshToken string - OAuthIdToken string - OAuthTokenType string - OAuthExpiry time.Time -} - -type ExternalUserInfo struct { - OAuthToken *oauth2.Token - AuthModule string - AuthId string - UserId int64 - Email string - Login string - Name string - Groups []string - OrgRoles map[int64]org.RoleType - IsGrafanaAdmin *bool // This is a pointer to know if we should sync this or not (nil = ignore sync) - IsDisabled bool - SkipTeamSync bool -} - -func (e *ExternalUserInfo) String() string { - return fmt.Sprintf("%+v", *e) -} - -type LoginInfo struct { - AuthModule string - User *user.User - ExternalUser ExternalUserInfo - LoginUsername string - HTTPStatus int - Error error -} - -// RequestURIKey is used as key to save request URI in contexts -// (used for the Enterprise auditing feature) -type RequestURIKey struct{} - -// --------------------- -// COMMANDS - -type UpsertUserCommand struct { - ReqContext *contextmodel.ReqContext - ExternalUser *ExternalUserInfo - UserLookupParams - SignupAllowed bool - - Result *user.User -} - -type SetAuthInfoCommand struct { - AuthModule string - AuthId string - UserId int64 - OAuthToken *oauth2.Token -} - -type UpdateAuthInfoCommand struct { - AuthModule string - AuthId string - UserId int64 - OAuthToken *oauth2.Token -} - -type DeleteAuthInfoCommand struct { - UserAuth *UserAuth -} - -// ---------------------- -// QUERIES - -type LoginUserQuery struct { - ReqContext *contextmodel.ReqContext - Username string - Password string - User *user.User - IpAddress string - AuthModule string - Cfg *setting.Cfg -} - -type GetUserByAuthInfoQuery struct { - AuthModule string - AuthId string - UserLookupParams -} - -type UserLookupParams struct { - // Describes lookup order as well - UserID *int64 // if set, will try to find the user by id - Email *string // if set, will try to find the user by email - Login *string // if set, will try to find the user by login -} - -type GetExternalUserInfoByLoginQuery struct { - LoginOrEmail string - - Result *ExternalUserInfo -} - -type GetAuthInfoQuery struct { - UserId int64 - AuthModule string - AuthId string - - Result *UserAuth -} - -type GetUserLabelsQuery struct { - UserIDs []int64 -} - -type TeamOrgGroupDTO struct { - TeamName string `json:"teamName"` - OrgName string `json:"orgName"` - GroupDN string `json:"groupDN"` -} diff --git a/pkg/services/auth/authtest/testing.go b/pkg/services/auth/authtest/testing.go index d2bbd09b46b..6a924c5ebf4 100644 --- a/pkg/services/auth/authtest/testing.go +++ b/pkg/services/auth/authtest/testing.go @@ -5,11 +5,12 @@ import ( "net" "time" - "github.com/grafana/grafana/pkg/models" + "golang.org/x/oauth2" + "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/user" - "golang.org/x/oauth2" ) type FakeUserAuthTokenService struct { @@ -112,7 +113,7 @@ func (s *FakeUserAuthTokenService) BatchRevokeAllUserTokens(ctx context.Context, type FakeOAuthTokenService struct { passThruEnabled bool - ExpectedAuthUser *models.UserAuth + ExpectedAuthUser *login.UserAuth ExpectedErrors map[string]error } @@ -129,7 +130,7 @@ func (ts *FakeOAuthTokenService) IsOAuthPassThruEnabled(*datasources.DataSource) return ts.passThruEnabled } -func (ts *FakeOAuthTokenService) HasOAuthEntry(context.Context, *user.SignedInUser) (*models.UserAuth, bool, error) { +func (ts *FakeOAuthTokenService) HasOAuthEntry(context.Context, *user.SignedInUser) (*login.UserAuth, bool, error) { if ts.ExpectedAuthUser != nil { return ts.ExpectedAuthUser, true, nil } @@ -139,14 +140,14 @@ func (ts *FakeOAuthTokenService) HasOAuthEntry(context.Context, *user.SignedInUs return nil, false, nil } -func (ts *FakeOAuthTokenService) InvalidateOAuthTokens(ctx context.Context, usr *models.UserAuth) error { +func (ts *FakeOAuthTokenService) InvalidateOAuthTokens(ctx context.Context, usr *login.UserAuth) error { ts.ExpectedAuthUser.OAuthAccessToken = "" ts.ExpectedAuthUser.OAuthRefreshToken = "" ts.ExpectedAuthUser.OAuthExpiry = time.Time{} return nil } -func (ts *FakeOAuthTokenService) TryTokenRefresh(ctx context.Context, usr *models.UserAuth) error { +func (ts *FakeOAuthTokenService) TryTokenRefresh(ctx context.Context, usr *login.UserAuth) error { if err, ok := ts.ExpectedErrors["TryTokenRefresh"]; ok { return err } diff --git a/pkg/services/authn/authn.go b/pkg/services/authn/authn.go index a6c435259c4..64522ff26b5 100644 --- a/pkg/services/authn/authn.go +++ b/pkg/services/authn/authn.go @@ -10,8 +10,8 @@ import ( "golang.org/x/oauth2" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/web" @@ -45,7 +45,7 @@ type ClientParams struct { // EnableDisabledUsers is a hint to the auth service that it should reenable disabled users EnableDisabledUsers bool // LookUpParams are the arguments used to look up the entity in the DB. - LookUpParams models.UserLookupParams + LookUpParams login.UserLookupParams } type PostAuthHookFn func(ctx context.Context, identity *Identity, r *Request) error @@ -247,9 +247,9 @@ func (i *Identity) SignedInUser() *user.SignedInUser { return u } -func (i *Identity) ExternalUserInfo() models.ExternalUserInfo { +func (i *Identity) ExternalUserInfo() login.ExternalUserInfo { _, id := i.NamespacedID() - return models.ExternalUserInfo{ + return login.ExternalUserInfo{ OAuthToken: i.OAuthToken, AuthModule: i.AuthModule, AuthId: i.AuthID, diff --git a/pkg/services/authn/authnimpl/sync/oauth_token_sync_test.go b/pkg/services/authn/authnimpl/sync/oauth_token_sync_test.go index 27a06408674..a180cab31a1 100644 --- a/pkg/services/authn/authnimpl/sync/oauth_token_sync_test.go +++ b/pkg/services/authn/authnimpl/sync/oauth_token_sync_test.go @@ -9,10 +9,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth/authtest" "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/oauthtoken/oauthtokentest" "github.com/grafana/grafana/pkg/services/user" ) @@ -22,7 +22,7 @@ func TestOauthTokenSync_SyncOauthToken(t *testing.T) { desc string identity *authn.Identity - expectedHasEntryToken *models.UserAuth + expectedHasEntryToken *login.UserAuth expectHasEntryCalled bool expectedTryRefreshErr error @@ -52,26 +52,26 @@ func TestOauthTokenSync_SyncOauthToken(t *testing.T) { desc: "should skip sync for when access token don't have expire time", identity: &authn.Identity{ID: "user:1", SessionToken: &auth.UserToken{}}, expectHasEntryCalled: true, - expectedHasEntryToken: &models.UserAuth{}, + expectedHasEntryToken: &login.UserAuth{}, }, { desc: "should skip sync when access token has no expired yet", identity: &authn.Identity{ID: "user:1", SessionToken: &auth.UserToken{}}, expectHasEntryCalled: true, - expectedHasEntryToken: &models.UserAuth{OAuthExpiry: time.Now().Add(10 * time.Minute)}, + expectedHasEntryToken: &login.UserAuth{OAuthExpiry: time.Now().Add(10 * time.Minute)}, }, { desc: "should skip sync when access token has no expired yet", identity: &authn.Identity{ID: "user:1", SessionToken: &auth.UserToken{}}, expectHasEntryCalled: true, - expectedHasEntryToken: &models.UserAuth{OAuthExpiry: time.Now().Add(10 * time.Minute)}, + expectedHasEntryToken: &login.UserAuth{OAuthExpiry: time.Now().Add(10 * time.Minute)}, }, { desc: "should refresh access token when is has expired", identity: &authn.Identity{ID: "user:1", SessionToken: &auth.UserToken{}}, expectHasEntryCalled: true, expectTryRefreshTokenCalled: true, - expectedHasEntryToken: &models.UserAuth{OAuthExpiry: time.Now().Add(-10 * time.Minute)}, + expectedHasEntryToken: &login.UserAuth{OAuthExpiry: time.Now().Add(-10 * time.Minute)}, }, { desc: "should invalidate access token and session token if access token can't be refreshed", @@ -81,7 +81,7 @@ func TestOauthTokenSync_SyncOauthToken(t *testing.T) { expectTryRefreshTokenCalled: true, expectInvalidateOauthTokensCalled: true, expectRevokeTokenCalled: true, - expectedHasEntryToken: &models.UserAuth{OAuthExpiry: time.Now().Add(-10 * time.Minute)}, + expectedHasEntryToken: &login.UserAuth{OAuthExpiry: time.Now().Add(-10 * time.Minute)}, expectedErr: errExpiredAccessToken, }, } @@ -96,15 +96,15 @@ func TestOauthTokenSync_SyncOauthToken(t *testing.T) { ) service := &oauthtokentest.MockOauthTokenService{ - HasOAuthEntryFunc: func(ctx context.Context, usr *user.SignedInUser) (*models.UserAuth, bool, error) { + HasOAuthEntryFunc: func(ctx context.Context, usr *user.SignedInUser) (*login.UserAuth, bool, error) { hasEntryCalled = true return tt.expectedHasEntryToken, tt.expectedHasEntryToken != nil, nil }, - InvalidateOAuthTokensFunc: func(ctx context.Context, usr *models.UserAuth) error { + InvalidateOAuthTokensFunc: func(ctx context.Context, usr *login.UserAuth) error { invalidateTokensCalled = true return nil }, - TryTokenRefreshFunc: func(ctx context.Context, usr *models.UserAuth) error { + TryTokenRefreshFunc: func(ctx context.Context, usr *login.UserAuth) error { tryRefreshCalled = true return tt.expectedTryRefreshErr }, diff --git a/pkg/services/authn/authnimpl/sync/org_sync_test.go b/pkg/services/authn/authnimpl/sync/org_sync_test.go index 4fd87ba7d9a..4c4f6d6ea06 100644 --- a/pkg/services/authn/authnimpl/sync/org_sync_test.go +++ b/pkg/services/authn/authnimpl/sync/org_sync_test.go @@ -4,17 +4,18 @@ import ( "context" "testing" + "github.com/stretchr/testify/assert" + "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgtest" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/usertest" - "github.com/stretchr/testify/assert" ) func TestOrgSync_SyncOrgUser(t *testing.T) { @@ -79,7 +80,7 @@ func TestOrgSync_SyncOrgUser(t *testing.T) { IsGrafanaAdmin: ptrBool(false), ClientParams: authn.ClientParams{ SyncUser: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: nil, Email: ptrString("test"), Login: nil, @@ -97,7 +98,7 @@ func TestOrgSync_SyncOrgUser(t *testing.T) { IsGrafanaAdmin: ptrBool(false), ClientParams: authn.ClientParams{ SyncUser: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: nil, Email: ptrString("test"), Login: nil, diff --git a/pkg/services/authn/authnimpl/sync/user_sync.go b/pkg/services/authn/authnimpl/sync/user_sync.go index f1740a33b0b..a054a7032ad 100644 --- a/pkg/services/authn/authnimpl/sync/user_sync.go +++ b/pkg/services/authn/authnimpl/sync/user_sync.go @@ -6,7 +6,6 @@ import ( "fmt" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" @@ -140,7 +139,7 @@ func (s *UserSync) updateAuthInfo(ctx context.Context, id *authn.Identity) error return fmt.Errorf("invalid namespace %q for user ID %q", namespace, userID) } - updateCmd := &models.UpdateAuthInfoCommand{ + updateCmd := &login.UpdateAuthInfoCommand{ AuthModule: id.AuthModule, AuthId: id.AuthID, UserId: userID, @@ -222,7 +221,7 @@ func (s *UserSync) createUser(ctx context.Context, id *authn.Identity) (*user.Us } if id.AuthModule != "" && id.AuthID != "" { - if errSetAuth := s.authInfoService.SetAuthInfo(ctx, &models.SetAuthInfoCommand{ + if errSetAuth := s.authInfoService.SetAuthInfo(ctx, &login.SetAuthInfoCommand{ UserId: usr.ID, AuthModule: id.AuthModule, AuthId: id.AuthID, @@ -241,10 +240,10 @@ func (s *UserSync) createUser(ctx context.Context, id *authn.Identity) (*user.Us func (s *UserSync) UserInDB(ctx context.Context, authID *string, authModule *string, - params models.UserLookupParams) (*user.User, error) { + params login.UserLookupParams) (*user.User, error) { // Check authinfo table if authID != nil && authModule != nil { - query := &models.GetAuthInfoQuery{ + query := &login.GetAuthInfoQuery{ AuthModule: *authModule, AuthId: *authID, } @@ -269,7 +268,7 @@ func (s *UserSync) UserInDB(ctx context.Context, return s.LookupByOneOf(ctx, ¶ms) } -func (s *UserSync) LookupByOneOf(ctx context.Context, params *models.UserLookupParams) (*user.User, error) { +func (s *UserSync) LookupByOneOf(ctx context.Context, params *login.UserLookupParams) (*user.User, error) { var usr *user.User var err error diff --git a/pkg/services/authn/authnimpl/sync/user_sync_test.go b/pkg/services/authn/authnimpl/sync/user_sync_test.go index cb92d1112e2..544419c76a8 100644 --- a/pkg/services/authn/authnimpl/sync/user_sync_test.go +++ b/pkg/services/authn/authnimpl/sync/user_sync_test.go @@ -4,7 +4,8 @@ import ( "context" "testing" - "github.com/grafana/grafana/pkg/models" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/login/authinfoservice" @@ -13,7 +14,6 @@ import ( "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/usertest" - "github.com/stretchr/testify/require" ) func ptrString(s string) *string { @@ -34,17 +34,17 @@ func TestUserSync_SyncUser(t *testing.T) { authFakeNil := &logintest.AuthInfoServiceFake{ ExpectedUser: nil, ExpectedError: user.ErrUserNotFound, - SetAuthInfoFn: func(ctx context.Context, cmd *models.SetAuthInfoCommand) error { + SetAuthInfoFn: func(ctx context.Context, cmd *login.SetAuthInfoCommand) error { return nil }, - UpdateAuthInfoFn: func(ctx context.Context, cmd *models.UpdateAuthInfoCommand) error { + UpdateAuthInfoFn: func(ctx context.Context, cmd *login.UpdateAuthInfoCommand) error { return nil }, } authFakeUserID := &logintest.AuthInfoServiceFake{ ExpectedUser: nil, ExpectedError: nil, - ExpectedUserAuth: &models.UserAuth{ + ExpectedUserAuth: &login.UserAuth{ AuthModule: "oauth", AuthId: "2032", UserId: 1, @@ -111,7 +111,7 @@ func TestUserSync_SyncUser(t *testing.T) { Name: "test", Email: "test", ClientParams: authn.ClientParams{ - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: nil, Email: ptrString("test"), Login: nil, @@ -126,7 +126,7 @@ func TestUserSync_SyncUser(t *testing.T) { Name: "test", Email: "test", ClientParams: authn.ClientParams{ - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: nil, Email: ptrString("test"), Login: nil, @@ -150,7 +150,7 @@ func TestUserSync_SyncUser(t *testing.T) { Email: "test", ClientParams: authn.ClientParams{ SyncUser: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: nil, Email: ptrString("test"), Login: nil, @@ -167,7 +167,7 @@ func TestUserSync_SyncUser(t *testing.T) { IsGrafanaAdmin: ptrBool(false), ClientParams: authn.ClientParams{ SyncUser: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: nil, Email: ptrString("test"), Login: nil, @@ -191,7 +191,7 @@ func TestUserSync_SyncUser(t *testing.T) { Email: "test", ClientParams: authn.ClientParams{ SyncUser: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: nil, Email: nil, Login: ptrString("test"), @@ -207,7 +207,7 @@ func TestUserSync_SyncUser(t *testing.T) { Email: "test", IsGrafanaAdmin: ptrBool(false), ClientParams: authn.ClientParams{ - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: nil, Email: nil, Login: ptrString("test"), @@ -232,7 +232,7 @@ func TestUserSync_SyncUser(t *testing.T) { Email: "test", ClientParams: authn.ClientParams{ SyncUser: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: ptrInt64(1), Email: nil, Login: nil, @@ -249,7 +249,7 @@ func TestUserSync_SyncUser(t *testing.T) { IsGrafanaAdmin: ptrBool(false), ClientParams: authn.ClientParams{ SyncUser: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: ptrInt64(1), Email: nil, Login: nil, @@ -274,7 +274,7 @@ func TestUserSync_SyncUser(t *testing.T) { Email: "test", ClientParams: authn.ClientParams{ SyncUser: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: nil, Email: nil, Login: nil, @@ -291,7 +291,7 @@ func TestUserSync_SyncUser(t *testing.T) { IsGrafanaAdmin: ptrBool(false), ClientParams: authn.ClientParams{ SyncUser: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: nil, Email: nil, Login: nil, @@ -317,7 +317,7 @@ func TestUserSync_SyncUser(t *testing.T) { AuthID: "2032", ClientParams: authn.ClientParams{ SyncUser: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: nil, Email: nil, Login: nil, @@ -348,7 +348,7 @@ func TestUserSync_SyncUser(t *testing.T) { SyncUser: true, AllowSignUp: true, EnableDisabledUsers: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: nil, Email: ptrString("test_create"), Login: nil, @@ -369,7 +369,7 @@ func TestUserSync_SyncUser(t *testing.T) { SyncUser: true, AllowSignUp: true, EnableDisabledUsers: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: nil, Email: ptrString("test_create"), Login: nil, @@ -396,7 +396,7 @@ func TestUserSync_SyncUser(t *testing.T) { ClientParams: authn.ClientParams{ SyncUser: true, EnableDisabledUsers: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: ptrInt64(3), Email: nil, Login: nil, @@ -415,7 +415,7 @@ func TestUserSync_SyncUser(t *testing.T) { ClientParams: authn.ClientParams{ SyncUser: true, EnableDisabledUsers: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: ptrInt64(3), Email: nil, Login: nil, diff --git a/pkg/services/authn/clients/grafana_test.go b/pkg/services/authn/clients/grafana_test.go index 918888f5a23..9bf3ce88a13 100644 --- a/pkg/services/authn/clients/grafana_test.go +++ b/pkg/services/authn/clients/grafana_test.go @@ -5,14 +5,15 @@ import ( "net/http" "testing" - "github.com/grafana/grafana/pkg/models" + "github.com/stretchr/testify/assert" + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" - "github.com/stretchr/testify/assert" ) func TestGrafana_AuthenticateProxy(t *testing.T) { @@ -51,7 +52,7 @@ func TestGrafana_AuthenticateProxy(t *testing.T) { SyncUser: true, SyncTeamMembers: true, AllowSignUp: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ Email: strPtr("email@email.com"), Login: strPtr("test"), }, @@ -72,7 +73,7 @@ func TestGrafana_AuthenticateProxy(t *testing.T) { SyncUser: true, SyncTeamMembers: true, AllowSignUp: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ Email: strPtr("test@test.com"), Login: strPtr("test@test.com"), }, diff --git a/pkg/services/authn/clients/jwt_test.go b/pkg/services/authn/clients/jwt_test.go index d95876fd4f6..29035b19145 100644 --- a/pkg/services/authn/clients/jwt_test.go +++ b/pkg/services/authn/clients/jwt_test.go @@ -10,10 +10,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/services/auth/jwt" "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/setting" ) @@ -52,7 +52,7 @@ func TestAuthenticateJWT(t *testing.T) { SyncTeamMembers: false, SyncUser: true, AllowSignUp: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: nil, Email: stringPtr("eai.doe@cor.po"), Login: stringPtr("eai-doe"), diff --git a/pkg/services/authn/clients/ldap.go b/pkg/services/authn/clients/ldap.go index 0e8f14a5be0..943601abf42 100644 --- a/pkg/services/authn/clients/ldap.go +++ b/pkg/services/authn/clients/ldap.go @@ -4,8 +4,8 @@ import ( "context" "errors" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/multildap" "github.com/grafana/grafana/pkg/setting" ) @@ -36,7 +36,7 @@ func (c *LDAP) AuthenticateProxy(ctx context.Context, r *authn.Request, username } func (c *LDAP) AuthenticatePassword(ctx context.Context, r *authn.Request, username, password string) (*authn.Identity, error) { - info, err := c.service.Login(&models.LoginUserQuery{ + info, err := c.service.Login(&login.LoginUserQuery{ Username: username, Password: password, }) @@ -61,8 +61,8 @@ func (c *LDAP) AuthenticatePassword(ctx context.Context, r *authn.Request, usern } type ldapService interface { - Login(query *models.LoginUserQuery) (*models.ExternalUserInfo, error) - User(username string) (*models.ExternalUserInfo, error) + Login(query *login.LoginUserQuery) (*login.ExternalUserInfo, error) + User(username string) (*login.ExternalUserInfo, error) } // FIXME: remove the implementation if we convert ldap to an actual service @@ -70,7 +70,7 @@ type ldapServiceImpl struct { cfg *setting.Cfg } -func (s *ldapServiceImpl) Login(query *models.LoginUserQuery) (*models.ExternalUserInfo, error) { +func (s *ldapServiceImpl) Login(query *login.LoginUserQuery) (*login.ExternalUserInfo, error) { cfg, err := multildap.GetConfig(s.cfg) if err != nil { return nil, err @@ -79,7 +79,7 @@ func (s *ldapServiceImpl) Login(query *models.LoginUserQuery) (*models.ExternalU return multildap.New(cfg.Servers).Login(query) } -func (s *ldapServiceImpl) User(username string) (*models.ExternalUserInfo, error) { +func (s *ldapServiceImpl) User(username string) (*login.ExternalUserInfo, error) { cfg, err := multildap.GetConfig(s.cfg) if err != nil { return nil, err @@ -89,7 +89,7 @@ func (s *ldapServiceImpl) User(username string) (*models.ExternalUserInfo, error return user, err } -func identityFromLDAPInfo(orgID int64, info *models.ExternalUserInfo, allowSignup bool) *authn.Identity { +func identityFromLDAPInfo(orgID int64, info *login.ExternalUserInfo, allowSignup bool) *authn.Identity { return &authn.Identity{ OrgID: orgID, OrgRoles: info.OrgRoles, @@ -105,7 +105,7 @@ func identityFromLDAPInfo(orgID int64, info *models.ExternalUserInfo, allowSignu SyncTeamMembers: true, AllowSignUp: allowSignup, EnableDisabledUsers: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ Login: &info.Login, Email: &info.Email, }, diff --git a/pkg/services/authn/clients/ldap_test.go b/pkg/services/authn/clients/ldap_test.go index e59516f2cf1..e5c59207320 100644 --- a/pkg/services/authn/clients/ldap_test.go +++ b/pkg/services/authn/clients/ldap_test.go @@ -4,14 +4,14 @@ import ( "context" "testing" - "github.com/grafana/grafana/pkg/models" + "github.com/stretchr/testify/assert" + "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/multildap" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" ) func TestLDAP_AuthenticateProxy(t *testing.T) { @@ -19,7 +19,7 @@ func TestLDAP_AuthenticateProxy(t *testing.T) { desc string username string expectedLDAPErr error - expectedLDAPInfo *models.ExternalUserInfo + expectedLDAPInfo *login.ExternalUserInfo expectedErr error expectedIdentity *authn.Identity } @@ -28,7 +28,7 @@ func TestLDAP_AuthenticateProxy(t *testing.T) { { desc: "should return valid identity when found by ldap service", username: "test", - expectedLDAPInfo: &models.ExternalUserInfo{ + expectedLDAPInfo: &login.ExternalUserInfo{ AuthModule: login.LDAPAuthModule, AuthId: "123", Email: "test@test.com", @@ -51,7 +51,7 @@ func TestLDAP_AuthenticateProxy(t *testing.T) { SyncTeamMembers: true, AllowSignUp: false, EnableDisabledUsers: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ Email: strPtr("test@test.com"), Login: strPtr("test"), }, @@ -83,7 +83,7 @@ func TestLDAP_AuthenticatePassword(t *testing.T) { password string expectedErr error expectedLDAPErr error - expectedLDAPInfo *models.ExternalUserInfo + expectedLDAPInfo *login.ExternalUserInfo expectedIdentity *authn.Identity } @@ -92,7 +92,7 @@ func TestLDAP_AuthenticatePassword(t *testing.T) { desc: "should successfully authenticate with correct username and password", username: "test", password: "test123", - expectedLDAPInfo: &models.ExternalUserInfo{ + expectedLDAPInfo: &login.ExternalUserInfo{ AuthModule: login.LDAPAuthModule, AuthId: "123", Email: "test@test.com", @@ -115,7 +115,7 @@ func TestLDAP_AuthenticatePassword(t *testing.T) { SyncTeamMembers: true, AllowSignUp: false, EnableDisabledUsers: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ Email: strPtr("test@test.com"), Login: strPtr("test"), }, @@ -157,13 +157,13 @@ var _ ldapService = new(fakeLDAPService) type fakeLDAPService struct { ExpectedErr error - ExpectedInfo *models.ExternalUserInfo + ExpectedInfo *login.ExternalUserInfo } -func (f fakeLDAPService) Login(query *models.LoginUserQuery) (*models.ExternalUserInfo, error) { +func (f fakeLDAPService) Login(query *login.LoginUserQuery) (*login.ExternalUserInfo, error) { return f.ExpectedInfo, f.ExpectedErr } -func (f fakeLDAPService) User(username string) (*models.ExternalUserInfo, error) { +func (f fakeLDAPService) User(username string) (*login.ExternalUserInfo, error) { return f.ExpectedInfo, f.ExpectedErr } diff --git a/pkg/services/contexthandler/auth_jwt.go b/pkg/services/contexthandler/auth_jwt.go index 24f7a2c0a22..a0a5b9750a9 100644 --- a/pkg/services/contexthandler/auth_jwt.go +++ b/pkg/services/contexthandler/auth_jwt.go @@ -9,10 +9,10 @@ import ( "github.com/jmespath/go-jmespath" "github.com/grafana/grafana/pkg/login" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models/roletype" authJWT "github.com/grafana/grafana/pkg/services/auth/jwt" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + loginsvc "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" ) @@ -61,8 +61,7 @@ func (h *ContextHandler) initContextWithJWT(ctx *contextmodel.ReqContext, orgId ctx.JsonApiErr(http.StatusUnauthorized, InvalidJWT, err) return true } - - extUser := &models.ExternalUserInfo{ + extUser := &loginsvc.ExternalUserInfo{ AuthModule: "jwt", AuthId: sub, OrgRoles: map[int64]org.RoleType{}, @@ -118,11 +117,11 @@ func (h *ContextHandler) initContextWithJWT(ctx *contextmodel.ReqContext, orgId } if h.Cfg.JWTAuthAutoSignUp { - upsert := &models.UpsertUserCommand{ + upsert := &loginsvc.UpsertUserCommand{ ReqContext: ctx, SignupAllowed: h.Cfg.JWTAuthAutoSignUp, ExternalUser: extUser, - UserLookupParams: models.UserLookupParams{ + UserLookupParams: loginsvc.UserLookupParams{ UserID: nil, Login: &query.Login, Email: &query.Email, diff --git a/pkg/services/contexthandler/auth_proxy_test.go b/pkg/services/contexthandler/auth_proxy_test.go index 55cf1ada314..773e6b36ee0 100644 --- a/pkg/services/contexthandler/auth_proxy_test.go +++ b/pkg/services/contexthandler/auth_proxy_test.go @@ -12,13 +12,13 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/remotecache" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth/authtest" "github.com/grafana/grafana/pkg/services/auth/jwt" "github.com/grafana/grafana/pkg/services/authn/authntest" "github.com/grafana/grafana/pkg/services/contexthandler/authproxy" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/login/loginservice" "github.com/grafana/grafana/pkg/services/org/orgtest" "github.com/grafana/grafana/pkg/services/rendering" @@ -114,6 +114,6 @@ func getContextHandler(t *testing.T) *ContextHandler { type fakeAuthenticator struct{} -func (fa *fakeAuthenticator) AuthenticateUser(c context.Context, query *models.LoginUserQuery) error { +func (fa *fakeAuthenticator) AuthenticateUser(c context.Context, query *login.LoginUserQuery) error { return nil } diff --git a/pkg/services/contexthandler/authproxy/authproxy.go b/pkg/services/contexthandler/authproxy/authproxy.go index 1cf0a7993d1..6a6fbda21b7 100644 --- a/pkg/services/contexthandler/authproxy/authproxy.go +++ b/pkg/services/contexthandler/authproxy/authproxy.go @@ -16,7 +16,6 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/remotecache" - "github.com/grafana/grafana/pkg/models" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/login" @@ -242,11 +241,11 @@ func (auth *AuthProxy) LoginViaLDAP(reqCtx *contextmodel.ReqContext) (int64, err } // Have to sync grafana and LDAP user during log in - upsert := &models.UpsertUserCommand{ + upsert := &login.UpsertUserCommand{ ReqContext: reqCtx, SignupAllowed: auth.cfg.LDAPAllowSignup, ExternalUser: extUser, - UserLookupParams: models.UserLookupParams{ + UserLookupParams: login.UserLookupParams{ Login: &extUser.Login, Email: &extUser.Email, UserID: nil, @@ -262,7 +261,7 @@ func (auth *AuthProxy) LoginViaLDAP(reqCtx *contextmodel.ReqContext) (int64, err // loginViaHeader logs in user from the header only func (auth *AuthProxy) loginViaHeader(reqCtx *contextmodel.ReqContext) (int64, error) { header := auth.getDecodedHeader(reqCtx, auth.cfg.AuthProxyHeaderName) - extUser := &models.ExternalUserInfo{ + extUser := &login.ExternalUserInfo{ AuthModule: login.AuthProxyAuthModule, AuthId: header, } @@ -304,11 +303,11 @@ func (auth *AuthProxy) loginViaHeader(reqCtx *contextmodel.ReqContext) (int64, e } }) - upsert := &models.UpsertUserCommand{ + upsert := &login.UpsertUserCommand{ ReqContext: reqCtx, SignupAllowed: auth.cfg.AuthProxyAutoSignUp, ExternalUser: extUser, - UserLookupParams: models.UserLookupParams{ + UserLookupParams: login.UserLookupParams{ UserID: nil, Login: &extUser.Login, Email: &extUser.Email, diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index 034cc80bcc0..14f3398ec85 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -20,7 +20,6 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" loginpkg "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/middleware/cookies" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth/jwt" @@ -419,7 +418,7 @@ func (h *ContextHandler) initContextWithBasicAuth(reqContext *contextmodel.ReqCo ctx := WithAuthHTTPHeader(reqContext.Req.Context(), "Authorization") *reqContext.Req = *reqContext.Req.WithContext(ctx) - authQuery := models.LoginUserQuery{ + authQuery := login.LoginUserQuery{ Username: username, Password: password, Cfg: h.Cfg, @@ -774,7 +773,7 @@ func AuthHTTPHeaderListFromContext(c context.Context) *AuthHTTPHeaderList { return nil } -func (h *ContextHandler) hasAccessTokenExpired(token *models.UserAuth) bool { +func (h *ContextHandler) hasAccessTokenExpired(token *login.UserAuth) bool { if token.OAuthExpiry.IsZero() { return false } diff --git a/pkg/services/hooks/hooks.go b/pkg/services/hooks/hooks.go index 1729067e676..af36933ee20 100644 --- a/pkg/services/hooks/hooks.go +++ b/pkg/services/hooks/hooks.go @@ -2,13 +2,13 @@ package hooks import ( "github.com/grafana/grafana/pkg/api/dtos" - "github.com/grafana/grafana/pkg/models" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/login" ) type IndexDataHook func(indexData *dtos.IndexViewData, req *contextmodel.ReqContext) -type LoginHook func(loginInfo *models.LoginInfo, req *contextmodel.ReqContext) +type LoginHook func(loginInfo *login.LoginInfo, req *contextmodel.ReqContext) type HooksService struct { indexDataHooks []IndexDataHook @@ -33,7 +33,7 @@ func (srv *HooksService) AddLoginHook(hook LoginHook) { srv.loginHooks = append(srv.loginHooks, hook) } -func (srv *HooksService) RunLoginHook(loginInfo *models.LoginInfo, req *contextmodel.ReqContext) { +func (srv *HooksService) RunLoginHook(loginInfo *login.LoginInfo, req *contextmodel.ReqContext) { for _, hook := range srv.loginHooks { hook(loginInfo, req) } diff --git a/pkg/services/ldap/ldap.go b/pkg/services/ldap/ldap.go index 934a3c0c8e0..8a65c939fcd 100644 --- a/pkg/services/ldap/ldap.go +++ b/pkg/services/ldap/ldap.go @@ -15,7 +15,6 @@ import ( "gopkg.in/ldap.v3" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" ) @@ -33,8 +32,8 @@ type IConnection interface { // IServer is interface for LDAP authorization type IServer interface { - Login(*models.LoginUserQuery) (*models.ExternalUserInfo, error) - Users([]string) ([]*models.ExternalUserInfo, error) + Login(*login.LoginUserQuery) (*login.ExternalUserInfo, error) + Users([]string) ([]*login.ExternalUserInfo, error) Bind() error UserBind(string, string) error Dial() error @@ -202,8 +201,8 @@ func (server *Server) Close() { // // Dial() sets the connection with the server for this Struct. Therefore, we require a // call to Dial() before being able to execute this function. -func (server *Server) Login(query *models.LoginUserQuery) ( - *models.ExternalUserInfo, error, +func (server *Server) Login(query *login.LoginUserQuery) ( + *login.ExternalUserInfo, error, ) { var err error var authAndBind bool @@ -279,7 +278,7 @@ func (server *Server) shouldSingleBind() bool { // Dial() sets the connection with the server for this Struct. Therefore, we require a // call to Dial() before being able to execute this function. func (server *Server) Users(logins []string) ( - []*models.ExternalUserInfo, + []*login.ExternalUserInfo, error, ) { var users [][]*ldap.Entry @@ -293,7 +292,7 @@ func (server *Server) Users(logins []string) ( } if len(users) == 0 { - return []*models.ExternalUserInfo{}, nil + return []*login.ExternalUserInfo{}, nil } serializedUsers, err := server.serializeUsers(users) @@ -361,7 +360,7 @@ func (server *Server) users(logins []string) ( // validateGrafanaUser validates user access. // If there are no ldap group mappings access is true // otherwise a single group must match -func (server *Server) validateGrafanaUser(user *models.ExternalUserInfo) error { +func (server *Server) validateGrafanaUser(user *login.ExternalUserInfo) error { if !SkipOrgRoleSync() && len(server.Config.Groups) > 0 && (len(user.OrgRoles) == 0 && (user.IsGrafanaAdmin == nil || !*user.IsGrafanaAdmin)) { server.log.Error( @@ -423,14 +422,14 @@ func (server *Server) getSearchRequest( } // buildGrafanaUser extracts info from UserInfo model to ExternalUserInfo -func (server *Server) buildGrafanaUser(user *ldap.Entry) (*models.ExternalUserInfo, error) { +func (server *Server) buildGrafanaUser(user *ldap.Entry) (*login.ExternalUserInfo, error) { memberOf, err := server.getMemberOf(user) if err != nil { return nil, err } attrs := server.Config.Attr - extUser := &models.ExternalUserInfo{ + extUser := &login.ExternalUserInfo{ AuthModule: login.LDAPAuthModule, AuthId: user.DN, Name: strings.TrimSpace( @@ -595,8 +594,8 @@ func (server *Server) requestMemberOf(entry *ldap.Entry) ([]string, error) { // from LDAP result to ExternalInfo struct func (server *Server) serializeUsers( entries [][]*ldap.Entry, -) ([]*models.ExternalUserInfo, error) { - var serialized []*models.ExternalUserInfo +) ([]*login.ExternalUserInfo, error) { + var serialized []*login.ExternalUserInfo var users = map[string]struct{}{} for _, dn := range entries { diff --git a/pkg/services/ldap/ldap_groups.go b/pkg/services/ldap/ldap_groups.go index a3e6053e575..be23712313d 100644 --- a/pkg/services/ldap/ldap_groups.go +++ b/pkg/services/ldap/ldap_groups.go @@ -1,9 +1,7 @@ package ldap -import "github.com/grafana/grafana/pkg/models" - type Groups interface { - GetTeams(groups []string, orgIDs []int64) ([]models.TeamOrgGroupDTO, error) + GetTeams(groups []string, orgIDs []int64) ([]TeamOrgGroupDTO, error) } type OSSGroups struct{} @@ -12,6 +10,6 @@ func ProvideGroupsService() *OSSGroups { return &OSSGroups{} } -func (*OSSGroups) GetTeams(_ []string, _ []int64) ([]models.TeamOrgGroupDTO, error) { +func (*OSSGroups) GetTeams(_ []string, _ []int64) ([]TeamOrgGroupDTO, error) { return nil, nil } diff --git a/pkg/services/ldap/ldap_login_test.go b/pkg/services/ldap/ldap_login_test.go index 7b552a8edfa..c31b2cb4dbd 100644 --- a/pkg/services/ldap/ldap_login_test.go +++ b/pkg/services/ldap/ldap_login_test.go @@ -10,10 +10,10 @@ import ( "gopkg.in/ldap.v3" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/login" ) -var defaultLogin = &models.LoginUserQuery{ +var defaultLogin = &login.LoginUserQuery{ Username: "user", Password: "pwd", IpAddress: "192.168.1.1:56433", diff --git a/pkg/services/ldap/ldap_private_test.go b/pkg/services/ldap/ldap_private_test.go index b7846f58f6c..e9d534051eb 100644 --- a/pkg/services/ldap/ldap_private_test.go +++ b/pkg/services/ldap/ldap_private_test.go @@ -10,7 +10,7 @@ import ( "gopkg.in/ldap.v3" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" ) @@ -159,7 +159,7 @@ func TestServer_validateGrafanaUser(t *testing.T) { log: logger.New("test"), } - user := &models.ExternalUserInfo{ + user := &login.ExternalUserInfo{ Login: "markelog", } @@ -179,7 +179,7 @@ func TestServer_validateGrafanaUser(t *testing.T) { log: logger.New("test"), } - user := &models.ExternalUserInfo{ + user := &login.ExternalUserInfo{ Login: "markelog", OrgRoles: map[int64]org.RoleType{ 1: "test", @@ -202,7 +202,7 @@ func TestServer_validateGrafanaUser(t *testing.T) { log: logger.New("test"), } - user := &models.ExternalUserInfo{ + user := &login.ExternalUserInfo{ Login: "markelog", } diff --git a/pkg/services/ldap/model.go b/pkg/services/ldap/model.go new file mode 100644 index 00000000000..302f5bffabe --- /dev/null +++ b/pkg/services/ldap/model.go @@ -0,0 +1,7 @@ +package ldap + +type TeamOrgGroupDTO struct { + TeamName string `json:"teamName"` + OrgName string `json:"orgName"` + GroupDN string `json:"groupDN"` +} diff --git a/pkg/services/login/authinfo.go b/pkg/services/login/authinfo.go index 01476e31ef2..9f7e8908533 100644 --- a/pkg/services/login/authinfo.go +++ b/pkg/services/login/authinfo.go @@ -3,17 +3,16 @@ package login import ( "context" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/user" ) type AuthInfoService interface { - LookupAndUpdate(ctx context.Context, query *models.GetUserByAuthInfoQuery) (*user.User, error) - GetAuthInfo(ctx context.Context, query *models.GetAuthInfoQuery) error - GetUserLabels(ctx context.Context, query models.GetUserLabelsQuery) (map[int64]string, error) - GetExternalUserInfoByLogin(ctx context.Context, query *models.GetExternalUserInfoByLoginQuery) error - SetAuthInfo(ctx context.Context, cmd *models.SetAuthInfoCommand) error - UpdateAuthInfo(ctx context.Context, cmd *models.UpdateAuthInfoCommand) error + LookupAndUpdate(ctx context.Context, query *GetUserByAuthInfoQuery) (*user.User, error) + GetAuthInfo(ctx context.Context, query *GetAuthInfoQuery) error + GetUserLabels(ctx context.Context, query GetUserLabelsQuery) (map[int64]string, error) + GetExternalUserInfoByLogin(ctx context.Context, query *GetExternalUserInfoByLoginQuery) error + SetAuthInfo(ctx context.Context, cmd *SetAuthInfoCommand) error + UpdateAuthInfo(ctx context.Context, cmd *UpdateAuthInfoCommand) error DeleteUserAuthInfo(ctx context.Context, userID int64) error } diff --git a/pkg/services/login/authinfoservice/database/database.go b/pkg/services/login/authinfoservice/database/database.go index 1aa23b77d17..1d4abbfe85b 100644 --- a/pkg/services/login/authinfoservice/database/database.go +++ b/pkg/services/login/authinfoservice/database/database.go @@ -7,7 +7,6 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/user" @@ -35,19 +34,19 @@ func ProvideAuthInfoStore(sqlStore db.DB, secretsService secrets.Service, userSe return store } -func (s *AuthInfoStore) GetExternalUserInfoByLogin(ctx context.Context, query *models.GetExternalUserInfoByLoginQuery) error { +func (s *AuthInfoStore) GetExternalUserInfoByLogin(ctx context.Context, query *login.GetExternalUserInfoByLoginQuery) error { userQuery := user.GetUserByLoginQuery{LoginOrEmail: query.LoginOrEmail} usr, err := s.userService.GetByLogin(ctx, &userQuery) if err != nil { return err } - authInfoQuery := &models.GetAuthInfoQuery{UserId: usr.ID} + authInfoQuery := &login.GetAuthInfoQuery{UserId: usr.ID} if err := s.GetAuthInfo(ctx, authInfoQuery); err != nil { return err } - query.Result = &models.ExternalUserInfo{ + query.Result = &login.ExternalUserInfo{ UserId: usr.ID, Login: usr.Login, Email: usr.Email, @@ -59,12 +58,12 @@ func (s *AuthInfoStore) GetExternalUserInfoByLogin(ctx context.Context, query *m return nil } -func (s *AuthInfoStore) GetAuthInfo(ctx context.Context, query *models.GetAuthInfoQuery) error { +func (s *AuthInfoStore) GetAuthInfo(ctx context.Context, query *login.GetAuthInfoQuery) error { if query.UserId == 0 && query.AuthId == "" { return user.ErrUserNotFound } - userAuth := &models.UserAuth{ + userAuth := &login.UserAuth{ UserId: query.UserId, AuthModule: query.AuthModule, AuthId: query.AuthId, @@ -110,8 +109,8 @@ func (s *AuthInfoStore) GetAuthInfo(ctx context.Context, query *models.GetAuthIn return nil } -func (s *AuthInfoStore) GetUserLabels(ctx context.Context, query models.GetUserLabelsQuery) (map[int64]string, error) { - userAuths := []models.UserAuth{} +func (s *AuthInfoStore) GetUserLabels(ctx context.Context, query login.GetUserLabelsQuery) (map[int64]string, error) { + userAuths := []login.UserAuth{} params := make([]interface{}, 0, len(query.UserIDs)) for _, id := range query.UserIDs { params = append(params, id) @@ -134,8 +133,8 @@ func (s *AuthInfoStore) GetUserLabels(ctx context.Context, query models.GetUserL return labelMap, nil } -func (s *AuthInfoStore) SetAuthInfo(ctx context.Context, cmd *models.SetAuthInfoCommand) error { - authUser := &models.UserAuth{ +func (s *AuthInfoStore) SetAuthInfo(ctx context.Context, cmd *login.SetAuthInfoCommand) error { + authUser := &login.UserAuth{ UserId: cmd.UserId, AuthModule: cmd.AuthModule, AuthId: cmd.AuthId, @@ -179,10 +178,10 @@ func (s *AuthInfoStore) SetAuthInfo(ctx context.Context, cmd *models.SetAuthInfo // UpdateAuthInfoDate updates the auth info for the user with the latest date. // Avoids overlapping entries hiding the last used one (ex: LDAP->SAML->LDAP). -func (s *AuthInfoStore) UpdateAuthInfoDate(ctx context.Context, authInfo *models.UserAuth) error { +func (s *AuthInfoStore) UpdateAuthInfoDate(ctx context.Context, authInfo *login.UserAuth) error { authInfo.Created = GetTime() - cond := &models.UserAuth{ + cond := &login.UserAuth{ Id: authInfo.Id, UserId: authInfo.UserId, AuthModule: authInfo.AuthModule, @@ -193,8 +192,8 @@ func (s *AuthInfoStore) UpdateAuthInfoDate(ctx context.Context, authInfo *models }) } -func (s *AuthInfoStore) UpdateAuthInfo(ctx context.Context, cmd *models.UpdateAuthInfoCommand) error { - authUser := &models.UserAuth{ +func (s *AuthInfoStore) UpdateAuthInfo(ctx context.Context, cmd *login.UpdateAuthInfoCommand) error { + authUser := &login.UserAuth{ UserId: cmd.UserId, AuthModule: cmd.AuthModule, AuthId: cmd.AuthId, @@ -237,7 +236,7 @@ func (s *AuthInfoStore) UpdateAuthInfo(ctx context.Context, cmd *models.UpdateAu }) } -func (s *AuthInfoStore) DeleteAuthInfo(ctx context.Context, cmd *models.DeleteAuthInfoCommand) error { +func (s *AuthInfoStore) DeleteAuthInfo(ctx context.Context, cmd *login.DeleteAuthInfoCommand) error { return s.sqlStore.WithTransactionalDbSession(ctx, func(sess *db.Session) error { _, err := sess.Delete(cmd.UserAuth) return err diff --git a/pkg/services/login/authinfoservice/service.go b/pkg/services/login/authinfoservice/service.go index 1f248453eb5..e0d6f440994 100644 --- a/pkg/services/login/authinfoservice/service.go +++ b/pkg/services/login/authinfoservice/service.go @@ -6,7 +6,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/usagestats" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/user" ) @@ -31,8 +30,8 @@ func ProvideAuthInfoService(userProtectionService login.UserProtectionService, a return s } -func (s *Implementation) LookupAndFix(ctx context.Context, query *models.GetUserByAuthInfoQuery) (bool, *user.User, *models.UserAuth, error) { - authQuery := &models.GetAuthInfoQuery{} +func (s *Implementation) LookupAndFix(ctx context.Context, query *login.GetUserByAuthInfoQuery) (bool, *user.User, *login.UserAuth, error) { + authQuery := &login.GetAuthInfoQuery{} // Try to find the user by auth module and id first if query.AuthModule != "" && query.AuthId != "" { @@ -49,7 +48,7 @@ func (s *Implementation) LookupAndFix(ctx context.Context, query *models.GetUser if query.UserLookupParams.UserID != nil && *query.UserLookupParams.UserID != 0 && *query.UserLookupParams.UserID != authQuery.Result.UserId { - if err := s.authInfoStore.DeleteAuthInfo(ctx, &models.DeleteAuthInfoCommand{ + if err := s.authInfoStore.DeleteAuthInfo(ctx, &login.DeleteAuthInfoCommand{ UserAuth: authQuery.Result, }); err != nil { s.logger.Error("Error removing user_auth entry", "error", err) @@ -61,7 +60,7 @@ func (s *Implementation) LookupAndFix(ctx context.Context, query *models.GetUser if err != nil { if errors.Is(err, user.ErrUserNotFound) { // if the user has been deleted then remove the entry - if errDel := s.authInfoStore.DeleteAuthInfo(ctx, &models.DeleteAuthInfoCommand{ + if errDel := s.authInfoStore.DeleteAuthInfo(ctx, &login.DeleteAuthInfoCommand{ UserAuth: authQuery.Result, }); errDel != nil { s.logger.Error("Error removing user_auth entry", "error", errDel) @@ -81,7 +80,7 @@ func (s *Implementation) LookupAndFix(ctx context.Context, query *models.GetUser return false, nil, nil, user.ErrUserNotFound } -func (s *Implementation) LookupByOneOf(ctx context.Context, params *models.UserLookupParams) (*user.User, error) { +func (s *Implementation) LookupByOneOf(ctx context.Context, params *login.UserLookupParams) (*user.User, error) { var usr *user.User var err error @@ -116,9 +115,9 @@ func (s *Implementation) LookupByOneOf(ctx context.Context, params *models.UserL return usr, nil } -func (s *Implementation) GenericOAuthLookup(ctx context.Context, authModule string, authId string, userID int64) (*models.UserAuth, error) { +func (s *Implementation) GenericOAuthLookup(ctx context.Context, authModule string, authId string, userID int64) (*login.UserAuth, error) { if authModule == genericOAuthModule && userID != 0 { - authQuery := &models.GetAuthInfoQuery{} + authQuery := &login.GetAuthInfoQuery{} authQuery.AuthModule = authModule authQuery.AuthId = authId authQuery.UserId = userID @@ -132,7 +131,7 @@ func (s *Implementation) GenericOAuthLookup(ctx context.Context, authModule stri return nil, nil } -func (s *Implementation) LookupAndUpdate(ctx context.Context, query *models.GetUserByAuthInfoQuery) (*user.User, error) { +func (s *Implementation) LookupAndUpdate(ctx context.Context, query *login.GetUserByAuthInfoQuery) (*user.User, error) { // 1. LookupAndFix = auth info, user, error // TODO: Not a big fan of the fact that we are deleting auth info here, might want to move that foundUser, usr, authInfo, err := s.LookupAndFix(ctx, query) @@ -165,7 +164,7 @@ func (s *Implementation) LookupAndUpdate(ctx context.Context, query *models.GetU if query.AuthModule != "" { if authInfo == nil { - cmd := &models.SetAuthInfoCommand{ + cmd := &login.SetAuthInfoCommand{ UserId: usr.ID, AuthModule: query.AuthModule, AuthId: query.AuthId, @@ -183,26 +182,26 @@ func (s *Implementation) LookupAndUpdate(ctx context.Context, query *models.GetU return usr, nil } -func (s *Implementation) GetAuthInfo(ctx context.Context, query *models.GetAuthInfoQuery) error { +func (s *Implementation) GetAuthInfo(ctx context.Context, query *login.GetAuthInfoQuery) error { return s.authInfoStore.GetAuthInfo(ctx, query) } -func (s *Implementation) GetUserLabels(ctx context.Context, query models.GetUserLabelsQuery) (map[int64]string, error) { +func (s *Implementation) GetUserLabels(ctx context.Context, query login.GetUserLabelsQuery) (map[int64]string, error) { if len(query.UserIDs) == 0 { return map[int64]string{}, nil } return s.authInfoStore.GetUserLabels(ctx, query) } -func (s *Implementation) UpdateAuthInfo(ctx context.Context, cmd *models.UpdateAuthInfoCommand) error { +func (s *Implementation) UpdateAuthInfo(ctx context.Context, cmd *login.UpdateAuthInfoCommand) error { return s.authInfoStore.UpdateAuthInfo(ctx, cmd) } -func (s *Implementation) SetAuthInfo(ctx context.Context, cmd *models.SetAuthInfoCommand) error { +func (s *Implementation) SetAuthInfo(ctx context.Context, cmd *login.SetAuthInfoCommand) error { return s.authInfoStore.SetAuthInfo(ctx, cmd) } -func (s *Implementation) GetExternalUserInfoByLogin(ctx context.Context, query *models.GetExternalUserInfoByLoginQuery) error { +func (s *Implementation) GetExternalUserInfoByLogin(ctx context.Context, query *login.GetExternalUserInfoByLoginQuery) error { return s.authInfoStore.GetExternalUserInfoByLogin(ctx, query) } diff --git a/pkg/services/login/authinfoservice/user_auth_test.go b/pkg/services/login/authinfoservice/user_auth_test.go index f22814020a5..d61e8191482 100644 --- a/pkg/services/login/authinfoservice/user_auth_test.go +++ b/pkg/services/login/authinfoservice/user_auth_test.go @@ -12,7 +12,6 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/usagestats" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/login/authinfoservice/database" "github.com/grafana/grafana/pkg/services/org/orgimpl" @@ -50,22 +49,22 @@ func TestUserAuth(t *testing.T) { t.Run("Can find existing user", func(t *testing.T) { // By Login - login := "loginuser0" + userlogin := "loginuser0" authInfoStore.ExpectedUser = &user.User{ Login: "loginuser0", ID: 1, Email: "user1@test.com", } - query := &models.GetUserByAuthInfoQuery{UserLookupParams: models.UserLookupParams{Login: &login}} + query := &login.GetUserByAuthInfoQuery{UserLookupParams: login.UserLookupParams{Login: &userlogin}} usr, err := srv.LookupAndUpdate(context.Background(), query) require.Nil(t, err) - require.Equal(t, usr.Login, login) + require.Equal(t, usr.Login, userlogin) // By ID id := usr.ID - usr, err = srv.LookupByOneOf(context.Background(), &models.UserLookupParams{ + usr, err = srv.LookupByOneOf(context.Background(), &login.UserLookupParams{ UserID: &id, }) @@ -75,7 +74,7 @@ func TestUserAuth(t *testing.T) { // By Email email := "user1@test.com" - usr, err = srv.LookupByOneOf(context.Background(), &models.UserLookupParams{ + usr, err = srv.LookupByOneOf(context.Background(), &login.UserLookupParams{ Email: &email, }) @@ -86,7 +85,7 @@ func TestUserAuth(t *testing.T) { // Don't find nonexistent user email = "nonexistent@test.com" - usr, err = srv.LookupByOneOf(context.Background(), &models.UserLookupParams{ + usr, err = srv.LookupByOneOf(context.Background(), &login.UserLookupParams{ Email: &email, }) @@ -98,29 +97,29 @@ func TestUserAuth(t *testing.T) { // get nonexistent user_auth entry authInfoStore.ExpectedUser = &user.User{} authInfoStore.ExpectedError = user.ErrUserNotFound - query := &models.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"} + query := &login.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"} usr, err := srv.LookupAndUpdate(context.Background(), query) require.Equal(t, user.ErrUserNotFound, err) require.Nil(t, usr) // create user_auth entry - login := "loginuser0" + userlogin := "loginuser0" authInfoStore.ExpectedUser = &user.User{Login: "loginuser0", ID: 1, Email: ""} authInfoStore.ExpectedError = nil - authInfoStore.ExpectedOAuth = &models.UserAuth{Id: 1} - query.UserLookupParams.Login = &login + authInfoStore.ExpectedOAuth = &login.UserAuth{Id: 1} + query.UserLookupParams.Login = &userlogin usr, err = srv.LookupAndUpdate(context.Background(), query) require.Nil(t, err) - require.Equal(t, usr.Login, login) + require.Equal(t, usr.Login, userlogin) // get via user_auth - query = &models.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"} + query = &login.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"} usr, err = srv.LookupAndUpdate(context.Background(), query) require.Nil(t, err) - require.Equal(t, usr.Login, login) + require.Equal(t, usr.Login, userlogin) // get with non-matching id idPlusOne := usr.ID + 1 @@ -133,7 +132,7 @@ func TestUserAuth(t *testing.T) { require.Equal(t, usr.Login, "loginuser1") // get via user_auth - query = &models.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"} + query = &login.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"} usr, err = srv.LookupAndUpdate(context.Background(), query) require.Nil(t, err) @@ -149,7 +148,7 @@ func TestUserAuth(t *testing.T) { authInfoStore.ExpectedUser = nil authInfoStore.ExpectedError = user.ErrUserNotFound // get via user_auth for deleted user - query = &models.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"} + query = &login.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"} usr, err = srv.LookupAndUpdate(context.Background(), query) require.Equal(t, err, user.ErrUserNotFound) @@ -167,10 +166,10 @@ func TestUserAuth(t *testing.T) { token = token.WithExtra(map[string]interface{}{"id_token": idToken}) // Find a user to set tokens on - login := "loginuser0" + userlogin := "loginuser0" authInfoStore.ExpectedUser = &user.User{Login: "loginuser0", ID: 1, Email: ""} authInfoStore.ExpectedError = nil - authInfoStore.ExpectedOAuth = &models.UserAuth{ + authInfoStore.ExpectedOAuth = &login.UserAuth{ Id: 1, OAuthAccessToken: token.AccessToken, OAuthRefreshToken: token.RefreshToken, @@ -179,15 +178,15 @@ func TestUserAuth(t *testing.T) { OAuthExpiry: token.Expiry, } // Calling GetUserByAuthInfoQuery on an existing user will populate an entry in the user_auth table - query := &models.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test", UserLookupParams: models.UserLookupParams{ - Login: &login, + query := &login.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test", UserLookupParams: login.UserLookupParams{ + Login: &userlogin, }} user, err := srv.LookupAndUpdate(context.Background(), query) require.Nil(t, err) - require.Equal(t, user.Login, login) + require.Equal(t, user.Login, userlogin) - cmd := &models.UpdateAuthInfoCommand{ + cmd := &login.UpdateAuthInfoCommand{ UserId: user.ID, AuthId: query.AuthId, AuthModule: query.AuthModule, @@ -197,7 +196,7 @@ func TestUserAuth(t *testing.T) { require.Nil(t, err) - getAuthQuery := &models.GetAuthInfoQuery{ + getAuthQuery := &login.GetAuthInfoQuery{ UserId: user.ID, } @@ -230,34 +229,34 @@ func TestUserAuth(t *testing.T) { } // Find a user to set tokens on - login := "loginuser0" + userlogin := "loginuser0" // Calling srv.LookupAndUpdateQuery on an existing user will populate an entry in the user_auth table // Make the first log-in during the past database.GetTime = func() time.Time { return time.Now().AddDate(0, 0, -2) } - query := &models.GetUserByAuthInfoQuery{AuthModule: "test1", AuthId: "test1", UserLookupParams: models.UserLookupParams{ - Login: &login, + query := &login.GetUserByAuthInfoQuery{AuthModule: "test1", AuthId: "test1", UserLookupParams: login.UserLookupParams{ + Login: &userlogin, }} user, err := srv.LookupAndUpdate(context.Background(), query) database.GetTime = time.Now require.Nil(t, err) - require.Equal(t, user.Login, login) + require.Equal(t, user.Login, userlogin) // Add a second auth module for this user // Have this module's last log-in be more recent database.GetTime = func() time.Time { return time.Now().AddDate(0, 0, -1) } - query = &models.GetUserByAuthInfoQuery{AuthModule: "test2", AuthId: "test2", UserLookupParams: models.UserLookupParams{ - Login: &login, + query = &login.GetUserByAuthInfoQuery{AuthModule: "test2", AuthId: "test2", UserLookupParams: login.UserLookupParams{ + Login: &userlogin, }} user, err = srv.LookupAndUpdate(context.Background(), query) database.GetTime = time.Now require.Nil(t, err) - require.Equal(t, user.Login, login) + require.Equal(t, user.Login, userlogin) authInfoStore.ExpectedOAuth.AuthModule = "test2" // Get the latest entry by not supply an authmodule or authid - getAuthQuery := &models.GetAuthInfoQuery{ + getAuthQuery := &login.GetAuthInfoQuery{ UserId: user.ID, } @@ -267,13 +266,13 @@ func TestUserAuth(t *testing.T) { require.Equal(t, getAuthQuery.Result.AuthModule, "test2") // "log in" again with the first auth module - updateAuthCmd := &models.UpdateAuthInfoCommand{UserId: user.ID, AuthModule: "test1", AuthId: "test1"} + updateAuthCmd := &login.UpdateAuthInfoCommand{UserId: user.ID, AuthModule: "test1", AuthId: "test1"} err = authInfoStore.UpdateAuthInfo(context.Background(), updateAuthCmd) require.Nil(t, err) authInfoStore.ExpectedOAuth.AuthModule = "test1" // Get the latest entry by not supply an authmodule or authid - getAuthQuery = &models.GetAuthInfoQuery{ + getAuthQuery = &login.GetAuthInfoQuery{ UserId: user.ID, } @@ -303,33 +302,33 @@ func TestUserAuth(t *testing.T) { } // Find a user to set tokens on - login := "loginuser0" + userlogin := "loginuser0" fixedTime := time.Now() // Calling srv.LookupAndUpdateQuery on an existing user will populate an entry in the user_auth table // Make the first log-in during the past database.GetTime = func() time.Time { return fixedTime.AddDate(0, 0, -2) } - queryOne := &models.GetUserByAuthInfoQuery{AuthModule: "test1", AuthId: "test1", UserLookupParams: models.UserLookupParams{ - Login: &login, + queryOne := &login.GetUserByAuthInfoQuery{AuthModule: "test1", AuthId: "test1", UserLookupParams: login.UserLookupParams{ + Login: &userlogin, }} user, err := srv.LookupAndUpdate(context.Background(), queryOne) database.GetTime = time.Now require.Nil(t, err) - require.Equal(t, user.Login, login) + require.Equal(t, user.Login, userlogin) // Add a second auth module for this user // Have this module's last log-in be more recent database.GetTime = func() time.Time { return fixedTime.AddDate(0, 0, -1) } - queryTwo := &models.GetUserByAuthInfoQuery{AuthModule: "test2", AuthId: "test2", UserLookupParams: models.UserLookupParams{ - Login: &login, + queryTwo := &login.GetUserByAuthInfoQuery{AuthModule: "test2", AuthId: "test2", UserLookupParams: login.UserLookupParams{ + Login: &userlogin, }} user, err = srv.LookupAndUpdate(context.Background(), queryTwo) require.Nil(t, err) - require.Equal(t, user.Login, login) + require.Equal(t, user.Login, userlogin) // Get the latest entry by not supply an authmodule or authid - getAuthQuery := &models.GetAuthInfoQuery{ + getAuthQuery := &login.GetAuthInfoQuery{ UserId: user.ID, } authInfoStore.ExpectedOAuth.AuthModule = "test2" @@ -343,7 +342,7 @@ func TestUserAuth(t *testing.T) { database.GetTime = func() time.Time { return fixedTime } // add oauth info to auth_info to make sure update date does not overwrite it - updateAuthCmd := &models.UpdateAuthInfoCommand{UserId: user.ID, AuthModule: "test1", AuthId: "test1", OAuthToken: &oauth2.Token{ + updateAuthCmd := &login.UpdateAuthInfoCommand{UserId: user.ID, AuthModule: "test1", AuthId: "test1", OAuthToken: &oauth2.Token{ AccessToken: "access_token", TokenType: "token_type", RefreshToken: "refresh_token", @@ -354,7 +353,7 @@ func TestUserAuth(t *testing.T) { user, err = srv.LookupAndUpdate(context.Background(), queryOne) require.Nil(t, err) - require.Equal(t, user.Login, login) + require.Equal(t, user.Login, userlogin) authInfoStore.ExpectedOAuth.AuthModule = "test1" authInfoStore.ExpectedOAuth.OAuthAccessToken = "access_token" err = authInfoStore.GetAuthInfo(context.Background(), getAuthQuery) @@ -368,7 +367,7 @@ func TestUserAuth(t *testing.T) { database.GetTime = func() time.Time { return fixedTime.AddDate(0, 0, 1) } user, err = srv.LookupAndUpdate(context.Background(), queryTwo) require.Nil(t, err) - require.Equal(t, user.Login, login) + require.Equal(t, user.Login, userlogin) authInfoStore.ExpectedOAuth.AuthModule = "test2" err = authInfoStore.GetAuthInfo(context.Background(), getAuthQuery) @@ -376,7 +375,7 @@ func TestUserAuth(t *testing.T) { require.Equal(t, "test2", getAuthQuery.Result.AuthModule) // Ensure test 1 did not have its entry modified - getAuthQueryUnchanged := &models.GetAuthInfoQuery{ + getAuthQueryUnchanged := &login.GetAuthInfoQuery{ UserId: user.ID, AuthModule: "test1", } @@ -389,23 +388,23 @@ func TestUserAuth(t *testing.T) { t.Run("Can set & locate by generic oauth auth module and user id", func(t *testing.T) { // Find a user to set tokens on - login := "loginuser0" + userlogin := "loginuser0" // Expect to pass since there's a matching login user database.GetTime = func() time.Time { return time.Now().AddDate(0, 0, -2) } - query := &models.GetUserByAuthInfoQuery{AuthModule: genericOAuthModule, AuthId: "", UserLookupParams: models.UserLookupParams{ - Login: &login, + query := &login.GetUserByAuthInfoQuery{AuthModule: genericOAuthModule, AuthId: "", UserLookupParams: login.UserLookupParams{ + Login: &userlogin, }} user, err := srv.LookupAndUpdate(context.Background(), query) database.GetTime = time.Now require.Nil(t, err) - require.Equal(t, user.Login, login) + require.Equal(t, user.Login, userlogin) otherLoginUser := "aloginuser" // Should throw a "user not found" error since there's no matching login user database.GetTime = func() time.Time { return time.Now().AddDate(0, 0, -2) } - query = &models.GetUserByAuthInfoQuery{AuthModule: genericOAuthModule, AuthId: "", UserLookupParams: models.UserLookupParams{ + query = &login.GetUserByAuthInfoQuery{AuthModule: genericOAuthModule, AuthId: "", UserLookupParams: login.UserLookupParams{ Login: &otherLoginUser, }} authInfoStore.ExpectedError = errors.New("some error") @@ -510,7 +509,7 @@ type FakeAuthInfoStore struct { login.AuthInfoService ExpectedError error ExpectedUser *user.User - ExpectedOAuth *models.UserAuth + ExpectedOAuth *login.UserAuth ExpectedDuplicateUserEntries int ExpectedHasDuplicateUserEntries int ExpectedLoginStats login.LoginStats @@ -520,23 +519,23 @@ func newFakeAuthInfoStore() *FakeAuthInfoStore { return &FakeAuthInfoStore{} } -func (f *FakeAuthInfoStore) GetExternalUserInfoByLogin(ctx context.Context, query *models.GetExternalUserInfoByLoginQuery) error { +func (f *FakeAuthInfoStore) GetExternalUserInfoByLogin(ctx context.Context, query *login.GetExternalUserInfoByLoginQuery) error { return f.ExpectedError } -func (f *FakeAuthInfoStore) GetAuthInfo(ctx context.Context, query *models.GetAuthInfoQuery) error { +func (f *FakeAuthInfoStore) GetAuthInfo(ctx context.Context, query *login.GetAuthInfoQuery) error { query.Result = f.ExpectedOAuth return f.ExpectedError } -func (f *FakeAuthInfoStore) SetAuthInfo(ctx context.Context, cmd *models.SetAuthInfoCommand) error { +func (f *FakeAuthInfoStore) SetAuthInfo(ctx context.Context, cmd *login.SetAuthInfoCommand) error { return f.ExpectedError } -func (f *FakeAuthInfoStore) UpdateAuthInfoDate(ctx context.Context, authInfo *models.UserAuth) error { +func (f *FakeAuthInfoStore) UpdateAuthInfoDate(ctx context.Context, authInfo *login.UserAuth) error { return f.ExpectedError } -func (f *FakeAuthInfoStore) UpdateAuthInfo(ctx context.Context, cmd *models.UpdateAuthInfoCommand) error { +func (f *FakeAuthInfoStore) UpdateAuthInfo(ctx context.Context, cmd *login.UpdateAuthInfoCommand) error { return f.ExpectedError } -func (f *FakeAuthInfoStore) DeleteAuthInfo(ctx context.Context, cmd *models.DeleteAuthInfoCommand) error { +func (f *FakeAuthInfoStore) DeleteAuthInfo(ctx context.Context, cmd *login.DeleteAuthInfoCommand) error { return f.ExpectedError } func (f *FakeAuthInfoStore) GetUserById(ctx context.Context, id int64) (*user.User, error) { diff --git a/pkg/services/login/login.go b/pkg/services/login/login.go index bb1ff508e4a..908d1c43038 100644 --- a/pkg/services/login/login.go +++ b/pkg/services/login/login.go @@ -4,7 +4,6 @@ import ( "context" "errors" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/user" ) @@ -15,10 +14,10 @@ var ( ErrSignupNotAllowed = errors.New("system administrator has disabled signup") ) -type TeamSyncFunc func(user *user.User, externalUser *models.ExternalUserInfo) error +type TeamSyncFunc func(user *user.User, externalUser *ExternalUserInfo) error type Service interface { - UpsertUser(ctx context.Context, cmd *models.UpsertUserCommand) error + UpsertUser(ctx context.Context, cmd *UpsertUserCommand) error DisableExternalUser(ctx context.Context, username string) error SetTeamSyncFunc(TeamSyncFunc) } diff --git a/pkg/services/login/loginservice/loginservice.go b/pkg/services/login/loginservice/loginservice.go index 89cc5351dae..0b18e713136 100644 --- a/pkg/services/login/loginservice/loginservice.go +++ b/pkg/services/login/loginservice/loginservice.go @@ -5,7 +5,6 @@ import ( "errors" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" @@ -44,10 +43,10 @@ type Implementation struct { } // UpsertUser updates an existing user, or if it doesn't exist, inserts a new one. -func (ls *Implementation) UpsertUser(ctx context.Context, cmd *models.UpsertUserCommand) error { +func (ls *Implementation) UpsertUser(ctx context.Context, cmd *login.UpsertUserCommand) error { extUser := cmd.ExternalUser - usr, errAuthLookup := ls.AuthInfoService.LookupAndUpdate(ctx, &models.GetUserByAuthInfoQuery{ + usr, errAuthLookup := ls.AuthInfoService.LookupAndUpdate(ctx, &login.GetUserByAuthInfoQuery{ AuthModule: extUser.AuthModule, AuthId: extUser.AuthId, UserLookupParams: cmd.UserLookupParams, @@ -109,7 +108,7 @@ func (ls *Implementation) UpsertUser(ctx context.Context, cmd *models.UpsertUser } if extUser.AuthModule != "" { - cmd2 := &models.SetAuthInfoCommand{ + cmd2 := &login.SetAuthInfoCommand{ UserId: cmd.Result.ID, AuthModule: extUser.AuthModule, AuthId: extUser.AuthId, @@ -166,7 +165,7 @@ func (ls *Implementation) UpsertUser(ctx context.Context, cmd *models.UpsertUser func (ls *Implementation) DisableExternalUser(ctx context.Context, username string) error { // Check if external user exist in Grafana - userQuery := &models.GetExternalUserInfoByLoginQuery{ + userQuery := &login.GetExternalUserInfoByLoginQuery{ LoginOrEmail: username, } @@ -209,7 +208,7 @@ func (ls *Implementation) SetTeamSyncFunc(teamSyncFunc login.TeamSyncFunc) { ls.TeamSync = teamSyncFunc } -func (ls *Implementation) updateUser(ctx context.Context, usr *user.User, extUser *models.ExternalUserInfo) error { +func (ls *Implementation) updateUser(ctx context.Context, usr *user.User, extUser *login.ExternalUserInfo) error { // sync user info updateCmd := &user.UpdateUserCommand{ UserID: usr.ID, @@ -242,8 +241,8 @@ func (ls *Implementation) updateUser(ctx context.Context, usr *user.User, extUse return ls.userService.Update(ctx, updateCmd) } -func (ls *Implementation) updateUserAuth(ctx context.Context, user *user.User, extUser *models.ExternalUserInfo) error { - updateCmd := &models.UpdateAuthInfoCommand{ +func (ls *Implementation) updateUserAuth(ctx context.Context, user *user.User, extUser *login.ExternalUserInfo) error { + updateCmd := &login.UpdateAuthInfoCommand{ AuthModule: extUser.AuthModule, AuthId: extUser.AuthId, UserId: user.ID, @@ -254,7 +253,7 @@ func (ls *Implementation) updateUserAuth(ctx context.Context, user *user.User, e return ls.AuthInfoService.UpdateAuthInfo(ctx, updateCmd) } -func (ls *Implementation) syncOrgRoles(ctx context.Context, usr *user.User, extUser *models.ExternalUserInfo) error { +func (ls *Implementation) syncOrgRoles(ctx context.Context, usr *user.User, extUser *login.ExternalUserInfo) error { logger.Debug("Syncing organization roles", "id", usr.ID, "extOrgRoles", extUser.OrgRoles) // don't sync org roles if none is specified diff --git a/pkg/services/login/loginservice/loginservice_mock.go b/pkg/services/login/loginservice/loginservice_mock.go index b77ac1b4caa..dbb39e0fcf6 100644 --- a/pkg/services/login/loginservice/loginservice_mock.go +++ b/pkg/services/login/loginservice/loginservice_mock.go @@ -3,7 +3,6 @@ package loginservice import ( "context" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/user" ) @@ -11,11 +10,11 @@ import ( type LoginServiceMock struct { login.Service ExpectedUser *user.User - ExpectedUserFunc func(cmd *models.UpsertUserCommand) *user.User + ExpectedUserFunc func(cmd *login.UpsertUserCommand) *user.User ExpectedError error } -func (s LoginServiceMock) UpsertUser(ctx context.Context, cmd *models.UpsertUserCommand) error { +func (s LoginServiceMock) UpsertUser(ctx context.Context, cmd *login.UpsertUserCommand) error { if s.ExpectedUserFunc != nil { cmd.Result = s.ExpectedUserFunc(cmd) return s.ExpectedError diff --git a/pkg/services/login/loginservice/loginservice_test.go b/pkg/services/login/loginservice/loginservice_test.go index c7d5a4762bc..626c3f519b6 100644 --- a/pkg/services/login/loginservice/loginservice_test.go +++ b/pkg/services/login/loginservice/loginservice_test.go @@ -8,7 +8,9 @@ import ( "github.com/go-kit/log" "github.com/go-kit/log/level" - "github.com/grafana/grafana/pkg/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/accesscontrol/actest" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/login/logintest" @@ -17,8 +19,6 @@ import ( "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/usertest" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func Test_syncOrgRoles_doesNotBreakWhenTryingToRemoveLastOrgAdmin(t *testing.T) { @@ -65,14 +65,14 @@ func Test_syncOrgRoles_whenTryingToRemoveLastOrgLogsError(t *testing.T) { func Test_teamSync(t *testing.T) { authInfoMock := &logintest.AuthInfoServiceFake{} - login := Implementation{ + loginsvc := Implementation{ QuotaService: quotatest.New(false, nil), AuthInfoService: authInfoMock, } email := "test_user@example.org" - upsertCmd := &models.UpsertUserCommand{ExternalUser: &models.ExternalUserInfo{Email: email}, - UserLookupParams: models.UserLookupParams{Email: &email}} + upsertCmd := &login.UpsertUserCommand{ExternalUser: &login.ExternalUserInfo{Email: email}, + UserLookupParams: login.UserLookupParams{Email: &email}} expectedUser := &user.User{ ID: 1, Email: email, @@ -82,22 +82,22 @@ func Test_teamSync(t *testing.T) { authInfoMock.ExpectedUser = expectedUser var actualUser *user.User - var actualExternalUser *models.ExternalUserInfo + var actualExternalUser *login.ExternalUserInfo t.Run("login.TeamSync should not be called when nil", func(t *testing.T) { - err := login.UpsertUser(context.Background(), upsertCmd) + err := loginsvc.UpsertUser(context.Background(), upsertCmd) require.Nil(t, err) assert.Nil(t, actualUser) assert.Nil(t, actualExternalUser) t.Run("login.TeamSync should be called when not nil", func(t *testing.T) { - teamSyncFunc := func(user *user.User, externalUser *models.ExternalUserInfo) error { + teamSyncFunc := func(user *user.User, externalUser *login.ExternalUserInfo) error { actualUser = user actualExternalUser = externalUser return nil } - login.TeamSync = teamSyncFunc - err := login.UpsertUser(context.Background(), upsertCmd) + loginsvc.TeamSync = teamSyncFunc + err := loginsvc.UpsertUser(context.Background(), upsertCmd) require.Nil(t, err) assert.Equal(t, actualUser, expectedUser) assert.Equal(t, actualExternalUser, upsertCmd.ExternalUser) @@ -105,33 +105,33 @@ func Test_teamSync(t *testing.T) { t.Run("login.TeamSync should not be called when not nil and skipTeamSync is set for externalUserInfo", func(t *testing.T) { var actualUser *user.User - var actualExternalUser *models.ExternalUserInfo - upsertCmdSkipTeamSync := &models.UpsertUserCommand{ - ExternalUser: &models.ExternalUserInfo{ + var actualExternalUser *login.ExternalUserInfo + upsertCmdSkipTeamSync := &login.UpsertUserCommand{ + ExternalUser: &login.ExternalUserInfo{ Email: email, // sending in ExternalUserInfo with SkipTeamSync yields no team sync SkipTeamSync: true, }, - UserLookupParams: models.UserLookupParams{Email: &email}, + UserLookupParams: login.UserLookupParams{Email: &email}, } - teamSyncFunc := func(user *user.User, externalUser *models.ExternalUserInfo) error { + teamSyncFunc := func(user *user.User, externalUser *login.ExternalUserInfo) error { actualUser = user actualExternalUser = externalUser return nil } - login.TeamSync = teamSyncFunc - err := login.UpsertUser(context.Background(), upsertCmdSkipTeamSync) + loginsvc.TeamSync = teamSyncFunc + err := loginsvc.UpsertUser(context.Background(), upsertCmdSkipTeamSync) require.Nil(t, err) assert.Nil(t, actualUser) assert.Nil(t, actualExternalUser) }) t.Run("login.TeamSync should propagate its errors to the caller", func(t *testing.T) { - teamSyncFunc := func(user *user.User, externalUser *models.ExternalUserInfo) error { + teamSyncFunc := func(user *user.User, externalUser *login.ExternalUserInfo) error { return errors.New("teamsync test error") } - login.TeamSync = teamSyncFunc - err := login.UpsertUser(context.Background(), upsertCmd) + loginsvc.TeamSync = teamSyncFunc + err := loginsvc.UpsertUser(context.Background(), upsertCmd) require.Error(t, err) }) }) @@ -166,8 +166,8 @@ func createUserOrgDTO() []*org.UserOrgDTO { return users } -func createSimpleExternalUser() models.ExternalUserInfo { - externalUser := models.ExternalUserInfo{ +func createSimpleExternalUser() login.ExternalUserInfo { + externalUser := login.ExternalUserInfo{ AuthModule: login.LDAPAuthModule, OrgRoles: map[int64]org.RoleType{ 1: org.RoleViewer, diff --git a/pkg/services/login/logintest/logintest.go b/pkg/services/login/logintest/logintest.go index 1ca3d532ca9..0c9c2b82944 100644 --- a/pkg/services/login/logintest/logintest.go +++ b/pkg/services/login/logintest/logintest.go @@ -3,14 +3,13 @@ package logintest import ( "context" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/user" ) type LoginServiceFake struct{} -func (l *LoginServiceFake) UpsertUser(ctx context.Context, cmd *models.UpsertUserCommand) error { +func (l *LoginServiceFake) UpsertUser(ctx context.Context, cmd *login.UpsertUserCommand) error { return nil } func (l *LoginServiceFake) DisableExternalUser(ctx context.Context, username string) error { @@ -21,17 +20,17 @@ func (l *LoginServiceFake) SetTeamSyncFunc(login.TeamSyncFunc) {} type AuthInfoServiceFake struct { login.AuthInfoService LatestUserID int64 - ExpectedUserAuth *models.UserAuth + ExpectedUserAuth *login.UserAuth ExpectedUser *user.User - ExpectedExternalUser *models.ExternalUserInfo + ExpectedExternalUser *login.ExternalUserInfo ExpectedError error ExpectedLabels map[int64]string - SetAuthInfoFn func(ctx context.Context, cmd *models.SetAuthInfoCommand) error - UpdateAuthInfoFn func(ctx context.Context, cmd *models.UpdateAuthInfoCommand) error + SetAuthInfoFn func(ctx context.Context, cmd *login.SetAuthInfoCommand) error + UpdateAuthInfoFn func(ctx context.Context, cmd *login.UpdateAuthInfoCommand) error } -func (a *AuthInfoServiceFake) LookupAndUpdate(ctx context.Context, query *models.GetUserByAuthInfoQuery) (*user.User, error) { +func (a *AuthInfoServiceFake) LookupAndUpdate(ctx context.Context, query *login.GetUserByAuthInfoQuery) (*user.User, error) { if query.UserLookupParams.UserID != nil { a.LatestUserID = *query.UserLookupParams.UserID } else { @@ -40,17 +39,17 @@ func (a *AuthInfoServiceFake) LookupAndUpdate(ctx context.Context, query *models return a.ExpectedUser, a.ExpectedError } -func (a *AuthInfoServiceFake) GetAuthInfo(ctx context.Context, query *models.GetAuthInfoQuery) error { +func (a *AuthInfoServiceFake) GetAuthInfo(ctx context.Context, query *login.GetAuthInfoQuery) error { a.LatestUserID = query.UserId query.Result = a.ExpectedUserAuth return a.ExpectedError } -func (a *AuthInfoServiceFake) GetUserLabels(ctx context.Context, query models.GetUserLabelsQuery) (map[int64]string, error) { +func (a *AuthInfoServiceFake) GetUserLabels(ctx context.Context, query login.GetUserLabelsQuery) (map[int64]string, error) { return a.ExpectedLabels, a.ExpectedError } -func (a *AuthInfoServiceFake) SetAuthInfo(ctx context.Context, cmd *models.SetAuthInfoCommand) error { +func (a *AuthInfoServiceFake) SetAuthInfo(ctx context.Context, cmd *login.SetAuthInfoCommand) error { if a.SetAuthInfoFn != nil { return a.SetAuthInfoFn(ctx, cmd) } @@ -58,7 +57,7 @@ func (a *AuthInfoServiceFake) SetAuthInfo(ctx context.Context, cmd *models.SetAu return a.ExpectedError } -func (a *AuthInfoServiceFake) UpdateAuthInfo(ctx context.Context, cmd *models.UpdateAuthInfoCommand) error { +func (a *AuthInfoServiceFake) UpdateAuthInfo(ctx context.Context, cmd *login.UpdateAuthInfoCommand) error { if a.UpdateAuthInfoFn != nil { return a.UpdateAuthInfoFn(ctx, cmd) } @@ -66,7 +65,7 @@ func (a *AuthInfoServiceFake) UpdateAuthInfo(ctx context.Context, cmd *models.Up return a.ExpectedError } -func (a *AuthInfoServiceFake) GetExternalUserInfoByLogin(ctx context.Context, query *models.GetExternalUserInfoByLoginQuery) error { +func (a *AuthInfoServiceFake) GetExternalUserInfoByLogin(ctx context.Context, query *login.GetExternalUserInfoByLoginQuery) error { query.Result = a.ExpectedExternalUser return a.ExpectedError } @@ -80,7 +79,7 @@ type AuthenticatorFake struct { ExpectedError error } -func (a *AuthenticatorFake) AuthenticateUser(c context.Context, query *models.LoginUserQuery) error { +func (a *AuthenticatorFake) AuthenticateUser(c context.Context, query *login.LoginUserQuery) error { query.User = a.ExpectedUser return a.ExpectedError } diff --git a/pkg/services/login/model.go b/pkg/services/login/model.go index a69b7d9fcba..60a3526872b 100644 --- a/pkg/services/login/model.go +++ b/pkg/services/login/model.go @@ -1,10 +1,17 @@ package login import ( + "fmt" "sync" "time" "github.com/prometheus/client_golang/prometheus" + "golang.org/x/oauth2" + + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" ) type LoginStats struct { @@ -30,3 +37,122 @@ var ( Once sync.Once Initialised bool = false ) + +type UserAuth struct { + Id int64 + UserId int64 + AuthModule string + AuthId string + Created time.Time + OAuthAccessToken string + OAuthRefreshToken string + OAuthIdToken string + OAuthTokenType string + OAuthExpiry time.Time +} + +type ExternalUserInfo struct { + OAuthToken *oauth2.Token + AuthModule string + AuthId string + UserId int64 + Email string + Login string + Name string + Groups []string + OrgRoles map[int64]org.RoleType + IsGrafanaAdmin *bool // This is a pointer to know if we should sync this or not (nil = ignore sync) + IsDisabled bool + SkipTeamSync bool +} + +func (e *ExternalUserInfo) String() string { + return fmt.Sprintf("%+v", *e) +} + +type LoginInfo struct { + AuthModule string + User *user.User + ExternalUser ExternalUserInfo + LoginUsername string + HTTPStatus int + Error error +} + +// RequestURIKey is used as key to save request URI in contexts +// (used for the Enterprise auditing feature) +type RequestURIKey struct{} + +// --------------------- +// COMMANDS + +type UpsertUserCommand struct { + ReqContext *contextmodel.ReqContext + ExternalUser *ExternalUserInfo + UserLookupParams + SignupAllowed bool + + Result *user.User +} + +type SetAuthInfoCommand struct { + AuthModule string + AuthId string + UserId int64 + OAuthToken *oauth2.Token +} + +type UpdateAuthInfoCommand struct { + AuthModule string + AuthId string + UserId int64 + OAuthToken *oauth2.Token +} + +type DeleteAuthInfoCommand struct { + UserAuth *UserAuth +} + +// ---------------------- +// QUERIES + +type LoginUserQuery struct { + ReqContext *contextmodel.ReqContext + Username string + Password string + User *user.User + IpAddress string + AuthModule string + Cfg *setting.Cfg +} + +type GetUserByAuthInfoQuery struct { + AuthModule string + AuthId string + UserLookupParams +} + +type UserLookupParams struct { + // Describes lookup order as well + UserID *int64 // if set, will try to find the user by id + Email *string // if set, will try to find the user by email + Login *string // if set, will try to find the user by login +} + +type GetExternalUserInfoByLoginQuery struct { + LoginOrEmail string + + Result *ExternalUserInfo +} + +type GetAuthInfoQuery struct { + UserId int64 + AuthModule string + AuthId string + + Result *UserAuth +} + +type GetUserLabelsQuery struct { + UserIDs []int64 +} diff --git a/pkg/services/login/userprotection.go b/pkg/services/login/userprotection.go index f77b6401244..4f94d6a030b 100644 --- a/pkg/services/login/userprotection.go +++ b/pkg/services/login/userprotection.go @@ -3,7 +3,6 @@ package login import ( "context" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/user" ) @@ -12,13 +11,13 @@ type UserProtectionService interface { } type Store interface { - GetExternalUserInfoByLogin(ctx context.Context, query *models.GetExternalUserInfoByLoginQuery) error - GetAuthInfo(ctx context.Context, query *models.GetAuthInfoQuery) error - GetUserLabels(ctx context.Context, query models.GetUserLabelsQuery) (map[int64]string, error) - SetAuthInfo(ctx context.Context, cmd *models.SetAuthInfoCommand) error - UpdateAuthInfo(ctx context.Context, cmd *models.UpdateAuthInfoCommand) error - UpdateAuthInfoDate(ctx context.Context, authInfo *models.UserAuth) error - DeleteAuthInfo(ctx context.Context, cmd *models.DeleteAuthInfoCommand) error + GetExternalUserInfoByLogin(ctx context.Context, query *GetExternalUserInfoByLoginQuery) error + GetAuthInfo(ctx context.Context, query *GetAuthInfoQuery) error + GetUserLabels(ctx context.Context, query GetUserLabelsQuery) (map[int64]string, error) + SetAuthInfo(ctx context.Context, cmd *SetAuthInfoCommand) error + UpdateAuthInfo(ctx context.Context, cmd *UpdateAuthInfoCommand) error + UpdateAuthInfoDate(ctx context.Context, authInfo *UserAuth) error + DeleteAuthInfo(ctx context.Context, cmd *DeleteAuthInfoCommand) error GetUserById(ctx context.Context, id int64) (*user.User, error) GetUserByLogin(ctx context.Context, login string) (*user.User, error) GetUserByEmail(ctx context.Context, email string) (*user.User, error) diff --git a/pkg/services/multildap/multidap_mock.go b/pkg/services/multildap/multidap_mock.go index d59ca876f00..31b2557d0a3 100644 --- a/pkg/services/multildap/multidap_mock.go +++ b/pkg/services/multildap/multidap_mock.go @@ -1,8 +1,8 @@ package multildap import ( - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/ldap" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/user" ) @@ -16,25 +16,25 @@ type MultiLDAPmock struct { ExpectedErr error } -func (m *MultiLDAPmock) Login(query *models.LoginUserQuery) ( - *models.ExternalUserInfo, error, +func (m *MultiLDAPmock) Login(query *login.LoginUserQuery) ( + *login.ExternalUserInfo, error, ) { m.LoginCalled = true query.User = m.UserInfo query.AuthModule = m.AuthModule - result := &models.ExternalUserInfo{ + result := &login.ExternalUserInfo{ UserId: m.ID, } return result, m.ExpectedErr } -func (m *MultiLDAPmock) User(login string) ( - *models.ExternalUserInfo, +func (m *MultiLDAPmock) User(loginstr string) ( + *login.ExternalUserInfo, ldap.ServerConfig, error, ) { m.UserCalled = true - result := &models.ExternalUserInfo{ + result := &login.ExternalUserInfo{ UserId: m.ID, } return result, ldap.ServerConfig{}, nil diff --git a/pkg/services/multildap/multildap.go b/pkg/services/multildap/multildap.go index b9ea8bd9ef3..25ec86fde72 100644 --- a/pkg/services/multildap/multildap.go +++ b/pkg/services/multildap/multildap.go @@ -4,8 +4,8 @@ import ( "errors" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/ldap" + "github.com/grafana/grafana/pkg/services/login" ) // logger to log @@ -43,16 +43,16 @@ type ServerStatus struct { // IMultiLDAP is interface for MultiLDAP type IMultiLDAP interface { Ping() ([]*ServerStatus, error) - Login(query *models.LoginUserQuery) ( - *models.ExternalUserInfo, error, + Login(query *login.LoginUserQuery) ( + *login.ExternalUserInfo, error, ) Users(logins []string) ( - []*models.ExternalUserInfo, error, + []*login.ExternalUserInfo, error, ) User(login string) ( - *models.ExternalUserInfo, ldap.ServerConfig, error, + *login.ExternalUserInfo, ldap.ServerConfig, error, ) } @@ -99,8 +99,8 @@ func (multiples *MultiLDAP) Ping() ([]*ServerStatus, error) { } // Login tries to log in the user in multiples LDAP -func (multiples *MultiLDAP) Login(query *models.LoginUserQuery) ( - *models.ExternalUserInfo, error, +func (multiples *MultiLDAP) Login(query *login.LoginUserQuery) ( + *login.ExternalUserInfo, error, ) { if len(multiples.configs) == 0 { return nil, ErrNoLDAPServers @@ -157,7 +157,7 @@ func (multiples *MultiLDAP) Login(query *models.LoginUserQuery) ( // User attempts to find an user by login/username by searching into all of the configured LDAP servers. Then, if the user is found it returns the user alongisde the server it was found. func (multiples *MultiLDAP) User(login string) ( - *models.ExternalUserInfo, + *login.ExternalUserInfo, ldap.ServerConfig, error, ) { @@ -200,10 +200,10 @@ func (multiples *MultiLDAP) User(login string) ( // Users gets users from multiple LDAP servers func (multiples *MultiLDAP) Users(logins []string) ( - []*models.ExternalUserInfo, + []*login.ExternalUserInfo, error, ) { - var result []*models.ExternalUserInfo + var result []*login.ExternalUserInfo if len(multiples.configs) == 0 { return nil, ErrNoLDAPServers diff --git a/pkg/services/multildap/multildap_test.go b/pkg/services/multildap/multildap_test.go index cbf33532b2d..8d61448cdde 100644 --- a/pkg/services/multildap/multildap_test.go +++ b/pkg/services/multildap/multildap_test.go @@ -4,8 +4,8 @@ import ( "errors" "testing" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/ldap" + "github.com/grafana/grafana/pkg/services/login" "github.com/stretchr/testify/require" @@ -71,7 +71,7 @@ func TestMultiLDAP(t *testing.T) { setup() multi := New([]*ldap.ServerConfig{}) - _, err := multi.Login(&models.LoginUserQuery{}) + _, err := multi.Login(&login.LoginUserQuery{}) require.Error(t, err) require.Equal(t, ErrNoLDAPServers, err) @@ -89,7 +89,7 @@ func TestMultiLDAP(t *testing.T) { {}, {}, }) - _, err := multi.Login(&models.LoginUserQuery{}) + _, err := multi.Login(&login.LoginUserQuery{}) require.Error(t, err) require.Equal(t, expected, err) @@ -104,7 +104,7 @@ func TestMultiLDAP(t *testing.T) { multi := New([]*ldap.ServerConfig{ {}, {}, }) - _, err := multi.Login(&models.LoginUserQuery{}) + _, err := multi.Login(&login.LoginUserQuery{}) require.Equal(t, 2, mock.dialCalledTimes) require.Equal(t, 2, mock.loginCalledTimes) @@ -118,14 +118,14 @@ func TestMultiLDAP(t *testing.T) { t.Run("Should get login result", func(t *testing.T) { mock := setup() - mock.loginReturn = &models.ExternalUserInfo{ + mock.loginReturn = &login.ExternalUserInfo{ Login: "killa", } multi := New([]*ldap.ServerConfig{ {}, {}, }) - result, err := multi.Login(&models.LoginUserQuery{}) + result, err := multi.Login(&login.LoginUserQuery{}) require.Equal(t, 1, mock.dialCalledTimes) require.Equal(t, 1, mock.loginCalledTimes) @@ -145,7 +145,7 @@ func TestMultiLDAP(t *testing.T) { multi := New([]*ldap.ServerConfig{ {}, {}, }) - _, err := multi.Login(&models.LoginUserQuery{}) + _, err := multi.Login(&login.LoginUserQuery{}) require.Equal(t, 2, mock.dialCalledTimes) require.Equal(t, 2, mock.loginCalledTimes) @@ -164,7 +164,7 @@ func TestMultiLDAP(t *testing.T) { multi := New([]*ldap.ServerConfig{ {}, {}, }) - _, err := multi.Login(&models.LoginUserQuery{}) + _, err := multi.Login(&login.LoginUserQuery{}) require.Equal(t, 2, mock.dialCalledTimes) require.Equal(t, 2, mock.loginCalledTimes) @@ -184,7 +184,7 @@ func TestMultiLDAP(t *testing.T) { multi := New([]*ldap.ServerConfig{ {}, {}, }) - _, err := multi.Login(&models.LoginUserQuery{}) + _, err := multi.Login(&login.LoginUserQuery{}) require.Equal(t, 2, mock.dialCalledTimes) @@ -202,7 +202,7 @@ func TestMultiLDAP(t *testing.T) { multi := New([]*ldap.ServerConfig{ {}, {}, }) - _, err := multi.Login(&models.LoginUserQuery{}) + _, err := multi.Login(&login.LoginUserQuery{}) require.Equal(t, 1, mock.dialCalledTimes) require.Equal(t, 1, mock.loginCalledTimes) @@ -285,7 +285,7 @@ func TestMultiLDAP(t *testing.T) { t.Run("Should get only one user", func(t *testing.T) { mock := setup() - mock.usersFirstReturn = []*models.ExternalUserInfo{ + mock.usersFirstReturn = []*login.ExternalUserInfo{ { Login: "one", }, @@ -415,7 +415,7 @@ func TestMultiLDAP(t *testing.T) { t.Run("Should get users", func(t *testing.T) { mock := setup() - mock.usersFirstReturn = []*models.ExternalUserInfo{ + mock.usersFirstReturn = []*login.ExternalUserInfo{ { Login: "one", }, @@ -425,7 +425,7 @@ func TestMultiLDAP(t *testing.T) { }, } - mock.usersRestReturn = []*models.ExternalUserInfo{ + mock.usersRestReturn = []*login.ExternalUserInfo{ { Login: "three", }, @@ -461,23 +461,23 @@ type mockLDAP struct { dialErrReturn error loginErrReturn error - loginReturn *models.ExternalUserInfo + loginReturn *login.ExternalUserInfo bindErrReturn error usersErrReturn error - usersFirstReturn []*models.ExternalUserInfo - usersRestReturn []*models.ExternalUserInfo + usersFirstReturn []*login.ExternalUserInfo + usersRestReturn []*login.ExternalUserInfo } // Login test fn -func (mock *mockLDAP) Login(*models.LoginUserQuery) (*models.ExternalUserInfo, error) { +func (mock *mockLDAP) Login(*login.LoginUserQuery) (*login.ExternalUserInfo, error) { mock.loginCalledTimes++ return mock.loginReturn, mock.loginErrReturn } // Users test fn -func (mock *mockLDAP) Users([]string) ([]*models.ExternalUserInfo, error) { +func (mock *mockLDAP) Users([]string) ([]*login.ExternalUserInfo, error) { mock.usersCalledTimes++ if mock.usersCalledTimes == 1 { diff --git a/pkg/services/oauthtoken/oauth_token.go b/pkg/services/oauthtoken/oauth_token.go index f2fc1118ac5..c773abdab2c 100644 --- a/pkg/services/oauthtoken/oauth_token.go +++ b/pkg/services/oauthtoken/oauth_token.go @@ -12,7 +12,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/login/social" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/user" @@ -38,9 +37,9 @@ type Service struct { type OAuthTokenService interface { GetCurrentOAuthToken(context.Context, *user.SignedInUser) *oauth2.Token IsOAuthPassThruEnabled(*datasources.DataSource) bool - HasOAuthEntry(context.Context, *user.SignedInUser) (*models.UserAuth, bool, error) - TryTokenRefresh(context.Context, *models.UserAuth) error - InvalidateOAuthTokens(context.Context, *models.UserAuth) error + HasOAuthEntry(context.Context, *user.SignedInUser) (*login.UserAuth, bool, error) + TryTokenRefresh(context.Context, *login.UserAuth) error + InvalidateOAuthTokens(context.Context, *login.UserAuth) error } func ProvideService(socialService social.Service, authInfoService login.AuthInfoService, cfg *setting.Cfg) *Service { @@ -59,7 +58,7 @@ func (o *Service) GetCurrentOAuthToken(ctx context.Context, usr *user.SignedInUs return nil } - authInfoQuery := &models.GetAuthInfoQuery{UserId: usr.UserID} + authInfoQuery := &login.GetAuthInfoQuery{UserId: usr.UserID} if err := o.AuthInfoService.GetAuthInfo(ctx, authInfoQuery); err != nil { if errors.Is(err, user.ErrUserNotFound) { // Not necessarily an error. User may be logged in another way. @@ -88,13 +87,13 @@ func (o *Service) IsOAuthPassThruEnabled(ds *datasources.DataSource) bool { } // HasOAuthEntry returns true and the UserAuth object when OAuth info exists for the specified User -func (o *Service) HasOAuthEntry(ctx context.Context, usr *user.SignedInUser) (*models.UserAuth, bool, error) { +func (o *Service) HasOAuthEntry(ctx context.Context, usr *user.SignedInUser) (*login.UserAuth, bool, error) { if usr == nil { // No user, therefore no token return nil, false, nil } - authInfoQuery := &models.GetAuthInfoQuery{UserId: usr.UserID} + authInfoQuery := &login.GetAuthInfoQuery{UserId: usr.UserID} err := o.AuthInfoService.GetAuthInfo(ctx, authInfoQuery) if err != nil { if errors.Is(err, user.ErrUserNotFound) { @@ -112,7 +111,7 @@ func (o *Service) HasOAuthEntry(ctx context.Context, usr *user.SignedInUser) (*m // TryTokenRefresh returns an error in case the OAuth token refresh was unsuccessful // It uses a singleflight.Group to prevent getting the Refresh Token multiple times for a given User -func (o *Service) TryTokenRefresh(ctx context.Context, usr *models.UserAuth) error { +func (o *Service) TryTokenRefresh(ctx context.Context, usr *login.UserAuth) error { lockKey := fmt.Sprintf("oauth-refresh-token-%d", usr.UserId) _, err, _ := o.singleFlightGroup.Do(lockKey, func() (interface{}, error) { logger.Debug("singleflight request for getting a new access token", "key", lockKey) @@ -122,7 +121,7 @@ func (o *Service) TryTokenRefresh(ctx context.Context, usr *models.UserAuth) err return err } -func buildOAuthTokenFromAuthInfo(authInfo *models.UserAuth) *oauth2.Token { +func buildOAuthTokenFromAuthInfo(authInfo *login.UserAuth) *oauth2.Token { token := &oauth2.Token{ AccessToken: authInfo.OAuthAccessToken, Expiry: authInfo.OAuthExpiry, @@ -137,7 +136,7 @@ func buildOAuthTokenFromAuthInfo(authInfo *models.UserAuth) *oauth2.Token { return token } -func checkOAuthRefreshToken(authInfo *models.UserAuth) error { +func checkOAuthRefreshToken(authInfo *login.UserAuth) error { if !strings.Contains(authInfo.AuthModule, "oauth") { logger.Warn("the specified user's auth provider is not oauth", "authmodule", authInfo.AuthModule, "userid", authInfo.UserId) @@ -154,8 +153,8 @@ func checkOAuthRefreshToken(authInfo *models.UserAuth) error { } // InvalidateOAuthTokens invalidates the OAuth tokens (access_token, refresh_token) and sets the Expiry to default/zero -func (o *Service) InvalidateOAuthTokens(ctx context.Context, usr *models.UserAuth) error { - return o.AuthInfoService.UpdateAuthInfo(ctx, &models.UpdateAuthInfoCommand{ +func (o *Service) InvalidateOAuthTokens(ctx context.Context, usr *login.UserAuth) error { + return o.AuthInfoService.UpdateAuthInfo(ctx, &login.UpdateAuthInfoCommand{ UserId: usr.UserId, AuthModule: usr.AuthModule, AuthId: usr.AuthId, @@ -167,7 +166,7 @@ func (o *Service) InvalidateOAuthTokens(ctx context.Context, usr *models.UserAut }) } -func (o *Service) tryGetOrRefreshAccessToken(ctx context.Context, usr *models.UserAuth) (*oauth2.Token, error) { +func (o *Service) tryGetOrRefreshAccessToken(ctx context.Context, usr *login.UserAuth) (*oauth2.Token, error) { if err := checkOAuthRefreshToken(usr); err != nil { return nil, err } @@ -198,7 +197,7 @@ func (o *Service) tryGetOrRefreshAccessToken(ctx context.Context, usr *models.Us // If the tokens are not the same, update the entry in the DB if !tokensEq(persistedToken, token) { - updateAuthCommand := &models.UpdateAuthInfoCommand{ + updateAuthCommand := &login.UpdateAuthInfoCommand{ UserId: usr.UserId, AuthModule: usr.AuthModule, AuthId: usr.AuthId, diff --git a/pkg/services/oauthtoken/oauth_token_test.go b/pkg/services/oauthtoken/oauth_token_test.go index 063de3ce3f2..1392d1e229e 100644 --- a/pkg/services/oauthtoken/oauth_token_test.go +++ b/pkg/services/oauthtoken/oauth_token_test.go @@ -8,29 +8,29 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/infra/usagestats" - "github.com/grafana/grafana/pkg/login/social" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/login" - "github.com/grafana/grafana/pkg/services/login/authinfoservice" - "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "golang.org/x/oauth2" "golang.org/x/sync/singleflight" + + "github.com/grafana/grafana/pkg/infra/usagestats" + "github.com/grafana/grafana/pkg/login/social" + "github.com/grafana/grafana/pkg/services/login" + "github.com/grafana/grafana/pkg/services/login/authinfoservice" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" ) func TestService_HasOAuthEntry(t *testing.T) { testCases := []struct { name string user *user.SignedInUser - want *models.UserAuth + want *login.UserAuth wantExist bool wantErr bool err error getAuthInfoErr error - getAuthInfoUser models.UserAuth + getAuthInfoUser login.UserAuth }{ { name: "returns false without an error in case user is nil", @@ -61,15 +61,15 @@ func TestService_HasOAuthEntry(t *testing.T) { want: nil, wantExist: false, wantErr: false, - getAuthInfoUser: models.UserAuth{AuthModule: "auth_saml"}, + getAuthInfoUser: login.UserAuth{AuthModule: "auth_saml"}, }, { name: "returns true when the auth entry is found", user: &user.SignedInUser{}, - want: &models.UserAuth{AuthModule: "oauth_generic_oauth"}, + want: &login.UserAuth{AuthModule: "oauth_generic_oauth"}, wantExist: true, wantErr: false, - getAuthInfoUser: models.UserAuth{AuthModule: "oauth_generic_oauth"}, + getAuthInfoUser: login.UserAuth{AuthModule: "oauth_generic_oauth"}, }, } for _, tc := range testCases { @@ -101,7 +101,7 @@ func TestService_TryTokenRefresh_ValidToken(t *testing.T) { Expiry: time.Now(), TokenType: "Bearer", } - usr := &models.UserAuth{ + usr := &login.UserAuth{ AuthModule: "oauth_generic_oauth", OAuthAccessToken: token.AccessToken, OAuthRefreshToken: token.RefreshToken, @@ -117,7 +117,7 @@ func TestService_TryTokenRefresh_ValidToken(t *testing.T) { assert.Nil(t, err) socialConnector.AssertNumberOfCalls(t, "TokenSource", 1) - authInfoQuery := &models.GetAuthInfoQuery{} + authInfoQuery := &login.GetAuthInfoQuery{} err = srv.AuthInfoService.GetAuthInfo(ctx, authInfoQuery) assert.Nil(t, err) @@ -139,7 +139,7 @@ func TestService_TryTokenRefresh_NoRefreshToken(t *testing.T) { Expiry: time.Now().Add(-time.Hour), TokenType: "Bearer", } - usr := &models.UserAuth{ + usr := &login.UserAuth{ AuthModule: "oauth_generic_oauth", OAuthAccessToken: token.AccessToken, OAuthRefreshToken: token.RefreshToken, @@ -174,7 +174,7 @@ func TestService_TryTokenRefresh_ExpiredToken(t *testing.T) { TokenType: "Bearer", } - usr := &models.UserAuth{ + usr := &login.UserAuth{ AuthModule: "oauth_generic_oauth", OAuthAccessToken: token.AccessToken, OAuthRefreshToken: token.RefreshToken, @@ -191,7 +191,7 @@ func TestService_TryTokenRefresh_ExpiredToken(t *testing.T) { assert.Nil(t, err) socialConnector.AssertNumberOfCalls(t, "TokenSource", 1) - authInfoQuery := &models.GetAuthInfoQuery{} + authInfoQuery := &login.GetAuthInfoQuery{} err = srv.AuthInfoService.GetAuthInfo(ctx, authInfoQuery) assert.Nil(t, err) @@ -207,7 +207,7 @@ func TestService_TryTokenRefresh_DifferentAuthModuleForUser(t *testing.T) { srv, _, socialConnector := setupOAuthTokenService(t) ctx := context.Background() token := &oauth2.Token{} - usr := &models.UserAuth{ + usr := &login.UserAuth{ AuthModule: "auth.saml", } @@ -307,30 +307,30 @@ type FakeAuthInfoStore struct { login.Store ExpectedError error ExpectedUser *user.User - ExpectedOAuth *models.UserAuth + ExpectedOAuth *login.UserAuth ExpectedDuplicateUserEntries int ExpectedHasDuplicateUserEntries int ExpectedLoginStats login.LoginStats } -func (f *FakeAuthInfoStore) GetExternalUserInfoByLogin(ctx context.Context, query *models.GetExternalUserInfoByLoginQuery) error { +func (f *FakeAuthInfoStore) GetExternalUserInfoByLogin(ctx context.Context, query *login.GetExternalUserInfoByLoginQuery) error { return f.ExpectedError } -func (f *FakeAuthInfoStore) GetAuthInfo(ctx context.Context, query *models.GetAuthInfoQuery) error { +func (f *FakeAuthInfoStore) GetAuthInfo(ctx context.Context, query *login.GetAuthInfoQuery) error { query.Result = f.ExpectedOAuth return f.ExpectedError } -func (f *FakeAuthInfoStore) SetAuthInfo(ctx context.Context, cmd *models.SetAuthInfoCommand) error { +func (f *FakeAuthInfoStore) SetAuthInfo(ctx context.Context, cmd *login.SetAuthInfoCommand) error { return f.ExpectedError } -func (f *FakeAuthInfoStore) UpdateAuthInfoDate(ctx context.Context, authInfo *models.UserAuth) error { +func (f *FakeAuthInfoStore) UpdateAuthInfoDate(ctx context.Context, authInfo *login.UserAuth) error { return f.ExpectedError } -func (f *FakeAuthInfoStore) UpdateAuthInfo(ctx context.Context, cmd *models.UpdateAuthInfoCommand) error { +func (f *FakeAuthInfoStore) UpdateAuthInfo(ctx context.Context, cmd *login.UpdateAuthInfoCommand) error { f.ExpectedOAuth.OAuthAccessToken = cmd.OAuthToken.AccessToken f.ExpectedOAuth.OAuthExpiry = cmd.OAuthToken.Expiry f.ExpectedOAuth.OAuthTokenType = cmd.OAuthToken.TokenType @@ -338,7 +338,7 @@ func (f *FakeAuthInfoStore) UpdateAuthInfo(ctx context.Context, cmd *models.Upda return f.ExpectedError } -func (f *FakeAuthInfoStore) DeleteAuthInfo(ctx context.Context, cmd *models.DeleteAuthInfoCommand) error { +func (f *FakeAuthInfoStore) DeleteAuthInfo(ctx context.Context, cmd *login.DeleteAuthInfoCommand) error { return f.ExpectedError } diff --git a/pkg/services/oauthtoken/oauthtokentest/mock.go b/pkg/services/oauthtoken/oauthtokentest/mock.go index 95bc1ccd204..e1af309faa3 100644 --- a/pkg/services/oauthtoken/oauthtokentest/mock.go +++ b/pkg/services/oauthtoken/oauthtokentest/mock.go @@ -3,18 +3,19 @@ package oauthtokentest import ( "context" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/datasources" - "github.com/grafana/grafana/pkg/services/user" "golang.org/x/oauth2" + + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/login" + "github.com/grafana/grafana/pkg/services/user" ) type MockOauthTokenService struct { GetCurrentOauthTokenFunc func(ctx context.Context, usr *user.SignedInUser) *oauth2.Token IsOAuthPassThruEnabledFunc func(ds *datasources.DataSource) bool - HasOAuthEntryFunc func(ctx context.Context, usr *user.SignedInUser) (*models.UserAuth, bool, error) - InvalidateOAuthTokensFunc func(ctx context.Context, usr *models.UserAuth) error - TryTokenRefreshFunc func(ctx context.Context, usr *models.UserAuth) error + HasOAuthEntryFunc func(ctx context.Context, usr *user.SignedInUser) (*login.UserAuth, bool, error) + InvalidateOAuthTokensFunc func(ctx context.Context, usr *login.UserAuth) error + TryTokenRefreshFunc func(ctx context.Context, usr *login.UserAuth) error } func (m *MockOauthTokenService) GetCurrentOAuthToken(ctx context.Context, usr *user.SignedInUser) *oauth2.Token { @@ -31,21 +32,21 @@ func (m *MockOauthTokenService) IsOAuthPassThruEnabled(ds *datasources.DataSourc return false } -func (m *MockOauthTokenService) HasOAuthEntry(ctx context.Context, usr *user.SignedInUser) (*models.UserAuth, bool, error) { +func (m *MockOauthTokenService) HasOAuthEntry(ctx context.Context, usr *user.SignedInUser) (*login.UserAuth, bool, error) { if m.HasOAuthEntryFunc != nil { return m.HasOAuthEntryFunc(ctx, usr) } return nil, false, nil } -func (m *MockOauthTokenService) InvalidateOAuthTokens(ctx context.Context, usr *models.UserAuth) error { +func (m *MockOauthTokenService) InvalidateOAuthTokens(ctx context.Context, usr *login.UserAuth) error { if m.InvalidateOAuthTokensFunc != nil { return m.InvalidateOAuthTokensFunc(ctx, usr) } return nil } -func (m *MockOauthTokenService) TryTokenRefresh(ctx context.Context, usr *models.UserAuth) error { +func (m *MockOauthTokenService) TryTokenRefresh(ctx context.Context, usr *login.UserAuth) error { if m.TryTokenRefreshFunc != nil { return m.TryTokenRefreshFunc(ctx, usr) } diff --git a/pkg/services/oauthtoken/oauthtokentest/oauthtokentest.go b/pkg/services/oauthtoken/oauthtokentest/oauthtokentest.go index 230b2d7e290..2bb990c8446 100644 --- a/pkg/services/oauthtoken/oauthtokentest/oauthtokentest.go +++ b/pkg/services/oauthtoken/oauthtokentest/oauthtokentest.go @@ -3,11 +3,12 @@ package oauthtokentest import ( "context" - "github.com/grafana/grafana/pkg/models" + "golang.org/x/oauth2" + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/oauthtoken" "github.com/grafana/grafana/pkg/services/user" - "golang.org/x/oauth2" ) // Service an OAuth token service suitable for tests. @@ -28,14 +29,14 @@ func (s *Service) IsOAuthPassThruEnabled(ds *datasources.DataSource) bool { return oauthtoken.IsOAuthPassThruEnabled(ds) } -func (s *Service) HasOAuthEntry(context.Context, *user.SignedInUser) (*models.UserAuth, bool, error) { +func (s *Service) HasOAuthEntry(context.Context, *user.SignedInUser) (*login.UserAuth, bool, error) { return nil, false, nil } -func (s *Service) TryTokenRefresh(context.Context, *models.UserAuth) error { +func (s *Service) TryTokenRefresh(context.Context, *login.UserAuth) error { return nil } -func (s *Service) InvalidateOAuthTokens(context.Context, *models.UserAuth) error { +func (s *Service) InvalidateOAuthTokens(context.Context, *login.UserAuth) error { return nil } From 3c616da83f39498a2dde3729a3749f16daf4c1bd Mon Sep 17 00:00:00 2001 From: gotjosh Date: Fri, 27 Jan 2023 18:49:49 +0000 Subject: [PATCH 064/117] Alerting: Refactor metrics/ngalert.go into seperate files (#62362) * Alerting: Refactor metrics/ngalert.go into seperate files --- pkg/services/ngalert/metrics/alertmanager.go | 21 ++ pkg/services/ngalert/metrics/api.go | 25 ++ .../ngalert/metrics/multi_org_alertmanager.go | 32 ++ pkg/services/ngalert/metrics/ngalert.go | 310 +----------------- pkg/services/ngalert/metrics/scheduler.go | 108 ++++++ pkg/services/ngalert/metrics/state.go | 33 ++ pkg/services/ngalert/metrics/util.go | 104 ++++++ 7 files changed, 335 insertions(+), 298 deletions(-) create mode 100644 pkg/services/ngalert/metrics/alertmanager.go create mode 100644 pkg/services/ngalert/metrics/api.go create mode 100644 pkg/services/ngalert/metrics/multi_org_alertmanager.go create mode 100644 pkg/services/ngalert/metrics/scheduler.go create mode 100644 pkg/services/ngalert/metrics/state.go create mode 100644 pkg/services/ngalert/metrics/util.go diff --git a/pkg/services/ngalert/metrics/alertmanager.go b/pkg/services/ngalert/metrics/alertmanager.go new file mode 100644 index 00000000000..23b85e20723 --- /dev/null +++ b/pkg/services/ngalert/metrics/alertmanager.go @@ -0,0 +1,21 @@ +package metrics + +import ( + "fmt" + + "github.com/prometheus/alertmanager/api/metrics" + "github.com/prometheus/client_golang/prometheus" +) + +type Alertmanager struct { + Registerer prometheus.Registerer + *metrics.Alerts +} + +// NewAlertmanagerMetrics creates a set of metrics for the Alertmanager of each organization. +func NewAlertmanagerMetrics(r prometheus.Registerer) *Alertmanager { + return &Alertmanager{ + Registerer: r, + Alerts: metrics.NewAlerts("grafana", prometheus.WrapRegistererWithPrefix(fmt.Sprintf("%s_%s_", Namespace, Subsystem), r)), + } +} diff --git a/pkg/services/ngalert/metrics/api.go b/pkg/services/ngalert/metrics/api.go new file mode 100644 index 00000000000..db97033a0b5 --- /dev/null +++ b/pkg/services/ngalert/metrics/api.go @@ -0,0 +1,25 @@ +package metrics + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +type API struct { + RequestDuration *prometheus.HistogramVec +} + +func NewAPIMetrics(r prometheus.Registerer) *API { + return &API{ + RequestDuration: promauto.With(r).NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "request_duration_seconds", + Help: "Histogram of requests to the Alerting API", + Buckets: prometheus.DefBuckets, + }, + []string{"method", "route", "status_code", "backend"}, + ), + } +} diff --git a/pkg/services/ngalert/metrics/multi_org_alertmanager.go b/pkg/services/ngalert/metrics/multi_org_alertmanager.go new file mode 100644 index 00000000000..c755457755d --- /dev/null +++ b/pkg/services/ngalert/metrics/multi_org_alertmanager.go @@ -0,0 +1,32 @@ +package metrics + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +type MultiOrgAlertmanager struct { + Registerer prometheus.Registerer + ActiveConfigurations prometheus.Gauge + DiscoveredConfigurations prometheus.Gauge + registries *OrgRegistries +} + +func NewMultiOrgAlertmanagerMetrics(r prometheus.Registerer) *MultiOrgAlertmanager { + return &MultiOrgAlertmanager{ + Registerer: r, + registries: NewOrgRegistries(), + DiscoveredConfigurations: promauto.With(r).NewGauge(prometheus.GaugeOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "discovered_configurations", + Help: "The number of organizations we've discovered that require an Alertmanager configuration.", + }), + ActiveConfigurations: promauto.With(r).NewGauge(prometheus.GaugeOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "active_configurations", + Help: "The number of active Alertmanager configurations.", + }), + } +} diff --git a/pkg/services/ngalert/metrics/ngalert.go b/pkg/services/ngalert/metrics/ngalert.go index 9681847786a..ae1a304ed34 100644 --- a/pkg/services/ngalert/metrics/ngalert.go +++ b/pkg/services/ngalert/metrics/ngalert.go @@ -1,22 +1,7 @@ package metrics import ( - "fmt" - "regexp" - "strings" - "sync" - "time" - - "github.com/prometheus/alertmanager/api/metrics" "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" - - "github.com/grafana/grafana/pkg/api/response" - contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" - apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" - "github.com/grafana/grafana/pkg/util/ticker" - - "github.com/grafana/grafana/pkg/web" ) const ( @@ -37,47 +22,24 @@ func ProvideServiceForTest() *NGAlert { } type NGAlert struct { - // Registerer is for use by subcomponents which register their own metrics. - Registerer prometheus.Registerer + // Registerer is used by subcomponents which register their own metrics. + Registerer prometheus.Registerer + schedulerMetrics *Scheduler stateMetrics *State multiOrgAlertmanagerMetrics *MultiOrgAlertmanager apiMetrics *API } -type Scheduler struct { - Registerer prometheus.Registerer - BehindSeconds prometheus.Gauge - EvalTotal *prometheus.CounterVec - EvalFailures *prometheus.CounterVec - EvalDuration *prometheus.HistogramVec - SchedulePeriodicDuration prometheus.Histogram - SchedulableAlertRules prometheus.Gauge - SchedulableAlertRulesHash prometheus.Gauge - UpdateSchedulableAlertRulesDuration prometheus.Histogram - Ticker *ticker.Metrics - EvaluationMissed *prometheus.CounterVec -} - -type MultiOrgAlertmanager struct { - Registerer prometheus.Registerer - ActiveConfigurations prometheus.Gauge - DiscoveredConfigurations prometheus.Gauge - registries *OrgRegistries -} - -type API struct { - RequestDuration *prometheus.HistogramVec -} - -type Alertmanager struct { - Registerer prometheus.Registerer - *metrics.Alerts -} - -type State struct { - GroupRules *prometheus.GaugeVec - AlertState *prometheus.GaugeVec +// NewNGAlert manages the metrics of all the alerting components. +func NewNGAlert(r prometheus.Registerer) *NGAlert { + return &NGAlert{ + Registerer: r, + schedulerMetrics: NewSchedulerMetrics(r), + stateMetrics: NewStateMetrics(r), + multiOrgAlertmanagerMetrics: NewMultiOrgAlertmanagerMetrics(r), + apiMetrics: NewAPIMetrics(r), + } } func (ng *NGAlert) GetSchedulerMetrics() *Scheduler { @@ -96,25 +58,6 @@ func (ng *NGAlert) GetMultiOrgAlertmanagerMetrics() *MultiOrgAlertmanager { return ng.multiOrgAlertmanagerMetrics } -// NewNGAlert manages the metrics of all the alerting components. -func NewNGAlert(r prometheus.Registerer) *NGAlert { - return &NGAlert{ - Registerer: r, - schedulerMetrics: NewSchedulerMetrics(r), - stateMetrics: newStateMetrics(r), - multiOrgAlertmanagerMetrics: newMultiOrgAlertmanagerMetrics(r), - apiMetrics: newAPIMetrics(r), - } -} - -// NewAlertmanagerMetrics creates a set of metrics for the Alertmanager of each organization. -func NewAlertmanagerMetrics(r prometheus.Registerer) *Alertmanager { - return &Alertmanager{ - Registerer: r, - Alerts: metrics.NewAlerts("grafana", prometheus.WrapRegistererWithPrefix(fmt.Sprintf("%s_%s_", Namespace, Subsystem), r)), - } -} - // RemoveOrgRegistry removes the *prometheus.Registry for the specified org. It is safe to call concurrently. func (moa *MultiOrgAlertmanager) RemoveOrgRegistry(id int64) { moa.registries.RemoveOrgRegistry(id) @@ -124,232 +67,3 @@ func (moa *MultiOrgAlertmanager) RemoveOrgRegistry(id int64) { func (moa *MultiOrgAlertmanager) GetOrCreateOrgRegistry(id int64) prometheus.Registerer { return moa.registries.GetOrCreateOrgRegistry(id) } - -func NewSchedulerMetrics(r prometheus.Registerer) *Scheduler { - return &Scheduler{ - Registerer: r, - BehindSeconds: promauto.With(r).NewGauge(prometheus.GaugeOpts{ - Namespace: Namespace, - Subsystem: Subsystem, - Name: "scheduler_behind_seconds", - Help: "The total number of seconds the scheduler is behind.", - }), - // TODO: once rule groups support multiple rules, consider partitioning - // on rule group as well as tenant, similar to loki|cortex. - EvalTotal: promauto.With(r).NewCounterVec( - prometheus.CounterOpts{ - Namespace: Namespace, - Subsystem: Subsystem, - Name: "rule_evaluations_total", - Help: "The total number of rule evaluations.", - }, - []string{"org"}, - ), - // TODO: once rule groups support multiple rules, consider partitioning - // on rule group as well as tenant, similar to loki|cortex. - EvalFailures: promauto.With(r).NewCounterVec( - prometheus.CounterOpts{ - Namespace: Namespace, - Subsystem: Subsystem, - Name: "rule_evaluation_failures_total", - Help: "The total number of rule evaluation failures.", - }, - []string{"org"}, - ), - EvalDuration: promauto.With(r).NewHistogramVec( - prometheus.HistogramOpts{ - Namespace: Namespace, - Subsystem: Subsystem, - Name: "rule_evaluation_duration_seconds", - Help: "The duration for a rule to execute.", - Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 25, 50, 100}, - }, - []string{"org"}, - ), - SchedulePeriodicDuration: promauto.With(r).NewHistogram( - prometheus.HistogramOpts{ - Namespace: Namespace, - Subsystem: Subsystem, - Name: "schedule_periodic_duration_seconds", - Help: "The time taken to run the scheduler.", - Buckets: []float64{0.1, 0.25, 0.5, 1, 2, 5, 10}, - }, - ), - SchedulableAlertRules: promauto.With(r).NewGauge( - prometheus.GaugeOpts{ - Namespace: Namespace, - Subsystem: Subsystem, - Name: "schedule_alert_rules", - Help: "The number of alert rules that could be considered for evaluation at the next tick.", - }, - ), - SchedulableAlertRulesHash: promauto.With(r).NewGauge( - prometheus.GaugeOpts{ - Namespace: Namespace, - Subsystem: Subsystem, - Name: "schedule_alert_rules_hash", - Help: "A hash of the alert rules that could be considered for evaluation at the next tick.", - }), - UpdateSchedulableAlertRulesDuration: promauto.With(r).NewHistogram( - prometheus.HistogramOpts{ - Namespace: Namespace, - Subsystem: Subsystem, - Name: "schedule_query_alert_rules_duration_seconds", - Help: "The time taken to fetch alert rules from the database.", - Buckets: []float64{0.1, 0.25, 0.5, 1, 2, 5, 10}, - }, - ), - Ticker: ticker.NewMetrics(r, "alerting"), - EvaluationMissed: promauto.With(r).NewCounterVec( - prometheus.CounterOpts{ - Namespace: Namespace, - Subsystem: Subsystem, - Name: "schedule_rule_evaluations_missed_total", - Help: "The total number of rule evaluations missed due to a slow rule evaluation.", - }, - []string{"org", "name"}, - ), - } -} - -func newStateMetrics(r prometheus.Registerer) *State { - return &State{ - // TODO: once rule groups support multiple rules, consider partitioning - // on rule group as well as tenant, similar to loki|cortex. - GroupRules: promauto.With(r).NewGaugeVec( - prometheus.GaugeOpts{ - Namespace: Namespace, - Subsystem: Subsystem, - Name: "rule_group_rules", - Help: "The number of rules.", - }, - []string{"org"}, - ), - AlertState: promauto.With(r).NewGaugeVec(prometheus.GaugeOpts{ - Namespace: Namespace, - Subsystem: Subsystem, - Name: "alerts", - Help: "How many alerts by state.", - }, []string{"state"}), - } -} - -func newMultiOrgAlertmanagerMetrics(r prometheus.Registerer) *MultiOrgAlertmanager { - return &MultiOrgAlertmanager{ - Registerer: r, - registries: NewOrgRegistries(), - DiscoveredConfigurations: promauto.With(r).NewGauge(prometheus.GaugeOpts{ - Namespace: Namespace, - Subsystem: Subsystem, - Name: "discovered_configurations", - Help: "The number of organizations we've discovered that require an Alertmanager configuration.", - }), - ActiveConfigurations: promauto.With(r).NewGauge(prometheus.GaugeOpts{ - Namespace: Namespace, - Subsystem: Subsystem, - Name: "active_configurations", - Help: "The number of active Alertmanager configurations.", - }), - } -} - -func newAPIMetrics(r prometheus.Registerer) *API { - return &API{ - RequestDuration: promauto.With(r).NewHistogramVec( - prometheus.HistogramOpts{ - Namespace: Namespace, - Subsystem: Subsystem, - Name: "request_duration_seconds", - Help: "Histogram of requests to the Alerting API", - Buckets: prometheus.DefBuckets, - }, - []string{"method", "route", "status_code", "backend"}, - ), - } -} - -// OrgRegistries represents a map of registries per org. -type OrgRegistries struct { - regsMu sync.Mutex - regs map[int64]prometheus.Registerer -} - -func NewOrgRegistries() *OrgRegistries { - return &OrgRegistries{ - regs: make(map[int64]prometheus.Registerer), - } -} - -// GetOrCreateOrgRegistry gets or creates a *prometheus.Registry for the specified org. It is safe to call concurrently. -func (m *OrgRegistries) GetOrCreateOrgRegistry(orgID int64) prometheus.Registerer { - m.regsMu.Lock() - defer m.regsMu.Unlock() - - orgRegistry, ok := m.regs[orgID] - if !ok { - reg := prometheus.NewRegistry() - m.regs[orgID] = reg - return reg - } - return orgRegistry -} - -// RemoveOrgRegistry removes the *prometheus.Registry for the specified org. It is safe to call concurrently. -func (m *OrgRegistries) RemoveOrgRegistry(org int64) { - m.regsMu.Lock() - defer m.regsMu.Unlock() - delete(m.regs, org) -} - -// Instrument wraps a middleware, instrumenting the request latencies. -func Instrument( - method, - path string, - action func(*contextmodel.ReqContext) response.Response, - metrics *API, -) web.Handler { - normalizedPath := MakeLabelValue(path) - - return func(c *contextmodel.ReqContext) { - start := time.Now() - res := action(c) - - // TODO: We could look up the datasource type via our datasource service - var backend string - datasourceID := web.Params(c.Req)[":DatasourceID"] - if datasourceID == apimodels.GrafanaBackend.String() || datasourceID == "" { - backend = GrafanaBackend - } else { - backend = ProxyBackend - } - - ls := prometheus.Labels{ - "method": method, - "route": normalizedPath, - "status_code": fmt.Sprint(res.Status()), - "backend": backend, - } - res.WriteTo(c) - metrics.RequestDuration.With(ls).Observe(time.Since(start).Seconds()) - } -} - -var invalidChars = regexp.MustCompile(`[^a-zA-Z0-9]+`) - -// MakeLabelValue normalizes a path template -func MakeLabelValue(path string) string { - // Convert non-alnums to underscores. - result := invalidChars.ReplaceAllString(path, "_") - - // Trim leading and trailing underscores. - result = strings.Trim(result, "_") - - // Make it all lowercase - result = strings.ToLower(result) - - // Special case. - if result == "" { - result = "root" - } - return result -} diff --git a/pkg/services/ngalert/metrics/scheduler.go b/pkg/services/ngalert/metrics/scheduler.go new file mode 100644 index 00000000000..fb1523918a3 --- /dev/null +++ b/pkg/services/ngalert/metrics/scheduler.go @@ -0,0 +1,108 @@ +package metrics + +import ( + "github.com/grafana/grafana/pkg/util/ticker" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +type Scheduler struct { + Registerer prometheus.Registerer + BehindSeconds prometheus.Gauge + EvalTotal *prometheus.CounterVec + EvalFailures *prometheus.CounterVec + EvalDuration *prometheus.HistogramVec + SchedulePeriodicDuration prometheus.Histogram + SchedulableAlertRules prometheus.Gauge + SchedulableAlertRulesHash prometheus.Gauge + UpdateSchedulableAlertRulesDuration prometheus.Histogram + Ticker *ticker.Metrics + EvaluationMissed *prometheus.CounterVec +} + +func NewSchedulerMetrics(r prometheus.Registerer) *Scheduler { + return &Scheduler{ + Registerer: r, + BehindSeconds: promauto.With(r).NewGauge(prometheus.GaugeOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "scheduler_behind_seconds", + Help: "The total number of seconds the scheduler is behind.", + }), + // TODO: once rule groups support multiple rules, consider partitioning + // on rule group as well as tenant, similar to loki|cortex. + EvalTotal: promauto.With(r).NewCounterVec( + prometheus.CounterOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "rule_evaluations_total", + Help: "The total number of rule evaluations.", + }, + []string{"org"}, + ), + // TODO: once rule groups support multiple rules, consider partitioning + // on rule group as well as tenant, similar to loki|cortex. + EvalFailures: promauto.With(r).NewCounterVec( + prometheus.CounterOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "rule_evaluation_failures_total", + Help: "The total number of rule evaluation failures.", + }, + []string{"org"}, + ), + EvalDuration: promauto.With(r).NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "rule_evaluation_duration_seconds", + Help: "The duration for a rule to execute.", + Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 25, 50, 100}, + }, + []string{"org"}, + ), + SchedulePeriodicDuration: promauto.With(r).NewHistogram( + prometheus.HistogramOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "schedule_periodic_duration_seconds", + Help: "The time taken to run the scheduler.", + Buckets: []float64{0.1, 0.25, 0.5, 1, 2, 5, 10}, + }, + ), + SchedulableAlertRules: promauto.With(r).NewGauge( + prometheus.GaugeOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "schedule_alert_rules", + Help: "The number of alert rules that could be considered for evaluation at the next tick.", + }, + ), + SchedulableAlertRulesHash: promauto.With(r).NewGauge( + prometheus.GaugeOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "schedule_alert_rules_hash", + Help: "A hash of the alert rules that could be considered for evaluation at the next tick.", + }), + UpdateSchedulableAlertRulesDuration: promauto.With(r).NewHistogram( + prometheus.HistogramOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "schedule_query_alert_rules_duration_seconds", + Help: "The time taken to fetch alert rules from the database.", + Buckets: []float64{0.1, 0.25, 0.5, 1, 2, 5, 10}, + }, + ), + Ticker: ticker.NewMetrics(r, "alerting"), + EvaluationMissed: promauto.With(r).NewCounterVec( + prometheus.CounterOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "schedule_rule_evaluations_missed_total", + Help: "The total number of rule evaluations missed due to a slow rule evaluation.", + }, + []string{"org", "name"}, + ), + } +} diff --git a/pkg/services/ngalert/metrics/state.go b/pkg/services/ngalert/metrics/state.go new file mode 100644 index 00000000000..2a5bf072b1a --- /dev/null +++ b/pkg/services/ngalert/metrics/state.go @@ -0,0 +1,33 @@ +package metrics + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +type State struct { + GroupRules *prometheus.GaugeVec + AlertState *prometheus.GaugeVec +} + +func NewStateMetrics(r prometheus.Registerer) *State { + return &State{ + // TODO: once rule groups support multiple rules, consider partitioning + // on rule group as well as tenant, similar to loki|cortex. + GroupRules: promauto.With(r).NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "rule_group_rules", + Help: "The number of rules.", + }, + []string{"org"}, + ), + AlertState: promauto.With(r).NewGaugeVec(prometheus.GaugeOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "alerts", + Help: "How many alerts by state.", + }, []string{"state"}), + } +} diff --git a/pkg/services/ngalert/metrics/util.go b/pkg/services/ngalert/metrics/util.go new file mode 100644 index 00000000000..db65e1e2b87 --- /dev/null +++ b/pkg/services/ngalert/metrics/util.go @@ -0,0 +1,104 @@ +package metrics + +import ( + "fmt" + "regexp" + "strings" + "sync" + "time" + + "github.com/grafana/grafana/pkg/web" + + "github.com/grafana/grafana/pkg/api/response" + + "github.com/prometheus/client_golang/prometheus" + + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" +) + +// OrgRegistries represents a map of registries per org. +type OrgRegistries struct { + regsMu sync.Mutex + regs map[int64]prometheus.Registerer +} + +func NewOrgRegistries() *OrgRegistries { + return &OrgRegistries{ + regs: make(map[int64]prometheus.Registerer), + } +} + +// GetOrCreateOrgRegistry gets or creates a *prometheus.Registry for the specified org. It is safe to call concurrently. +func (m *OrgRegistries) GetOrCreateOrgRegistry(orgID int64) prometheus.Registerer { + m.regsMu.Lock() + defer m.regsMu.Unlock() + + orgRegistry, ok := m.regs[orgID] + if !ok { + reg := prometheus.NewRegistry() + m.regs[orgID] = reg + return reg + } + return orgRegistry +} + +// RemoveOrgRegistry removes the *prometheus.Registry for the specified org. It is safe to call concurrently. +func (m *OrgRegistries) RemoveOrgRegistry(org int64) { + m.regsMu.Lock() + defer m.regsMu.Unlock() + delete(m.regs, org) +} + +// Instrument wraps a middleware, instrumenting the request latencies. +func Instrument( + method, + path string, + action func(*contextmodel.ReqContext) response.Response, + metrics *API, +) web.Handler { + normalizedPath := MakeLabelValue(path) + + return func(c *contextmodel.ReqContext) { + start := time.Now() + res := action(c) + + // TODO: We could look up the datasource type via our datasource service + var backend string + datasourceID := web.Params(c.Req)[":DatasourceID"] + if datasourceID == apimodels.GrafanaBackend.String() || datasourceID == "" { + backend = GrafanaBackend + } else { + backend = ProxyBackend + } + + ls := prometheus.Labels{ + "method": method, + "route": normalizedPath, + "status_code": fmt.Sprint(res.Status()), + "backend": backend, + } + res.WriteTo(c) + metrics.RequestDuration.With(ls).Observe(time.Since(start).Seconds()) + } +} + +var invalidChars = regexp.MustCompile(`[^a-zA-Z0-9]+`) + +// MakeLabelValue normalizes a path template +func MakeLabelValue(path string) string { + // Convert non-alnums to underscores. + result := invalidChars.ReplaceAllString(path, "_") + + // Trim leading and trailing underscores. + result = strings.Trim(result, "_") + + // Make it all lowercase + result = strings.ToLower(result) + + // Special case. + if result == "" { + result = "root" + } + return result +} From 290f23a50bc09a84f342308a0e9d8ecd2c993ea5 Mon Sep 17 00:00:00 2001 From: Isabel <76437239+imatwawana@users.noreply.github.com> Date: Fri, 27 Jan 2023 14:31:55 -0500 Subject: [PATCH 065/117] docs: clarify note re export dashboard as PDF (#62398) clarify note re export dashboard as PDF Update note under Export dashboard as PDF to indicate that it's only available on Enterprise and Cloud, as well as which version of Enterprise it's available on. --- docs/sources/dashboards/create-reports/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/dashboards/create-reports/index.md b/docs/sources/dashboards/create-reports/index.md index 2ddd5e6673e..7def573355e 100644 --- a/docs/sources/dashboards/create-reports/index.md +++ b/docs/sources/dashboards/create-reports/index.md @@ -181,7 +181,7 @@ You can send a report email with an image of the dashboard embedded in the email You can generate and save PDF files of any dashboard. -> **Note:** Available in [Grafana Enterprise]({{< relref "../../introduction/grafana-enterprise/" >}}). +> **Note:** Available in [Grafana Enterprise]({{< relref "../../introduction/grafana-enterprise/" >}}) version 6.7 and later, and [Grafana Cloud Pro and Advanced](/docs/grafana-cloud/). 1. In the upper-right corner of the dashboard that you want to export as PDF, click the **Share dashboard** icon. 1. On the PDF tab, select a layout option for the exported dashboard: **Portrait** or **Landscape**. From 07dc9947652e0df8f296c029cb85bef550faa711 Mon Sep 17 00:00:00 2001 From: Drew Slobodnjak <60050885+drew08t@users.noreply.github.com> Date: Fri, 27 Jan 2023 11:35:57 -0800 Subject: [PATCH 066/117] Canvas: Anchor highlight persistance (#62364) * Canvas: Anchor highlight persistance * Update public/app/plugins/panel/canvas/ConnectionAnchors.tsx Co-authored-by: Nathan Marrs * Fix CONNECTION_ANCHOR_ALT dependencies * Simplify anchor type assertion logic * Ensure that anchor highlight is reset when anchors are hidden * Add helpful comment on return bool of function --------- Co-authored-by: Nathan Marrs --- .../panel/canvas/ConnectionAnchors.tsx | 19 +++++++++++++++---- .../app/plugins/panel/canvas/Connections.tsx | 14 ++++++++++++-- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/panel/canvas/ConnectionAnchors.tsx b/public/app/plugins/panel/canvas/ConnectionAnchors.tsx index 211617fe664..199cec648cd 100644 --- a/public/app/plugins/panel/canvas/ConnectionAnchors.tsx +++ b/public/app/plugins/panel/canvas/ConnectionAnchors.tsx @@ -7,10 +7,13 @@ import { ConnectionCoordinates } from 'app/features/canvas'; type Props = { setRef: (anchorElement: HTMLDivElement) => void; - handleMouseLeave: (event: React.MouseEvent | React.FocusEvent) => void; + handleMouseLeave: ( + event: React.MouseEvent | React.FocusEvent + ) => boolean; }; export const CONNECTION_ANCHOR_DIV_ID = 'connectionControl'; +export const CONNECTION_ANCHOR_ALT = 'connection anchor'; export const ConnectionAnchors = ({ setRef, handleMouseLeave }: Props) => { const highlightEllipseRef = useRef(null); @@ -38,7 +41,15 @@ export const ConnectionAnchors = ({ setRef, handleMouseLeave }: Props) => { } }; - const connectionAnchorAlt = 'connection anchor'; + const handleMouseLeaveAnchors = ( + event: React.MouseEvent | React.FocusEvent + ) => { + const didHideAnchors = handleMouseLeave(event); + + if (didHideAnchors) { + onMouseLeaveHighlightElement(); + } + }; // Unit is percentage from the middle of the element // 0, 0 middle; -1, -1 bottom left; 1, 1 top right @@ -75,7 +86,7 @@ export const ConnectionAnchors = ({ setRef, handleMouseLeave }: Props) => { {connectionAnchorAlt} { return (
-
+
{ + // Return boolean indicates if connection anchors were hidden or not + handleMouseLeave = (event: React.MouseEvent | React.FocusEvent): boolean => { + // If mouse is leaving INTO the anchor image, don't remove div + if ( + event.relatedTarget instanceof HTMLImageElement && + event.relatedTarget.getAttribute('alt') === CONNECTION_ANCHOR_ALT + ) { + return false; + } + this.connectionAnchorDiv!.style.display = 'none'; + return true; }; connectionListener = (event: MouseEvent) => { From 6c990b461e4650effbfddb1fc9264bb6ad4d726a Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Fri, 27 Jan 2023 20:04:04 +0000 Subject: [PATCH 067/117] SupportBundles: Feature flag + access control navtree item (#62337) * SupportBundles: Feature flag + access control navtree item * remove translation --- pkg/services/navtree/navtreeimpl/navtree.go | 42 +++++++++++++++------ public/app/core/components/NavBar/utils.ts | 25 +----------- public/locales/de-DE/grafana.json | 1 - public/locales/en-US/grafana.json | 3 +- public/locales/es-ES/grafana.json | 1 - public/locales/fr-FR/grafana.json | 1 - public/locales/pseudo-LOCALE/grafana.json | 3 +- public/locales/zh-Hans/grafana.json | 1 - 8 files changed, 34 insertions(+), 43 deletions(-) diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index 57c6097614c..e81d1c82ade 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -20,6 +20,7 @@ import ( pref "github.com/grafana/grafana/pkg/services/preference" "github.com/grafana/grafana/pkg/services/querylibrary" "github.com/grafana/grafana/pkg/services/star" + "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlesimpl" "github.com/grafana/grafana/pkg/setting" ) @@ -232,6 +233,11 @@ func (s *ServiceImpl) getHomeNode(c *contextmodel.ReqContext, prefs *pref.Prefer return homeNode } +func isSupportBundlesEnabled(s *ServiceImpl) bool { + return s.cfg.SectionWithEnvOverrides("support_bundles").Key("enabled").MustBool(false) && + s.features.IsEnabled(featuremgmt.FlagSupportBundles) +} + func (s *ServiceImpl) addHelpLinks(treeRoot *navtree.NavTreeRoot, c *contextmodel.ReqContext) { if setting.HelpEnabled { helpVersion := fmt.Sprintf(`%s v%s (%s)`, setting.ApplicationName, setting.BuildVersion, setting.BuildCommit) @@ -239,16 +245,7 @@ func (s *ServiceImpl) addHelpLinks(treeRoot *navtree.NavTreeRoot, c *contextmode helpVersion = setting.ApplicationName } - supportBundleNode := &navtree.NavLink{ - Text: "Support bundles", - Id: "support-bundles", - Url: "/support-bundles", - Icon: "wrench", - Section: navtree.NavSectionConfig, - SortWeight: navtree.WeightHelp, - } - - treeRoot.AddSection(&navtree.NavLink{ + helpNode := &navtree.NavLink{ Text: "Help", SubTitle: helpVersion, Id: "help", @@ -256,8 +253,29 @@ func (s *ServiceImpl) addHelpLinks(treeRoot *navtree.NavTreeRoot, c *contextmode Icon: "question-circle", SortWeight: navtree.WeightHelp, Section: navtree.NavSectionConfig, - Children: []*navtree.NavLink{supportBundleNode}, - }) + Children: []*navtree.NavLink{}, + } + + treeRoot.AddSection(helpNode) + + hasAccess := ac.HasAccess(s.accessControl, c) + supportBundleAccess := ac.EvalAny( + ac.EvalPermission(supportbundlesimpl.ActionRead), + ac.EvalPermission(supportbundlesimpl.ActionCreate), + ) + + if isSupportBundlesEnabled(s) && hasAccess(ac.ReqGrafanaAdmin, supportBundleAccess) { + supportBundleNode := &navtree.NavLink{ + Text: "Support bundles", + Id: "support-bundles", + Url: "/support-bundles", + Icon: "wrench", + Section: navtree.NavSectionConfig, + SortWeight: navtree.WeightHelp, + } + + helpNode.Children = append(helpNode.Children, supportBundleNode) + } } } diff --git a/public/app/core/components/NavBar/utils.ts b/public/app/core/components/NavBar/utils.ts index eef4c0cece1..df94b105376 100644 --- a/public/app/core/components/NavBar/utils.ts +++ b/public/app/core/components/NavBar/utils.ts @@ -4,11 +4,10 @@ import { locationUtil, NavModelItem, NavSection } from '@grafana/data'; import { config, reportInteraction } from '@grafana/runtime'; import { t } from 'app/core/internationalization'; import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction } from 'app/types'; import { ShowModalReactEvent } from '../../../types/events'; import appEvents from '../../app_events'; -import { FooterLink, getFooterLinks } from '../Footer/Footer'; +import { getFooterLinks } from '../Footer/Footer'; import { OrgSwitcher } from '../OrgSwitcher'; import { HelpModal } from '../help/HelpModal'; @@ -53,8 +52,8 @@ export const enrichConfigItems = (items: NavModelItem[], location: Location { - const hasAccess = - contextSrv.hasAccess(AccessControlAction.ActionSupportBundlesCreate, contextSrv.isGrafanaAdmin) || - contextSrv.hasAccess(AccessControlAction.ActionSupportBundlesRead, contextSrv.isGrafanaAdmin); - - if (!cfg.supportBundlesEnabled || !hasAccess) { - return []; - } - - return [ - { - target: '_self', - id: 'support-bundle', - text: t('nav.help/support-bundle', 'Support Bundles'), - icon: 'question-circle', - url: '/support-bundles', - }, - ]; -}; - export const enrichWithInteractionTracking = (item: NavModelItem, expandedState: boolean) => { const onClick = item.onClick; item.onClick = () => { diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index 7938d1ed771..03a1590e4e1 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -229,7 +229,6 @@ "help/documentation": "Dokumentation", "help/keyboard-shortcuts": "Tastaturbefehle", "help/support": "Support", - "help/support-bundle": "Support Bundles", "home": { "title": "Home" }, diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 4450f5977a4..ce504e80bb7 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -13,7 +13,7 @@ "search": "Search" }, "search-box": { - "placeholder": "Search or jump to..." + "placeholder": "Search Grafana" }, "section": { "actions": "Actions", @@ -229,7 +229,6 @@ "help/documentation": "Documentation", "help/keyboard-shortcuts": "Keyboard shortcuts", "help/support": "Support", - "help/support-bundle": "Support Bundles", "home": { "title": "Home" }, diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index a921571d27b..67b51a2fc28 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -229,7 +229,6 @@ "help/documentation": "Documentación", "help/keyboard-shortcuts": "Atajos de teclado", "help/support": "Asistencia", - "help/support-bundle": "Paquetes de apoyo", "home": { "title": "Inicio" }, diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 753dcced8d3..579ecc1b245 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -229,7 +229,6 @@ "help/documentation": "Documentation", "help/keyboard-shortcuts": "Raccourcis clavier", "help/support": "Assistance", - "help/support-bundle": "Packs d’assistance", "home": { "title": "Accueil" }, diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 9b900ef8d2f..57932f6f053 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -13,7 +13,7 @@ "search": "Ŝęäřčĥ" }, "search-box": { - "placeholder": "Ŝęäřčĥ őř ĵūmp ŧő..." + "placeholder": "Ŝęäřčĥ Ğřäƒäʼnä" }, "section": { "actions": "Åčŧįőʼnş", @@ -229,7 +229,6 @@ "help/documentation": "Đőčūmęʼnŧäŧįőʼn", "help/keyboard-shortcuts": "Ķęyþőäřđ şĥőřŧčūŧş", "help/support": "Ŝūppőřŧ", - "help/support-bundle": "Ŝūppőřŧ ßūʼnđľęş", "home": { "title": "Ħőmę" }, diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index cd6416ae13f..442a0ebc52d 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -229,7 +229,6 @@ "help/documentation": "文档", "help/keyboard-shortcuts": "快捷键", "help/support": "支持", - "help/support-bundle": "", "home": { "title": "首页" }, From 0c4671e31fd77ced27effa5ba116ab0f2804c0c7 Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Fri, 27 Jan 2023 16:26:22 -0500 Subject: [PATCH 068/117] Alerting: Update historian to ignore transitions from Normal Paused and Updated (#62267) --- pkg/services/ngalert/state/historian/core.go | 12 +++++- .../ngalert/state/historian/core_test.go | 40 +++++++++---------- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/pkg/services/ngalert/state/historian/core.go b/pkg/services/ngalert/state/historian/core.go index d8d49868413..d59181cf317 100644 --- a/pkg/services/ngalert/state/historian/core.go +++ b/pkg/services/ngalert/state/historian/core.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/models" @@ -12,8 +13,17 @@ import ( ) func shouldRecord(transition state.StateTransition) bool { + if !transition.Changed() { + return false + } + // Do not log not transitioned states normal states if it was marked as stale - if !transition.Changed() || transition.StateReason == models.StateReasonMissingSeries && transition.PreviousState == eval.Normal && transition.State.State == eval.Normal { + if transition.StateReason == models.StateReasonMissingSeries && transition.PreviousState == eval.Normal && transition.State.State == eval.Normal { + return false + } + // Do not log transition from Normal (Paused|Updated) to Normal + if transition.State.State == eval.Normal && transition.StateReason == "" && + transition.PreviousState == eval.Normal && (transition.PreviousStateReason == models.StateReasonPaused || transition.PreviousStateReason == models.StateReasonUpdated) { return false } return true diff --git a/pkg/services/ngalert/state/historian/core_test.go b/pkg/services/ngalert/state/historian/core_test.go index 0eae1303640..b2f3f522d3c 100644 --- a/pkg/services/ngalert/state/historian/core_test.go +++ b/pkg/services/ngalert/state/historian/core_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/state" @@ -44,10 +45,14 @@ func TestShouldRecord(t *testing.T) { knownReasons := []string{ "", models.StateReasonMissingSeries, + models.StateReasonPaused, + models.StateReasonUpdated, + models.StateReasonRuleDeleted, eval.Error.String(), eval.NoData.String(), } + // all combinations does not reflect the real transitions that could happen, which is a subset. allCombinations := make([]Transition, 0, len(allStates)*len(allStates)*len(knownReasons)*len(knownReasons)) for _, from := range allStates { for _, reasonFrom := range knownReasons { @@ -60,30 +65,23 @@ func TestShouldRecord(t *testing.T) { } negativeTransitions := map[Transition]struct{}{ - noTransition(eval.Normal, ""): {}, - noTransition(eval.Normal, eval.Error.String()): {}, - noTransition(eval.Normal, eval.NoData.String()): {}, - noTransition(eval.Normal, models.StateReasonMissingSeries): {}, - noTransition(eval.Alerting, ""): {}, - noTransition(eval.Alerting, eval.Error.String()): {}, - noTransition(eval.Alerting, eval.NoData.String()): {}, - noTransition(eval.Alerting, models.StateReasonMissingSeries): {}, - noTransition(eval.Pending, ""): {}, - noTransition(eval.Pending, eval.Error.String()): {}, - noTransition(eval.Pending, eval.NoData.String()): {}, - noTransition(eval.Pending, models.StateReasonMissingSeries): {}, - noTransition(eval.NoData, ""): {}, - noTransition(eval.NoData, eval.Error.String()): {}, - noTransition(eval.NoData, eval.NoData.String()): {}, - noTransition(eval.NoData, models.StateReasonMissingSeries): {}, - noTransition(eval.Error, ""): {}, - noTransition(eval.Error, eval.Error.String()): {}, - noTransition(eval.Error, eval.NoData.String()): {}, - noTransition(eval.Error, models.StateReasonMissingSeries): {}, - transition(eval.Normal, "", eval.Normal, models.StateReasonMissingSeries): {}, transition(eval.Normal, eval.Error.String(), eval.Normal, models.StateReasonMissingSeries): {}, transition(eval.Normal, eval.NoData.String(), eval.Normal, models.StateReasonMissingSeries): {}, + + transition(eval.Normal, models.StateReasonPaused, eval.Normal, ""): {}, + transition(eval.Normal, models.StateReasonUpdated, eval.Normal, ""): {}, + + // these transitions are actually not possible + transition(eval.Normal, models.StateReasonRuleDeleted, eval.Normal, models.StateReasonMissingSeries): {}, + transition(eval.Normal, models.StateReasonPaused, eval.Normal, models.StateReasonMissingSeries): {}, + transition(eval.Normal, models.StateReasonUpdated, eval.Normal, models.StateReasonMissingSeries): {}, + } + // add all transitions from reason X(Y) to X(Y) as negative. + for _, s := range allStates { + for _, reason := range knownReasons { + negativeTransitions[noTransition(s, reason)] = struct{}{} + } } for _, tc := range allCombinations { From 82e8ad8a0f40df9cb2128fc0943a91ce0f2d14a9 Mon Sep 17 00:00:00 2001 From: Sarah Zinger Date: Fri, 27 Jan 2023 16:46:08 -0500 Subject: [PATCH 069/117] Cloudwatch: Set CloudwatchCrossAccountQuery feature to stable (#62348) * Cloudwatch: Set CloudwatchCrossAccountQuery feature to stable Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> --- docs/sources/datasources/aws-cloudwatch/_index.md | 8 +++++++- .../datasources/aws-cloudwatch/query-editor/index.md | 9 --------- .../configure-grafana/feature-toggles/index.md | 2 +- pkg/services/featuremgmt/registry.go | 5 +++-- pkg/services/featuremgmt/toggles_gen.go | 2 +- 5 files changed, 12 insertions(+), 14 deletions(-) diff --git a/docs/sources/datasources/aws-cloudwatch/_index.md b/docs/sources/datasources/aws-cloudwatch/_index.md index 9fc5edc9bd3..2df89e86dc5 100644 --- a/docs/sources/datasources/aws-cloudwatch/_index.md +++ b/docs/sources/datasources/aws-cloudwatch/_index.md @@ -167,7 +167,7 @@ You can attach these permissions to the IAM role or IAM user you configured in [ } ``` -**Cross-account observability:** +**Cross-account observability: (see below) ** ```json { @@ -369,3 +369,9 @@ If you use multiple regions or configured more than one CloudWatch data source t To request a quota increase, visit the [AWS Service Quotas console](https://console.aws.amazon.com/servicequotas/home?r#!/services/monitoring/quotas/L-5E141212). For more information, refer to the AWS documentation for [Service Quotas](https://docs.aws.amazon.com/servicequotas/latest/userguide/intro.html) and [CloudWatch limits](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_limits.html). + +## Cross-account observability + +The CloudWatch plugin enables you to monitor and troubleshoot applications across multiple regional accounts. Using cross-account observability, you can seamlessly search, visualize and analyze metrics and logs without worrying about account boundaries. + +To use this feature, configure in the [AWS console under Cloudwatch Settings](https://aws.amazon.com/blogs/aws/new-amazon-cloudwatch-cross-account-observability/), a monitoring and source account, and then add the necessary IAM permissions as described above. diff --git a/docs/sources/datasources/aws-cloudwatch/query-editor/index.md b/docs/sources/datasources/aws-cloudwatch/query-editor/index.md index fe91bc93cbc..37d85968eca 100644 --- a/docs/sources/datasources/aws-cloudwatch/query-editor/index.md +++ b/docs/sources/datasources/aws-cloudwatch/query-editor/index.md @@ -218,15 +218,6 @@ When making `stats` queries in [Explore]({{< relref "../../../explore/" >}}), ma {{< figure src="/static/img/docs/v70/explore-mode-switcher.png" max-width="500px" class="docs-image--right" caption="Explore mode switcher" >}} -## Cross-account observability - -The CloudWatch plugin provides the ability to monitor and troubleshoot applications that span across multiple accounts within a region. Using cross-account observability, you can seamlessly search, visualize and analyze metrics and logs, without having to worry about account boundaries. - -> **Note:** This feature is currently behind the `cloudWatchCrossAccountQuerying` feature toggle. - -> You can enable feature toggles through configuration file or environment variables. See configuration [docs]({{< relref "../../../setup-grafana/configure-grafana/#feature_toggles" >}}) for details. -> Grafana Cloud users can access this feature by [opening a support ticket in the Cloud Portal](https://grafana.com/profile/org#support). - ### Getting started To enable cross-account observability, first enable it in CloudWatch using the official [CloudWatch docs](http://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Unified-Cross-Account.html), then add [two new API actions]({{< relref "../#cross-account-observability" >}}) to the IAM policy attached to the role/user running the plugin. diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index bcec59423e3..a404c8b6f33 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -28,6 +28,7 @@ Some stable features are enabled by default. You can disable a stable feature by | `commandPalette` | Enable command palette | Yes | | `cloudWatchDynamicLabels` | Use dynamic labels instead of alias patterns in CloudWatch datasource | Yes | | `internationalization` | Enables internationalization | Yes | +| `cloudWatchCrossAccountQuerying` | Enables cross-account querying in CloudWatch datasources | Yes | | `accessTokenExpirationCheck` | Enable OAuth access_token expiration check and token refresh using the refresh_token | | | `disablePrometheusExemplarSampling` | Disable Prometheus examplar sampling | | | `logsSampleInExplore` | Enables access to the logs sample feature in Explore | Yes | @@ -84,7 +85,6 @@ Alpha features might be changed or removed without prior notice. | `dataConnectionsConsole` | Enables a new top-level page called Connections. This page is an experiment that provides a better experience when you install and configure data sources and other plugins. | | `topnav` | New top nav and page layouts | | `flameGraph` | Show the flame graph | -| `cloudWatchCrossAccountQuerying` | Use cross-account querying in CloudWatch datasource | | `redshiftAsyncQueryDataSupport` | Enable async query data support for Redshift | | `athenaAsyncQueryDataSupport` | Enable async query data support for Athena | | `increaseInMemDatabaseQueryCache` | Enable more in memory caching for database queries | diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 6db9cb8d4bc..e27478fa809 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -305,8 +305,9 @@ var ( }, { Name: "cloudWatchCrossAccountQuerying", - Description: "Use cross-account querying in CloudWatch datasource", - State: FeatureStateAlpha, + Description: "Enables cross-account querying in CloudWatch datasources", + State: FeatureStateStable, + Expression: "true", //enabled by default }, { Name: "redshiftAsyncQueryDataSupport", diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 617186f858d..737d8483c0e 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -220,7 +220,7 @@ const ( FlagFlameGraph = "flameGraph" // FlagCloudWatchCrossAccountQuerying - // Use cross-account querying in CloudWatch datasource + // Enables cross-account querying in CloudWatch datasources FlagCloudWatchCrossAccountQuerying = "cloudWatchCrossAccountQuerying" // FlagRedshiftAsyncQueryDataSupport From 13159d03bae148ad2728d108a91a9c1fe7682e6b Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Sat, 28 Jan 2023 18:39:23 +0100 Subject: [PATCH 070/117] Alerting: Use optional chaining for accessing frames (#61814) --- .../alerting/unified/components/expressions/util.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/public/app/features/alerting/unified/components/expressions/util.ts b/public/app/features/alerting/unified/components/expressions/util.ts index 61a6b097dd8..2a8407a6416 100644 --- a/public/app/features/alerting/unified/components/expressions/util.ts +++ b/public/app/features/alerting/unified/components/expressions/util.ts @@ -1,11 +1,20 @@ import { DataFrame, Labels, roundDecimals } from '@grafana/data'; +/** + * ⚠️ `frame.fields` could be an empty array ⚠️ + * + * TypeScript will NOT complain about it when accessing items via index signatures. + * Make sure to check for empty array or use optional chaining! + * + * see https://github.com/Microsoft/TypeScript/issues/13778 + */ + const getSeriesName = (frame: DataFrame): string => { - return frame.name ?? formatLabels(frame.fields[0].labels ?? {}); + return frame.name ?? formatLabels(frame.fields[0]?.labels ?? {}); }; const getSeriesValue = (frame: DataFrame) => { - const value = frame.fields[0].values.get(0); + const value = frame.fields[0]?.values.get(0); if (Number.isFinite(value)) { return roundDecimals(value, 5); From 0d2a786816c7bac6bb14ab8ca2233a3e95c12da8 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Sun, 29 Jan 2023 20:14:12 -0800 Subject: [PATCH 071/117] Schema: Add schema for library panels (#62169) --- .betterer.results | 12 +- .../core/librarypanel/schema-reference.md | 70 ++++++ kinds/dashboard/dashboard_kind.cue | 8 + kinds/librarypanel/librarypanel_kind.cue | 63 +++++ packages/grafana-schema/src/index.gen.ts | 18 ++ .../raw/dashboard/x/dashboard_types.gen.ts | 9 + .../librarypanel/x/librarypanel_types.gen.ts | 65 ++++++ .../src/veneer/librarypanel.types.ts | 7 + pkg/kinds/dashboard/dashboard_types_gen.go | 9 +- .../librarypanel/librarypanel_kind_gen.go | 113 +++++++++ .../librarypanel/librarypanel_types_gen.go | 58 +++++ pkg/kindsys/report.json | 32 ++- pkg/registry/corekind/base_gen.go | 14 ++ pkg/services/libraryelements/database.go | 67 +++--- .../libraryelements_create_test.go | 37 +-- .../libraryelements_get_all_test.go | 217 +++++++++--------- .../libraryelements_get_test.go | 25 +- .../libraryelements_patch_test.go | 31 +-- .../libraryelements_permissions_test.go | 16 +- .../libraryelements/libraryelements_test.go | 7 +- pkg/services/libraryelements/models.go | 27 +-- .../librarypanels/librarypanels_test.go | 25 +- .../AddPanelWidget/AddPanelWidget.tsx | 2 +- .../DashExportModal/DashboardExporter.ts | 2 +- .../features/dashboard/state/PanelModel.ts | 6 +- .../LibraryPanelCard/LibraryPanelCard.tsx | 2 +- .../LibraryPanelsSearch.test.tsx | 18 +- .../LibraryPanelsView/reducer.test.ts | 11 +- .../SaveLibraryPanelModal.tsx | 4 +- public/app/features/library-panels/types.ts | 46 +--- .../ImportDashboardLibraryPanelsList.tsx | 4 +- .../manage-dashboards/state/actions.ts | 2 - 32 files changed, 724 insertions(+), 303 deletions(-) create mode 100644 docs/sources/developers/kinds/core/librarypanel/schema-reference.md create mode 100644 kinds/librarypanel/librarypanel_kind.cue create mode 100644 packages/grafana-schema/src/raw/librarypanel/x/librarypanel_types.gen.ts create mode 100644 packages/grafana-schema/src/veneer/librarypanel.types.ts create mode 100644 pkg/kinds/librarypanel/librarypanel_kind_gen.go create mode 100644 pkg/kinds/librarypanel/librarypanel_types_gen.go diff --git a/.betterer.results b/.betterer.results index 43127440a4e..f20d99d74fb 100644 --- a/.betterer.results +++ b/.betterer.results @@ -3106,8 +3106,10 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "6"], [0, 0, 0, "Unexpected any. Specify a different type.", "7"], [0, 0, 0, "Do not use any type assertions.", "8"], - [0, 0, 0, "Do not use any type assertions.", "9"], - [0, 0, 0, "Unexpected any. Specify a different type.", "10"] + [0, 0, 0, "Unexpected any. Specify a different type.", "9"], + [0, 0, 0, "Do not use any type assertions.", "10"], + [0, 0, 0, "Do not use any type assertions.", "11"], + [0, 0, 0, "Unexpected any. Specify a different type.", "12"] ], "public/app/features/dashboard/components/DashboardPrompt/DashboardPrompt.test.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -3988,9 +3990,6 @@ exports[`better eslint`] = { "public/app/features/library-panels/components/LibraryPanelsView/actions.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "public/app/features/library-panels/types.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/features/library-panels/utils.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] @@ -4091,6 +4090,9 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "4"], [0, 0, 0, "Unexpected any. Specify a different type.", "5"] ], + "public/app/features/manage-dashboards/components/ImportDashboardLibraryPanelsList.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], "public/app/features/manage-dashboards/components/SnapshotListTable.test.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], diff --git a/docs/sources/developers/kinds/core/librarypanel/schema-reference.md b/docs/sources/developers/kinds/core/librarypanel/schema-reference.md new file mode 100644 index 00000000000..6ac3ad434f7 --- /dev/null +++ b/docs/sources/developers/kinds/core/librarypanel/schema-reference.md @@ -0,0 +1,70 @@ +--- +keywords: + - grafana + - schema +title: LibraryPanel kind +--- +> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. + +# LibraryPanel kind + +## Maturity: experimental +## Version: 0.0 + +## Properties + +| Property | Type | Required | Description | +|-----------------|-------------------------------------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------| +| `model` | [object](#model) | **Yes** | TODO: should be the same panel schema defined in dashboard
Typescript: Omit; | +| `name` | string | **Yes** | Panel name (also saved in the model) | +| `type` | string | **Yes** | The panel type (from inside the model) | +| `uid` | string | **Yes** | Library element UID | +| `version` | integer | **Yes** | panel version, incremented each time the dashboard is updated. | +| `description` | string | No | Panel description | +| `folderUid` | string | No | Folder UID | +| `meta` | [LibraryElementDTOMeta](#libraryelementdtometa) | No | | +| `schemaVersion` | integer | No | Dashboard version when this was saved (zero if unknown) | + +## LibraryElementDTOMeta + +### Properties + +| Property | Type | Required | Description | +|-----------------------|---------------------------------------------------------|----------|-------------| +| `connectedDashboards` | integer | **Yes** | | +| `createdBy` | [LibraryElementDTOMetaUser](#libraryelementdtometauser) | **Yes** | | +| `created` | string | **Yes** | | +| `folderName` | string | **Yes** | | +| `folderUid` | string | **Yes** | | +| `updatedBy` | [LibraryElementDTOMetaUser](#libraryelementdtometauser) | **Yes** | | +| `updated` | string | **Yes** | | + +### LibraryElementDTOMetaUser + +#### Properties + +| Property | Type | Required | Description | +|-------------|---------|----------|-------------| +| `avatarUrl` | string | **Yes** | | +| `id` | integer | **Yes** | | +| `name` | string | **Yes** | | + +### LibraryElementDTOMetaUser + +#### Properties + +| Property | Type | Required | Description | +|-------------|---------|----------|-------------| +| `avatarUrl` | string | **Yes** | | +| `id` | integer | **Yes** | | +| `name` | string | **Yes** | | + +## model + +TODO: should be the same panel schema defined in dashboard +Typescript: Omit; + +| Property | Type | Required | Description | +|----------|------|----------|-------------| + + diff --git a/kinds/dashboard/dashboard_kind.cue b/kinds/dashboard/dashboard_kind.cue index 2aa725ad809..002331eb31b 100644 --- a/kinds/dashboard/dashboard_kind.cue +++ b/kinds/dashboard/dashboard_kind.cue @@ -397,6 +397,9 @@ lineage: seqs: [ // TODO tighter constraint timeShift?: string @grafanamaturity(NeedsExpertReview) + // Dynamically load the panel + libraryPanel?: #LibraryPanelRef + // options is specified by the PanelOptions field in panel // plugin schemas. options: {...} @grafanamaturity(NeedsExpertReview) @@ -412,6 +415,11 @@ lineage: seqs: [ }] @grafanamaturity(NeedsExpertReview) } @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview) + #LibraryPanelRef: { + name: string + uid: string + } @cuetsy(kind="interface") + #MatcherConfig: { id: string | *"" @grafanamaturity(NeedsExpertReview) options?: _ @grafanamaturity(NeedsExpertReview) diff --git a/kinds/librarypanel/librarypanel_kind.cue b/kinds/librarypanel/librarypanel_kind.cue new file mode 100644 index 00000000000..6a9cbd5c897 --- /dev/null +++ b/kinds/librarypanel/librarypanel_kind.cue @@ -0,0 +1,63 @@ +package kind + +import "strings" + +name: "LibraryPanel" +maturity: "experimental" + +lineage: seqs: [ + { + schemas: [ + // 0.0 + { + @grafana(TSVeneer="type") + + // Folder UID + folderUid?: string @grafanamaturity(ToMetadata="sys") + + // Library element UID + uid: string + + // Panel name (also saved in the model) + name: string & strings.MinRunes(1) + + // Panel description + description?: string + + // The panel type (from inside the model) + type: string & strings.MinRunes(1) + + // Dashboard version when this was saved (zero if unknown) + schemaVersion?: uint16 + + // panel version, incremented each time the dashboard is updated. + version: int64 @grafanamaturity(NeedsExpertReview) + + // TODO: should be the same panel schema defined in dashboard + // Typescript: Omit; + model: {...} + + // Object storage metadata + meta?: #LibraryElementDTOMeta @grafanamaturity(ToMetadata="sys") + + #LibraryElementDTOMetaUser: { + id: int64 + name: string + avatarUrl: string + } @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview) + + #LibraryElementDTOMeta: { + folderName: string + folderUid: string @grafanamaturity(ToMetadata="sys") + connectedDashboards: int64 + + created: string @grafanamaturity(ToMetadata="sys") // time.Time in golang + updated: string @grafanamaturity(ToMetadata="sys") // time.Time in golang + + createdBy: #LibraryElementDTOMetaUser @grafanamaturity(ToMetadata="sys") + updatedBy: #LibraryElementDTOMetaUser @grafanamaturity(ToMetadata="sys") + } @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview) + }, + ] + }, +] diff --git a/packages/grafana-schema/src/index.gen.ts b/packages/grafana-schema/src/index.gen.ts index a7a76a0c3df..f37d525a6ad 100644 --- a/packages/grafana-schema/src/index.gen.ts +++ b/packages/grafana-schema/src/index.gen.ts @@ -26,6 +26,7 @@ export type { SpecialValueMap, ValueMappingResult, Transformation, + LibraryPanelRef, RowPanel, GraphPanel, HeatmapPanel @@ -86,6 +87,23 @@ export { defaultFieldConfig } from './veneer/dashboard.types'; +// Raw generated types from LibraryPanel kind. +export type { + LibraryElementDTOMetaUser, + LibraryElementDTOMeta +} from './raw/librarypanel/x/librarypanel_types.gen'; + +// The following exported declarations correspond to types in the librarypanel@0.0 kind's +// schema with attribute @grafana(TSVeneer="type"). +// +// The handwritten file for these type and default veneers is expected to be at +// packages/grafana-schema/src/veneer/librarypanel.types.ts. +// This re-export declaration enforces that the handwritten veneer file exists, +// and exports all the symbols in the list. +// +// TODO generate code such that tsc enforces type compatibility between raw and veneer decls +export type { LibraryPanel } from './veneer/librarypanel.types'; + // Raw generated types from Playlist kind. export type { Playlist, diff --git a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts index 706d6522ba9..95b22b152ce 100644 --- a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts +++ b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts @@ -404,6 +404,10 @@ export interface Panel { * TODO tighter constraint */ interval?: string; + /** + * Dynamically load the panel + */ + libraryPanel?: LibraryPanelRef; /** * Panel links. * TODO fill this out - seems there are a couple variants? @@ -503,6 +507,11 @@ export const defaultFieldConfigSource: Partial = { overrides: [], }; +export interface LibraryPanelRef { + name: string; + uid: string; +} + export interface MatcherConfig { id: string; options?: unknown; diff --git a/packages/grafana-schema/src/raw/librarypanel/x/librarypanel_types.gen.ts b/packages/grafana-schema/src/raw/librarypanel/x/librarypanel_types.gen.ts new file mode 100644 index 00000000000..97cba2a5c5e --- /dev/null +++ b/packages/grafana-schema/src/raw/librarypanel/x/librarypanel_types.gen.ts @@ -0,0 +1,65 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. +// +// Generated by: +// kinds/gen.go +// Using jennies: +// TSTypesJenny +// LatestMajorsOrXJenny +// +// Run 'make gen-cue' from repository root to regenerate. + +export interface LibraryElementDTOMetaUser { + avatarUrl: string; + id: number; + name: string; +} + +export interface LibraryElementDTOMeta { + connectedDashboards: number; + created: string; + createdBy: LibraryElementDTOMetaUser; + folderName: string; + folderUid: string; + updated: string; + updatedBy: LibraryElementDTOMetaUser; +} + +export interface LibraryPanel { + /** + * Panel description + */ + description?: string; + /** + * Folder UID + */ + folderUid?: string; + /** + * Object storage metadata + */ + meta?: LibraryElementDTOMeta; + /** + * TODO: should be the same panel schema defined in dashboard + * Typescript: Omit; + */ + model: Record; + /** + * Panel name (also saved in the model) + */ + name: string; + /** + * Dashboard version when this was saved (zero if unknown) + */ + schemaVersion?: number; + /** + * The panel type (from inside the model) + */ + type: string; + /** + * Library element UID + */ + uid: string; + /** + * panel version, incremented each time the dashboard is updated. + */ + version: number; +} diff --git a/packages/grafana-schema/src/veneer/librarypanel.types.ts b/packages/grafana-schema/src/veneer/librarypanel.types.ts new file mode 100644 index 00000000000..007bb085742 --- /dev/null +++ b/packages/grafana-schema/src/veneer/librarypanel.types.ts @@ -0,0 +1,7 @@ +import * as raw from '../raw/librarypanel/x/librarypanel_types.gen'; + +import { Panel } from './dashboard.types'; + +export interface LibraryPanel extends raw.LibraryPanel { + model: Omit; +} diff --git a/pkg/kinds/dashboard/dashboard_types_gen.go b/pkg/kinds/dashboard/dashboard_types_gen.go index 7ed218331cc..9fdc6e22532 100644 --- a/pkg/kinds/dashboard/dashboard_types_gen.go +++ b/pkg/kinds/dashboard/dashboard_types_gen.go @@ -458,6 +458,12 @@ type HeatmapPanel struct { // HeatmapPanelType defines model for HeatmapPanel.Type. type HeatmapPanelType string +// LibraryPanelRef defines model for LibraryPanelRef. +type LibraryPanelRef struct { + Name string `json:"name"` + Uid string `json:"uid"` +} + // LoadingState defines model for LoadingState. type LoadingState string @@ -490,7 +496,8 @@ type Panel struct { // TODO docs // TODO tighter constraint - Interval *string `json:"interval,omitempty"` + Interval *string `json:"interval,omitempty"` + LibraryPanel *LibraryPanelRef `json:"libraryPanel,omitempty"` // Panel links. // TODO fill this out - seems there are a couple variants? diff --git a/pkg/kinds/librarypanel/librarypanel_kind_gen.go b/pkg/kinds/librarypanel/librarypanel_kind_gen.go new file mode 100644 index 00000000000..564ddfb169c --- /dev/null +++ b/pkg/kinds/librarypanel/librarypanel_kind_gen.go @@ -0,0 +1,113 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. +// +// Generated by: +// kinds/gen.go +// Using jennies: +// CoreKindJenny +// +// Run 'make gen-cue' from repository root to regenerate. + +package librarypanel + +import ( + "github.com/grafana/grafana/pkg/kindsys" + "github.com/grafana/thema" + "github.com/grafana/thema/vmux" +) + +// rootrel is the relative path from the grafana repository root to the +// directory containing the .cue files in which this kind is declared. Necessary +// for runtime errors related to the declaration and/or lineage to provide +// a real path to the correct .cue file. +const rootrel string = "kinds/librarypanel" + +// TODO standard generated docs +type Kind struct { + lin thema.ConvergentLineage[*LibraryPanel] + jcodec vmux.Codec + valmux vmux.ValueMux[*LibraryPanel] + decl kindsys.Decl[kindsys.CoreProperties] +} + +// type guard +var _ kindsys.Core = &Kind{} + +// TODO standard generated docs +func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) { + decl, err := kindsys.LoadCoreKind(rootrel, rt.Context(), nil) + if err != nil { + return nil, err + } + k := &Kind{ + decl: decl, + } + + lin, err := decl.Some().BindKindLineage(rt, opts...) + if err != nil { + return nil, err + } + + // Get the thema.Schema that the meta says is in the current version (which + // codegen ensures is always the latest) + cursch := thema.SchemaP(lin, k.decl.Properties.CurrentVersion) + tsch, err := thema.BindType[*LibraryPanel](cursch, &LibraryPanel{}) + if err != nil { + // Should be unreachable, modulo bugs in the Thema->Go code generator + return nil, err + } + + k.jcodec = vmux.NewJSONCodec("librarypanel.json") + k.lin = tsch.ConvergentLineage() + k.valmux = vmux.NewValueMux(k.lin.TypedSchema(), k.jcodec) + return k, nil +} + +// TODO standard generated docs +func (k *Kind) Name() string { + return "librarypanel" +} + +// TODO standard generated docs +func (k *Kind) MachineName() string { + return "librarypanel" +} + +// TODO standard generated docs +func (k *Kind) Lineage() thema.Lineage { + return k.lin +} + +// TODO standard generated docs +func (k *Kind) ConvergentLineage() thema.ConvergentLineage[*LibraryPanel] { + return k.lin +} + +// JSONValueMux is a version multiplexer that maps a []byte containing JSON data +// at any schematized dashboard version to an instance of LibraryPanel. +// +// Validation and translation errors emitted from this func will identify the +// input bytes as "dashboard.json". +// +// This is a thin wrapper around Thema's [vmux.ValueMux]. +func (k *Kind) JSONValueMux(b []byte) (*LibraryPanel, thema.TranslationLacunas, error) { + return k.valmux(b) +} + +// TODO standard generated docs +func (k *Kind) Maturity() kindsys.Maturity { + return k.decl.Properties.Maturity +} + +// Decl returns the [kindsys.Decl] containing both CUE and Go representations of the +// librarypanel declaration in .cue files. +func (k *Kind) Decl() kindsys.Decl[kindsys.CoreProperties] { + return k.decl +} + +// Props returns a [kindsys.SomeKindProps], with underlying type [kindsys.CoreProperties], +// representing the static properties declared in the librarypanel kind. +// +// This method is identical to calling Decl().Props. It is provided to satisfy [kindsys.Interface]. +func (k *Kind) Props() kindsys.SomeKindProperties { + return k.decl.Properties +} diff --git a/pkg/kinds/librarypanel/librarypanel_types_gen.go b/pkg/kinds/librarypanel/librarypanel_types_gen.go new file mode 100644 index 00000000000..bdd4545030d --- /dev/null +++ b/pkg/kinds/librarypanel/librarypanel_types_gen.go @@ -0,0 +1,58 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. +// +// Generated by: +// kinds/gen.go +// Using jennies: +// GoTypesJenny +// LatestJenny +// +// Run 'make gen-cue' from repository root to regenerate. + +package librarypanel + +// LibraryElementDTOMeta defines model for LibraryElementDTOMeta. +type LibraryElementDTOMeta struct { + ConnectedDashboards int64 `json:"connectedDashboards"` + Created string `json:"created"` + CreatedBy LibraryElementDTOMetaUser `json:"createdBy"` + FolderName string `json:"folderName"` + FolderUid string `json:"folderUid"` + Updated string `json:"updated"` + UpdatedBy LibraryElementDTOMetaUser `json:"updatedBy"` +} + +// LibraryElementDTOMetaUser defines model for LibraryElementDTOMetaUser. +type LibraryElementDTOMetaUser struct { + AvatarUrl string `json:"avatarUrl"` + Id int64 `json:"id"` + Name string `json:"name"` +} + +// LibraryPanel defines model for LibraryPanel. +type LibraryPanel struct { + // Panel description + Description *string `json:"description,omitempty"` + + // Folder UID + FolderUid *string `json:"folderUid,omitempty"` + Meta *LibraryElementDTOMeta `json:"meta,omitempty"` + + // TODO: should be the same panel schema defined in dashboard + // Typescript: Omit; + Model map[string]interface{} `json:"model"` + + // Panel name (also saved in the model) + Name string `json:"name"` + + // Dashboard version when this was saved (zero if unknown) + SchemaVersion *int `json:"schemaVersion,omitempty"` + + // The panel type (from inside the model) + Type string `json:"type"` + + // Library element UID + Uid string `json:"uid"` + + // panel version, incremented each time the dashboard is updated. + Version int64 `json:"version"` +} diff --git a/pkg/kindsys/report.json b/pkg/kindsys/report.json index d6f4b88460e..0944a34047f 100644 --- a/pkg/kindsys/report.json +++ b/pkg/kindsys/report.json @@ -807,6 +807,32 @@ "pluralName": "JaegerDataSourceCfgs", "schemaInterface": "DataSourceCfg" }, + "librarypanel": { + "category": "core", + "codeowners": [ + "grafana/grafana-as-code", + "grafana/grafana-bi-squad", + "grafana/plugins-platform-frontend", + "grafana/user-essentials" + ], + "currentVersion": [ + 0, + 0 + ], + "grafanaMaturityCount": 10, + "lineageIsGroup": false, + "links": { + "docs": "https://grafana.com/docs/grafana/next/developers/kinds/core/librarypanel/schema-reference", + "go": "https://github.com/grafana/grafana/tree/main/pkg/kinds/librarypanel", + "schema": "https://github.com/grafana/grafana/tree/main/kinds/librarypanel/librarypanel_kind.cue", + "ts": "https://github.com/grafana/grafana/tree/main/packages/grafana-schema/src/raw/librarypanel/x/librarypanel_types.gen.ts" + }, + "machineName": "librarypanel", + "maturity": "experimental", + "name": "LibraryPanel", + "pluralMachineName": "librarypanels", + "pluralName": "LibraryPanels" + }, "livepanelcfg": { "category": "composable", "codeowners": [], @@ -1846,6 +1872,7 @@ "dashboard", "datasource", "folder", + "librarypanel", "playlist", "preferences", "publicdashboard", @@ -1856,7 +1883,7 @@ "thumb", "user" ], - "count": 13 + "count": 14 } }, "maturity": { @@ -1870,6 +1897,7 @@ "dashboardlistpanelcfg", "gaugepanelcfg", "histogrampanelcfg", + "librarypanel", "newspanelcfg", "nodegraphpanelcfg", "piechartpanelcfg", @@ -1880,7 +1908,7 @@ "textpanelcfg", "xychartpanelcfg" ], - "count": 16 + "count": 17 }, "mature": { "name": "mature", diff --git a/pkg/registry/corekind/base_gen.go b/pkg/registry/corekind/base_gen.go index 8a180db1ca0..99d00e9c0d0 100644 --- a/pkg/registry/corekind/base_gen.go +++ b/pkg/registry/corekind/base_gen.go @@ -13,6 +13,7 @@ import ( "fmt" "github.com/grafana/grafana/pkg/kinds/dashboard" + "github.com/grafana/grafana/pkg/kinds/librarypanel" "github.com/grafana/grafana/pkg/kinds/playlist" "github.com/grafana/grafana/pkg/kinds/preferences" "github.com/grafana/grafana/pkg/kinds/publicdashboard" @@ -35,6 +36,7 @@ import ( type Base struct { all []kindsys.Core dashboard *dashboard.Kind + librarypanel *librarypanel.Kind playlist *playlist.Kind preferences *preferences.Kind publicdashboard *publicdashboard.Kind @@ -45,6 +47,7 @@ type Base struct { // type guards var ( _ kindsys.Core = &dashboard.Kind{} + _ kindsys.Core = &librarypanel.Kind{} _ kindsys.Core = &playlist.Kind{} _ kindsys.Core = &preferences.Kind{} _ kindsys.Core = &publicdashboard.Kind{} @@ -57,6 +60,11 @@ func (b *Base) Dashboard() *dashboard.Kind { return b.dashboard } +// LibraryPanel returns the [kindsys.Interface] implementation for the librarypanel kind. +func (b *Base) LibraryPanel() *librarypanel.Kind { + return b.librarypanel +} + // Playlist returns the [kindsys.Interface] implementation for the playlist kind. func (b *Base) Playlist() *playlist.Kind { return b.playlist @@ -92,6 +100,12 @@ func doNewBase(rt *thema.Runtime) *Base { } reg.all = append(reg.all, reg.dashboard) + reg.librarypanel, err = librarypanel.NewKind(rt) + if err != nil { + panic(fmt.Sprintf("error while initializing the librarypanel Kind: %s", err)) + } + reg.all = append(reg.all, reg.librarypanel) + reg.playlist, err = playlist.NewKind(rt) if err != nil { panic(fmt.Sprintf("error while initializing the playlist Kind: %s", err)) diff --git a/pkg/services/libraryelements/database.go b/pkg/services/libraryelements/database.go index 99f6f1d3057..ae8f3cf8df3 100644 --- a/pkg/services/libraryelements/database.go +++ b/pkg/services/libraryelements/database.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/kinds/librarypanel" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" @@ -166,15 +167,15 @@ func (l *LibraryElementService) createLibraryElement(c context.Context, signedIn ConnectedDashboards: 0, Created: element.Created, Updated: element.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: element.CreatedBy, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: element.CreatedBy, Name: signedInUser.Login, - AvatarURL: dtos.GetGravatarUrl(signedInUser.Email), + AvatarUrl: dtos.GetGravatarUrl(signedInUser.Email), }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: element.UpdatedBy, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: element.UpdatedBy, Name: signedInUser.Login, - AvatarURL: dtos.GetGravatarUrl(signedInUser.Email), + AvatarUrl: dtos.GetGravatarUrl(signedInUser.Email), }, }, } @@ -279,15 +280,15 @@ func getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signed ConnectedDashboards: libraryElement.ConnectedDashboards, Created: libraryElement.Created, Updated: libraryElement.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: libraryElement.CreatedBy, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: libraryElement.CreatedBy, Name: libraryElement.CreatedByName, - AvatarURL: dtos.GetGravatarUrl(libraryElement.CreatedByEmail), + AvatarUrl: dtos.GetGravatarUrl(libraryElement.CreatedByEmail), }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: libraryElement.UpdatedBy, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: libraryElement.UpdatedBy, Name: libraryElement.UpdatedByName, - AvatarURL: dtos.GetGravatarUrl(libraryElement.UpdatedByEmail), + AvatarUrl: dtos.GetGravatarUrl(libraryElement.UpdatedByEmail), }, }, } @@ -392,15 +393,15 @@ func (l *LibraryElementService) getAllLibraryElements(c context.Context, signedI ConnectedDashboards: element.ConnectedDashboards, Created: element.Created, Updated: element.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: element.CreatedBy, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: element.CreatedBy, Name: element.CreatedByName, - AvatarURL: dtos.GetGravatarUrl(element.CreatedByEmail), + AvatarUrl: dtos.GetGravatarUrl(element.CreatedByEmail), }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: element.UpdatedBy, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: element.UpdatedBy, Name: element.UpdatedByName, - AvatarURL: dtos.GetGravatarUrl(element.UpdatedByEmail), + AvatarUrl: dtos.GetGravatarUrl(element.UpdatedByEmail), }, }, }) @@ -541,15 +542,15 @@ func (l *LibraryElementService) patchLibraryElement(c context.Context, signedInU ConnectedDashboards: elementInDB.ConnectedDashboards, Created: libraryElement.Created, Updated: libraryElement.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: elementInDB.CreatedBy, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: elementInDB.CreatedBy, Name: elementInDB.CreatedByName, - AvatarURL: dtos.GetGravatarUrl(elementInDB.CreatedByEmail), + AvatarUrl: dtos.GetGravatarUrl(elementInDB.CreatedByEmail), }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: libraryElement.UpdatedBy, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: libraryElement.UpdatedBy, Name: signedInUser.Login, - AvatarURL: dtos.GetGravatarUrl(signedInUser.Email), + AvatarUrl: dtos.GetGravatarUrl(signedInUser.Email), }, }, } @@ -589,10 +590,10 @@ func (l *LibraryElementService) getConnections(c context.Context, signedInUser * ConnectionID: connection.ConnectionID, ConnectionUID: connection.ConnectionUID, Created: connection.Created, - CreatedBy: LibraryElementDTOMetaUser{ - ID: connection.CreatedBy, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: connection.CreatedBy, Name: connection.CreatedByName, - AvatarURL: dtos.GetGravatarUrl(connection.CreatedByEmail), + AvatarUrl: dtos.GetGravatarUrl(connection.CreatedByEmail), }, }) } @@ -638,15 +639,15 @@ func (l *LibraryElementService) getElementsForDashboardID(c context.Context, das ConnectedDashboards: element.ConnectedDashboards, Created: element.Created, Updated: element.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: element.CreatedBy, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: element.CreatedBy, Name: element.CreatedByName, - AvatarURL: dtos.GetGravatarUrl(element.CreatedByEmail), + AvatarUrl: dtos.GetGravatarUrl(element.CreatedByEmail), }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: element.UpdatedBy, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: element.UpdatedBy, Name: element.UpdatedByName, - AvatarURL: dtos.GetGravatarUrl(element.UpdatedByEmail), + AvatarUrl: dtos.GetGravatarUrl(element.UpdatedByEmail), }, }, } diff --git a/pkg/services/libraryelements/libraryelements_create_test.go b/pkg/services/libraryelements/libraryelements_create_test.go index ecc54aca467..ef71130e19b 100644 --- a/pkg/services/libraryelements/libraryelements_create_test.go +++ b/pkg/services/libraryelements/libraryelements_create_test.go @@ -6,6 +6,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/kinds/librarypanel" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/util" ) @@ -45,15 +46,15 @@ func TestCreateLibraryElement(t *testing.T) { ConnectedDashboards: 0, Created: sc.initialResult.Result.Meta.Created, Updated: sc.initialResult.Result.Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: "signed_in_user", - AvatarURL: "/avatar/37524e1eb8b3e32850b57db0a19af93b", + AvatarUrl: "/avatar/37524e1eb8b3e32850b57db0a19af93b", }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: "signed_in_user", - AvatarURL: "/avatar/37524e1eb8b3e32850b57db0a19af93b", + AvatarUrl: "/avatar/37524e1eb8b3e32850b57db0a19af93b", }, }, }, @@ -94,15 +95,15 @@ func TestCreateLibraryElement(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Meta.Created, Updated: result.Result.Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: "signed_in_user", - AvatarURL: "/avatar/37524e1eb8b3e32850b57db0a19af93b", + AvatarUrl: "/avatar/37524e1eb8b3e32850b57db0a19af93b", }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: "signed_in_user", - AvatarURL: "/avatar/37524e1eb8b3e32850b57db0a19af93b", + AvatarUrl: "/avatar/37524e1eb8b3e32850b57db0a19af93b", }, }, }, @@ -169,15 +170,15 @@ func TestCreateLibraryElement(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Meta.Created, Updated: result.Result.Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: "signed_in_user", - AvatarURL: "/avatar/37524e1eb8b3e32850b57db0a19af93b", + AvatarUrl: "/avatar/37524e1eb8b3e32850b57db0a19af93b", }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: "signed_in_user", - AvatarURL: "/avatar/37524e1eb8b3e32850b57db0a19af93b", + AvatarUrl: "/avatar/37524e1eb8b3e32850b57db0a19af93b", }, }, }, diff --git a/pkg/services/libraryelements/libraryelements_get_all_test.go b/pkg/services/libraryelements/libraryelements_get_all_test.go index bac68141c57..2c304eaab2f 100644 --- a/pkg/services/libraryelements/libraryelements_get_all_test.go +++ b/pkg/services/libraryelements/libraryelements_get_all_test.go @@ -8,6 +8,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/kinds/librarypanel" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/search" @@ -81,15 +82,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -146,15 +147,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -208,15 +209,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -243,15 +244,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[1].Meta.Created, Updated: result.Result.Elements[1].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -308,15 +309,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -343,15 +344,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[1].Meta.Created, Updated: result.Result.Elements[1].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -429,15 +430,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -464,15 +465,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[1].Meta.Created, Updated: result.Result.Elements[1].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -568,15 +569,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -665,15 +666,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -700,15 +701,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[1].Meta.Created, Updated: result.Result.Elements[1].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -765,15 +766,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -830,15 +831,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -896,15 +897,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -971,15 +972,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -1044,15 +1045,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -1079,15 +1080,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[1].Meta.Created, Updated: result.Result.Elements[1].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -1146,15 +1147,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, diff --git a/pkg/services/libraryelements/libraryelements_get_test.go b/pkg/services/libraryelements/libraryelements_get_test.go index ff587a20d85..ce6d6aecc73 100644 --- a/pkg/services/libraryelements/libraryelements_get_test.go +++ b/pkg/services/libraryelements/libraryelements_get_test.go @@ -5,6 +5,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/kinds/librarypanel" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/web" @@ -54,15 +55,15 @@ func TestGetLibraryElement(t *testing.T) { ConnectedDashboards: 0, Created: res.Result.Meta.Created, Updated: res.Result.Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -149,15 +150,15 @@ func TestGetLibraryElement(t *testing.T) { ConnectedDashboards: 1, Created: res.Result.Meta.Created, Updated: res.Result.Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, diff --git a/pkg/services/libraryelements/libraryelements_patch_test.go b/pkg/services/libraryelements/libraryelements_patch_test.go index 51d1f06e822..83e3bd25c22 100644 --- a/pkg/services/libraryelements/libraryelements_patch_test.go +++ b/pkg/services/libraryelements/libraryelements_patch_test.go @@ -3,6 +3,7 @@ package libraryelements import ( "testing" + "github.com/grafana/grafana/pkg/kinds/librarypanel" "github.com/grafana/grafana/pkg/util" "github.com/google/go-cmp/cmp" @@ -68,15 +69,15 @@ func TestPatchLibraryElement(t *testing.T) { ConnectedDashboards: 0, Created: sc.initialResult.Result.Meta.Created, Updated: result.Result.Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: "signed_in_user", - AvatarURL: "/avatar/37524e1eb8b3e32850b57db0a19af93b", + AvatarUrl: "/avatar/37524e1eb8b3e32850b57db0a19af93b", }, }, }, @@ -101,7 +102,7 @@ func TestPatchLibraryElement(t *testing.T) { var result = validateAndUnMarshalResponse(t, resp) sc.initialResult.Result.FolderID = newFolder.ID sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName - sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar + sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = userInDbAvatar sc.initialResult.Result.Meta.Updated = result.Result.Meta.Updated sc.initialResult.Result.Version = 2 sc.initialResult.Result.Meta.FolderName = "NewFolder" @@ -125,7 +126,7 @@ func TestPatchLibraryElement(t *testing.T) { var result = validateAndUnMarshalResponse(t, resp) sc.initialResult.Result.Name = "New Name" sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName - sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar + sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = userInDbAvatar sc.initialResult.Result.Meta.Updated = result.Result.Meta.Updated sc.initialResult.Result.Model["title"] = "Text - Library Panel" sc.initialResult.Result.Version = 2 @@ -148,7 +149,7 @@ func TestPatchLibraryElement(t *testing.T) { var result = validateAndUnMarshalResponse(t, resp) sc.initialResult.Result.UID = cmd.UID sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName - sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar + sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = userInDbAvatar sc.initialResult.Result.Meta.Updated = result.Result.Meta.Updated sc.initialResult.Result.Model["title"] = "Text - Library Panel" sc.initialResult.Result.Version = 2 @@ -225,7 +226,7 @@ func TestPatchLibraryElement(t *testing.T) { "description": "New description", } sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName - sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar + sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = userInDbAvatar sc.initialResult.Result.Meta.Updated = result.Result.Meta.Updated sc.initialResult.Result.Version = 2 if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" { @@ -252,7 +253,7 @@ func TestPatchLibraryElement(t *testing.T) { "description": "New description", } sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName - sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar + sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = userInDbAvatar sc.initialResult.Result.Meta.Updated = result.Result.Meta.Updated sc.initialResult.Result.Version = 2 if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" { @@ -279,7 +280,7 @@ func TestPatchLibraryElement(t *testing.T) { "description": "A description", } sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName - sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar + sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = userInDbAvatar sc.initialResult.Result.Meta.Updated = result.Result.Meta.Updated sc.initialResult.Result.Version = 2 if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" { @@ -295,9 +296,9 @@ func TestPatchLibraryElement(t *testing.T) { sc.ctx.Req.Body = mockRequestBody(cmd) resp := sc.service.patchHandler(sc.reqContext) var result = validateAndUnMarshalResponse(t, resp) - sc.initialResult.Result.Meta.UpdatedBy.ID = int64(2) + sc.initialResult.Result.Meta.UpdatedBy.Id = int64(2) sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName - sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar + sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = userInDbAvatar sc.initialResult.Result.Meta.Updated = result.Result.Meta.Updated sc.initialResult.Result.Version = 2 if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" { @@ -393,7 +394,7 @@ func TestPatchLibraryElement(t *testing.T) { "description": "A description", } sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName - sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar + sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = userInDbAvatar sc.initialResult.Result.Meta.Updated = result.Result.Meta.Updated sc.initialResult.Result.Version = 2 if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" { diff --git a/pkg/services/libraryelements/libraryelements_permissions_test.go b/pkg/services/libraryelements/libraryelements_permissions_test.go index 3e3c1b4e6dc..b7078f7a5d6 100644 --- a/pkg/services/libraryelements/libraryelements_permissions_test.go +++ b/pkg/services/libraryelements/libraryelements_permissions_test.go @@ -252,9 +252,9 @@ func TestLibraryElementPermissions(t *testing.T) { resp := sc.service.createHandler(sc.reqContext) result := validateAndUnMarshalResponse(t, resp) result.Result.Meta.CreatedBy.Name = userInDbName - result.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar + result.Result.Meta.CreatedBy.AvatarUrl = userInDbAvatar result.Result.Meta.UpdatedBy.Name = userInDbName - result.Result.Meta.UpdatedBy.AvatarURL = userInDbAvatar + result.Result.Meta.UpdatedBy.AvatarUrl = userInDbAvatar result.Result.Meta.FolderName = folder.Title result.Result.Meta.FolderUID = folder.UID results = append(results, result.Result) @@ -275,9 +275,9 @@ func TestLibraryElementPermissions(t *testing.T) { resp := sc.service.createHandler(sc.reqContext) result := validateAndUnMarshalResponse(t, resp) result.Result.Meta.CreatedBy.Name = userInDbName - result.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar + result.Result.Meta.CreatedBy.AvatarUrl = userInDbAvatar result.Result.Meta.UpdatedBy.Name = userInDbName - result.Result.Meta.UpdatedBy.AvatarURL = userInDbAvatar + result.Result.Meta.UpdatedBy.AvatarUrl = userInDbAvatar result.Result.Meta.FolderName = "General" result.Result.Meta.FolderUID = "" sc.reqContext.SignedInUser.OrgRole = testCase.role @@ -315,9 +315,9 @@ func TestLibraryElementPermissions(t *testing.T) { resp := sc.service.createHandler(sc.reqContext) result := validateAndUnMarshalResponse(t, resp) result.Result.Meta.CreatedBy.Name = userInDbName - result.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar + result.Result.Meta.CreatedBy.AvatarUrl = userInDbAvatar result.Result.Meta.UpdatedBy.Name = userInDbName - result.Result.Meta.UpdatedBy.AvatarURL = userInDbAvatar + result.Result.Meta.UpdatedBy.AvatarUrl = userInDbAvatar result.Result.Meta.FolderName = folder.Title result.Result.Meta.FolderUID = folder.UID results = append(results, result.Result) @@ -367,9 +367,9 @@ func TestLibraryElementPermissions(t *testing.T) { resp := sc.service.createHandler(sc.reqContext) result := validateAndUnMarshalResponse(t, resp) result.Result.Meta.CreatedBy.Name = userInDbName - result.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar + result.Result.Meta.CreatedBy.AvatarUrl = userInDbAvatar result.Result.Meta.UpdatedBy.Name = userInDbName - result.Result.Meta.UpdatedBy.AvatarURL = userInDbAvatar + result.Result.Meta.UpdatedBy.AvatarUrl = userInDbAvatar result.Result.Meta.FolderName = "General" sc.reqContext.SignedInUser.OrgRole = testCase.role diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index 79171ff888a..68b852bcea5 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -19,6 +19,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/kinds/librarypanel" "github.com/grafana/grafana/pkg/models" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/alerting" @@ -165,10 +166,10 @@ func TestGetLibraryPanelConnections(t *testing.T) { ConnectionID: dashInDB.ID, ConnectionUID: dashInDB.UID, Created: res.Result[0].Created, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, diff --git a/pkg/services/libraryelements/models.go b/pkg/services/libraryelements/models.go index 54af6dbd9a0..3fd5d1db981 100644 --- a/pkg/services/libraryelements/models.go +++ b/pkg/services/libraryelements/models.go @@ -4,6 +4,8 @@ import ( "encoding/json" "errors" "time" + + "github.com/grafana/grafana/pkg/kinds/librarypanel" ) type LibraryConnectionKind int @@ -93,15 +95,8 @@ type LibraryElementDTOMeta struct { Created time.Time `json:"created"` Updated time.Time `json:"updated"` - CreatedBy LibraryElementDTOMetaUser `json:"createdBy"` - UpdatedBy LibraryElementDTOMetaUser `json:"updatedBy"` -} - -// LibraryElementDTOMetaUser is the meta information for user that creates/changes the library element. -type LibraryElementDTOMetaUser struct { - ID int64 `json:"id"` - Name string `json:"name"` - AvatarURL string `json:"avatarUrl"` + CreatedBy librarypanel.LibraryElementDTOMetaUser `json:"createdBy"` + UpdatedBy librarypanel.LibraryElementDTOMetaUser `json:"updatedBy"` } // libraryElementConnection is the model for library element connections. @@ -129,13 +124,13 @@ type libraryElementConnectionWithMeta struct { // LibraryElementConnectionDTO is the frontend DTO for element connections. type LibraryElementConnectionDTO struct { - ID int64 `json:"id"` - Kind int64 `json:"kind"` - ElementID int64 `json:"elementId"` - ConnectionID int64 `json:"connectionId"` - ConnectionUID string `json:"connectionUid"` - Created time.Time `json:"created"` - CreatedBy LibraryElementDTOMetaUser `json:"createdBy"` + ID int64 `json:"id"` + Kind int64 `json:"kind"` + ElementID int64 `json:"elementId"` + ConnectionID int64 `json:"connectionId"` + ConnectionUID string `json:"connectionUid"` + Created time.Time `json:"created"` + CreatedBy librarypanel.LibraryElementDTOMetaUser `json:"createdBy"` } var ( diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index c0ae1ba624c..85929ee8282 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -18,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/kinds/librarypanel" "github.com/grafana/grafana/pkg/models" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/alerting" @@ -634,15 +635,15 @@ func toLibraryElement(t *testing.T, res libraryelements.LibraryElementDTO) libra ConnectedDashboards: res.Meta.ConnectedDashboards, Created: res.Meta.Created, Updated: res.Meta.Updated, - CreatedBy: libraryelements.LibraryElementDTOMetaUser{ - ID: res.Meta.CreatedBy.ID, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: res.Meta.CreatedBy.Id, Name: res.Meta.CreatedBy.Name, - AvatarURL: res.Meta.CreatedBy.AvatarURL, + AvatarUrl: res.Meta.CreatedBy.AvatarUrl, }, - UpdatedBy: libraryelements.LibraryElementDTOMetaUser{ - ID: res.Meta.UpdatedBy.ID, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: res.Meta.UpdatedBy.Id, Name: res.Meta.UpdatedBy.Name, - AvatarURL: res.Meta.UpdatedBy.AvatarURL, + AvatarUrl: res.Meta.UpdatedBy.AvatarUrl, }, }, } @@ -672,15 +673,15 @@ func getExpected(t *testing.T, res libraryelements.LibraryElementDTO, UID string ConnectedDashboards: 0, Created: res.Meta.Created, Updated: res.Meta.Updated, - CreatedBy: libraryelements.LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: libraryelements.LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, } diff --git a/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx index cf158139c4f..b88b3d683a6 100644 --- a/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx +++ b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx @@ -113,7 +113,7 @@ export const AddPanelWidgetUnconnected = ({ panel, dashboard }: Props) => { const onAddLibraryPanel = (panelInfo: LibraryElementDTO) => { const { gridPos } = panel; - const newPanel: PanelModel = { + const newPanel = { ...panelInfo.model, gridPos, libraryPanel: panelInfo, diff --git a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts index 86596469639..2fd1e04f537 100644 --- a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts +++ b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts @@ -177,7 +177,7 @@ export class DashboardExporter { model = libPanel.model; } - const { gridPos, id, ...rest } = model; + const { gridPos, id, ...rest } = model as any; if (!libraryPanels.has(uid)) { libraryPanels.set(uid, { name, uid, kind: LibraryElementKind.Panel, model: rest }); } diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index 6a1645da5fd..d55fba4feaf 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -23,6 +23,7 @@ import { restoreCustomOverrideRules, } from '@grafana/data'; import { getTemplateSrv, RefreshEvent } from '@grafana/runtime'; +import { LibraryPanel, LibraryPanelRef } from '@grafana/schema'; import config from 'app/core/config'; import { safeStringifyValue } from 'app/core/utils/explore'; import { getNextRefIdChar } from 'app/core/utils/query'; @@ -35,7 +36,6 @@ import { RenderEvent, } from 'app/types/events'; -import { LibraryElementDTO, LibraryPanelRef } from '../../library-panels/types'; import { PanelQueryRunner } from '../../query/state/PanelQueryRunner'; import { getVariablesUrlParams } from '../../variables/getAllVariableValuesForUrl'; import { getTimeSrv } from '../services/TimeSrv'; @@ -172,7 +172,7 @@ export class PanelModel implements DataConfigSource, IPanelModel { links?: DataLink[]; declare transparent: boolean; - libraryPanel?: LibraryPanelRef | LibraryElementDTO; + libraryPanel?: LibraryPanelRef | LibraryPanel; autoMigrateFrom?: string; @@ -680,7 +680,7 @@ export class PanelModel implements DataConfigSource, IPanelModel { return this.replaceVariables(this.title, undefined, 'text'); } - initLibraryPanel(libPanel: LibraryElementDTO) { + initLibraryPanel(libPanel: LibraryPanel) { for (const [key, val] of Object.entries(libPanel.model)) { switch (key) { case 'id': diff --git a/public/app/features/library-panels/components/LibraryPanelCard/LibraryPanelCard.tsx b/public/app/features/library-panels/components/LibraryPanelCard/LibraryPanelCard.tsx index 1717ab7c313..e3921fddb70 100644 --- a/public/app/features/library-panels/components/LibraryPanelCard/LibraryPanelCard.tsx +++ b/public/app/features/library-panels/components/LibraryPanelCard/LibraryPanelCard.tsx @@ -62,7 +62,7 @@ interface FolderLinkProps { function FolderLink({ libraryPanel }: FolderLinkProps): ReactElement | null { const styles = useStyles2(getStyles); - if (!libraryPanel.meta.folderUid && !libraryPanel.meta.folderName) { + if (!libraryPanel.meta?.folderUid && !libraryPanel.meta?.folderName) { return null; } diff --git a/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.test.tsx b/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.test.tsx index 726957acb8c..3b9df62c30c 100644 --- a/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.test.tsx +++ b/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.test.tsx @@ -4,11 +4,12 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; import { PanelPluginMeta, PluginType } from '@grafana/data'; +import { Panel } from '@grafana/schema'; import { backendSrv } from '../../../../core/services/backend_srv'; import * as panelUtils from '../../../panel/state/util'; import * as api from '../../state/api'; -import { LibraryElementKind, LibraryElementsSearchResult } from '../../types'; +import { LibraryElementsSearchResult } from '../../types'; import { LibraryPanelsSearch, LibraryPanelsSearchProps } from './LibraryPanelsSearch'; @@ -182,15 +183,12 @@ describe('LibraryPanelsSearch', () => { { elements: [ { - id: 1, name: 'Library Panel Name', - kind: LibraryElementKind.Panel, uid: 'uid', description: 'Library Panel Description', folderUid: '', - model: { type: 'timeseries', title: 'A title' }, + model: { type: 'timeseries', title: 'A title' } as Panel, type: 'timeseries', - orgId: 1, version: 1, meta: { folderName: 'General', @@ -237,15 +235,12 @@ describe('LibraryPanelsSearch', () => { perPage: 40, elements: [ { - id: 1, name: 'Library Panel Name', - kind: LibraryElementKind.Panel, uid: 'uid', description: 'Library Panel Description', folderUid: '', - model: { type: 'timeseries', title: 'A title' }, + model: { type: 'timeseries', title: 'A title' } as Panel, type: 'timeseries', - orgId: 1, version: 1, meta: { folderName: 'General', @@ -281,15 +276,12 @@ describe('LibraryPanelsSearch', () => { perPage: 40, elements: [ { - id: 1, name: 'Library Panel Name', - kind: LibraryElementKind.Panel, uid: 'uid', description: 'Library Panel Description', folderUid: '', - model: { type: 'timeseries', title: 'A title' }, + model: { type: 'timeseries', title: 'A title' } as Panel, type: 'timeseries', - orgId: 1, version: 1, meta: { folderName: 'General', diff --git a/public/app/features/library-panels/components/LibraryPanelsView/reducer.test.ts b/public/app/features/library-panels/components/LibraryPanelsView/reducer.test.ts index 527d345534b..ec96cc842d2 100644 --- a/public/app/features/library-panels/components/LibraryPanelsView/reducer.test.ts +++ b/public/app/features/library-panels/components/LibraryPanelsView/reducer.test.ts @@ -1,7 +1,8 @@ import { LoadingState } from '@grafana/data'; +import { Panel } from '@grafana/schema'; import { reducerTester } from '../../../../../test/core/redux/reducerTester'; -import { LibraryElementDTO, LibraryElementKind } from '../../types'; +import { LibraryElementDTO } from '../../types'; import { changePage, @@ -93,7 +94,6 @@ function getLibraryPanelMocks(count: number): LibraryElementDTO[] { mocks.push( mockLibraryPanel({ uid: i.toString(10), - id: i, name: `Test Panel ${i}`, }) ); @@ -104,11 +104,9 @@ function getLibraryPanelMocks(count: number): LibraryElementDTO[] { function mockLibraryPanel({ uid = '1', - id = 1, - orgId = 1, folderUid = '', name = 'Test Panel', - model = { type: 'text', title: 'Test Panel' }, + model = { type: 'text', title: 'Test Panel' } as Panel, meta = { folderName: 'General', folderUid: '', @@ -124,11 +122,8 @@ function mockLibraryPanel({ }: Partial = {}): LibraryElementDTO { return { uid, - id, - orgId, folderUid, name, - kind: LibraryElementKind.Panel, model, version, meta, diff --git a/public/app/features/library-panels/components/SaveLibraryPanelModal/SaveLibraryPanelModal.tsx b/public/app/features/library-panels/components/SaveLibraryPanelModal/SaveLibraryPanelModal.tsx index e3b551d302a..6956b60a2d9 100644 --- a/public/app/features/library-panels/components/SaveLibraryPanelModal/SaveLibraryPanelModal.tsx +++ b/public/app/features/library-panels/components/SaveLibraryPanelModal/SaveLibraryPanelModal.tsx @@ -64,8 +64,8 @@ export const SaveLibraryPanelModal = ({

{'This update will affect '} - {panel.libraryPanel.meta.connectedDashboards}{' '} - {panel.libraryPanel.meta.connectedDashboards === 1 ? 'dashboard' : 'dashboards'}. + {panel.libraryPanel.meta?.connectedDashboards}{' '} + {panel.libraryPanel.meta?.connectedDashboards === 1 ? 'dashboard' : 'dashboards'}. The following dashboards using the panel will be affected:

diff --git a/public/app/features/library-panels/types.ts b/public/app/features/library-panels/types.ts index d4d1cd4c085..0d4356ff7ba 100644 --- a/public/app/features/library-panels/types.ts +++ b/public/app/features/library-panels/types.ts @@ -1,17 +1,22 @@ import { AnyAction } from '@reduxjs/toolkit'; import { Dispatch } from 'react'; +import { LibraryPanel } from '@grafana/schema'; +import { LibraryElementDTOMetaUser } from '@grafana/schema/src/raw/librarypanel/x/librarypanel_types.gen'; + import { PanelModel } from '../dashboard/state'; export enum LibraryElementKind { Panel = 1, - Variable, } export enum LibraryElementConnectionKind { Dashboard = 1, } +/** @deprecated use LibraryPanel */ +export interface LibraryElementDTO extends LibraryPanel {} + export interface LibraryElementConnectionDTO { id: number; kind: LibraryElementConnectionKind; @@ -24,48 +29,13 @@ export interface LibraryElementConnectionDTO { export interface LibraryElementsSearchResult { totalCount: number; - elements: LibraryElementDTO[]; + elements: LibraryPanel[]; perPage: number; page: number; } -export interface LibraryElementDTO { - id: number; - orgId: number; - folderUid: string; - uid: string; - name: string; - kind: LibraryElementKind; - type: string; - description: string; - model: any; - version: number; - meta: LibraryElementDTOMeta; -} - -export interface LibraryElementDTOMeta { - folderName: string; - folderUid: string; - connectedDashboards: number; - created: string; - updated: string; - createdBy: LibraryElementDTOMetaUser; - updatedBy: LibraryElementDTOMetaUser; -} - -export interface LibraryElementDTOMetaUser { - id: number; - name: string; - avatarUrl: string; -} - -export interface LibraryPanelRef { - name: string; - uid: string; -} - export interface PanelModelWithLibraryPanel extends PanelModel { - libraryPanel: LibraryElementDTO; + libraryPanel: LibraryPanel; } export type DispatchResult = (dispatch: Dispatch) => void; diff --git a/public/app/features/manage-dashboards/components/ImportDashboardLibraryPanelsList.tsx b/public/app/features/manage-dashboards/components/ImportDashboardLibraryPanelsList.tsx index cb3b47b7b71..3228e8107ff 100644 --- a/public/app/features/manage-dashboards/components/ImportDashboardLibraryPanelsList.tsx +++ b/public/app/features/manage-dashboards/components/ImportDashboardLibraryPanelsList.tsx @@ -2,6 +2,7 @@ import { css } from '@emotion/css'; import React, { ReactElement } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; +import { LibraryPanel } from '@grafana/schema'; import { Field, useStyles2 } from '@grafana/ui'; import { LibraryPanelCard } from '../../library-panels/components/LibraryPanelCard/LibraryPanelCard'; @@ -36,9 +37,10 @@ export function ImportDashboardLibraryPanelsList({ input.state === LibraryPanelInputState.New ? { ...input.model, meta: { ...input.model.meta, folderName: folderName ?? 'General' } } : { ...input.model }; + return (
- undefined} /> + undefined} />
); })} diff --git a/public/app/features/manage-dashboards/state/actions.ts b/public/app/features/manage-dashboards/state/actions.ts index 0969bad4959..1579cd6306b 100644 --- a/public/app/features/manage-dashboards/state/actions.ts +++ b/public/app/features/manage-dashboards/state/actions.ts @@ -101,8 +101,6 @@ function processElements(dashboardJson?: { __elements?: Record Date: Mon, 30 Jan 2023 08:29:13 +0100 Subject: [PATCH 072/117] Datasources: Extend properties for the datasource-test tracking event (#62292) chore: extend properties for the test datasource tracking event --- public/app/features/datasources/state/actions.test.ts | 8 +++++--- public/app/features/datasources/state/actions.ts | 4 ++++ public/app/features/datasources/state/hooks.ts | 3 ++- public/app/features/datasources/tracking.ts | 2 ++ 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/public/app/features/datasources/state/actions.test.ts b/public/app/features/datasources/state/actions.test.ts index 5d06e3fb211..daf75d1ac69 100644 --- a/public/app/features/datasources/state/actions.test.ts +++ b/public/app/features/datasources/state/actions.test.ts @@ -72,7 +72,7 @@ const failDataSourceTest = async (error: object) => { }; const dispatchedActions = await thunkTester(state) .givenThunk(testDataSource) - .whenThunkIsDispatched('Azure Monitor', dependencies); + .whenThunkIsDispatched('Azure Monitor', DATASOURCES_ROUTES.Edit, dependencies); return dispatchedActions; }; @@ -234,7 +234,7 @@ describe('testDataSource', () => { }; const dispatchedActions = await thunkTester(state) .givenThunk(testDataSource) - .whenThunkIsDispatched('CloudWatch', dependencies); + .whenThunkIsDispatched('CloudWatch', DATASOURCES_ROUTES.Edit, dependencies); expect(dispatchedActions).toEqual([testDataSourceStarting(), testDataSourceSucceeded(state.testingStatus)]); expect(trackDataSourceTested).toHaveBeenCalledWith({ @@ -242,6 +242,7 @@ describe('testDataSource', () => { datasource_uid: 'CW1234', grafana_version: '1.0', success: true, + editLink: '/datasources/edit/CloudWatch', }); }); @@ -270,7 +271,7 @@ describe('testDataSource', () => { }; const dispatchedActions = await thunkTester(state) .givenThunk(testDataSource) - .whenThunkIsDispatched('Azure Monitor', dependencies); + .whenThunkIsDispatched('Azure Monitor', DATASOURCES_ROUTES.Edit, dependencies); expect(dispatchedActions).toEqual([testDataSourceStarting(), testDataSourceFailed(result)]); expect(trackDataSourceTested).toHaveBeenCalledWith({ @@ -278,6 +279,7 @@ describe('testDataSource', () => { datasource_uid: 'azM0nit0R', grafana_version: '1.0', success: false, + editLink: '/datasources/edit/Azure Monitor', }); }); diff --git a/public/app/features/datasources/state/actions.ts b/public/app/features/datasources/state/actions.ts index eb8a772ac36..06067222151 100644 --- a/public/app/features/datasources/state/actions.ts +++ b/public/app/features/datasources/state/actions.ts @@ -90,6 +90,7 @@ export const initDataSourceSettings = ( export const testDataSource = ( dataSourceName: string, + editRoute = DATASOURCES_ROUTES.Edit, dependencies: TestDataSourceDependencies = { getDatasourceSrv, getBackendSrv, @@ -97,6 +98,7 @@ export const testDataSource = ( ): ThunkResult => { return async (dispatch: ThunkDispatch, getState) => { const dsApi = await dependencies.getDatasourceSrv().get(dataSourceName); + const editLink = editRoute.replace(/:uid/gi, dataSourceName); if (!dsApi.testDatasource) { return; @@ -114,6 +116,7 @@ export const testDataSource = ( plugin_id: dsApi.type, datasource_uid: dsApi.uid, success: true, + editLink, }); } catch (err) { let message: string | undefined; @@ -134,6 +137,7 @@ export const testDataSource = ( plugin_id: dsApi.type, datasource_uid: dsApi.uid, success: false, + editLink, }); } }); diff --git a/public/app/features/datasources/state/hooks.ts b/public/app/features/datasources/state/hooks.ts index 70bf7acfaa4..8432bb01bb7 100644 --- a/public/app/features/datasources/state/hooks.ts +++ b/public/app/features/datasources/state/hooks.ts @@ -43,8 +43,9 @@ export const useInitDataSourceSettings = (uid: string) => { export const useTestDataSource = (uid: string) => { const dispatch = useDispatch(); + const dataSourcesRoutes = useDataSourcesRoutes(); - return () => dispatch(testDataSource(uid)); + return () => dispatch(testDataSource(uid, dataSourcesRoutes.Edit)); }; export const useLoadDataSources = () => { diff --git a/public/app/features/datasources/tracking.ts b/public/app/features/datasources/tracking.ts index 5de67f15209..a1622bbff14 100644 --- a/public/app/features/datasources/tracking.ts +++ b/public/app/features/datasources/tracking.ts @@ -53,4 +53,6 @@ type DataSourceTestedProps = { plugin_version?: string; /** Whether or not the datasource test succeeded = the datasource was successfully configured */ success: boolean; + /** The URL that points to the edit page for the datasoruce. We are using this to be able to distinguish between the performance of different datasource edit locations. */ + editLink?: string; }; From 324310abbc8865e3e42f9fa6af61853d82d91d4b Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Mon, 30 Jan 2023 09:18:26 +0100 Subject: [PATCH 073/117] Chore: Fix goimports grouping in pkg/api (#62419) * fix goimports * fix goimports order --- pkg/api/admin_test.go | 2 +- pkg/api/avatar/avatar.go | 3 ++- pkg/api/avatar/avatar_test.go | 3 ++- pkg/api/datasources_test.go | 2 +- pkg/api/dtos/index.go | 4 ++-- pkg/api/dtos/models_test.go | 3 ++- pkg/api/frontend_logging_test.go | 6 +++--- pkg/api/frontendlogging/sentry.go | 1 + pkg/api/frontendlogging/source_maps.go | 2 +- pkg/api/frontendsettings_test.go | 6 +++--- pkg/api/health_test.go | 3 ++- pkg/api/http_server_test.go | 3 ++- pkg/api/org_invite_test.go | 6 +++--- pkg/api/plugin_dashboards_test.go | 3 ++- pkg/api/plugin_metrics.go | 1 + pkg/api/plugin_metrics_test.go | 3 ++- pkg/api/plugin_resource_test.go | 3 +-- pkg/api/plugins_test.go | 4 +--- pkg/api/response/response_test.go | 4 ++-- pkg/api/short_url_test.go | 3 ++- pkg/api/user_token.go | 3 ++- 21 files changed, 38 insertions(+), 30 deletions(-) diff --git a/pkg/api/admin_test.go b/pkg/api/admin_test.go index 8c63fbed185..04c69f4755b 100644 --- a/pkg/api/admin_test.go +++ b/pkg/api/admin_test.go @@ -5,10 +5,10 @@ import ( "net/http" "testing" - "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/stats/statstest" "github.com/grafana/grafana/pkg/setting" diff --git a/pkg/api/avatar/avatar.go b/pkg/api/avatar/avatar.go index bcfe2eb77c6..d331cee8664 100644 --- a/pkg/api/avatar/avatar.go +++ b/pkg/api/avatar/avatar.go @@ -20,11 +20,12 @@ import ( "sync" "time" + gocache "github.com/patrickmn/go-cache" + "github.com/grafana/grafana/pkg/infra/log" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" - gocache "github.com/patrickmn/go-cache" ) const ( diff --git a/pkg/api/avatar/avatar_test.go b/pkg/api/avatar/avatar_test.go index abc98a59b77..916ba082e22 100644 --- a/pkg/api/avatar/avatar_test.go +++ b/pkg/api/avatar/avatar_test.go @@ -7,8 +7,9 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/setting" ) const DEFAULT_NONSENSE_HASH string = "9e107d9d372bb6826bd81d3542a419d6" diff --git a/pkg/api/datasources_test.go b/pkg/api/datasources_test.go index be708e89e6f..8eabf669b34 100644 --- a/pkg/api/datasources_test.go +++ b/pkg/api/datasources_test.go @@ -8,7 +8,6 @@ import ( "strings" "testing" - "github.com/grafana/grafana/pkg/web/webtest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -24,6 +23,7 @@ import ( "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/datasources/permissions" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/web/webtest" ) const ( diff --git a/pkg/api/dtos/index.go b/pkg/api/dtos/index.go index 6d1263d0c1b..049a13f0425 100644 --- a/pkg/api/dtos/index.go +++ b/pkg/api/dtos/index.go @@ -1,10 +1,10 @@ package dtos import ( + "html/template" + "github.com/grafana/grafana/pkg/services/navtree" "github.com/grafana/grafana/pkg/setting" - - "html/template" ) type IndexViewData struct { diff --git a/pkg/api/dtos/models_test.go b/pkg/api/dtos/models_test.go index 931676f5a04..39dd133f2fe 100644 --- a/pkg/api/dtos/models_test.go +++ b/pkg/api/dtos/models_test.go @@ -4,10 +4,11 @@ import ( "sort" "testing" + "github.com/stretchr/testify/assert" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" ) func TestGetUniqueDatasourceTypes(t *testing.T) { diff --git a/pkg/api/frontend_logging_test.go b/pkg/api/frontend_logging_test.go index 563ae9ecd7a..5771bb4465c 100644 --- a/pkg/api/frontend_logging_test.go +++ b/pkg/api/frontend_logging_test.go @@ -12,15 +12,15 @@ import ( "github.com/getsentry/sentry-go" "github.com/go-kit/log" "github.com/go-kit/log/level" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/frontendlogging" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/plugins" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/setting" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) type SourceMapReadRecord struct { diff --git a/pkg/api/frontendlogging/sentry.go b/pkg/api/frontendlogging/sentry.go index 032bb874ef1..66b74c09c04 100644 --- a/pkg/api/frontendlogging/sentry.go +++ b/pkg/api/frontendlogging/sentry.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/getsentry/sentry-go" + "github.com/grafana/grafana/pkg/infra/log" ) diff --git a/pkg/api/frontendlogging/source_maps.go b/pkg/api/frontendlogging/source_maps.go index 09536c24e5b..95a4802a2ab 100644 --- a/pkg/api/frontendlogging/source_maps.go +++ b/pkg/api/frontendlogging/source_maps.go @@ -9,9 +9,9 @@ import ( "strings" "sync" + "github.com/getsentry/sentry-go" sourcemap "github.com/go-sourcemap/sourcemap" - "github.com/getsentry/sentry-go" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/setting" ) diff --git a/pkg/api/frontendsettings_test.go b/pkg/api/frontendsettings_test.go index 5569e30d4e2..07d3a4d1ab5 100644 --- a/pkg/api/frontendsettings_test.go +++ b/pkg/api/frontendsettings_test.go @@ -7,14 +7,14 @@ import ( "path/filepath" "testing" - "github.com/grafana/grafana/pkg/login/social" - "github.com/grafana/grafana/pkg/plugins/config" - "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/licensing" diff --git a/pkg/api/health_test.go b/pkg/api/health_test.go index 699f85607af..96ab7e2612e 100644 --- a/pkg/api/health_test.go +++ b/pkg/api/health_test.go @@ -7,11 +7,12 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" - "github.com/stretchr/testify/require" ) func TestHealthAPI_Version(t *testing.T) { diff --git a/pkg/api/http_server_test.go b/pkg/api/http_server_test.go index c0d45258855..5cb99004bef 100644 --- a/pkg/api/http_server_test.go +++ b/pkg/api/http_server_test.go @@ -3,8 +3,9 @@ package api import ( "testing" - "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" + + "github.com/grafana/grafana/pkg/setting" ) func TestHTTPServer_MetricsBasicAuth(t *testing.T) { diff --git a/pkg/api/org_invite_test.go b/pkg/api/org_invite_test.go index ed20e17894c..8b8829d8512 100644 --- a/pkg/api/org_invite_test.go +++ b/pkg/api/org_invite_test.go @@ -5,15 +5,15 @@ import ( "strings" "testing" - "github.com/grafana/grafana/pkg/services/org/orgtest" - "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/web/webtest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/org/orgtest" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/usertest" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/web/webtest" ) func TestOrgInvitesAPIEndpoint_RBAC(t *testing.T) { diff --git a/pkg/api/plugin_dashboards_test.go b/pkg/api/plugin_dashboards_test.go index 2abafcd5365..ae3709ba39c 100644 --- a/pkg/api/plugin_dashboards_test.go +++ b/pkg/api/plugin_dashboards_test.go @@ -9,13 +9,14 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/plugindashboards" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/web/webtest" - "github.com/stretchr/testify/require" ) func TestGetPluginDashboards(t *testing.T) { diff --git a/pkg/api/plugin_metrics.go b/pkg/api/plugin_metrics.go index 85379d05d52..e32a3ad29ab 100644 --- a/pkg/api/plugin_metrics.go +++ b/pkg/api/plugin_metrics.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/plugins/backendplugin" "github.com/grafana/grafana/pkg/web" ) diff --git a/pkg/api/plugin_metrics_test.go b/pkg/api/plugin_metrics_test.go index 03acced669c..40d4f6f3921 100644 --- a/pkg/api/plugin_metrics_test.go +++ b/pkg/api/plugin_metrics_test.go @@ -7,12 +7,13 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web/webtest" - "github.com/stretchr/testify/require" ) func TestPluginMetricsEndpoint(t *testing.T) { diff --git a/pkg/api/plugin_resource_test.go b/pkg/api/plugin_resource_test.go index a63e9485264..5b5944c8d3f 100644 --- a/pkg/api/plugin_resource_test.go +++ b/pkg/api/plugin_resource_test.go @@ -9,10 +9,9 @@ import ( "strings" "testing" - "github.com/stretchr/testify/require" - "github.com/grafana/grafana-azure-sdk-go/azsettings" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/localcache" diff --git a/pkg/api/plugins_test.go b/pkg/api/plugins_test.go index fef5ceba31b..43765e56f51 100644 --- a/pkg/api/plugins_test.go +++ b/pkg/api/plugins_test.go @@ -12,13 +12,11 @@ import ( "strings" "testing" + "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" - "github.com/stretchr/testify/require" - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log/logtest" diff --git a/pkg/api/response/response_test.go b/pkg/api/response/response_test.go index 67658cb497a..a100c42ae48 100644 --- a/pkg/api/response/response_test.go +++ b/pkg/api/response/response_test.go @@ -5,10 +5,10 @@ import ( "net/http" "testing" - "github.com/grafana/grafana/pkg/util/errutil" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/util/errutil" ) func TestErrors(t *testing.T) { diff --git a/pkg/api/short_url_test.go b/pkg/api/short_url_test.go index 17a4597958f..e84efbc9ce3 100644 --- a/pkg/api/short_url_test.go +++ b/pkg/api/short_url_test.go @@ -6,6 +6,8 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" @@ -14,7 +16,6 @@ import ( "github.com/grafana/grafana/pkg/services/shorturls" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/require" ) func TestShortURLAPIEndpoint(t *testing.T) { diff --git a/pkg/api/user_token.go b/pkg/api/user_token.go index dc13fa1ce77..8a55c1e3385 100644 --- a/pkg/api/user_token.go +++ b/pkg/api/user_token.go @@ -6,6 +6,8 @@ import ( "net/http" "time" + "github.com/ua-parser/uap-go/uaparser" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/services/auth" @@ -13,7 +15,6 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" - "github.com/ua-parser/uap-go/uaparser" ) // swagger:route GET /user/auth-tokens signed_in_user getUserAuthTokens From bc2813ef0661eb0fd317a7ed2dff4db056cbe7e6 Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Mon, 30 Jan 2023 09:21:27 +0100 Subject: [PATCH 074/117] Chore: Fix goimports grouping in pkg/services (#62420) * fix goimports * fix goimports order --- .../annotations/annotationsimpl/xorm_store_test.go | 2 +- pkg/services/cleanup/cleanup_test.go | 3 ++- pkg/services/comments/sql_storage_test.go | 4 ++-- pkg/services/contexthandler/contexthandler_test.go | 2 +- pkg/services/contexthandler/model/model.go | 3 ++- pkg/services/contexthandler/model/model_test.go | 3 ++- pkg/services/correlations/api.go | 1 - pkg/services/correlations/correlations.go | 1 - pkg/services/dashboardimport/api/api_test.go | 3 ++- pkg/services/dashboardimport/service/service_test.go | 3 ++- .../utils/dash_template_evaluator_test.go | 3 ++- pkg/services/dashboards/dashboard_service_mock.go | 3 ++- pkg/services/dashboards/folder_store_mock.go | 3 ++- pkg/services/dashboards/models_test.go | 5 +++-- pkg/services/dashboards/store_mock.go | 3 +-- pkg/services/encryption/provider/cipher_aescfb_test.go | 1 - pkg/services/encryption/provider/decipher_aes_test.go | 3 ++- pkg/services/encryption/service/helpers.go | 3 ++- pkg/services/encryption/service/service_test.go | 5 +++-- pkg/services/folder/folderimpl/folder.go | 7 +++---- pkg/services/folder/folderimpl/sqlstore.go | 1 + pkg/services/grpcserver/health.go | 4 ++-- pkg/services/grpcserver/interceptors/auth.go | 8 ++++---- pkg/services/grpcserver/interceptors/auth_test.go | 5 +++-- pkg/services/grpcserver/interceptors/tracing.go | 3 ++- pkg/services/grpcserver/reflection.go | 4 ++-- pkg/services/grpcserver/service.go | 10 +++++----- pkg/services/navtree/navtreeimpl/applinks_test.go | 3 ++- pkg/services/notifications/codes_test.go | 4 ++-- .../notifications/send_email_integration_test.go | 4 ++-- pkg/services/notifications/smtp.go | 3 ++- pkg/services/notifications/smtp_test.go | 3 ++- pkg/services/org/orgimpl/org_test.go | 3 ++- pkg/services/plugindashboards/service/service_test.go | 3 ++- pkg/services/provisioning/alerting/config_reader.go | 3 ++- .../provisioning/alerting/config_reader_test.go | 3 ++- .../provisioning/alerting/contact_point_types_test.go | 3 ++- pkg/services/provisioning/alerting/file/rules_types.go | 4 ++-- .../alerting/notification_policy_types_test.go | 3 +-- pkg/services/provisioning/dashboards/config_reader.go | 3 ++- .../dashboards/file_reader_symlink_test.go | 3 ++- .../provisioning/plugins/config_reader_test.go | 3 ++- pkg/services/provisioning/provisioning_test.go | 3 ++- pkg/services/provisioning/values/values_test.go | 3 ++- pkg/services/query/models.go | 1 + pkg/services/queryhistory/queryhistory_create_test.go | 3 ++- .../queryhistory/queryhistory_delete_stale_test.go | 3 ++- pkg/services/queryhistory/queryhistory_migrate_test.go | 3 ++- pkg/services/queryhistory/queryhistory_patch_test.go | 3 ++- pkg/services/queryhistory/queryhistory_star_test.go | 3 ++- pkg/services/queryhistory/queryhistory_unstar_test.go | 3 ++- pkg/services/quota/quotaimpl/quota.go | 3 ++- pkg/services/rendering/mock.go | 1 + pkg/services/rendering/rendering_test.go | 5 +++-- pkg/services/search/service.go | 3 +-- pkg/services/secrets/kvstore/plugin_test.go | 3 ++- pkg/services/secrets/manager/helpers.go | 5 +++-- pkg/services/secrets/manager/manager.go | 6 +++--- pkg/services/secrets/manager/metrics.go | 3 ++- pkg/services/sqlstore/logger.go | 4 ++-- .../migrations/accesscontrol/action_migrator.go | 4 ++-- .../accesscontrol/managed_permission_migrator.go | 7 ++++--- .../sqlstore/migrations/accesscontrol/test/ac_test.go | 5 ++--- .../accesscontrol/test/action_migrator_test.go | 4 ++-- .../test/managed_permission_migrator_test.go | 5 +++-- pkg/services/sqlstore/migrations/annotation_mig.go | 3 ++- .../sqlstore/migrations/external_alertmanagers.go | 3 ++- pkg/services/sqlstore/migrations/temp_user.go | 3 ++- pkg/services/sqlstore/migrations/user_mig.go | 3 ++- pkg/services/sqlstore/migrator/postgres_dialect.go | 1 - pkg/services/sqlstore/session.go | 3 +-- pkg/services/sqlstore/sqlstore_test.go | 3 ++- pkg/services/tag/tagimpl/store_test.go | 4 ++-- pkg/services/user/userimpl/user_test.go | 6 +++--- 74 files changed, 147 insertions(+), 109 deletions(-) diff --git a/pkg/services/annotations/annotationsimpl/xorm_store_test.go b/pkg/services/annotations/annotationsimpl/xorm_store_test.go index cb752e988ce..2ae6f10ba82 100644 --- a/pkg/services/annotations/annotationsimpl/xorm_store_test.go +++ b/pkg/services/annotations/annotationsimpl/xorm_store_test.go @@ -7,7 +7,6 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -20,6 +19,7 @@ import ( dashboardstore "github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/quota/quotatest" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" diff --git a/pkg/services/cleanup/cleanup_test.go b/pkg/services/cleanup/cleanup_test.go index 8d92c28d8f4..4db896a207d 100644 --- a/pkg/services/cleanup/cleanup_test.go +++ b/pkg/services/cleanup/cleanup_test.go @@ -4,8 +4,9 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/setting" ) func TestCleanUpTmpFiles(t *testing.T) { diff --git a/pkg/services/comments/sql_storage_test.go b/pkg/services/comments/sql_storage_test.go index 145a2cec5f3..4c8a01a1a24 100644 --- a/pkg/services/comments/sql_storage_test.go +++ b/pkg/services/comments/sql_storage_test.go @@ -5,10 +5,10 @@ import ( "strconv" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/comments/commentmodel" - - "github.com/stretchr/testify/require" ) func createSqlStorage(t *testing.T) Storage { diff --git a/pkg/services/contexthandler/contexthandler_test.go b/pkg/services/contexthandler/contexthandler_test.go index 870222a6884..efdb5ccb275 100644 --- a/pkg/services/contexthandler/contexthandler_test.go +++ b/pkg/services/contexthandler/contexthandler_test.go @@ -7,10 +7,10 @@ import ( "net/http/httptest" "testing" + "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth/authtest" diff --git a/pkg/services/contexthandler/model/model.go b/pkg/services/contexthandler/model/model.go index 0ae7d23c3b9..58c874a2193 100644 --- a/pkg/services/contexthandler/model/model.go +++ b/pkg/services/contexthandler/model/model.go @@ -5,6 +5,8 @@ import ( "net/http" "strings" + "github.com/prometheus/client_golang/prometheus" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/models/usertoken" @@ -13,7 +15,6 @@ import ( "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util/errutil" "github.com/grafana/grafana/pkg/web" - "github.com/prometheus/client_golang/prometheus" ) type ReqContext struct { diff --git a/pkg/services/contexthandler/model/model_test.go b/pkg/services/contexthandler/model/model_test.go index aa492d30999..151236e4247 100644 --- a/pkg/services/contexthandler/model/model_test.go +++ b/pkg/services/contexthandler/model/model_test.go @@ -4,8 +4,9 @@ import ( "net/http" "testing" - "github.com/grafana/grafana/pkg/web" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/web" ) func TestQueryBoolWithDefault(t *testing.T) { diff --git a/pkg/services/correlations/api.go b/pkg/services/correlations/api.go index 773236a6eb6..a654d3c9bc7 100644 --- a/pkg/services/correlations/api.go +++ b/pkg/services/correlations/api.go @@ -10,7 +10,6 @@ import ( ac "github.com/grafana/grafana/pkg/services/accesscontrol" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" - "github.com/grafana/grafana/pkg/web" ) diff --git a/pkg/services/correlations/correlations.go b/pkg/services/correlations/correlations.go index ed6da7b6fe1..2afdde08361 100644 --- a/pkg/services/correlations/correlations.go +++ b/pkg/services/correlations/correlations.go @@ -6,7 +6,6 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/events" - "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" diff --git a/pkg/services/dashboardimport/api/api_test.go b/pkg/services/dashboardimport/api/api_test.go index 6d5a07b8862..4752f4c27ff 100644 --- a/pkg/services/dashboardimport/api/api_test.go +++ b/pkg/services/dashboardimport/api/api_test.go @@ -7,6 +7,8 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/components/simplejson" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" @@ -15,7 +17,6 @@ import ( "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/web/webtest" - "github.com/stretchr/testify/require" ) func TestImportDashboardAPI(t *testing.T) { diff --git a/pkg/services/dashboardimport/service/service_test.go b/pkg/services/dashboardimport/service/service_test.go index f3086f2a863..c0c14d601e2 100644 --- a/pkg/services/dashboardimport/service/service_test.go +++ b/pkg/services/dashboardimport/service/service_test.go @@ -6,6 +6,8 @@ import ( "path/filepath" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/dashboardimport" "github.com/grafana/grafana/pkg/services/dashboards" @@ -15,7 +17,6 @@ import ( "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/plugindashboards" "github.com/grafana/grafana/pkg/services/user" - "github.com/stretchr/testify/require" ) func TestImportDashboardService(t *testing.T) { diff --git a/pkg/services/dashboardimport/utils/dash_template_evaluator_test.go b/pkg/services/dashboardimport/utils/dash_template_evaluator_test.go index 9d6053228a4..0fe2a3d09a5 100644 --- a/pkg/services/dashboardimport/utils/dash_template_evaluator_test.go +++ b/pkg/services/dashboardimport/utils/dash_template_evaluator_test.go @@ -3,9 +3,10 @@ package utils import ( "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/dashboardimport" - "github.com/stretchr/testify/require" ) func TestDashTemplateEvaluator(t *testing.T) { diff --git a/pkg/services/dashboards/dashboard_service_mock.go b/pkg/services/dashboards/dashboard_service_mock.go index 3da367b22be..a59d811dbbe 100644 --- a/pkg/services/dashboards/dashboard_service_mock.go +++ b/pkg/services/dashboards/dashboard_service_mock.go @@ -5,8 +5,9 @@ package dashboards import ( context "context" - folder "github.com/grafana/grafana/pkg/services/folder" mock "github.com/stretchr/testify/mock" + + folder "github.com/grafana/grafana/pkg/services/folder" ) // FakeDashboardService is an autogenerated mock type for the DashboardService type diff --git a/pkg/services/dashboards/folder_store_mock.go b/pkg/services/dashboards/folder_store_mock.go index df548e85efb..47fdf2baa87 100644 --- a/pkg/services/dashboards/folder_store_mock.go +++ b/pkg/services/dashboards/folder_store_mock.go @@ -5,8 +5,9 @@ package dashboards import ( context "context" - folder "github.com/grafana/grafana/pkg/services/folder" mock "github.com/stretchr/testify/mock" + + folder "github.com/grafana/grafana/pkg/services/folder" ) // FakeFolderStore is an autogenerated mock type for the FolderStore type diff --git a/pkg/services/dashboards/models_test.go b/pkg/services/dashboards/models_test.go index 9a6299997a4..b7ba915324b 100644 --- a/pkg/services/dashboards/models_test.go +++ b/pkg/services/dashboards/models_test.go @@ -3,11 +3,12 @@ package dashboards import ( "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestGetDashboardUrl(t *testing.T) { diff --git a/pkg/services/dashboards/store_mock.go b/pkg/services/dashboards/store_mock.go index f32268a86ce..4b2b5a9bded 100644 --- a/pkg/services/dashboards/store_mock.go +++ b/pkg/services/dashboards/store_mock.go @@ -5,11 +5,10 @@ package dashboards import ( context "context" - folder "github.com/grafana/grafana/pkg/services/folder" mock "github.com/stretchr/testify/mock" models "github.com/grafana/grafana/pkg/services/alerting/models" - + folder "github.com/grafana/grafana/pkg/services/folder" quota "github.com/grafana/grafana/pkg/services/quota" ) diff --git a/pkg/services/encryption/provider/cipher_aescfb_test.go b/pkg/services/encryption/provider/cipher_aescfb_test.go index d717381590f..ff067781ca8 100644 --- a/pkg/services/encryption/provider/cipher_aescfb_test.go +++ b/pkg/services/encryption/provider/cipher_aescfb_test.go @@ -5,7 +5,6 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) diff --git a/pkg/services/encryption/provider/decipher_aes_test.go b/pkg/services/encryption/provider/decipher_aes_test.go index b31958e8886..5dbe9142c66 100644 --- a/pkg/services/encryption/provider/decipher_aes_test.go +++ b/pkg/services/encryption/provider/decipher_aes_test.go @@ -4,9 +4,10 @@ import ( "context" "testing" - "github.com/grafana/grafana/pkg/services/encryption" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/encryption" ) func Test_aesDecipher(t *testing.T) { diff --git a/pkg/services/encryption/service/helpers.go b/pkg/services/encryption/service/helpers.go index d32c1600d94..bbd1b5850f1 100644 --- a/pkg/services/encryption/service/helpers.go +++ b/pkg/services/encryption/service/helpers.go @@ -3,10 +3,11 @@ package service import ( "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/usagestats" encryptionprovider "github.com/grafana/grafana/pkg/services/encryption/provider" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/require" ) func SetupTestService(tb testing.TB) *Service { diff --git a/pkg/services/encryption/service/service_test.go b/pkg/services/encryption/service/service_test.go index e4850724699..6e0a58d40d8 100644 --- a/pkg/services/encryption/service/service_test.go +++ b/pkg/services/encryption/service/service_test.go @@ -4,12 +4,13 @@ import ( "context" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/services/encryption" "github.com/grafana/grafana/pkg/services/encryption/provider" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func Test_Service(t *testing.T) { diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index f7dfb2e3266..0ac9918b47b 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -15,14 +15,13 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" - "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/services/sqlstore/migrator" - "github.com/grafana/grafana/pkg/util" - "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" ) type Service struct { diff --git a/pkg/services/folder/folderimpl/sqlstore.go b/pkg/services/folder/folderimpl/sqlstore.go index 40797519ee9..4e7767d745d 100644 --- a/pkg/services/folder/folderimpl/sqlstore.go +++ b/pkg/services/folder/folderimpl/sqlstore.go @@ -8,6 +8,7 @@ import ( "github.com/VividCortex/mysqlerr" "github.com/go-sql-driver/mysql" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/slugify" diff --git a/pkg/services/grpcserver/health.go b/pkg/services/grpcserver/health.go index c00b3e3cd48..b7ef28f10ae 100644 --- a/pkg/services/grpcserver/health.go +++ b/pkg/services/grpcserver/health.go @@ -3,10 +3,10 @@ package grpcserver import ( "context" - "github.com/grafana/grafana/pkg/setting" - "google.golang.org/grpc/health" "google.golang.org/grpc/health/grpc_health_v1" + + "github.com/grafana/grafana/pkg/setting" ) // HealthService implements GRPC Health Checking Protocol: diff --git a/pkg/services/grpcserver/interceptors/auth.go b/pkg/services/grpcserver/interceptors/auth.go index fc25c51b019..f5f7d25e87a 100644 --- a/pkg/services/grpcserver/interceptors/auth.go +++ b/pkg/services/grpcserver/interceptors/auth.go @@ -4,6 +4,10 @@ import ( "context" "strings" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + apikeygenprefix "github.com/grafana/grafana/pkg/components/apikeygenprefixed" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -11,10 +15,6 @@ import ( grpccontext "github.com/grafana/grafana/pkg/services/grpcserver/context" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" - - "google.golang.org/grpc/codes" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" ) type Authenticator interface { diff --git a/pkg/services/grpcserver/interceptors/auth_test.go b/pkg/services/grpcserver/interceptors/auth_test.go index bc1dacd5541..785e3e4ec86 100644 --- a/pkg/services/grpcserver/interceptors/auth_test.go +++ b/pkg/services/grpcserver/interceptors/auth_test.go @@ -4,6 +4,9 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/metadata" + apikeygenprefix "github.com/grafana/grafana/pkg/components/apikeygenprefixed" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -12,8 +15,6 @@ import ( grpccontext "github.com/grafana/grafana/pkg/services/grpcserver/context" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" - "github.com/stretchr/testify/require" - "google.golang.org/grpc/metadata" ) func TestAuthenticator_Authenticate(t *testing.T) { diff --git a/pkg/services/grpcserver/interceptors/tracing.go b/pkg/services/grpcserver/interceptors/tracing.go index ccf805ddad7..9f9fa15f4b5 100644 --- a/pkg/services/grpcserver/interceptors/tracing.go +++ b/pkg/services/grpcserver/interceptors/tracing.go @@ -3,8 +3,9 @@ package interceptors import ( "context" - "github.com/grafana/grafana/pkg/infra/tracing" "google.golang.org/grpc" + + "github.com/grafana/grafana/pkg/infra/tracing" ) const tracingPrefix = "gRPC Server " diff --git a/pkg/services/grpcserver/reflection.go b/pkg/services/grpcserver/reflection.go index 3a4ecd09fe4..2a2751ddeed 100644 --- a/pkg/services/grpcserver/reflection.go +++ b/pkg/services/grpcserver/reflection.go @@ -3,10 +3,10 @@ package grpcserver import ( "context" - "github.com/grafana/grafana/pkg/setting" - "google.golang.org/grpc/reflection" "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" + + "github.com/grafana/grafana/pkg/setting" ) // ReflectionService implements the gRPC Server Reflection Protocol: diff --git a/pkg/services/grpcserver/service.go b/pkg/services/grpcserver/service.go index c7349a48954..bf267fb420e 100644 --- a/pkg/services/grpcserver/service.go +++ b/pkg/services/grpcserver/service.go @@ -6,17 +6,17 @@ import ( "net" "github.com/grafana/grafana-plugin-sdk-go/backend" + grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" + grpcAuth "github.com/grpc-ecosystem/go-grpc-middleware/auth" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/grpcserver/interceptors" "github.com/grafana/grafana/pkg/setting" - grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" - grpcAuth "github.com/grpc-ecosystem/go-grpc-middleware/auth" - - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" ) type Provider interface { diff --git a/pkg/services/navtree/navtreeimpl/applinks_test.go b/pkg/services/navtree/navtreeimpl/applinks_test.go index 2864a5fa9c8..32727630992 100644 --- a/pkg/services/navtree/navtreeimpl/applinks_test.go +++ b/pkg/services/navtree/navtreeimpl/applinks_test.go @@ -4,6 +4,8 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/plugins" @@ -18,7 +20,6 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" - "github.com/stretchr/testify/require" ) func TestAddAppLinks(t *testing.T) { diff --git a/pkg/services/notifications/codes_test.go b/pkg/services/notifications/codes_test.go index 515a31b734d..fbcfce51e19 100644 --- a/pkg/services/notifications/codes_test.go +++ b/pkg/services/notifications/codes_test.go @@ -6,10 +6,10 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - - "github.com/stretchr/testify/require" ) func TestTimeLimitCodes(t *testing.T) { diff --git a/pkg/services/notifications/send_email_integration_test.go b/pkg/services/notifications/send_email_integration_test.go index 99f0cee48ab..482848f03b5 100644 --- a/pkg/services/notifications/send_email_integration_test.go +++ b/pkg/services/notifications/send_email_integration_test.go @@ -5,9 +5,9 @@ import ( "os" "testing" - "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/setting" ) func TestEmailIntegrationTest(t *testing.T) { diff --git a/pkg/services/notifications/smtp.go b/pkg/services/notifications/smtp.go index 175f4e8dc13..c012a11d5c0 100644 --- a/pkg/services/notifications/smtp.go +++ b/pkg/services/notifications/smtp.go @@ -8,8 +8,9 @@ import ( "strconv" "strings" - "github.com/grafana/grafana/pkg/setting" gomail "gopkg.in/mail.v2" + + "github.com/grafana/grafana/pkg/setting" ) type SmtpClient struct { diff --git a/pkg/services/notifications/smtp_test.go b/pkg/services/notifications/smtp_test.go index 8ca5a11ed5d..fe2edd6cc6b 100644 --- a/pkg/services/notifications/smtp_test.go +++ b/pkg/services/notifications/smtp_test.go @@ -5,9 +5,10 @@ import ( "strings" "testing" - "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/setting" ) func TestBuildMail(t *testing.T) { diff --git a/pkg/services/org/orgimpl/org_test.go b/pkg/services/org/orgimpl/org_test.go index 206e6d2c016..952a96fab88 100644 --- a/pkg/services/org/orgimpl/org_test.go +++ b/pkg/services/org/orgimpl/org_test.go @@ -4,10 +4,11 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/require" ) func TestOrgService(t *testing.T) { diff --git a/pkg/services/plugindashboards/service/service_test.go b/pkg/services/plugindashboards/service/service_test.go index 2349602ebdc..6cfed39d71f 100644 --- a/pkg/services/plugindashboards/service/service_test.go +++ b/pkg/services/plugindashboards/service/service_test.go @@ -8,12 +8,13 @@ import ( "sort" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/manager/dashboards" dashmodels "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/plugindashboards" - "github.com/stretchr/testify/require" ) func TestGetPluginDashboards(t *testing.T) { diff --git a/pkg/services/provisioning/alerting/config_reader.go b/pkg/services/provisioning/alerting/config_reader.go index 489e5b5776b..b6ca9d2c3dd 100644 --- a/pkg/services/provisioning/alerting/config_reader.go +++ b/pkg/services/provisioning/alerting/config_reader.go @@ -8,8 +8,9 @@ import ( "path/filepath" "strings" - "github.com/grafana/grafana/pkg/infra/log" "gopkg.in/yaml.v3" + + "github.com/grafana/grafana/pkg/infra/log" ) type rulesConfigReader struct { diff --git a/pkg/services/provisioning/alerting/config_reader_test.go b/pkg/services/provisioning/alerting/config_reader_test.go index f17cc3a8b61..edcf431e690 100644 --- a/pkg/services/provisioning/alerting/config_reader_test.go +++ b/pkg/services/provisioning/alerting/config_reader_test.go @@ -4,8 +4,9 @@ import ( "context" "testing" - "github.com/grafana/grafana/pkg/infra/log" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/log" ) const ( diff --git a/pkg/services/provisioning/alerting/contact_point_types_test.go b/pkg/services/provisioning/alerting/contact_point_types_test.go index 34141f0949e..57ef9acfca0 100644 --- a/pkg/services/provisioning/alerting/contact_point_types_test.go +++ b/pkg/services/provisioning/alerting/contact_point_types_test.go @@ -3,9 +3,10 @@ package alerting import ( "testing" - "github.com/grafana/grafana/pkg/services/provisioning/values" "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" + + "github.com/grafana/grafana/pkg/services/provisioning/values" ) func TestReceivers(t *testing.T) { diff --git a/pkg/services/provisioning/alerting/file/rules_types.go b/pkg/services/provisioning/alerting/file/rules_types.go index 3c726dffc41..8c31876a805 100644 --- a/pkg/services/provisioning/alerting/file/rules_types.go +++ b/pkg/services/provisioning/alerting/file/rules_types.go @@ -7,10 +7,10 @@ import ( "strings" "time" + "github.com/prometheus/common/model" + "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/provisioning/values" - - "github.com/prometheus/common/model" ) type RuleDelete struct { diff --git a/pkg/services/provisioning/alerting/notification_policy_types_test.go b/pkg/services/provisioning/alerting/notification_policy_types_test.go index d281427e5a6..15080a154f3 100644 --- a/pkg/services/provisioning/alerting/notification_policy_types_test.go +++ b/pkg/services/provisioning/alerting/notification_policy_types_test.go @@ -4,9 +4,8 @@ import ( "os" "testing" - "gopkg.in/yaml.v3" - "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" ) func TestNotificationPolicy(t *testing.T) { diff --git a/pkg/services/provisioning/dashboards/config_reader.go b/pkg/services/provisioning/dashboards/config_reader.go index a8aa1a06b3b..8d93c4de46b 100644 --- a/pkg/services/provisioning/dashboards/config_reader.go +++ b/pkg/services/provisioning/dashboards/config_reader.go @@ -8,10 +8,11 @@ import ( "path/filepath" "strings" + "gopkg.in/yaml.v3" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/provisioning/utils" - "gopkg.in/yaml.v3" ) type configReader struct { diff --git a/pkg/services/provisioning/dashboards/file_reader_symlink_test.go b/pkg/services/provisioning/dashboards/file_reader_symlink_test.go index 91b356e251a..b72425ea1c5 100644 --- a/pkg/services/provisioning/dashboards/file_reader_symlink_test.go +++ b/pkg/services/provisioning/dashboards/file_reader_symlink_test.go @@ -7,9 +7,10 @@ import ( "path/filepath" "testing" - "github.com/grafana/grafana/pkg/infra/log" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/log" ) var ( diff --git a/pkg/services/provisioning/plugins/config_reader_test.go b/pkg/services/provisioning/plugins/config_reader_test.go index 497cca2c11c..3440c5848d7 100644 --- a/pkg/services/provisioning/plugins/config_reader_test.go +++ b/pkg/services/provisioning/plugins/config_reader_test.go @@ -5,9 +5,10 @@ import ( "os" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" - "github.com/stretchr/testify/require" ) const ( diff --git a/pkg/services/provisioning/provisioning_test.go b/pkg/services/provisioning/provisioning_test.go index 4b18fab3310..e7626af9d37 100644 --- a/pkg/services/provisioning/provisioning_test.go +++ b/pkg/services/provisioning/provisioning_test.go @@ -6,12 +6,13 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + dashboardstore "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/provisioning/dashboards" "github.com/grafana/grafana/pkg/services/provisioning/utils" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" ) func TestProvisioningServiceImpl(t *testing.T) { diff --git a/pkg/services/provisioning/values/values_test.go b/pkg/services/provisioning/values/values_test.go index 15954efd401..7ee470ec8ff 100644 --- a/pkg/services/provisioning/values/values_test.go +++ b/pkg/services/provisioning/values/values_test.go @@ -6,11 +6,12 @@ import ( "os" "testing" - "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/ini.v1" "gopkg.in/yaml.v3" + + "github.com/grafana/grafana/pkg/setting" ) func TestValues(t *testing.T) { diff --git a/pkg/services/query/models.go b/pkg/services/query/models.go index 7e209923575..bb8bf8db6f0 100644 --- a/pkg/services/query/models.go +++ b/pkg/services/query/models.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/datasources" diff --git a/pkg/services/queryhistory/queryhistory_create_test.go b/pkg/services/queryhistory/queryhistory_create_test.go index a04d967a64f..3e14f106890 100644 --- a/pkg/services/queryhistory/queryhistory_create_test.go +++ b/pkg/services/queryhistory/queryhistory_create_test.go @@ -3,8 +3,9 @@ package queryhistory import ( "testing" - "github.com/grafana/grafana/pkg/components/simplejson" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/components/simplejson" ) func TestIntegrationCreateQueryInQueryHistory(t *testing.T) { diff --git a/pkg/services/queryhistory/queryhistory_delete_stale_test.go b/pkg/services/queryhistory/queryhistory_delete_stale_test.go index 4d1a5e1a271..84ed7839572 100644 --- a/pkg/services/queryhistory/queryhistory_delete_stale_test.go +++ b/pkg/services/queryhistory/queryhistory_delete_stale_test.go @@ -5,8 +5,9 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/web" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/web" ) func TestIntegrationDeleteStaleQueryFromQueryHistory(t *testing.T) { diff --git a/pkg/services/queryhistory/queryhistory_migrate_test.go b/pkg/services/queryhistory/queryhistory_migrate_test.go index dc4156dbc37..a8f5667eb25 100644 --- a/pkg/services/queryhistory/queryhistory_migrate_test.go +++ b/pkg/services/queryhistory/queryhistory_migrate_test.go @@ -5,8 +5,9 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/components/simplejson" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/components/simplejson" ) func TestIntegrationMigrateQueriesToQueryHistory(t *testing.T) { diff --git a/pkg/services/queryhistory/queryhistory_patch_test.go b/pkg/services/queryhistory/queryhistory_patch_test.go index 14e0dabe37f..b9280367b1e 100644 --- a/pkg/services/queryhistory/queryhistory_patch_test.go +++ b/pkg/services/queryhistory/queryhistory_patch_test.go @@ -3,8 +3,9 @@ package queryhistory import ( "testing" - "github.com/grafana/grafana/pkg/web" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/web" ) func TestIntegrationPatchQueryCommentInQueryHistory(t *testing.T) { diff --git a/pkg/services/queryhistory/queryhistory_star_test.go b/pkg/services/queryhistory/queryhistory_star_test.go index a5fab3430c9..408435ef065 100644 --- a/pkg/services/queryhistory/queryhistory_star_test.go +++ b/pkg/services/queryhistory/queryhistory_star_test.go @@ -3,8 +3,9 @@ package queryhistory import ( "testing" - "github.com/grafana/grafana/pkg/web" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/web" ) func TestIntegrationStarQueryInQueryHistory(t *testing.T) { diff --git a/pkg/services/queryhistory/queryhistory_unstar_test.go b/pkg/services/queryhistory/queryhistory_unstar_test.go index eff47541fdc..0953a8f01d6 100644 --- a/pkg/services/queryhistory/queryhistory_unstar_test.go +++ b/pkg/services/queryhistory/queryhistory_unstar_test.go @@ -3,8 +3,9 @@ package queryhistory import ( "testing" - "github.com/grafana/grafana/pkg/web" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/web" ) func TestIntegrationUnstarQueryInQueryHistory(t *testing.T) { diff --git a/pkg/services/quota/quotaimpl/quota.go b/pkg/services/quota/quotaimpl/quota.go index 1eebaad635a..b4213fd10a5 100644 --- a/pkg/services/quota/quotaimpl/quota.go +++ b/pkg/services/quota/quotaimpl/quota.go @@ -4,12 +4,13 @@ import ( "context" "sync" + "golang.org/x/sync/errgroup" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/setting" - "golang.org/x/sync/errgroup" ) type serviceDisabled struct { diff --git a/pkg/services/rendering/mock.go b/pkg/services/rendering/mock.go index 6d5d0f33f92..6098d59e204 100644 --- a/pkg/services/rendering/mock.go +++ b/pkg/services/rendering/mock.go @@ -9,6 +9,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" + models "github.com/grafana/grafana/pkg/models" ) diff --git a/pkg/services/rendering/rendering_test.go b/pkg/services/rendering/rendering_test.go index 6bb798638f1..0cb8edd2a8e 100644 --- a/pkg/services/rendering/rendering_test.go +++ b/pkg/services/rendering/rendering_test.go @@ -9,12 +9,13 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestGetUrl(t *testing.T) { diff --git a/pkg/services/search/service.go b/pkg/services/search/service.go index 8f45effe504..365516c33fe 100644 --- a/pkg/services/search/service.go +++ b/pkg/services/search/service.go @@ -5,12 +5,11 @@ import ( "sort" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/star" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - - "github.com/grafana/grafana/pkg/models" ) func ProvideService(cfg *setting.Cfg, sqlstore db.DB, starService star.Service, dashboardService dashboards.DashboardService) *SearchService { diff --git a/pkg/services/secrets/kvstore/plugin_test.go b/pkg/services/secrets/kvstore/plugin_test.go index 19af6217f29..460adea650d 100644 --- a/pkg/services/secrets/kvstore/plugin_test.go +++ b/pkg/services/secrets/kvstore/plugin_test.go @@ -4,9 +4,10 @@ import ( "context" "testing" - "github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin" ) // Set fatal flag to true, then simulate a plugin start failure diff --git a/pkg/services/secrets/manager/helpers.go b/pkg/services/secrets/manager/helpers.go index 967c909be33..e063c38046d 100644 --- a/pkg/services/secrets/manager/helpers.go +++ b/pkg/services/secrets/manager/helpers.go @@ -3,6 +3,9 @@ package manager import ( "testing" + "github.com/stretchr/testify/require" + "gopkg.in/ini.v1" + "github.com/grafana/grafana/pkg/infra/usagestats" encryptionprovider "github.com/grafana/grafana/pkg/services/encryption/provider" encryptionservice "github.com/grafana/grafana/pkg/services/encryption/service" @@ -10,8 +13,6 @@ import ( "github.com/grafana/grafana/pkg/services/kmsproviders/osskmsproviders" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/require" - "gopkg.in/ini.v1" ) func SetupTestService(tb testing.TB, store secrets.Store) *SecretsService { diff --git a/pkg/services/secrets/manager/manager.go b/pkg/services/secrets/manager/manager.go index c9c5c2b9967..6e0180a2454 100644 --- a/pkg/services/secrets/manager/manager.go +++ b/pkg/services/secrets/manager/manager.go @@ -11,6 +11,9 @@ import ( "sync" "time" + "github.com/prometheus/client_golang/prometheus" + "golang.org/x/sync/errgroup" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/services/encryption" @@ -19,9 +22,6 @@ import ( "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" - - "github.com/prometheus/client_golang/prometheus" - "golang.org/x/sync/errgroup" ) const ( diff --git a/pkg/services/secrets/manager/metrics.go b/pkg/services/secrets/manager/metrics.go index b435f71036d..669d4c7a1cc 100644 --- a/pkg/services/secrets/manager/metrics.go +++ b/pkg/services/secrets/manager/metrics.go @@ -1,9 +1,10 @@ package manager import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/infra/metrics/metricutil" - "github.com/prometheus/client_golang/prometheus" ) const ( diff --git a/pkg/services/sqlstore/logger.go b/pkg/services/sqlstore/logger.go index 850e10cd214..199a36f1d13 100644 --- a/pkg/services/sqlstore/logger.go +++ b/pkg/services/sqlstore/logger.go @@ -3,9 +3,9 @@ package sqlstore import ( "fmt" - glog "github.com/grafana/grafana/pkg/infra/log" - "xorm.io/core" + + glog "github.com/grafana/grafana/pkg/infra/log" ) type XormLogger struct { diff --git a/pkg/services/sqlstore/migrations/accesscontrol/action_migrator.go b/pkg/services/sqlstore/migrations/accesscontrol/action_migrator.go index 47de5f8fa30..b5c2ffffa3d 100644 --- a/pkg/services/sqlstore/migrations/accesscontrol/action_migrator.go +++ b/pkg/services/sqlstore/migrations/accesscontrol/action_migrator.go @@ -4,10 +4,10 @@ import ( "fmt" "strings" + "xorm.io/xorm" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" - - "xorm.io/xorm" ) const ActionMigrationID = "RBAC action name migrator" diff --git a/pkg/services/sqlstore/migrations/accesscontrol/managed_permission_migrator.go b/pkg/services/sqlstore/migrations/accesscontrol/managed_permission_migrator.go index 2a2a285c2c0..87f742ed38c 100644 --- a/pkg/services/sqlstore/migrations/accesscontrol/managed_permission_migrator.go +++ b/pkg/services/sqlstore/migrations/accesscontrol/managed_permission_migrator.go @@ -12,13 +12,14 @@ import ( "strings" "time" + "golang.org/x/text/cases" + "golang.org/x/text/language" + "xorm.io/xorm" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" - "golang.org/x/text/cases" - "golang.org/x/text/language" - "xorm.io/xorm" ) const ManagedPermissionsMigrationID = "managed permissions migration" diff --git a/pkg/services/sqlstore/migrations/accesscontrol/test/ac_test.go b/pkg/services/sqlstore/migrations/accesscontrol/test/ac_test.go index 0501f62580c..0c5ae95ae3d 100644 --- a/pkg/services/sqlstore/migrations/accesscontrol/test/ac_test.go +++ b/pkg/services/sqlstore/migrations/accesscontrol/test/ac_test.go @@ -6,6 +6,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "xorm.io/xorm" "github.com/grafana/grafana/pkg/infra/log" @@ -19,9 +21,6 @@ import ( "github.com/grafana/grafana/pkg/services/team" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) type rawPermission struct { diff --git a/pkg/services/sqlstore/migrations/accesscontrol/test/action_migrator_test.go b/pkg/services/sqlstore/migrations/accesscontrol/test/action_migrator_test.go index c3d78009a8b..ff0f3174aa7 100644 --- a/pkg/services/sqlstore/migrations/accesscontrol/test/action_migrator_test.go +++ b/pkg/services/sqlstore/migrations/accesscontrol/test/action_migrator_test.go @@ -4,15 +4,15 @@ import ( "fmt" "testing" - "github.com/grafana/grafana/pkg/services/dashboards" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/dashboards" acmig "github.com/grafana/grafana/pkg/services/sqlstore/migrations/accesscontrol" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/require" ) func TestActionMigration(t *testing.T) { diff --git a/pkg/services/sqlstore/migrations/accesscontrol/test/managed_permission_migrator_test.go b/pkg/services/sqlstore/migrations/accesscontrol/test/managed_permission_migrator_test.go index e8dd6d728c6..94ce65f9ee7 100644 --- a/pkg/services/sqlstore/migrations/accesscontrol/test/managed_permission_migrator_test.go +++ b/pkg/services/sqlstore/migrations/accesscontrol/test/managed_permission_migrator_test.go @@ -6,13 +6,14 @@ import ( "strings" "testing" + "github.com/stretchr/testify/require" + "xorm.io/xorm" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" acmig "github.com/grafana/grafana/pkg/services/sqlstore/migrations/accesscontrol" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/require" - "xorm.io/xorm" ) type inheritanceTestCase struct { diff --git a/pkg/services/sqlstore/migrations/annotation_mig.go b/pkg/services/sqlstore/migrations/annotation_mig.go index f66a63f06c0..dd23ffea93d 100644 --- a/pkg/services/sqlstore/migrations/annotation_mig.go +++ b/pkg/services/sqlstore/migrations/annotation_mig.go @@ -1,8 +1,9 @@ package migrations import ( - . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "xorm.io/xorm" + + . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" ) func addAnnotationMig(mg *Migrator) { diff --git a/pkg/services/sqlstore/migrations/external_alertmanagers.go b/pkg/services/sqlstore/migrations/external_alertmanagers.go index 781341f7b3f..3403dc50c3e 100644 --- a/pkg/services/sqlstore/migrations/external_alertmanagers.go +++ b/pkg/services/sqlstore/migrations/external_alertmanagers.go @@ -5,12 +5,13 @@ import ( "net/url" "time" + "xorm.io/xorm" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/sqlstore/migrations/ualert" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/util" - "xorm.io/xorm" ) func AddExternalAlertmanagerToDatasourceMigration(mg *migrator.Migrator) { diff --git a/pkg/services/sqlstore/migrations/temp_user.go b/pkg/services/sqlstore/migrations/temp_user.go index a4fd1072aa7..af60817f035 100644 --- a/pkg/services/sqlstore/migrations/temp_user.go +++ b/pkg/services/sqlstore/migrations/temp_user.go @@ -3,8 +3,9 @@ package migrations import ( "time" - . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "xorm.io/xorm" + + . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" ) func addTempUserMigrations(mg *Migrator) { diff --git a/pkg/services/sqlstore/migrations/user_mig.go b/pkg/services/sqlstore/migrations/user_mig.go index 6c33849621c..6edf5161f47 100644 --- a/pkg/services/sqlstore/migrations/user_mig.go +++ b/pkg/services/sqlstore/migrations/user_mig.go @@ -3,9 +3,10 @@ package migrations import ( "fmt" + "xorm.io/xorm" + . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/util" - "xorm.io/xorm" ) func addUserMigrations(mg *Migrator) { diff --git a/pkg/services/sqlstore/migrator/postgres_dialect.go b/pkg/services/sqlstore/migrator/postgres_dialect.go index b29577e35f6..b996eabac75 100644 --- a/pkg/services/sqlstore/migrator/postgres_dialect.go +++ b/pkg/services/sqlstore/migrator/postgres_dialect.go @@ -9,7 +9,6 @@ import ( "github.com/golang-migrate/migrate/v4/database" "github.com/lib/pq" - "xorm.io/xorm" ) diff --git a/pkg/services/sqlstore/session.go b/pkg/services/sqlstore/session.go index cffd88231a0..062d166963f 100644 --- a/pkg/services/sqlstore/session.go +++ b/pkg/services/sqlstore/session.go @@ -7,11 +7,10 @@ import ( "reflect" "time" + "github.com/mattn/go-sqlite3" "go.opentelemetry.io/otel/attribute" "xorm.io/xorm" - "github.com/mattn/go-sqlite3" - "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" diff --git a/pkg/services/sqlstore/sqlstore_test.go b/pkg/services/sqlstore/sqlstore_test.go index d17ba44c9a0..b85c7dd77b0 100644 --- a/pkg/services/sqlstore/sqlstore_test.go +++ b/pkg/services/sqlstore/sqlstore_test.go @@ -5,8 +5,9 @@ import ( "net/url" "testing" - "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/setting" ) type sqlStoreTest struct { diff --git a/pkg/services/tag/tagimpl/store_test.go b/pkg/services/tag/tagimpl/store_test.go index 0b865587ef5..3d631136d39 100644 --- a/pkg/services/tag/tagimpl/store_test.go +++ b/pkg/services/tag/tagimpl/store_test.go @@ -4,10 +4,10 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/tag" - - "github.com/stretchr/testify/require" ) type getStore func(db.DB) store diff --git a/pkg/services/user/userimpl/user_test.go b/pkg/services/user/userimpl/user_test.go index 53f63e72940..4f3b87d23b4 100644 --- a/pkg/services/user/userimpl/user_test.go +++ b/pkg/services/user/userimpl/user_test.go @@ -5,6 +5,9 @@ import ( "errors" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/services/org" @@ -12,9 +15,6 @@ import ( "github.com/grafana/grafana/pkg/services/team/teamtest" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestUserService(t *testing.T) { From e2d49ea17fdedf1360e23d36592464bf06a9f1ef Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Mon, 30 Jan 2023 09:25:58 +0100 Subject: [PATCH 075/117] Chore: Fix goimports grouping (#62423) * fix goimports * fix goimports order --- pkg/tsdb/grafanads/grafana.go | 5 +++-- pkg/util/proxyutil/proxyutil_test.go | 3 ++- pkg/util/proxyutil/reverse_proxy_test.go | 3 ++- pkg/web/macaron.go | 3 +-- pkg/web/webtest/webtest.go | 1 + pkg/web/webtest/webtest_test.go | 3 ++- 6 files changed, 11 insertions(+), 7 deletions(-) diff --git a/pkg/tsdb/grafanads/grafana.go b/pkg/tsdb/grafanads/grafana.go index 5fe344c1423..f79c1893338 100644 --- a/pkg/tsdb/grafanads/grafana.go +++ b/pkg/tsdb/grafanads/grafana.go @@ -9,14 +9,15 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/searchV2" "github.com/grafana/grafana/pkg/services/store" "github.com/grafana/grafana/pkg/tsdb/testdatasource" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" ) // DatasourceName is the string constant used as the datasource name in requests diff --git a/pkg/util/proxyutil/proxyutil_test.go b/pkg/util/proxyutil/proxyutil_test.go index 95cbf69cf23..24a5d157781 100644 --- a/pkg/util/proxyutil/proxyutil_test.go +++ b/pkg/util/proxyutil/proxyutil_test.go @@ -4,8 +4,9 @@ import ( "net/http" "testing" - "github.com/grafana/grafana/pkg/services/user" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/user" ) func TestPrepareProxyRequest(t *testing.T) { diff --git a/pkg/util/proxyutil/reverse_proxy_test.go b/pkg/util/proxyutil/reverse_proxy_test.go index 6d3376a421f..0bab66b9b75 100644 --- a/pkg/util/proxyutil/reverse_proxy_test.go +++ b/pkg/util/proxyutil/reverse_proxy_test.go @@ -8,9 +8,10 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/contexthandler" - "github.com/stretchr/testify/require" ) func TestReverseProxy(t *testing.T) { diff --git a/pkg/web/macaron.go b/pkg/web/macaron.go index 774f4d2d499..758b42309b5 100644 --- a/pkg/web/macaron.go +++ b/pkg/web/macaron.go @@ -19,11 +19,10 @@ package web import ( - _ "unsafe" - "context" "net/http" "strings" + _ "unsafe" ) const _VERSION = "1.3.4.0805" diff --git a/pkg/web/webtest/webtest.go b/pkg/web/webtest/webtest.go index d29764e2e45..36d477ee673 100644 --- a/pkg/web/webtest/webtest.go +++ b/pkg/web/webtest/webtest.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/google/uuid" + "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" diff --git a/pkg/web/webtest/webtest_test.go b/pkg/web/webtest/webtest_test.go index e250605cada..c1d696abe2a 100644 --- a/pkg/web/webtest/webtest_test.go +++ b/pkg/web/webtest/webtest_test.go @@ -7,11 +7,12 @@ import ( "strings" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/user" - "github.com/stretchr/testify/require" ) func TestServer(t *testing.T) { From 2ff715cb7a340c4bcb20d6ce58c0e9e5a37ef7e5 Mon Sep 17 00:00:00 2001 From: Jan Garaj Date: Mon, 30 Jan 2023 08:26:14 +0000 Subject: [PATCH 076/117] CloudWatch: Add missing AWS/DX metric (#62405) --- pkg/tsdb/cloudwatch/constants/metrics.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tsdb/cloudwatch/constants/metrics.go b/pkg/tsdb/cloudwatch/constants/metrics.go index c2be49a9d67..39e72be22a3 100644 --- a/pkg/tsdb/cloudwatch/constants/metrics.go +++ b/pkg/tsdb/cloudwatch/constants/metrics.go @@ -27,7 +27,7 @@ var NamespaceMetricsMap = map[string][]string{ "AWS/DDoSProtection": {"DDoSDetected", "DDoSAttackBitsPerSecond", "DDoSAttackPacketsPerSecond", "DDoSAttackRequestsPerSecond", "VolumeBitsPerSecond", "VolumePacketsPerSecond"}, "AWS/DMS": {"CDCChangesDiskSource", "CDCChangesDiskTarget", "CDCChangesMemorySource", "CDCChangesMemoryTarget", "CDCIncomingChanges", "CDCLatencySource", "CDCLatencyTarget", "CDCThroughputBandwidthSource", "CDCThroughputBandwidthTarget", "CDCThroughputRowsSource", "CDCThroughputRowsTarget", "CPUUtilization", "FreeStorageSpace", "FreeableMemory", "FullLoadThroughputBandwidthSource", "FullLoadThroughputBandwidthTarget", "FullLoadThroughputRowsSource", "FullLoadThroughputRowsTarget", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "ReadIOPS", "ReadLatency", "ReadThroughput", "SwapUsage", "WriteIOPS", "WriteLatency", "WriteThroughput"}, "AWS/DocDB": {"BackupRetentionPeriodStorageUsed", "BufferCacheHitRatio", "ChangeStreamLogSize", "CPUUtilization", "DatabaseConnections", "DBInstanceReplicaLag", "DBClusterReplicaLagMaximum", "DBClusterReplicaLagMinimum", "DiskQueueDepth", "EngineUptime", "FreeableMemory", "FreeLocalStorage", "NetworkReceiveThroughput", "NetworkThroughput", "NetworkTransmitThroughput", "ReadIOPS", "ReadLatency", "ReadThroughput", "SnapshotStorageUsed", "SwapUsage", "TotalBackupStorageBilled", "VolumeBytesUsed", "VolumeReadIOPs", "VolumeWriteIOPs", "WriteIOPS", "WriteLatency", "WriteThroughput"}, - "AWS/DX": {"ConnectionBpsEgress", "ConnectionBpsIngress", "ConnectionErrorCount", "ConnectionCRCErrorCount", "ConnectionLightLevelRx", "ConnectionLightLevelTx", "ConnectionPpsEgress", "ConnectionPpsIngress", "ConnectionState", "VirtualInterfaceBpsEgress", "VirtualInterfaceBpsIngress", "VirtualInterfacePpsEgress", "VirtualInterfacePpsIngress"}, + "AWS/DX": {"ConnectionEncryptionState", "ConnectionBpsEgress", "ConnectionBpsIngress", "ConnectionErrorCount", "ConnectionCRCErrorCount", "ConnectionLightLevelRx", "ConnectionLightLevelTx", "ConnectionPpsEgress", "ConnectionPpsIngress", "ConnectionState", "VirtualInterfaceBpsEgress", "VirtualInterfaceBpsIngress", "VirtualInterfacePpsEgress", "VirtualInterfacePpsIngress"}, "AWS/DAX": {"CPUUtilization", "NetworkPacketsIn", "NetworkPacketsOut", "GetItemRequestCount", "BatchGetItemRequestCount", "BatchWriteItemRequestCount", "DeleteItemRequestCount", "PutItemRequestCount", "UpdateItemRequestCount", "TransactWriteItemsCount", "TransactGetItemsCount", "ItemCacheHits", "ItemCacheMisses", "QueryCacheHits", "QueryCacheMisses", "ScanCacheHits", "ScanCacheMisses", "TotalRequestCount", "ErrorRequestCount", "FaultRequestCount", "FailedRequestCount", "QueryRequestCount", "ScanRequestCount", "ClientConnections", "EstimatedDbSize", "EvictedSize"}, "AWS/DynamoDB": {"AccountMaxReads", "AccountMaxTableLevelReads", "AccountMaxTableLevelWrites", "AccountMaxWrites", "AccountProvisionedReadCapacityUtilization", "AccountProvisionedWriteCapacityUtilization", "AgeOfOldestUnreplicatedRecord", "ConditionalCheckFailedRequests", "ConsumedChangeDataCaptureUnits", "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", "FailedToReplicateRecordCount", "MaxProvisionedTableReadCapacityUtilization", "MaxProvisionedTableWriteCapacityUtilization", "OnlineIndexConsumedWriteCapacity", "OnlineIndexPercentageProgress", "OnlineIndexThrottleEvents", "PendingReplicationCount", "ProvisionedReadCapacityUnits", "ProvisionedWriteCapacityUnits", "ReadThrottleEvents", "ReplicationLatency", "ReturnedBytes", "ReturnedItemCount", "ReturnedRecordsCount", "SuccessfulRequestLatency", "SystemErrors", "TimeToLiveDeletedItemCount", "ThrottledPutRecordCount", "ThrottledRequests", "TransactionConflict", "UserErrors", "WriteThrottleEvents"}, "AWS/EBS": {"BurstBalance", "VolumeConsumedReadWriteOps", "VolumeIdleTime", "VolumeQueueLength", "VolumeReadBytes", "VolumeReadOps", "VolumeThroughputPercentage", "VolumeTotalReadTime", "VolumeTotalWriteTime", "VolumeWriteBytes", "VolumeWriteOps"}, From da7065d3dc3db4ce08b888f17760025b674dce0d Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Mon, 30 Jan 2023 09:26:27 +0100 Subject: [PATCH 077/117] LokiContext: Fix wrong queries being run when reopened (#62353) * fix wrong query being run when reopened * fix typo Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> * and test for `onClose` * rename functions --------- Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> --- .../datasource/loki/components/LokiContextUi.test.tsx | 11 +++++++++++ .../datasource/loki/components/LokiContextUi.tsx | 10 +++++++++- public/app/plugins/datasource/loki/datasource.ts | 9 ++++++++- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/loki/components/LokiContextUi.test.tsx b/public/app/plugins/datasource/loki/components/LokiContextUi.test.tsx index 1e730abbfcc..587beb46e2a 100644 --- a/public/app/plugins/datasource/loki/components/LokiContextUi.test.tsx +++ b/public/app/plugins/datasource/loki/components/LokiContextUi.test.tsx @@ -55,6 +55,7 @@ describe('LokiContextUi', () => { label3: 'value3', }, } as unknown as LogRowModel, + onClose: jest.fn(), }; return defaults; @@ -113,4 +114,14 @@ describe('LokiContextUi', () => { jest.useRealTimers(); }); + + it('unmounts and calls onClose', async () => { + const props = setupProps(); + const comp = render(); + comp.unmount(); + + await waitFor(() => { + expect(props.onClose).toHaveBeenCalled(); + }); + }); }); diff --git a/public/app/plugins/datasource/loki/components/LokiContextUi.tsx b/public/app/plugins/datasource/loki/components/LokiContextUi.tsx index 3f14392eccc..33396254e24 100644 --- a/public/app/plugins/datasource/loki/components/LokiContextUi.tsx +++ b/public/app/plugins/datasource/loki/components/LokiContextUi.tsx @@ -13,6 +13,7 @@ export interface LokiContextUiProps { languageProvider: LokiLanguageProvider; row: LogRowModel; updateFilter: (value: ContextFilter[]) => void; + onClose: () => void; } function getStyles(theme: GrafanaTheme2) { @@ -43,7 +44,7 @@ const formatOptionLabel = memoizeOne(({ label, description }: SelectableValue([]); @@ -74,6 +75,13 @@ export function LokiContextUi(props: LokiContextUiProps) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [contextFilters, initialized]); + useEffect(() => { + return () => { + clearTimeout(timerHandle.current); + onClose(); + }; + }, [onClose]); + useAsync(async () => { await languageProvider.start(); const allLabels = languageProvider.getLabelKeys(); diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index a96fa406940..b1da90ab4d4 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -758,7 +758,7 @@ export class LokiDatasource }; }; - async prepareContextExpr(row: LogRowModel, origQuery?: DataQuery): Promise { + async prepareContextExprWithoutParsedLabels(row: LogRowModel, origQuery?: DataQuery): Promise { await this.languageProvider.start(); const labels = this.languageProvider.getLabelKeys(); const expr = Object.keys(row.labels) @@ -775,10 +775,17 @@ export class LokiDatasource return `{${expr}}`; } + async prepareContextExpr(row: LogRowModel, origQuery?: DataQuery): Promise { + return await this.prepareContextExprWithoutParsedLabels(row, origQuery); + } + getLogRowContextUi(row: LogRowModel, runContextQuery: () => void): React.ReactNode { return LokiContextUi({ row, languageProvider: this.languageProvider, + onClose: () => { + this.prepareContextExpr = this.prepareContextExprWithoutParsedLabels; + }, updateFilter: (contextFilters: ContextFilter[]) => { this.prepareContextExpr = async (row: LogRowModel, origQuery?: DataQuery) => { await this.languageProvider.start(); From aebcecf5386f24a9caba8345af821915041d3d6f Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Mon, 30 Jan 2023 09:26:42 +0100 Subject: [PATCH 078/117] Chore: Fix goimports grouping in other backend platform packages (#62422) * fix goimports * fix goimports order * fix goimports order * fix goimports order * fix goimports order * fix goimports order --- pkg/bus/bus_test.go | 3 ++- pkg/cmd/grafana-cli/commands/cli.go | 3 ++- pkg/cmd/grafana-cli/commands/commandstest/context.go | 3 ++- pkg/cmd/grafana-cli/commands/ls_command.go | 1 + pkg/cmd/grafana-cli/commands/upgrade_all_command.go | 3 ++- pkg/cmd/grafana-cli/commands/upgrade_all_command_test.go | 3 ++- pkg/cmd/grafana-cli/commands/upgrade_command.go | 1 + pkg/cmd/grafana-cli/utils/command_line.go | 3 ++- pkg/cmd/grafana-server/commands/cli.go | 3 ++- pkg/cmd/grafana/main.go | 3 ++- pkg/middleware/logger_test.go | 3 ++- pkg/middleware/middleware_basic_auth_test.go | 5 +++-- pkg/middleware/middleware_jwt_auth_test.go | 3 +-- pkg/middleware/request_metrics.go | 3 ++- pkg/mocks/mock_gcsifaces/mocks.go | 3 ++- pkg/server/server.go | 7 +++---- pkg/server/server_test.go | 3 ++- pkg/setting/date_formats_test.go | 3 +-- pkg/setting/expanders_test.go | 3 +-- pkg/setting/setting.go | 7 +++---- pkg/setting/setting_azure_test.go | 1 - pkg/setting/setting_feature_toggles.go | 3 ++- pkg/setting/setting_session_test.go | 4 ++-- pkg/setting/setting_test.go | 1 - pkg/setting/setting_unified_alerting.go | 5 ++--- pkg/tests/api/azuremonitor/azuremonitor_test.go | 3 ++- pkg/tests/api/correlations/correlations_create_test.go | 3 ++- pkg/tests/api/correlations/correlations_delete_test.go | 3 ++- pkg/tests/api/correlations/correlations_update_test.go | 3 ++- pkg/tests/api/elasticsearch/elasticsearch_test.go | 3 ++- pkg/tests/api/graphite/graphite_test.go | 3 ++- pkg/tests/api/influxdb/influxdb_test.go | 3 ++- pkg/tests/api/loki/loki_test.go | 3 ++- pkg/tests/api/opentdsb/opentdsb_test.go | 3 ++- pkg/tests/api/plugins/api_plugins_test.go | 6 +++--- pkg/tests/api/plugins/backendplugin/backendplugin_test.go | 5 +++-- pkg/tests/api/prometheus/prometheus_test.go | 3 ++- pkg/tests/web/index_view_test.go | 3 ++- 38 files changed, 72 insertions(+), 52 deletions(-) diff --git a/pkg/bus/bus_test.go b/pkg/bus/bus_test.go index 3e1338e053d..170297797bd 100644 --- a/pkg/bus/bus_test.go +++ b/pkg/bus/bus_test.go @@ -4,8 +4,9 @@ import ( "context" "testing" - "github.com/grafana/grafana/pkg/infra/tracing" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/tracing" ) type testQuery struct { diff --git a/pkg/cmd/grafana-cli/commands/cli.go b/pkg/cmd/grafana-cli/commands/cli.go index eefeb65bc55..4d421fedd6d 100644 --- a/pkg/cmd/grafana-cli/commands/cli.go +++ b/pkg/cmd/grafana-cli/commands/cli.go @@ -4,10 +4,11 @@ import ( "os" "runtime" + "github.com/urfave/cli/v2" + "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" "github.com/grafana/grafana/pkg/cmd/grafana-cli/services" "github.com/grafana/grafana/pkg/cmd/grafana-cli/utils" - "github.com/urfave/cli/v2" ) // RunCLI is the entrypoint for the grafana-cli command. It returns the exit code for the grafana-cli program. diff --git a/pkg/cmd/grafana-cli/commands/commandstest/context.go b/pkg/cmd/grafana-cli/commands/commandstest/context.go index 3da4a8d754c..4ca73bfa873 100644 --- a/pkg/cmd/grafana-cli/commands/commandstest/context.go +++ b/pkg/cmd/grafana-cli/commands/commandstest/context.go @@ -3,8 +3,9 @@ package commandstest import ( "flag" - "github.com/grafana/grafana/pkg/cmd/grafana-cli/utils" "github.com/urfave/cli/v2" + + "github.com/grafana/grafana/pkg/cmd/grafana-cli/utils" ) // NewCliContext creates a new CLI context with a certain set of flags. diff --git a/pkg/cmd/grafana-cli/commands/ls_command.go b/pkg/cmd/grafana-cli/commands/ls_command.go index b23afd48ced..35c7fdd591b 100644 --- a/pkg/cmd/grafana-cli/commands/ls_command.go +++ b/pkg/cmd/grafana-cli/commands/ls_command.go @@ -4,6 +4,7 @@ import ( "errors" "github.com/fatih/color" + "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" "github.com/grafana/grafana/pkg/cmd/grafana-cli/models" "github.com/grafana/grafana/pkg/cmd/grafana-cli/services" diff --git a/pkg/cmd/grafana-cli/commands/upgrade_all_command.go b/pkg/cmd/grafana-cli/commands/upgrade_all_command.go index 9e6e655253e..759b6561bca 100644 --- a/pkg/cmd/grafana-cli/commands/upgrade_all_command.go +++ b/pkg/cmd/grafana-cli/commands/upgrade_all_command.go @@ -3,11 +3,12 @@ package commands import ( "context" + "github.com/hashicorp/go-version" + "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" "github.com/grafana/grafana/pkg/cmd/grafana-cli/models" "github.com/grafana/grafana/pkg/cmd/grafana-cli/services" "github.com/grafana/grafana/pkg/cmd/grafana-cli/utils" - "github.com/hashicorp/go-version" ) func shouldUpgrade(installed string, remote *models.Plugin) bool { diff --git a/pkg/cmd/grafana-cli/commands/upgrade_all_command_test.go b/pkg/cmd/grafana-cli/commands/upgrade_all_command_test.go index 57ea8c15dbc..c69f0a18a08 100644 --- a/pkg/cmd/grafana-cli/commands/upgrade_all_command_test.go +++ b/pkg/cmd/grafana-cli/commands/upgrade_all_command_test.go @@ -4,8 +4,9 @@ import ( "fmt" "testing" - "github.com/grafana/grafana/pkg/cmd/grafana-cli/models" "github.com/stretchr/testify/assert" + + "github.com/grafana/grafana/pkg/cmd/grafana-cli/models" ) func TestVersionComparison(t *testing.T) { diff --git a/pkg/cmd/grafana-cli/commands/upgrade_command.go b/pkg/cmd/grafana-cli/commands/upgrade_command.go index 5e7fffe9037..9e1eb5df6df 100644 --- a/pkg/cmd/grafana-cli/commands/upgrade_command.go +++ b/pkg/cmd/grafana-cli/commands/upgrade_command.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/fatih/color" + "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" "github.com/grafana/grafana/pkg/cmd/grafana-cli/services" "github.com/grafana/grafana/pkg/cmd/grafana-cli/utils" diff --git a/pkg/cmd/grafana-cli/utils/command_line.go b/pkg/cmd/grafana-cli/utils/command_line.go index 31cfc5b2e64..d8faff51969 100644 --- a/pkg/cmd/grafana-cli/utils/command_line.go +++ b/pkg/cmd/grafana-cli/utils/command_line.go @@ -3,8 +3,9 @@ package utils import ( "os" - "github.com/grafana/grafana/pkg/cmd/grafana-cli/models" "github.com/urfave/cli/v2" + + "github.com/grafana/grafana/pkg/cmd/grafana-cli/models" ) type CommandLine interface { diff --git a/pkg/cmd/grafana-server/commands/cli.go b/pkg/cmd/grafana-server/commands/cli.go index 5d8616c0baa..293bdb713c7 100644 --- a/pkg/cmd/grafana-server/commands/cli.go +++ b/pkg/cmd/grafana-server/commands/cli.go @@ -15,6 +15,8 @@ import ( "syscall" "time" + "github.com/urfave/cli/v2" + "github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/extensions" "github.com/grafana/grafana/pkg/infra/log" @@ -24,7 +26,6 @@ import ( _ "github.com/grafana/grafana/pkg/services/alerting/conditions" _ "github.com/grafana/grafana/pkg/services/alerting/notifiers" "github.com/grafana/grafana/pkg/setting" - "github.com/urfave/cli/v2" ) type ServerOptions struct { diff --git a/pkg/cmd/grafana/main.go b/pkg/cmd/grafana/main.go index edc58ed5875..b209b916fe8 100644 --- a/pkg/cmd/grafana/main.go +++ b/pkg/cmd/grafana/main.go @@ -5,9 +5,10 @@ import ( "os" "github.com/fatih/color" + "github.com/urfave/cli/v2" + gcli "github.com/grafana/grafana/pkg/cmd/grafana-cli/commands" gsrv "github.com/grafana/grafana/pkg/cmd/grafana-server/commands" - "github.com/urfave/cli/v2" ) // The following variables cannot be constants, since they can be overridden through the -X link flag diff --git a/pkg/middleware/logger_test.go b/pkg/middleware/logger_test.go index fd17c87787e..a18033960d0 100644 --- a/pkg/middleware/logger_test.go +++ b/pkg/middleware/logger_test.go @@ -3,9 +3,10 @@ package middleware import ( "testing" + "github.com/stretchr/testify/assert" + "github.com/grafana/grafana/pkg/infra/log" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" - "github.com/stretchr/testify/assert" ) func Test_sanitizeURL(t *testing.T) { diff --git a/pkg/middleware/middleware_basic_auth_test.go b/pkg/middleware/middleware_basic_auth_test.go index 215a819f79e..e02e2f8e92f 100644 --- a/pkg/middleware/middleware_basic_auth_test.go +++ b/pkg/middleware/middleware_basic_auth_test.go @@ -4,6 +4,9 @@ import ( "encoding/json" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/contexthandler" @@ -12,8 +15,6 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestMiddlewareBasicAuth(t *testing.T) { diff --git a/pkg/middleware/middleware_jwt_auth_test.go b/pkg/middleware/middleware_jwt_auth_test.go index cc3b48c001a..3af4eaf88b6 100644 --- a/pkg/middleware/middleware_jwt_auth_test.go +++ b/pkg/middleware/middleware_jwt_auth_test.go @@ -9,9 +9,8 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/services/auth/jwt" - "github.com/grafana/grafana/pkg/services/org" - "github.com/grafana/grafana/pkg/services/contexthandler" + "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" ) diff --git a/pkg/middleware/request_metrics.go b/pkg/middleware/request_metrics.go index 9706968fd2e..b37e49ebc36 100644 --- a/pkg/middleware/request_metrics.go +++ b/pkg/middleware/request_metrics.go @@ -6,12 +6,13 @@ import ( "strings" "time" + "github.com/prometheus/client_golang/prometheus" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/web" - "github.com/prometheus/client_golang/prometheus" ) var ( diff --git a/pkg/mocks/mock_gcsifaces/mocks.go b/pkg/mocks/mock_gcsifaces/mocks.go index 68fda5383d1..2a10e18b375 100644 --- a/pkg/mocks/mock_gcsifaces/mocks.go +++ b/pkg/mocks/mock_gcsifaces/mocks.go @@ -10,9 +10,10 @@ import ( storage "cloud.google.com/go/storage" gomock "github.com/golang/mock/gomock" - gcsifaces "github.com/grafana/grafana/pkg/ifaces/gcsifaces" google "golang.org/x/oauth2/google" jwt "golang.org/x/oauth2/jwt" + + gcsifaces "github.com/grafana/grafana/pkg/ifaces/gcsifaces" ) // MockStorageClient is a mock of StorageClient interface diff --git a/pkg/server/server.go b/pkg/server/server.go index c59a37e1c6f..0c06701694c 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -11,18 +11,17 @@ import ( "strconv" "sync" - "github.com/grafana/grafana/pkg/infra/usagestats/statscollector" - "github.com/grafana/grafana/pkg/services/accesscontrol" + "golang.org/x/sync/errgroup" "github.com/grafana/grafana/pkg/api" _ "github.com/grafana/grafana/pkg/extensions" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/metrics" + "github.com/grafana/grafana/pkg/infra/usagestats/statscollector" "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/provisioning" - "github.com/grafana/grafana/pkg/setting" - "golang.org/x/sync/errgroup" ) // Options contains parameters for the New function. diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go index be4aca1585e..6864cbe589c 100644 --- a/pkg/server/server_test.go +++ b/pkg/server/server_test.go @@ -7,11 +7,12 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/server/backgroundsvcs" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/require" ) type testService struct { diff --git a/pkg/setting/date_formats_test.go b/pkg/setting/date_formats_test.go index 36048fd7302..9a4be580d84 100644 --- a/pkg/setting/date_formats_test.go +++ b/pkg/setting/date_formats_test.go @@ -3,10 +3,9 @@ package setting import ( "testing" - "gopkg.in/ini.v1" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "gopkg.in/ini.v1" ) func TestValueAsTimezone(t *testing.T) { diff --git a/pkg/setting/expanders_test.go b/pkg/setting/expanders_test.go index a4687157ddd..fbf2b6bea64 100644 --- a/pkg/setting/expanders_test.go +++ b/pkg/setting/expanders_test.go @@ -7,9 +7,8 @@ import ( "os" "testing" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestExpandVar_EnvSuccessful(t *testing.T) { diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index b2b1feeb842..dbcb416f317 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -21,16 +21,15 @@ import ( "strings" "time" + "github.com/gobwas/glob" "github.com/grafana/grafana-aws-sdk/pkg/awsds" "github.com/grafana/grafana-azure-sdk-go/azsettings" "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" + "github.com/prometheus/common/model" + "gopkg.in/ini.v1" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/util" - - "github.com/gobwas/glob" - "github.com/prometheus/common/model" - "gopkg.in/ini.v1" ) type Scheme string diff --git a/pkg/setting/setting_azure_test.go b/pkg/setting/setting_azure_test.go index b5fe304df49..7df70e823b4 100644 --- a/pkg/setting/setting_azure_test.go +++ b/pkg/setting/setting_azure_test.go @@ -4,7 +4,6 @@ import ( "testing" "github.com/grafana/grafana-azure-sdk-go/azsettings" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/setting/setting_feature_toggles.go b/pkg/setting/setting_feature_toggles.go index abef83d7c48..a29f24adab3 100644 --- a/pkg/setting/setting_feature_toggles.go +++ b/pkg/setting/setting_feature_toggles.go @@ -3,8 +3,9 @@ package setting import ( "strconv" - "github.com/grafana/grafana/pkg/util" "gopkg.in/ini.v1" + + "github.com/grafana/grafana/pkg/util" ) // @deprecated -- should use `featuremgmt.FeatureToggles` diff --git a/pkg/setting/setting_session_test.go b/pkg/setting/setting_session_test.go index 07554d87885..5c86811dbe7 100644 --- a/pkg/setting/setting_session_test.go +++ b/pkg/setting/setting_session_test.go @@ -4,9 +4,9 @@ import ( "path/filepath" "testing" - "github.com/grafana/grafana/pkg/infra/log/logtest" - "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/log/logtest" ) func TestSessionSettings(t *testing.T) { diff --git a/pkg/setting/setting_test.go b/pkg/setting/setting_test.go index e46c8ae10b4..2bc539cbeb4 100644 --- a/pkg/setting/setting_test.go +++ b/pkg/setting/setting_test.go @@ -14,7 +14,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "gopkg.in/ini.v1" ) diff --git a/pkg/setting/setting_unified_alerting.go b/pkg/setting/setting_unified_alerting.go index d81511fa3e4..d417be97417 100644 --- a/pkg/setting/setting_unified_alerting.go +++ b/pkg/setting/setting_unified_alerting.go @@ -8,11 +8,10 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" - - "github.com/grafana/grafana/pkg/util" - "github.com/prometheus/alertmanager/cluster" "gopkg.in/ini.v1" + + "github.com/grafana/grafana/pkg/util" ) const ( diff --git a/pkg/tests/api/azuremonitor/azuremonitor_test.go b/pkg/tests/api/azuremonitor/azuremonitor_test.go index e39184e36e5..c262c7bbce0 100644 --- a/pkg/tests/api/azuremonitor/azuremonitor_test.go +++ b/pkg/tests/api/azuremonitor/azuremonitor_test.go @@ -10,13 +10,14 @@ import ( "net/http/httptest" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" - "github.com/stretchr/testify/require" ) func TestIntegrationAzureMonitor(t *testing.T) { diff --git a/pkg/tests/api/correlations/correlations_create_test.go b/pkg/tests/api/correlations/correlations_create_test.go index 182be0c70dc..3921767c6fa 100644 --- a/pkg/tests/api/correlations/correlations_create_test.go +++ b/pkg/tests/api/correlations/correlations_create_test.go @@ -7,11 +7,12 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/correlations" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" - "github.com/stretchr/testify/require" ) func TestIntegrationCreateCorrelation(t *testing.T) { diff --git a/pkg/tests/api/correlations/correlations_delete_test.go b/pkg/tests/api/correlations/correlations_delete_test.go index defb10e4551..9002a5582d7 100644 --- a/pkg/tests/api/correlations/correlations_delete_test.go +++ b/pkg/tests/api/correlations/correlations_delete_test.go @@ -7,11 +7,12 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/correlations" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" - "github.com/stretchr/testify/require" ) func TestIntegrationDeleteCorrelation(t *testing.T) { diff --git a/pkg/tests/api/correlations/correlations_update_test.go b/pkg/tests/api/correlations/correlations_update_test.go index 5b603f9eb71..a085abac46f 100644 --- a/pkg/tests/api/correlations/correlations_update_test.go +++ b/pkg/tests/api/correlations/correlations_update_test.go @@ -7,11 +7,12 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/correlations" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" - "github.com/stretchr/testify/require" ) func TestIntegrationUpdateCorrelation(t *testing.T) { diff --git a/pkg/tests/api/elasticsearch/elasticsearch_test.go b/pkg/tests/api/elasticsearch/elasticsearch_test.go index 9c138f1ac5f..ad6204de604 100644 --- a/pkg/tests/api/elasticsearch/elasticsearch_test.go +++ b/pkg/tests/api/elasticsearch/elasticsearch_test.go @@ -10,13 +10,14 @@ import ( "net/http/httptest" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" - "github.com/stretchr/testify/require" ) func TestIntegrationElasticsearch(t *testing.T) { diff --git a/pkg/tests/api/graphite/graphite_test.go b/pkg/tests/api/graphite/graphite_test.go index 0b53ba56305..f70f41802e2 100644 --- a/pkg/tests/api/graphite/graphite_test.go +++ b/pkg/tests/api/graphite/graphite_test.go @@ -10,13 +10,14 @@ import ( "net/http/httptest" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" - "github.com/stretchr/testify/require" ) func TestIntegrationGraphite(t *testing.T) { diff --git a/pkg/tests/api/influxdb/influxdb_test.go b/pkg/tests/api/influxdb/influxdb_test.go index a76ba3171e7..70740bb81a9 100644 --- a/pkg/tests/api/influxdb/influxdb_test.go +++ b/pkg/tests/api/influxdb/influxdb_test.go @@ -10,13 +10,14 @@ import ( "net/http/httptest" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" - "github.com/stretchr/testify/require" ) func TestIntegrationInflux(t *testing.T) { diff --git a/pkg/tests/api/loki/loki_test.go b/pkg/tests/api/loki/loki_test.go index 9cd826b4def..6e86628ac53 100644 --- a/pkg/tests/api/loki/loki_test.go +++ b/pkg/tests/api/loki/loki_test.go @@ -10,13 +10,14 @@ import ( "net/http/httptest" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" - "github.com/stretchr/testify/require" ) func TestIntegrationLoki(t *testing.T) { diff --git a/pkg/tests/api/opentdsb/opentdsb_test.go b/pkg/tests/api/opentdsb/opentdsb_test.go index 12598b02f9d..f464757fa1a 100644 --- a/pkg/tests/api/opentdsb/opentdsb_test.go +++ b/pkg/tests/api/opentdsb/opentdsb_test.go @@ -10,13 +10,14 @@ import ( "net/http/httptest" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" - "github.com/stretchr/testify/require" ) func TestIntegrationOpenTSDB(t *testing.T) { diff --git a/pkg/tests/api/plugins/api_plugins_test.go b/pkg/tests/api/plugins/api_plugins_test.go index 251c9389310..1e7be227b39 100644 --- a/pkg/tests/api/plugins/api_plugins_test.go +++ b/pkg/tests/api/plugins/api_plugins_test.go @@ -11,15 +11,15 @@ import ( "path/filepath" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/quota/quotaimpl" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/tests/testinfra" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) const ( diff --git a/pkg/tests/api/plugins/backendplugin/backendplugin_test.go b/pkg/tests/api/plugins/backendplugin/backendplugin_test.go index 9b6f58b60a4..7630033d4ac 100644 --- a/pkg/tests/api/plugins/backendplugin/backendplugin_test.go +++ b/pkg/tests/api/plugins/backendplugin/backendplugin_test.go @@ -12,6 +12,9 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/stretchr/testify/require" + "golang.org/x/oauth2" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" @@ -22,8 +25,6 @@ import ( "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" - "github.com/stretchr/testify/require" - "golang.org/x/oauth2" ) const loginCookieName = "grafana_session" diff --git a/pkg/tests/api/prometheus/prometheus_test.go b/pkg/tests/api/prometheus/prometheus_test.go index eb5c0dc34f2..c5b9f258c42 100644 --- a/pkg/tests/api/prometheus/prometheus_test.go +++ b/pkg/tests/api/prometheus/prometheus_test.go @@ -10,13 +10,14 @@ import ( "net/http/httptest" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" - "github.com/stretchr/testify/require" ) func TestIntegrationPrometheus(t *testing.T) { diff --git a/pkg/tests/web/index_view_test.go b/pkg/tests/web/index_view_test.go index e2ef840ede6..8286abde819 100644 --- a/pkg/tests/web/index_view_test.go +++ b/pkg/tests/web/index_view_test.go @@ -7,9 +7,10 @@ import ( "strings" "testing" - "github.com/grafana/grafana/pkg/tests/testinfra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/tests/testinfra" ) // TestIntegrationIndexView tests the Grafana index view. From f531074d89972bd4e5ad7274dc053502dea3162e Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Mon, 30 Jan 2023 09:32:25 +0100 Subject: [PATCH 079/117] Chore: Fix goimports grouping in pkg/infra (#62421) * fix goimports * fix goimports order --- pkg/infra/appcontext/user_test.go | 3 ++- pkg/infra/log/file_test.go | 3 +-- pkg/infra/metrics/metrics.go | 3 ++- pkg/infra/metrics/settings.go | 3 ++- pkg/infra/remotecache/memcached_storage.go | 1 + pkg/infra/remotecache/redis_storage.go | 1 + pkg/infra/tracing/optentelemetry_tracing_test.go | 3 ++- pkg/infra/tracing/tracing_test.go | 3 ++- pkg/infra/usagestats/statscollector/prometheus_flavor_test.go | 1 - pkg/infra/usagestats/statscollector/service_test.go | 4 +--- 10 files changed, 14 insertions(+), 11 deletions(-) diff --git a/pkg/infra/appcontext/user_test.go b/pkg/infra/appcontext/user_test.go index 5122d9044e0..6db4735b647 100644 --- a/pkg/infra/appcontext/user_test.go +++ b/pkg/infra/appcontext/user_test.go @@ -6,13 +6,14 @@ import ( "math/big" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" grpccontext "github.com/grafana/grafana/pkg/services/grpcserver/context" "github.com/grafana/grafana/pkg/services/user" - "github.com/stretchr/testify/require" ) func TestUserFromContext(t *testing.T) { diff --git a/pkg/infra/log/file_test.go b/pkg/infra/log/file_test.go index f6361e152a3..aaa21f8ead2 100644 --- a/pkg/infra/log/file_test.go +++ b/pkg/infra/log/file_test.go @@ -4,9 +4,8 @@ import ( "os" "testing" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func (w *FileLogWriter) WriteLine(line string) error { diff --git a/pkg/infra/metrics/metrics.go b/pkg/infra/metrics/metrics.go index c70040c7c40..ee9017cac53 100644 --- a/pkg/infra/metrics/metrics.go +++ b/pkg/infra/metrics/metrics.go @@ -3,10 +3,11 @@ package metrics import ( "runtime" + "github.com/prometheus/client_golang/prometheus" + "github.com/grafana/grafana/pkg/infra/metrics/metricutil" pubdash "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/grafana/grafana/pkg/setting" - "github.com/prometheus/client_golang/prometheus" ) // ExporterName is used as namespace for exposing prometheus metrics diff --git a/pkg/infra/metrics/settings.go b/pkg/infra/metrics/settings.go index cde0e447b28..e07f5533a00 100644 --- a/pkg/infra/metrics/settings.go +++ b/pkg/infra/metrics/settings.go @@ -5,9 +5,10 @@ import ( "strings" "time" + "github.com/prometheus/client_golang/prometheus" + "github.com/grafana/grafana/pkg/infra/metrics/graphitebridge" "github.com/grafana/grafana/pkg/setting" - "github.com/prometheus/client_golang/prometheus" ) func (im *InternalMetricsService) readSettings() error { diff --git a/pkg/infra/remotecache/memcached_storage.go b/pkg/infra/remotecache/memcached_storage.go index b8c1b4e4823..f8601eb1a2d 100644 --- a/pkg/infra/remotecache/memcached_storage.go +++ b/pkg/infra/remotecache/memcached_storage.go @@ -5,6 +5,7 @@ import ( "time" "github.com/bradfitz/gomemcache/memcache" + "github.com/grafana/grafana/pkg/setting" ) diff --git a/pkg/infra/remotecache/redis_storage.go b/pkg/infra/remotecache/redis_storage.go index 7c76345a5bf..f8ed10d5bb8 100644 --- a/pkg/infra/remotecache/redis_storage.go +++ b/pkg/infra/remotecache/redis_storage.go @@ -9,6 +9,7 @@ import ( "time" "github.com/go-redis/redis/v8" + "github.com/grafana/grafana/pkg/setting" ) diff --git a/pkg/infra/tracing/optentelemetry_tracing_test.go b/pkg/infra/tracing/optentelemetry_tracing_test.go index 01e817da776..690edb80070 100644 --- a/pkg/infra/tracing/optentelemetry_tracing_test.go +++ b/pkg/infra/tracing/optentelemetry_tracing_test.go @@ -3,9 +3,10 @@ package tracing import ( "testing" - "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" "go.opentelemetry.io/otel/attribute" + + "github.com/grafana/grafana/pkg/setting" ) func TestSplitCustomAttribs(t *testing.T) { diff --git a/pkg/infra/tracing/tracing_test.go b/pkg/infra/tracing/tracing_test.go index d40390b5276..6c2a01b4c45 100644 --- a/pkg/infra/tracing/tracing_test.go +++ b/pkg/infra/tracing/tracing_test.go @@ -4,9 +4,10 @@ import ( "os" "testing" - "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/setting" ) func TestGroupSplit(t *testing.T) { diff --git a/pkg/infra/usagestats/statscollector/prometheus_flavor_test.go b/pkg/infra/usagestats/statscollector/prometheus_flavor_test.go index b0f4cad6d2a..69d85caf58b 100644 --- a/pkg/infra/usagestats/statscollector/prometheus_flavor_test.go +++ b/pkg/infra/usagestats/statscollector/prometheus_flavor_test.go @@ -8,7 +8,6 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db/dbtest" diff --git a/pkg/infra/usagestats/statscollector/service_test.go b/pkg/infra/usagestats/statscollector/service_test.go index 0200588e3f1..cb8074dedb1 100644 --- a/pkg/infra/usagestats/statscollector/service_test.go +++ b/pkg/infra/usagestats/statscollector/service_test.go @@ -7,13 +7,11 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/components/simplejson" - sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/httpclient" From 7dbd2cd139c26f8707d5ee30e8565c28fb4261d2 Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Mon, 30 Jan 2023 09:34:18 +0100 Subject: [PATCH 080/117] Chore: Fix goimports grouping (#62426) fix goimports ordering --- pkg/login/social/azuread_oauth.go | 6 +++--- pkg/login/social/generic_oauth_test.go | 2 +- pkg/login/social/gitlab_oauth_test.go | 3 ++- pkg/login/social/grafana_com_oauth.go | 4 ++-- pkg/login/social/social.go | 3 +-- pkg/services/accesscontrol/acimpl/accesscontrol_test.go | 3 ++- pkg/services/accesscontrol/acimpl/service_bench_test.go | 3 ++- pkg/services/accesscontrol/api/api_test.go | 3 ++- pkg/services/accesscontrol/checker_test.go | 3 ++- pkg/services/accesscontrol/pluginutils/utils_test.go | 3 ++- pkg/services/accesscontrol/resolvers_test.go | 3 ++- pkg/services/auth/authimpl/auth_token_test.go | 2 +- pkg/services/auth/jwt/key_sets.go | 3 ++- pkg/services/authn/authnimpl/priority_queue_test.go | 2 +- pkg/services/authn/clients/anonymous_test.go | 5 +++-- pkg/services/authn/clients/basic_test.go | 3 ++- pkg/services/authn/clients/password_test.go | 2 +- pkg/services/authn/clients/proxy_test.go | 2 +- pkg/services/authn/clients/session_test.go | 5 +++-- pkg/services/ldap/ldap_login_test.go | 1 - pkg/services/ldap/ldap_private_test.go | 4 +--- .../loginattempt/loginattemptimpl/login_attempt_test.go | 3 ++- pkg/services/multildap/multildap_test.go | 4 ++-- pkg/services/serviceaccounts/database/stats_test.go | 5 +++-- pkg/services/serviceaccounts/database/token_store_test.go | 3 ++- pkg/services/serviceaccounts/manager/service_test.go | 4 ++-- pkg/services/serviceaccounts/manager/stats_test.go | 5 +++-- pkg/services/serviceaccounts/secretscan/service_test.go | 5 +++-- .../supportbundles/supportbundlesimpl/service_test.go | 3 ++- pkg/services/supportbundles/supportbundlesimpl/store.go | 1 + pkg/services/teamguardian/database/database_mock.go | 3 ++- pkg/services/teamguardian/manager/service_mock.go | 3 ++- 32 files changed, 60 insertions(+), 44 deletions(-) diff --git a/pkg/login/social/azuread_oauth.go b/pkg/login/social/azuread_oauth.go index 8f10f8e812c..c5120818d70 100644 --- a/pkg/login/social/azuread_oauth.go +++ b/pkg/login/social/azuread_oauth.go @@ -8,11 +8,11 @@ import ( "net/http" "strings" - "github.com/grafana/grafana/pkg/models/roletype" - "github.com/grafana/grafana/pkg/services/org" - "golang.org/x/oauth2" "gopkg.in/square/go-jose.v2/jwt" + + "github.com/grafana/grafana/pkg/models/roletype" + "github.com/grafana/grafana/pkg/services/org" ) type SocialAzureAD struct { diff --git a/pkg/login/social/generic_oauth_test.go b/pkg/login/social/generic_oauth_test.go index c11d2fde527..40f19e3a3d9 100644 --- a/pkg/login/social/generic_oauth_test.go +++ b/pkg/login/social/generic_oauth_test.go @@ -7,11 +7,11 @@ import ( "testing" "time" + "github.com/go-kit/log/level" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/oauth2" - "github.com/go-kit/log/level" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/org" ) diff --git a/pkg/login/social/gitlab_oauth_test.go b/pkg/login/social/gitlab_oauth_test.go index 188b23e7d44..a4eb8480447 100644 --- a/pkg/login/social/gitlab_oauth_test.go +++ b/pkg/login/social/gitlab_oauth_test.go @@ -6,8 +6,9 @@ import ( "strings" "testing" - "github.com/grafana/grafana/pkg/services/org" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/org" ) const ( diff --git a/pkg/login/social/grafana_com_oauth.go b/pkg/login/social/grafana_com_oauth.go index 7db69cf0aa3..7ac1568cd1e 100644 --- a/pkg/login/social/grafana_com_oauth.go +++ b/pkg/login/social/grafana_com_oauth.go @@ -5,10 +5,10 @@ import ( "fmt" "net/http" + "golang.org/x/oauth2" + "github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/services/org" - - "golang.org/x/oauth2" ) type SocialGrafanaCom struct { diff --git a/pkg/login/social/social.go b/pkg/login/social/social.go index af56b006c4b..9781bd832f9 100644 --- a/pkg/login/social/social.go +++ b/pkg/login/social/social.go @@ -1,6 +1,7 @@ package social import ( + "context" "crypto/tls" "crypto/x509" "encoding/json" @@ -9,8 +10,6 @@ import ( "os" "strings" - "context" - "golang.org/x/oauth2" "golang.org/x/text/cases" "golang.org/x/text/language" diff --git a/pkg/services/accesscontrol/acimpl/accesscontrol_test.go b/pkg/services/accesscontrol/acimpl/accesscontrol_test.go index 4c53ee45aea..e3cc5fbcd92 100644 --- a/pkg/services/accesscontrol/acimpl/accesscontrol_test.go +++ b/pkg/services/accesscontrol/acimpl/accesscontrol_test.go @@ -4,10 +4,11 @@ import ( "context" "testing" + "github.com/stretchr/testify/assert" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" ) func TestAccessControl_Evaluate(t *testing.T) { diff --git a/pkg/services/accesscontrol/acimpl/service_bench_test.go b/pkg/services/accesscontrol/acimpl/service_bench_test.go index 2a12199eda2..81e6d0e858f 100644 --- a/pkg/services/accesscontrol/acimpl/service_bench_test.go +++ b/pkg/services/accesscontrol/acimpl/service_bench_test.go @@ -6,6 +6,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -14,7 +16,6 @@ import ( "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/require" ) // setupBenchEnv will create userCount users, userCount managed roles with resourceCount managed permission each diff --git a/pkg/services/accesscontrol/api/api_test.go b/pkg/services/accesscontrol/api/api_test.go index 13a2f6161fc..a9313789810 100644 --- a/pkg/services/accesscontrol/api/api_test.go +++ b/pkg/services/accesscontrol/api/api_test.go @@ -5,6 +5,8 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/routing" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" @@ -13,7 +15,6 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web/webtest" - "github.com/stretchr/testify/require" ) func TestAPI_getUserActions(t *testing.T) { diff --git a/pkg/services/accesscontrol/checker_test.go b/pkg/services/accesscontrol/checker_test.go index a9e5798978b..ad7051d3721 100644 --- a/pkg/services/accesscontrol/checker_test.go +++ b/pkg/services/accesscontrol/checker_test.go @@ -4,8 +4,9 @@ import ( "strconv" "testing" - "github.com/grafana/grafana/pkg/services/user" "github.com/stretchr/testify/assert" + + "github.com/grafana/grafana/pkg/services/user" ) type testData struct { diff --git a/pkg/services/accesscontrol/pluginutils/utils_test.go b/pkg/services/accesscontrol/pluginutils/utils_test.go index 0284ebbb49e..c6432a355ab 100644 --- a/pkg/services/accesscontrol/pluginutils/utils_test.go +++ b/pkg/services/accesscontrol/pluginutils/utils_test.go @@ -3,9 +3,10 @@ package pluginutils import ( "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/plugins" ac "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/stretchr/testify/require" ) func TestToRegistrations(t *testing.T) { diff --git a/pkg/services/accesscontrol/resolvers_test.go b/pkg/services/accesscontrol/resolvers_test.go index 0b0b782cd90..310afce1748 100644 --- a/pkg/services/accesscontrol/resolvers_test.go +++ b/pkg/services/accesscontrol/resolvers_test.go @@ -4,10 +4,11 @@ import ( "context" "testing" + "github.com/stretchr/testify/assert" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/datasources" - "github.com/stretchr/testify/assert" ) func TestResolvers_AttributeScope(t *testing.T) { diff --git a/pkg/services/auth/authimpl/auth_token_test.go b/pkg/services/auth/authimpl/auth_token_test.go index 25358aed7dd..4220a30d9cf 100644 --- a/pkg/services/auth/authimpl/auth_token_test.go +++ b/pkg/services/auth/authimpl/auth_token_test.go @@ -8,12 +8,12 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/services/auth" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" diff --git a/pkg/services/auth/jwt/key_sets.go b/pkg/services/auth/jwt/key_sets.go index 63d4c940e73..175b60c14d2 100644 --- a/pkg/services/auth/jwt/key_sets.go +++ b/pkg/services/auth/jwt/key_sets.go @@ -14,9 +14,10 @@ import ( "os" "time" + jose "gopkg.in/square/go-jose.v2" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/remotecache" - jose "gopkg.in/square/go-jose.v2" ) var ErrFailedToParsePemFile = errors.New("failed to parse pem-encoded file") diff --git a/pkg/services/authn/authnimpl/priority_queue_test.go b/pkg/services/authn/authnimpl/priority_queue_test.go index 119e3a9e5b8..003b858245e 100644 --- a/pkg/services/authn/authnimpl/priority_queue_test.go +++ b/pkg/services/authn/authnimpl/priority_queue_test.go @@ -3,11 +3,11 @@ package authnimpl import ( "testing" - "github.com/grafana/grafana/pkg/services/authn/authntest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/authn/authntest" ) func TestQueue(t *testing.T) { diff --git a/pkg/services/authn/clients/anonymous_test.go b/pkg/services/authn/clients/anonymous_test.go index 7ecca2bc583..70867622d9b 100644 --- a/pkg/services/authn/clients/anonymous_test.go +++ b/pkg/services/authn/clients/anonymous_test.go @@ -5,13 +5,14 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgtest" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestAnonymous_Authenticate(t *testing.T) { diff --git a/pkg/services/authn/clients/basic_test.go b/pkg/services/authn/clients/basic_test.go index 81b974ee20d..fbf2a96a2d7 100644 --- a/pkg/services/authn/clients/basic_test.go +++ b/pkg/services/authn/clients/basic_test.go @@ -5,9 +5,10 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/assert" + "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/authn/authntest" - "github.com/stretchr/testify/assert" ) func TestBasic_Authenticate(t *testing.T) { diff --git a/pkg/services/authn/clients/password_test.go b/pkg/services/authn/clients/password_test.go index e064265e90a..85997429041 100644 --- a/pkg/services/authn/clients/password_test.go +++ b/pkg/services/authn/clients/password_test.go @@ -4,11 +4,11 @@ import ( "context" "testing" - "github.com/grafana/grafana/pkg/services/loginattempt/loginattempttest" "github.com/stretchr/testify/assert" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/authn/authntest" + "github.com/grafana/grafana/pkg/services/loginattempt/loginattempttest" ) func TestPassword_AuthenticatePassword(t *testing.T) { diff --git a/pkg/services/authn/clients/proxy_test.go b/pkg/services/authn/clients/proxy_test.go index 62a72e32ff8..e682bb269cf 100644 --- a/pkg/services/authn/clients/proxy_test.go +++ b/pkg/services/authn/clients/proxy_test.go @@ -5,11 +5,11 @@ import ( "net/http" "testing" - "github.com/grafana/grafana/pkg/services/authn/authntest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/authn/authntest" "github.com/grafana/grafana/pkg/setting" ) diff --git a/pkg/services/authn/clients/session_test.go b/pkg/services/authn/clients/session_test.go index 65b7bb62326..443f6371a6c 100644 --- a/pkg/services/authn/clients/session_test.go +++ b/pkg/services/authn/clients/session_test.go @@ -7,6 +7,9 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/models/usertoken" "github.com/grafana/grafana/pkg/services/auth" @@ -15,8 +18,6 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/web" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestSession_Test(t *testing.T) { diff --git a/pkg/services/ldap/ldap_login_test.go b/pkg/services/ldap/ldap_login_test.go index c31b2cb4dbd..806812881f9 100644 --- a/pkg/services/ldap/ldap_login_test.go +++ b/pkg/services/ldap/ldap_login_test.go @@ -6,7 +6,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "gopkg.in/ldap.v3" "github.com/grafana/grafana/pkg/infra/log" diff --git a/pkg/services/ldap/ldap_private_test.go b/pkg/services/ldap/ldap_private_test.go index e9d534051eb..1bbcfb6c48c 100644 --- a/pkg/services/ldap/ldap_private_test.go +++ b/pkg/services/ldap/ldap_private_test.go @@ -3,10 +3,8 @@ package ldap import ( "testing" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/assert" - + "github.com/stretchr/testify/require" "gopkg.in/ldap.v3" "github.com/grafana/grafana/pkg/infra/log" diff --git a/pkg/services/loginattempt/loginattemptimpl/login_attempt_test.go b/pkg/services/loginattempt/loginattemptimpl/login_attempt_test.go index 84e93e0049f..e6ba7c31fe4 100644 --- a/pkg/services/loginattempt/loginattemptimpl/login_attempt_test.go +++ b/pkg/services/loginattempt/loginattemptimpl/login_attempt_test.go @@ -4,8 +4,9 @@ import ( "context" "testing" - "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" + + "github.com/grafana/grafana/pkg/setting" ) func TestService_Validate(t *testing.T) { diff --git a/pkg/services/multildap/multildap_test.go b/pkg/services/multildap/multildap_test.go index 8d61448cdde..e15638b4ca8 100644 --- a/pkg/services/multildap/multildap_test.go +++ b/pkg/services/multildap/multildap_test.go @@ -4,11 +4,11 @@ import ( "errors" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/login" - "github.com/stretchr/testify/require" - //TODO(sh0rez): remove once import cycle resolved _ "github.com/grafana/grafana/pkg/api/response" ) diff --git a/pkg/services/serviceaccounts/database/stats_test.go b/pkg/services/serviceaccounts/database/stats_test.go index 6050ff70154..15f6983b138 100644 --- a/pkg/services/serviceaccounts/database/stats_test.go +++ b/pkg/services/serviceaccounts/database/stats_test.go @@ -4,11 +4,12 @@ import ( "context" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/components/apikeygen" "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts/tests" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestStore_UsageStats(t *testing.T) { diff --git a/pkg/services/serviceaccounts/database/token_store_test.go b/pkg/services/serviceaccounts/database/token_store_test.go index d06011139fb..c95cd1cd9ff 100644 --- a/pkg/services/serviceaccounts/database/token_store_test.go +++ b/pkg/services/serviceaccounts/database/token_store_test.go @@ -4,11 +4,12 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/components/apikeygen" "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts/tests" - "github.com/stretchr/testify/require" ) func TestStore_AddServiceAccountToken(t *testing.T) { diff --git a/pkg/services/serviceaccounts/manager/service_test.go b/pkg/services/serviceaccounts/manager/service_test.go index 916e825ca67..a5425acd7ad 100644 --- a/pkg/services/serviceaccounts/manager/service_test.go +++ b/pkg/services/serviceaccounts/manager/service_test.go @@ -4,12 +4,12 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/serviceaccounts" - - "github.com/stretchr/testify/require" ) type FakeServiceAccountStore struct { diff --git a/pkg/services/serviceaccounts/manager/stats_test.go b/pkg/services/serviceaccounts/manager/stats_test.go index 4b4b2dd6ff1..a7b21041303 100644 --- a/pkg/services/serviceaccounts/manager/stats_test.go +++ b/pkg/services/serviceaccounts/manager/stats_test.go @@ -4,10 +4,11 @@ import ( "context" "testing" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/serviceaccounts" ) func Test_UsageStats(t *testing.T) { diff --git a/pkg/services/serviceaccounts/secretscan/service_test.go b/pkg/services/serviceaccounts/secretscan/service_test.go index 1bb61b800e0..824b3f2e5e6 100644 --- a/pkg/services/serviceaccounts/secretscan/service_test.go +++ b/pkg/services/serviceaccounts/secretscan/service_test.go @@ -4,10 +4,11 @@ import ( "context" "testing" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/services/apikey" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/apikey" ) func TestService_CheckTokens(t *testing.T) { diff --git a/pkg/services/supportbundles/supportbundlesimpl/service_test.go b/pkg/services/supportbundles/supportbundlesimpl/service_test.go index a2c3176b15e..39241804020 100644 --- a/pkg/services/supportbundles/supportbundlesimpl/service_test.go +++ b/pkg/services/supportbundles/supportbundlesimpl/service_test.go @@ -4,10 +4,11 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/supportbundles" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/require" ) func TestService_RegisterSupportItemCollector(t *testing.T) { diff --git a/pkg/services/supportbundles/supportbundlesimpl/store.go b/pkg/services/supportbundles/supportbundlesimpl/store.go index ca57a072ece..921ee551b30 100644 --- a/pkg/services/supportbundles/supportbundlesimpl/store.go +++ b/pkg/services/supportbundles/supportbundlesimpl/store.go @@ -9,6 +9,7 @@ import ( "time" "github.com/google/uuid" + "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/services/supportbundles" "github.com/grafana/grafana/pkg/services/user" diff --git a/pkg/services/teamguardian/database/database_mock.go b/pkg/services/teamguardian/database/database_mock.go index 3b58cc06ebb..0a7dd142e56 100644 --- a/pkg/services/teamguardian/database/database_mock.go +++ b/pkg/services/teamguardian/database/database_mock.go @@ -3,8 +3,9 @@ package database import ( "context" - "github.com/grafana/grafana/pkg/services/team" "github.com/stretchr/testify/mock" + + "github.com/grafana/grafana/pkg/services/team" ) type TeamGuardianStoreMock struct { diff --git a/pkg/services/teamguardian/manager/service_mock.go b/pkg/services/teamguardian/manager/service_mock.go index 8ba08d98c45..b561b926d69 100644 --- a/pkg/services/teamguardian/manager/service_mock.go +++ b/pkg/services/teamguardian/manager/service_mock.go @@ -3,8 +3,9 @@ package manager import ( "context" - "github.com/grafana/grafana/pkg/services/user" "github.com/stretchr/testify/mock" + + "github.com/grafana/grafana/pkg/services/user" ) type TeamGuardianMock struct { From f8ec35e643cacf13dcf80921a8641a1634fa6f95 Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Mon, 30 Jan 2023 09:38:51 +0100 Subject: [PATCH 081/117] Chore: Fix goimports grouping (#62427) * fix goimports order * fix goimports order --- pkg/expr/classic/classic_test.go | 2 +- pkg/expr/classic/evaluator_test.go | 3 ++- pkg/expr/classic/reduce_test.go | 2 +- pkg/expr/graph.go | 4 ++-- pkg/expr/nodes.go | 3 +-- pkg/tsdb/graphite/graphite_test.go | 3 ++- pkg/tsdb/influxdb/flux/executor_test.go | 5 ++--- pkg/tsdb/influxdb/flux/query_models.go | 1 + pkg/tsdb/influxdb/model_parser.go | 1 + pkg/tsdb/influxdb/query.go | 1 + pkg/tsdb/prometheus/azureauth/azure.go | 2 +- pkg/tsdb/prometheus/azureauth/azure_test.go | 4 ++-- pkg/tsdb/prometheus/client/client.go | 1 + pkg/tsdb/prometheus/client/client_test.go | 3 ++- pkg/tsdb/prometheus/client/transport.go | 1 + pkg/tsdb/prometheus/client/transport_test.go | 3 ++- pkg/tsdb/prometheus/middleware/custom_query_params.go | 1 + pkg/tsdb/prometheus/middleware/custom_query_params_test.go | 3 ++- pkg/tsdb/prometheus/middleware/force_http_get.go | 1 + pkg/tsdb/prometheus/middleware/force_http_get_test.go | 3 ++- pkg/tsdb/prometheus/models/query.go | 1 + pkg/tsdb/prometheus/models/query_test.go | 3 ++- pkg/tsdb/prometheus/prometheus.go | 5 +++-- pkg/tsdb/prometheus/prometheus_test.go | 3 ++- .../prometheus/querydata/exemplar/sampler_stddev_test.go | 1 + pkg/tsdb/prometheus/querydata/exemplar/sampler_test.go | 1 + pkg/tsdb/prometheus/querydata/framing_bench_test.go | 3 ++- pkg/tsdb/prometheus/querydata/framing_test.go | 3 +-- pkg/tsdb/prometheus/querydata/request_test.go | 2 +- pkg/tsdb/prometheus/utils/utils.go | 3 ++- 30 files changed, 45 insertions(+), 27 deletions(-) diff --git a/pkg/expr/classic/classic_test.go b/pkg/expr/classic/classic_test.go index de159f32f21..31ccdbe1425 100644 --- a/pkg/expr/classic/classic_test.go +++ b/pkg/expr/classic/classic_test.go @@ -6,10 +6,10 @@ import ( "testing" "time" + "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/stretchr/testify/require" ptr "github.com/xorcare/pointer" - "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/expr/mathexp" ) diff --git a/pkg/expr/classic/evaluator_test.go b/pkg/expr/classic/evaluator_test.go index 4deb92491ad..6cad30ff646 100644 --- a/pkg/expr/classic/evaluator_test.go +++ b/pkg/expr/classic/evaluator_test.go @@ -3,9 +3,10 @@ package classic import ( "testing" - "github.com/grafana/grafana/pkg/expr/mathexp" "github.com/stretchr/testify/require" ptr "github.com/xorcare/pointer" + + "github.com/grafana/grafana/pkg/expr/mathexp" ) func TestThresholdEvaluator(t *testing.T) { diff --git a/pkg/expr/classic/reduce_test.go b/pkg/expr/classic/reduce_test.go index 98cb5e63718..f46798449ed 100644 --- a/pkg/expr/classic/reduce_test.go +++ b/pkg/expr/classic/reduce_test.go @@ -5,10 +5,10 @@ import ( "testing" "time" + "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/stretchr/testify/require" ptr "github.com/xorcare/pointer" - "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/expr/mathexp" ) diff --git a/pkg/expr/graph.go b/pkg/expr/graph.go index 9e78e5e2f2e..1d9269d1f93 100644 --- a/pkg/expr/graph.go +++ b/pkg/expr/graph.go @@ -6,10 +6,10 @@ import ( "fmt" "time" - "github.com/grafana/grafana/pkg/expr/mathexp" - "gonum.org/v1/gonum/graph/simple" "gonum.org/v1/gonum/graph/topo" + + "github.com/grafana/grafana/pkg/expr/mathexp" ) // NodeType is the type of a DPNode. Currently either a expression command or datasource query. diff --git a/pkg/expr/nodes.go b/pkg/expr/nodes.go index ab23dabbae0..fca11ec45cb 100644 --- a/pkg/expr/nodes.go +++ b/pkg/expr/nodes.go @@ -8,14 +8,13 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" + "gonum.org/v1/gonum/graph/simple" "github.com/grafana/grafana/pkg/expr/classic" "github.com/grafana/grafana/pkg/expr/mathexp" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins/adapters" "github.com/grafana/grafana/pkg/services/datasources" - - "gonum.org/v1/gonum/graph/simple" ) var ( diff --git a/pkg/tsdb/graphite/graphite_test.go b/pkg/tsdb/graphite/graphite_test.go index 88b2f677be9..12336433f39 100644 --- a/pkg/tsdb/graphite/graphite_test.go +++ b/pkg/tsdb/graphite/graphite_test.go @@ -14,9 +14,10 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/components/simplejson" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/components/simplejson" ) func TestFixIntervalFormat(t *testing.T) { diff --git a/pkg/tsdb/influxdb/flux/executor_test.go b/pkg/tsdb/influxdb/flux/executor_test.go index 937f825eb1f..0bb8398d666 100644 --- a/pkg/tsdb/influxdb/flux/executor_test.go +++ b/pkg/tsdb/influxdb/flux/executor_test.go @@ -14,15 +14,14 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana-plugin-sdk-go/experimental" + influxdb2 "github.com/influxdata/influxdb-client-go/v2" + "github.com/influxdata/influxdb-client-go/v2/api" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/xorcare/pointer" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/tsdb/influxdb/models" - - influxdb2 "github.com/influxdata/influxdb-client-go/v2" - "github.com/influxdata/influxdb-client-go/v2/api" ) //-------------------------------------------------------------- diff --git a/pkg/tsdb/influxdb/flux/query_models.go b/pkg/tsdb/influxdb/flux/query_models.go index 8633229282c..3a97efeffdf 100644 --- a/pkg/tsdb/influxdb/flux/query_models.go +++ b/pkg/tsdb/influxdb/flux/query_models.go @@ -6,6 +6,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/tsdb/influxdb/models" ) diff --git a/pkg/tsdb/influxdb/model_parser.go b/pkg/tsdb/influxdb/model_parser.go index 9b8930d0bd9..d33e2aec34c 100644 --- a/pkg/tsdb/influxdb/model_parser.go +++ b/pkg/tsdb/influxdb/model_parser.go @@ -6,6 +6,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/components/simplejson" ) diff --git a/pkg/tsdb/influxdb/query.go b/pkg/tsdb/influxdb/query.go index abf160edd15..8ee02dda628 100644 --- a/pkg/tsdb/influxdb/query.go +++ b/pkg/tsdb/influxdb/query.go @@ -8,6 +8,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/tsdb/intervalv2" ) diff --git a/pkg/tsdb/prometheus/azureauth/azure.go b/pkg/tsdb/prometheus/azureauth/azure.go index b80bfcf41cc..2bb7711ac82 100644 --- a/pkg/tsdb/prometheus/azureauth/azure.go +++ b/pkg/tsdb/prometheus/azureauth/azure.go @@ -10,8 +10,8 @@ import ( "github.com/grafana/grafana-azure-sdk-go/azsettings" "github.com/grafana/grafana-plugin-sdk-go/backend" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" - "github.com/grafana/grafana/pkg/tsdb/prometheus/utils" + "github.com/grafana/grafana/pkg/tsdb/prometheus/utils" "github.com/grafana/grafana/pkg/util/maputil" ) diff --git a/pkg/tsdb/prometheus/azureauth/azure_test.go b/pkg/tsdb/prometheus/azureauth/azure_test.go index 8888c13cdec..e6037dfc09f 100644 --- a/pkg/tsdb/prometheus/azureauth/azure_test.go +++ b/pkg/tsdb/prometheus/azureauth/azure_test.go @@ -6,10 +6,10 @@ import ( "github.com/grafana/grafana-azure-sdk-go/azsettings" "github.com/grafana/grafana-plugin-sdk-go/backend" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" - - "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/setting" ) func TestConfigureAzureAuthentication(t *testing.T) { diff --git a/pkg/tsdb/prometheus/client/client.go b/pkg/tsdb/prometheus/client/client.go index d99d229509b..47a5969d373 100644 --- a/pkg/tsdb/prometheus/client/client.go +++ b/pkg/tsdb/prometheus/client/client.go @@ -12,6 +12,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/tsdb/prometheus/models" ) diff --git a/pkg/tsdb/prometheus/client/client_test.go b/pkg/tsdb/prometheus/client/client_test.go index e9928909e93..920baf81dfb 100644 --- a/pkg/tsdb/prometheus/client/client_test.go +++ b/pkg/tsdb/prometheus/client/client_test.go @@ -8,9 +8,10 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" "github.com/grafana/grafana/pkg/tsdb/prometheus/models" - "github.com/stretchr/testify/require" ) type MockDoer struct { diff --git a/pkg/tsdb/prometheus/client/transport.go b/pkg/tsdb/prometheus/client/transport.go index 5e4cccd1038..e74437efee0 100644 --- a/pkg/tsdb/prometheus/client/transport.go +++ b/pkg/tsdb/prometheus/client/transport.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/prometheus/azureauth" diff --git a/pkg/tsdb/prometheus/client/transport_test.go b/pkg/tsdb/prometheus/client/transport_test.go index 945e0ada0dc..67f0e92b49d 100644 --- a/pkg/tsdb/prometheus/client/transport_test.go +++ b/pkg/tsdb/prometheus/client/transport_test.go @@ -5,9 +5,10 @@ import ( "github.com/grafana/grafana-azure-sdk-go/azsettings" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/log/logtest" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/require" ) func TestCreateTransportOptions(t *testing.T) { diff --git a/pkg/tsdb/prometheus/middleware/custom_query_params.go b/pkg/tsdb/prometheus/middleware/custom_query_params.go index b352c2f6a0d..88c9ceeb384 100644 --- a/pkg/tsdb/prometheus/middleware/custom_query_params.go +++ b/pkg/tsdb/prometheus/middleware/custom_query_params.go @@ -5,6 +5,7 @@ import ( "net/url" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana/pkg/infra/log" ) diff --git a/pkg/tsdb/prometheus/middleware/custom_query_params_test.go b/pkg/tsdb/prometheus/middleware/custom_query_params_test.go index fd1e0e22e0a..b242add37a5 100644 --- a/pkg/tsdb/prometheus/middleware/custom_query_params_test.go +++ b/pkg/tsdb/prometheus/middleware/custom_query_params_test.go @@ -7,8 +7,9 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" - "github.com/grafana/grafana/pkg/infra/log" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/log" ) func TestCustomQueryParametersMiddleware(t *testing.T) { diff --git a/pkg/tsdb/prometheus/middleware/force_http_get.go b/pkg/tsdb/prometheus/middleware/force_http_get.go index 89061609478..38fc8433607 100644 --- a/pkg/tsdb/prometheus/middleware/force_http_get.go +++ b/pkg/tsdb/prometheus/middleware/force_http_get.go @@ -4,6 +4,7 @@ import ( "net/http" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana/pkg/infra/log" ) diff --git a/pkg/tsdb/prometheus/middleware/force_http_get_test.go b/pkg/tsdb/prometheus/middleware/force_http_get_test.go index 21cadae7290..8eb96121dc0 100644 --- a/pkg/tsdb/prometheus/middleware/force_http_get_test.go +++ b/pkg/tsdb/prometheus/middleware/force_http_get_test.go @@ -5,8 +5,9 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" - "github.com/grafana/grafana/pkg/infra/log" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/log" ) func TestEnsureHttpMethodMiddleware(t *testing.T) { diff --git a/pkg/tsdb/prometheus/models/query.go b/pkg/tsdb/prometheus/models/query.go index e69bcb9b226..233d8fddc50 100644 --- a/pkg/tsdb/prometheus/models/query.go +++ b/pkg/tsdb/prometheus/models/query.go @@ -8,6 +8,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/tsdb/intervalv2" ) diff --git a/pkg/tsdb/prometheus/models/query_test.go b/pkg/tsdb/prometheus/models/query_test.go index fc0efbcb123..dd30c5f0b24 100644 --- a/pkg/tsdb/prometheus/models/query_test.go +++ b/pkg/tsdb/prometheus/models/query_test.go @@ -6,9 +6,10 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/tsdb/intervalv2" "github.com/grafana/grafana/pkg/tsdb/prometheus/models" - "github.com/stretchr/testify/require" ) var ( diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index e03f7263733..cde81f9968f 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/pkg/tsdb/prometheus/prometheus.go @@ -10,6 +10,9 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" + "github.com/patrickmn/go-cache" + apiv1 "github.com/prometheus/client_golang/api/prometheus/v1" + "github.com/grafana/grafana/pkg/infra/httpclient" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" @@ -18,8 +21,6 @@ import ( "github.com/grafana/grafana/pkg/tsdb/prometheus/client" "github.com/grafana/grafana/pkg/tsdb/prometheus/querydata" "github.com/grafana/grafana/pkg/tsdb/prometheus/resource" - "github.com/patrickmn/go-cache" - apiv1 "github.com/prometheus/client_golang/api/prometheus/v1" ) var plog = log.New("tsdb.prometheus") diff --git a/pkg/tsdb/prometheus/prometheus_test.go b/pkg/tsdb/prometheus/prometheus_test.go index 5f9969ce237..ee2b9a1c289 100644 --- a/pkg/tsdb/prometheus/prometheus_test.go +++ b/pkg/tsdb/prometheus/prometheus_test.go @@ -10,10 +10,11 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" sdkHttpClient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/httpclient" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/require" ) type fakeSender struct{} diff --git a/pkg/tsdb/prometheus/querydata/exemplar/sampler_stddev_test.go b/pkg/tsdb/prometheus/querydata/exemplar/sampler_stddev_test.go index 994abdd551c..b96f6d63b28 100644 --- a/pkg/tsdb/prometheus/querydata/exemplar/sampler_stddev_test.go +++ b/pkg/tsdb/prometheus/querydata/exemplar/sampler_stddev_test.go @@ -5,6 +5,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/experimental" + "github.com/grafana/grafana/pkg/tsdb/prometheus/models" "github.com/grafana/grafana/pkg/tsdb/prometheus/querydata/exemplar" ) diff --git a/pkg/tsdb/prometheus/querydata/exemplar/sampler_test.go b/pkg/tsdb/prometheus/querydata/exemplar/sampler_test.go index 3387b16e34b..f071f1b9025 100644 --- a/pkg/tsdb/prometheus/querydata/exemplar/sampler_test.go +++ b/pkg/tsdb/prometheus/querydata/exemplar/sampler_test.go @@ -5,6 +5,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/experimental" + "github.com/grafana/grafana/pkg/tsdb/prometheus/models" "github.com/grafana/grafana/pkg/tsdb/prometheus/querydata/exemplar" ) diff --git a/pkg/tsdb/prometheus/querydata/framing_bench_test.go b/pkg/tsdb/prometheus/querydata/framing_bench_test.go index dee98077e7d..7f319ea72da 100644 --- a/pkg/tsdb/prometheus/querydata/framing_bench_test.go +++ b/pkg/tsdb/prometheus/querydata/framing_bench_test.go @@ -15,8 +15,9 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/tsdb/prometheus/models" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/tsdb/prometheus/models" ) // when memory-profiling this benchmark, these commands are recommended: diff --git a/pkg/tsdb/prometheus/querydata/framing_test.go b/pkg/tsdb/prometheus/querydata/framing_test.go index 40d96ea830d..09355702fc2 100644 --- a/pkg/tsdb/prometheus/querydata/framing_test.go +++ b/pkg/tsdb/prometheus/querydata/framing_test.go @@ -12,10 +12,9 @@ import ( "testing" "time" - "github.com/stretchr/testify/require" - "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/experimental" + "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/tsdb/prometheus/models" ) diff --git a/pkg/tsdb/prometheus/querydata/request_test.go b/pkg/tsdb/prometheus/querydata/request_test.go index 8005790781a..7ab268b4e7b 100644 --- a/pkg/tsdb/prometheus/querydata/request_test.go +++ b/pkg/tsdb/prometheus/querydata/request_test.go @@ -13,7 +13,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/tsdb/prometheus/client" apiv1 "github.com/prometheus/client_golang/api/prometheus/v1" p "github.com/prometheus/common/model" "github.com/stretchr/testify/require" @@ -22,6 +21,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log/logtest" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tsdb/prometheus/client" "github.com/grafana/grafana/pkg/tsdb/prometheus/models" "github.com/grafana/grafana/pkg/tsdb/prometheus/querydata" ) diff --git a/pkg/tsdb/prometheus/utils/utils.go b/pkg/tsdb/prometheus/utils/utils.go index 8930a214f91..a4e64c2819e 100644 --- a/pkg/tsdb/prometheus/utils/utils.go +++ b/pkg/tsdb/prometheus/utils/utils.go @@ -6,8 +6,9 @@ import ( "fmt" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/infra/tracing" "go.opentelemetry.io/otel/attribute" + + "github.com/grafana/grafana/pkg/infra/tracing" ) // GetJsonData just gets the json in easier to work with type. It's used on multiple places which isn't super effective From 0bf4093005946b973187a02c1226421c2399fa07 Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Mon, 30 Jan 2023 09:50:27 +0100 Subject: [PATCH 082/117] Chore: Fix goimports grouping (#62428) * fix goimports order * fix goimports order --- pkg/tsdb/elasticsearch/client/client_test.go | 3 ++- pkg/tsdb/elasticsearch/client/search_request_test.go | 4 ++-- pkg/tsdb/elasticsearch/elasticsearch_test.go | 3 ++- pkg/tsdb/elasticsearch/parse_query.go | 1 + pkg/tsdb/elasticsearch/querydata_test.go | 1 + pkg/tsdb/elasticsearch/response_parser.go | 1 + pkg/tsdb/elasticsearch/response_parser_test.go | 3 ++- pkg/tsdb/elasticsearch/time_series_query.go | 1 + pkg/tsdb/elasticsearch/time_series_query_test.go | 3 ++- pkg/tsdb/loki/api.go | 3 ++- pkg/tsdb/loki/parse_query.go | 1 + 11 files changed, 17 insertions(+), 7 deletions(-) diff --git a/pkg/tsdb/elasticsearch/client/client_test.go b/pkg/tsdb/elasticsearch/client/client_test.go index 0e0bb1bed42..d4c2f66da96 100644 --- a/pkg/tsdb/elasticsearch/client/client_test.go +++ b/pkg/tsdb/elasticsearch/client/client_test.go @@ -11,9 +11,10 @@ import ( "github.com/Masterminds/semver" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/components/simplejson" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/components/simplejson" ) func TestClient_ExecuteMultisearch(t *testing.T) { diff --git a/pkg/tsdb/elasticsearch/client/search_request_test.go b/pkg/tsdb/elasticsearch/client/search_request_test.go index 618249d1619..f3f1601c9a1 100644 --- a/pkg/tsdb/elasticsearch/client/search_request_test.go +++ b/pkg/tsdb/elasticsearch/client/search_request_test.go @@ -5,9 +5,9 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/components/simplejson" ) func TestSearchRequest(t *testing.T) { diff --git a/pkg/tsdb/elasticsearch/elasticsearch_test.go b/pkg/tsdb/elasticsearch/elasticsearch_test.go index 4de838cddaa..529972963ae 100644 --- a/pkg/tsdb/elasticsearch/elasticsearch_test.go +++ b/pkg/tsdb/elasticsearch/elasticsearch_test.go @@ -5,8 +5,9 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/infra/httpclient" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/httpclient" ) type datasourceInfo struct { diff --git a/pkg/tsdb/elasticsearch/parse_query.go b/pkg/tsdb/elasticsearch/parse_query.go index e43ea6059cb..db397d945e6 100644 --- a/pkg/tsdb/elasticsearch/parse_query.go +++ b/pkg/tsdb/elasticsearch/parse_query.go @@ -2,6 +2,7 @@ package elasticsearch import ( "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/components/simplejson" ) diff --git a/pkg/tsdb/elasticsearch/querydata_test.go b/pkg/tsdb/elasticsearch/querydata_test.go index 34cc82d28a0..c9cb0d49f43 100644 --- a/pkg/tsdb/elasticsearch/querydata_test.go +++ b/pkg/tsdb/elasticsearch/querydata_test.go @@ -11,6 +11,7 @@ import ( "github.com/Masterminds/semver" "github.com/grafana/grafana-plugin-sdk-go/backend" + es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" ) diff --git a/pkg/tsdb/elasticsearch/response_parser.go b/pkg/tsdb/elasticsearch/response_parser.go index 5c154f9c09c..76a63c12b1d 100644 --- a/pkg/tsdb/elasticsearch/response_parser.go +++ b/pkg/tsdb/elasticsearch/response_parser.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/components/simplejson" es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" ) diff --git a/pkg/tsdb/elasticsearch/response_parser_test.go b/pkg/tsdb/elasticsearch/response_parser_test.go index 8108da42167..5be058c9bea 100644 --- a/pkg/tsdb/elasticsearch/response_parser_test.go +++ b/pkg/tsdb/elasticsearch/response_parser_test.go @@ -9,9 +9,10 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/experimental" - es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" ) var update = flag.Bool("update", true, "update golden files") diff --git a/pkg/tsdb/elasticsearch/time_series_query.go b/pkg/tsdb/elasticsearch/time_series_query.go index 2e88a92e3b9..f8fc378a7e3 100644 --- a/pkg/tsdb/elasticsearch/time_series_query.go +++ b/pkg/tsdb/elasticsearch/time_series_query.go @@ -7,6 +7,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/components/simplejson" es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" ) diff --git a/pkg/tsdb/elasticsearch/time_series_query_test.go b/pkg/tsdb/elasticsearch/time_series_query_test.go index c74c4e152cd..a7af8f5368d 100644 --- a/pkg/tsdb/elasticsearch/time_series_query_test.go +++ b/pkg/tsdb/elasticsearch/time_series_query_test.go @@ -6,9 +6,10 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" - es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" ) func TestExecuteTimeSeriesQuery(t *testing.T) { diff --git a/pkg/tsdb/loki/api.go b/pkg/tsdb/loki/api.go index e18752383d2..2e87490042a 100644 --- a/pkg/tsdb/loki/api.go +++ b/pkg/tsdb/loki/api.go @@ -12,9 +12,10 @@ import ( "strconv" "github.com/grafana/grafana-plugin-sdk-go/data" + jsoniter "github.com/json-iterator/go" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/util/converter" - jsoniter "github.com/json-iterator/go" ) type LokiAPI struct { diff --git a/pkg/tsdb/loki/parse_query.go b/pkg/tsdb/loki/parse_query.go index 5e4434bfce0..b4b1f889088 100644 --- a/pkg/tsdb/loki/parse_query.go +++ b/pkg/tsdb/loki/parse_query.go @@ -8,6 +8,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/tsdb/intervalv2" ) From d6d40975671f44580c5b71d5b997c2968e2d30ab Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Mon, 30 Jan 2023 09:55:35 +0100 Subject: [PATCH 083/117] Chore: Fix goimports grouping in alerting (#62424) * fix goimports * fix goimports order --- .../alerting/conditions/evaluator_test.go | 3 ++- pkg/services/alerting/conditions/query.go | 7 +++---- .../conditions/query_interval_test.go | 4 ++-- .../alerting/conditions/query_test.go | 20 +++++++++---------- pkg/services/alerting/conditions/reducer.go | 1 - .../alerting/conditions/reducer_test.go | 3 ++- .../alerting/engine_integration_test.go | 3 ++- pkg/services/alerting/eval_handler_test.go | 4 ++-- pkg/services/alerting/notifiers/sensu_test.go | 4 ++-- pkg/services/ngalert/api/api_configuration.go | 4 ++-- .../ngalert/api/api_configuration_test.go | 3 ++- pkg/services/ngalert/api/api_prometheus.go | 4 ++-- .../ngalert/api/api_prometheus_test.go | 2 +- pkg/services/ngalert/api/api_ruler.go | 15 +++++++------- pkg/services/ngalert/api/lotex_ruler.go | 6 +++--- pkg/services/ngalert/api/promql_compat.go | 2 +- pkg/services/ngalert/api/testing.go | 4 ++-- .../definitions/provisioning_alert_rules.go | 4 ++-- .../definitions/provisioning_mute_timings.go | 3 ++- .../api/tooling/definitions/testing.go | 3 +-- pkg/services/ngalert/eval/eval.go | 6 +++--- .../eval/eval_mocks/ConditionEvaluator.go | 6 ++---- pkg/services/ngalert/eval/extract_md.go | 1 + pkg/services/ngalert/eval/extract_md_test.go | 3 ++- pkg/services/ngalert/image/cache_mock.go | 1 + pkg/services/ngalert/metrics/scheduler.go | 3 ++- pkg/services/ngalert/metrics/util.go | 6 ++---- pkg/services/ngalert/models/alert_rule.go | 1 - pkg/services/ngalert/notifier/alertmanager.go | 4 ++-- .../ngalert/notifier/alertmanager_test.go | 5 ++--- pkg/services/ngalert/notifier/config_test.go | 3 ++- .../ngalert/notifier/multiorg_alertmanager.go | 10 +++++----- .../notifier/multiorg_alertmanager_test.go | 8 ++++---- pkg/services/ngalert/notifier/receivers.go | 6 +++--- .../ngalert/notifier/receivers_test.go | 1 - .../ngalert/provisioning/alert_rules_test.go | 4 ++-- .../ngalert/provisioning/mute_timings.go | 3 ++- .../ngalert/provisioning/mute_timings_test.go | 7 ++++--- .../notification_policies_test.go | 9 +++++---- .../ngalert/provisioning/persist_mock.go | 4 ++-- .../provisioning/provisioning_store_mock.go | 4 ++-- .../provisioning/quota_checker_mock.go | 3 ++- .../ngalert/provisioning/templates_test.go | 5 +++-- pkg/services/ngalert/provisioning/testing.go | 3 ++- .../ngalert/schedule/alerts_sender_mock.go | 2 +- pkg/services/ngalert/schedule/compat.go | 3 +-- pkg/services/ngalert/schedule/compat_test.go | 3 +-- pkg/services/ngalert/schedule/schedule.go | 8 +++----- .../ngalert/schedule/schedule_mock.go | 4 ++-- pkg/services/ngalert/sender/router_test.go | 2 +- pkg/services/ngalert/sender/sender.go | 5 +++-- .../ngalert/state/historian/annotation.go | 1 + .../state/historian/annotation_test.go | 3 ++- .../ngalert/state/historian/core_test.go | 3 +-- .../ngalert/state/historian/dashboard.go | 5 +++-- .../ngalert/state/historian/dashboard_test.go | 3 ++- pkg/services/ngalert/state/historian/loki.go | 1 + .../ngalert/state/historian/loki_http_test.go | 3 ++- .../state/historian/model/rule_test.go | 3 ++- pkg/services/ngalert/state/historian/query.go | 1 + pkg/services/ngalert/state/historian/sql.go | 1 + pkg/services/ngalert/state/image_mock.go | 1 + .../store/admin_configuration_store_mock.go | 3 ++- pkg/services/ngalert/store/deltas_test.go | 7 ++++--- .../ngalert/store/provisioning_store_test.go | 3 ++- .../migrations/ualert/alert_rule_test.go | 4 ++-- .../sqlstore/migrations/ualert/ualert.go | 3 +-- .../api_alertmanager_configuration_test.go | 5 +++-- .../api/alerting/api_provisioning_test.go | 3 ++- 69 files changed, 149 insertions(+), 136 deletions(-) diff --git a/pkg/services/alerting/conditions/evaluator_test.go b/pkg/services/alerting/conditions/evaluator_test.go index f7188f8a49c..937c0527525 100644 --- a/pkg/services/alerting/conditions/evaluator_test.go +++ b/pkg/services/alerting/conditions/evaluator_test.go @@ -3,9 +3,10 @@ package conditions import ( "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/components/null" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/stretchr/testify/require" ) func evaluatorScenario(t *testing.T, json string, reducedValue float64, datapoints ...float64) bool { diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go index a51c9d3b6ed..d16a39d5fa1 100644 --- a/pkg/services/alerting/conditions/query.go +++ b/pkg/services/alerting/conditions/query.go @@ -7,10 +7,6 @@ import ( "strings" "time" - "github.com/grafana/grafana/pkg/tsdb/legacydata" - "github.com/grafana/grafana/pkg/tsdb/legacydata/interval" - "github.com/grafana/grafana/pkg/tsdb/prometheus" - "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/components/null" @@ -18,6 +14,9 @@ import ( "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/datasources" ngalertmodels "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/tsdb/legacydata" + "github.com/grafana/grafana/pkg/tsdb/legacydata/interval" + "github.com/grafana/grafana/pkg/tsdb/prometheus" ) func init() { diff --git a/pkg/services/alerting/conditions/query_interval_test.go b/pkg/services/alerting/conditions/query_interval_test.go index 7fe1f7177fa..9a42504f7f2 100644 --- a/pkg/services/alerting/conditions/query_interval_test.go +++ b/pkg/services/alerting/conditions/query_interval_test.go @@ -4,6 +4,8 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/localcache" @@ -14,8 +16,6 @@ import ( "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/intervalv2" "github.com/grafana/grafana/pkg/tsdb/legacydata" - - "github.com/stretchr/testify/require" ) func TestQueryInterval(t *testing.T) { diff --git a/pkg/services/alerting/conditions/query_test.go b/pkg/services/alerting/conditions/query_test.go index 3def8a5042a..43baf96826a 100644 --- a/pkg/services/alerting/conditions/query_test.go +++ b/pkg/services/alerting/conditions/query_test.go @@ -6,24 +6,22 @@ import ( "testing" "time" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/stretchr/testify/require" + "github.com/xorcare/pointer" + + "github.com/grafana/grafana/pkg/components/null" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/localcache" + "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/datasources" fd "github.com/grafana/grafana/pkg/services/datasources/fakes" "github.com/grafana/grafana/pkg/services/validations" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/legacydata" - - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "github.com/grafana/grafana-plugin-sdk-go/data" - - "github.com/grafana/grafana/pkg/components/null" - "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/services/alerting" - - "github.com/stretchr/testify/require" - "github.com/xorcare/pointer" ) func newTimeSeriesPointsFromArgs(values ...float64) legacydata.DataTimeSeriesPoints { diff --git a/pkg/services/alerting/conditions/reducer.go b/pkg/services/alerting/conditions/reducer.go index 28d2795e267..6fb986cb5aa 100644 --- a/pkg/services/alerting/conditions/reducer.go +++ b/pkg/services/alerting/conditions/reducer.go @@ -2,7 +2,6 @@ package conditions import ( "math" - "sort" "github.com/grafana/grafana/pkg/components/null" diff --git a/pkg/services/alerting/conditions/reducer_test.go b/pkg/services/alerting/conditions/reducer_test.go index 9542953306c..c357a2e9c4b 100644 --- a/pkg/services/alerting/conditions/reducer_test.go +++ b/pkg/services/alerting/conditions/reducer_test.go @@ -4,9 +4,10 @@ import ( "math" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/components/null" "github.com/grafana/grafana/pkg/tsdb/legacydata" - "github.com/stretchr/testify/require" ) func TestSimpleReducer(t *testing.T) { diff --git a/pkg/services/alerting/engine_integration_test.go b/pkg/services/alerting/engine_integration_test.go index 6daf0c29b43..519157b195e 100644 --- a/pkg/services/alerting/engine_integration_test.go +++ b/pkg/services/alerting/engine_integration_test.go @@ -9,6 +9,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/infra/usagestats" @@ -17,7 +19,6 @@ import ( encryptionprovider "github.com/grafana/grafana/pkg/services/encryption/provider" encryptionservice "github.com/grafana/grafana/pkg/services/encryption/service" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/require" ) func TestIntegrationEngineTimeouts(t *testing.T) { diff --git a/pkg/services/alerting/eval_handler_test.go b/pkg/services/alerting/eval_handler_test.go index 67baf26cab9..df12cd335e2 100644 --- a/pkg/services/alerting/eval_handler_test.go +++ b/pkg/services/alerting/eval_handler_test.go @@ -4,11 +4,11 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/annotations/annotationstest" "github.com/grafana/grafana/pkg/services/validations" "github.com/grafana/grafana/pkg/tsdb/legacydata" - - "github.com/stretchr/testify/require" ) type conditionStub struct { diff --git a/pkg/services/alerting/notifiers/sensu_test.go b/pkg/services/alerting/notifiers/sensu_test.go index 78073b54988..5b6503f798f 100644 --- a/pkg/services/alerting/notifiers/sensu_test.go +++ b/pkg/services/alerting/notifiers/sensu_test.go @@ -3,11 +3,11 @@ package notifiers import ( "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/alerting/models" encryptionservice "github.com/grafana/grafana/pkg/services/encryption/service" - - "github.com/stretchr/testify/require" ) func TestSensuNotifier(t *testing.T) { diff --git a/pkg/services/ngalert/api/api_configuration.go b/pkg/services/ngalert/api/api_configuration.go index 9ffb08c4ff5..cbf2cf16ba1 100644 --- a/pkg/services/ngalert/api/api_configuration.go +++ b/pkg/services/ngalert/api/api_configuration.go @@ -6,6 +6,8 @@ import ( "fmt" "net/http" + v1 "github.com/prometheus/client_golang/api/prometheus/v1" + "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" @@ -15,8 +17,6 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/util" - - v1 "github.com/prometheus/client_golang/api/prometheus/v1" ) type ConfigSrv struct { diff --git a/pkg/services/ngalert/api/api_configuration_test.go b/pkg/services/ngalert/api/api_configuration_test.go index df24489f311..f4470d93e17 100644 --- a/pkg/services/ngalert/api/api_configuration_test.go +++ b/pkg/services/ngalert/api/api_configuration_test.go @@ -5,13 +5,14 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/datasources" fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/org" - "github.com/stretchr/testify/require" ) func TestExternalAlertmanagerChoice(t *testing.T) { diff --git a/pkg/services/ngalert/api/api_prometheus.go b/pkg/services/ngalert/api/api_prometheus.go index 7c1a682e9fe..3c9a1350640 100644 --- a/pkg/services/ngalert/api/api_prometheus.go +++ b/pkg/services/ngalert/api/api_prometheus.go @@ -10,6 +10,8 @@ import ( "strings" "time" + apiv1 "github.com/prometheus/client_golang/api/prometheus/v1" + "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -19,8 +21,6 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/eval" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/state" - - apiv1 "github.com/prometheus/client_golang/api/prometheus/v1" ) type PrometheusSrv struct { diff --git a/pkg/services/ngalert/api/api_prometheus_test.go b/pkg/services/ngalert/api/api_prometheus_test.go index 0014b331aa1..9d7599027a0 100644 --- a/pkg/services/ngalert/api/api_prometheus_test.go +++ b/pkg/services/ngalert/api/api_prometheus_test.go @@ -9,11 +9,11 @@ import ( "testing" "time" + alertingModels "github.com/grafana/alerting/alerting/models" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - alertingModels "github.com/grafana/alerting/alerting/models" "github.com/grafana/grafana/pkg/infra/log" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go index 732ec14dcdb..b467a3b4034 100644 --- a/pkg/services/ngalert/api/api_ruler.go +++ b/pkg/services/ngalert/api/api_ruler.go @@ -8,23 +8,22 @@ import ( "strings" "time" - "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/dashboards" - "github.com/grafana/grafana/pkg/services/ngalert/eval" - "github.com/grafana/grafana/pkg/services/ngalert/provisioning" - "github.com/grafana/grafana/pkg/services/ngalert/store" - "github.com/grafana/grafana/pkg/services/quota" - "github.com/grafana/grafana/pkg/setting" - "github.com/prometheus/common/model" "github.com/grafana/grafana/pkg/api/apierrors" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/accesscontrol" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/dashboards" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/ngalert/eval" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/provisioning" "github.com/grafana/grafana/pkg/services/ngalert/schedule" + "github.com/grafana/grafana/pkg/services/ngalert/store" + "github.com/grafana/grafana/pkg/services/quota" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) diff --git a/pkg/services/ngalert/api/lotex_ruler.go b/pkg/services/ngalert/api/lotex_ruler.go index 9f94a9be7ad..5a6a3905cc2 100644 --- a/pkg/services/ngalert/api/lotex_ruler.go +++ b/pkg/services/ngalert/api/lotex_ruler.go @@ -6,13 +6,13 @@ import ( "net/http" "net/url" - contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" - apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" - "github.com/grafana/grafana/pkg/web" "gopkg.in/yaml.v3" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/web" ) const ( diff --git a/pkg/services/ngalert/api/promql_compat.go b/pkg/services/ngalert/api/promql_compat.go index 889771ccb23..0b4cb2b621b 100644 --- a/pkg/services/ngalert/api/promql_compat.go +++ b/pkg/services/ngalert/api/promql_compat.go @@ -9,12 +9,12 @@ import ( cortex_util "github.com/cortexproject/cortex/pkg/util" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/prometheus/prometheus/pkg/labels" "github.com/prometheus/prometheus/promql" "github.com/prometheus/prometheus/promql/parser" "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/util" ) diff --git a/pkg/services/ngalert/api/testing.go b/pkg/services/ngalert/api/testing.go index 68504135781..d2a72db8529 100644 --- a/pkg/services/ngalert/api/testing.go +++ b/pkg/services/ngalert/api/testing.go @@ -6,10 +6,10 @@ import ( "testing" "time" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/state" - - "github.com/grafana/grafana-plugin-sdk-go/data" ) type fakeAlertInstanceManager struct { diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go index 0e51a514bbf..74929e8b362 100644 --- a/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go @@ -3,10 +3,10 @@ package definitions import ( "time" + "github.com/prometheus/common/model" + "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/provisioning/alerting/file" - - "github.com/prometheus/common/model" ) // swagger:route GET /api/v1/provisioning/alert-rules provisioning stable RouteGetAlertRules diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_mute_timings.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_mute_timings.go index 9331d72b484..99d15fd2989 100644 --- a/pkg/services/ngalert/api/tooling/definitions/provisioning_mute_timings.go +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_mute_timings.go @@ -1,8 +1,9 @@ package definitions import ( - "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/prometheus/alertmanager/config" + + "github.com/grafana/grafana/pkg/services/ngalert/models" ) // swagger:route GET /api/v1/provisioning/mute-timings provisioning stable RouteGetMuteTimings diff --git a/pkg/services/ngalert/api/tooling/definitions/testing.go b/pkg/services/ngalert/api/tooling/definitions/testing.go index 4195d5fd3f7..2fb3738e4de 100644 --- a/pkg/services/ngalert/api/tooling/definitions/testing.go +++ b/pkg/services/ngalert/api/tooling/definitions/testing.go @@ -7,9 +7,8 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/prometheus/common/model" - "github.com/prometheus/alertmanager/config" + "github.com/prometheus/common/model" "github.com/prometheus/prometheus/promql" "github.com/grafana/grafana/pkg/services/ngalert/models" diff --git a/pkg/services/ngalert/eval/eval.go b/pkg/services/ngalert/eval/eval.go index 41f2a5ecddc..f4f7d6733f4 100644 --- a/pkg/services/ngalert/eval/eval.go +++ b/pkg/services/ngalert/eval/eval.go @@ -12,6 +12,9 @@ import ( "strings" "time" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/expr/classic" "github.com/grafana/grafana/pkg/infra/log" @@ -19,9 +22,6 @@ import ( "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/setting" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana-plugin-sdk-go/data" ) var logger = log.New("ngalert.eval") diff --git a/pkg/services/ngalert/eval/eval_mocks/ConditionEvaluator.go b/pkg/services/ngalert/eval/eval_mocks/ConditionEvaluator.go index 3d82a79a5e4..be2fd170f69 100644 --- a/pkg/services/ngalert/eval/eval_mocks/ConditionEvaluator.go +++ b/pkg/services/ngalert/eval/eval_mocks/ConditionEvaluator.go @@ -4,14 +4,12 @@ package eval_mocks import ( context "context" + time "time" backend "github.com/grafana/grafana-plugin-sdk-go/backend" - - eval "github.com/grafana/grafana/pkg/services/ngalert/eval" - mock "github.com/stretchr/testify/mock" - time "time" + eval "github.com/grafana/grafana/pkg/services/ngalert/eval" ) // ConditionEvaluatorMock is an autogenerated mock type for the ConditionEvaluator type diff --git a/pkg/services/ngalert/eval/extract_md.go b/pkg/services/ngalert/eval/extract_md.go index e08a7b22bbc..263e5c2a922 100644 --- a/pkg/services/ngalert/eval/extract_md.go +++ b/pkg/services/ngalert/eval/extract_md.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/expr/classic" ) diff --git a/pkg/services/ngalert/eval/extract_md_test.go b/pkg/services/ngalert/eval/extract_md_test.go index 3a1ef7849a9..6951a8caa15 100644 --- a/pkg/services/ngalert/eval/extract_md_test.go +++ b/pkg/services/ngalert/eval/extract_md_test.go @@ -4,9 +4,10 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/expr/classic" "github.com/stretchr/testify/require" ptr "github.com/xorcare/pointer" + + "github.com/grafana/grafana/pkg/expr/classic" ) func TestExtractEvalString(t *testing.T) { diff --git a/pkg/services/ngalert/image/cache_mock.go b/pkg/services/ngalert/image/cache_mock.go index 655e43fe241..3ff0a3237b6 100644 --- a/pkg/services/ngalert/image/cache_mock.go +++ b/pkg/services/ngalert/image/cache_mock.go @@ -9,6 +9,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" + models "github.com/grafana/grafana/pkg/services/ngalert/models" ) diff --git a/pkg/services/ngalert/metrics/scheduler.go b/pkg/services/ngalert/metrics/scheduler.go index fb1523918a3..4cd844b1bef 100644 --- a/pkg/services/ngalert/metrics/scheduler.go +++ b/pkg/services/ngalert/metrics/scheduler.go @@ -1,9 +1,10 @@ package metrics import ( - "github.com/grafana/grafana/pkg/util/ticker" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" + + "github.com/grafana/grafana/pkg/util/ticker" ) type Scheduler struct { diff --git a/pkg/services/ngalert/metrics/util.go b/pkg/services/ngalert/metrics/util.go index db65e1e2b87..eaa82e9c8ae 100644 --- a/pkg/services/ngalert/metrics/util.go +++ b/pkg/services/ngalert/metrics/util.go @@ -7,14 +7,12 @@ import ( "sync" "time" - "github.com/grafana/grafana/pkg/web" - - "github.com/grafana/grafana/pkg/api/response" - "github.com/prometheus/client_golang/prometheus" + "github.com/grafana/grafana/pkg/api/response" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/web" ) // OrgRegistries represents a map of registries per org. diff --git a/pkg/services/ngalert/models/alert_rule.go b/pkg/services/ngalert/models/alert_rule.go index f7d58293b26..5e96c82cbca 100644 --- a/pkg/services/ngalert/models/alert_rule.go +++ b/pkg/services/ngalert/models/alert_rule.go @@ -11,7 +11,6 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - alertingModels "github.com/grafana/alerting/alerting/models" "github.com/grafana/grafana/pkg/services/quota" diff --git a/pkg/services/ngalert/notifier/alertmanager.go b/pkg/services/ngalert/notifier/alertmanager.go index 05b83bfc284..f6aca475f63 100644 --- a/pkg/services/ngalert/notifier/alertmanager.go +++ b/pkg/services/ngalert/notifier/alertmanager.go @@ -13,6 +13,8 @@ import ( "github.com/grafana/alerting/alerting" "github.com/grafana/alerting/alerting/notifier/channels" + amv2 "github.com/prometheus/alertmanager/api/v2/models" + "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/log" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" @@ -22,8 +24,6 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" - - amv2 "github.com/prometheus/alertmanager/api/v2/models" ) const ( diff --git a/pkg/services/ngalert/notifier/alertmanager_test.go b/pkg/services/ngalert/notifier/alertmanager_test.go index 889de070c0c..b2a0408745f 100644 --- a/pkg/services/ngalert/notifier/alertmanager_test.go +++ b/pkg/services/ngalert/notifier/alertmanager_test.go @@ -5,16 +5,15 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/services/dashboards" - "github.com/grafana/grafana/pkg/services/secrets/database" - "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/services/ngalert/store" + "github.com/grafana/grafana/pkg/services/secrets/database" secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/grafana/grafana/pkg/setting" ) diff --git a/pkg/services/ngalert/notifier/config_test.go b/pkg/services/ngalert/notifier/config_test.go index df04d6fc4b3..5650ffb3471 100644 --- a/pkg/services/ngalert/notifier/config_test.go +++ b/pkg/services/ngalert/notifier/config_test.go @@ -6,9 +6,10 @@ import ( "path/filepath" "testing" - api "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + api "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ) func TestPersistTemplates(t *testing.T) { diff --git a/pkg/services/ngalert/notifier/multiorg_alertmanager.go b/pkg/services/ngalert/notifier/multiorg_alertmanager.go index 785f5779d02..7757c0220b0 100644 --- a/pkg/services/ngalert/notifier/multiorg_alertmanager.go +++ b/pkg/services/ngalert/notifier/multiorg_alertmanager.go @@ -9,6 +9,11 @@ import ( "sync" "time" + "github.com/grafana/alerting/alerting" + "github.com/grafana/alerting/alerting/notifier/channels" + "github.com/prometheus/alertmanager/cluster" + "github.com/prometheus/client_golang/prometheus" + "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/metrics" @@ -18,11 +23,6 @@ import ( "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/setting" - - "github.com/grafana/alerting/alerting" - "github.com/grafana/alerting/alerting/notifier/channels" - "github.com/prometheus/alertmanager/cluster" - "github.com/prometheus/client_golang/prometheus" ) var ( diff --git a/pkg/services/ngalert/notifier/multiorg_alertmanager_test.go b/pkg/services/ngalert/notifier/multiorg_alertmanager_test.go index 4011dd31cfe..521e980ea00 100644 --- a/pkg/services/ngalert/notifier/multiorg_alertmanager_test.go +++ b/pkg/services/ngalert/notifier/multiorg_alertmanager_test.go @@ -10,6 +10,10 @@ import ( "testing" "time" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/services/ngalert/models" @@ -17,10 +21,6 @@ import ( "github.com/grafana/grafana/pkg/services/secrets/fakes" secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/grafana/grafana/pkg/setting" - - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/testutil" - "github.com/stretchr/testify/require" ) func TestMultiOrgAlertmanager_SyncAlertmanagersForOrgs(t *testing.T) { diff --git a/pkg/services/ngalert/notifier/receivers.go b/pkg/services/ngalert/notifier/receivers.go index 2a964b1e5d0..7e56baaa64b 100644 --- a/pkg/services/ngalert/notifier/receivers.go +++ b/pkg/services/ngalert/notifier/receivers.go @@ -7,12 +7,12 @@ import ( "fmt" "time" - "github.com/grafana/alerting/alerting" - "github.com/go-openapi/strfmt" - apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/alerting/alerting" "github.com/prometheus/alertmanager/api/v2/models" "github.com/prometheus/alertmanager/types" + + apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ) var ( diff --git a/pkg/services/ngalert/notifier/receivers_test.go b/pkg/services/ngalert/notifier/receivers_test.go index 25d3195b2af..d9b923c5de9 100644 --- a/pkg/services/ngalert/notifier/receivers_test.go +++ b/pkg/services/ngalert/notifier/receivers_test.go @@ -7,7 +7,6 @@ import ( "testing" "github.com/grafana/alerting/alerting" - "github.com/stretchr/testify/require" ) diff --git a/pkg/services/ngalert/provisioning/alert_rules_test.go b/pkg/services/ngalert/provisioning/alert_rules_test.go index 43c37a2efbd..993930887b3 100644 --- a/pkg/services/ngalert/provisioning/alert_rules_test.go +++ b/pkg/services/ngalert/provisioning/alert_rules_test.go @@ -7,13 +7,13 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/setting" - - "github.com/stretchr/testify/require" ) func TestAlertRuleService(t *testing.T) { diff --git a/pkg/services/ngalert/provisioning/mute_timings.go b/pkg/services/ngalert/provisioning/mute_timings.go index a96e55f7c22..ef65d70dffe 100644 --- a/pkg/services/ngalert/provisioning/mute_timings.go +++ b/pkg/services/ngalert/provisioning/mute_timings.go @@ -4,10 +4,11 @@ import ( "context" "fmt" + "github.com/prometheus/alertmanager/config" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/prometheus/alertmanager/config" ) type MuteTimingService struct { diff --git a/pkg/services/ngalert/provisioning/mute_timings_test.go b/pkg/services/ngalert/provisioning/mute_timings_test.go index 84845a9ba53..3c1f2a09d70 100644 --- a/pkg/services/ngalert/provisioning/mute_timings_test.go +++ b/pkg/services/ngalert/provisioning/mute_timings_test.go @@ -5,12 +5,13 @@ import ( "fmt" "testing" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" - "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/prometheus/alertmanager/config" mock "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/ngalert/models" ) func TestMuteTimingService(t *testing.T) { diff --git a/pkg/services/ngalert/provisioning/notification_policies_test.go b/pkg/services/ngalert/provisioning/notification_policies_test.go index 11e551a20e2..5ffd39c06d9 100644 --- a/pkg/services/ngalert/provisioning/notification_policies_test.go +++ b/pkg/services/ngalert/provisioning/notification_policies_test.go @@ -4,15 +4,16 @@ import ( "context" "testing" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" - "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/setting" "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/timeinterval" "github.com/prometheus/common/model" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/setting" ) func TestNotificationPolicyService(t *testing.T) { diff --git a/pkg/services/ngalert/provisioning/persist_mock.go b/pkg/services/ngalert/provisioning/persist_mock.go index 7a0c1c1d0be..61726e40cec 100644 --- a/pkg/services/ngalert/provisioning/persist_mock.go +++ b/pkg/services/ngalert/provisioning/persist_mock.go @@ -4,11 +4,11 @@ package provisioning import ( context "context" + testing "testing" - models "github.com/grafana/grafana/pkg/services/ngalert/models" mock "github.com/stretchr/testify/mock" - testing "testing" + models "github.com/grafana/grafana/pkg/services/ngalert/models" ) // MockAMConfigStore is an autogenerated mock type for the AMConfigStore type diff --git a/pkg/services/ngalert/provisioning/provisioning_store_mock.go b/pkg/services/ngalert/provisioning/provisioning_store_mock.go index ebc65ef8857..da168e92ba4 100644 --- a/pkg/services/ngalert/provisioning/provisioning_store_mock.go +++ b/pkg/services/ngalert/provisioning/provisioning_store_mock.go @@ -4,11 +4,11 @@ package provisioning import ( context "context" + testing "testing" - models "github.com/grafana/grafana/pkg/services/ngalert/models" mock "github.com/stretchr/testify/mock" - testing "testing" + models "github.com/grafana/grafana/pkg/services/ngalert/models" ) // MockProvisioningStore is an autogenerated mock type for the ProvisioningStore type diff --git a/pkg/services/ngalert/provisioning/quota_checker_mock.go b/pkg/services/ngalert/provisioning/quota_checker_mock.go index fcdfbfdbaa0..8261378ee37 100644 --- a/pkg/services/ngalert/provisioning/quota_checker_mock.go +++ b/pkg/services/ngalert/provisioning/quota_checker_mock.go @@ -5,8 +5,9 @@ package provisioning import ( context "context" - quota "github.com/grafana/grafana/pkg/services/quota" mock "github.com/stretchr/testify/mock" + + quota "github.com/grafana/grafana/pkg/services/quota" ) // MockQuotaChecker is an autogenerated mock type for the QuotaChecker type diff --git a/pkg/services/ngalert/provisioning/templates_test.go b/pkg/services/ngalert/provisioning/templates_test.go index c9fa25785a1..49d01d8e221 100644 --- a/pkg/services/ngalert/provisioning/templates_test.go +++ b/pkg/services/ngalert/provisioning/templates_test.go @@ -5,12 +5,13 @@ import ( "fmt" "testing" + mock "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/setting" - mock "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" ) func TestTemplateService(t *testing.T) { diff --git a/pkg/services/ngalert/provisioning/testing.go b/pkg/services/ngalert/provisioning/testing.go index 3afb57daf87..53c34929a6a 100644 --- a/pkg/services/ngalert/provisioning/testing.go +++ b/pkg/services/ngalert/provisioning/testing.go @@ -6,8 +6,9 @@ import ( "fmt" "strings" - "github.com/grafana/grafana/pkg/services/ngalert/models" mock "github.com/stretchr/testify/mock" + + "github.com/grafana/grafana/pkg/services/ngalert/models" ) const defaultAlertmanagerConfigJSON = ` diff --git a/pkg/services/ngalert/schedule/alerts_sender_mock.go b/pkg/services/ngalert/schedule/alerts_sender_mock.go index 6f0a28eb122..cdf789a94d7 100644 --- a/pkg/services/ngalert/schedule/alerts_sender_mock.go +++ b/pkg/services/ngalert/schedule/alerts_sender_mock.go @@ -3,9 +3,9 @@ package schedule import ( - definitions "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" mock "github.com/stretchr/testify/mock" + definitions "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" models "github.com/grafana/grafana/pkg/services/ngalert/models" ) diff --git a/pkg/services/ngalert/schedule/compat.go b/pkg/services/ngalert/schedule/compat.go index a4754d72a22..282403bd4f7 100644 --- a/pkg/services/ngalert/schedule/compat.go +++ b/pkg/services/ngalert/schedule/compat.go @@ -10,12 +10,11 @@ import ( "github.com/benbjohnson/clock" "github.com/go-openapi/strfmt" + alertingModels "github.com/grafana/alerting/alerting/models" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/prometheus/alertmanager/api/v2/models" "github.com/prometheus/common/model" - alertingModels "github.com/grafana/alerting/alerting/models" - apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/eval" ngModels "github.com/grafana/grafana/pkg/services/ngalert/models" diff --git a/pkg/services/ngalert/schedule/compat_test.go b/pkg/services/ngalert/schedule/compat_test.go index 8443f9a4cc8..fa97ad58305 100644 --- a/pkg/services/ngalert/schedule/compat_test.go +++ b/pkg/services/ngalert/schedule/compat_test.go @@ -9,12 +9,11 @@ import ( "github.com/benbjohnson/clock" "github.com/go-openapi/strfmt" + alertingModels "github.com/grafana/alerting/alerting/models" "github.com/prometheus/alertmanager/api/v2/models" "github.com/prometheus/common/model" "github.com/stretchr/testify/require" - alertingModels "github.com/grafana/alerting/alerting/models" - "github.com/grafana/grafana/pkg/services/ngalert/eval" ngModels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/state" diff --git a/pkg/services/ngalert/schedule/schedule.go b/pkg/services/ngalert/schedule/schedule.go index be4d3f43137..8cce260d003 100644 --- a/pkg/services/ngalert/schedule/schedule.go +++ b/pkg/services/ngalert/schedule/schedule.go @@ -7,11 +7,12 @@ import ( "net/url" "time" + "github.com/benbjohnson/clock" + alertingModels "github.com/grafana/alerting/alerting/models" "github.com/hashicorp/go-multierror" prometheusModel "github.com/prometheus/common/model" "go.opentelemetry.io/otel/attribute" - - alertingModels "github.com/grafana/alerting/alerting/models" + "golang.org/x/sync/errgroup" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" @@ -24,9 +25,6 @@ import ( "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util/ticker" - - "github.com/benbjohnson/clock" - "golang.org/x/sync/errgroup" ) // ScheduleService is an interface for a service that schedules the evaluation diff --git a/pkg/services/ngalert/schedule/schedule_mock.go b/pkg/services/ngalert/schedule/schedule_mock.go index 9a950779f32..d9b1a9ac723 100644 --- a/pkg/services/ngalert/schedule/schedule_mock.go +++ b/pkg/services/ngalert/schedule/schedule_mock.go @@ -4,11 +4,11 @@ package schedule import ( context "context" + time "time" - models "github.com/grafana/grafana/pkg/services/ngalert/models" mock "github.com/stretchr/testify/mock" - time "time" + models "github.com/grafana/grafana/pkg/services/ngalert/models" ) // FakeScheduleService is an autogenerated mock type for the ScheduleService type diff --git a/pkg/services/ngalert/sender/router_test.go b/pkg/services/ngalert/sender/router_test.go index 7fd6376d8e9..daca9986768 100644 --- a/pkg/services/ngalert/sender/router_test.go +++ b/pkg/services/ngalert/sender/router_test.go @@ -10,13 +10,13 @@ import ( "github.com/benbjohnson/clock" "github.com/go-openapi/strfmt" - "github.com/grafana/grafana/pkg/infra/log/logtest" models2 "github.com/prometheus/alertmanager/api/v2/models" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/log/logtest" "github.com/grafana/grafana/pkg/services/datasources" fake_ds "github.com/grafana/grafana/pkg/services/datasources/fakes" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" diff --git a/pkg/services/ngalert/sender/sender.go b/pkg/services/ngalert/sender/sender.go index 31ba8c2b3e2..2d3b25b5a5b 100644 --- a/pkg/services/ngalert/sender/sender.go +++ b/pkg/services/ngalert/sender/sender.go @@ -12,8 +12,6 @@ import ( "time" "unicode" - "github.com/grafana/grafana/pkg/infra/log" - apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/prometheus/alertmanager/api/v2/models" "github.com/prometheus/client_golang/prometheus" common_config "github.com/prometheus/common/config" @@ -22,6 +20,9 @@ import ( "github.com/prometheus/prometheus/discovery" "github.com/prometheus/prometheus/notifier" "github.com/prometheus/prometheus/pkg/labels" + + "github.com/grafana/grafana/pkg/infra/log" + apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ) const ( diff --git a/pkg/services/ngalert/state/historian/annotation.go b/pkg/services/ngalert/state/historian/annotation.go index 188c9010371..bca76460e78 100644 --- a/pkg/services/ngalert/state/historian/annotation.go +++ b/pkg/services/ngalert/state/historian/annotation.go @@ -9,6 +9,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/annotations" diff --git a/pkg/services/ngalert/state/historian/annotation_test.go b/pkg/services/ngalert/state/historian/annotation_test.go index 25497a6d6b6..e94869c9d5a 100644 --- a/pkg/services/ngalert/state/historian/annotation_test.go +++ b/pkg/services/ngalert/state/historian/annotation_test.go @@ -5,6 +5,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/annotations" @@ -12,7 +14,6 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" - "github.com/stretchr/testify/require" ) func TestAnnotationHistorian_Integration(t *testing.T) { diff --git a/pkg/services/ngalert/state/historian/core_test.go b/pkg/services/ngalert/state/historian/core_test.go index b2f3f522d3c..d88bb192721 100644 --- a/pkg/services/ngalert/state/historian/core_test.go +++ b/pkg/services/ngalert/state/historian/core_test.go @@ -4,9 +4,8 @@ import ( "fmt" "testing" - "github.com/stretchr/testify/require" - "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/models" diff --git a/pkg/services/ngalert/state/historian/dashboard.go b/pkg/services/ngalert/state/historian/dashboard.go index 74b097298e2..e5e4b9f2d34 100644 --- a/pkg/services/ngalert/state/historian/dashboard.go +++ b/pkg/services/ngalert/state/historian/dashboard.go @@ -7,10 +7,11 @@ import ( "strconv" "time" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/services/dashboards" "github.com/patrickmn/go-cache" "golang.org/x/sync/singleflight" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/dashboards" ) const ( diff --git a/pkg/services/ngalert/state/historian/dashboard_test.go b/pkg/services/ngalert/state/historian/dashboard_test.go index f455ef0f70d..34410851043 100644 --- a/pkg/services/ngalert/state/historian/dashboard_test.go +++ b/pkg/services/ngalert/state/historian/dashboard_test.go @@ -5,9 +5,10 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/services/dashboards" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/dashboards" ) func TestDashboardResolver(t *testing.T) { diff --git a/pkg/services/ngalert/state/historian/loki.go b/pkg/services/ngalert/state/historian/loki.go index 466fc6137c4..f3bd9f5661d 100644 --- a/pkg/services/ngalert/state/historian/loki.go +++ b/pkg/services/ngalert/state/historian/loki.go @@ -7,6 +7,7 @@ import ( "sort" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/eval" diff --git a/pkg/services/ngalert/state/historian/loki_http_test.go b/pkg/services/ngalert/state/historian/loki_http_test.go index f9ec280e4f1..e5834dbc31a 100644 --- a/pkg/services/ngalert/state/historian/loki_http_test.go +++ b/pkg/services/ngalert/state/historian/loki_http_test.go @@ -5,8 +5,9 @@ import ( "net/url" "testing" - "github.com/grafana/grafana/pkg/infra/log" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/log" ) // This function can be used for local testing, just remove the skip call. diff --git a/pkg/services/ngalert/state/historian/model/rule_test.go b/pkg/services/ngalert/state/historian/model/rule_test.go index 228e9537307..df86f032389 100644 --- a/pkg/services/ngalert/state/historian/model/rule_test.go +++ b/pkg/services/ngalert/state/historian/model/rule_test.go @@ -3,9 +3,10 @@ package model import ( "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/stretchr/testify/require" ) func TestNewRuleMeta(t *testing.T) { diff --git a/pkg/services/ngalert/state/historian/query.go b/pkg/services/ngalert/state/historian/query.go index 26849c88a5f..2648851d9fe 100644 --- a/pkg/services/ngalert/state/historian/query.go +++ b/pkg/services/ngalert/state/historian/query.go @@ -4,6 +4,7 @@ import ( "context" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/services/ngalert/models" ) diff --git a/pkg/services/ngalert/state/historian/sql.go b/pkg/services/ngalert/state/historian/sql.go index 64e716204b8..8dc1b435e9a 100644 --- a/pkg/services/ngalert/state/historian/sql.go +++ b/pkg/services/ngalert/state/historian/sql.go @@ -4,6 +4,7 @@ import ( "context" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/state" diff --git a/pkg/services/ngalert/state/image_mock.go b/pkg/services/ngalert/state/image_mock.go index 62913c5c05c..f2cc71e5204 100644 --- a/pkg/services/ngalert/state/image_mock.go +++ b/pkg/services/ngalert/state/image_mock.go @@ -9,6 +9,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" + models "github.com/grafana/grafana/pkg/services/ngalert/models" ) diff --git a/pkg/services/ngalert/store/admin_configuration_store_mock.go b/pkg/services/ngalert/store/admin_configuration_store_mock.go index 755f16a08af..f57d00592d8 100644 --- a/pkg/services/ngalert/store/admin_configuration_store_mock.go +++ b/pkg/services/ngalert/store/admin_configuration_store_mock.go @@ -3,8 +3,9 @@ package store import ( - models "github.com/grafana/grafana/pkg/services/ngalert/models" mock "github.com/stretchr/testify/mock" + + models "github.com/grafana/grafana/pkg/services/ngalert/models" ) // AdminConfigurationStoreMock is an autogenerated mock type for the AdminConfigurationStore type diff --git a/pkg/services/ngalert/store/deltas_test.go b/pkg/services/ngalert/store/deltas_test.go index e9bc7b947ba..f802e3509e6 100644 --- a/pkg/services/ngalert/store/deltas_test.go +++ b/pkg/services/ngalert/store/deltas_test.go @@ -7,13 +7,14 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/exp/rand" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" "github.com/grafana/grafana/pkg/util" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "golang.org/x/exp/rand" ) func TestCalculateChanges(t *testing.T) { diff --git a/pkg/services/ngalert/store/provisioning_store_test.go b/pkg/services/ngalert/store/provisioning_store_test.go index e09a9bde51a..568f870027d 100644 --- a/pkg/services/ngalert/store/provisioning_store_test.go +++ b/pkg/services/ngalert/store/provisioning_store_test.go @@ -4,12 +4,13 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/ngalert" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/provisioning" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/ngalert/tests" - "github.com/stretchr/testify/require" ) const testAlertingIntervalSeconds = 10 diff --git a/pkg/services/sqlstore/migrations/ualert/alert_rule_test.go b/pkg/services/sqlstore/migrations/ualert/alert_rule_test.go index 5e589993cf1..64896d24666 100644 --- a/pkg/services/sqlstore/migrations/ualert/alert_rule_test.go +++ b/pkg/services/sqlstore/migrations/ualert/alert_rule_test.go @@ -5,9 +5,9 @@ import ( "strings" "testing" - "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/components/simplejson" ) func TestMigrateAlertRuleQueries(t *testing.T) { diff --git a/pkg/services/sqlstore/migrations/ualert/ualert.go b/pkg/services/sqlstore/migrations/ualert/ualert.go index 29765e1ccd1..eb319f03787 100644 --- a/pkg/services/sqlstore/migrations/ualert/ualert.go +++ b/pkg/services/sqlstore/migrations/ualert/ualert.go @@ -12,11 +12,10 @@ import ( "strings" "time" + "github.com/grafana/alerting/alerting/notifier/channels" pb "github.com/prometheus/alertmanager/silence/silencepb" "xorm.io/xorm" - "github.com/grafana/alerting/alerting/notifier/channels" - ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/notifier/channels_config" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" diff --git a/pkg/tests/api/alerting/api_alertmanager_configuration_test.go b/pkg/tests/api/alerting/api_alertmanager_configuration_test.go index 59b06869c94..e6eb902872c 100644 --- a/pkg/tests/api/alerting/api_alertmanager_configuration_test.go +++ b/pkg/tests/api/alerting/api_alertmanager_configuration_test.go @@ -10,14 +10,15 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestIntegrationAlertmanagerConfigurationIsTransactional(t *testing.T) { diff --git a/pkg/tests/api/alerting/api_provisioning_test.go b/pkg/tests/api/alerting/api_provisioning_test.go index ce115c647ee..4bef914bb2a 100644 --- a/pkg/tests/api/alerting/api_provisioning_test.go +++ b/pkg/tests/api/alerting/api_provisioning_test.go @@ -7,10 +7,11 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" - "github.com/stretchr/testify/require" ) func TestIntegrationProvisioning(t *testing.T) { From 907e2a840e817717f785f16038a6b94b6544a9c7 Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Mon, 30 Jan 2023 09:57:50 +0100 Subject: [PATCH 084/117] Chore: Fix goimports grouping (#62429) * fix goimports ordering * fix goimports order --- pkg/infra/filestorage/filter.go | 1 + pkg/services/live/database/tests/storage_test.go | 3 ++- pkg/services/live/features/broadcast.go | 4 ++-- pkg/services/live/features/broadcast_mock.go | 1 + pkg/services/live/features/broadcast_test.go | 3 ++- pkg/services/live/features/comment.go | 4 ++-- pkg/services/live/features/plugin.go | 6 +++--- pkg/services/live/features/plugin_mock.go | 1 + pkg/services/live/live.go | 16 ++++++++-------- pkg/services/live/live_test.go | 4 ++-- pkg/services/live/liveplugin/plugin.go | 6 +++--- .../live/managedstream/cache_memory_test.go | 1 - pkg/services/live/managedstream/cache_redis.go | 4 ++-- pkg/services/live/managedstream/runner.go | 8 ++++---- pkg/services/live/pipeline/config.go | 1 + .../live/pipeline/data_output_builtin.go | 4 ++-- .../pipeline/data_output_local_subscribers.go | 4 ++-- pkg/services/live/pipeline/devdata.go | 4 ++-- .../pipeline/frame_output_local_subscribers.go | 4 ++-- .../live/pipeline/frame_output_managed_stream.go | 4 ++-- .../live/pipeline/frame_output_remote_write.go | 3 ++- .../pipeline/frame_output_remote_write_test.go | 3 +-- pkg/services/live/pipeline/frame_storage.go | 4 ++-- pkg/services/live/pipeline/pipeline.go | 6 +++--- pkg/services/live/pipeline/pipeline_test.go | 1 - .../live/pipeline/rule_builder_storage.go | 1 + pkg/services/live/pipeline/subscribe_builtin.go | 6 +++--- .../live/pipeline/subscribe_managed_stream.go | 4 ++-- pkg/services/live/pipeline/subscribe_multiple.go | 1 + pkg/services/live/pushhttp/push.go | 4 ++-- pkg/services/live/pushws/push_pipeline.go | 4 ++-- pkg/services/live/pushws/push_stream.go | 6 +++--- pkg/services/live/runstream/manager.go | 4 ++-- pkg/services/live/runstream/manager_test.go | 3 ++- pkg/services/live/runstream/mock.go | 1 + pkg/services/live/survey/survey.go | 1 + pkg/services/live/telemetry/telegraf/convert.go | 3 ++- pkg/services/querylibrary/tests/common.go | 3 ++- pkg/services/searchV2/allowed_actions.go | 1 + pkg/services/searchV2/http.go | 3 ++- pkg/services/searchV2/index_test.go | 7 +++---- pkg/services/searchV2/queries.go | 1 + pkg/services/searchV2/search_service_mock.go | 1 - pkg/services/searchV2/service.go | 3 +-- pkg/services/searchV2/service_bench_test.go | 4 ++-- pkg/services/searchV2/stub.go | 1 + pkg/services/searchV2/types.go | 4 ++-- pkg/services/searchV2/usage.go | 5 +++-- pkg/services/store/entity/entity.pb.go | 5 +++-- pkg/services/store/entity/entity_grpc.pb.go | 1 + .../store/entity/httpentitystore/service.go | 5 ++--- pkg/services/store/entity/tests/common.go | 7 ++++--- pkg/services/store/k8saccess/client.go | 5 +++-- pkg/services/store/k8saccess/service.go | 7 ++++--- pkg/services/store/kind/playlist/summary_test.go | 3 ++- pkg/services/store/storage_disk.go | 3 ++- pkg/services/store/storage_git.go | 4 ++-- pkg/services/store/storage_sql.go | 4 ++-- pkg/services/store/system_users_mock.go | 2 +- pkg/services/store/system_users_test.go | 3 ++- pkg/services/store/tree.go | 1 + pkg/services/store/types.go | 1 + pkg/services/thumbs/crawler.go | 2 +- pkg/services/thumbs/datasources_lookup.go | 1 + pkg/services/thumbs/datasources_lookup_test.go | 5 +++-- 65 files changed, 127 insertions(+), 103 deletions(-) diff --git a/pkg/infra/filestorage/filter.go b/pkg/infra/filestorage/filter.go index db6cadd3fa1..9df3e0c3280 100644 --- a/pkg/infra/filestorage/filter.go +++ b/pkg/infra/filestorage/filter.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/armon/go-radix" + "github.com/grafana/grafana/pkg/services/accesscontrol" ) diff --git a/pkg/services/live/database/tests/storage_test.go b/pkg/services/live/database/tests/storage_test.go index 0f107dcc098..cd3a483e390 100644 --- a/pkg/services/live/database/tests/storage_test.go +++ b/pkg/services/live/database/tests/storage_test.go @@ -4,8 +4,9 @@ import ( "encoding/json" "testing" - "github.com/grafana/grafana/pkg/services/live/model" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/live/model" ) func TestIntegrationLiveMessage(t *testing.T) { diff --git a/pkg/services/live/features/broadcast.go b/pkg/services/live/features/broadcast.go index c479a82d505..e85741056e2 100644 --- a/pkg/services/live/features/broadcast.go +++ b/pkg/services/live/features/broadcast.go @@ -3,11 +3,11 @@ package features import ( "context" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/live/model" "github.com/grafana/grafana/pkg/services/user" - - "github.com/grafana/grafana-plugin-sdk-go/backend" ) var ( diff --git a/pkg/services/live/features/broadcast_mock.go b/pkg/services/live/features/broadcast_mock.go index 19e4afa6b57..54df3472a98 100644 --- a/pkg/services/live/features/broadcast_mock.go +++ b/pkg/services/live/features/broadcast_mock.go @@ -8,6 +8,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" + model "github.com/grafana/grafana/pkg/services/live/model" ) diff --git a/pkg/services/live/features/broadcast_test.go b/pkg/services/live/features/broadcast_test.go index 459f07e2b2f..25065087420 100644 --- a/pkg/services/live/features/broadcast_test.go +++ b/pkg/services/live/features/broadcast_test.go @@ -7,9 +7,10 @@ import ( "github.com/golang/mock/gomock" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/live/model" "github.com/grafana/grafana/pkg/services/user" - "github.com/stretchr/testify/require" ) func TestNewBroadcastRunner(t *testing.T) { diff --git a/pkg/services/live/features/comment.go b/pkg/services/live/features/comment.go index b33e882f0cb..49531148fe7 100644 --- a/pkg/services/live/features/comment.go +++ b/pkg/services/live/features/comment.go @@ -4,11 +4,11 @@ import ( "context" "strings" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/services/comments/commentmodel" "github.com/grafana/grafana/pkg/services/live/model" "github.com/grafana/grafana/pkg/services/user" - - "github.com/grafana/grafana-plugin-sdk-go/backend" ) // CommentHandler manages all the `grafana/comment/*` channels. diff --git a/pkg/services/live/features/plugin.go b/pkg/services/live/features/plugin.go index b97f82a0775..bbb85ec0948 100644 --- a/pkg/services/live/features/plugin.go +++ b/pkg/services/live/features/plugin.go @@ -3,13 +3,13 @@ package features import ( "context" + "github.com/centrifugal/centrifuge" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/services/live/model" "github.com/grafana/grafana/pkg/services/live/orgchannel" "github.com/grafana/grafana/pkg/services/live/runstream" "github.com/grafana/grafana/pkg/services/user" - - "github.com/centrifugal/centrifuge" - "github.com/grafana/grafana-plugin-sdk-go/backend" ) //go:generate mockgen -destination=plugin_mock.go -package=features github.com/grafana/grafana/pkg/services/live/features PluginContextGetter diff --git a/pkg/services/live/features/plugin_mock.go b/pkg/services/live/features/plugin_mock.go index 5a3f059b540..1056d04c07f 100644 --- a/pkg/services/live/features/plugin_mock.go +++ b/pkg/services/live/features/plugin_mock.go @@ -10,6 +10,7 @@ import ( gomock "github.com/golang/mock/gomock" backend "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/services/user" ) diff --git a/pkg/services/live/live.go b/pkg/services/live/live.go index 7954ac4df46..18861520244 100644 --- a/pkg/services/live/live.go +++ b/pkg/services/live/live.go @@ -14,6 +14,14 @@ import ( "sync" "time" + "github.com/centrifugal/centrifuge" + "github.com/go-redis/redis/v8" + "github.com/gobwas/glob" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/live" + jsoniter "github.com/json-iterator/go" + "golang.org/x/sync/errgroup" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" @@ -50,14 +58,6 @@ import ( "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util/errutil" "github.com/grafana/grafana/pkg/web" - - "github.com/centrifugal/centrifuge" - "github.com/go-redis/redis/v8" - "github.com/gobwas/glob" - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana-plugin-sdk-go/live" - jsoniter "github.com/json-iterator/go" - "golang.org/x/sync/errgroup" ) var ( diff --git a/pkg/services/live/live_test.go b/pkg/services/live/live_test.go index d4dff995fcd..a448e55d7c0 100644 --- a/pkg/services/live/live_test.go +++ b/pkg/services/live/live_test.go @@ -7,9 +7,9 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/setting" ) func Test_runConcurrentlyIfNeeded_Concurrent(t *testing.T) { diff --git a/pkg/services/live/liveplugin/plugin.go b/pkg/services/live/liveplugin/plugin.go index 72b1f5f8cf8..f35302ef12a 100644 --- a/pkg/services/live/liveplugin/plugin.go +++ b/pkg/services/live/liveplugin/plugin.go @@ -4,14 +4,14 @@ import ( "context" "fmt" + "github.com/centrifugal/centrifuge" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/plugins/plugincontext" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/live/orgchannel" "github.com/grafana/grafana/pkg/services/live/pipeline" "github.com/grafana/grafana/pkg/services/user" - - "github.com/centrifugal/centrifuge" - "github.com/grafana/grafana-plugin-sdk-go/backend" ) type ChannelLocalPublisher struct { diff --git a/pkg/services/live/managedstream/cache_memory_test.go b/pkg/services/live/managedstream/cache_memory_test.go index ae695fc5e2d..37db71204d9 100644 --- a/pkg/services/live/managedstream/cache_memory_test.go +++ b/pkg/services/live/managedstream/cache_memory_test.go @@ -6,7 +6,6 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/stretchr/testify/require" ) diff --git a/pkg/services/live/managedstream/cache_redis.go b/pkg/services/live/managedstream/cache_redis.go index 1fe83211206..b547a11cd20 100644 --- a/pkg/services/live/managedstream/cache_redis.go +++ b/pkg/services/live/managedstream/cache_redis.go @@ -7,10 +7,10 @@ import ( "sync" "time" - "github.com/grafana/grafana/pkg/services/live/orgchannel" - "github.com/go-redis/redis/v8" "github.com/grafana/grafana-plugin-sdk-go/data" + + "github.com/grafana/grafana/pkg/services/live/orgchannel" ) // RedisFrameCache ... diff --git a/pkg/services/live/managedstream/runner.go b/pkg/services/live/managedstream/runner.go index 6ed4de0469d..b9fd21ad5e6 100644 --- a/pkg/services/live/managedstream/runner.go +++ b/pkg/services/live/managedstream/runner.go @@ -8,14 +8,14 @@ import ( "sync" "time" - "github.com/grafana/grafana/pkg/services/live/model" - "github.com/grafana/grafana/pkg/services/live/orgchannel" - "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana-plugin-sdk-go/live" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/live/model" + "github.com/grafana/grafana/pkg/services/live/orgchannel" + "github.com/grafana/grafana/pkg/services/user" ) var ( diff --git a/pkg/services/live/pipeline/config.go b/pkg/services/live/pipeline/config.go index f3133b2fc77..13319049f15 100644 --- a/pkg/services/live/pipeline/config.go +++ b/pkg/services/live/pipeline/config.go @@ -2,6 +2,7 @@ package pipeline import ( "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/services/org" ) diff --git a/pkg/services/live/pipeline/data_output_builtin.go b/pkg/services/live/pipeline/data_output_builtin.go index e5175f23ba9..1a4ab62f941 100644 --- a/pkg/services/live/pipeline/data_output_builtin.go +++ b/pkg/services/live/pipeline/data_output_builtin.go @@ -4,10 +4,10 @@ import ( "context" "errors" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/services/live/livecontext" "github.com/grafana/grafana/pkg/services/live/model" - - "github.com/grafana/grafana-plugin-sdk-go/backend" ) type BuiltinDataOutput struct { diff --git a/pkg/services/live/pipeline/data_output_local_subscribers.go b/pkg/services/live/pipeline/data_output_local_subscribers.go index 186d4b19b1a..5ea8206809e 100644 --- a/pkg/services/live/pipeline/data_output_local_subscribers.go +++ b/pkg/services/live/pipeline/data_output_local_subscribers.go @@ -4,9 +4,9 @@ import ( "context" "fmt" - "github.com/grafana/grafana/pkg/services/live/orgchannel" - "github.com/centrifugal/centrifuge" + + "github.com/grafana/grafana/pkg/services/live/orgchannel" ) type LocalSubscribersDataOutput struct { diff --git a/pkg/services/live/pipeline/devdata.go b/pkg/services/live/pipeline/devdata.go index db7c278bf79..ae17ecf9eba 100644 --- a/pkg/services/live/pipeline/devdata.go +++ b/pkg/services/live/pipeline/devdata.go @@ -10,10 +10,10 @@ import ( "os" "time" - "github.com/grafana/grafana/pkg/services/live/managedstream" - "github.com/centrifugal/centrifuge" "github.com/grafana/grafana-plugin-sdk-go/data" + + "github.com/grafana/grafana/pkg/services/live/managedstream" ) type Data struct { diff --git a/pkg/services/live/pipeline/frame_output_local_subscribers.go b/pkg/services/live/pipeline/frame_output_local_subscribers.go index 1f4179be7e8..396dffd5955 100644 --- a/pkg/services/live/pipeline/frame_output_local_subscribers.go +++ b/pkg/services/live/pipeline/frame_output_local_subscribers.go @@ -5,10 +5,10 @@ import ( "encoding/json" "fmt" - "github.com/grafana/grafana/pkg/services/live/orgchannel" - "github.com/centrifugal/centrifuge" "github.com/grafana/grafana-plugin-sdk-go/data" + + "github.com/grafana/grafana/pkg/services/live/orgchannel" ) type LocalSubscribersFrameOutput struct { diff --git a/pkg/services/live/pipeline/frame_output_managed_stream.go b/pkg/services/live/pipeline/frame_output_managed_stream.go index 705f3735f32..e1388e32baa 100644 --- a/pkg/services/live/pipeline/frame_output_managed_stream.go +++ b/pkg/services/live/pipeline/frame_output_managed_stream.go @@ -3,9 +3,9 @@ package pipeline import ( "context" - "github.com/grafana/grafana/pkg/services/live/managedstream" - "github.com/grafana/grafana-plugin-sdk-go/data" + + "github.com/grafana/grafana/pkg/services/live/managedstream" ) type ManagedStreamFrameOutput struct { diff --git a/pkg/services/live/pipeline/frame_output_remote_write.go b/pkg/services/live/pipeline/frame_output_remote_write.go index 6a75c2e9dbd..02aa4d505d9 100644 --- a/pkg/services/live/pipeline/frame_output_remote_write.go +++ b/pkg/services/live/pipeline/frame_output_remote_write.go @@ -10,8 +10,9 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/services/live/remotewrite" "github.com/prometheus/prometheus/prompb" + + "github.com/grafana/grafana/pkg/services/live/remotewrite" ) const flushInterval = 15 * time.Second diff --git a/pkg/services/live/pipeline/frame_output_remote_write_test.go b/pkg/services/live/pipeline/frame_output_remote_write_test.go index 3ef8acc4172..1780439a5dd 100644 --- a/pkg/services/live/pipeline/frame_output_remote_write_test.go +++ b/pkg/services/live/pipeline/frame_output_remote_write_test.go @@ -4,9 +4,8 @@ import ( "testing" "time" - "github.com/stretchr/testify/require" - "github.com/prometheus/prometheus/prompb" + "github.com/stretchr/testify/require" ) func TestRemoteWriteFrameOutput_sample(t *testing.T) { diff --git a/pkg/services/live/pipeline/frame_storage.go b/pkg/services/live/pipeline/frame_storage.go index 517ebc7bf80..9ac3d297993 100644 --- a/pkg/services/live/pipeline/frame_storage.go +++ b/pkg/services/live/pipeline/frame_storage.go @@ -3,9 +3,9 @@ package pipeline import ( "sync" - "github.com/grafana/grafana/pkg/services/live/orgchannel" - "github.com/grafana/grafana-plugin-sdk-go/data" + + "github.com/grafana/grafana/pkg/services/live/orgchannel" ) // FrameStorage keeps last channel frame in memory. Not usable in HA setup. diff --git a/pkg/services/live/pipeline/pipeline.go b/pkg/services/live/pipeline/pipeline.go index 12e647b62b2..2d4fb8ffcf2 100644 --- a/pkg/services/live/pipeline/pipeline.go +++ b/pkg/services/live/pipeline/pipeline.go @@ -6,9 +6,6 @@ import ( "fmt" "os" - "github.com/grafana/grafana/pkg/services/live/model" - "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana-plugin-sdk-go/live" @@ -19,6 +16,9 @@ import ( tracesdk "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.4.0" "go.opentelemetry.io/otel/trace" + + "github.com/grafana/grafana/pkg/services/live/model" + "github.com/grafana/grafana/pkg/services/user" ) const ( diff --git a/pkg/services/live/pipeline/pipeline_test.go b/pkg/services/live/pipeline/pipeline_test.go index fc3bb61b326..48c94a98a70 100644 --- a/pkg/services/live/pipeline/pipeline_test.go +++ b/pkg/services/live/pipeline/pipeline_test.go @@ -7,7 +7,6 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/stretchr/testify/require" ) diff --git a/pkg/services/live/pipeline/rule_builder_storage.go b/pkg/services/live/pipeline/rule_builder_storage.go index a8500e766e8..d800bf7feca 100644 --- a/pkg/services/live/pipeline/rule_builder_storage.go +++ b/pkg/services/live/pipeline/rule_builder_storage.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/centrifugal/centrifuge" + "github.com/grafana/grafana/pkg/services/live/managedstream" "github.com/grafana/grafana/pkg/services/secrets" ) diff --git a/pkg/services/live/pipeline/subscribe_builtin.go b/pkg/services/live/pipeline/subscribe_builtin.go index 4cc82af0601..14e35488ad4 100644 --- a/pkg/services/live/pipeline/subscribe_builtin.go +++ b/pkg/services/live/pipeline/subscribe_builtin.go @@ -3,12 +3,12 @@ package pipeline import ( "context" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/live" + "github.com/grafana/grafana/pkg/services/live/livecontext" "github.com/grafana/grafana/pkg/services/live/model" "github.com/grafana/grafana/pkg/services/user" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana-plugin-sdk-go/live" ) type BuiltinSubscriber struct { diff --git a/pkg/services/live/pipeline/subscribe_managed_stream.go b/pkg/services/live/pipeline/subscribe_managed_stream.go index 344b732b546..4f668469995 100644 --- a/pkg/services/live/pipeline/subscribe_managed_stream.go +++ b/pkg/services/live/pipeline/subscribe_managed_stream.go @@ -3,11 +3,11 @@ package pipeline import ( "context" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/services/live/livecontext" "github.com/grafana/grafana/pkg/services/live/managedstream" "github.com/grafana/grafana/pkg/services/live/model" - - "github.com/grafana/grafana-plugin-sdk-go/backend" ) type ManagedStreamSubscriber struct { diff --git a/pkg/services/live/pipeline/subscribe_multiple.go b/pkg/services/live/pipeline/subscribe_multiple.go index a7a6148a99d..9b7b6789236 100644 --- a/pkg/services/live/pipeline/subscribe_multiple.go +++ b/pkg/services/live/pipeline/subscribe_multiple.go @@ -4,6 +4,7 @@ import ( "context" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/services/live/model" ) diff --git a/pkg/services/live/pushhttp/push.go b/pkg/services/live/pushhttp/push.go index 302a5665c28..271fe9467d1 100644 --- a/pkg/services/live/pushhttp/push.go +++ b/pkg/services/live/pushhttp/push.go @@ -6,14 +6,14 @@ import ( "io" "net/http" + liveDto "github.com/grafana/grafana-plugin-sdk-go/live" + "github.com/grafana/grafana/pkg/infra/log" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/live" "github.com/grafana/grafana/pkg/services/live/convert" "github.com/grafana/grafana/pkg/services/live/pushurl" "github.com/grafana/grafana/pkg/setting" - - liveDto "github.com/grafana/grafana-plugin-sdk-go/live" "github.com/grafana/grafana/pkg/web" ) diff --git a/pkg/services/live/pushws/push_pipeline.go b/pkg/services/live/pushws/push_pipeline.go index b8bba51d810..c96251193c9 100644 --- a/pkg/services/live/pushws/push_pipeline.go +++ b/pkg/services/live/pushws/push_pipeline.go @@ -3,11 +3,11 @@ package pushws import ( "net/http" + "github.com/gorilla/websocket" + "github.com/grafana/grafana/pkg/services/live/convert" "github.com/grafana/grafana/pkg/services/live/livecontext" "github.com/grafana/grafana/pkg/services/live/pipeline" - - "github.com/gorilla/websocket" ) // PipelinePushHandler handles WebSocket client connections that push data to Live Pipeline. diff --git a/pkg/services/live/pushws/push_stream.go b/pkg/services/live/pushws/push_stream.go index d8a66984a18..2a9a42a781e 100644 --- a/pkg/services/live/pushws/push_stream.go +++ b/pkg/services/live/pushws/push_stream.go @@ -3,13 +3,13 @@ package pushws import ( "net/http" + "github.com/gorilla/websocket" + liveDto "github.com/grafana/grafana-plugin-sdk-go/live" + "github.com/grafana/grafana/pkg/services/live/convert" "github.com/grafana/grafana/pkg/services/live/livecontext" "github.com/grafana/grafana/pkg/services/live/managedstream" "github.com/grafana/grafana/pkg/services/live/pushurl" - - "github.com/gorilla/websocket" - liveDto "github.com/grafana/grafana-plugin-sdk-go/live" ) // Handler handles WebSocket client connections that push data to Live. diff --git a/pkg/services/live/runstream/manager.go b/pkg/services/live/runstream/manager.go index f4ab4d7b134..e431ef49623 100644 --- a/pkg/services/live/runstream/manager.go +++ b/pkg/services/live/runstream/manager.go @@ -8,10 +8,10 @@ import ( "sync" "time" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/user" - - "github.com/grafana/grafana-plugin-sdk-go/backend" ) var ( diff --git a/pkg/services/live/runstream/manager_test.go b/pkg/services/live/runstream/manager_test.go index 0d7be455724..7614f74c195 100644 --- a/pkg/services/live/runstream/manager_test.go +++ b/pkg/services/live/runstream/manager_test.go @@ -8,8 +8,9 @@ import ( "github.com/golang/mock/gomock" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/services/user" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/user" ) // wait until channel closed with timeout. diff --git a/pkg/services/live/runstream/mock.go b/pkg/services/live/runstream/mock.go index 9b38e653c90..0ce2e242379 100644 --- a/pkg/services/live/runstream/mock.go +++ b/pkg/services/live/runstream/mock.go @@ -10,6 +10,7 @@ import ( gomock "github.com/golang/mock/gomock" backend "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/services/user" ) diff --git a/pkg/services/live/survey/survey.go b/pkg/services/live/survey/survey.go index 98474f70d73..2ab62fadc76 100644 --- a/pkg/services/live/survey/survey.go +++ b/pkg/services/live/survey/survey.go @@ -10,6 +10,7 @@ import ( "time" "github.com/centrifugal/centrifuge" + "github.com/grafana/grafana/pkg/services/live/managedstream" ) diff --git a/pkg/services/live/telemetry/telegraf/convert.go b/pkg/services/live/telemetry/telegraf/convert.go index b445a444303..fd2616facda 100644 --- a/pkg/services/live/telemetry/telegraf/convert.go +++ b/pkg/services/live/telemetry/telegraf/convert.go @@ -7,9 +7,10 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana-plugin-sdk-go/data/converters" + influx "github.com/influxdata/line-protocol" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/live/telemetry" - influx "github.com/influxdata/line-protocol" ) var ( diff --git a/pkg/services/querylibrary/tests/common.go b/pkg/services/querylibrary/tests/common.go index f8cfe67ccfa..fddced65a0b 100644 --- a/pkg/services/querylibrary/tests/common.go +++ b/pkg/services/querylibrary/tests/common.go @@ -4,6 +4,8 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/require" + apikeygenprefix "github.com/grafana/grafana/pkg/components/apikeygenprefixed" "github.com/grafana/grafana/pkg/server" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -12,7 +14,6 @@ import ( saTests "github.com/grafana/grafana/pkg/services/serviceaccounts/tests" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" - "github.com/stretchr/testify/require" ) func createServiceAccountAdminToken(t *testing.T, name string, env *server.TestEnv) (string, *user.SignedInUser) { diff --git a/pkg/services/searchV2/allowed_actions.go b/pkg/services/searchV2/allowed_actions.go index 2e9dd13907e..0b5f1064bd7 100644 --- a/pkg/services/searchV2/allowed_actions.go +++ b/pkg/services/searchV2/allowed_actions.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources" diff --git a/pkg/services/searchV2/http.go b/pkg/services/searchV2/http.go index 4bc03fcb1ee..09f99413643 100644 --- a/pkg/services/searchV2/http.go +++ b/pkg/services/searchV2/http.go @@ -7,11 +7,12 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/prometheus/client_golang/prometheus" + "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" - "github.com/prometheus/client_golang/prometheus" ) type SearchHTTPService interface { diff --git a/pkg/services/searchV2/index_test.go b/pkg/services/searchV2/index_test.go index 69ab523cf8f..f0fa100dc91 100644 --- a/pkg/services/searchV2/index_test.go +++ b/pkg/services/searchV2/index_test.go @@ -6,8 +6,11 @@ import ( "path/filepath" "testing" + "github.com/blugelabs/bluge" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana-plugin-sdk-go/experimental" + "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" @@ -15,10 +18,6 @@ import ( "github.com/grafana/grafana/pkg/services/store" "github.com/grafana/grafana/pkg/services/store/entity" "github.com/grafana/grafana/pkg/setting" - - "github.com/blugelabs/bluge" - "github.com/grafana/grafana-plugin-sdk-go/experimental" - "github.com/stretchr/testify/require" ) type testDashboardLoader struct { diff --git a/pkg/services/searchV2/queries.go b/pkg/services/searchV2/queries.go index 110f16f4e0e..e7104e26640 100644 --- a/pkg/services/searchV2/queries.go +++ b/pkg/services/searchV2/queries.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/services/querylibrary" "github.com/grafana/grafana/pkg/services/user" ) diff --git a/pkg/services/searchV2/search_service_mock.go b/pkg/services/searchV2/search_service_mock.go index 98b74731cc5..63f7214c067 100644 --- a/pkg/services/searchV2/search_service_mock.go +++ b/pkg/services/searchV2/search_service_mock.go @@ -6,7 +6,6 @@ import ( context "context" backend "github.com/grafana/grafana-plugin-sdk-go/backend" - mock "github.com/stretchr/testify/mock" user "github.com/grafana/grafana/pkg/services/user" diff --git a/pkg/services/searchV2/service.go b/pkg/services/searchV2/service.go index 3f611f51f99..c8fe32896f6 100644 --- a/pkg/services/searchV2/service.go +++ b/pkg/services/searchV2/service.go @@ -10,8 +10,6 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" - "github.com/grafana/grafana/pkg/services/querylibrary" - "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" @@ -19,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/querylibrary" "github.com/grafana/grafana/pkg/services/store" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" diff --git a/pkg/services/searchV2/service_bench_test.go b/pkg/services/searchV2/service_bench_test.go index 9fc9fbfa573..989542d5aa7 100644 --- a/pkg/services/searchV2/service_bench_test.go +++ b/pkg/services/searchV2/service_bench_test.go @@ -6,6 +6,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" @@ -18,8 +20,6 @@ import ( "github.com/grafana/grafana/pkg/services/store" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - - "github.com/stretchr/testify/require" ) // setupBenchEnv will set up a database with folderCount folders and dashboardsPerFolder dashboards per folder diff --git a/pkg/services/searchV2/stub.go b/pkg/services/searchV2/stub.go index b3930c16c50..f3edd473fe6 100644 --- a/pkg/services/searchV2/stub.go +++ b/pkg/services/searchV2/stub.go @@ -5,6 +5,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/services/user" ) diff --git a/pkg/services/searchV2/types.go b/pkg/services/searchV2/types.go index 95b7a937aab..2c6b7fcd726 100644 --- a/pkg/services/searchV2/types.go +++ b/pkg/services/searchV2/types.go @@ -3,10 +3,10 @@ package searchV2 import ( "context" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/user" - - "github.com/grafana/grafana-plugin-sdk-go/backend" ) type FacetField struct { diff --git a/pkg/services/searchV2/usage.go b/pkg/services/searchV2/usage.go index 8fd2caaefd0..fe325e084b6 100644 --- a/pkg/services/searchV2/usage.go +++ b/pkg/services/searchV2/usage.go @@ -6,10 +6,11 @@ import ( "github.com/blugelabs/bluge" "github.com/blugelabs/bluge/search" "github.com/blugelabs/bluge/search/aggregations" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/infra/tracing" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" ) type usageGauge struct { diff --git a/pkg/services/store/entity/entity.pb.go b/pkg/services/store/entity/entity.pb.go index fcf78fd5e8d..eb8f1fe35ec 100644 --- a/pkg/services/store/entity/entity.pb.go +++ b/pkg/services/store/entity/entity.pb.go @@ -7,10 +7,11 @@ package entity import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" ) const ( diff --git a/pkg/services/store/entity/entity_grpc.pb.go b/pkg/services/store/entity/entity_grpc.pb.go index eb26acf9381..727d3b24ceb 100644 --- a/pkg/services/store/entity/entity_grpc.pb.go +++ b/pkg/services/store/entity/entity_grpc.pb.go @@ -8,6 +8,7 @@ package entity import ( context "context" + grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" diff --git a/pkg/services/store/entity/httpentitystore/service.go b/pkg/services/store/entity/httpentitystore/service.go index 649206ed0b0..4fe1daee5e9 100644 --- a/pkg/services/store/entity/httpentitystore/service.go +++ b/pkg/services/store/entity/httpentitystore/service.go @@ -8,6 +8,8 @@ import ( "strconv" "strings" + "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/middleware" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" @@ -15,9 +17,6 @@ import ( "github.com/grafana/grafana/pkg/services/store/kind" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" - - "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/api/routing" ) type HTTPEntityStore interface { diff --git a/pkg/services/store/entity/tests/common.go b/pkg/services/store/entity/tests/common.go index b40122f9cef..f481365ddf5 100644 --- a/pkg/services/store/entity/tests/common.go +++ b/pkg/services/store/entity/tests/common.go @@ -4,6 +4,10 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + apikeygenprefix "github.com/grafana/grafana/pkg/components/apikeygenprefixed" "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/server" @@ -14,9 +18,6 @@ import ( "github.com/grafana/grafana/pkg/services/store/entity" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" - "github.com/stretchr/testify/require" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" ) func createServiceAccountAdminToken(t *testing.T, env *server.TestEnv) (string, *user.SignedInUser) { diff --git a/pkg/services/store/k8saccess/client.go b/pkg/services/store/k8saccess/client.go index f8d143ecdbf..9ae67fbcb0c 100644 --- a/pkg/services/store/k8saccess/client.go +++ b/pkg/services/store/k8saccess/client.go @@ -4,11 +4,12 @@ import ( "net/http" "net/url" - contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" - "github.com/grafana/grafana/pkg/web" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" + + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/web" ) type clientWrapper struct { diff --git a/pkg/services/store/k8saccess/service.go b/pkg/services/store/k8saccess/service.go index fa52768e9f7..ce57f19ef01 100644 --- a/pkg/services/store/k8saccess/service.go +++ b/pkg/services/store/k8saccess/service.go @@ -4,12 +4,13 @@ import ( "os" "path/filepath" - "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/registry" - "github.com/grafana/grafana/pkg/services/featuremgmt" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" + + "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/featuremgmt" ) type K8SAccess interface { diff --git a/pkg/services/store/kind/playlist/summary_test.go b/pkg/services/store/kind/playlist/summary_test.go index b4d5d726c88..d69cdc5844e 100644 --- a/pkg/services/store/kind/playlist/summary_test.go +++ b/pkg/services/store/kind/playlist/summary_test.go @@ -5,8 +5,9 @@ import ( "encoding/json" "testing" - "github.com/grafana/grafana/pkg/kinds/playlist" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/kinds/playlist" ) func TestPlaylistSummary(t *testing.T) { diff --git a/pkg/services/store/storage_disk.go b/pkg/services/store/storage_disk.go index c5baa7824a2..a02c100fbeb 100644 --- a/pkg/services/store/storage_disk.go +++ b/pkg/services/store/storage_disk.go @@ -5,8 +5,9 @@ import ( "strings" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/infra/filestorage" "gocloud.dev/blob" + + "github.com/grafana/grafana/pkg/infra/filestorage" ) const rootStorageTypeDisk = "disk" diff --git a/pkg/services/store/storage_git.go b/pkg/services/store/storage_git.go index c3f8f92b173..d14bd515d02 100644 --- a/pkg/services/store/storage_git.go +++ b/pkg/services/store/storage_git.go @@ -11,12 +11,12 @@ import ( "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/object" - "github.com/grafana/grafana-plugin-sdk-go/data" + "gocloud.dev/blob" + "github.com/grafana/grafana/pkg/infra/filestorage" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - "gocloud.dev/blob" ) const rootStorageTypeGit = "git" diff --git a/pkg/services/store/storage_sql.go b/pkg/services/store/storage_sql.go index f69f1666c81..57a826f2ecd 100644 --- a/pkg/services/store/storage_sql.go +++ b/pkg/services/store/storage_sql.go @@ -5,10 +5,10 @@ import ( "fmt" "strings" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/filestorage" - - "github.com/grafana/grafana-plugin-sdk-go/data" ) const rootStorageTypeSQL = "sql" diff --git a/pkg/services/store/system_users_mock.go b/pkg/services/store/system_users_mock.go index 4a66c62663b..525a807dd69 100644 --- a/pkg/services/store/system_users_mock.go +++ b/pkg/services/store/system_users_mock.go @@ -3,9 +3,9 @@ package store import ( - filestorage "github.com/grafana/grafana/pkg/infra/filestorage" mock "github.com/stretchr/testify/mock" + filestorage "github.com/grafana/grafana/pkg/infra/filestorage" user "github.com/grafana/grafana/pkg/services/user" ) diff --git a/pkg/services/store/system_users_test.go b/pkg/services/store/system_users_test.go index 422d975974f..0e34435cded 100644 --- a/pkg/services/store/system_users_test.go +++ b/pkg/services/store/system_users_test.go @@ -3,9 +3,10 @@ package store import ( "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/filestorage" "github.com/grafana/grafana/pkg/services/user" - "github.com/stretchr/testify/require" ) const admin SystemUserType = "storageAdmin" diff --git a/pkg/services/store/tree.go b/pkg/services/store/tree.go index 49c8b396c26..1d05ee572b2 100644 --- a/pkg/services/store/tree.go +++ b/pkg/services/store/tree.go @@ -6,6 +6,7 @@ import ( "sync" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/infra/filestorage" ac "github.com/grafana/grafana/pkg/services/accesscontrol" ) diff --git a/pkg/services/store/types.go b/pkg/services/store/types.go index 0a8a1cafc81..861ddfd5423 100644 --- a/pkg/services/store/types.go +++ b/pkg/services/store/types.go @@ -5,6 +5,7 @@ import ( "encoding/json" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/infra/filestorage" "github.com/grafana/grafana/pkg/services/user" ) diff --git a/pkg/services/thumbs/crawler.go b/pkg/services/thumbs/crawler.go index 86647b9df0c..cca086cf51d 100644 --- a/pkg/services/thumbs/crawler.go +++ b/pkg/services/thumbs/crawler.go @@ -10,7 +10,6 @@ import ( "sync" "time" - "github.com/grafana/grafana/pkg/setting" "golang.org/x/sync/errgroup" "github.com/grafana/grafana/pkg/infra/log" @@ -18,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/live" "github.com/grafana/grafana/pkg/services/rendering" + "github.com/grafana/grafana/pkg/setting" ) type simpleCrawler struct { diff --git a/pkg/services/thumbs/datasources_lookup.go b/pkg/services/thumbs/datasources_lookup.go index 20af4c19a4b..32ce1d24ec0 100644 --- a/pkg/services/thumbs/datasources_lookup.go +++ b/pkg/services/thumbs/datasources_lookup.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/searchV2" "github.com/grafana/grafana/pkg/tsdb/grafanads" diff --git a/pkg/services/thumbs/datasources_lookup_test.go b/pkg/services/thumbs/datasources_lookup_test.go index 60484739d47..2643655c59a 100644 --- a/pkg/services/thumbs/datasources_lookup_test.go +++ b/pkg/services/thumbs/datasources_lookup_test.go @@ -7,10 +7,11 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/searchV2" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/searchV2" ) var ( From 8379a5338ca57b0b771ead1f3241572539a60fe6 Mon Sep 17 00:00:00 2001 From: Jack Baldry Date: Mon, 30 Jan 2023 09:27:11 +0000 Subject: [PATCH 085/117] CI: Lint starlark files with `buildifier` (#59157) * Add verify-starlark build action that returns an error for starlark files with lint Relies on `buildifier` tool. Signed-off-by: Jack Baldry * Add verify_starlark_step to PR pipeline Signed-off-by: Jack Baldry * Manually fetch buildifier in curl_image until a new build_image is created Signed-off-by: Jack Baldry * Format with buildifier Signed-off-by: Jack Baldry * Remove all unused variables retaining one unused function Signed-off-by: Jack Baldry * Use snake_case for variable Signed-off-by: Jack Baldry * Replace deprecated dictionary concatenation with .update() method Signed-off-by: Jack Baldry * Start adding docstrings for all modules and functions Signed-off-by: Jack Baldry * Prefer os.WriteFile as ioutil.WriteFile has been deprecated since go 1.16 Signed-off-by: Jack Baldry * Attempt to document the behavior of the init_enterprise_step Signed-off-by: Jack Baldry * Document test_backend pipeline Signed-off-by: Jack Baldry * Document enterprise_downstream_step Signed-off-by: Jack Baldry * Document the pipeline utility function Signed-off-by: Jack Baldry * Document publish_images_step Signed-off-by: Jack Baldry * Document publish_images_steps Signed-off-by: Jack Baldry * Document enterprise2_pipelines function Signed-off-by: Jack Baldry * Add tags table for Starlark files. Signed-off-by: Jack Baldry * Document test_frontend Signed-off-by: Jack Baldry * Document windows function Signed-off-by: Jack Baldry * Add docstrings to verifystarlark functions Signed-off-by: Jack Baldry * Refactor error handling to be more clear and document complex behavior Signed-off-by: Jack Baldry * Split errors into execution errors and verification errors Signed-off-by: Jack Baldry * Document all other library functions Signed-off-by: Jack Baldry * Add local variables to TAGS Signed-off-by: Jack Baldry * Add blank line between all Args and Returns sections Signed-off-by: Jack Baldry * Fix new linting errors Signed-off-by: Jack Baldry * Lint new Starlark files Signed-off-by: Jack Baldry * Correct buildifier binary mv Signed-off-by: Jack Baldry * Document the need to set nofile ulimit to at least 2048 Signed-off-by: Jack Baldry * Update build-container to include buildifier Signed-off-by: Jack Baldry * Ensure buildifier binary is executable Signed-off-by: Jack Baldry * Fix valid content test Signed-off-by: Jack Baldry * Simply return execution error Signed-off-by: Jack Baldry * Only check files rather than fixing them Signed-off-by: Jack Baldry * Use updated build-container with executable buildifier Signed-off-by: Jack Baldry * Test that context cancellation stops execution Signed-off-by: Jack Baldry * Simplify error handling Return execution errors that short circuit WalkDir rather than separately tracking that error. Signed-off-by: Jack Baldry * Remove fetching of buildifier binary now that it is in the build-container Signed-off-by: Jack Baldry * Use build image in verify-starlark step Signed-off-by: Jack Baldry * Use semver tag The image is the same but uses a semver tag to make it clearer that this is a forward upgrade from the old version. Signed-off-by: Jack Baldry * Use node 18 image with buildifier Signed-off-by: Jack Baldry --------- Signed-off-by: Jack Baldry --- .drone.star | 87 +- .drone.yml | 503 ++--- Makefile | 6 +- pkg/build/cmd/main.go | 6 + pkg/build/cmd/verifystarlark.go | 142 ++ pkg/build/cmd/verifystarlark_test.go | 135 ++ scripts/build/ci-build/Dockerfile | 7 + scripts/build/ci-build/README.md | 2 +- scripts/drone/TAGS | 628 ++++++ scripts/drone/events/cron.star | 134 +- scripts/drone/events/main.star | 144 +- scripts/drone/events/pr.star | 176 +- scripts/drone/events/release.star | 732 +++---- scripts/drone/pipelines/aws_marketplace.star | 53 +- scripts/drone/pipelines/build.star | 213 ++- scripts/drone/pipelines/docs.star | 88 +- scripts/drone/pipelines/github.star | 48 +- .../drone/pipelines/integration_tests.star | 65 +- scripts/drone/pipelines/lint_backend.star | 51 +- scripts/drone/pipelines/lint_frontend.star | 43 +- scripts/drone/pipelines/publish_images.star | 100 +- scripts/drone/pipelines/shellcheck.star | 53 +- scripts/drone/pipelines/test_backend.star | 101 +- scripts/drone/pipelines/test_frontend.star | 93 +- .../drone/pipelines/trigger_downstream.star | 54 +- scripts/drone/pipelines/verify_drone.star | 36 +- scripts/drone/pipelines/verify_starlark.star | 32 + scripts/drone/pipelines/windows.star | 51 +- scripts/drone/services/services.star | 72 +- scripts/drone/steps/lib.star | 1690 +++++++++-------- scripts/drone/utils/utils.star | 158 +- scripts/drone/vault.star | 110 +- scripts/drone/version.star | 23 +- 33 files changed, 3613 insertions(+), 2223 deletions(-) create mode 100644 pkg/build/cmd/verifystarlark.go create mode 100644 pkg/build/cmd/verifystarlark_test.go create mode 100644 scripts/drone/TAGS create mode 100644 scripts/drone/pipelines/verify_starlark.star diff --git a/.drone.star b/.drone.star index f5994191a0c..8c2a3c03767 100644 --- a/.drone.star +++ b/.drone.star @@ -3,54 +3,55 @@ # 2. Login to drone and export the env variables (token and server) shown here: https://drone.grafana.net/account # 3. Run `make drone` # More information about this process here: https://github.com/grafana/deployment_tools/blob/master/docs/infrastructure/drone/signing.md +""" +This module returns a Drone configuration including pipelines and secrets. +""" -load('scripts/drone/events/pr.star', 'pr_pipelines') -load('scripts/drone/events/main.star', 'main_pipelines') -load('scripts/drone/pipelines/docs.star', 'docs_pipelines') +load("scripts/drone/events/pr.star", "pr_pipelines") +load("scripts/drone/events/main.star", "main_pipelines") load( - 'scripts/drone/events/release.star', - 'oss_pipelines', - 'enterprise_pipelines', - 'enterprise2_pipelines', - 'publish_artifacts_pipelines', - 'publish_npm_pipelines', - 'publish_packages_pipeline', - 'artifacts_page_pipeline', + "scripts/drone/events/release.star", + "artifacts_page_pipeline", + "enterprise2_pipelines", + "enterprise_pipelines", + "oss_pipelines", + "publish_artifacts_pipelines", + "publish_npm_pipelines", + "publish_packages_pipeline", ) load( - 'scripts/drone/pipelines/publish_images.star', - 'publish_image_pipelines_public', - 'publish_image_pipelines_security', + "scripts/drone/pipelines/publish_images.star", + "publish_image_pipelines_public", + "publish_image_pipelines_security", ) -load('scripts/drone/pipelines/github.star', 'publish_github_pipeline') -load('scripts/drone/pipelines/aws_marketplace.star', 'publish_aws_marketplace_pipeline') -load('scripts/drone/version.star', 'version_branch_pipelines') -load('scripts/drone/events/cron.star', 'cronjobs') -load('scripts/drone/vault.star', 'secrets') +load("scripts/drone/pipelines/github.star", "publish_github_pipeline") +load("scripts/drone/pipelines/aws_marketplace.star", "publish_aws_marketplace_pipeline") +load("scripts/drone/version.star", "version_branch_pipelines") +load("scripts/drone/events/cron.star", "cronjobs") +load("scripts/drone/vault.star", "secrets") - -def main(ctx): +def main(_ctx): return ( - pr_pipelines() - + main_pipelines() - + oss_pipelines() - + enterprise_pipelines() - + enterprise2_pipelines() - + enterprise2_pipelines( - prefix='custom-', - trigger={'event': ['custom']}, - ) - + publish_image_pipelines_public() - + publish_image_pipelines_security() - + publish_github_pipeline('public') - + publish_github_pipeline('security') - + publish_aws_marketplace_pipeline('public') - + publish_artifacts_pipelines('security') - + publish_artifacts_pipelines('public') - + publish_npm_pipelines() - + publish_packages_pipeline() - + artifacts_page_pipeline() - + version_branch_pipelines() - + cronjobs() - + secrets() + pr_pipelines() + + main_pipelines() + + oss_pipelines() + + enterprise_pipelines() + + enterprise2_pipelines() + + enterprise2_pipelines( + prefix = "custom-", + trigger = {"event": ["custom"]}, + ) + + publish_image_pipelines_public() + + publish_image_pipelines_security() + + publish_github_pipeline("public") + + publish_github_pipeline("security") + + publish_aws_marketplace_pipeline("public") + + publish_artifacts_pipelines("security") + + publish_artifacts_pipelines("public") + + publish_npm_pipelines() + + publish_packages_pipeline() + + artifacts_page_pipeline() + + version_branch_pipelines() + + cronjobs() + + secrets() ) diff --git a/.drone.yml b/.drone.yml index e787f4007f8..1879dd0a72b 100644 --- a/.drone.yml +++ b/.drone.yml @@ -63,6 +63,61 @@ environment: image_pull_secrets: - dockerconfigjson kind: pipeline +name: pr-verify-starlark +node: + type: no-parallel +platform: + arch: amd64 + os: linux +services: [] +steps: +- commands: + - echo $DRONE_RUNNER_NAME + image: alpine:3.15.6 + name: identify-runner +- commands: + - mkdir -p bin + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl + - chmod +x bin/grabpl + image: byrnedo/alpine-curl:0.1.8 + name: grabpl +- commands: + - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd + depends_on: [] + environment: + CGO_ENABLED: 0 + image: golang:1.19.4 + name: compile-build-cmd +- commands: + - ./bin/build verify-starlark . + depends_on: + - compile-build-cmd + image: grafana/build-container:v1.7.1 + name: lint-starlark +trigger: + event: + - pull_request + paths: + exclude: + - docs/** + - '*.md' + include: + - scripts/drone/** + - .drone.star +type: docker +volumes: +- host: + path: /var/run/docker.sock + name: docker +--- +clone: + retries: 3 +depends_on: [] +environment: + EDITION: oss +image_pull_secrets: +- dockerconfigjson +kind: pipeline name: pr-test-frontend node: type: no-parallel @@ -84,13 +139,13 @@ steps: - commands: - yarn install --immutable depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - yarn betterer ci depends_on: - yarn-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: betterer-frontend - commands: - yarn run ci:test-frontend @@ -98,7 +153,7 @@ steps: - yarn-install environment: TEST_MAX_WORKERS: 50% - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-frontend trigger: event: @@ -141,7 +196,7 @@ steps: - commands: - yarn install --immutable depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - yarn run prettier:check @@ -152,7 +207,7 @@ steps: - yarn-install environment: TEST_MAX_WORKERS: 50% - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: lint-frontend trigger: event: @@ -206,7 +261,7 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -214,25 +269,25 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-jsonnet - commands: - make gen-go depends_on: - verify-gen-cue - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - go test -short -covermode=atomic -timeout=5m ./pkg/... depends_on: - wire-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-backend - commands: - go test -run Integration -covermode=atomic -timeout=5m ./pkg/... depends_on: - wire-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-backend-integration trigger: event: @@ -286,7 +341,7 @@ steps: - commands: - make gen-go depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - apt-get update && apt-get install make @@ -359,7 +414,7 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -367,18 +422,18 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-jsonnet - commands: - make gen-go depends_on: - verify-gen-cue - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - yarn install --immutable depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - git clone "https://$${GITHUB_TOKEN}@github.com/grafana/grafana-enterprise.git" @@ -403,7 +458,7 @@ steps: from_secret: github_token_pr TEST_TAG: v0.0.0-test failure: ignore - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: trigger-test-release when: paths: @@ -430,7 +485,7 @@ steps: depends_on: - wire-install - compile-build-cmd - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-backend - commands: - ./bin/build build-frontend --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} @@ -439,7 +494,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend - commands: - ./bin/build build-frontend-packages --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} @@ -448,7 +503,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend-packages - commands: - ./bin/build build-plugins --jobs 8 --edition oss @@ -456,7 +511,7 @@ steps: - compile-build-cmd - yarn-install environment: null - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-plugins - commands: - . scripts/build/gpg-test-vars.sh && ./bin/build package --jobs 8 --edition oss @@ -467,7 +522,7 @@ steps: - build-frontend - build-frontend-packages environment: null - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: package - commands: - ./scripts/grafana-server/start-server @@ -480,7 +535,7 @@ steps: environment: ARCH: linux-amd64 PORT: 3001 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: grafana-server - commands: - apt-get install -y netcat @@ -583,7 +638,7 @@ steps: - build-frontend-packages environment: NODE_OPTIONS: --max_old_space_size=4096 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-storybook when: paths: @@ -594,7 +649,7 @@ steps: - cp dist/*.tar.gz* packaging/docker/ depends_on: - package - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: copy-packages-for-docker - commands: - yarn wait-on http://$HOST:$PORT @@ -696,7 +751,7 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -704,13 +759,13 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-jsonnet - commands: - make gen-go depends_on: - verify-gen-cue - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - apt-get update @@ -726,7 +781,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: postgres-integration-tests - commands: - apt-get update @@ -742,7 +797,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: mysql-integration-tests trigger: event: @@ -800,7 +855,7 @@ steps: - commands: - yarn install --immutable depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - |- @@ -812,7 +867,7 @@ steps: wan" > words_to_ignore.txt - codespell -I words_to_ignore.txt docs/ - rm words_to_ignore.txt - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: codespell - commands: - yarn run prettier:checkDocs @@ -820,7 +875,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: lint-docs - commands: - mkdir -p /hugo/content/docs/grafana @@ -870,7 +925,7 @@ steps: - ./bin/build shellcheck depends_on: - compile-build-cmd - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: shellcheck trigger: event: @@ -917,7 +972,7 @@ steps: - commands: - yarn install --immutable depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - |- @@ -929,7 +984,7 @@ steps: wan" > words_to_ignore.txt - codespell -I words_to_ignore.txt docs/ - rm words_to_ignore.txt - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: codespell - commands: - yarn run prettier:checkDocs @@ -937,7 +992,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: lint-docs - commands: - mkdir -p /hugo/content/docs/grafana @@ -990,13 +1045,13 @@ steps: - commands: - yarn install --immutable depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - yarn betterer ci depends_on: - yarn-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: betterer-frontend - commands: - yarn run ci:test-frontend @@ -1004,7 +1059,7 @@ steps: - yarn-install environment: TEST_MAX_WORKERS: 50% - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-frontend trigger: branch: main @@ -1044,7 +1099,7 @@ steps: - commands: - yarn install --immutable depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - yarn run prettier:check @@ -1055,7 +1110,7 @@ steps: - yarn-install environment: TEST_MAX_WORKERS: 50% - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: lint-frontend trigger: branch: main @@ -1106,7 +1161,7 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -1114,25 +1169,25 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-jsonnet - commands: - make gen-go depends_on: - verify-gen-cue - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - go test -short -covermode=atomic -timeout=5m ./pkg/... depends_on: - wire-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-backend - commands: - go test -run Integration -covermode=atomic -timeout=5m ./pkg/... depends_on: - wire-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-backend-integration trigger: branch: main @@ -1179,7 +1234,7 @@ steps: - commands: - make gen-go depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - apt-get update && apt-get install make @@ -1251,7 +1306,7 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -1259,25 +1314,25 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-jsonnet - commands: - make gen-go depends_on: - verify-gen-cue - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - yarn install --immutable depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - ./bin/build build-backend --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} depends_on: - wire-install - compile-build-cmd - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-backend - commands: - ./bin/build build-frontend --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} @@ -1286,7 +1341,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend - commands: - ./bin/build build-frontend-packages --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} @@ -1295,7 +1350,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend-packages - commands: - ./bin/build build-plugins --jobs 8 --edition oss @@ -1305,7 +1360,7 @@ steps: environment: GRAFANA_API_KEY: from_secret: grafana_api_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-plugins - commands: - ./bin/build package --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} --sign @@ -1323,7 +1378,7 @@ steps: from_secret: packages_gpg_public_key GRAFANA_API_KEY: from_secret: grafana_api_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: package - commands: - ./scripts/grafana-server/start-server @@ -1336,7 +1391,7 @@ steps: environment: ARCH: linux-amd64 PORT: 3001 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: grafana-server - commands: - apt-get install -y netcat @@ -1439,7 +1494,7 @@ steps: - build-frontend-packages environment: NODE_OPTIONS: --max_old_space_size=4096 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-storybook when: paths: @@ -1450,7 +1505,7 @@ steps: - cp dist/*.tar.gz* packaging/docker/ depends_on: - package - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: copy-packages-for-docker - commands: - yarn wait-on http://$HOST:$PORT @@ -1494,7 +1549,7 @@ steps: GRAFANA_MISC_STATS_API_KEY: from_secret: grafana_misc_stats_api_key failure: ignore - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: publish-frontend-metrics when: repo: @@ -1575,7 +1630,7 @@ steps: environment: NPM_TOKEN: from_secret: npm_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: release-canary-npm-packages when: repo: @@ -1686,7 +1741,7 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -1694,13 +1749,13 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-jsonnet - commands: - make gen-go depends_on: - verify-gen-cue - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - apt-get update @@ -1716,7 +1771,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: postgres-integration-tests - commands: - apt-get update @@ -1732,7 +1787,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: mysql-integration-tests trigger: branch: main @@ -1970,18 +2025,18 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - make gen-go depends_on: - verify-gen-cue - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - yarn install --immutable depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -1995,7 +2050,7 @@ steps: depends_on: - wire-install - compile-build-cmd - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-backend - commands: - ./bin/build build-frontend --jobs 8 --edition oss ${DRONE_TAG} @@ -2004,7 +2059,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend - commands: - ./bin/build build-frontend-packages --jobs 8 --edition oss ${DRONE_TAG} @@ -2013,7 +2068,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend-packages - commands: - ./bin/build build-plugins --jobs 8 --edition oss @@ -2023,7 +2078,7 @@ steps: environment: GRAFANA_API_KEY: from_secret: grafana_api_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-plugins - commands: - ./bin/build package --jobs 8 --edition oss --sign ${DRONE_TAG} @@ -2041,14 +2096,14 @@ steps: from_secret: packages_gpg_public_key GRAFANA_API_KEY: from_secret: grafana_api_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: package - commands: - ls dist/*.tar.gz* - cp dist/*.tar.gz* packaging/docker/ depends_on: - package - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: copy-packages-for-docker - commands: - ./bin/build build-docker --edition oss --shouldSave @@ -2087,7 +2142,7 @@ steps: environment: ARCH: linux-amd64 PORT: 3001 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: grafana-server - commands: - apt-get install -y netcat @@ -2164,7 +2219,7 @@ steps: - build-frontend-packages environment: NODE_OPTIONS: --max_old_space_size=4096 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-storybook when: event: @@ -2223,7 +2278,7 @@ steps: from_secret: gcp_key PRERELEASE_BUCKET: from_secret: prerelease_bucket - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: store-npm-packages trigger: event: @@ -2272,13 +2327,13 @@ steps: - commands: - yarn install --immutable depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - yarn betterer ci depends_on: - yarn-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: betterer-frontend - commands: - yarn run ci:test-frontend @@ -2286,7 +2341,7 @@ steps: - yarn-install environment: TEST_MAX_WORKERS: 50% - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-frontend trigger: event: @@ -2334,7 +2389,7 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -2342,25 +2397,25 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-jsonnet - commands: - make gen-go depends_on: - verify-gen-cue - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - go test -short -covermode=atomic -timeout=5m ./pkg/... depends_on: - wire-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-backend - commands: - go test -run Integration -covermode=atomic -timeout=5m ./pkg/... depends_on: - wire-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-backend-integration trigger: event: @@ -2427,7 +2482,7 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -2435,13 +2490,13 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-jsonnet - commands: - make gen-go depends_on: - verify-gen-cue - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - apt-get update @@ -2457,7 +2512,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: postgres-integration-tests - commands: - apt-get update @@ -2473,7 +2528,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: mysql-integration-tests trigger: event: @@ -2590,7 +2645,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: clone-enterprise - commands: - mv bin/grabpl /tmp/ @@ -2606,7 +2661,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: init-enterprise - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -2620,13 +2675,13 @@ steps: - make gen-go depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - yarn install --immutable depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -2636,7 +2691,7 @@ steps: - CODEGEN_VERIFY=1 make gen-cue depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -2645,14 +2700,14 @@ steps: - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-jsonnet - commands: - ./bin/build build-backend --jobs 8 --edition enterprise ${DRONE_TAG} depends_on: - wire-install - compile-build-cmd - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-backend - commands: - ./bin/build build-frontend --jobs 8 --edition enterprise ${DRONE_TAG} @@ -2661,7 +2716,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend - commands: - ./bin/build build-frontend-packages --jobs 8 --edition enterprise ${DRONE_TAG} @@ -2670,7 +2725,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend-packages - commands: - ./bin/build build-plugins --jobs 8 --edition enterprise @@ -2680,7 +2735,7 @@ steps: environment: GRAFANA_API_KEY: from_secret: grafana_api_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-plugins - commands: - ./bin/build package --jobs 8 --edition enterprise --sign ${DRONE_TAG} @@ -2698,14 +2753,14 @@ steps: from_secret: packages_gpg_public_key GRAFANA_API_KEY: from_secret: grafana_api_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: package - commands: - ls dist/*.tar.gz* - cp dist/*.tar.gz* packaging/docker/ depends_on: - package - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: copy-packages-for-docker - commands: - ./bin/build build-docker --edition enterprise --shouldSave @@ -2745,7 +2800,7 @@ steps: ARCH: linux-amd64 PORT: 3001 RUNDIR: scripts/grafana-server/tmp-grafana-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: grafana-server - commands: - apt-get install -y netcat @@ -2877,7 +2932,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: clone-enterprise - commands: - mv bin/grabpl /tmp/ @@ -2893,7 +2948,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: init-enterprise - commands: - echo $DRONE_RUNNER_NAME @@ -2909,14 +2964,14 @@ steps: - yarn install --immutable depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - yarn betterer ci depends_on: - init-enterprise - yarn-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: betterer-frontend - commands: - yarn run ci:test-frontend @@ -2925,7 +2980,7 @@ steps: - yarn-install environment: TEST_MAX_WORKERS: 50% - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-frontend trigger: event: @@ -2962,7 +3017,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: clone-enterprise - commands: - mkdir -p bin @@ -2984,7 +3039,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: init-enterprise - commands: - echo $DRONE_RUNNER_NAME @@ -3006,7 +3061,7 @@ steps: - CODEGEN_VERIFY=1 make gen-cue depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -3015,25 +3070,25 @@ steps: - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-jsonnet - commands: - make gen-go depends_on: - verify-gen-cue - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - go test -short -covermode=atomic -timeout=5m ./pkg/... depends_on: - wire-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-backend - commands: - go test -run Integration -covermode=atomic -timeout=5m ./pkg/... depends_on: - wire-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-backend-integration trigger: event: @@ -3106,7 +3161,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: clone-enterprise - commands: - mv bin/grabpl /tmp/ @@ -3122,7 +3177,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: init-enterprise - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -3132,7 +3187,7 @@ steps: - CODEGEN_VERIFY=1 make gen-cue depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -3141,13 +3196,13 @@ steps: - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-jsonnet - commands: - make gen-go depends_on: - verify-gen-cue - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - apt-get update @@ -3163,7 +3218,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: postgres-integration-tests - commands: - apt-get update @@ -3179,18 +3234,16 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: mysql-integration-tests - commands: - dockerize -wait tcp://redis:6379/0 -timeout 120s - - go clean -testcache - - go list './pkg/...' | xargs -I {} sh -c 'go test -run Integration -covermode=atomic - -timeout=5m {}' + - ./bin/grabpl integration-tests depends_on: - wire-install environment: REDIS_URL: redis://redis:6379/0 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: redis-integration-tests - commands: - dockerize -wait tcp://memcached:11211 -timeout 120s @@ -3201,7 +3254,7 @@ steps: - wire-install environment: MEMCACHED_HOSTS: memcached:11211 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: memcached-integration-tests trigger: event: @@ -3338,7 +3391,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: clone-enterprise - commands: - mv bin/grabpl /tmp/ @@ -3354,7 +3407,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: init-enterprise - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -3368,13 +3421,13 @@ steps: - make gen-go depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - yarn install --immutable depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -3384,7 +3437,7 @@ steps: - CODEGEN_VERIFY=1 make gen-cue depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - ./bin/build build-frontend --jobs 8 --edition enterprise ${DRONE_TAG} @@ -3393,7 +3446,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend - commands: - ./bin/build build-frontend-packages --jobs 8 --edition enterprise ${DRONE_TAG} @@ -3402,7 +3455,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend-packages - commands: - ./bin/build build-plugins --jobs 8 --edition enterprise @@ -3412,14 +3465,14 @@ steps: environment: GRAFANA_API_KEY: from_secret: grafana_api_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-plugins - commands: - ./bin/build build-backend --jobs 8 --edition enterprise2 ${DRONE_TAG} depends_on: - wire-install - compile-build-cmd - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-backend-enterprise2 - commands: - ./bin/build package --jobs 8 --edition enterprise2 --sign ${DRONE_TAG} @@ -3437,7 +3490,7 @@ steps: from_secret: packages_gpg_public_key GRAFANA_API_KEY: from_secret: grafana_api_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: package-enterprise2 - commands: - ./bin/build upload-cdn --edition enterprise2 @@ -3457,7 +3510,7 @@ steps: - cp dist/*.tar.gz* packaging/docker/ depends_on: - package-enterprise2 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: copy-packages-for-docker - commands: - ./bin/build build-docker --edition enterprise2 --shouldSave @@ -3588,7 +3641,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: clone-enterprise - commands: - mv bin/grabpl /tmp/ @@ -3604,7 +3657,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: init-enterprise - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -3618,13 +3671,13 @@ steps: - make gen-go depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - yarn install --immutable depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -3634,7 +3687,7 @@ steps: - CODEGEN_VERIFY=1 make gen-cue depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - ./bin/build build-frontend --jobs 8 --edition enterprise ${DRONE_TAG} @@ -3643,7 +3696,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend - commands: - ./bin/build build-frontend-packages --jobs 8 --edition enterprise ${DRONE_TAG} @@ -3652,7 +3705,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend-packages - commands: - ./bin/build build-plugins --jobs 8 --edition enterprise @@ -3662,14 +3715,14 @@ steps: environment: GRAFANA_API_KEY: from_secret: grafana_api_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-plugins - commands: - ./bin/build build-backend --jobs 8 --edition enterprise2 ${DRONE_TAG} depends_on: - wire-install - compile-build-cmd - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-backend-enterprise2 - commands: - ./bin/build package --jobs 8 --edition enterprise2 --sign ${DRONE_TAG} @@ -3687,7 +3740,7 @@ steps: from_secret: packages_gpg_public_key GRAFANA_API_KEY: from_secret: grafana_api_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: package-enterprise2 - commands: - ./bin/build upload-cdn --edition enterprise2 @@ -3707,7 +3760,7 @@ steps: - cp dist/*.tar.gz* packaging/docker/ depends_on: - package-enterprise2 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: copy-packages-for-docker - commands: - ./bin/build build-docker --edition enterprise2 --shouldSave @@ -4366,7 +4419,7 @@ steps: - commands: - yarn install --immutable depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - ./bin/build artifacts npm retrieve --tag ${DRONE_TAG} @@ -4390,7 +4443,7 @@ steps: NPM_TOKEN: from_secret: npm_token failure: ignore - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: release-npm-packages trigger: event: @@ -4626,7 +4679,7 @@ steps: environment: GCP_KEY: from_secret: gcp_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: artifacts-page trigger: event: @@ -4671,18 +4724,18 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - make gen-go depends_on: - verify-gen-cue - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - yarn install --immutable depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -4696,7 +4749,7 @@ steps: depends_on: - wire-install - compile-build-cmd - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-backend - commands: - ./bin/build build-frontend --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} @@ -4705,7 +4758,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend - commands: - ./bin/build build-frontend-packages --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} @@ -4714,7 +4767,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend-packages - commands: - ./bin/build build-plugins --jobs 8 --edition oss @@ -4724,7 +4777,7 @@ steps: environment: GRAFANA_API_KEY: from_secret: grafana_api_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-plugins - commands: - ./bin/build package --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} --sign @@ -4742,14 +4795,14 @@ steps: from_secret: packages_gpg_public_key GRAFANA_API_KEY: from_secret: grafana_api_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: package - commands: - ls dist/*.tar.gz* - cp dist/*.tar.gz* packaging/docker/ depends_on: - package - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: copy-packages-for-docker - commands: - ./bin/build build-docker --edition oss --shouldSave @@ -4788,7 +4841,7 @@ steps: environment: ARCH: linux-amd64 PORT: 3001 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: grafana-server - commands: - apt-get install -y netcat @@ -4865,7 +4918,7 @@ steps: - build-frontend-packages environment: NODE_OPTIONS: --max_old_space_size=4096 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-storybook when: paths: @@ -4946,13 +4999,13 @@ steps: - commands: - yarn install --immutable depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - yarn betterer ci depends_on: - yarn-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: betterer-frontend - commands: - yarn run ci:test-frontend @@ -4960,7 +5013,7 @@ steps: - yarn-install environment: TEST_MAX_WORKERS: 50% - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-frontend trigger: ref: @@ -5005,7 +5058,7 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -5013,25 +5066,25 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-jsonnet - commands: - make gen-go depends_on: - verify-gen-cue - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - go test -short -covermode=atomic -timeout=5m ./pkg/... depends_on: - wire-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-backend - commands: - go test -run Integration -covermode=atomic -timeout=5m ./pkg/... depends_on: - wire-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-backend-integration trigger: ref: @@ -5095,7 +5148,7 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -5103,13 +5156,13 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-jsonnet - commands: - make gen-go depends_on: - verify-gen-cue - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - apt-get update @@ -5125,7 +5178,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: postgres-integration-tests - commands: - apt-get update @@ -5141,7 +5194,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: mysql-integration-tests trigger: ref: @@ -5248,7 +5301,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: clone-enterprise - commands: - mv bin/grabpl /tmp/ @@ -5263,7 +5316,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: init-enterprise - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -5277,13 +5330,13 @@ steps: - make gen-go depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - yarn install --immutable depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -5293,7 +5346,7 @@ steps: - CODEGEN_VERIFY=1 make gen-cue depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -5302,14 +5355,14 @@ steps: - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-jsonnet - commands: - ./bin/build build-backend --jobs 8 --edition enterprise --build-id ${DRONE_BUILD_NUMBER} depends_on: - wire-install - compile-build-cmd - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-backend - commands: - ./bin/build build-frontend --jobs 8 --edition enterprise --build-id ${DRONE_BUILD_NUMBER} @@ -5318,7 +5371,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend - commands: - ./bin/build build-frontend-packages --jobs 8 --edition enterprise --build-id ${DRONE_BUILD_NUMBER} @@ -5327,7 +5380,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend-packages - commands: - ./bin/build build-plugins --jobs 8 --edition enterprise @@ -5337,7 +5390,7 @@ steps: environment: GRAFANA_API_KEY: from_secret: grafana_api_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-plugins - commands: - ./bin/build package --jobs 8 --edition enterprise --build-id ${DRONE_BUILD_NUMBER} @@ -5356,14 +5409,14 @@ steps: from_secret: packages_gpg_public_key GRAFANA_API_KEY: from_secret: grafana_api_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: package - commands: - ls dist/*.tar.gz* - cp dist/*.tar.gz* packaging/docker/ depends_on: - package - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: copy-packages-for-docker - commands: - ./bin/build build-docker --edition enterprise --shouldSave @@ -5403,7 +5456,7 @@ steps: ARCH: linux-amd64 PORT: 3001 RUNDIR: scripts/grafana-server/tmp-grafana-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: grafana-server - commands: - apt-get install -y netcat @@ -5538,7 +5591,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: clone-enterprise - commands: - mv bin/grabpl /tmp/ @@ -5553,7 +5606,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: init-enterprise - commands: - echo $DRONE_RUNNER_NAME @@ -5569,14 +5622,14 @@ steps: - yarn install --immutable depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - yarn betterer ci depends_on: - init-enterprise - yarn-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: betterer-frontend - commands: - yarn run ci:test-frontend @@ -5585,7 +5638,7 @@ steps: - yarn-install environment: TEST_MAX_WORKERS: 50% - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-frontend trigger: ref: @@ -5619,7 +5672,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: clone-enterprise - commands: - mkdir -p bin @@ -5640,7 +5693,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: init-enterprise - commands: - echo $DRONE_RUNNER_NAME @@ -5662,7 +5715,7 @@ steps: - CODEGEN_VERIFY=1 make gen-cue depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -5671,25 +5724,25 @@ steps: - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-jsonnet - commands: - make gen-go depends_on: - verify-gen-cue - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - go test -short -covermode=atomic -timeout=5m ./pkg/... depends_on: - wire-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-backend - commands: - go test -run Integration -covermode=atomic -timeout=5m ./pkg/... depends_on: - wire-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-backend-integration trigger: ref: @@ -5759,7 +5812,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: clone-enterprise - commands: - mv bin/grabpl /tmp/ @@ -5774,7 +5827,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: init-enterprise - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -5784,7 +5837,7 @@ steps: - CODEGEN_VERIFY=1 make gen-cue depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -5793,13 +5846,13 @@ steps: - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-jsonnet - commands: - make gen-go depends_on: - verify-gen-cue - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - apt-get update @@ -5815,7 +5868,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: postgres-integration-tests - commands: - apt-get update @@ -5831,18 +5884,16 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: mysql-integration-tests - commands: - dockerize -wait tcp://redis:6379/0 -timeout 120s - - go clean -testcache - - go list './pkg/...' | xargs -I {} sh -c 'go test -run Integration -covermode=atomic - -timeout=5m {}' + - ./bin/grabpl integration-tests depends_on: - wire-install environment: REDIS_URL: redis://redis:6379/0 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: redis-integration-tests - commands: - dockerize -wait tcp://memcached:11211 -timeout 120s @@ -5853,7 +5904,7 @@ steps: - wire-install environment: MEMCACHED_HOSTS: memcached:11211 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: memcached-integration-tests trigger: ref: @@ -5980,7 +6031,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: clone-enterprise - commands: - mv bin/grabpl /tmp/ @@ -5995,7 +6046,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: init-enterprise - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -6009,13 +6060,13 @@ steps: - make gen-go depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - yarn install --immutable depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -6025,7 +6076,7 @@ steps: - CODEGEN_VERIFY=1 make gen-cue depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - ./bin/build build-frontend --jobs 8 --edition enterprise --build-id ${DRONE_BUILD_NUMBER} @@ -6034,7 +6085,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend - commands: - ./bin/build build-frontend-packages --jobs 8 --edition enterprise --build-id ${DRONE_BUILD_NUMBER} @@ -6043,7 +6094,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend-packages - commands: - ./bin/build build-plugins --jobs 8 --edition enterprise @@ -6053,7 +6104,7 @@ steps: environment: GRAFANA_API_KEY: from_secret: grafana_api_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-plugins - commands: - ./bin/build build-backend --jobs 8 --edition enterprise2 --build-id ${DRONE_BUILD_NUMBER} @@ -6061,7 +6112,7 @@ steps: depends_on: - wire-install - compile-build-cmd - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-backend-enterprise2 - commands: - ./bin/build package --jobs 8 --edition enterprise2 --build-id ${DRONE_BUILD_NUMBER} @@ -6080,7 +6131,7 @@ steps: from_secret: packages_gpg_public_key GRAFANA_API_KEY: from_secret: grafana_api_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: package-enterprise2 - commands: - ./bin/build upload-cdn --edition enterprise2 @@ -6100,7 +6151,7 @@ steps: - cp dist/*.tar.gz* packaging/docker/ depends_on: - package-enterprise2 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: copy-packages-for-docker - commands: - ./bin/build build-docker --edition enterprise2 --shouldSave @@ -6451,6 +6502,6 @@ kind: secret name: aws_secret_access_key --- kind: signature -hmac: 86222a86386ae1d2afce23b4a15f09e97aaeb873275ed34ac73d733335d63e4b +hmac: 6e76bf175f2c58fd4ffdc42e2120c558345a71a45011279b14092acb67252b28 ... diff --git a/Makefile b/Makefile index 78cb6f15963..49acbcf06fa 100644 --- a/Makefile +++ b/Makefile @@ -239,8 +239,12 @@ drone: $(DRONE) $(DRONE) lint .drone.yml --trusted $(DRONE) --server https://drone.grafana.net sign --save grafana/grafana +# Generate an Emacs tags table (https://www.gnu.org/software/emacs/manual/html_node/emacs/Tags-Tables.html) for Starlark files. +scripts/drone/TAGS: $(shell find scripts/drone -name '*.star') + etags --lang none --regex="/def \(\w+\)[^:]+:/\1/" --regex="/\s*\(\w+\) =/\1/" $^ -o $@ + format-drone: - black --include '\.star$$' -S scripts/drone/ .drone.star + buildifier -r scripts/drone help: ## Display this help. @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) diff --git a/pkg/build/cmd/main.go b/pkg/build/cmd/main.go index 69a326f5335..6725fb7e050 100644 --- a/pkg/build/cmd/main.go +++ b/pkg/build/cmd/main.go @@ -132,6 +132,12 @@ func main() { Usage: "Verify Drone configuration", Action: VerifyDrone, }, + { + Name: "verify-starlark", + Usage: "Verify Starlark configuration", + ArgsUsage: "", + Action: VerifyStarlark, + }, { Name: "export-version", Usage: "Exports version in dist/grafana.version", diff --git a/pkg/build/cmd/verifystarlark.go b/pkg/build/cmd/verifystarlark.go new file mode 100644 index 00000000000..ff33a77a3af --- /dev/null +++ b/pkg/build/cmd/verifystarlark.go @@ -0,0 +1,142 @@ +package main + +import ( + "context" + "errors" + "fmt" + "io/fs" + "os/exec" + "path/filepath" + "strings" + + "github.com/urfave/cli/v2" +) + +func mapSlice[I any, O any](a []I, f func(I) O) []O { + o := make([]O, len(a)) + for i, e := range a { + o[i] = f(e) + } + return o +} + +// VerifyStarlark is the CLI Action for verifying Starlark files in a workspace. +// It expects a single context argument which is the path to the workspace. +// The actual verification procedure can return multiple errors which are +// joined together to be one holistic error for the action. +func VerifyStarlark(c *cli.Context) error { + if c.NArg() != 1 { + var message string + if c.NArg() == 0 { + message = "ERROR: missing required argument " + } + if c.NArg() > 1 { + message = "ERROR: too many arguments" + } + + if err := cli.ShowSubcommandHelp(c); err != nil { + return err + } + + return cli.Exit(message, 1) + } + + workspace := c.Args().Get(0) + verificationErrs, executionErr := verifyStarlark(c.Context, workspace, buildifierLintCommand) + if executionErr != nil { + return executionErr + } + + if len(verificationErrs) == 0 { + return nil + } + + noun := "file" + if len(verificationErrs) > 1 { + noun += "s" + } + + return fmt.Errorf("verification failed for %d %s:\n%s", + len(verificationErrs), + noun, + strings.Join( + mapSlice(verificationErrs, func(e error) string { return e.Error() }), + "\n", + )) +} + +type commandFunc = func(path string) (command string, args []string) + +func buildifierLintCommand(path string) (string, []string) { + return "buildifier", []string{"-lint", "warn", "-mode", "check", path} +} + +// verifyStarlark walks all directories starting at provided workspace path and +// verifies any Starlark files it finds. +// Starlark files are assumed to end with the .star extension. +// The verification relies on linting frovided by the 'buildifier' binary which +// must be in the PATH. +// A slice of verification errors are returned, one for each file that failed verification. +// If any execution of the `buildifier` command fails, this is returned separately. +// commandFn is executed on every Starlark file to determine the command and arguments to be executed. +// The caller is trusted and it is the callers responsibility to ensure that the resulting command is safe to execute. +func verifyStarlark(ctx context.Context, workspace string, commandFn commandFunc) ([]error, error) { + var verificationErrs []error + + // All errors from filepath.WalkDir are filtered by the fs.WalkDirFunc. + // Lstat or ReadDir errors are reported as verificationErrors. + // If any execution of the `buildifier` command fails or if the context is cancelled, + // it is reported as an error and any verification of subsequent files is skipped. + err := filepath.WalkDir(workspace, func(path string, d fs.DirEntry, err error) error { + // Skip verification of the file or files within the directory if there is an error + // returned by Lstat or ReadDir. + if err != nil { + verificationErrs = append(verificationErrs, err) + return nil + } + + if d.IsDir() { + return nil + } + + if filepath.Ext(path) == ".star" { + command, args := commandFn(path) + // The caller is trusted. + //nolint:gosec + cmd := exec.CommandContext(ctx, command, args...) + cmd.Dir = workspace + + _, err = cmd.Output() + if err == nil { // No error, early return. + return nil + } + + // The error returned from cmd.Output() is never wrapped. + //nolint:errorlint + if err, ok := err.(*exec.ExitError); ok { + switch err.ExitCode() { + // Case comments are informed by the output of `buildifier --help` + case 1: // syntax errors in input + verificationErrs = append(verificationErrs, errors.New(string(err.Stderr))) + return nil + case 2: // usage errors: invoked incorrectly + return fmt.Errorf("command %q: %s", cmd, err.Stderr) + case 3: // unexpected runtime errors: file I/O problems or internal bugs + return fmt.Errorf("command %q: %s", cmd, err.Stderr) + case 4: // check mode failed (reformat is needed) + verificationErrs = append(verificationErrs, errors.New(string(err.Stderr))) + return nil + default: + return fmt.Errorf("command %q: %s", cmd, err.Stderr) + } + } + + // Error was not an exit error from the command. + return fmt.Errorf("command %q: %v", cmd, err) + } + + return nil + }) + + return verificationErrs, err +} diff --git a/pkg/build/cmd/verifystarlark_test.go b/pkg/build/cmd/verifystarlark_test.go new file mode 100644 index 00000000000..8fe61dc2e8c --- /dev/null +++ b/pkg/build/cmd/verifystarlark_test.go @@ -0,0 +1,135 @@ +package main + +import ( + "context" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestVerifyStarlark(t *testing.T) { + t.Run("execution errors", func(t *testing.T) { + t.Run("invalid usage", func(t *testing.T) { + ctx := context.Background() + workspace := t.TempDir() + err := os.WriteFile(filepath.Join(workspace, "ignored.star"), []byte{}, os.ModePerm) + if err != nil { + t.Fatalf(err.Error()) + } + + _, executionErr := verifyStarlark(ctx, workspace, func(string) (string, []string) { return "buildifier", []string{"--invalid"} }) + if executionErr == nil { + t.Fatalf("Expected execution error but got none") + } + }) + + t.Run("context cancellation", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + workspace := t.TempDir() + err := os.WriteFile(filepath.Join(workspace, "ignored.star"), []byte{}, os.ModePerm) + if err != nil { + t.Fatalf(err.Error()) + } + err = os.WriteFile(filepath.Join(workspace, "other-ignored.star"), []byte{}, os.ModePerm) + if err != nil { + t.Fatalf(err.Error()) + } + cancel() + + _, executionErr := verifyStarlark(ctx, workspace, buildifierLintCommand) + if executionErr == nil { + t.Fatalf("Expected execution error but got none") + } + }) + }) + + t.Run("verification errors", func(t *testing.T) { + t.Run("a single file with lint", func(t *testing.T) { + ctx := context.Background() + workspace := t.TempDir() + + invalidContent := []byte(`load("scripts/drone/other.star", "function") + +function()`) + err := os.WriteFile(filepath.Join(workspace, "has-lint.star"), invalidContent, os.ModePerm) + if err != nil { + t.Fatalf(err.Error()) + } + + verificationErrs, executionErr := verifyStarlark(ctx, workspace, buildifierLintCommand) + if executionErr != nil { + t.Fatalf("Unexpected execution error: %v", executionErr) + } + if len(verificationErrs) == 0 { + t.Fatalf(`"has-lint.star" requires linting but the verifyStarlark function provided no linting error`) + } + if len(verificationErrs) > 1 { + t.Fatalf(`verifyStarlark returned multiple errors for the "has-lint.star" file but only one was expected: %v`, verificationErrs) + } + if !strings.Contains(verificationErrs[0].Error(), "has-lint.star:1: module-docstring: The file has no module docstring.") { + t.Fatalf(`"has-lint.star" is missing a module docstring but the verifyStarlark function linting error did not mention this, instead we got: %v`, verificationErrs[0]) + } + }) + + t.Run("no files with lint", func(t *testing.T) { + ctx := context.Background() + workspace := t.TempDir() + + content := []byte(`""" +This module does nothing. +""" + +load("scripts/drone/other.star", "function") + +function() +`) + require.NoError(t, os.WriteFile(filepath.Join(workspace, "no-lint.star"), content, os.ModePerm)) + + verificationErrs, executionErr := verifyStarlark(ctx, workspace, buildifierLintCommand) + if executionErr != nil { + t.Fatalf("Unexpected execution error: %v", executionErr) + } + if len(verificationErrs) != 0 { + t.Log(`"no-lint.star" has no lint but the verifyStarlark function provided at least one error`) + for _, err := range verificationErrs { + t.Log(err) + } + t.FailNow() + } + }) + + t.Run("multiple files with lint", func(t *testing.T) { + ctx := context.Background() + workspace := t.TempDir() + + invalidContent := []byte(`load("scripts/drone/other.star", "function") + +function()`) + require.NoError(t, os.WriteFile(filepath.Join(workspace, "has-lint.star"), invalidContent, os.ModePerm)) + require.NoError(t, os.WriteFile(filepath.Join(workspace, "has-lint2.star"), invalidContent, os.ModePerm)) + + verificationErrs, executionErr := verifyStarlark(ctx, workspace, buildifierLintCommand) + if executionErr != nil { + t.Fatalf("Unexpected execution error: %v", executionErr) + } + if len(verificationErrs) == 0 { + t.Fatalf(`Two files require linting but the verifyStarlark function provided no linting error`) + } + if len(verificationErrs) == 1 { + t.Fatalf(`Two files require linting but the verifyStarlark function provided only one linting error: %v`, verificationErrs[0]) + } + if len(verificationErrs) > 2 { + t.Fatalf(`verifyStarlark returned more errors than expected: %v`, verificationErrs) + } + if !strings.Contains(verificationErrs[0].Error(), "has-lint.star:1: module-docstring: The file has no module docstring.") { + t.Errorf(`"has-lint.star" is missing a module docstring but the verifyStarlark function linting error did not mention this, instead we got: %v`, verificationErrs[0]) + } + if !strings.Contains(verificationErrs[1].Error(), "has-lint2.star:1: module-docstring: The file has no module docstring.") { + t.Fatalf(`"has-lint2.star" is missing a module docstring but the verifyStarlark function linting error did not mention this, instead we got: %v`, verificationErrs[0]) + } + }) + }) +} diff --git a/scripts/build/ci-build/Dockerfile b/scripts/build/ci-build/Dockerfile index 32c90995f10..3316a3fcfc2 100644 --- a/scripts/build/ci-build/Dockerfile +++ b/scripts/build/ci-build/Dockerfile @@ -87,6 +87,12 @@ RUN curl -fLO http://storage.googleapis.com/grafana-downloads/ci-dependencies/sh RUN echo $SHELLCHECK_CHKSUM shellcheck-v${SHELLCHECK_VERSION}.linux.x86_64.tar.xz | sha512sum --check --strict --status RUN tar xf shellcheck-v${SHELLCHECK_VERSION}.linux.x86_64.tar.xz && mv shellcheck-v${SHELLCHECK_VERSION}/shellcheck /tmp/ +ARG BUILDIFIER_VERSION=5.1.0 +ARG BUILDIFIER_CHKSUM=52bf6b102cb4f88464e197caac06d69793fa2b05f5ad50a7e7bf6fbd656648a3 +RUN curl -fLO https://github.com/bazelbuild/buildtools/releases/download/${BUILDIFIER_VERSION}/buildifier-linux-amd64 +RUN echo $BUILDIFIER_CHKSUM buildifier-linux-amd64 | sha256sum --check --strict --status +RUN mv buildifier-linux-amd64 /tmp/buildifier && chmod +x /tmp/buildifier + ARG CUE_VERSION=0.3.0-alpha5 ARG CUE_CHKSUM=9d3131e470cdb5182afd9966688f1c052d383145cce005a947156b5591da39b7 RUN curl -fLO https://github.com/cuelang/cue/releases/download/v${CUE_VERSION}/cue_${CUE_VERSION}_Linux_x86_64.tar.gz @@ -113,6 +119,7 @@ ARG DEBIAN_FRONTEND=noninteractive COPY --from=toolchain /tmp/x86_64-centos6-linux-gnu.tar.xz /tmp/osxcross.tar.xz /tmp/ COPY --from=toolchain /tmp/shellcheck /usr/local/bin/ +COPY --from=toolchain /tmp/buildifier /usr/local/bin/ COPY --from=toolchain /tmp/cue /usr/local/bin/ COPY --from=toolchain /tmp/dockerize /usr/local/bin/ diff --git a/scripts/build/ci-build/README.md b/scripts/build/ci-build/README.md index 72bd15705f7..ed6dbe509aa 100644 --- a/scripts/build/ci-build/README.md +++ b/scripts/build/ci-build/README.md @@ -11,7 +11,7 @@ In order to build and publish the Grafana build Docker image, execute the follow ``` # Download MacOSX10.15.sdk.tar.xz from our private GCS bucket into this directory -docker build -t grafana/build-container: . +docker build -t grafana/build-container: --ulimit nofile=2048:2048 . docker push grafana/build-container: ``` diff --git a/scripts/drone/TAGS b/scripts/drone/TAGS new file mode 100644 index 00000000000..1c9f9da89a5 --- /dev/null +++ b/scripts/drone/TAGS @@ -0,0 +1,628 @@ + +events/release.star,6652 +ver_mode =ver_mode64,1602 +release_trigger =release_trigger65,1623 +def store_npm_packages_step():store_npm_packages_step74,1752 +def retrieve_npm_packages_step():retrieve_npm_packages_step90,2193 +def release_npm_packages_step():release_npm_packages_step107,2663 +def oss_pipelines(ver_mode = ver_mode, trigger = release_trigger):oss_pipelines123,3076 + environment =environment135,3492 + edition =edition136,3529 + services =services137,3549 + volumes =volumes138,3609 + package_steps =package_steps139,3659 + publish_steps =publish_steps140,3682 + should_publish =should_publish141,3705 + should_upload =should_upload142,3748 + init_steps =init_steps143,3818 + build_steps =build_steps152,4033 + integration_test_steps =integration_test_steps159,4342 + build_storybook =build_storybook182,5254 + publish_step =publish_step190,5674 + store_npm_step =store_npm_step191,5758 + windows_package_steps =windows_package_steps196,5957 + windows_pipeline =windows_pipeline198,6044 + name =name199,6077 + edition =edition200,6127 + trigger =trigger201,6154 + steps =steps202,6181 + platform =platform203,6256 + depends_on =depends_on204,6286 + environment =environment207,6393 + pipelines =pipelines209,6434 + name =name211,6470 + edition =edition212,6550 + trigger =trigger213,6581 + services =services214,6612 + steps =steps215,6639 + environment =environment216,6717 + volumes =volumes217,6756 + name =name225,6970 + edition =edition226,7038 + trigger =trigger227,7073 + services =services228,7108 + steps =steps229,7145 + environment =environment230,7329 + volumes =volumes231,7372 + deps =deps234,7433 +def enterprise_pipelines(ver_mode = ver_mode, trigger = release_trigger):enterprise_pipelines247,7856 + environment =environment259,8284 + edition =edition260,8328 + services =services261,8355 + volumes =volumes262,8415 + package_steps =package_steps263,8465 + publish_steps =publish_steps264,8488 + should_publish =should_publish265,8511 + should_upload =should_upload266,8554 + include_enterprise =include_enterprise267,8624 + edition2 =edition2268,8673 + init_steps =init_steps269,8702 + build_steps =build_steps277,8909 + integration_test_steps =integration_test_steps284,9218 + build_storybook =build_storybook312,10299 + publish_step =publish_step324,10892 + store_npm_step =store_npm_step325,10976 + windows_package_steps =windows_package_steps330,11175 + step =step333,11284 + deps_on_clone_enterprise_step =deps_on_clone_enterprise_step337,11418 + windows_pipeline =windows_pipeline347,11746 + name =name348,11779 + edition =edition349,11836 + trigger =trigger350,11863 + steps =steps351,11890 + platform =platform352,11965 + depends_on =depends_on353,11995 + environment =environment356,12109 + pipelines =pipelines358,12150 + name =name360,12186 + edition =edition361,12273 + trigger =trigger362,12304 + services =services363,12335 + steps =steps364,12362 + environment =environment365,12440 + volumes =volumes366,12479 + name =name374,12711 + edition =edition375,12786 + trigger =trigger376,12821 + services =services377,12856 + steps =steps378,12893 + environment =environment379,13213 + volumes =volumes380,13256 + deps =deps383,13317 +def enterprise2_pipelines(prefix = "", ver_mode = ver_mode, trigger = release_trigger):enterprise2_pipelines397,13769 + environment =environment412,14364 + edition =edition415,14424 + volumes =volumes416,14451 + package_steps =package_steps417,14501 + publish_steps =publish_steps418,14524 + should_publish =should_publish419,14547 + should_upload =should_upload420,14590 + include_enterprise =include_enterprise421,14660 + edition2 =edition2422,14709 + init_steps =init_steps423,14738 + build_steps =build_steps431,14945 + fetch_images =fetch_images442,15355 + upload_cdn =upload_cdn444,15497 + step =step458,16187 + deps_on_clone_enterprise_step =deps_on_clone_enterprise_step462,16321 + pipelines =pipelines472,16608 + name =name474,16644 + edition =edition475,16742 + trigger =trigger476,16773 + services =services477,16804 + steps =steps478,16831 + volumes =volumes479,16909 + environment =environment480,16940 +def publish_artifacts_step(mode):publish_artifacts_step486,17019 + security =security487,17053 + security =security489,17098 +def publish_artifacts_pipelines(mode):publish_artifacts_pipelines501,17538 + trigger =trigger502,17577 + steps =steps506,17655 + name =name512,17768 + trigger =trigger513,17820 + steps =steps514,17847 + edition =edition515,17870 + environment =environment516,17895 +def publish_packages_pipeline():publish_packages_pipeline519,17945 + trigger =trigger526,18162 + oss_steps =oss_steps530,18244 + enterprise_steps =enterprise_steps538,18560 + deps =deps545,18903 + name =name552,19062 + trigger =trigger553,19101 + steps =steps554,19128 + edition =edition555,19155 + depends_on =depends_on556,19180 + environment =environment557,19207 + name =name559,19266 + trigger =trigger560,19312 + steps =steps561,19339 + edition =edition562,19373 + depends_on =depends_on563,19398 + environment =environment564,19425 +def publish_npm_pipelines(mode):publish_npm_pipelines567,19482 + trigger =trigger568,19515 + steps =steps572,19593 + name =name580,19772 + trigger =trigger581,19827 + steps =steps582,19854 + edition =edition583,19877 + environment =environment584,19902 +def artifacts_page_pipeline():artifacts_page_pipeline587,19952 + trigger =trigger588,19983 + name =name593,20087 + trigger =trigger594,20128 + steps =steps595,20155 + edition =edition596,20220 + environment =environment597,20245 +def get_e2e_suffix():get_e2e_suffix600,20295 + +events/cron.star,1016 +aquasec_trivy_image =aquasec_trivy_image8,209 +def cronjobs(edition):cronjobs10,255 + grafana_com_nightly_pipeline =grafana_com_nightly_pipeline11,278 + cronName =cronName12,332 + name =name13,374 + steps =steps14,412 +def cron_job_pipeline(cronName, name, steps):cron_job_pipeline24,773 +def scan_docker_image_pipeline(edition, tag):scan_docker_image_pipeline43,1175 + edition =edition55,1530 + edition =edition57,1579 + docker_image =docker_image59,1608 + cronName =cronName62,1695 + name =name63,1725 + steps =steps64,1775 +def scan_docker_image_unkown_low_medium_vulnerabilities_step(docker_image):scan_docker_image_unkown_low_medium_vulnerabilities_step71,2047 +def scan_docker_image_high_critical_vulnerabilities_step(docker_image):scan_docker_image_high_critical_vulnerabilities_step80,2353 +def slack_job_failed_step(channel, image):slack_job_failed_step89,2646 +def post_to_grafana_com_step():post_to_grafana_com_step103,3069 + +events/main.star,633 +ver_mode =ver_mode49,966 +trigger =trigger50,984 +def main_pipelines(edition):main_pipelines62,1168 + drone_change_trigger =drone_change_trigger63,1197 + pipelines =pipelines79,1513 + name =name89,1951 + slack_channel =slack_channel90,1994 + trigger =trigger91,2045 + template =template92,2089 + secret =secret93,2135 + name =name97,2276 + slack_channel =slack_channel98,2310 + trigger =trigger99,2366 + depends_on =depends_on100,2425 + template =template101,2563 + secret =secret102,2604 + +events/pr.star,252 +ver_mode =ver_mode48,997 +trigger =trigger49,1013 +def pr_pipelines(edition):pr_pipelines62,1198 +def get_pr_trigger(include_paths = None, exclude_paths = None):get_pr_trigger76,2396 + paths_ex =paths_ex91,3080 + paths_in =paths_in92,3115 + +services/services.star,225 +def integration_test_services_volumes():integration_test_services_volumes5,79 +def integration_test_services(edition):integration_test_services14,292 + services =services15,332 +def ldap_service():ldap_service59,1616 + +utils/utils.star,561 +failure_template =failure_template11,191 +drone_change_template =drone_change_template12,509 + services =services19,932 + platform =platform20,955 + depends_on =depends_on21,983 + environment =environment22,1008 + volumes =volumes23,1036 + platform_conf =platform_conf50,2166 + platform_conf =platform_conf62,2534 + pipeline =pipeline70,2713 +def notify_pipeline(name, slack_channel, trigger, depends_on = [], template = None, secret = None):notify_pipeline105,3545 + trigger =trigger106,3645 + +pipelines/trigger_downstream.star,440 +trigger =trigger14,249 +def enterprise_downstream_pipeline(edition, ver_mode):enterprise_downstream_pipeline26,433 + environment =environment27,488 + steps =steps28,527 + deps =deps29,587 + name =name31,672 + edition =edition32,714 + trigger =trigger33,741 + services =services34,768 + steps =steps35,791 + depends_on =depends_on36,814 + environment =environment37,841 + +pipelines/verify_starlark.star,323 +def verify_starlark(trigger, ver_mode):verify_starlark17,305 + environment =environment18,345 + steps =steps19,382 + name =name26,546 + edition =edition27,600 + trigger =trigger28,625 + services =services29,652 + steps =steps30,675 + environment =environment31,698 + +pipelines/build.star,508 +def build_e2e(trigger, ver_mode, edition):build_e2e39,936 + environment =environment50,1096 + variants =variants51,1135 + init_steps =init_steps52,1219 + build_steps =build_steps61,1491 + publish_suffix =publish_suffix107,4049 + publish_suffix =publish_suffix109,4100 + name =name112,4158 + edition =edition113,4224 + environment =environment114,4249 + services =services115,4284 + steps =steps116,4307 + trigger =trigger117,4349 + +pipelines/shellcheck.star,386 +trigger =trigger15,235 +def shellcheck_step():shellcheck_step31,483 +def shellcheck_pipeline():shellcheck_pipeline43,725 + environment =environment44,752 + steps =steps45,789 + name =name50,886 + edition =edition51,918 + trigger =trigger52,943 + services =services53,970 + steps =steps54,993 + environment =environment55,1016 + +pipelines/verify_drone.star,317 +def verify_drone(trigger, ver_mode):verify_drone17,293 + environment =environment18,330 + steps =steps19,367 + name =name26,528 + edition =edition27,579 + trigger =trigger28,604 + services =services29,631 + steps =steps30,654 + environment =environment31,677 + +pipelines/test_backend.star,474 +def test_backend(trigger, ver_mode, edition = "oss"):test_backend23,463 + environment =environment35,882 + init_steps =init_steps36,921 + test_steps =test_steps46,1291 + pipeline_name =pipeline_name51,1387 + pipeline_name =pipeline_name53,1492 + name =name55,1584 + edition =edition56,1614 + trigger =trigger57,1641 + services =services58,1668 + steps =steps59,1691 + environment =environment60,1732 + +pipelines/lint_frontend.star,415 +def lint_frontend_pipeline(trigger, ver_mode):lint_frontend_pipeline16,260 + environment =environment26,546 + yarn_step =yarn_step27,583 + init_steps =init_steps29,660 + test_steps =test_steps33,736 + name =name37,812 + edition =edition38,864 + trigger =trigger39,889 + services =services40,916 + steps =steps41,939 + environment =environment42,980 + +pipelines/docs.star,494 +docs_paths =docs_paths19,383 +def docs_pipelines(edition, ver_mode, trigger):docs_pipelines28,511 + environment =environment29,559 + steps =steps30,598 + name =name40,815 + edition =edition41,858 + trigger =trigger42,885 + services =services43,912 + steps =steps44,935 + environment =environment45,958 +def lint_docs():lint_docs48,1000 +def trigger_docs_main():trigger_docs_main63,1328 +def trigger_docs_pr():trigger_docs_pr72,1478 + +pipelines/test_frontend.star,476 +def test_frontend(trigger, ver_mode, edition = "oss"):test_frontend20,374 + environment =environment32,794 + init_steps =init_steps33,833 + test_steps =test_steps41,1102 + pipeline_name =pipeline_name45,1205 + pipeline_name =pipeline_name47,1311 + name =name49,1404 + edition =edition50,1434 + trigger =trigger51,1461 + services =services52,1488 + steps =steps53,1511 + environment =environment54,1552 + +pipelines/integration_tests.star,483 +def integration_tests(trigger, ver_mode, edition):integration_tests26,542 + environment =environment37,900 + services =services38,939 + volumes =volumes39,989 + init_steps =init_steps40,1039 + test_steps =test_steps48,1282 + name =name54,1412 + edition =edition55,1468 + trigger =trigger56,1493 + services =services57,1520 + steps =steps58,1549 + environment =environment59,1590 + volumes =volumes60,1625 + +pipelines/windows.star,954 +def windows(trigger, edition, ver_mode):windows17,339 + environment =environment29,798 + init_cmds =init_cmds30,837 + steps =steps38,1205 + bucket =bucket49,1497 + ver_part =ver_part51,1590 + dir =dir52,1628 + dir =dir54,1670 + bucket =bucket55,1695 + build_no =build_no56,1736 + ver_part =ver_part57,1780 + installer_commands =installer_commands58,1842 + committish =committish100,3763 + committish =committish102,3846 + committish =committish104,3906 + download_grabpl_step_cmds =download_grabpl_step_cmds107,4057 + clone_cmds =clone_cmds113,4363 + name =name146,5711 + edition =edition147,5742 + trigger =trigger148,5769 + steps =steps149,5830 + depends_on =depends_on150,5889 + platform =platform151,6007 + environment =environment152,6037 + +pipelines/lint_backend.star,418 +def lint_backend_pipeline(trigger, ver_mode):lint_backend_pipeline18,306 + environment =environment28,590 + wire_step =wire_step29,627 + init_steps =init_steps31,704 + test_steps =test_steps36,809 + name =name43,959 + edition =edition44,1010 + trigger =trigger45,1035 + services =services46,1062 + steps =steps47,1085 + environment =environment48,1126 + +pipelines/publish_images.star,998 +def publish_image_steps(edition, mode, docker_repo):publish_image_steps17,303 + additional_docker_repo =additional_docker_repo31,922 + additional_docker_repo =additional_docker_repo33,979 + steps =steps34,1034 +def publish_image_pipelines_public():publish_image_pipelines_public45,1369 + mode =mode51,1521 + trigger =trigger52,1541 + name =name57,1641 + trigger =trigger58,1694 + steps =steps59,1721 + edition =edition60,1813 + environment =environment61,1835 + name =name63,1894 + trigger =trigger64,1954 + steps =steps65,1981 + edition =edition66,2091 + environment =environment67,2113 +def publish_image_pipelines_security():publish_image_pipelines_security70,2170 + mode =mode71,2210 + trigger =trigger72,2232 + name =name77,2332 + trigger =trigger78,2392 + steps =steps79,2419 + edition =edition80,2529 + environment =environment81,2551 + +steps/lib.star,8579 +grabpl_version =grabpl_version7,181 +build_image =build_image8,208 +publish_image =publish_image9,254 +deploy_docker_image =deploy_docker_image10,304 +alpine_image =alpine_image11,380 +curl_image =curl_image12,411 +windows_image =windows_image13,452 +wix_image =wix_image14,501 +go_image =go_image15,536 +disable_tests =disable_tests17,564 +trigger_oss =trigger_oss18,586 +def slack_step(channel, template, secret):slack_step24,653 +def yarn_install_step(edition = "oss"):yarn_install_step35,918 + deps =deps36,958 + deps =deps38,1004 +def wire_install_step():wire_install_step48,1222 +def identify_runner_step(platform = "linux"):identify_runner_step60,1454 +def clone_enterprise_step(ver_mode):clone_enterprise_step78,1916 + committish =committish87,2193 + committish =committish89,2268 + committish =committish91,2317 +def init_enterprise_step(ver_mode):init_enterprise_step105,2747 + source_commit =source_commit115,3098 + source_commit =source_commit117,3151 + environment =environment118,3191 + token =token121,3280 + environment =environment123,3369 + token =token126,3458 + environment =environment128,3518 + token =token129,3543 +def download_grabpl_step(platform = "linux"):download_grabpl_step148,4147 +def lint_drone_step():lint_drone_step173,4973 +def lint_starlark_step():lint_starlark_step185,5216 +def enterprise_downstream_step(edition, ver_mode):enterprise_downstream_step206,6000 + repo =repo219,6482 + step =step225,6623 +def lint_backend_step():lint_backend_step247,7248 +def benchmark_ldap_step():benchmark_ldap_step265,7713 +def build_storybook_step(edition, ver_mode):build_storybook_step278,8087 +def store_storybook_step(edition, ver_mode, trigger = None):store_storybook_step300,8743 + commands =commands314,9202 + commands =commands323,9521 + step =step325,9593 + when_cond =when_cond338,10125 + step =step346,10330 +def e2e_tests_artifacts(edition):e2e_tests_artifacts349,10391 +def upload_cdn_step(edition, ver_mode, trigger = None):upload_cdn_step386,12378 + deps =deps397,12763 + step =step407,12970 + step =step420,13423 +def build_backend_step(edition, ver_mode, variants = None):build_backend_step423,13482 + variants_str =variants_str437,14070 + variants_str =variants_str439,14109 + cmds =cmds443,14256 + build_no =build_no449,14418 + cmds =cmds450,14461 +def build_frontend_step(edition, ver_mode):build_frontend_step468,14906 + build_no =build_no478,15246 + cmds =cmds482,15356 + cmds =cmds487,15505 +def build_frontend_package_step(edition, ver_mode):build_frontend_package_step505,15960 + build_no =build_no515,16312 + cmds =cmds519,16422 + cmds =cmds524,16580 +def build_plugins_step(edition, ver_mode):build_plugins_step542,17053 + env =env544,17121 + env =env548,17220 +def test_backend_step():test_backend_step563,17607 +def test_backend_integration_step():test_backend_integration_step575,17880 +def betterer_frontend_step(edition = "oss"):betterer_frontend_step587,18187 + deps =deps596,18427 +def test_frontend_step(edition = "oss"):test_frontend_step609,18728 + deps =deps618,18962 +def lint_frontend_step():lint_frontend_step634,19343 +def test_a11y_frontend_step(ver_mode, edition, port = 3001):test_a11y_frontend_step652,19793 + commands =commands664,20279 + failure =failure667,20345 + failure =failure672,20483 +def frontend_metrics_step(edition, trigger = None):frontend_metrics_step693,21146 + step =step706,21507 + step =step721,22007 +def codespell_step():codespell_step724,22066 +def package_step(edition, ver_mode, variants = None):package_step736,22468 + deps =deps750,23006 + variants_str =variants_str757,23167 + variants_str =variants_str759,23206 + sign_args =sign_args762,23332 + env =env763,23362 + test_args =test_args769,23628 + sign_args =sign_args771,23661 + env =env772,23684 + test_args =test_args773,23703 + cmds =cmds777,23829 + build_no =build_no784,24036 + cmds =cmds785,24079 +def grafana_server_step(edition, port = 3001):grafana_server_step798,24459 + package_file_pfx =package_file_pfx808,24729 + package_file_pfx =package_file_pfx810,24788 + package_file_pfx =package_file_pfx812,24889 + environment =environment814,24938 +def e2e_tests_step(suite, edition, port = 3001, tries = None):e2e_tests_step837,25554 + cmd =cmd838,25617 +def cloud_plugins_e2e_tests_step(suite, edition, cloud, trigger = None):cloud_plugins_e2e_tests_step856,26186 + environment =environment869,26649 + when =when870,26670 + when =when872,26700 + environment =environment874,26748 + when =when882,27129 + branch =branch888,27345 + step =step889,27401 + step =step901,27822 +def build_docs_website_step():build_docs_website_step904,27874 +def copy_packages_for_docker_step(edition = None):copy_packages_for_docker_step916,28272 +def build_docker_images_step(edition, archs = None, ubuntu = False, publish = False):build_docker_images_step929,28622 + cmd =cmd943,29193 + ubuntu_sfx =ubuntu_sfx947,29307 + ubuntu_sfx =ubuntu_sfx949,29342 + environment =environment955,29468 +def fetch_images_step(edition):fetch_images_step979,30079 +def publish_images_step(edition, ver_mode, mode, docker_repo, trigger = None):publish_images_step997,30745 + name =name1013,31562 + docker_repo =docker_repo1014,31585 + mode =mode1016,31663 + mode =mode1018,31709 + environment =environment1020,31728 + cmd =cmd1026,31912 + deps =deps1029,32041 + deps =deps1032,32147 + name =name1035,32250 + docker_repo =docker_repo1036,32273 + cmd =cmd1038,32459 + step =step1040,32565 + step =step1052,32929 +def postgres_integration_tests_step():postgres_integration_tests_step1056,32989 + cmds =cmds1057,33028 +def mysql_integration_tests_step():mysql_integration_tests_step1079,33850 + cmds =cmds1080,33886 +def redis_integration_tests_step():redis_integration_tests_step1100,34629 +def memcached_integration_tests_step():memcached_integration_tests_step1114,35026 +def release_canary_npm_packages_step(edition, trigger = None):release_canary_npm_packages_step1128,35435 + step =step1141,35805 + step =step1153,36143 +def enterprise2_suffix(edition):enterprise2_suffix1156,36202 +def upload_packages_step(edition, ver_mode, trigger = None):upload_packages_step1161,36320 + deps =deps1176,36816 + step =step1184,37036 + step =step1195,37471 +def publish_grafanacom_step(edition, ver_mode):publish_grafanacom_step1198,37530 + cmd =cmd1211,38044 + build_no =build_no1215,38188 + cmd =cmd1216,38231 +def publish_linux_packages_step(edition, package_manager = "deb"):publish_linux_packages_step1239,38866 +def get_windows_steps(edition, ver_mode):get_windows_steps1261,39989 + init_cmds =init_cmds1270,40281 + steps =steps1278,40649 + bucket =bucket1289,40941 + ver_part =ver_part1291,41034 + dir =dir1292,41072 + dir =dir1294,41114 + bucket =bucket1295,41139 + build_no =build_no1296,41180 + ver_part =ver_part1297,41224 + installer_commands =installer_commands1298,41286 + committish =committish1340,43207 + committish =committish1342,43290 + committish =committish1344,43350 + download_grabpl_step_cmds =download_grabpl_step_cmds1347,43501 + clone_cmds =clone_cmds1353,43807 +def verify_gen_cue_step(edition):verify_gen_cue_step1387,45152 + deps =deps1388,45186 +def verify_gen_jsonnet_step(edition):verify_gen_jsonnet_step1402,45694 + deps =deps1403,45732 +def trigger_test_release():trigger_test_release1417,46236 +def artifacts_page_step():artifacts_page_step1451,47731 +def end_to_end_tests_deps():end_to_end_tests_deps1466,48058 +def compile_build_cmd(edition = "oss"):compile_build_cmd1476,48321 + dependencies =dependencies1477,48361 + dependencies =dependencies1479,48432 +def get_trigger_storybook(ver_mode):get_trigger_storybook1492,48780 + trigger_storybook =trigger_storybook1500,49031 + trigger_storybook =trigger_storybook1502,49088 + trigger_storybook =trigger_storybook1506,49168 + +vault.star,444 +pull_secret =pull_secret4,87 +github_token =github_token5,120 +drone_token =drone_token6,150 +prerelease_bucket =prerelease_bucket7,178 +gcp_upload_artifacts_key =gcp_upload_artifacts_key8,218 +azure_sp_app_id =azure_sp_app_id9,272 +azure_sp_app_pw =azure_sp_app_pw10,308 +azure_tenant =azure_tenant11,344 +def from_secret(secret):from_secret13,375 +def vault_secret(name, path, key):vault_secret18,451 +def secrets():secrets28,633 + +version.star,116 +ver_mode =ver_mode12,197 +trigger =trigger13,225 +def version_branch_pipelines():version_branch_pipelines15,268 diff --git a/scripts/drone/events/cron.star b/scripts/drone/events/cron.star index d311214ea72..9b718f79572 100644 --- a/scripts/drone/events/cron.star +++ b/scripts/drone/events/cron.star @@ -1,110 +1,114 @@ -load('scripts/drone/vault.star', 'from_secret') +""" +This module provides functions for cronjob pipelines and steps used within. +""" + +load("scripts/drone/vault.star", "from_secret") load( - 'scripts/drone/steps/lib.star', - 'publish_image', - 'compile_build_cmd', + "scripts/drone/steps/lib.star", + "compile_build_cmd", + "publish_image", ) -aquasec_trivy_image = 'aquasec/trivy:0.21.0' - +aquasec_trivy_image = "aquasec/trivy:0.21.0" def cronjobs(): return [ - scan_docker_image_pipeline('latest'), - scan_docker_image_pipeline('main'), - scan_docker_image_pipeline('latest-ubuntu'), - scan_docker_image_pipeline('main-ubuntu'), + scan_docker_image_pipeline("latest"), + scan_docker_image_pipeline("main"), + scan_docker_image_pipeline("latest-ubuntu"), + scan_docker_image_pipeline("main-ubuntu"), grafana_com_nightly_pipeline(), ] - def cron_job_pipeline(cronName, name, steps): return { - 'kind': 'pipeline', - 'type': 'docker', - 'platform': { - 'os': 'linux', - 'arch': 'amd64', + "kind": "pipeline", + "type": "docker", + "platform": { + "os": "linux", + "arch": "amd64", }, - 'name': name, - 'trigger': { - 'event': 'cron', - 'cron': cronName, + "name": name, + "trigger": { + "event": "cron", + "cron": cronName, }, - 'clone': { - 'retries': 3, + "clone": { + "retries": 3, }, - 'steps': steps, + "steps": steps, } - def scan_docker_image_pipeline(tag): - dockerImage = 'grafana/{}:{}'.format('grafana', tag) + """Generates a cronjob pipeline for nightly scans of grafana Docker images. + + Args: + tag: determines which image tag is scanned. + + Returns: + Drone cronjob pipeline. + """ + docker_image = "grafana/grafana:{}".format(tag) return cron_job_pipeline( - cronName='nightly', - name='scan-' + dockerImage + '-image', - steps=[ - scan_docker_image_unkown_low_medium_vulnerabilities_step(dockerImage), - scan_docker_image_high_critical_vulnerabilities_step(dockerImage), - slack_job_failed_step('grafana-backend-ops', dockerImage), + cronName = "nightly", + name = "scan-" + docker_image + "-image", + steps = [ + scan_docker_image_unkown_low_medium_vulnerabilities_step(docker_image), + scan_docker_image_high_critical_vulnerabilities_step(docker_image), + slack_job_failed_step("grafana-backend-ops", docker_image), ], ) - -def scan_docker_image_unkown_low_medium_vulnerabilities_step(dockerImage): +def scan_docker_image_unkown_low_medium_vulnerabilities_step(docker_image): return { - 'name': 'scan-unkown-low-medium-vulnerabilities', - 'image': aquasec_trivy_image, - 'commands': [ - 'trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM ' + dockerImage, + "name": "scan-unkown-low-medium-vulnerabilities", + "image": aquasec_trivy_image, + "commands": [ + "trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM " + docker_image, ], } - -def scan_docker_image_high_critical_vulnerabilities_step(dockerImage): +def scan_docker_image_high_critical_vulnerabilities_step(docker_image): return { - 'name': 'scan-high-critical-vulnerabilities', - 'image': aquasec_trivy_image, - 'commands': [ - 'trivy --exit-code 1 --severity HIGH,CRITICAL ' + dockerImage, + "name": "scan-high-critical-vulnerabilities", + "image": aquasec_trivy_image, + "commands": [ + "trivy --exit-code 1 --severity HIGH,CRITICAL " + docker_image, ], } - def slack_job_failed_step(channel, image): return { - 'name': 'slack-notify-failure', - 'image': 'plugins/slack', - 'settings': { - 'webhook': from_secret('slack_webhook_backend'), - 'channel': channel, - 'template': 'Nightly docker image scan job for ' - + image - + ' failed: {{build.link}}', + "name": "slack-notify-failure", + "image": "plugins/slack", + "settings": { + "webhook": from_secret("slack_webhook_backend"), + "channel": channel, + "template": "Nightly docker image scan job for " + + image + + " failed: {{build.link}}", }, - 'when': {'status': 'failure'}, + "when": {"status": "failure"}, } - def post_to_grafana_com_step(): return { - 'name': 'post-to-grafana-com', - 'image': publish_image, - 'environment': { - 'GRAFANA_COM_API_KEY': from_secret('grafana_api_key'), - 'GCP_KEY': from_secret('gcp_key'), + "name": "post-to-grafana-com", + "image": publish_image, + "environment": { + "GRAFANA_COM_API_KEY": from_secret("grafana_api_key"), + "GCP_KEY": from_secret("gcp_key"), }, - 'depends_on': ['compile-build-cmd'], - 'commands': ['./bin/build publish grafana-com --edition oss'], + "depends_on": ["compile-build-cmd"], + "commands": ["./bin/build publish grafana-com --edition oss"], } - def grafana_com_nightly_pipeline(): return cron_job_pipeline( - cronName='grafana-com-nightly', - name='grafana-com-nightly', - steps=[ + cronName = "grafana-com-nightly", + name = "grafana-com-nightly", + steps = [ compile_build_cmd(), post_to_grafana_com_step(), ], diff --git a/scripts/drone/events/main.star b/scripts/drone/events/main.star index bc238ecf719..ab003e15abb 100644 --- a/scripts/drone/events/main.star +++ b/scripts/drone/events/main.star @@ -1,125 +1,115 @@ -load( - 'scripts/drone/utils/utils.star', - 'pipeline', - 'notify_pipeline', - 'failure_template', - 'drone_change_template', -) +""" +This module returns all the pipelines used in the event of pushes to the main branch. +""" load( - 'scripts/drone/pipelines/docs.star', - 'docs_pipelines', - 'trigger_docs_main', + "scripts/drone/utils/utils.star", + "drone_change_template", + "failure_template", + "notify_pipeline", ) - load( - 'scripts/drone/pipelines/test_frontend.star', - 'test_frontend', + "scripts/drone/pipelines/docs.star", + "docs_pipelines", + "trigger_docs_main", ) - load( - 'scripts/drone/pipelines/test_backend.star', - 'test_backend', + "scripts/drone/pipelines/test_frontend.star", + "test_frontend", ) - load( - 'scripts/drone/pipelines/integration_tests.star', - 'integration_tests', + "scripts/drone/pipelines/test_backend.star", + "test_backend", ) - load( - 'scripts/drone/pipelines/build.star', - 'build_e2e', + "scripts/drone/pipelines/integration_tests.star", + "integration_tests", ) - load( - 'scripts/drone/pipelines/windows.star', - 'windows', + "scripts/drone/pipelines/build.star", + "build_e2e", ) - load( - 'scripts/drone/pipelines/trigger_downstream.star', - 'enterprise_downstream_pipeline', + "scripts/drone/pipelines/windows.star", + "windows", ) - load( - 'scripts/drone/pipelines/lint_backend.star', - 'lint_backend_pipeline', + "scripts/drone/pipelines/trigger_downstream.star", + "enterprise_downstream_pipeline", ) - load( - 'scripts/drone/pipelines/lint_frontend.star', - 'lint_frontend_pipeline', + "scripts/drone/pipelines/lint_backend.star", + "lint_backend_pipeline", +) +load( + "scripts/drone/pipelines/lint_frontend.star", + "lint_frontend_pipeline", ) -load('scripts/drone/vault.star', 'from_secret') - - -ver_mode = 'main' +ver_mode = "main" trigger = { - 'event': [ - 'push', + "event": [ + "push", ], - 'branch': 'main', - 'paths': { - 'exclude': [ - '*.md', - 'docs/**', - 'latest.json', + "branch": "main", + "paths": { + "exclude": [ + "*.md", + "docs/**", + "latest.json", ], }, } - def main_pipelines(): drone_change_trigger = { - 'event': [ - 'push', + "event": [ + "push", ], - 'branch': 'main', - 'repo': [ - 'grafana/grafana', + "branch": "main", + "repo": [ + "grafana/grafana", ], - 'paths': { - 'include': [ - '.drone.yml', + "paths": { + "include": [ + ".drone.yml", ], - 'exclude': [ - 'exclude', + "exclude": [ + "exclude", ], }, } pipelines = [ docs_pipelines(ver_mode, trigger_docs_main()), - test_frontend(trigger, ver_mode, committish='${DRONE_COMMIT}'), + test_frontend(trigger, ver_mode), lint_frontend_pipeline(trigger, ver_mode), - test_backend(trigger, ver_mode, committish='${DRONE_COMMIT}'), + test_backend(trigger, ver_mode), lint_backend_pipeline(trigger, ver_mode), build_e2e(trigger, ver_mode), - integration_tests(trigger, prefix=ver_mode), - windows(trigger, edition='oss', ver_mode=ver_mode), + integration_tests(trigger, prefix = ver_mode), + windows(trigger, edition = "oss", ver_mode = ver_mode), notify_pipeline( - name='notify-drone-changes', - slack_channel='slack-webhooks-test', - trigger=drone_change_trigger, - template=drone_change_template, - secret='drone-changes-webhook', + name = "notify-drone-changes", + slack_channel = "slack-webhooks-test", + trigger = drone_change_trigger, + template = drone_change_template, + secret = "drone-changes-webhook", ), enterprise_downstream_pipeline(), notify_pipeline( - name='main-notify', - slack_channel='grafana-ci-notifications', - trigger=dict(trigger, status=['failure']), - depends_on=[ - 'main-test-frontend', - 'main-test-backend', - 'main-build-e2e-publish', - 'main-integration-tests', - 'main-windows', + name = "main-notify", + slack_channel = "grafana-ci-notifications", + trigger = dict(trigger, status = ["failure"]), + depends_on = [ + "main-test-frontend", + "main-test-backend", + "main-build-e2e-publish", + "main-integration-tests", + "main-windows", ], - template=failure_template, - secret='slack_webhook', + template = failure_template, + secret = "slack_webhook", ), ] diff --git a/scripts/drone/events/pr.star b/scripts/drone/events/pr.star index d9afe947305..73ab62d2680 100644 --- a/scripts/drone/events/pr.star +++ b/scripts/drone/events/pr.star @@ -1,143 +1,155 @@ -load( - 'scripts/drone/utils/utils.star', - 'pipeline', -) +""" +This module returns all pipelines used in the event of a pull request. +It also includes a function generating a PR trigger from a list of included and excluded paths. +""" load( - 'scripts/drone/pipelines/test_frontend.star', - 'test_frontend', + "scripts/drone/pipelines/test_frontend.star", + "test_frontend", ) - load( - 'scripts/drone/pipelines/test_backend.star', - 'test_backend', + "scripts/drone/pipelines/test_backend.star", + "test_backend", ) - load( - 'scripts/drone/pipelines/integration_tests.star', - 'integration_tests', + "scripts/drone/pipelines/integration_tests.star", + "integration_tests", ) - load( - 'scripts/drone/pipelines/build.star', - 'build_e2e', + "scripts/drone/pipelines/build.star", + "build_e2e", ) - load( - 'scripts/drone/pipelines/verify_drone.star', - 'verify_drone', + "scripts/drone/pipelines/verify_drone.star", + "verify_drone", ) - load( - 'scripts/drone/pipelines/docs.star', - 'docs_pipelines', - 'trigger_docs_pr', + "scripts/drone/pipelines/verify_starlark.star", + "verify_starlark", ) - load( - 'scripts/drone/pipelines/shellcheck.star', - 'shellcheck_pipeline', + "scripts/drone/pipelines/docs.star", + "docs_pipelines", + "trigger_docs_pr", ) - load( - 'scripts/drone/pipelines/lint_backend.star', - 'lint_backend_pipeline', + "scripts/drone/pipelines/shellcheck.star", + "shellcheck_pipeline", ) - load( - 'scripts/drone/pipelines/lint_frontend.star', - 'lint_frontend_pipeline', + "scripts/drone/pipelines/lint_backend.star", + "lint_backend_pipeline", +) +load( + "scripts/drone/pipelines/lint_frontend.star", + "lint_frontend_pipeline", ) -ver_mode = 'pr' +ver_mode = "pr" trigger = { - 'event': [ - 'pull_request', + "event": [ + "pull_request", ], - 'paths': { - 'exclude': [ - '*.md', - 'docs/**', - 'latest.json', + "paths": { + "exclude": [ + "*.md", + "docs/**", + "latest.json", ], }, } - def pr_pipelines(): return [ verify_drone( get_pr_trigger( - include_paths=['scripts/drone/**', '.drone.yml', '.drone.star'] + include_paths = ["scripts/drone/**", ".drone.yml", ".drone.star"], + ), + ver_mode, + ), + verify_starlark( + get_pr_trigger( + include_paths = ["scripts/drone/**", ".drone.star"], ), ver_mode, ), test_frontend( get_pr_trigger( - exclude_paths=['pkg/**', 'packaging/**', 'go.sum', 'go.mod'] + exclude_paths = ["pkg/**", "packaging/**", "go.sum", "go.mod"], ), ver_mode, - committish='${DRONE_COMMIT}', ), lint_frontend_pipeline( get_pr_trigger( - exclude_paths=['pkg/**', 'packaging/**', 'go.sum', 'go.mod'] + exclude_paths = ["pkg/**", "packaging/**", "go.sum", "go.mod"], ), ver_mode, ), test_backend( get_pr_trigger( - include_paths=[ - 'pkg/**', - 'packaging/**', - '.drone.yml', - 'conf/**', - 'go.sum', - 'go.mod', - 'public/app/plugins/**/plugin.json', - 'devenv/**', - ] + include_paths = [ + "pkg/**", + "packaging/**", + ".drone.yml", + "conf/**", + "go.sum", + "go.mod", + "public/app/plugins/**/plugin.json", + "devenv/**", + ], ), ver_mode, - committish='${DRONE_COMMIT}', ), lint_backend_pipeline( get_pr_trigger( - include_paths=[ - 'pkg/**', - 'packaging/**', - 'conf/**', - 'go.sum', - 'go.mod', - 'public/app/plugins/**/plugin.json', - 'devenv/**', - '.bingo/**', - ] + include_paths = [ + "pkg/**", + "packaging/**", + "conf/**", + "go.sum", + "go.mod", + "public/app/plugins/**/plugin.json", + "devenv/**", + ".bingo/**", + ], ), ver_mode, ), build_e2e(trigger, ver_mode), integration_tests( get_pr_trigger( - include_paths=[ - 'pkg/**', - 'packaging/**', - '.drone.yml', - 'conf/**', - 'go.sum', - 'go.mod', - 'public/app/plugins/**/plugin.json', - ] + include_paths = [ + "pkg/**", + "packaging/**", + ".drone.yml", + "conf/**", + "go.sum", + "go.mod", + "public/app/plugins/**/plugin.json", + ], ), - prefix=ver_mode, + prefix = ver_mode, ), docs_pipelines(ver_mode, trigger_docs_pr()), shellcheck_pipeline(), ] +def get_pr_trigger(include_paths = None, exclude_paths = None): + """Generates a trigger filter from the lists of included and excluded path patterns. -def get_pr_trigger(include_paths=None, exclude_paths=None): - paths_ex = ['docs/**', '*.md'] + This function is primarily intended to generate a trigger for code changes + as the patterns 'docs/**' and '*.md' are always excluded. + + Args: + include_paths: a list of path patterns using the same syntax as gitignore. + Changes affecting files matching these path patterns trigger the pipeline. + exclude_paths: a list of path patterns using the same syntax as gitignore. + Changes affecting files matching these path patterns do not trigger the pipeline. + + Returns: + Drone trigger. + """ + paths_ex = ["docs/**", "*.md"] paths_in = [] if include_paths: for path in include_paths: @@ -146,11 +158,11 @@ def get_pr_trigger(include_paths=None, exclude_paths=None): for path in exclude_paths: paths_ex.extend([path]) return { - 'event': [ - 'pull_request', + "event": [ + "pull_request", ], - 'paths': { - 'exclude': paths_ex, - 'include': paths_in, + "paths": { + "exclude": paths_ex, + "include": paths_in, }, } diff --git a/scripts/drone/events/release.star b/scripts/drone/events/release.star index c1983d7dee9..d9edb1c9bee 100644 --- a/scripts/drone/events/release.star +++ b/scripts/drone/events/release.star @@ -1,155 +1,137 @@ -load( - 'scripts/drone/steps/lib.star', - 'artifacts_page_step', - 'benchmark_ldap_step', - 'build_backend_step', - 'build_docker_images_step', - 'build_frontend_package_step', - 'build_frontend_step', - 'build_image', - 'build_plugins_step', - 'build_storybook_step', - 'clone_enterprise_step', - 'compile_build_cmd', - 'copy_packages_for_docker_step', - 'download_grabpl_step', - 'e2e_tests_artifacts', - 'e2e_tests_step', - 'fetch_images_step', - 'get_windows_steps', - 'grafana_server_step', - 'identify_runner_step', - 'init_enterprise_step', - 'lint_backend_step', - 'lint_drone_step', - 'lint_frontend_step', - 'memcached_integration_tests_step', - 'mysql_integration_tests_step', - 'package_step', - 'postgres_integration_tests_step', - 'publish_grafanacom_step', - 'publish_image', - 'publish_images_step', - 'publish_linux_packages_step', - 'redis_integration_tests_step', - 'store_storybook_step', - 'test_backend_integration_step', - 'test_backend_step', - 'test_frontend_step', - 'trigger_oss', - 'upload_cdn_step', - 'upload_packages_step', - 'verify_gen_cue_step', - 'verify_gen_jsonnet_step', - 'wire_install_step', - 'yarn_install_step', -) +""" +This module returns all the pipelines used in the event of a release along with supporting functions. +""" load( - 'scripts/drone/services/services.star', - 'integration_test_services', - 'integration_test_services_volumes', - 'ldap_service', + "scripts/drone/steps/lib.star", + "artifacts_page_step", + "build_backend_step", + "build_docker_images_step", + "build_frontend_package_step", + "build_frontend_step", + "build_image", + "build_plugins_step", + "build_storybook_step", + "clone_enterprise_step", + "compile_build_cmd", + "copy_packages_for_docker_step", + "download_grabpl_step", + "e2e_tests_artifacts", + "e2e_tests_step", + "fetch_images_step", + "get_windows_steps", + "grafana_server_step", + "identify_runner_step", + "init_enterprise_step", + "memcached_integration_tests_step", + "mysql_integration_tests_step", + "package_step", + "postgres_integration_tests_step", + "publish_grafanacom_step", + "publish_image", + "publish_images_step", + "publish_linux_packages_step", + "redis_integration_tests_step", + "store_storybook_step", + "trigger_oss", + "upload_cdn_step", + "upload_packages_step", + "verify_gen_cue_step", + "verify_gen_jsonnet_step", + "wire_install_step", + "yarn_install_step", ) - load( - 'scripts/drone/utils/utils.star', - 'pipeline', - 'notify_pipeline', - 'failure_template', - 'drone_change_template', - 'with_deps', + "scripts/drone/services/services.star", + "integration_test_services", + "integration_test_services_volumes", ) - load( - 'scripts/drone/pipelines/test_frontend.star', - 'test_frontend', - 'test_frontend_enterprise', + "scripts/drone/utils/utils.star", + "pipeline", + "with_deps", ) - load( - 'scripts/drone/pipelines/test_backend.star', - 'test_backend', - 'test_backend_enterprise', + "scripts/drone/pipelines/test_frontend.star", + "test_frontend", + "test_frontend_enterprise", ) - load( - 'scripts/drone/vault.star', - 'from_secret', - 'pull_secret', - 'drone_token', - 'prerelease_bucket', + "scripts/drone/pipelines/test_backend.star", + "test_backend", + "test_backend_enterprise", ) +load("scripts/drone/vault.star", "from_secret", "prerelease_bucket") -ver_mode = 'release' +ver_mode = "release" release_trigger = { - 'event': {'exclude': ['promote']}, - 'ref': [ - 'refs/tags/v*', + "event": {"exclude": ["promote"]}, + "ref": [ + "refs/tags/v*", ], } - def store_npm_packages_step(): return { - 'name': 'store-npm-packages', - 'image': build_image, - 'depends_on': [ - 'compile-build-cmd', - 'build-frontend-packages', + "name": "store-npm-packages", + "image": build_image, + "depends_on": [ + "compile-build-cmd", + "build-frontend-packages", ], - 'environment': { - 'GCP_KEY': from_secret('gcp_key'), - 'PRERELEASE_BUCKET': from_secret(prerelease_bucket), + "environment": { + "GCP_KEY": from_secret("gcp_key"), + "PRERELEASE_BUCKET": from_secret(prerelease_bucket), }, - 'commands': ['./bin/build artifacts npm store --tag ${DRONE_TAG}'], + "commands": ["./bin/build artifacts npm store --tag ${DRONE_TAG}"], } - def retrieve_npm_packages_step(): return { - 'name': 'retrieve-npm-packages', - 'image': publish_image, - 'depends_on': [ - 'compile-build-cmd', - 'yarn-install', + "name": "retrieve-npm-packages", + "image": publish_image, + "depends_on": [ + "compile-build-cmd", + "yarn-install", ], - 'failure': 'ignore', - 'environment': { - 'GCP_KEY': from_secret('gcp_key'), - 'PRERELEASE_BUCKET': from_secret(prerelease_bucket), + "failure": "ignore", + "environment": { + "GCP_KEY": from_secret("gcp_key"), + "PRERELEASE_BUCKET": from_secret(prerelease_bucket), }, - 'commands': ['./bin/build artifacts npm retrieve --tag ${DRONE_TAG}'], + "commands": ["./bin/build artifacts npm retrieve --tag ${DRONE_TAG}"], } - def release_npm_packages_step(): return { - 'name': 'release-npm-packages', - 'image': build_image, - 'depends_on': [ - 'compile-build-cmd', - 'retrieve-npm-packages', + "name": "release-npm-packages", + "image": build_image, + "depends_on": [ + "compile-build-cmd", + "retrieve-npm-packages", ], - 'failure': 'ignore', - 'environment': { - 'NPM_TOKEN': from_secret('npm_token'), + "failure": "ignore", + "environment": { + "NPM_TOKEN": from_secret("npm_token"), }, - 'commands': ['./bin/build artifacts npm release --tag ${DRONE_TAG}'], + "commands": ["./bin/build artifacts npm release --tag ${DRONE_TAG}"], } +def oss_pipelines(ver_mode = ver_mode, trigger = release_trigger): + """Generates all pipelines used for Grafana OSS. -def oss_pipelines(ver_mode=ver_mode, trigger=release_trigger): - if ver_mode == 'release': - committish = '${DRONE_TAG}' - elif ver_mode == 'release-branch': - committish = '${DRONE_BRANCH}' - else: - committish = '${DRONE_COMMIT}' + Args: + ver_mode: controls which steps are included in the pipeline. + Defaults to 'release'. + trigger: controls which events can trigger the pipeline execution. + Defaults to tag events for tags with a 'v' prefix. - environment = {'EDITION': 'oss'} + Returns: + List of Drone pipelines. + """ + environment = {"EDITION": "oss"} - services = integration_test_services(edition='oss') + services = integration_test_services(edition = "oss") volumes = integration_test_services_volumes() init_steps = [ @@ -162,46 +144,50 @@ def oss_pipelines(ver_mode=ver_mode, trigger=release_trigger): ] build_steps = [ - build_backend_step(edition='oss', ver_mode=ver_mode), - build_frontend_step(edition='oss', ver_mode=ver_mode), - build_frontend_package_step(edition='oss', ver_mode=ver_mode), - build_plugins_step(edition='oss', ver_mode=ver_mode), - package_step(edition='oss', ver_mode=ver_mode), + build_backend_step(edition = "oss", ver_mode = ver_mode), + build_frontend_step(edition = "oss", ver_mode = ver_mode), + build_frontend_package_step(edition = "oss", ver_mode = ver_mode), + build_plugins_step(edition = "oss", ver_mode = ver_mode), + package_step(edition = "oss", ver_mode = ver_mode), copy_packages_for_docker_step(), - build_docker_images_step(edition='oss', ver_mode=ver_mode, publish=True), + build_docker_images_step(edition = "oss", publish = True), build_docker_images_step( - edition='oss', ver_mode=ver_mode, publish=True, ubuntu=True + edition = "oss", + publish = True, + ubuntu = True, ), - grafana_server_step(edition='oss'), - e2e_tests_step('dashboards-suite', tries=3), - e2e_tests_step('smoke-tests-suite', tries=3), - e2e_tests_step('panels-suite', tries=3), - e2e_tests_step('various-suite', tries=3), + grafana_server_step(edition = "oss"), + e2e_tests_step("dashboards-suite", tries = 3), + e2e_tests_step("smoke-tests-suite", tries = 3), + e2e_tests_step("panels-suite", tries = 3), + e2e_tests_step("various-suite", tries = 3), e2e_tests_artifacts(), - build_storybook_step(ver_mode=ver_mode), + build_storybook_step(ver_mode = ver_mode), ] publish_steps = [] if ver_mode in ( - 'release', - 'release-branch', + "release", + "release-branch", ): publish_steps.extend( [ - upload_cdn_step(edition='oss', ver_mode=ver_mode, trigger=trigger_oss), + upload_cdn_step(edition = "oss", ver_mode = ver_mode, trigger = trigger_oss), upload_packages_step( - edition='oss', ver_mode=ver_mode, trigger=trigger_oss + edition = "oss", + ver_mode = ver_mode, + trigger = trigger_oss, ), - ] + ], ) - if ver_mode in ('release',): + if ver_mode in ("release",): publish_steps.extend( [ - store_storybook_step(ver_mode=ver_mode), + store_storybook_step(ver_mode = ver_mode), store_npm_packages_step(), - ] + ], ) integration_test_steps = [ @@ -210,74 +196,84 @@ def oss_pipelines(ver_mode=ver_mode, trigger=release_trigger): ] windows_pipeline = pipeline( - name='{}-oss-windows'.format(ver_mode), - edition='oss', - trigger=trigger, - steps=get_windows_steps(edition='oss', ver_mode=ver_mode), - platform='windows', - depends_on=[ + name = "{}-oss-windows".format(ver_mode), + edition = "oss", + trigger = trigger, + steps = get_windows_steps(edition = "oss", ver_mode = ver_mode), + platform = "windows", + depends_on = [ # 'oss-build-e2e-publish-{}'.format(ver_mode), - '{}-oss-build-e2e-publish'.format(ver_mode), - '{}-oss-test-frontend'.format(ver_mode), - '{}-oss-test-backend'.format(ver_mode), - '{}-oss-integration-tests'.format(ver_mode), + "{}-oss-build-e2e-publish".format(ver_mode), + "{}-oss-test-frontend".format(ver_mode), + "{}-oss-test-backend".format(ver_mode), + "{}-oss-integration-tests".format(ver_mode), ], - environment=environment, + environment = environment, ) pipelines = [ pipeline( - name='{}-oss-build-e2e-publish'.format(ver_mode), - edition='oss', - trigger=trigger, - services=[], - steps=init_steps + build_steps + publish_steps, - environment=environment, - volumes=volumes, + name = "{}-oss-build-e2e-publish".format(ver_mode), + edition = "oss", + trigger = trigger, + services = [], + steps = init_steps + build_steps + publish_steps, + environment = environment, + volumes = volumes, ), - test_frontend(trigger, ver_mode, committish=committish), - test_backend(trigger, ver_mode, committish=committish), + test_frontend(trigger, ver_mode), + test_backend(trigger, ver_mode), pipeline( - name='{}-oss-integration-tests'.format(ver_mode), - edition='oss', - trigger=trigger, - services=services, - steps=[ - download_grabpl_step(), - identify_runner_step(), - verify_gen_cue_step(), - verify_gen_jsonnet_step(), - wire_install_step(), - ] - + integration_test_steps, - environment=environment, - volumes=volumes, + name = "{}-oss-integration-tests".format(ver_mode), + edition = "oss", + trigger = trigger, + services = services, + steps = [ + download_grabpl_step(), + identify_runner_step(), + verify_gen_cue_step(), + verify_gen_jsonnet_step(), + wire_install_step(), + ] + + integration_test_steps, + environment = environment, + volumes = volumes, ), windows_pipeline, ] return pipelines +def enterprise_pipelines(ver_mode = ver_mode, trigger = release_trigger): + """Generates all pipelines used for Grafana Enterprise. -def enterprise_pipelines(ver_mode=ver_mode, trigger=release_trigger): - if ver_mode == 'release': - committish = '${DRONE_TAG}' - elif ver_mode == 'release-branch': - committish = '${DRONE_BRANCH}' + Args: + ver_mode: controls which steps are included in the pipeline. + Defaults to 'release'. + trigger: controls which events can trigger the pipeline execution. + Defaults to tag events for tags with a 'v' prefix. + + Returns: + List of Drone pipelines. + """ + if ver_mode == "release": + committish = "${DRONE_TAG}" + elif ver_mode == "release-branch": + committish = "${DRONE_BRANCH}" else: - committish = '${DRONE_COMMIT}' + committish = "${DRONE_COMMIT}" - environment = {'EDITION': 'enterprise'} + environment = {"EDITION": "enterprise"} - services = integration_test_services(edition='enterprise') + services = integration_test_services(edition = "enterprise") volumes = integration_test_services_volumes() init_steps = [ download_grabpl_step(), identify_runner_step(), - clone_enterprise_step(committish=committish), + clone_enterprise_step(committish = committish), init_enterprise_step(ver_mode), - compile_build_cmd('enterprise'), + compile_build_cmd("enterprise"), ] + with_deps( [ wire_install_step(), @@ -286,50 +282,56 @@ def enterprise_pipelines(ver_mode=ver_mode, trigger=release_trigger): verify_gen_jsonnet_step(), ], [ - 'init-enterprise', + "init-enterprise", ], ) build_steps = [ - build_backend_step(edition='enterprise', ver_mode=ver_mode), - build_frontend_step(edition='enterprise', ver_mode=ver_mode), - build_frontend_package_step(edition='enterprise', ver_mode=ver_mode), - build_plugins_step(edition='enterprise', ver_mode=ver_mode), + build_backend_step(edition = "enterprise", ver_mode = ver_mode), + build_frontend_step(edition = "enterprise", ver_mode = ver_mode), + build_frontend_package_step(edition = "enterprise", ver_mode = ver_mode), + build_plugins_step(edition = "enterprise", ver_mode = ver_mode), package_step( - edition='enterprise', - ver_mode=ver_mode, + edition = "enterprise", + ver_mode = ver_mode, ), copy_packages_for_docker_step(), - build_docker_images_step(edition='enterprise', ver_mode=ver_mode, publish=True), + build_docker_images_step(edition = "enterprise", publish = True), build_docker_images_step( - edition='enterprise', ver_mode=ver_mode, publish=True, ubuntu=True + edition = "enterprise", + publish = True, + ubuntu = True, ), - grafana_server_step(edition='enterprise'), - e2e_tests_step('dashboards-suite', tries=3), - e2e_tests_step('smoke-tests-suite', tries=3), - e2e_tests_step('panels-suite', tries=3), - e2e_tests_step('various-suite', tries=3), + grafana_server_step(edition = "enterprise"), + e2e_tests_step("dashboards-suite", tries = 3), + e2e_tests_step("smoke-tests-suite", tries = 3), + e2e_tests_step("panels-suite", tries = 3), + e2e_tests_step("various-suite", tries = 3), e2e_tests_artifacts(), ] publish_steps = [] if ver_mode in ( - 'release', - 'release-branch', + "release", + "release-branch", ): upload_packages_enterprise = upload_packages_step( - edition='enterprise', ver_mode=ver_mode, trigger=trigger_oss + edition = "enterprise", + ver_mode = ver_mode, + trigger = trigger_oss, ) - upload_packages_enterprise['depends_on'] = ['package'] + upload_packages_enterprise["depends_on"] = ["package"] publish_steps.extend( [ upload_cdn_step( - edition='enterprise', ver_mode=ver_mode, trigger=trigger_oss + edition = "enterprise", + ver_mode = ver_mode, + trigger = trigger_oss, ), upload_packages_enterprise, - ] + ], ) integration_test_steps = [ @@ -338,91 +340,103 @@ def enterprise_pipelines(ver_mode=ver_mode, trigger=release_trigger): ] windows_pipeline = pipeline( - name='{}-enterprise-windows'.format(ver_mode), - edition='enterprise', - trigger=trigger, - steps=get_windows_steps(edition='enterprise', ver_mode=ver_mode), - platform='windows', - depends_on=[ + name = "{}-enterprise-windows".format(ver_mode), + edition = "enterprise", + trigger = trigger, + steps = get_windows_steps(edition = "enterprise", ver_mode = ver_mode), + platform = "windows", + depends_on = [ # 'enterprise-build-e2e-publish-{}'.format(ver_mode), - '{}-enterprise-build-e2e-publish'.format(ver_mode), - '{}-enterprise-test-frontend'.format(ver_mode), - '{}-enterprise-test-backend'.format(ver_mode), - '{}-enterprise-integration-tests'.format(ver_mode), + "{}-enterprise-build-e2e-publish".format(ver_mode), + "{}-enterprise-test-frontend".format(ver_mode), + "{}-enterprise-test-backend".format(ver_mode), + "{}-enterprise-integration-tests".format(ver_mode), ], - environment=environment, + environment = environment, ) pipelines = [ pipeline( - name='{}-enterprise-build-e2e-publish'.format(ver_mode), - edition='enterprise', - trigger=trigger, - services=[], - steps=init_steps + build_steps + publish_steps, - environment=environment, - volumes=volumes, + name = "{}-enterprise-build-e2e-publish".format(ver_mode), + edition = "enterprise", + trigger = trigger, + services = [], + steps = init_steps + build_steps + publish_steps, + environment = environment, + volumes = volumes, ), - test_frontend_enterprise(trigger, ver_mode, committish=committish), - test_backend_enterprise(trigger, ver_mode, committish=committish), + test_frontend_enterprise(trigger, ver_mode, committish = committish), + test_backend_enterprise(trigger, ver_mode, committish = committish), pipeline( - name='{}-enterprise-integration-tests'.format(ver_mode), - edition='enterprise', - trigger=trigger, - services=services, - steps=[ - download_grabpl_step(), - identify_runner_step(), - clone_enterprise_step(committish=committish), - init_enterprise_step(ver_mode), - ] - + with_deps( - [ - verify_gen_cue_step(), - verify_gen_jsonnet_step(), - ], - [ - 'init-enterprise', - ], - ) - + [ - wire_install_step(), - ] - + integration_test_steps - + [ - redis_integration_tests_step(), - memcached_integration_tests_step(), - ], - environment=environment, - volumes=volumes, + name = "{}-enterprise-integration-tests".format(ver_mode), + edition = "enterprise", + trigger = trigger, + services = services, + steps = [ + download_grabpl_step(), + identify_runner_step(), + clone_enterprise_step(committish = committish), + init_enterprise_step(ver_mode), + ] + + with_deps( + [ + verify_gen_cue_step(), + verify_gen_jsonnet_step(), + ], + [ + "init-enterprise", + ], + ) + + [ + wire_install_step(), + ] + + integration_test_steps + + [ + redis_integration_tests_step(), + memcached_integration_tests_step(), + ], + environment = environment, + volumes = volumes, ), windows_pipeline, ] return pipelines +def enterprise2_pipelines(prefix = "", ver_mode = ver_mode, trigger = release_trigger): + """Generate the next generation of pipelines for Grafana Enterprise. -def enterprise2_pipelines(prefix='', ver_mode=ver_mode, trigger=release_trigger): - if ver_mode == 'release': - committish = '${DRONE_TAG}' - elif ver_mode == 'release-branch': - committish = '${DRONE_BRANCH}' + Args: + prefix: a prefix for the pipeline name used to differentiate multiple instances of + the same pipeline. + Defaults to ''. + ver_mode: controls which steps are included in the pipeline. + Defaults to 'release'. + trigger: controls which events can trigger the pipeline execution. + Defaults to tag events for tags with a 'v' prefix. + + Returns: + List of Drone pipelines. + """ + if ver_mode == "release": + committish = "${DRONE_TAG}" + elif ver_mode == "release-branch": + committish = "${DRONE_BRANCH}" else: - committish = '${DRONE_COMMIT}' + committish = "${DRONE_COMMIT}" environment = { - 'EDITION': 'enterprise2', + "EDITION": "enterprise2", } - services = integration_test_services(edition='enterprise') volumes = integration_test_services_volumes() init_steps = [ download_grabpl_step(), identify_runner_step(), - clone_enterprise_step(committish=committish), + clone_enterprise_step(committish = committish), init_enterprise_step(ver_mode), - compile_build_cmd('enterprise'), + compile_build_cmd("enterprise"), ] + with_deps( [ wire_install_step(), @@ -430,104 +444,107 @@ def enterprise2_pipelines(prefix='', ver_mode=ver_mode, trigger=release_trigger) verify_gen_cue_step(), ], [ - 'init-enterprise', + "init-enterprise", ], ) build_steps = [ - build_frontend_step(edition='enterprise', ver_mode=ver_mode), - build_frontend_package_step(edition='enterprise', ver_mode=ver_mode), - build_plugins_step(edition='enterprise', ver_mode=ver_mode), + build_frontend_step(edition = "enterprise", ver_mode = ver_mode), + build_frontend_package_step(edition = "enterprise", ver_mode = ver_mode), + build_plugins_step(edition = "enterprise", ver_mode = ver_mode), build_backend_step( - edition='enterprise2', ver_mode=ver_mode, variants=['linux-amd64'] + edition = "enterprise2", + ver_mode = ver_mode, + variants = ["linux-amd64"], ), ] - fetch_images = fetch_images_step('enterprise2') + fetch_images = fetch_images_step("enterprise2") fetch_images.update( - {'depends_on': ['build-docker-images', 'build-docker-images-ubuntu']} + {"depends_on": ["build-docker-images", "build-docker-images-ubuntu"]}, ) - upload_cdn = upload_cdn_step(edition='enterprise2', ver_mode=ver_mode) - upload_cdn['environment'].update( - {'ENTERPRISE2_CDN_PATH': from_secret('enterprise2-cdn-path')} + upload_cdn = upload_cdn_step(edition = "enterprise2", ver_mode = ver_mode) + upload_cdn["environment"].update( + {"ENTERPRISE2_CDN_PATH": from_secret("enterprise2-cdn-path")}, ) build_steps.extend( [ package_step( - edition='enterprise2', - ver_mode=ver_mode, - variants=['linux-amd64'], + edition = "enterprise2", + ver_mode = ver_mode, + variants = ["linux-amd64"], ), upload_cdn, - copy_packages_for_docker_step(edition='enterprise2'), + copy_packages_for_docker_step(edition = "enterprise2"), build_docker_images_step( - edition='enterprise2', ver_mode=ver_mode, publish=True + edition = "enterprise2", + publish = True, ), build_docker_images_step( - edition='enterprise2', ver_mode=ver_mode, publish=True, ubuntu=True + edition = "enterprise2", + publish = True, + ubuntu = True, ), fetch_images, publish_images_step( - 'enterprise2', - 'release', - mode='enterprise2', - docker_repo='${{DOCKER_ENTERPRISE2_REPO}}', + "enterprise2", + "release", + mode = "enterprise2", + docker_repo = "${{DOCKER_ENTERPRISE2_REPO}}", ), - ] + ], ) publish_steps = [] if ver_mode in ( - 'release', - 'release-branch', + "release", + "release-branch", ): - step = upload_packages_step(edition='enterprise2', ver_mode=ver_mode) - step['depends_on'] = ['package-enterprise2'] + step = upload_packages_step(edition = "enterprise2", ver_mode = ver_mode) + step["depends_on"] = ["package-enterprise2"] publish_steps.append(step) pipelines = [ pipeline( - name='{}{}-enterprise2-build-e2e-publish'.format(prefix, ver_mode), - edition='enterprise', - trigger=trigger, - services=[], - steps=init_steps + build_steps + publish_steps, - volumes=volumes, - environment=environment, + name = "{}{}-enterprise2-build-e2e-publish".format(prefix, ver_mode), + edition = "enterprise", + trigger = trigger, + services = [], + steps = init_steps + build_steps + publish_steps, + volumes = volumes, + environment = environment, ), ] return pipelines - def publish_artifacts_step(mode): - security = '' - if mode == 'security': - security = '--security ' + security = "" + if mode == "security": + security = "--security " return { - 'name': 'publish-artifacts', - 'image': publish_image, - 'environment': { - 'GCP_KEY': from_secret('gcp_key'), - 'PRERELEASE_BUCKET': from_secret('prerelease_bucket'), + "name": "publish-artifacts", + "image": publish_image, + "environment": { + "GCP_KEY": from_secret("gcp_key"), + "PRERELEASE_BUCKET": from_secret("prerelease_bucket"), }, - 'commands': [ - './bin/grabpl artifacts publish {}--tag $${{DRONE_TAG}} --src-bucket $${{PRERELEASE_BUCKET}}'.format( - security - ) + "commands": [ + "./bin/grabpl artifacts publish {}--tag $${{DRONE_TAG}} --src-bucket $${{PRERELEASE_BUCKET}}".format( + security, + ), ], - 'depends_on': ['grabpl'], + "depends_on": ["grabpl"], } - def publish_artifacts_pipelines(mode): trigger = { - 'event': ['promote'], - 'target': [mode], + "event": ["promote"], + "target": [mode], } steps = [ download_grabpl_step(), @@ -536,65 +553,69 @@ def publish_artifacts_pipelines(mode): return [ pipeline( - name='publish-artifacts-{}'.format(mode), - trigger=trigger, - steps=steps, - edition="all", - environment={'EDITION': 'all'}, - ) + name = "publish-artifacts-{}".format(mode), + trigger = trigger, + steps = steps, + edition = "all", + environment = {"EDITION": "all"}, + ), ] - def publish_packages_pipeline(): + """Generates pipelines used for publishing packages for both OSS and enterprise. + + Returns: + List of Drone pipelines. One for each of OSS and enterprise packages. + """ + trigger = { - 'event': ['promote'], - 'target': ['public'], + "event": ["promote"], + "target": ["public"], } oss_steps = [ download_grabpl_step(), compile_build_cmd(), - publish_linux_packages_step(edition='oss', package_manager='deb'), - publish_linux_packages_step(edition='oss', package_manager='rpm'), - publish_grafanacom_step(edition='oss', ver_mode='release'), + publish_linux_packages_step(edition = "oss", package_manager = "deb"), + publish_linux_packages_step(edition = "oss", package_manager = "rpm"), + publish_grafanacom_step(edition = "oss", ver_mode = "release"), ] enterprise_steps = [ download_grabpl_step(), compile_build_cmd(), - publish_linux_packages_step(edition='enterprise', package_manager='deb'), - publish_linux_packages_step(edition='enterprise', package_manager='rpm'), - publish_grafanacom_step(edition='enterprise', ver_mode='release'), + publish_linux_packages_step(edition = "enterprise", package_manager = "deb"), + publish_linux_packages_step(edition = "enterprise", package_manager = "rpm"), + publish_grafanacom_step(edition = "enterprise", ver_mode = "release"), ] deps = [ - 'publish-artifacts-public', - 'publish-docker-oss-public', - 'publish-docker-enterprise-public', + "publish-artifacts-public", + "publish-docker-oss-public", + "publish-docker-enterprise-public", ] return [ pipeline( - name='publish-packages-oss', - trigger=trigger, - steps=oss_steps, - edition="all", - depends_on=deps, - environment={'EDITION': 'oss'}, + name = "publish-packages-oss", + trigger = trigger, + steps = oss_steps, + edition = "all", + depends_on = deps, + environment = {"EDITION": "oss"}, ), pipeline( - name='publish-packages-enterprise', - trigger=trigger, - steps=enterprise_steps, - edition="all", - depends_on=deps, - environment={'EDITION': 'enterprise'}, + name = "publish-packages-enterprise", + trigger = trigger, + steps = enterprise_steps, + edition = "all", + depends_on = deps, + environment = {"EDITION": "enterprise"}, ), ] - def publish_npm_pipelines(): trigger = { - 'event': ['promote'], - 'target': ['public'], + "event": ["promote"], + "target": ["public"], } steps = [ compile_build_cmd(), @@ -605,26 +626,25 @@ def publish_npm_pipelines(): return [ pipeline( - name='publish-npm-packages-public', - trigger=trigger, - steps=steps, - edition="all", - environment={'EDITION': 'all'}, - ) + name = "publish-npm-packages-public", + trigger = trigger, + steps = steps, + edition = "all", + environment = {"EDITION": "all"}, + ), ] - def artifacts_page_pipeline(): trigger = { - 'event': ['promote'], - 'target': 'security', + "event": ["promote"], + "target": "security", } return [ pipeline( - name='publish-artifacts-page', - trigger=trigger, - steps=[download_grabpl_step(), artifacts_page_step()], - edition="all", - environment={'EDITION': 'all'}, - ) + name = "publish-artifacts-page", + trigger = trigger, + steps = [download_grabpl_step(), artifacts_page_step()], + edition = "all", + environment = {"EDITION": "all"}, + ), ] diff --git a/scripts/drone/pipelines/aws_marketplace.star b/scripts/drone/pipelines/aws_marketplace.star index 299bc15b845..bbcc5b16bd0 100644 --- a/scripts/drone/pipelines/aws_marketplace.star +++ b/scripts/drone/pipelines/aws_marketplace.star @@ -1,38 +1,43 @@ +""" +This module contains steps and pipelines publishing to AWS Marketplace. +""" + load( - 'scripts/drone/steps/lib.star', - 'download_grabpl_step', - 'publish_images_step', - 'compile_build_cmd', - 'fetch_images_step', - 'publish_image', + "scripts/drone/steps/lib.star", + "compile_build_cmd", + "fetch_images_step", + "publish_image", ) - -load('scripts/drone/vault.star', 'from_secret') - +load("scripts/drone/vault.star", "from_secret") load( - 'scripts/drone/utils/utils.star', - 'pipeline', + "scripts/drone/utils/utils.star", + "pipeline", ) def publish_aws_marketplace_step(): return { - 'name': 'publish-aws-marketplace', - 'image': publish_image, - 'commands': ['./bin/build publish aws --image grafana/grafana-enterprise --repo grafana-labs/grafanaenterprise --product 422b46fb-bea6-4f27-8bcc-832117bd627e'], - 'depends_on': ['fetch-images-enterprise'], - 'environment': { - 'AWS_REGION': from_secret('aws_region'), - 'AWS_ACCESS_KEY_ID': from_secret('aws_access_key_id'), - 'AWS_SECRET_ACCESS_KEY': from_secret('aws_secret_access_key'), + "name": "publish-aws-marketplace", + "image": publish_image, + "commands": ["./bin/build publish aws --image grafana/grafana-enterprise --repo grafana-labs/grafanaenterprise --product 422b46fb-bea6-4f27-8bcc-832117bd627e"], + "depends_on": ["fetch-images-enterprise"], + "environment": { + "AWS_REGION": from_secret("aws_region"), + "AWS_ACCESS_KEY_ID": from_secret("aws_access_key_id"), + "AWS_SECRET_ACCESS_KEY": from_secret("aws_secret_access_key"), }, - 'volumes': [{'name': 'docker', 'path': '/var/run/docker.sock'}], + "volumes": [{"name": "docker", "path": "/var/run/docker.sock"}], } def publish_aws_marketplace_pipeline(mode): trigger = { - 'event': ['promote'], - 'target': [mode], + "event": ["promote"], + "target": [mode], } return [pipeline( - name='publish-aws-marketplace-{}'.format(mode), trigger=trigger, steps=[compile_build_cmd(), fetch_images_step('enterprise'), publish_aws_marketplace_step()], edition="", depends_on = ['publish-docker-enterprise-public'], environment = {'EDITION': 'enterprise2'} - ),] + name = "publish-aws-marketplace-{}".format(mode), + trigger = trigger, + steps = [compile_build_cmd(), fetch_images_step("enterprise"), publish_aws_marketplace_step()], + edition = "", + depends_on = ["publish-docker-enterprise-public"], + environment = {"EDITION": "enterprise2"}, + )] diff --git a/scripts/drone/pipelines/build.star b/scripts/drone/pipelines/build.star index 2b4512daed8..2c03aa2b0ed 100644 --- a/scripts/drone/pipelines/build.star +++ b/scripts/drone/pipelines/build.star @@ -1,52 +1,55 @@ -load( - 'scripts/drone/steps/lib.star', - 'benchmark_ldap_step', - 'betterer_frontend_step', - 'build_backend_step', - 'build_docker_images_step', - 'build_frontend_package_step', - 'build_frontend_step', - 'build_image', - 'build_plugins_step', - 'build_storybook_step', - 'cloud_plugins_e2e_tests_step', - 'compile_build_cmd', - 'copy_packages_for_docker_step', - 'download_grabpl_step', - 'e2e_tests_artifacts', - 'e2e_tests_step', - 'enterprise_downstream_step', - 'frontend_metrics_step', - 'grafana_server_step', - 'identify_runner_step', - 'memcached_integration_tests_step', - 'mysql_integration_tests_step', - 'package_step', - 'postgres_integration_tests_step', - 'publish_images_step', - 'redis_integration_tests_step', - 'release_canary_npm_packages_step', - 'store_storybook_step', - 'test_a11y_frontend_step', - 'trigger_oss', - 'trigger_test_release', - 'upload_cdn_step', - 'upload_packages_step', - 'verify_gen_cue_step', - 'verify_gen_jsonnet_step', - 'wire_install_step', - 'yarn_install_step', -) +"""This module contains the comprehensive build pipeline.""" load( - 'scripts/drone/utils/utils.star', - 'pipeline', + "scripts/drone/steps/lib.star", + "build_backend_step", + "build_docker_images_step", + "build_frontend_package_step", + "build_frontend_step", + "build_plugins_step", + "build_storybook_step", + "cloud_plugins_e2e_tests_step", + "compile_build_cmd", + "copy_packages_for_docker_step", + "download_grabpl_step", + "e2e_tests_artifacts", + "e2e_tests_step", + "enterprise_downstream_step", + "frontend_metrics_step", + "grafana_server_step", + "identify_runner_step", + "package_step", + "publish_images_step", + "release_canary_npm_packages_step", + "store_storybook_step", + "test_a11y_frontend_step", + "trigger_oss", + "trigger_test_release", + "upload_cdn_step", + "upload_packages_step", + "verify_gen_cue_step", + "verify_gen_jsonnet_step", + "wire_install_step", + "yarn_install_step", +) +load( + "scripts/drone/utils/utils.star", + "pipeline", ) - +# @unused def build_e2e(trigger, ver_mode): - edition = 'oss' - environment = {'EDITION': edition} + """Perform e2e building, testing, and publishing." + + Args: + trigger: controls which events can trigger the pipeline execution. + ver_mode: used in the naming of the pipeline. + + Returns: + Drone pipeline. + """ + edition = "oss" + environment = {"EDITION": edition} init_steps = [ identify_runner_step(), download_grabpl_step(), @@ -60,101 +63,107 @@ def build_e2e(trigger, ver_mode): build_steps = [] variants = None - if ver_mode == 'pr': + if ver_mode == "pr": build_steps.extend( [ trigger_test_release(), - enterprise_downstream_step(ver_mode=ver_mode), - ] + enterprise_downstream_step(ver_mode = ver_mode), + ], ) variants = [ - 'linux-amd64', - 'linux-amd64-musl', - 'darwin-amd64', - 'windows-amd64', + "linux-amd64", + "linux-amd64-musl", + "darwin-amd64", + "windows-amd64", ] build_steps.extend( [ - build_backend_step(edition=edition, ver_mode=ver_mode), - build_frontend_step(edition=edition, ver_mode=ver_mode), - build_frontend_package_step(edition=edition, ver_mode=ver_mode), - build_plugins_step(edition=edition, ver_mode=ver_mode), - package_step(edition=edition, ver_mode=ver_mode, variants=variants), - grafana_server_step(edition=edition), - e2e_tests_step('dashboards-suite'), - e2e_tests_step('smoke-tests-suite'), - e2e_tests_step('panels-suite'), - e2e_tests_step('various-suite'), + build_backend_step(edition = edition, ver_mode = ver_mode), + build_frontend_step(edition = edition, ver_mode = ver_mode), + build_frontend_package_step(edition = edition, ver_mode = ver_mode), + build_plugins_step(edition = edition, ver_mode = ver_mode), + package_step(edition = edition, variants = variants, ver_mode = ver_mode), + grafana_server_step(edition = edition), + e2e_tests_step("dashboards-suite"), + e2e_tests_step("smoke-tests-suite"), + e2e_tests_step("panels-suite"), + e2e_tests_step("various-suite"), cloud_plugins_e2e_tests_step( - 'cloud-plugins-suite', - cloud='azure', - trigger=trigger_oss, + "cloud-plugins-suite", + cloud = "azure", + trigger = trigger_oss, ), e2e_tests_artifacts(), - build_storybook_step(ver_mode=ver_mode), + build_storybook_step(ver_mode = ver_mode), copy_packages_for_docker_step(), - test_a11y_frontend_step(ver_mode=ver_mode), - ] + test_a11y_frontend_step(ver_mode = ver_mode), + ], ) - if ver_mode == 'main': + if ver_mode == "main": build_steps.extend( [ - store_storybook_step(ver_mode=ver_mode, trigger=trigger_oss), - frontend_metrics_step(trigger=trigger_oss), + store_storybook_step(trigger = trigger_oss, ver_mode = ver_mode), + frontend_metrics_step(trigger = trigger_oss), build_docker_images_step( - edition=edition, ver_mode=ver_mode, publish=False + edition = edition, + publish = False, ), build_docker_images_step( - edition=edition, ver_mode=ver_mode, publish=False, ubuntu=True + edition = edition, + publish = False, + ubuntu = True, ), publish_images_step( - edition=edition, - ver_mode=ver_mode, - mode='', - docker_repo='grafana', - trigger=trigger_oss, + docker_repo = "grafana", + edition = edition, + mode = "", + trigger = trigger_oss, + ver_mode = ver_mode, ), publish_images_step( - edition=edition, - ver_mode=ver_mode, - mode='', - docker_repo='grafana-oss', - trigger=trigger_oss, + docker_repo = "grafana-oss", + edition = edition, + mode = "", + trigger = trigger_oss, + ver_mode = ver_mode, ), - release_canary_npm_packages_step(trigger=trigger_oss), + release_canary_npm_packages_step(trigger = trigger_oss), upload_packages_step( - edition=edition, ver_mode=ver_mode, trigger=trigger_oss + edition = edition, + trigger = trigger_oss, + ver_mode = ver_mode, ), upload_cdn_step( - edition=edition, ver_mode=ver_mode, trigger=trigger_oss + edition = edition, + trigger = trigger_oss, + ver_mode = ver_mode, ), - ] + ], ) - elif ver_mode == 'pr': + elif ver_mode == "pr": build_steps.extend( [ build_docker_images_step( - edition=edition, - ver_mode=ver_mode, - archs=[ - 'amd64', + archs = [ + "amd64", ], - ) - ] + edition = edition, + ), + ], ) - publish_suffix = '' - if ver_mode == 'main': - publish_suffix = '-publish' + publish_suffix = "" + if ver_mode == "main": + publish_suffix = "-publish" return pipeline( - name='{}-build-e2e{}'.format(ver_mode, publish_suffix), - edition="oss", - trigger=trigger, - services=[], - steps=init_steps + build_steps, - environment=environment, + name = "{}-build-e2e{}".format(ver_mode, publish_suffix), + edition = "oss", + environment = environment, + services = [], + steps = init_steps + build_steps, + trigger = trigger, ) diff --git a/scripts/drone/pipelines/docs.star b/scripts/drone/pipelines/docs.star index 031efa946ce..cba222fd2ff 100644 --- a/scripts/drone/pipelines/docs.star +++ b/scripts/drone/pipelines/docs.star @@ -1,39 +1,32 @@ -load( - 'scripts/drone/steps/lib.star', - 'build_image', - 'yarn_install_step', - 'identify_runner_step', - 'download_grabpl_step', - 'lint_frontend_step', - 'codespell_step', - 'test_frontend_step', - 'build_storybook_step', - 'build_docs_website_step', -) +""" +This module returns all the pipelines used in the event of documentation changes along with supporting functions. +""" load( - 'scripts/drone/services/services.star', - 'integration_test_services', - 'ldap_service', + "scripts/drone/steps/lib.star", + "build_docs_website_step", + "build_image", + "codespell_step", + "download_grabpl_step", + "identify_runner_step", + "yarn_install_step", ) - load( - 'scripts/drone/utils/utils.star', - 'pipeline', + "scripts/drone/utils/utils.star", + "pipeline", ) docs_paths = { - 'include': [ - '*.md', - 'docs/**', - 'packages/**/*.md', - 'latest.json', + "include": [ + "*.md", + "docs/**", + "packages/**/*.md", + "latest.json", ], } - def docs_pipelines(ver_mode, trigger): - environment = {'EDITION': 'oss'} + environment = {"EDITION": "oss"} steps = [ download_grabpl_step(), identify_runner_step(), @@ -44,45 +37,42 @@ def docs_pipelines(ver_mode, trigger): ] return pipeline( - name='{}-docs'.format(ver_mode), - edition='oss', - trigger=trigger, - services=[], - steps=steps, - environment=environment, + name = "{}-docs".format(ver_mode), + edition = "oss", + trigger = trigger, + services = [], + steps = steps, + environment = environment, ) - def lint_docs(): return { - 'name': 'lint-docs', - 'image': build_image, - 'depends_on': [ - 'yarn-install', + "name": "lint-docs", + "image": build_image, + "depends_on": [ + "yarn-install", ], - 'environment': { - 'NODE_OPTIONS': '--max_old_space_size=8192', + "environment": { + "NODE_OPTIONS": "--max_old_space_size=8192", }, - 'commands': [ - 'yarn run prettier:checkDocs', + "commands": [ + "yarn run prettier:checkDocs", ], } - def trigger_docs_main(): return { - 'branch': 'main', - 'event': [ - 'push', + "branch": "main", + "event": [ + "push", ], - 'paths': docs_paths, + "paths": docs_paths, } - def trigger_docs_pr(): return { - 'event': [ - 'pull_request', + "event": [ + "pull_request", ], - 'paths': docs_paths, + "paths": docs_paths, } diff --git a/scripts/drone/pipelines/github.star b/scripts/drone/pipelines/github.star index 06096327295..d3ccb6b30c9 100644 --- a/scripts/drone/pipelines/github.star +++ b/scripts/drone/pipelines/github.star @@ -1,36 +1,40 @@ +""" +This module contains steps and pipelines relating to GitHub. +""" + load( - 'scripts/drone/steps/lib.star', - 'download_grabpl_step', - 'publish_images_step', - 'compile_build_cmd', - 'fetch_images_step', - 'publish_image', + "scripts/drone/steps/lib.star", + "compile_build_cmd", + "fetch_images_step", + "publish_image", ) - -load('scripts/drone/vault.star', 'from_secret') - +load("scripts/drone/vault.star", "from_secret") load( - 'scripts/drone/utils/utils.star', - 'pipeline', + "scripts/drone/utils/utils.star", + "pipeline", ) def publish_github_step(): return { - 'name': 'publish-github', - 'image': publish_image, - 'commands': ['./bin/build publish github --repo $${GH_REGISTRY} --create'], - 'depends_on': ['fetch-images-enterprise2'], - 'environment': { - 'GH_TOKEN': from_secret('github_token'), - 'GH_REGISTRY': from_secret('gh_registry'), + "name": "publish-github", + "image": publish_image, + "commands": ["./bin/build publish github --repo $${GH_REGISTRY} --create"], + "depends_on": ["fetch-images-enterprise2"], + "environment": { + "GH_TOKEN": from_secret("github_token"), + "GH_REGISTRY": from_secret("gh_registry"), }, } def publish_github_pipeline(mode): trigger = { - 'event': ['promote'], - 'target': [mode], + "event": ["promote"], + "target": [mode], } return [pipeline( - name='publish-github-{}'.format(mode), trigger=trigger, steps=[compile_build_cmd(), fetch_images_step('enterprise2'), publish_github_step()], edition="", environment = {'EDITION': 'enterprise2'} - ),] + name = "publish-github-{}".format(mode), + trigger = trigger, + steps = [compile_build_cmd(), fetch_images_step("enterprise2"), publish_github_step()], + edition = "", + environment = {"EDITION": "enterprise2"}, + )] diff --git a/scripts/drone/pipelines/integration_tests.star b/scripts/drone/pipelines/integration_tests.star index efb810d58ab..a5ceb011d78 100644 --- a/scripts/drone/pipelines/integration_tests.star +++ b/scripts/drone/pipelines/integration_tests.star @@ -1,32 +1,41 @@ -load( - 'scripts/drone/steps/lib.star', - 'compile_build_cmd', - 'download_grabpl_step', - 'identify_runner_step', - 'verify_gen_cue_step', - 'verify_gen_jsonnet_step', - 'wire_install_step', - 'postgres_integration_tests_step', - 'mysql_integration_tests_step', -) +""" +This module returns the pipeline used for integration tests. +""" load( - 'scripts/drone/services/services.star', - 'integration_test_services', - 'integration_test_services_volumes', - 'ldap_service', + "scripts/drone/steps/lib.star", + "compile_build_cmd", + "download_grabpl_step", + "identify_runner_step", + "mysql_integration_tests_step", + "postgres_integration_tests_step", + "verify_gen_cue_step", + "verify_gen_jsonnet_step", + "wire_install_step", ) - load( - 'scripts/drone/utils/utils.star', - 'pipeline', + "scripts/drone/services/services.star", + "integration_test_services", + "integration_test_services_volumes", +) +load( + "scripts/drone/utils/utils.star", + "pipeline", ) - def integration_tests(trigger, prefix): - environment = {'EDITION': 'oss'} + """Generate a pipeline for integration tests. - services = integration_test_services(edition="oss") + Args: + trigger: controls which events can trigger the pipeline execution. + prefix: used in the naming of the pipeline. + + Returns: + Drone pipeline. + """ + environment = {"EDITION": "oss"} + + services = integration_test_services(edition = "oss") volumes = integration_test_services_volumes() init_steps = [ @@ -44,11 +53,11 @@ def integration_tests(trigger, prefix): ] return pipeline( - name='{}-integration-tests'.format(prefix), - edition='oss', - trigger=trigger, - environment=environment, - services=services, - volumes=volumes, - steps=init_steps + test_steps, + name = "{}-integration-tests".format(prefix), + edition = "oss", + trigger = trigger, + environment = environment, + services = services, + volumes = volumes, + steps = init_steps + test_steps, ) diff --git a/scripts/drone/pipelines/lint_backend.star b/scripts/drone/pipelines/lint_backend.star index 1efbc24fb7e..73788b1e800 100644 --- a/scripts/drone/pipelines/lint_backend.star +++ b/scripts/drone/pipelines/lint_backend.star @@ -1,23 +1,34 @@ -load( - 'scripts/drone/steps/lib.star', - 'identify_runner_step', - 'wire_install_step', - 'lint_backend_step', - 'lint_drone_step', - 'compile_build_cmd', -) +""" +This module returns the pipeline used for linting backend code. +""" load( - 'scripts/drone/utils/utils.star', - 'pipeline', + "scripts/drone/steps/lib.star", + "compile_build_cmd", + "identify_runner_step", + "lint_backend_step", + "lint_drone_step", + "wire_install_step", +) +load( + "scripts/drone/utils/utils.star", + "pipeline", ) - def lint_backend_pipeline(trigger, ver_mode): - environment = {'EDITION': 'oss'} + """Generates the pipelines used linting backend code. + + Args: + trigger: controls which events can trigger the pipeline execution. + ver_mode: used in the naming of the pipeline. + + Returns: + Drone pipeline. + """ + environment = {"EDITION": "oss"} wire_step = wire_install_step() - wire_step.update({'depends_on': []}) + wire_step.update({"depends_on": []}) init_steps = [ identify_runner_step(), @@ -29,14 +40,14 @@ def lint_backend_pipeline(trigger, ver_mode): lint_backend_step(), ] - if ver_mode == 'main': + if ver_mode == "main": test_steps.append(lint_drone_step()) return pipeline( - name='{}-lint-backend'.format(ver_mode), - edition="oss", - trigger=trigger, - services=[], - steps=init_steps + test_steps, - environment=environment, + name = "{}-lint-backend".format(ver_mode), + edition = "oss", + trigger = trigger, + services = [], + steps = init_steps + test_steps, + environment = environment, ) diff --git a/scripts/drone/pipelines/lint_frontend.star b/scripts/drone/pipelines/lint_frontend.star index dd3947aac8d..7bdb542982f 100644 --- a/scripts/drone/pipelines/lint_frontend.star +++ b/scripts/drone/pipelines/lint_frontend.star @@ -1,18 +1,29 @@ -load( - 'scripts/drone/steps/lib.star', - 'identify_runner_step', - 'yarn_install_step', - 'lint_frontend_step', -) +""" +This module returns the pipeline used for linting frontend code. +""" load( - 'scripts/drone/utils/utils.star', - 'pipeline', + "scripts/drone/steps/lib.star", + "identify_runner_step", + "lint_frontend_step", + "yarn_install_step", +) +load( + "scripts/drone/utils/utils.star", + "pipeline", ) - def lint_frontend_pipeline(trigger, ver_mode): - environment = {'EDITION': 'oss'} + """Generates the pipelines used linting frontend code. + + Args: + trigger: controls which events can trigger the pipeline execution. + ver_mode: used in the naming of the pipeline. + + Returns: + Drone pipeline. + """ + environment = {"EDITION": "oss"} init_steps = [ identify_runner_step(), @@ -24,10 +35,10 @@ def lint_frontend_pipeline(trigger, ver_mode): ] return pipeline( - name='{}-lint-frontend'.format(ver_mode), - edition="oss", - trigger=trigger, - services=[], - steps=init_steps + test_steps, - environment=environment, + name = "{}-lint-frontend".format(ver_mode), + edition = "oss", + trigger = trigger, + services = [], + steps = init_steps + test_steps, + environment = environment, ) diff --git a/scripts/drone/pipelines/publish_images.star b/scripts/drone/pipelines/publish_images.star index f2b7cd28cd3..cb848a12c61 100644 --- a/scripts/drone/pipelines/publish_images.star +++ b/scripts/drone/pipelines/publish_images.star @@ -1,75 +1,97 @@ -load( - 'scripts/drone/steps/lib.star', - 'identify_runner_step', - 'download_grabpl_step', - 'publish_images_step', - 'compile_build_cmd', - 'fetch_images_step', -) +""" +This module returns the pipeline used for publishing Docker images and its steps. +""" load( - 'scripts/drone/utils/utils.star', - 'pipeline', + "scripts/drone/steps/lib.star", + "compile_build_cmd", + "download_grabpl_step", + "fetch_images_step", + "identify_runner_step", + "publish_images_step", +) +load( + "scripts/drone/utils/utils.star", + "pipeline", ) - def publish_image_steps(edition, mode, docker_repo): + """Generates the steps used for publising Docker images using grabpl. + + Args: + edition: controls which version of an image is fetched in the case of a release. + It also controls which publishing implementation is used. + If edition == 'oss', it additionally publishes the grafana/grafana-oss repository. + mode: uses to control the publishing of security images when mode == 'security'. + docker_repo: the Docker image name. + It is combined with the 'grafana/' library prefix. + + Returns: + List of Drone steps. + """ steps = [ identify_runner_step(), download_grabpl_step(), compile_build_cmd(), fetch_images_step(edition), - publish_images_step(edition, 'release', mode, docker_repo), + publish_images_step(edition, "release", mode, docker_repo), ] - if edition == 'oss': + if edition == "oss": steps.append( - publish_images_step(edition, 'release', mode, 'grafana/grafana-oss') + publish_images_step(edition, "release", mode, "grafana/grafana-oss"), ) return steps - def publish_image_pipelines_public(): - mode = 'public' + """Generates the pipeline used for publising public Docker images. + + Returns: + Drone pipeline + """ + mode = "public" trigger = { - 'event': ['promote'], - 'target': [mode], + "event": ["promote"], + "target": [mode], } return [ pipeline( - name='publish-docker-oss-{}'.format(mode), - trigger=trigger, - steps=publish_image_steps(edition='oss', mode=mode, docker_repo='grafana'), - edition="", - environment={'EDITION': 'oss'}, + name = "publish-docker-oss-{}".format(mode), + trigger = trigger, + steps = publish_image_steps(edition = "oss", mode = mode, docker_repo = "grafana"), + edition = "", + environment = {"EDITION": "oss"}, ), pipeline( - name='publish-docker-enterprise-{}'.format(mode), - trigger=trigger, - steps=publish_image_steps( - edition='enterprise', mode=mode, docker_repo='grafana-enterprise' + name = "publish-docker-enterprise-{}".format(mode), + trigger = trigger, + steps = publish_image_steps( + edition = "enterprise", + mode = mode, + docker_repo = "grafana-enterprise", ), - edition="", - environment={'EDITION': 'enterprise'}, + edition = "", + environment = {"EDITION": "enterprise"}, ), ] - def publish_image_pipelines_security(): - mode = 'security' + mode = "security" trigger = { - 'event': ['promote'], - 'target': [mode], + "event": ["promote"], + "target": [mode], } return [ pipeline( - name='publish-docker-enterprise-{}'.format(mode), - trigger=trigger, - steps=publish_image_steps( - edition='enterprise', mode=mode, docker_repo='grafana-enterprise' + name = "publish-docker-enterprise-{}".format(mode), + trigger = trigger, + steps = publish_image_steps( + edition = "enterprise", + mode = mode, + docker_repo = "grafana-enterprise", ), - edition="", - environment={'EDITION': 'enterprise'}, + edition = "", + environment = {"EDITION": "enterprise"}, ), ] diff --git a/scripts/drone/pipelines/shellcheck.star b/scripts/drone/pipelines/shellcheck.star index 78a356adf8d..2a2aa55149e 100644 --- a/scripts/drone/pipelines/shellcheck.star +++ b/scripts/drone/pipelines/shellcheck.star @@ -1,49 +1,50 @@ -load('scripts/drone/steps/lib.star', 'build_image', 'compile_build_cmd') +""" +This module returns a Drone step and pipeline for linting with shellcheck. +""" +load("scripts/drone/steps/lib.star", "build_image", "compile_build_cmd") load( - 'scripts/drone/utils/utils.star', - 'pipeline', + "scripts/drone/utils/utils.star", + "pipeline", ) trigger = { - 'event': [ - 'pull_request', + "event": [ + "pull_request", ], - 'paths': { - 'exclude': [ - '*.md', - 'docs/**', - 'latest.json', + "paths": { + "exclude": [ + "*.md", + "docs/**", + "latest.json", ], - 'include': ['scripts/**/*.sh'], + "include": ["scripts/**/*.sh"], }, } - def shellcheck_step(): return { - 'name': 'shellcheck', - 'image': build_image, - 'depends_on': [ - 'compile-build-cmd', + "name": "shellcheck", + "image": build_image, + "depends_on": [ + "compile-build-cmd", ], - 'commands': [ - './bin/build shellcheck', + "commands": [ + "./bin/build shellcheck", ], } - def shellcheck_pipeline(): - environment = {'EDITION': 'oss'} + environment = {"EDITION": "oss"} steps = [ compile_build_cmd(), shellcheck_step(), ] return pipeline( - name='pr-shellcheck', - edition="oss", - trigger=trigger, - services=[], - steps=steps, - environment=environment, + name = "pr-shellcheck", + edition = "oss", + trigger = trigger, + services = [], + steps = steps, + environment = environment, ) diff --git a/scripts/drone/pipelines/test_backend.star b/scripts/drone/pipelines/test_backend.star index aa5cdfd8834..4b9dfb119a1 100644 --- a/scripts/drone/pipelines/test_backend.star +++ b/scripts/drone/pipelines/test_backend.star @@ -1,30 +1,41 @@ -load( - 'scripts/drone/steps/lib.star', - 'identify_runner_step', - 'download_grabpl_step', - 'wire_install_step', - 'test_backend_step', - 'test_backend_integration_step', - 'verify_gen_cue_step', - 'verify_gen_jsonnet_step', - 'compile_build_cmd', - 'clone_enterprise_step', - 'init_enterprise_step', -) +""" +This module returns the pipeline used for testing backend code. +""" load( - 'scripts/drone/utils/utils.star', - 'pipeline', - 'with_deps', + "scripts/drone/utils/utils.star", + "pipeline", + "with_deps", +) +load( + "scripts/drone/steps/lib.star", + "clone_enterprise_step", + "compile_build_cmd", + "download_grabpl_step", + "identify_runner_step", + "init_enterprise_step", + "test_backend_integration_step", + "test_backend_step", + "verify_gen_cue_step", + "verify_gen_jsonnet_step", + "wire_install_step", ) +def test_backend(trigger, ver_mode): + """Generates the pipeline used for testing OSS backend code. -def test_backend(trigger, ver_mode, committish): - environment = {'EDITION': 'oss'} + Args: + trigger: a Drone trigger for the pipeline. + ver_mode: affects the pipeline name. + + Returns: + Drone pipeline. + """ + environment = {"EDITION": "oss"} steps = [ identify_runner_step(), - compile_build_cmd(edition='oss'), + compile_build_cmd(edition = "oss"), verify_gen_cue_step(), verify_gen_jsonnet_step(), wire_install_step(), @@ -32,21 +43,31 @@ def test_backend(trigger, ver_mode, committish): test_backend_integration_step(), ] - pipeline_name = '{}-test-backend'.format(ver_mode) + pipeline_name = "{}-test-backend".format(ver_mode) if ver_mode in ("release-branch", "release"): - pipeline_name = '{}-{}-test-backend'.format(ver_mode, 'oss') + pipeline_name = "{}-{}-test-backend".format(ver_mode, "oss") return pipeline( - name=pipeline_name, - edition='oss', - trigger=trigger, - steps=steps, - environment=environment, + name = pipeline_name, + edition = "oss", + trigger = trigger, + steps = steps, + environment = environment, ) +def test_backend_enterprise(trigger, ver_mode, committish, edition = "enterprise"): + """Generates the pipeline used for testing backend enterprise code. -def test_backend_enterprise(trigger, ver_mode, committish, edition="enterprise"): - environment = {'EDITION': edition} + Args: + trigger: a Drone trigger for the pipeline. + ver_mode: affects the pipeline name. + committish: controls what revision of enterprise code to test with. + edition: affects the clone step in the pipeline and also affects the pipeline name. + + Returns: + Drone pipeline. + """ + environment = {"EDITION": edition} steps = ( [ @@ -55,31 +76,31 @@ def test_backend_enterprise(trigger, ver_mode, committish, edition="enterprise") init_enterprise_step(ver_mode), identify_runner_step(), compile_build_cmd(edition), - ] - + with_deps( + ] + + with_deps( [ verify_gen_cue_step(), verify_gen_jsonnet_step(), ], [ - 'init-enterprise', + "init-enterprise", ], - ) - + [ + ) + + [ wire_install_step(), test_backend_step(), test_backend_integration_step(), ] ) - pipeline_name = '{}-test-backend'.format(ver_mode) + pipeline_name = "{}-test-backend".format(ver_mode) if ver_mode in ("release-branch", "release"): - pipeline_name = '{}-{}-test-backend'.format(ver_mode, edition) + pipeline_name = "{}-{}-test-backend".format(ver_mode, edition) return pipeline( - name=pipeline_name, - edition=edition, - trigger=trigger, - steps=steps, - environment=environment, + name = pipeline_name, + edition = edition, + trigger = trigger, + steps = steps, + environment = environment, ) diff --git a/scripts/drone/pipelines/test_frontend.star b/scripts/drone/pipelines/test_frontend.star index 0f3b7fd4655..55035efee0b 100644 --- a/scripts/drone/pipelines/test_frontend.star +++ b/scripts/drone/pipelines/test_frontend.star @@ -1,47 +1,68 @@ -load( - 'scripts/drone/steps/lib.star', - 'identify_runner_step', - 'clone_enterprise_step', - 'init_enterprise_step', - 'download_grabpl_step', - 'yarn_install_step', - 'betterer_frontend_step', - 'test_frontend_step', -) +""" +This module returns the pipeline used for testing backend code. +""" load( - 'scripts/drone/utils/utils.star', - 'pipeline', - 'with_deps', + "scripts/drone/utils/utils.star", + "pipeline", + "with_deps", +) +load( + "scripts/drone/steps/lib.star", + "betterer_frontend_step", + "clone_enterprise_step", + "download_grabpl_step", + "identify_runner_step", + "init_enterprise_step", + "test_frontend_step", + "yarn_install_step", ) +def test_frontend(trigger, ver_mode): + """Generates the pipeline used for testing frontend code. -def test_frontend(trigger, ver_mode, committish): - environment = {'EDITION': 'oss'} + Args: + trigger: a Drone trigger for the pipeline + ver_mode: indirectly controls which revision of enterprise code to use. + + Returns: + Drone pipeline. + """ + environment = {"EDITION": "oss"} steps = [ identify_runner_step(), download_grabpl_step(), yarn_install_step(), - betterer_frontend_step(edition='oss'), - test_frontend_step(edition='oss'), + betterer_frontend_step(edition = "oss"), + test_frontend_step(edition = "oss"), ] - pipeline_name = '{}-test-frontend'.format(ver_mode) + pipeline_name = "{}-test-frontend".format(ver_mode) if ver_mode in ("release-branch", "release"): - pipeline_name = '{}-{}-test-frontend'.format(ver_mode, 'oss') + pipeline_name = "{}-{}-test-frontend".format(ver_mode, "oss") return pipeline( - name=pipeline_name, - edition='oss', - trigger=trigger, - steps=steps, - environment=environment, + name = pipeline_name, + edition = "oss", + trigger = trigger, + steps = steps, + environment = environment, ) +def test_frontend_enterprise(trigger, ver_mode, committish, edition = "enterprise"): + """Generates the pipeline used for testing frontend enterprise code. -def test_frontend_enterprise(trigger, ver_mode, committish, edition='enterprise'): - environment = {'EDITION': edition} + Args: + trigger: a Drone trigger for the pipeline. + ver_mode: affects the pipeline name. + committish: controls what revision of enterprise code to test with. + edition: affects the clone step in the pipeline and also affects the pipeline name. + + Returns: + Drone pipeline. + """ + environment = {"EDITION": edition} steps = ( [ @@ -49,22 +70,22 @@ def test_frontend_enterprise(trigger, ver_mode, committish, edition='enterprise' init_enterprise_step(ver_mode), identify_runner_step(), download_grabpl_step(), - ] - + with_deps([yarn_install_step()], ['init-enterprise']) - + [ + ] + + with_deps([yarn_install_step()], ["init-enterprise"]) + + [ betterer_frontend_step(edition), test_frontend_step(edition), ] ) - pipeline_name = '{}-test-frontend'.format(ver_mode) + pipeline_name = "{}-test-frontend".format(ver_mode) if ver_mode in ("release-branch", "release"): - pipeline_name = '{}-{}-test-frontend'.format(ver_mode, edition) + pipeline_name = "{}-{}-test-frontend".format(ver_mode, edition) return pipeline( - name=pipeline_name, - edition=edition, - trigger=trigger, - steps=steps, - environment=environment, + name = pipeline_name, + edition = edition, + trigger = trigger, + steps = steps, + environment = environment, ) diff --git a/scripts/drone/pipelines/trigger_downstream.star b/scripts/drone/pipelines/trigger_downstream.star index dafab45fe35..d3403de50f1 100644 --- a/scripts/drone/pipelines/trigger_downstream.star +++ b/scripts/drone/pipelines/trigger_downstream.star @@ -1,43 +1,45 @@ -load( - 'scripts/drone/steps/lib.star', - 'enterprise_downstream_step', -) +""" +This module returns the pipeline used for triggering a downstream pipeline for Grafana Enterprise. +""" load( - 'scripts/drone/utils/utils.star', - 'pipeline', + "scripts/drone/steps/lib.star", + "enterprise_downstream_step", +) +load( + "scripts/drone/utils/utils.star", + "pipeline", ) trigger = { - 'event': [ - 'push', + "event": [ + "push", ], - 'branch': 'main', - 'paths': { - 'exclude': [ - '*.md', - 'docs/**', - 'latest.json', + "branch": "main", + "paths": { + "exclude": [ + "*.md", + "docs/**", + "latest.json", ], }, } - def enterprise_downstream_pipeline(): - environment = {'EDITION': 'oss'} + environment = {"EDITION": "oss"} steps = [ - enterprise_downstream_step(ver_mode='main'), + enterprise_downstream_step(ver_mode = "main"), ] deps = [ - 'main-build-e2e-publish', - 'main-integration-tests', + "main-build-e2e-publish", + "main-integration-tests", ] return pipeline( - name='main-trigger-downstream', - edition='oss', - trigger=trigger, - services=[], - steps=steps, - depends_on=deps, - environment=environment, + name = "main-trigger-downstream", + edition = "oss", + trigger = trigger, + services = [], + steps = steps, + depends_on = deps, + environment = environment, ) diff --git a/scripts/drone/pipelines/verify_drone.star b/scripts/drone/pipelines/verify_drone.star index 4a2f3c91d45..83fb2458c82 100644 --- a/scripts/drone/pipelines/verify_drone.star +++ b/scripts/drone/pipelines/verify_drone.star @@ -1,19 +1,21 @@ -load( - 'scripts/drone/steps/lib.star', - 'identify_runner_step', - 'download_grabpl_step', - 'lint_drone_step', - 'compile_build_cmd', -) +""" +This module returns the pipeline used for verifying Drone configuration. +""" load( - 'scripts/drone/utils/utils.star', - 'pipeline', + "scripts/drone/steps/lib.star", + "compile_build_cmd", + "download_grabpl_step", + "identify_runner_step", + "lint_drone_step", +) +load( + "scripts/drone/utils/utils.star", + "pipeline", ) - def verify_drone(trigger, ver_mode): - environment = {'EDITION': 'oss'} + environment = {"EDITION": "oss"} steps = [ identify_runner_step(), download_grabpl_step(), @@ -21,10 +23,10 @@ def verify_drone(trigger, ver_mode): lint_drone_step(), ] return pipeline( - name='{}-verify-drone'.format(ver_mode), - edition="oss", - trigger=trigger, - services=[], - steps=steps, - environment=environment, + name = "{}-verify-drone".format(ver_mode), + edition = "oss", + trigger = trigger, + services = [], + steps = steps, + environment = environment, ) diff --git a/scripts/drone/pipelines/verify_starlark.star b/scripts/drone/pipelines/verify_starlark.star new file mode 100644 index 00000000000..2970e598f12 --- /dev/null +++ b/scripts/drone/pipelines/verify_starlark.star @@ -0,0 +1,32 @@ +""" +This module returns a Drone pipeline that verifies all Starlark files are linted. +""" + +load( + "scripts/drone/steps/lib.star", + "compile_build_cmd", + "download_grabpl_step", + "identify_runner_step", + "lint_starlark_step", +) +load( + "scripts/drone/utils/utils.star", + "pipeline", +) + +def verify_starlark(trigger, ver_mode): + environment = {"EDITION": "oss"} + steps = [ + identify_runner_step(), + download_grabpl_step(), + compile_build_cmd(), + lint_starlark_step(), + ] + return pipeline( + name = "{}-verify-starlark".format(ver_mode), + edition = "oss", + trigger = trigger, + services = [], + steps = steps, + environment = environment, + ) diff --git a/scripts/drone/pipelines/windows.star b/scripts/drone/pipelines/windows.star index e3af9b99f2d..b726edc2463 100644 --- a/scripts/drone/pipelines/windows.star +++ b/scripts/drone/pipelines/windows.star @@ -1,28 +1,41 @@ -load( - 'scripts/drone/steps/lib.star', - 'get_windows_steps', -) +""" +This module returns the pipeline used for building Grafana on Windows. +""" load( - 'scripts/drone/utils/utils.star', - 'pipeline', + "scripts/drone/utils/utils.star", + "pipeline", +) +load( + "scripts/drone/steps/lib.star", + "get_windows_steps", ) - def windows(trigger, edition, ver_mode): - environment = {'EDITION': edition} + """Generates the pipeline used for building Grafana on Windows. + + Args: + trigger: a Drone trigger for the pipeline. + edition: controls whether enterprise code is included in the pipeline steps. + ver_mode: controls whether a pre-release or actual release pipeline is generated. + Also indirectly controls which version of enterprise code is used. + + Returns: + Drone pipeline. + """ + environment = {"EDITION": edition} return pipeline( - name='main-windows', - edition=edition, - trigger=dict(trigger, repo=['grafana/grafana']), - steps=get_windows_steps(edition, ver_mode), - depends_on=[ - 'main-test-frontend', - 'main-test-backend', - 'main-build-e2e-publish', - 'main-integration-tests', + name = "main-windows", + edition = edition, + trigger = dict(trigger, repo = ["grafana/grafana"]), + steps = get_windows_steps(edition, ver_mode), + depends_on = [ + "main-test-frontend", + "main-test-backend", + "main-build-e2e-publish", + "main-integration-tests", ], - platform='windows', - environment=environment, + platform = "windows", + environment = environment, ) diff --git a/scripts/drone/services/services.star b/scripts/drone/services/services.star index b0d745b0b7a..9d7d04a2ded 100644 --- a/scripts/drone/services/services.star +++ b/scripts/drone/services/services.star @@ -1,64 +1,66 @@ +""" +This module has functions for Drone services to be used in pipelines. +""" + def integration_test_services_volumes(): return [ - {'name': 'postgres', 'temp': {'medium': 'memory'}}, - {'name': 'mysql', 'temp': {'medium': 'memory'}}, + {"name": "postgres", "temp": {"medium": "memory"}}, + {"name": "mysql", "temp": {"medium": "memory"}}, ] - def integration_test_services(edition): services = [ { - 'name': 'postgres', - 'image': 'postgres:12.3-alpine', - 'environment': { - 'POSTGRES_USER': 'grafanatest', - 'POSTGRES_PASSWORD': 'grafanatest', - 'POSTGRES_DB': 'grafanatest', - 'PGDATA': '/var/lib/postgresql/data/pgdata', + "name": "postgres", + "image": "postgres:12.3-alpine", + "environment": { + "POSTGRES_USER": "grafanatest", + "POSTGRES_PASSWORD": "grafanatest", + "POSTGRES_DB": "grafanatest", + "PGDATA": "/var/lib/postgresql/data/pgdata", }, - 'volumes': [ - {'name': 'postgres', 'path': '/var/lib/postgresql/data/pgdata'} + "volumes": [ + {"name": "postgres", "path": "/var/lib/postgresql/data/pgdata"}, ], }, { - 'name': 'mysql', - 'image': 'mysql:5.7.39', - 'environment': { - 'MYSQL_ROOT_PASSWORD': 'rootpass', - 'MYSQL_DATABASE': 'grafana_tests', - 'MYSQL_USER': 'grafana', - 'MYSQL_PASSWORD': 'password', + "name": "mysql", + "image": "mysql:5.7.39", + "environment": { + "MYSQL_ROOT_PASSWORD": "rootpass", + "MYSQL_DATABASE": "grafana_tests", + "MYSQL_USER": "grafana", + "MYSQL_PASSWORD": "password", }, - 'volumes': [{'name': 'mysql', 'path': '/var/lib/mysql'}], + "volumes": [{"name": "mysql", "path": "/var/lib/mysql"}], }, ] - if edition in ('enterprise', 'enterprise2'): + if edition in ("enterprise", "enterprise2"): services.extend( [ { - 'name': 'redis', - 'image': 'redis:6.2.1-alpine', - 'environment': {}, + "name": "redis", + "image": "redis:6.2.1-alpine", + "environment": {}, }, { - 'name': 'memcached', - 'image': 'memcached:1.6.9-alpine', - 'environment': {}, + "name": "memcached", + "image": "memcached:1.6.9-alpine", + "environment": {}, }, - ] + ], ) return services - def ldap_service(): return { - 'name': 'ldap', - 'image': 'osixia/openldap:1.4.0', - 'environment': { - 'LDAP_ADMIN_PASSWORD': 'grafana', - 'LDAP_DOMAIN': 'grafana.org', - 'SLAPD_ADDITIONAL_MODULES': 'memberof', + "name": "ldap", + "image": "osixia/openldap:1.4.0", + "environment": { + "LDAP_ADMIN_PASSWORD": "grafana", + "LDAP_DOMAIN": "grafana.org", + "SLAPD_ADDITIONAL_MODULES": "memberof", }, } diff --git a/scripts/drone/steps/lib.star b/scripts/drone/steps/lib.star index 2bc37806f14..8ce9b098468 100644 --- a/scripts/drone/steps/lib.star +++ b/scripts/drone/steps/lib.star @@ -1,389 +1,451 @@ +""" +This module is a library of Drone steps and other pipeline components. +""" + load( - 'scripts/drone/vault.star', - 'from_secret', - 'prerelease_bucket', - 'pull_secret', + "scripts/drone/vault.star", + "from_secret", + "prerelease_bucket", ) -grabpl_version = 'v3.0.20' -build_image = 'grafana/build-container:1.6.7' -publish_image = 'grafana/grafana-ci-deploy:1.3.3' -deploy_docker_image = 'us.gcr.io/kubernetes-dev/drone/plugins/deploy-image' -alpine_image = 'alpine:3.15.6' -curl_image = 'byrnedo/alpine-curl:0.1.8' -windows_image = 'mcr.microsoft.com/windows:1809' -wix_image = 'grafana/ci-wix:0.1.1' -go_image = 'golang:1.19.4' +grabpl_version = "v3.0.20" +build_image = "grafana/build-container:v1.7.1" +publish_image = "grafana/grafana-ci-deploy:1.3.3" +deploy_docker_image = "us.gcr.io/kubernetes-dev/drone/plugins/deploy-image" +alpine_image = "alpine:3.15.6" +curl_image = "byrnedo/alpine-curl:0.1.8" +windows_image = "mcr.microsoft.com/windows:1809" +wix_image = "grafana/ci-wix:0.1.1" +go_image = "golang:1.19.4" trigger_oss = { - 'repo': [ - 'grafana/grafana', - ] + "repo": [ + "grafana/grafana", + ], } - def slack_step(channel, template, secret): return { - 'name': 'slack', - 'image': 'plugins/slack', - 'settings': { - 'webhook': from_secret(secret), - 'channel': channel, - 'template': template, + "name": "slack", + "image": "plugins/slack", + "settings": { + "webhook": from_secret(secret), + "channel": channel, + "template": template, }, } - def yarn_install_step(): return { - 'name': 'yarn-install', - 'image': build_image, - 'commands': [ - 'yarn install --immutable', + "name": "yarn-install", + "image": build_image, + "commands": [ + "yarn install --immutable", ], - 'depends_on': [], + "depends_on": [], } - def wire_install_step(): return { - 'name': 'wire-install', - 'image': build_image, - 'commands': [ - 'make gen-go', + "name": "wire-install", + "image": build_image, + "commands": [ + "make gen-go", ], - 'depends_on': [ - 'verify-gen-cue', + "depends_on": [ + "verify-gen-cue", ], } - -def identify_runner_step(platform='linux'): - if platform == 'linux': +def identify_runner_step(platform = "linux"): + if platform == "linux": return { - 'name': 'identify-runner', - 'image': alpine_image, - 'commands': [ - 'echo $DRONE_RUNNER_NAME', + "name": "identify-runner", + "image": alpine_image, + "commands": [ + "echo $DRONE_RUNNER_NAME", ], } else: return { - 'name': 'identify-runner', - 'image': windows_image, - 'commands': [ - 'echo $env:DRONE_RUNNER_NAME', + "name": "identify-runner", + "image": windows_image, + "commands": [ + "echo $env:DRONE_RUNNER_NAME", ], } +def clone_enterprise_step(committish = "${DRONE_COMMIT}"): + """Clone the enterprise source into the ./grafana-enterprise directory. -def clone_enterprise_step(committish='${DRONE_COMMIT}'): + Args: + committish: controls which revision of grafana-enterprise is cloned. + + Returns: + Drone step. + """ return { - 'name': 'clone-enterprise', - 'image': build_image, - 'environment': { - 'GITHUB_TOKEN': from_secret('github_token'), + "name": "clone-enterprise", + "image": build_image, + "environment": { + "GITHUB_TOKEN": from_secret("github_token"), }, - 'commands': [ + "commands": [ 'git clone "https://$${GITHUB_TOKEN}@github.com/grafana/grafana-enterprise.git"', - 'cd grafana-enterprise', - 'git checkout {}'.format(committish), + "cd grafana-enterprise", + "git checkout {}".format(committish), ], } - def init_enterprise_step(ver_mode): - source_commit = '' - if ver_mode == 'release': - source_commit = ' ${DRONE_TAG}' + """Adds the enterprise deployment configuration into the source directory. + + Args: + ver_mode: controls what revision of the OSS source to use. + If ver_mode is 'release', the step uses the tagged revision. + Otherwise, the DRONE_SOURCE_BRANCH is used. + + Returns: + Drone step. + """ + source_commit = "" + if ver_mode == "release": + source_commit = " ${DRONE_TAG}" environment = { - 'GITHUB_TOKEN': from_secret('github_token'), + "GITHUB_TOKEN": from_secret("github_token"), } token = "--github-token $${GITHUB_TOKEN}" - elif ver_mode == 'release-branch': + elif ver_mode == "release-branch": environment = { - 'GITHUB_TOKEN': from_secret('github_token'), + "GITHUB_TOKEN": from_secret("github_token"), } token = "--github-token $${GITHUB_TOKEN}" else: environment = {} token = "" return { - 'name': 'init-enterprise', - 'image': build_image, - 'depends_on': [ - 'clone-enterprise', + "name": "init-enterprise", + "image": build_image, + "depends_on": [ + "clone-enterprise", ], - 'environment': environment, - 'commands': [ - 'mv bin/grabpl /tmp/', - 'rmdir bin', - 'mv grafana-enterprise /tmp/', - '/tmp/grabpl init-enterprise {} /tmp/grafana-enterprise{}'.format( - token, source_commit + "environment": environment, + "commands": [ + "mv bin/grabpl /tmp/", + "rmdir bin", + "mv grafana-enterprise /tmp/", + "/tmp/grabpl init-enterprise {} /tmp/grafana-enterprise{}".format( + token, + source_commit, ).rstrip(), - 'mv /tmp/grafana-enterprise/deployment_tools_config.json deployment_tools_config.json', - 'mkdir bin', - 'mv /tmp/grabpl bin/', + "mv /tmp/grafana-enterprise/deployment_tools_config.json deployment_tools_config.json", + "mkdir bin", + "mv /tmp/grabpl bin/", ], } - -def download_grabpl_step(platform="linux"): - if platform == 'windows': +def download_grabpl_step(platform = "linux"): + if platform == "windows": return { - 'name': 'grabpl', - 'image': wix_image, - 'commands': [ + "name": "grabpl", + "image": wix_image, + "commands": [ '$$ProgressPreference = "SilentlyContinue"', - 'Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/{}/windows/grabpl.exe -OutFile grabpl.exe'.format( - grabpl_version + "Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/{}/windows/grabpl.exe -OutFile grabpl.exe".format( + grabpl_version, ), ], } return { - 'name': 'grabpl', - 'image': curl_image, - 'commands': [ - 'mkdir -p bin', - 'curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/{}/grabpl'.format( - grabpl_version + "name": "grabpl", + "image": curl_image, + "commands": [ + "mkdir -p bin", + "curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/{}/grabpl".format( + grabpl_version, ), - 'chmod +x bin/grabpl', + "chmod +x bin/grabpl", ], } - def lint_drone_step(): return { - 'name': 'lint-drone', - 'image': curl_image, - 'commands': [ - './bin/build verify-drone', + "name": "lint-drone", + "image": curl_image, + "commands": [ + "./bin/build verify-drone", ], - 'depends_on': [ - 'compile-build-cmd', + "depends_on": [ + "compile-build-cmd", ], } +def lint_starlark_step(): + return { + "name": "lint-starlark", + "image": build_image, + "commands": [ + "./bin/build verify-starlark .", + ], + "depends_on": [ + "compile-build-cmd", + ], + } def enterprise_downstream_step(ver_mode): - repo = 'grafana/grafana-enterprise@' - if ver_mode == 'pr': - repo += '${DRONE_SOURCE_BRANCH}' + """Triggers a downstream pipeline in the grafana-enterprise repository. + + Args: + ver_mode: indirectly controls the revision used for downstream pipelines. + It also used to allow the step to fail for pull requests without blocking merging. + + Returns: + Drone step. + """ + repo = "grafana/grafana-enterprise@" + if ver_mode == "pr": + repo += "${DRONE_SOURCE_BRANCH}" else: - repo += 'main' + repo += "main" step = { - 'name': 'trigger-enterprise-downstream', - 'image': 'grafana/drone-downstream', - 'settings': { - 'server': 'https://drone.grafana.net', - 'token': from_secret('drone_token'), - 'repositories': [ + "name": "trigger-enterprise-downstream", + "image": "grafana/drone-downstream", + "settings": { + "server": "https://drone.grafana.net", + "token": from_secret("drone_token"), + "repositories": [ repo, ], - 'params': [ - 'SOURCE_BUILD_NUMBER=${DRONE_COMMIT}', - 'SOURCE_COMMIT=${DRONE_COMMIT}', + "params": [ + "SOURCE_BUILD_NUMBER=${DRONE_COMMIT}", + "SOURCE_COMMIT=${DRONE_COMMIT}", ], }, } - if ver_mode == 'pr': - step.update({'failure': 'ignore'}) - step['settings']['params'].append('OSS_PULL_REQUEST=${DRONE_PULL_REQUEST}') + if ver_mode == "pr": + step.update({"failure": "ignore"}) + step["settings"]["params"].append("OSS_PULL_REQUEST=${DRONE_PULL_REQUEST}") return step - def lint_backend_step(): return { - 'name': 'lint-backend', + "name": "lint-backend", # TODO: build_image or go_image? - 'image': go_image, - 'environment': { + "image": go_image, + "environment": { # We need CGO because of go-sqlite3 - 'CGO_ENABLED': '1', + "CGO_ENABLED": "1", }, - 'depends_on': [ - 'wire-install', + "depends_on": [ + "wire-install", ], - 'commands': [ - 'apt-get update && apt-get install make', + "commands": [ + "apt-get update && apt-get install make", # Don't use Make since it will re-download the linters - 'make lint-go', + "make lint-go", ], } - def benchmark_ldap_step(): return { - 'name': 'benchmark-ldap', - 'image': build_image, - 'environment': { - 'LDAP_HOSTNAME': 'ldap', + "name": "benchmark-ldap", + "image": build_image, + "environment": { + "LDAP_HOSTNAME": "ldap", }, - 'commands': [ - 'dockerize -wait tcp://ldap:389 -timeout 120s', + "commands": [ + "dockerize -wait tcp://ldap:389 -timeout 120s", 'go test -benchmem -run=^$ ./pkg/extensions/ldapsync -bench "^(Benchmark50Users)$"', ], } - def build_storybook_step(ver_mode): return { - 'name': 'build-storybook', - 'image': build_image, - 'depends_on': [ + "name": "build-storybook", + "image": build_image, + "depends_on": [ # Best to ensure that this step doesn't mess with what's getting built and packaged - 'build-frontend', - 'build-frontend-packages', + "build-frontend", + "build-frontend-packages", ], - 'environment': { - 'NODE_OPTIONS': '--max_old_space_size=4096', + "environment": { + "NODE_OPTIONS": "--max_old_space_size=4096", }, - 'commands': [ - 'yarn storybook:build', - './bin/build verify-storybook', + "commands": [ + "yarn storybook:build", + "./bin/build verify-storybook", ], - 'when': get_trigger_storybook(ver_mode), + "when": get_trigger_storybook(ver_mode), } +def store_storybook_step(ver_mode, trigger = None): + """Publishes the Grafana UI components storybook. -def store_storybook_step(ver_mode, trigger=None): + Args: + ver_mode: controls whether a release or canary version is published. + trigger: a Drone trigger for the step. + Defaults to None. + + Returns: + Drone step. + """ commands = [] - if ver_mode == 'release': + if ver_mode == "release": commands.extend( [ - './bin/build store-storybook --deployment latest', - './bin/build store-storybook --deployment ${DRONE_TAG}', - ] + "./bin/build store-storybook --deployment latest", + "./bin/build store-storybook --deployment ${DRONE_TAG}", + ], ) else: # main pipelines should deploy storybook to grafana-storybook/canary public bucket commands = [ - './bin/build store-storybook --deployment canary', + "./bin/build store-storybook --deployment canary", ] step = { - 'name': 'store-storybook', - 'image': publish_image, - 'depends_on': [ - 'build-storybook', - ] - + end_to_end_tests_deps(), - 'environment': { - 'GCP_KEY': from_secret('gcp_key'), - 'PRERELEASE_BUCKET': from_secret(prerelease_bucket), + "name": "store-storybook", + "image": publish_image, + "depends_on": [ + "build-storybook", + ] + + end_to_end_tests_deps(), + "environment": { + "GCP_KEY": from_secret("gcp_key"), + "PRERELEASE_BUCKET": from_secret(prerelease_bucket), }, - 'commands': commands, - 'when': get_trigger_storybook(ver_mode), + "commands": commands, + "when": get_trigger_storybook(ver_mode), } if trigger and ver_mode in ("release-branch", "main"): # no dict merge operation available, https://github.com/harness/drone-cli/pull/220 when_cond = { - 'repo': [ - 'grafana/grafana', + "repo": [ + "grafana/grafana", ], - 'paths': { - 'include': [ - 'packages/grafana-ui/**', + "paths": { + "include": [ + "packages/grafana-ui/**", ], }, } - step = dict(step, when=when_cond) + step = dict(step, when = when_cond) return step - def e2e_tests_artifacts(): return { - 'name': 'e2e-tests-artifacts-upload', - 'image': 'google/cloud-sdk:406.0.0', - 'depends_on': [ - 'end-to-end-tests-dashboards-suite', - 'end-to-end-tests-panels-suite', - 'end-to-end-tests-smoke-tests-suite', - 'end-to-end-tests-various-suite', + "name": "e2e-tests-artifacts-upload", + "image": "google/cloud-sdk:406.0.0", + "depends_on": [ + "end-to-end-tests-dashboards-suite", + "end-to-end-tests-panels-suite", + "end-to-end-tests-smoke-tests-suite", + "end-to-end-tests-various-suite", ], - 'failure': 'ignore', - 'when': { - 'status': [ - 'success', - 'failure', - ] + "failure": "ignore", + "when": { + "status": [ + "success", + "failure", + ], }, - 'environment': { - 'GCP_GRAFANA_UPLOAD_ARTIFACTS_KEY': from_secret('gcp_upload_artifacts_key'), - 'E2E_TEST_ARTIFACTS_BUCKET': 'releng-pipeline-artifacts-dev', - 'GITHUB_TOKEN': from_secret('github_token'), + "environment": { + "GCP_GRAFANA_UPLOAD_ARTIFACTS_KEY": from_secret("gcp_upload_artifacts_key"), + "E2E_TEST_ARTIFACTS_BUCKET": "releng-pipeline-artifacts-dev", + "GITHUB_TOKEN": from_secret("github_token"), }, - 'commands': [ - 'apt-get update', - 'apt-get install -yq zip', - 'printenv GCP_GRAFANA_UPLOAD_ARTIFACTS_KEY > /tmp/gcpkey_upload_artifacts.json', - 'gcloud auth activate-service-account --key-file=/tmp/gcpkey_upload_artifacts.json', + "commands": [ + "apt-get update", + "apt-get install -yq zip", + "printenv GCP_GRAFANA_UPLOAD_ARTIFACTS_KEY > /tmp/gcpkey_upload_artifacts.json", + "gcloud auth activate-service-account --key-file=/tmp/gcpkey_upload_artifacts.json", # we want to only include files in e2e folder that end with .spec.ts.mp4 'find ./e2e -type f -name "*spec.ts.mp4" | zip e2e/videos.zip -@', - 'gsutil cp e2e/videos.zip gs://$${E2E_TEST_ARTIFACTS_BUCKET}/${DRONE_BUILD_NUMBER}/artifacts/videos/videos.zip', - 'export E2E_ARTIFACTS_VIDEO_ZIP=https://storage.googleapis.com/$${E2E_TEST_ARTIFACTS_BUCKET}/${DRONE_BUILD_NUMBER}/artifacts/videos/videos.zip', + "gsutil cp e2e/videos.zip gs://$${E2E_TEST_ARTIFACTS_BUCKET}/${DRONE_BUILD_NUMBER}/artifacts/videos/videos.zip", + "export E2E_ARTIFACTS_VIDEO_ZIP=https://storage.googleapis.com/$${E2E_TEST_ARTIFACTS_BUCKET}/${DRONE_BUILD_NUMBER}/artifacts/videos/videos.zip", 'echo "E2E Test artifacts uploaded to: $${E2E_ARTIFACTS_VIDEO_ZIP}"', - 'curl -X POST https://api.github.com/repos/${DRONE_REPO}/statuses/${DRONE_COMMIT_SHA} -H "Authorization: token $${GITHUB_TOKEN}" -d ' - + '"{\\"state\\":\\"success\\",\\"target_url\\":\\"$${E2E_ARTIFACTS_VIDEO_ZIP}\\", \\"description\\": \\"Click on the details to download e2e recording videos\\", \\"context\\": \\"e2e_artifacts\\"}"', + 'curl -X POST https://api.github.com/repos/${DRONE_REPO}/statuses/${DRONE_COMMIT_SHA} -H "Authorization: token $${GITHUB_TOKEN}" -d ' + + '"{\\"state\\":\\"success\\",\\"target_url\\":\\"$${E2E_ARTIFACTS_VIDEO_ZIP}\\", \\"description\\": \\"Click on the details to download e2e recording videos\\", \\"context\\": \\"e2e_artifacts\\"}"', ], } +def upload_cdn_step(edition, ver_mode, trigger = None): + """Uploads CDN assets using the Grafana build tool. -def upload_cdn_step(edition, ver_mode, trigger=None): + Args: + edition: controls the output directory for the CDN assets. + ver_mode: only uses the step trigger when ver_mode == 'release-branch' or 'main' + trigger: a Drone trigger for the step. + Defaults to None. + + Returns: + Drone step. + """ deps = [] - if edition in 'enterprise2': + if edition in "enterprise2": deps.extend( [ - 'package' + enterprise2_suffix(edition), - ] + "package" + enterprise2_suffix(edition), + ], ) else: deps.extend( [ - 'grafana-server', - ] + "grafana-server", + ], ) step = { - 'name': 'upload-cdn-assets' + enterprise2_suffix(edition), - 'image': publish_image, - 'depends_on': deps, - 'environment': { - 'GCP_KEY': from_secret('gcp_key'), - 'PRERELEASE_BUCKET': from_secret(prerelease_bucket), + "name": "upload-cdn-assets" + enterprise2_suffix(edition), + "image": publish_image, + "depends_on": deps, + "environment": { + "GCP_KEY": from_secret("gcp_key"), + "PRERELEASE_BUCKET": from_secret(prerelease_bucket), }, - 'commands': [ - './bin/build upload-cdn --edition {}'.format(edition), + "commands": [ + "./bin/build upload-cdn --edition {}".format(edition), ], } if trigger and ver_mode in ("release-branch", "main"): - step = dict(step, when=trigger) + step = dict(step, when = trigger) return step +def build_backend_step(edition, ver_mode, variants = None): + """Build the backend code using the Grafana build tool. -def build_backend_step(edition, ver_mode, variants=None): - variants_str = '' + Args: + edition: controls which edition of the backend is built. + ver_mode: if ver_mode != 'release', pass the DRONE_BUILD_NUMBER environment + variable as the value for the --build-id option. + TODO: is this option actually used by the build-backend subcommand? + variants: a list of variants be passed to the build-backend subcommand + using the --variants option. + Defaults to None. + + Returns: + Drone step. + """ + variants_str = "" if variants: - variants_str = ' --variants {}'.format(','.join(variants)) + variants_str = " --variants {}".format(",".join(variants)) # TODO: Convert number of jobs to percentage - if ver_mode == 'release': + if ver_mode == "release": cmds = [ - './bin/build build-backend --jobs 8 --edition {} ${{DRONE_TAG}}'.format( + "./bin/build build-backend --jobs 8 --edition {} ${{DRONE_TAG}}".format( edition, ), ] else: - build_no = '${DRONE_BUILD_NUMBER}' + build_no = "${DRONE_BUILD_NUMBER}" cmds = [ - './bin/build build-backend --jobs 8 --edition {} --build-id {}{}'.format( + "./bin/build build-backend --jobs 8 --edition {} --build-id {}{}".format( edition, build_no, variants_str, @@ -391,938 +453,1084 @@ def build_backend_step(edition, ver_mode, variants=None): ] return { - 'name': 'build-backend' + enterprise2_suffix(edition), - 'image': build_image, - 'depends_on': [ - 'wire-install', - 'compile-build-cmd', + "name": "build-backend" + enterprise2_suffix(edition), + "image": build_image, + "depends_on": [ + "wire-install", + "compile-build-cmd", ], - 'commands': cmds, + "commands": cmds, } - def build_frontend_step(edition, ver_mode): - build_no = '${DRONE_BUILD_NUMBER}' + """Build the frontend code using the Grafana build tool. + + Args: + edition: controls which edition of the frontend is built. + ver_mode: if ver_mode != 'release', use the DRONE_BUILD_NUMBER environment + variable as a build identifier. + + Returns: + Drone step. + """ + build_no = "${DRONE_BUILD_NUMBER}" # TODO: Use percentage for num jobs - if ver_mode == 'release': + if ver_mode == "release": cmds = [ - './bin/build build-frontend --jobs 8 ' - + '--edition {} ${{DRONE_TAG}}'.format(edition), + "./bin/build build-frontend --jobs 8 " + + "--edition {} ${{DRONE_TAG}}".format(edition), ] else: cmds = [ - './bin/build build-frontend --jobs 8 --edition {} '.format(edition) - + '--build-id {}'.format(build_no), + "./bin/build build-frontend --jobs 8 --edition {} ".format(edition) + + "--build-id {}".format(build_no), ] return { - 'name': 'build-frontend', - 'image': build_image, - 'environment': { - 'NODE_OPTIONS': '--max_old_space_size=8192', + "name": "build-frontend", + "image": build_image, + "environment": { + "NODE_OPTIONS": "--max_old_space_size=8192", }, - 'depends_on': [ - 'compile-build-cmd', - 'yarn-install', + "depends_on": [ + "compile-build-cmd", + "yarn-install", ], - 'commands': cmds, + "commands": cmds, } - def build_frontend_package_step(edition, ver_mode): - build_no = '${DRONE_BUILD_NUMBER}' + """Build the frontend packages using the Grafana build tool. + + Args: + edition: controls which edition of the frontend is built. + ver_mode: if ver_mode != 'release', use the DRONE_BUILD_NUMBER environment + variable as a build identifier. + + Returns: + Drone step. + """ + build_no = "${DRONE_BUILD_NUMBER}" # TODO: Use percentage for num jobs - if ver_mode == 'release': + if ver_mode == "release": cmds = [ - './bin/build build-frontend-packages --jobs 8 ' - + '--edition {} ${{DRONE_TAG}}'.format(edition), + "./bin/build build-frontend-packages --jobs 8 " + + "--edition {} ${{DRONE_TAG}}".format(edition), ] else: cmds = [ - './bin/build build-frontend-packages --jobs 8 --edition {} '.format(edition) - + '--build-id {}'.format(build_no), + "./bin/build build-frontend-packages --jobs 8 --edition {} ".format(edition) + + "--build-id {}".format(build_no), ] return { - 'name': 'build-frontend-packages', - 'image': build_image, - 'environment': { - 'NODE_OPTIONS': '--max_old_space_size=8192', + "name": "build-frontend-packages", + "image": build_image, + "environment": { + "NODE_OPTIONS": "--max_old_space_size=8192", }, - 'depends_on': [ - 'compile-build-cmd', - 'yarn-install', + "depends_on": [ + "compile-build-cmd", + "yarn-install", ], - 'commands': cmds, + "commands": cmds, } - def build_plugins_step(edition, ver_mode): - if ver_mode != 'pr': + if ver_mode != "pr": env = { - 'GRAFANA_API_KEY': from_secret('grafana_api_key'), + "GRAFANA_API_KEY": from_secret("grafana_api_key"), } else: env = None return { - 'name': 'build-plugins', - 'image': build_image, - 'environment': env, - 'depends_on': [ - 'compile-build-cmd', - 'yarn-install', + "name": "build-plugins", + "image": build_image, + "environment": env, + "depends_on": [ + "compile-build-cmd", + "yarn-install", ], - 'commands': [ + "commands": [ # TODO: Use percentage for num jobs - './bin/build build-plugins --jobs 8 --edition {}'.format(edition), + "./bin/build build-plugins --jobs 8 --edition {}".format(edition), ], } - def test_backend_step(): return { - 'name': 'test-backend', - 'image': build_image, - 'depends_on': [ - 'wire-install', + "name": "test-backend", + "image": build_image, + "depends_on": [ + "wire-install", ], - 'commands': [ - 'go test -short -covermode=atomic -timeout=5m ./pkg/...', + "commands": [ + "go test -short -covermode=atomic -timeout=5m ./pkg/...", ], } - def test_backend_integration_step(): return { - 'name': 'test-backend-integration', - 'image': build_image, - 'depends_on': [ - 'wire-install', + "name": "test-backend-integration", + "image": build_image, + "depends_on": [ + "wire-install", ], - 'commands': [ - 'go test -run Integration -covermode=atomic -timeout=5m ./pkg/...', + "commands": [ + "go test -run Integration -covermode=atomic -timeout=5m ./pkg/...", ], } +def betterer_frontend_step(edition = "oss"): + """Run betterer on frontend code. -def betterer_frontend_step(edition="oss"): + Args: + edition: controls whether enterprise code is also included in the source. + Defaults to 'oss'. + + Returns: + Drone step. + """ deps = [] if edition == "enterprise": - deps.extend(['init-enterprise']) - deps.extend(['yarn-install']) + deps.extend(["init-enterprise"]) + deps.extend(["yarn-install"]) return { - 'name': 'betterer-frontend', - 'image': build_image, - 'depends_on': deps, - 'commands': [ - 'yarn betterer ci', + "name": "betterer-frontend", + "image": build_image, + "depends_on": deps, + "commands": [ + "yarn betterer ci", ], } +def test_frontend_step(edition = "oss"): + """Runs tests on frontend code. -def test_frontend_step(edition="oss"): + Args: + edition: controls whether enterprise code is also included in the source. + Defaults to 'oss'. + + Returns: + Drone step. + """ deps = [] if edition == "enterprise": - deps.extend(['init-enterprise']) - deps.extend(['yarn-install']) + deps.extend(["init-enterprise"]) + deps.extend(["yarn-install"]) return { - 'name': 'test-frontend', - 'image': build_image, - 'environment': { - 'TEST_MAX_WORKERS': '50%', + "name": "test-frontend", + "image": build_image, + "environment": { + "TEST_MAX_WORKERS": "50%", }, - 'depends_on': deps, - 'commands': [ - 'yarn run ci:test-frontend', + "depends_on": deps, + "commands": [ + "yarn run ci:test-frontend", ], } - def lint_frontend_step(): return { - 'name': 'lint-frontend', - 'image': build_image, - 'environment': { - 'TEST_MAX_WORKERS': '50%', + "name": "lint-frontend", + "image": build_image, + "environment": { + "TEST_MAX_WORKERS": "50%", }, - 'depends_on': [ - 'yarn-install', + "depends_on": [ + "yarn-install", ], - 'commands': [ - 'yarn run prettier:check', - 'yarn run lint', - 'yarn run i18n:compile', # TODO: right place for this? - 'yarn run typecheck', + "commands": [ + "yarn run prettier:check", + "yarn run lint", + "yarn run i18n:compile", # TODO: right place for this? + "yarn run typecheck", ], } +def test_a11y_frontend_step(ver_mode, port = 3001): + """Runs automated accessiblity tests against the frontend. -def test_a11y_frontend_step(ver_mode, port=3001): + Args: + ver_mode: controls whether the step is blocking or just reporting. + If ver_mode == 'pr', the step causes the pipeline to fail. + port: which port to grafana-server is expected to be listening on. + Defaults to 3001. + + Returns: + Drone step. + """ commands = [ - 'yarn wait-on http://$HOST:$PORT', + "yarn wait-on http://$HOST:$PORT", ] - failure = 'ignore' - if ver_mode == 'pr': + failure = "ignore" + if ver_mode == "pr": commands.extend( [ - 'pa11y-ci --config .pa11yci-pr.conf.js', - ] + "pa11y-ci --config .pa11yci-pr.conf.js", + ], ) - failure = 'always' + failure = "always" else: commands.extend( [ - 'pa11y-ci --config .pa11yci.conf.js --json > pa11y-ci-results.json', - ] + "pa11y-ci --config .pa11yci.conf.js --json > pa11y-ci-results.json", + ], ) return { - 'name': 'test-a11y-frontend', + "name": "test-a11y-frontend", # TODO which image should be used? - 'image': 'grafana/docker-puppeteer:1.1.0', - 'depends_on': [ - 'grafana-server', + "image": "grafana/docker-puppeteer:1.1.0", + "depends_on": [ + "grafana-server", ], - 'environment': { - 'GRAFANA_MISC_STATS_API_KEY': from_secret('grafana_misc_stats_api_key'), - 'HOST': 'grafana-server', - 'PORT': port, + "environment": { + "GRAFANA_MISC_STATS_API_KEY": from_secret("grafana_misc_stats_api_key"), + "HOST": "grafana-server", + "PORT": port, }, - 'failure': failure, - 'commands': commands, + "failure": failure, + "commands": commands, } +def frontend_metrics_step(trigger = None): + """Reports frontend metrics to Grafana Cloud. -def frontend_metrics_step(trigger=None): + Args: + trigger: a Drone trigger for the step. + Defaults to None. + + Returns: + Drone step. + """ step = { - 'name': 'publish-frontend-metrics', - 'image': build_image, - 'depends_on': [ - 'test-a11y-frontend', + "name": "publish-frontend-metrics", + "image": build_image, + "depends_on": [ + "test-a11y-frontend", ], - 'environment': { - 'GRAFANA_MISC_STATS_API_KEY': from_secret('grafana_misc_stats_api_key'), + "environment": { + "GRAFANA_MISC_STATS_API_KEY": from_secret("grafana_misc_stats_api_key"), }, - 'failure': 'ignore', - 'commands': [ - './scripts/ci-frontend-metrics.sh | ./bin/build publish-metrics $${GRAFANA_MISC_STATS_API_KEY}', + "failure": "ignore", + "commands": [ + "./scripts/ci-frontend-metrics.sh | ./bin/build publish-metrics $${GRAFANA_MISC_STATS_API_KEY}", ], } if trigger: - step = dict(step, when=trigger) + step = dict(step, when = trigger) return step - def codespell_step(): return { - 'name': 'codespell', - 'image': build_image, - 'commands': [ + "name": "codespell", + "image": build_image, + "commands": [ # Important: all words have to be in lowercase, and separated by "\n". 'echo -e "unknwon\nreferer\nerrorstring\neror\niam\nwan" > words_to_ignore.txt', - 'codespell -I words_to_ignore.txt docs/', - 'rm words_to_ignore.txt', + "codespell -I words_to_ignore.txt docs/", + "rm words_to_ignore.txt", ], } +def package_step(edition, ver_mode, variants = None): + """Packages Grafana with the Grafana build tool. -def package_step(edition, ver_mode, variants=None): + Args: + edition: controls which edition of Grafana is packaged. + ver_mode: controls whether the packages are signed for a release. + If ver_mode != 'release', use the DRONE_BUILD_NUMBER environment + variable as a build identifier. + variants: a list of variants be passed to the package subcommand + using the --variants option. + Defaults to None. + + Returns: + Drone step. + """ deps = [ - 'build-plugins', - 'build-backend' + enterprise2_suffix(edition), - 'build-frontend', - 'build-frontend-packages', + "build-plugins", + "build-backend" + enterprise2_suffix(edition), + "build-frontend", + "build-frontend-packages", ] - variants_str = '' + variants_str = "" if variants: - variants_str = ' --variants {}'.format(','.join(variants)) + variants_str = " --variants {}".format(",".join(variants)) - if ver_mode in ('main', 'release', 'release-branch'): - sign_args = ' --sign' + if ver_mode in ("main", "release", "release-branch"): + sign_args = " --sign" env = { - 'GRAFANA_API_KEY': from_secret('grafana_api_key'), - 'GPG_PRIV_KEY': from_secret('packages_gpg_private_key'), - 'GPG_PUB_KEY': from_secret('packages_gpg_public_key'), - 'GPG_KEY_PASSWORD': from_secret('packages_gpg_passphrase'), + "GRAFANA_API_KEY": from_secret("grafana_api_key"), + "GPG_PRIV_KEY": from_secret("packages_gpg_private_key"), + "GPG_PUB_KEY": from_secret("packages_gpg_public_key"), + "GPG_KEY_PASSWORD": from_secret("packages_gpg_passphrase"), } - test_args = '' + test_args = "" else: - sign_args = '' + sign_args = "" env = None + # TODO: env vars no longer needed by build if not signing - test_args = '. scripts/build/gpg-test-vars.sh && ' + test_args = ". scripts/build/gpg-test-vars.sh && " # TODO: Use percentage for jobs - if ver_mode == 'release': + if ver_mode == "release": cmds = [ - '{}./bin/build package --jobs 8 --edition {} '.format(test_args, edition) - + '{} ${{DRONE_TAG}}'.format(sign_args), + "{}./bin/build package --jobs 8 --edition {} ".format(test_args, edition) + + "{} ${{DRONE_TAG}}".format(sign_args), ] else: - build_no = '${DRONE_BUILD_NUMBER}' + build_no = "${DRONE_BUILD_NUMBER}" cmds = [ - '{}./bin/build package --jobs 8 --edition {} '.format(test_args, edition) - + '--build-id {}{}{}'.format(build_no, variants_str, sign_args), + "{}./bin/build package --jobs 8 --edition {} ".format(test_args, edition) + + "--build-id {}{}{}".format(build_no, variants_str, sign_args), ] return { - 'name': 'package' + enterprise2_suffix(edition), - 'image': build_image, - 'depends_on': deps, - 'environment': env, - 'commands': cmds, + "name": "package" + enterprise2_suffix(edition), + "image": build_image, + "depends_on": deps, + "environment": env, + "commands": cmds, } +def grafana_server_step(edition, port = 3001): + """Runs the grafana-server binary as a service. -def grafana_server_step(edition, port=3001): - environment = {'PORT': port, 'ARCH': 'linux-amd64'} - if edition == 'enterprise': - environment['RUNDIR'] = 'scripts/grafana-server/tmp-grafana-enterprise' + Args: + edition: controls which edition of grafana-server to run. + port: port to listen on. + Defaults to 3001. + + Returns: + Drone step. + """ + environment = {"PORT": port, "ARCH": "linux-amd64"} + if edition == "enterprise": + environment["RUNDIR"] = "scripts/grafana-server/tmp-grafana-enterprise" return { - 'name': 'grafana-server', - 'image': build_image, - 'detach': True, - 'depends_on': [ - 'build-plugins', - 'build-backend', - 'build-frontend', - 'build-frontend-packages', + "name": "grafana-server", + "image": build_image, + "detach": True, + "depends_on": [ + "build-plugins", + "build-backend", + "build-frontend", + "build-frontend-packages", ], - 'environment': environment, - 'commands': [ - './scripts/grafana-server/start-server', + "environment": environment, + "commands": [ + "./scripts/grafana-server/start-server", ], } - -def e2e_tests_step(suite, port=3001, tries=None): - cmd = './bin/build e2e-tests --port {} --suite {}'.format(port, suite) +def e2e_tests_step(suite, port = 3001, tries = None): + cmd = "./bin/build e2e-tests --port {} --suite {}".format(port, suite) if tries: - cmd += ' --tries {}'.format(tries) + cmd += " --tries {}".format(tries) return { - 'name': 'end-to-end-tests-{}'.format(suite), - 'image': 'cypress/included:9.5.1-node16.14.0-slim-chrome99-ff97', - 'depends_on': [ - 'grafana-server', + "name": "end-to-end-tests-{}".format(suite), + "image": "cypress/included:9.5.1-node16.14.0-slim-chrome99-ff97", + "depends_on": [ + "grafana-server", ], - 'environment': { - 'HOST': 'grafana-server', + "environment": { + "HOST": "grafana-server", }, - 'commands': [ - 'apt-get install -y netcat', + "commands": [ + "apt-get install -y netcat", cmd, ], } +def cloud_plugins_e2e_tests_step(suite, cloud, trigger = None): + """Run cloud plugins end-to-end tests. -def cloud_plugins_e2e_tests_step(suite, cloud, port=3001, video="false", trigger=None): + Args: + suite: affects the pipeline name. + TODO: check if this actually affects step behavior. + cloud: used to determine cloud provider specific tests. + trigger: a Drone trigger for the step. + Defaults to None. + + Returns: + Drone step. + """ environment = {} when = {} if trigger: when = trigger - if cloud == 'azure': + if cloud == "azure": environment = { - 'CYPRESS_CI': 'true', - 'HOST': 'grafana-server', - 'GITHUB_TOKEN': from_secret('github_token_pr'), - 'AZURE_SP_APP_ID': from_secret('azure_sp_app_id'), - 'AZURE_SP_PASSWORD': from_secret('azure_sp_app_pw'), - 'AZURE_TENANT': from_secret('azure_tenant'), + "CYPRESS_CI": "true", + "HOST": "grafana-server", + "GITHUB_TOKEN": from_secret("github_token_pr"), + "AZURE_SP_APP_ID": from_secret("azure_sp_app_id"), + "AZURE_SP_PASSWORD": from_secret("azure_sp_app_pw"), + "AZURE_TENANT": from_secret("azure_tenant"), } when = dict( when, - paths={ - 'include': [ - 'pkg/tsdb/azuremonitor/**', - 'public/app/plugins/datasource/grafana-azure-monitor-datasource/**', - 'e2e/cloud-plugins-suite/azure-monitor.spec.ts', - ] + paths = { + "include": [ + "pkg/tsdb/azuremonitor/**", + "public/app/plugins/datasource/grafana-azure-monitor-datasource/**", + "e2e/cloud-plugins-suite/azure-monitor.spec.ts", + ], }, ) branch = "${DRONE_SOURCE_BRANCH}".replace("/", "-") step = { - 'name': 'end-to-end-tests-{}-{}'.format(suite, cloud), - 'image': 'us-docker.pkg.dev/grafanalabs-dev/cloud-data-sources/e2e:latest', - 'depends_on': [ - 'grafana-server', + "name": "end-to-end-tests-{}-{}".format(suite, cloud), + "image": "us-docker.pkg.dev/grafanalabs-dev/cloud-data-sources/e2e:latest", + "depends_on": [ + "grafana-server", ], - 'environment': environment, - 'commands': ['cd /', './cpp-e2e/scripts/ci-run.sh {} {}'.format(cloud, branch)], + "environment": environment, + "commands": ["cd /", "./cpp-e2e/scripts/ci-run.sh {} {}".format(cloud, branch)], } - step = dict(step, when=when) + step = dict(step, when = when) return step - def build_docs_website_step(): return { - 'name': 'build-docs-website', + "name": "build-docs-website", # Use latest revision here, since we want to catch if it breaks - 'image': 'grafana/docs-base:latest', - 'commands': [ - 'mkdir -p /hugo/content/docs/grafana', - 'cp -r docs/sources/* /hugo/content/docs/grafana/latest/', - 'cd /hugo && make prod', + "image": "grafana/docs-base:latest", + "commands": [ + "mkdir -p /hugo/content/docs/grafana", + "cp -r docs/sources/* /hugo/content/docs/grafana/latest/", + "cd /hugo && make prod", ], } - -def copy_packages_for_docker_step(edition=None): +def copy_packages_for_docker_step(edition = None): return { - 'name': 'copy-packages-for-docker', - 'image': build_image, - 'depends_on': [ - 'package' + enterprise2_suffix(edition), + "name": "copy-packages-for-docker", + "image": build_image, + "depends_on": [ + "package" + enterprise2_suffix(edition), ], - 'commands': [ - 'ls dist/*.tar.gz*', - 'cp dist/*.tar.gz* packaging/docker/', + "commands": [ + "ls dist/*.tar.gz*", + "cp dist/*.tar.gz* packaging/docker/", ], } +def build_docker_images_step(edition, archs = None, ubuntu = False, publish = False): + """Build Docker images using the Grafana build tool. -def build_docker_images_step( - edition, ver_mode, archs=None, ubuntu=False, publish=False -): - cmd = './bin/build build-docker --edition {}'.format(edition) + Args: + edition: controls which repository the image is published to. + archs: a list of architectures to build the image for. + Defaults to None. + ubuntu: controls whether the final image is built from an Ubuntu base image. + Defaults to False. + publish: controls whether the built image is saved to a pre-release repository. + Defaults to False. + + Returns: + Drone step. + """ + cmd = "./bin/build build-docker --edition {}".format(edition) if publish: - cmd += ' --shouldSave' + cmd += " --shouldSave" - ubuntu_sfx = '' + ubuntu_sfx = "" if ubuntu: - ubuntu_sfx = '-ubuntu' - cmd += ' --ubuntu' + ubuntu_sfx = "-ubuntu" + cmd += " --ubuntu" if archs: - cmd += ' -archs {}'.format(','.join(archs)) + cmd += " -archs {}".format(",".join(archs)) environment = { - 'GCP_KEY': from_secret('gcp_key'), + "GCP_KEY": from_secret("gcp_key"), } - if edition == 'enterprise2': + if edition == "enterprise2": environment.update( - {'DOCKER_ENTERPRISE2_REPO': from_secret('docker_enterprise2_repo')} + {"DOCKER_ENTERPRISE2_REPO": from_secret("docker_enterprise2_repo")}, ) return { - 'name': 'build-docker-images' + ubuntu_sfx, - 'image': 'google/cloud-sdk', - 'depends_on': [ - 'copy-packages-for-docker', - 'compile-build-cmd', + "name": "build-docker-images" + ubuntu_sfx, + "image": "google/cloud-sdk", + "depends_on": [ + "copy-packages-for-docker", + "compile-build-cmd", ], - 'commands': [cmd], - 'volumes': [{'name': 'docker', 'path': '/var/run/docker.sock'}], - 'environment': environment, + "commands": [cmd], + "volumes": [{"name": "docker", "path": "/var/run/docker.sock"}], + "environment": environment, } - def fetch_images_step(edition): return { - 'name': 'fetch-images-{}'.format(edition), - 'image': 'google/cloud-sdk', - 'environment': { - 'GCP_KEY': from_secret('gcp_key'), - 'DOCKER_USER': from_secret('docker_username'), - 'DOCKER_PASSWORD': from_secret('docker_password'), - 'DOCKER_ENTERPRISE2_REPO': from_secret('docker_enterprise2_repo'), + "name": "fetch-images-{}".format(edition), + "image": "google/cloud-sdk", + "environment": { + "GCP_KEY": from_secret("gcp_key"), + "DOCKER_USER": from_secret("docker_username"), + "DOCKER_PASSWORD": from_secret("docker_password"), + "DOCKER_ENTERPRISE2_REPO": from_secret("docker_enterprise2_repo"), }, - 'commands': ['./bin/build artifacts docker fetch --edition {}'.format(edition)], - 'depends_on': ['compile-build-cmd'], - 'volumes': [{'name': 'docker', 'path': '/var/run/docker.sock'}], + "commands": ["./bin/build artifacts docker fetch --edition {}".format(edition)], + "depends_on": ["compile-build-cmd"], + "volumes": [{"name": "docker", "path": "/var/run/docker.sock"}], } +def publish_images_step(edition, ver_mode, mode, docker_repo, trigger = None): + """Generates a step for publishing public Docker images with grabpl. -def publish_images_step(edition, ver_mode, mode, docker_repo, trigger=None): + Args: + edition: controls which version of an image is fetched in the case of a release. + It also controls which publishing implementation is used. + ver_mode: controls whether the image needs to be built or retrieved from a previous build. + If ver_mode == 'release', the previously built image is fetched instead of being built again. + mode: uses to control the publishing of security images when mode == 'security'. + docker_repo: the Docker image name. + It is combined with the 'grafana/' library prefix. + trigger: a Drone trigger for the pipeline. + Defaults to None. + + Returns: + Drone step. + """ name = docker_repo - docker_repo = 'grafana/{}'.format(docker_repo) - if mode == 'security': - mode = '--{} '.format(mode) + docker_repo = "grafana/{}".format(docker_repo) + if mode == "security": + mode = "--{} ".format(mode) else: - mode = '' + mode = "" environment = { - 'GCP_KEY': from_secret('gcp_key'), - 'DOCKER_USER': from_secret('docker_username'), - 'DOCKER_PASSWORD': from_secret('docker_password'), + "GCP_KEY": from_secret("gcp_key"), + "DOCKER_USER": from_secret("docker_username"), + "DOCKER_PASSWORD": from_secret("docker_password"), } - cmd = './bin/grabpl artifacts docker publish {}--dockerhub-repo {}'.format( - mode, docker_repo + cmd = "./bin/grabpl artifacts docker publish {}--dockerhub-repo {}".format( + mode, + docker_repo, ) - deps = ['build-docker-images', 'build-docker-images-ubuntu'] - if ver_mode == 'release': - deps = ['fetch-images-{}'.format(edition)] - cmd += ' --version-tag ${DRONE_TAG}' + deps = ["build-docker-images", "build-docker-images-ubuntu"] + if ver_mode == "release": + deps = ["fetch-images-{}".format(edition)] + cmd += " --version-tag ${DRONE_TAG}" - if edition == 'enterprise2': + if edition == "enterprise2": name = edition - docker_repo = '$${DOCKER_ENTERPRISE2_REPO}' + docker_repo = "$${DOCKER_ENTERPRISE2_REPO}" environment.update( { - 'GCP_KEY': from_secret('gcp_key_hg'), - 'DOCKER_ENTERPRISE2_REPO': from_secret('docker_enterprise2_repo'), - } + "GCP_KEY": from_secret("gcp_key_hg"), + "DOCKER_ENTERPRISE2_REPO": from_secret("docker_enterprise2_repo"), + }, ) - cmd = './bin/build artifacts docker publish-enterprise2 --dockerhub-repo {}'.format( - docker_repo + cmd = "./bin/build artifacts docker publish-enterprise2 --dockerhub-repo {}".format( + docker_repo, ) step = { - 'name': 'publish-images-{}'.format(name), - 'image': 'google/cloud-sdk', - 'environment': environment, - 'commands': [cmd], - 'depends_on': deps, - 'volumes': [{'name': 'docker', 'path': '/var/run/docker.sock'}], + "name": "publish-images-{}".format(name), + "image": "google/cloud-sdk", + "environment": environment, + "commands": [cmd], + "depends_on": deps, + "volumes": [{"name": "docker", "path": "/var/run/docker.sock"}], } if trigger and ver_mode in ("release-branch", "main"): - step = dict(step, when=trigger) + step = dict(step, when = trigger) return step - def postgres_integration_tests_step(): cmds = [ - 'apt-get update', - 'apt-get install -yq postgresql-client', - 'dockerize -wait tcp://postgres:5432 -timeout 120s', - 'psql -p 5432 -h postgres -U grafanatest -d grafanatest -f ' - + 'devenv/docker/blocks/postgres_tests/setup.sql', + "apt-get update", + "apt-get install -yq postgresql-client", + "dockerize -wait tcp://postgres:5432 -timeout 120s", + "psql -p 5432 -h postgres -U grafanatest -d grafanatest -f " + + "devenv/docker/blocks/postgres_tests/setup.sql", # Make sure that we don't use cached results for another database - 'go clean -testcache', + "go clean -testcache", "go list './pkg/...' | xargs -I {} sh -c 'go test -run Integration -covermode=atomic -timeout=5m {}'", ] return { - 'name': 'postgres-integration-tests', - 'image': build_image, - 'depends_on': ['wire-install'], - 'environment': { - 'PGPASSWORD': 'grafanatest', - 'GRAFANA_TEST_DB': 'postgres', - 'POSTGRES_HOST': 'postgres', + "name": "postgres-integration-tests", + "image": build_image, + "depends_on": ["wire-install"], + "environment": { + "PGPASSWORD": "grafanatest", + "GRAFANA_TEST_DB": "postgres", + "POSTGRES_HOST": "postgres", }, - 'commands': cmds, + "commands": cmds, } - def mysql_integration_tests_step(): cmds = [ - 'apt-get update', - 'apt-get install -yq default-mysql-client', - 'dockerize -wait tcp://mysql:3306 -timeout 120s', - 'cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql -P 3306 -u root -prootpass', + "apt-get update", + "apt-get install -yq default-mysql-client", + "dockerize -wait tcp://mysql:3306 -timeout 120s", + "cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql -P 3306 -u root -prootpass", # Make sure that we don't use cached results for another database - 'go clean -testcache', + "go clean -testcache", "go list './pkg/...' | xargs -I {} sh -c 'go test -run Integration -covermode=atomic -timeout=5m {}'", ] return { - 'name': 'mysql-integration-tests', - 'image': build_image, - 'depends_on': ['wire-install'], - 'environment': { - 'GRAFANA_TEST_DB': 'mysql', - 'MYSQL_HOST': 'mysql', + "name": "mysql-integration-tests", + "image": build_image, + "depends_on": ["wire-install"], + "environment": { + "GRAFANA_TEST_DB": "mysql", + "MYSQL_HOST": "mysql", }, - 'commands': cmds, + "commands": cmds, } - def redis_integration_tests_step(): return { - 'name': 'redis-integration-tests', - 'image': build_image, - 'depends_on': ['wire-install'], - 'environment': { - 'REDIS_URL': 'redis://redis:6379/0', + "name": "redis-integration-tests", + "image": build_image, + "depends_on": ["wire-install"], + "environment": { + "REDIS_URL": "redis://redis:6379/0", }, - 'commands': [ - 'dockerize -wait tcp://redis:6379/0 -timeout 120s', - 'go clean -testcache', - "go list './pkg/...' | xargs -I {} sh -c 'go test -run Integration -covermode=atomic -timeout=5m {}'", + "commands": [ + "dockerize -wait tcp://redis:6379/0 -timeout 120s", + "./bin/grabpl integration-tests", ], } - def memcached_integration_tests_step(): return { - 'name': 'memcached-integration-tests', - 'image': build_image, - 'depends_on': ['wire-install'], - 'environment': { - 'MEMCACHED_HOSTS': 'memcached:11211', + "name": "memcached-integration-tests", + "image": build_image, + "depends_on": ["wire-install"], + "environment": { + "MEMCACHED_HOSTS": "memcached:11211", }, - 'commands': [ - 'dockerize -wait tcp://memcached:11211 -timeout 120s', - 'go clean -testcache', + "commands": [ + "dockerize -wait tcp://memcached:11211 -timeout 120s", + "go clean -testcache", "go list './pkg/...' | xargs -I {} sh -c 'go test -run Integration -covermode=atomic -timeout=5m {}'", ], } +def release_canary_npm_packages_step(trigger = None): + """Releases canary NPM packages. -def release_canary_npm_packages_step(trigger=None): + Args: + trigger: a Drone trigger for the step. + Defaults to None. + + Returns: + Drone step. + """ step = { - 'name': 'release-canary-npm-packages', - 'image': build_image, - 'depends_on': end_to_end_tests_deps(), - 'environment': { - 'NPM_TOKEN': from_secret('npm_token'), + "name": "release-canary-npm-packages", + "image": build_image, + "depends_on": end_to_end_tests_deps(), + "environment": { + "NPM_TOKEN": from_secret("npm_token"), }, - 'commands': [ - './scripts/circle-release-canary-packages.sh', + "commands": [ + "./scripts/circle-release-canary-packages.sh", ], } if trigger: - step = dict(step, when=trigger) + step = dict(step, when = trigger) return step - def enterprise2_suffix(edition): - if edition == 'enterprise2': - return '-{}'.format(edition) - return '' + if edition == "enterprise2": + return "-{}".format(edition) + return "" +def upload_packages_step(edition, ver_mode, trigger = None): + """Upload packages to object storage. -def upload_packages_step(edition, ver_mode, trigger=None): + Args: + edition: controls which edition of Grafana packages to upload. + ver_mode: when ver_mode == 'main', inhibit upload of enterprise + edition packages when executed. + trigger: a Drone trigger for the step. + Defaults to None. + + Returns: + Drone step. + """ step = { - 'name': 'upload-packages' + enterprise2_suffix(edition), - 'image': publish_image, - 'depends_on': end_to_end_tests_deps(), - 'environment': { - 'GCP_KEY': from_secret('gcp_key'), - 'PRERELEASE_BUCKET': from_secret('prerelease_bucket'), + "name": "upload-packages" + enterprise2_suffix(edition), + "image": publish_image, + "depends_on": end_to_end_tests_deps(), + "environment": { + "GCP_KEY": from_secret("gcp_key"), + "PRERELEASE_BUCKET": from_secret("prerelease_bucket"), }, - 'commands': [ - './bin/build upload-packages --edition {}'.format(edition), + "commands": [ + "./bin/build upload-packages --edition {}".format(edition), ], } if trigger and ver_mode in ("release-branch", "main"): - step = dict(step, when=trigger) + step = dict(step, when = trigger) return step - def publish_grafanacom_step(edition, ver_mode): - if ver_mode == 'release': - cmd = './bin/build publish grafana-com --edition {} ${{DRONE_TAG}}'.format( + """Publishes Grafana packages to grafana.com. + + Args: + edition: controls which edition of Grafana to publish to. + ver_mode: if ver_mode == 'main', pass the DRONE_BUILD_NUMBER environment + variable as the value for the --build-id option. + TODO: is this actually used by the grafanacom subcommand? I think it might + just use the environment varaiable directly. + + Returns: + Drone step. + """ + if ver_mode == "release": + cmd = "./bin/build publish grafana-com --edition {} ${{DRONE_TAG}}".format( edition, ) - elif ver_mode == 'main': - build_no = '${DRONE_BUILD_NUMBER}' - cmd = './bin/build publish grafana-com --edition {} --build-id {}'.format( + elif ver_mode == "main": + build_no = "${DRONE_BUILD_NUMBER}" + cmd = "./bin/build publish grafana-com --edition {} --build-id {}".format( edition, build_no, ) else: - fail('Unexpected version mode {}'.format(ver_mode)) + fail("Unexpected version mode {}".format(ver_mode)) return { - 'name': 'publish-grafanacom-{}'.format(edition), - 'image': publish_image, - 'depends_on': [ - 'publish-linux-packages-deb', - 'publish-linux-packages-rpm', + "name": "publish-grafanacom-{}".format(edition), + "image": publish_image, + "depends_on": [ + "publish-linux-packages-deb", + "publish-linux-packages-rpm", ], - 'environment': { - 'GRAFANA_COM_API_KEY': from_secret('grafana_api_key'), - 'GCP_KEY': from_secret('gcp_key'), + "environment": { + "GRAFANA_COM_API_KEY": from_secret("grafana_api_key"), + "GCP_KEY": from_secret("gcp_key"), }, - 'commands': [ + "commands": [ cmd, ], } - -def publish_linux_packages_step(edition, package_manager='deb'): +def publish_linux_packages_step(edition, package_manager = "deb"): return { - 'name': 'publish-linux-packages-{}'.format(package_manager), + "name": "publish-linux-packages-{}".format(package_manager), # See https://github.com/grafana/deployment_tools/blob/master/docker/package-publish/README.md for docs on that image - 'image': 'us.gcr.io/kubernetes-dev/package-publish:latest', - 'depends_on': ['grabpl'], - 'privileged': True, - 'settings': { - 'access_key_id': from_secret('packages_access_key_id'), - 'secret_access_key': from_secret('packages_secret_access_key'), - 'service_account_json': from_secret('packages_service_account'), - 'target_bucket': 'grafana-packages', - 'deb_distribution': 'auto', - 'gpg_passphrase': from_secret('packages_gpg_passphrase'), - 'gpg_public_key': from_secret('packages_gpg_public_key'), - 'gpg_private_key': from_secret('packages_gpg_private_key'), - 'package_path': 'gs://grafana-prerelease/artifacts/downloads/*${{DRONE_TAG}}/{}/**.{}'.format( - edition, package_manager + "image": "us.gcr.io/kubernetes-dev/package-publish:latest", + "depends_on": ["grabpl"], + "privileged": True, + "settings": { + "access_key_id": from_secret("packages_access_key_id"), + "secret_access_key": from_secret("packages_secret_access_key"), + "service_account_json": from_secret("packages_service_account"), + "target_bucket": "grafana-packages", + "deb_distribution": "auto", + "gpg_passphrase": from_secret("packages_gpg_passphrase"), + "gpg_public_key": from_secret("packages_gpg_public_key"), + "gpg_private_key": from_secret("packages_gpg_private_key"), + "package_path": "gs://grafana-prerelease/artifacts/downloads/*${{DRONE_TAG}}/{}/**.{}".format( + edition, + package_manager, ), }, } - def get_windows_steps(edition, ver_mode): + """Generate the list of Windows steps. + + Args: + edition: used to differentiate steps for different Grafana editions. + ver_mode: used to differentiate steps for different version modes. + + Returns: + List of Drone steps. + """ steps = [ - identify_runner_step('windows'), + identify_runner_step("windows"), ] - if edition in ('enterprise', 'enterprise2'): - if ver_mode == 'release': - committish = '${DRONE_TAG}' - elif ver_mode == 'release-branch': - committish = '$$env:DRONE_BRANCH' + if edition in ("enterprise", "enterprise2"): + if ver_mode == "release": + committish = "${DRONE_TAG}" + elif ver_mode == "release-branch": + committish = "$$env:DRONE_BRANCH" else: - committish = '$$env:DRONE_COMMIT' + committish = "$$env:DRONE_COMMIT" # For enterprise, we have to clone both OSS and enterprise and merge the latter into the former download_grabpl_cmds = [ '$$ProgressPreference = "SilentlyContinue"', - 'Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/{}/windows/grabpl.exe -OutFile grabpl.exe'.format( - grabpl_version + "Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/{}/windows/grabpl.exe -OutFile grabpl.exe".format( + grabpl_version, ), ] clone_cmds = [ 'git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git"', - 'cd grafana-enterprise', - 'git checkout {}'.format(committish), + "cd grafana-enterprise", + "git checkout {}".format(committish), ] init_cmds = [ # Need to move grafana-enterprise out of the way, so directory is empty and can be cloned into - 'cp -r grafana-enterprise C:\\App\\grafana-enterprise', - 'rm -r -force grafana-enterprise', - 'cp grabpl.exe C:\\App\\grabpl.exe', - 'rm -force grabpl.exe', - 'C:\\App\\grabpl.exe init-enterprise --github-token $$env:GITHUB_TOKEN C:\\App\\grafana-enterprise', - 'cp C:\\App\\grabpl.exe grabpl.exe', + "cp -r grafana-enterprise C:\\App\\grafana-enterprise", + "rm -r -force grafana-enterprise", + "cp grabpl.exe C:\\App\\grabpl.exe", + "rm -force grabpl.exe", + "C:\\App\\grabpl.exe init-enterprise --github-token $$env:GITHUB_TOKEN C:\\App\\grafana-enterprise", + "cp C:\\App\\grabpl.exe grabpl.exe", ] steps.extend( [ { - 'name': 'clone', - 'image': wix_image, - 'environment': { - 'GITHUB_TOKEN': from_secret('github_token'), + "name": "clone", + "image": wix_image, + "environment": { + "GITHUB_TOKEN": from_secret("github_token"), }, - 'commands': download_grabpl_cmds + clone_cmds, + "commands": download_grabpl_cmds + clone_cmds, }, { - 'name': 'windows-init', - 'image': wix_image, - 'commands': init_cmds, - 'depends_on': ['clone'], - 'environment': {'GITHUB_TOKEN': from_secret('github_token')}, + "name": "windows-init", + "image": wix_image, + "commands": init_cmds, + "depends_on": ["clone"], + "environment": {"GITHUB_TOKEN": from_secret("github_token")}, }, - ] + ], ) else: init_cmds = [ '$$ProgressPreference = "SilentlyContinue"', - 'Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/{}/windows/grabpl.exe -OutFile grabpl.exe'.format( - grabpl_version + "Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/{}/windows/grabpl.exe -OutFile grabpl.exe".format( + grabpl_version, ), ] steps.extend( [ { - 'name': 'windows-init', - 'image': wix_image, - 'commands': init_cmds, + "name": "windows-init", + "image": wix_image, + "commands": init_cmds, }, - ] + ], ) if ( - ver_mode == 'main' and (edition not in ('enterprise', 'enterprise2')) + ver_mode == "main" and (edition not in ("enterprise", "enterprise2")) ) or ver_mode in ( - 'release', - 'release-branch', + "release", + "release-branch", ): - bucket = '%PRERELEASE_BUCKET%/artifacts/downloads' - if ver_mode == 'release': - ver_part = '${DRONE_TAG}' - dir = 'release' + bucket = "%PRERELEASE_BUCKET%/artifacts/downloads" + if ver_mode == "release": + ver_part = "${DRONE_TAG}" + dir = "release" else: - dir = 'main' - bucket = 'grafana-downloads' - build_no = 'DRONE_BUILD_NUMBER' - ver_part = '--build-id $$env:{}'.format(build_no) + dir = "main" + bucket = "grafana-downloads" + build_no = "DRONE_BUILD_NUMBER" + ver_part = "--build-id $$env:{}".format(build_no) installer_commands = [ - '$$gcpKey = $$env:GCP_KEY', - '[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($$gcpKey)) > gcpkey.json', + "$$gcpKey = $$env:GCP_KEY", + "[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($$gcpKey)) > gcpkey.json", # gcloud fails to read the file unless converted with dos2unix - 'dos2unix gcpkey.json', - 'gcloud auth activate-service-account --key-file=gcpkey.json', - 'rm gcpkey.json', - 'cp C:\\App\\nssm-2.24.zip .', + "dos2unix gcpkey.json", + "gcloud auth activate-service-account --key-file=gcpkey.json", + "rm gcpkey.json", + "cp C:\\App\\nssm-2.24.zip .", ] if ( - ver_mode == 'main' and (edition not in ('enterprise', 'enterprise2')) - ) or ver_mode in ('release',): + ver_mode == "main" and (edition not in ("enterprise", "enterprise2")) + ) or ver_mode in ("release",): installer_commands.extend( [ - '.\\grabpl.exe windows-installer --edition {} {}'.format( - edition, ver_part + ".\\grabpl.exe windows-installer --edition {} {}".format( + edition, + ver_part, ), '$$fname = ((Get-Childitem grafana*.msi -name) -split "`n")[0]', - ] + ], ) - if ver_mode == 'main': + if ver_mode == "main": installer_commands.extend( [ - 'gsutil cp $$fname gs://{}/{}/{}/'.format(bucket, edition, dir), + "gsutil cp $$fname gs://{}/{}/{}/".format(bucket, edition, dir), 'gsutil cp "$$fname.sha256" gs://{}/{}/{}/'.format( - bucket, edition, dir + bucket, + edition, + dir, ), - ] + ], ) else: installer_commands.extend( [ - 'gsutil cp $$fname gs://{}/{}/{}/{}/'.format( - bucket, ver_part, edition, dir + "gsutil cp $$fname gs://{}/{}/{}/{}/".format( + bucket, + ver_part, + edition, + dir, ), 'gsutil cp "$$fname.sha256" gs://{}/{}/{}/{}/'.format( - bucket, ver_part, edition, dir + bucket, + ver_part, + edition, + dir, ), - ] + ], ) steps.append( { - 'name': 'build-windows-installer', - 'image': wix_image, - 'depends_on': [ - 'windows-init', + "name": "build-windows-installer", + "image": wix_image, + "depends_on": [ + "windows-init", ], - 'environment': { - 'GCP_KEY': from_secret('gcp_key'), - 'PRERELEASE_BUCKET': from_secret(prerelease_bucket), - 'GITHUB_TOKEN': from_secret('github_token'), + "environment": { + "GCP_KEY": from_secret("gcp_key"), + "PRERELEASE_BUCKET": from_secret(prerelease_bucket), + "GITHUB_TOKEN": from_secret("github_token"), }, - 'commands': installer_commands, - } + "commands": installer_commands, + }, ) return steps - def verify_gen_cue_step(): return { - 'name': 'verify-gen-cue', - 'image': build_image, - 'depends_on': [], - 'commands': [ - '# It is required that code generated from Thema/CUE be committed and in sync with its inputs.', - '# The following command will fail if running code generators produces any diff in output.', - 'CODEGEN_VERIFY=1 make gen-cue', + "name": "verify-gen-cue", + "image": build_image, + "depends_on": [], + "commands": [ + "# It is required that code generated from Thema/CUE be committed and in sync with its inputs.", + "# The following command will fail if running code generators produces any diff in output.", + "CODEGEN_VERIFY=1 make gen-cue", ], } - def verify_gen_jsonnet_step(): return { - 'name': 'verify-gen-jsonnet', - 'image': build_image, - 'depends_on': [], - 'commands': [ - '# It is required that generated jsonnet is committed and in sync with its inputs.', - '# The following command will fail if running code generators produces any diff in output.', - 'CODEGEN_VERIFY=1 make gen-jsonnet', + "name": "verify-gen-jsonnet", + "image": build_image, + "depends_on": [], + "commands": [ + "# It is required that generated jsonnet is committed and in sync with its inputs.", + "# The following command will fail if running code generators produces any diff in output.", + "CODEGEN_VERIFY=1 make gen-jsonnet", ], } - def trigger_test_release(): return { - 'name': 'trigger-test-release', - 'image': build_image, - 'environment': { - 'GITHUB_TOKEN': from_secret('github_token_pr'), - 'DOWNSTREAM_REPO': from_secret('downstream'), - 'TEST_TAG': 'v0.0.0-test', + "name": "trigger-test-release", + "image": build_image, + "environment": { + "GITHUB_TOKEN": from_secret("github_token_pr"), + "DOWNSTREAM_REPO": from_secret("downstream"), + "TEST_TAG": "v0.0.0-test", }, - 'commands': [ + "commands": [ 'git clone "https://$${GITHUB_TOKEN}@github.com/grafana/grafana-enterprise.git" --depth=1', - 'cd grafana-enterprise', + "cd grafana-enterprise", 'git fetch origin "refs/tags/*:refs/tags/*" --quiet', - 'if git show-ref --tags $${TEST_TAG} --quiet; then git tag -d $${TEST_TAG} && git push --delete origin $${TEST_TAG}; fi', - 'git tag $${TEST_TAG} && git push origin $${TEST_TAG}', - 'cd -', + "if git show-ref --tags $${TEST_TAG} --quiet; then git tag -d $${TEST_TAG} && git push --delete origin $${TEST_TAG}; fi", + "git tag $${TEST_TAG} && git push origin $${TEST_TAG}", + "cd -", 'git fetch https://$${GITHUB_TOKEN}@github.com/grafana/grafana.git "refs/tags/*:refs/tags/*" --quiet && git fetch --quiet', - 'if git show-ref --tags $${TEST_TAG} --quiet; then git tag -d $${TEST_TAG} && git push --delete https://$${GITHUB_TOKEN}@github.com/grafana/grafana.git $${TEST_TAG}; fi', - 'git tag $${TEST_TAG} && git push https://$${GITHUB_TOKEN}@github.com/grafana/grafana.git $${TEST_TAG}', + "if git show-ref --tags $${TEST_TAG} --quiet; then git tag -d $${TEST_TAG} && git push --delete https://$${GITHUB_TOKEN}@github.com/grafana/grafana.git $${TEST_TAG}; fi", + "git tag $${TEST_TAG} && git push https://$${GITHUB_TOKEN}@github.com/grafana/grafana.git $${TEST_TAG}", ], - 'failure': 'ignore', - 'when': { - 'paths': { - 'include': [ - '.drone.yml', - 'pkg/build/**', - ] + "failure": "ignore", + "when": { + "paths": { + "include": [ + ".drone.yml", + "pkg/build/**", + ], }, - 'repo': [ - 'grafana/grafana', + "repo": [ + "grafana/grafana", ], }, } - def artifacts_page_step(): return { - 'name': 'artifacts-page', - 'image': build_image, - 'depends_on': [ - 'grabpl', + "name": "artifacts-page", + "image": build_image, + "depends_on": [ + "grabpl", ], - 'environment': { - 'GCP_KEY': from_secret('gcp_key'), + "environment": { + "GCP_KEY": from_secret("gcp_key"), }, - 'commands': [ - './bin/grabpl artifacts-page', + "commands": [ + "./bin/grabpl artifacts-page", ], } - def end_to_end_tests_deps(): return [ - 'end-to-end-tests-dashboards-suite', - 'end-to-end-tests-panels-suite', - 'end-to-end-tests-smoke-tests-suite', - 'end-to-end-tests-various-suite', + "end-to-end-tests-dashboards-suite", + "end-to-end-tests-panels-suite", + "end-to-end-tests-smoke-tests-suite", + "end-to-end-tests-various-suite", ] - -def compile_build_cmd(edition='oss'): +def compile_build_cmd(edition = "oss"): dependencies = [] - if edition in ('enterprise', 'enterprise2'): + if edition in ("enterprise", "enterprise2"): dependencies = [ - 'init-enterprise', + "init-enterprise", ] return { - 'name': 'compile-build-cmd', - 'image': go_image, - 'commands': [ + "name": "compile-build-cmd", + "image": go_image, + "commands": [ "go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd", ], - 'depends_on': dependencies, - 'environment': { - 'CGO_ENABLED': 0, + "depends_on": dependencies, + "environment": { + "CGO_ENABLED": 0, }, } - def get_trigger_storybook(ver_mode): - trigger_storybook = '' - if ver_mode == 'release': - trigger_storybook = {'event': ['tag']} + """Generate a Drone trigger for UI changes that affect the Grafana UI storybook. + + Args: + ver_mode: affects whether the trigger is event tags or changed files. + + Returns: + Drone trigger. + """ + trigger_storybook = "" + if ver_mode == "release": + trigger_storybook = {"event": ["tag"]} else: trigger_storybook = { - 'paths': { - 'include': [ - 'packages/grafana-ui/**', + "paths": { + "include": [ + "packages/grafana-ui/**", ], - } + }, } return trigger_storybook diff --git a/scripts/drone/utils/utils.star b/scripts/drone/utils/utils.star index 4178dae8a99..e3c79beee89 100644 --- a/scripts/drone/utils/utils.star +++ b/scripts/drone/utils/utils.star @@ -1,112 +1,136 @@ -load( - 'scripts/drone/steps/lib.star', - 'download_grabpl_step', - 'slack_step', -) +""" +This module contains utility functions for generating Drone pipelines. +""" load( - 'scripts/drone/vault.star', - 'from_secret', - 'pull_secret', + "scripts/drone/steps/lib.star", + "slack_step", ) +load("scripts/drone/vault.star", "pull_secret") -failure_template = 'Build {{build.number}} failed for commit: : {{build.link}}\nBranch: \nAuthor: {{build.author}}' -drone_change_template = '`.drone.yml` and `starlark` files have been changed on the OSS repo, by: {{build.author}}. \nBranch: \nCommit hash: ' - +failure_template = "Build {{build.number}} failed for commit: : {{build.link}}\nBranch: \nAuthor: {{build.author}}" +drone_change_template = "`.drone.yml` and `starlark` files have been changed on the OSS repo, by: {{build.author}}. \nBranch: \nCommit hash: " def pipeline( - name, - edition, - trigger, - steps, - services=[], - platform='linux', - depends_on=[], - environment=None, - volumes=[], -): - if platform != 'windows': + name, + edition, + trigger, + steps, + services = [], + platform = "linux", + depends_on = [], + environment = None, + volumes = []): + """Generate a Drone Docker pipeline with commonly used values. + + In addition to the parameters provided, it configures: + - the use of an image pull secret + - a retry count for cloning + - a volume 'docker' that can be used to access the Docker socket + + Args: + name: controls the pipeline name. + edition: used to differentiate the pipeline for enterprise builds. + trigger: a Drone trigger for the pipeline. + steps: the Drone steps for the pipeline. + services: auxilliary services used during the pipeline. + Defaults to []. + platform: abstracts platform specific configuration primarily for different Drone behavior on Windows. + Defaults to 'linux'. + depends_on: list of pipelines that must have succeeded before this pipeline can start. + Defaults to []. + environment: environment variables passed through to pipeline steps. + Defaults to None. + volumes: additional volumes available to be mounted by pipeline steps. + Defaults to []. + + Returns: + Drone pipeline + """ + if platform != "windows": platform_conf = { - 'platform': {'os': 'linux', 'arch': 'amd64'}, + "platform": {"os": "linux", "arch": "amd64"}, # A shared cache is used on the host # To avoid issues with parallel builds, we run this repo on single build agents - 'node': {'type': 'no-parallel'}, + "node": {"type": "no-parallel"}, } else: platform_conf = { - 'platform': { - 'os': 'windows', - 'arch': 'amd64', - 'version': '1809', - } + "platform": { + "os": "windows", + "arch": "amd64", + "version": "1809", + }, } pipeline = { - 'kind': 'pipeline', - 'type': 'docker', - 'name': name, - 'trigger': trigger, - 'services': services, - 'steps': steps, - 'clone': { - 'retries': 3, + "kind": "pipeline", + "type": "docker", + "name": name, + "trigger": trigger, + "services": services, + "steps": steps, + "clone": { + "retries": 3, }, - 'volumes': [ + "volumes": [ { - 'name': 'docker', - 'host': { - 'path': '/var/run/docker.sock', + "name": "docker", + "host": { + "path": "/var/run/docker.sock", }, - } + }, ], - 'depends_on': depends_on, - 'image_pull_secrets': [pull_secret], + "depends_on": depends_on, + "image_pull_secrets": [pull_secret], } if environment: pipeline.update( { - 'environment': environment, - } + "environment": environment, + }, ) - pipeline['volumes'].extend(volumes) + pipeline["volumes"].extend(volumes) pipeline.update(platform_conf) - if edition in ('enterprise', 'enterprise2'): + if edition in ("enterprise", "enterprise2"): # We have a custom clone step for enterprise - pipeline['clone'] = { - 'disable': True, + pipeline["clone"] = { + "disable": True, } return pipeline - def notify_pipeline( - name, slack_channel, trigger, depends_on=[], template=None, secret=None -): + name, + slack_channel, + trigger, + depends_on = [], + template = None, + secret = None): trigger = dict(trigger) return { - 'kind': 'pipeline', - 'type': 'docker', - 'platform': { - 'os': 'linux', - 'arch': 'amd64', + "kind": "pipeline", + "type": "docker", + "platform": { + "os": "linux", + "arch": "amd64", }, - 'name': name, - 'trigger': trigger, - 'steps': [ + "name": name, + "trigger": trigger, + "steps": [ slack_step(slack_channel, template, secret), ], - 'clone': { - 'retries': 3, + "clone": { + "retries": 3, }, - 'depends_on': depends_on, + "depends_on": depends_on, } - # TODO: this overrides any existing dependencies because we're following the existing logic # it should append to any existing dependencies -def with_deps(steps, deps=[]): +def with_deps(steps, deps = []): for step in steps: - step['depends_on'] = deps + step["depends_on"] = deps return steps diff --git a/scripts/drone/vault.star b/scripts/drone/vault.star index c2f228c7a54..09a6bae3009 100644 --- a/scripts/drone/vault.star +++ b/scripts/drone/vault.star @@ -1,97 +1,97 @@ -pull_secret = 'dockerconfigjson' -drone_token = 'drone_token' -prerelease_bucket = 'prerelease_bucket' -gcp_upload_artifacts_key = 'gcp_upload_artifacts_key' -azure_sp_app_id = 'azure_sp_app_id' -azure_sp_app_pw = 'azure_sp_app_pw' -azure_tenant = 'azure_tenant' - +""" +This module returns functions for generating Drone secrets fetched from Vault. +""" +pull_secret = "dockerconfigjson" +drone_token = "drone_token" +prerelease_bucket = "prerelease_bucket" +gcp_upload_artifacts_key = "gcp_upload_artifacts_key" +azure_sp_app_id = "azure_sp_app_id" +azure_sp_app_pw = "azure_sp_app_pw" +azure_tenant = "azure_tenant" def from_secret(secret): - return {'from_secret': secret} - + return {"from_secret": secret} def vault_secret(name, path, key): return { - 'kind': 'secret', - 'name': name, - 'get': { - 'path': path, - 'name': key, + "kind": "secret", + "name": name, + "get": { + "path": path, + "name": key, }, } - def secrets(): return [ - vault_secret(pull_secret, 'secret/data/common/gcr', '.dockerconfigjson'), - vault_secret('github_token', 'infra/data/ci/github/grafanabot', 'pat'), - vault_secret(drone_token, 'infra/data/ci/drone', 'machine-user-token'), - vault_secret(prerelease_bucket, 'infra/data/ci/grafana/prerelease', 'bucket'), + vault_secret(pull_secret, "secret/data/common/gcr", ".dockerconfigjson"), + vault_secret("github_token", "infra/data/ci/github/grafanabot", "pat"), + vault_secret(drone_token, "infra/data/ci/drone", "machine-user-token"), + vault_secret(prerelease_bucket, "infra/data/ci/grafana/prerelease", "bucket"), vault_secret( gcp_upload_artifacts_key, - 'infra/data/ci/grafana/releng/artifacts-uploader-service-account', - 'credentials.json', + "infra/data/ci/grafana/releng/artifacts-uploader-service-account", + "credentials.json", ), vault_secret( azure_sp_app_id, - 'infra/data/ci/datasources/cpp-azure-resourcemanager-credentials', - 'application_id', + "infra/data/ci/datasources/cpp-azure-resourcemanager-credentials", + "application_id", ), vault_secret( azure_sp_app_pw, - 'infra/data/ci/datasources/cpp-azure-resourcemanager-credentials', - 'application_secret', + "infra/data/ci/datasources/cpp-azure-resourcemanager-credentials", + "application_secret", ), vault_secret( azure_tenant, - 'infra/data/ci/datasources/cpp-azure-resourcemanager-credentials', - 'tenant_id', + "infra/data/ci/datasources/cpp-azure-resourcemanager-credentials", + "tenant_id", ), # Package publishing vault_secret( - 'packages_gpg_public_key', - 'infra/data/ci/packages-publish/gpg', - 'public-key-b64', + "packages_gpg_public_key", + "infra/data/ci/packages-publish/gpg", + "public-key-b64", ), vault_secret( - 'packages_gpg_private_key', - 'infra/data/ci/packages-publish/gpg', - 'private-key-b64', + "packages_gpg_private_key", + "infra/data/ci/packages-publish/gpg", + "private-key-b64", ), vault_secret( - 'packages_gpg_passphrase', - 'infra/data/ci/packages-publish/gpg', - 'passphrase', + "packages_gpg_passphrase", + "infra/data/ci/packages-publish/gpg", + "passphrase", ), vault_secret( - 'packages_service_account', - 'infra/data/ci/packages-publish/service-account', - 'credentials.json', + "packages_service_account", + "infra/data/ci/packages-publish/service-account", + "credentials.json", ), vault_secret( - 'packages_access_key_id', - 'infra/data/ci/packages-publish/bucket-credentials', - 'AccessID', + "packages_access_key_id", + "infra/data/ci/packages-publish/bucket-credentials", + "AccessID", ), vault_secret( - 'packages_secret_access_key', - 'infra/data/ci/packages-publish/bucket-credentials', - 'Secret', + "packages_secret_access_key", + "infra/data/ci/packages-publish/bucket-credentials", + "Secret", ), vault_secret( - 'aws_region', - 'secret/data/common/aws-marketplace', - 'aws_region', + "aws_region", + "secret/data/common/aws-marketplace", + "aws_region", ), vault_secret( - 'aws_access_key_id', - 'secret/data/common/aws-marketplace', - 'aws_access_key_id', + "aws_access_key_id", + "secret/data/common/aws-marketplace", + "aws_access_key_id", ), vault_secret( - 'aws_secret_access_key', - 'secret/data/common/aws-marketplace', - 'aws_secret_access_key', + "aws_secret_access_key", + "secret/data/common/aws-marketplace", + "aws_secret_access_key", ), ] diff --git a/scripts/drone/version.star b/scripts/drone/version.star index dfdc2da5323..f56a8274000 100644 --- a/scripts/drone/version.star +++ b/scripts/drone/version.star @@ -1,17 +1,20 @@ +""" +This module returns the pipeline used for version branches. +""" + load( - 'scripts/drone/events/release.star', - 'oss_pipelines', - 'enterprise_pipelines', - 'enterprise2_pipelines', + "scripts/drone/events/release.star", + "enterprise2_pipelines", + "enterprise_pipelines", + "oss_pipelines", ) -ver_mode = 'release-branch' -trigger = {'ref': ['refs/heads/v[0-9]*']} - +ver_mode = "release-branch" +trigger = {"ref": ["refs/heads/v[0-9]*"]} def version_branch_pipelines(): return ( - oss_pipelines(ver_mode=ver_mode, trigger=trigger) - + enterprise_pipelines(ver_mode=ver_mode, trigger=trigger) - + enterprise2_pipelines(ver_mode=ver_mode, trigger=trigger) + oss_pipelines(ver_mode = ver_mode, trigger = trigger) + + enterprise_pipelines(ver_mode = ver_mode, trigger = trigger) + + enterprise2_pipelines(ver_mode = ver_mode, trigger = trigger) ) From dae980860248e4198d917f192731f04b8c2fcdd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 30 Jan 2023 10:30:01 +0100 Subject: [PATCH 086/117] AppPlugins: Remove unused rootNav parameter (#62440) --- packages/grafana-data/src/types/app.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/grafana-data/src/types/app.ts b/packages/grafana-data/src/types/app.ts index aeb336fb15f..f1c3eda327a 100644 --- a/packages/grafana-data/src/types/app.ts +++ b/packages/grafana-data/src/types/app.ts @@ -52,7 +52,6 @@ export interface AppPluginMeta extends PluginMeta export class AppPlugin extends GrafanaPlugin> { // Content under: /a/${plugin-id}/* root?: ComponentType>; - rootNav?: NavModel; // Initial navigation model /** * Called after the module has loaded, and before the app is used. @@ -66,12 +65,9 @@ export class AppPlugin extends GrafanaPlugin>, rootNav?: NavModel) { + setRootPage(root: ComponentType>) { this.root = root; - this.rootNav = rootNav; return this; } From bed1bb1a73140e71ff2f5e0d1198f0a9bc7542f1 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Mon, 30 Jan 2023 10:49:20 +0100 Subject: [PATCH 087/117] Azure Monitor: Allow to specify a region when listing resources (#62306) * Azure Monitor: Allow to specify a region when listing resources * Add region template variable to e2e tests --- e2e/cloud-plugins-suite/azure-monitor.spec.ts | 12 ++- .../__mocks__/datasource.ts | 2 +- .../azure_monitor_datasource.test.ts | 18 +++- .../azure_monitor/azure_monitor_datasource.ts | 89 +++++++++++-------- .../VariableEditor/VariableEditor.test.tsx | 2 + .../VariableEditor/VariableEditor.tsx | 36 ++++++++ .../datasource.ts | 4 +- .../e2e/selectors.ts | 3 + .../types/query.ts | 3 +- .../types/types.ts | 1 + .../variables.ts | 3 +- 11 files changed, 126 insertions(+), 47 deletions(-) diff --git a/e2e/cloud-plugins-suite/azure-monitor.spec.ts b/e2e/cloud-plugins-suite/azure-monitor.spec.ts index b965fa5880b..178b3a4bed0 100644 --- a/e2e/cloud-plugins-suite/azure-monitor.spec.ts +++ b/e2e/cloud-plugins-suite/azure-monitor.spec.ts @@ -56,7 +56,7 @@ const addAzureMonitorVariable = ( name: string, type: AzureQueryType, isFirst: boolean, - options?: { subscription?: string; resourceGroup?: string; namespace?: string; resource?: string } + options?: { subscription?: string; resourceGroup?: string; namespace?: string; resource?: string; region?: string } ) => { e2e.components.PageToolbar.item('Dashboard settings').click(); e2e.components.Tab.title('Variables').click(); @@ -75,6 +75,9 @@ const addAzureMonitorVariable = ( case AzureQueryType.ResourceGroupsQuery: e2eSelectors.variableEditor.subscription.input().find('input').type(`${options?.subscription}{enter}`); break; + case AzureQueryType.LocationsQuery: + e2eSelectors.variableEditor.subscription.input().find('input').type(`${options?.subscription}{enter}`); + break; case AzureQueryType.NamespacesQuery: e2eSelectors.variableEditor.subscription.input().find('input').type(`${options?.subscription}{enter}`); e2eSelectors.variableEditor.resourceGroup.input().find('input').type(`${options?.resourceGroup}{enter}`); @@ -83,6 +86,7 @@ const addAzureMonitorVariable = ( e2eSelectors.variableEditor.subscription.input().find('input').type(`${options?.subscription}{enter}`); e2eSelectors.variableEditor.resourceGroup.input().find('input').type(`${options?.resourceGroup}{enter}`); e2eSelectors.variableEditor.namespace.input().find('input').type(`${options?.namespace}{enter}`); + e2eSelectors.variableEditor.region.input().find('input').type(`${options?.region}{enter}`); break; case AzureQueryType.MetricNamesQuery: e2eSelectors.variableEditor.subscription.input().find('input').type(`${options?.subscription}{enter}`); @@ -223,10 +227,14 @@ e2e.scenario({ subscription: '$subscription', resourceGroup: '$resourceGroups', }); + addAzureMonitorVariable('region', AzureQueryType.LocationsQuery, false, { + subscription: '$subscription', + }); addAzureMonitorVariable('resource', AzureQueryType.ResourceNamesQuery, false, { subscription: '$subscription', resourceGroup: '$resourceGroups', namespace: '$namespace', + region: '$region', }); e2e.pages.Dashboard.SubMenu.submenuItemLabels('subscription').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('grafanalabs-datasources-dev').click(); @@ -254,6 +262,8 @@ e2e.scenario({ e2eSelectors.queryEditor.resourcePicker.advanced.subscription.input().find('input').type('$subscription'); e2eSelectors.queryEditor.resourcePicker.advanced.resourceGroup.input().find('input').type('$resourceGroups'); e2eSelectors.queryEditor.resourcePicker.advanced.namespace.input().find('input').type('$namespaces'); + // TODO: Enable this input once multiple resources feature flag is removed + // e2eSelectors.queryEditor.resourcePicker.advanced.region.input().find('input').type('$region'); e2eSelectors.queryEditor.resourcePicker.advanced.resource.input().find('input').type('$resource'); e2eSelectors.queryEditor.resourcePicker.apply.button().click(); e2eSelectors.queryEditor.metricsQueryEditor.metricName.input().find('input').type('Transactions{enter}'); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/datasource.ts index e12e778136d..da450501276 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/datasource.ts @@ -32,7 +32,7 @@ export default function createMockDatasource(overrides?: DeepPartial }), getLocations: jest .fn() - .mockResolvedValueOnce( + .mockResolvedValue( new Map([['northeurope', { displayName: 'North Europe', name: 'northeurope', supportsLogs: false }]]) ), }, diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts index 8d5cdf15074..080078e6fe7 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts @@ -580,6 +580,7 @@ describe('AzureMonitorDatasource', () => { let subscription = 'mock-subscription-id'; let resourceGroup = 'nodeapp'; let metricNamespace = 'microsoft.insights/components'; + let region = ''; beforeEach(() => { subscription = 'mock-subscription-id'; @@ -605,7 +606,9 @@ describe('AzureMonitorDatasource', () => { ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => { const basePath = `azuremonitor/subscriptions/${subscription}/resourceGroups`; expect(path).toBe( - `${basePath}/${resourceGroup}/resources?api-version=2021-04-01&$filter=resourceType eq '${metricNamespace}'` + `${basePath}/${resourceGroup}/resources?api-version=2021-04-01&$filter=resourceType eq '${metricNamespace}'${ + region ? ` and location eq '${region}'` : '' + }` ); return Promise.resolve(response); }); @@ -632,11 +635,22 @@ describe('AzureMonitorDatasource', () => { }); }); + it('should return include a region', () => { + region = 'eastus'; + return ctx.ds + .getResourceNames(subscription, resourceGroup, metricNamespace, region) + .then((results: Array<{ text: string; value: string }>) => { + expect(results.length).toEqual(1); + expect(results[0].text).toEqual('nodeapp'); + expect(results[0].value).toEqual('nodeapp'); + }); + }); + it('should return multiple resources from a template variable', () => { const tsrv = new TemplateSrv(); tsrv.replace = jest .fn() - .mockImplementation((value: string) => (value === `$${multiVariable.id}` ? 'foo,bar' : value)); + .mockImplementation((value: string) => (value === `$${multiVariable.id}` ? 'foo,bar' : value ?? '')); const ds = new AzureMonitorDatasource(ctx.instanceSettings, templateSrv); ds.azureMonitorDatasource.templateSrv = tsrv; ds.azureMonitorDatasource.getResource = jest diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts index 84d8b3bebf9..1eb89ab56d3 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts @@ -165,47 +165,56 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend { - const validMetricNamespace = startsWith(metricNamespace?.toLowerCase(), 'microsoft.storage/storageaccounts/') - ? 'microsoft.storage/storageaccounts' - : metricNamespace; - let url = `${this.resourcePath}/subscriptions/${subscriptionId}`; - if (resourceGroup) { - url += `/resourceGroups/${resourceGroup}`; - } - url += `/resources?api-version=${this.listByResourceGroupApiVersion}`; - if (validMetricNamespace) { - url += `&$filter=resourceType eq '${validMetricNamespace}'`; - } - if (skipToken) { - url += `&$skiptoken=${skipToken}`; - } - return this.getResource(url).then(async (result: any) => { - let list: Array<{ text: string; value: string }> = []; - if (startsWith(metricNamespace?.toLowerCase(), 'microsoft.storage/storageaccounts/')) { - list = ResponseParser.parseResourceNames(result, 'microsoft.storage/storageaccounts'); - for (let i = 0; i < list.length; i++) { - list[i].text += '/default'; - list[i].value += '/default'; - } - } else { - list = ResponseParser.parseResourceNames(result, metricNamespace); + const promises = this.replaceTemplateVariables(query).map( + ({ metricNamespace, subscriptionId, resourceGroup, region }) => { + const validMetricNamespace = startsWith(metricNamespace?.toLowerCase(), 'microsoft.storage/storageaccounts/') + ? 'microsoft.storage/storageaccounts' + : metricNamespace; + let url = `${this.resourcePath}/subscriptions/${subscriptionId}`; + if (resourceGroup) { + url += `/resourceGroups/${resourceGroup}`; } - - if (result.nextLink) { - // If there is a nextLink, we should request more pages - const nextURL = new URL(result.nextLink); - const nextToken = nextURL.searchParams.get('$skiptoken'); - if (!nextToken) { - throw Error('unable to request the next page of resources'); - } - const nextPage = await this.getResourceNames({ metricNamespace, subscriptionId, resourceGroup }, nextToken); - list = list.concat(nextPage); + url += `/resources?api-version=${this.listByResourceGroupApiVersion}`; + const filters: string[] = []; + if (validMetricNamespace) { + filters.push(`resourceType eq '${validMetricNamespace}'`); } + if (region) { + filters.push(`location eq '${region}'`); + } + if (filters.length > 0) { + url += `&$filter=${filters.join(' and ')}`; + } + if (skipToken) { + url += `&$skiptoken=${skipToken}`; + } + return this.getResource(url).then(async (result: any) => { + let list: Array<{ text: string; value: string }> = []; + if (startsWith(metricNamespace?.toLowerCase(), 'microsoft.storage/storageaccounts/')) { + list = ResponseParser.parseResourceNames(result, 'microsoft.storage/storageaccounts'); + for (let i = 0; i < list.length; i++) { + list[i].text += '/default'; + list[i].value += '/default'; + } + } else { + list = ResponseParser.parseResourceNames(result, metricNamespace); + } - return list; - }); - }); + if (result.nextLink) { + // If there is a nextLink, we should request more pages + const nextURL = new URL(result.nextLink); + const nextToken = nextURL.searchParams.get('$skiptoken'); + if (!nextToken) { + throw Error('unable to request the next page of resources'); + } + const nextPage = await this.getResourceNames({ metricNamespace, subscriptionId, resourceGroup }, nextToken); + list = list.concat(nextPage); + } + + return list; + }); + } + ); return (await Promise.all(promises)).flat(); } @@ -347,7 +356,9 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend( - `${routeNames.azureMonitor}/subscriptions/${subscription}/locations?api-version=${this.locationsApiVersion}` + `${routeNames.azureMonitor}/subscriptions/${this.templateSrv.replace(subscription)}/locations?api-version=${ + this.locationsApiVersion + }` ) ); for (const location of subLocations) { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.test.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.test.tsx index 4aab7246106..a0f8bbcbfe3 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.test.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.test.tsx @@ -258,10 +258,12 @@ describe('VariableEditor:', () => { await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument()); await selectAndRerender('select query type', 'Resource Names', onChange, rerender); await selectAndRerender('select subscription', 'Primary Subscription', onChange, rerender); + await selectAndRerender('select region', 'North Europe', onChange, rerender); expect(onChange).toHaveBeenCalledWith( expect.objectContaining({ queryType: AzureQueryType.ResourceNamesQuery, subscription: 'sub', + region: 'northeurope', refId: 'A', }) ); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.tsx index 44c77300584..9ada46d6676 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.tsx @@ -51,6 +51,7 @@ const VariableEditor = (props: Props) => { const [requireSubscription, setRequireSubscription] = useState(false); const [hasResourceGroup, setHasResourceGroup] = useState(false); const [hasNamespace, setHasNamespace] = useState(false); + const [hasRegion, setHasRegion] = useState(false); const [requireResourceGroup, setRequireResourceGroup] = useState(false); const [requireNamespace, setRequireNamespace] = useState(false); const [requireResource, setRequireResource] = useState(false); @@ -58,6 +59,7 @@ const VariableEditor = (props: Props) => { const [resourceGroups, setResourceGroups] = useState([]); const [namespaces, setNamespaces] = useState([]); const [resources, setResources] = useState([]); + const [regions, setRegions] = useState([]); const [errorMessage, setError] = useLastError(); const queryType = typeof query === 'string' ? '' : query.queryType; @@ -87,6 +89,7 @@ const VariableEditor = (props: Props) => { setRequireSubscription(true); setHasResourceGroup(true); setHasNamespace(true); + setHasRegion(true); break; case AzureQueryType.MetricNamesQuery: setRequireSubscription(true); @@ -137,6 +140,16 @@ const VariableEditor = (props: Props) => { } }, [datasource, subscription, resourceGroup]); + useEffect(() => { + if (subscription) { + datasource.azureMonitorDatasource.getLocations([subscription]).then((rgs) => { + const regions: SelectableValue[] = []; + rgs.forEach((r) => regions.push({ label: r.displayName, value: r.name })); + setRegions(regions); + }); + } + }, [datasource, subscription, resourceGroup]); + const namespace = (typeof query === 'object' && query.namespace) || ''; useEffect(() => { if (subscription) { @@ -193,6 +206,13 @@ const VariableEditor = (props: Props) => { }); }; + const onChangeRegion = (selectableValue: SelectableValue) => { + onChange({ + ...query, + region: selectableValue.value, + }); + }; + const onChangeResource = (selectableValue: SelectableValue) => { onChange({ ...query, @@ -298,6 +318,22 @@ const VariableEditor = (props: Props) => { /> )} + {hasRegion && ( + + ` +- There is a lot of overlap between `RuleActionButtons` and `RuleDetailsActionButtons`. As these components contain a lot of logic it would be nice to extract that logic into hoooks ## Bug fixes diff --git a/public/app/features/alerting/unified/components/AlertLabels.tsx b/public/app/features/alerting/unified/components/AlertLabels.tsx index e6fda61b93a..8c6c37cd82d 100644 --- a/public/app/features/alerting/unified/components/AlertLabels.tsx +++ b/public/app/features/alerting/unified/components/AlertLabels.tsx @@ -2,13 +2,16 @@ import React from 'react'; import { TagList } from '@grafana/ui'; -type Props = { labels: Record; className?: string }; +interface Props { + labels: Record; + className?: string; +} export const AlertLabels = ({ labels, className }: Props) => { const pairs = Object.entries(labels).filter(([key]) => !(key.startsWith('__') && key.endsWith('__'))); return (
- `${label}=${value}`)} /> + `${label}=${value}`)} className={className} />
); }; diff --git a/public/app/features/alerting/unified/components/DetailsField.tsx b/public/app/features/alerting/unified/components/DetailsField.tsx index e7cd4aa2627..4ea87052d92 100644 --- a/public/app/features/alerting/unified/components/DetailsField.tsx +++ b/public/app/features/alerting/unified/components/DetailsField.tsx @@ -8,15 +8,22 @@ interface Props { label: React.ReactNode; className?: string; horizontal?: boolean; + childrenWrapperClassName?: string; } -export const DetailsField = ({ className, label, horizontal, children }: React.PropsWithChildren) => { +export const DetailsField = ({ + className, + label, + horizontal, + children, + childrenWrapperClassName, +}: React.PropsWithChildren) => { const styles = useStyles2(getStyles); return ( -
+
{label}
-
{children}
+
{children}
); }; diff --git a/public/app/features/alerting/unified/components/alert-groups/AlertGroup.tsx b/public/app/features/alerting/unified/components/alert-groups/AlertGroup.tsx index 0ba5409acea..c89bd2f3cf5 100644 --- a/public/app/features/alerting/unified/components/alert-groups/AlertGroup.tsx +++ b/public/app/features/alerting/unified/components/alert-groups/AlertGroup.tsx @@ -30,11 +30,7 @@ export const AlertGroup = ({ alertManagerSourceName, group }: Props) => { onToggle={() => setIsCollapsed(!isCollapsed)} data-testid="alert-group-collapse-toggle" /> - {Object.keys(group.labels).length ? ( - - ) : ( - No grouping - )} + {Object.keys(group.labels).length ? : No grouping}
@@ -49,10 +45,6 @@ const getStyles = (theme: GrafanaTheme2) => ({ margin-top: ${theme.spacing(2)}; } `, - headerLabels: css` - padding-bottom: 0 !important; - margin-bottom: -${theme.spacing(0.5)}; - `, header: css` display: flex; flex-direction: row; diff --git a/public/app/features/alerting/unified/components/alert-groups/AlertGroupAlertsTable.tsx b/public/app/features/alerting/unified/components/alert-groups/AlertGroupAlertsTable.tsx index 38c0caaeab6..6883839f34b 100644 --- a/public/app/features/alerting/unified/components/alert-groups/AlertGroupAlertsTable.tsx +++ b/public/app/features/alerting/unified/components/alert-groups/AlertGroupAlertsTable.tsx @@ -47,7 +47,7 @@ export const AlertGroupAlertsTable = ({ alerts, alertManagerSourceName }: Props) id: 'labels', label: 'Labels', // eslint-disable-next-line react/display-name - renderCell: ({ data: { labels } }) => , + renderCell: ({ data: { labels } }) => , size: 1, }, ], @@ -88,7 +88,4 @@ const getStyles = (theme: GrafanaTheme2) => ({ margin-left: ${theme.spacing(1)}; font-size: ${theme.typography.bodySmall.fontSize}; `, - labels: css` - padding-bottom: 0; - `, }); diff --git a/public/app/features/alerting/unified/components/rules/CloneRuleButton.tsx b/public/app/features/alerting/unified/components/rules/CloneRuleButton.tsx new file mode 100644 index 00000000000..860ba3c6de5 --- /dev/null +++ b/public/app/features/alerting/unified/components/rules/CloneRuleButton.tsx @@ -0,0 +1,73 @@ +import { css } from '@emotion/css'; +import React, { useState } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { locationService } from '@grafana/runtime'; +import { ConfirmModal, LinkButton, useStyles2 } from '@grafana/ui'; +import { RuleIdentifier } from 'app/types/unified-alerting'; + +import * as ruleId from '../../utils/rule-id'; +import { createUrl } from '../../utils/url'; + +interface CloneRuleButtonProps { + ruleIdentifier: RuleIdentifier; + isProvisioned: boolean; + text?: string; + className?: string; +} + +export const CloneRuleButton = React.forwardRef( + ({ text, ruleIdentifier, isProvisioned, className }, ref) => { + // For provisioned rules an additional confirmation step is required + // Users have to be aware that the cloned rule will NOT be marked as provisioned + const [provRuleCloneUrl, setProvRuleCloneUrl] = useState(undefined); + + const styles = useStyles2(getStyles); + const cloneUrl = createUrl('/alerting/new', { copyFrom: ruleId.stringifyIdentifier(ruleIdentifier) }); + + return ( + <> + setProvRuleCloneUrl(cloneUrl) : undefined} + ref={ref} + > + {text} + + + +

+ The new rule will NOT be marked as a provisioned rule. +

+

+ You will need to set a new alert group for the cloned rule because the original one has been provisioned + and cannot be used for rules created in the UI. +

+
+ } + confirmText="Clone" + onConfirm={() => provRuleCloneUrl && locationService.push(provRuleCloneUrl)} + onDismiss={() => setProvRuleCloneUrl(undefined)} + /> + + ); + } +); + +CloneRuleButton.displayName = 'CloneRuleButton'; + +const getStyles = (theme: GrafanaTheme2) => ({ + bold: css` + font-weight: ${theme.typography.fontWeightBold}; + `, +}); diff --git a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx index 71e80704617..03fb2340630 100644 --- a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx @@ -4,7 +4,7 @@ import { useLocation } from 'react-router-dom'; import { GrafanaTheme2 } from '@grafana/data'; import { Stack } from '@grafana/experimental'; -import { config, locationService } from '@grafana/runtime'; +import { config } from '@grafana/runtime'; import { Button, ClipboardButton, ConfirmModal, LinkButton, Tooltip, useStyles2 } from '@grafana/ui'; import { useAppNotification } from 'app/core/copy/appNotification'; import { useDispatch } from 'app/types'; @@ -18,6 +18,7 @@ import * as ruleId from '../../utils/rule-id'; import { isFederatedRuleGroup, isGrafanaRulerRule } from '../../utils/rules'; import { createUrl } from '../../utils/url'; +import { CloneRuleButton } from './CloneRuleButton'; export const matchesWidth = (width: number) => window.matchMedia(`(max-width: ${width}px)`).matches; interface Props { @@ -32,7 +33,6 @@ export const RuleActionsButtons: FC = ({ rule, rulesSource }) => { const style = useStyles2(getStyles); const { namespace, group, rulerRule } = rule; const [ruleToDelete, setRuleToDelete] = useState(); - const [provRuleCloneUrl, setProvRuleCloneUrl] = useState(undefined); const rulesSourceName = getRulesSourceName(rulesSource); @@ -128,21 +128,9 @@ export const RuleActionsButtons: FC = ({ rule, rulesSource }) => { ); } - const cloneUrl = createUrl('/alerting/new', { copyFrom: ruleId.stringifyIdentifier(identifier) }); - // For provisioned rules an additional confirmation step is required - // Users have to be aware that the cloned rule will NOT be marked as provisioned buttons.push( - - setProvRuleCloneUrl(cloneUrl) : undefined} - /> + + ); } @@ -183,24 +171,6 @@ export const RuleActionsButtons: FC = ({ rule, rulesSource }) => { onDismiss={() => setRuleToDelete(undefined)} /> )} - -

- The new rule will NOT be marked as a provisioned rule. -

-

- You will need to set a new alert group for the cloned rule because the original one has been provisioned - and cannot be used for rules created in the UI. -

-
- } - confirmText="Clone" - onConfirm={() => provRuleCloneUrl && locationService.push(provRuleCloneUrl)} - onDismiss={() => setProvRuleCloneUrl(undefined)} - /> ); } @@ -216,7 +186,4 @@ export const getStyles = (theme: GrafanaTheme2) => ({ button: css` padding: 0 ${theme.spacing(2)}; `, - bold: css` - font-weight: ${theme.typography.fontWeightBold}; - `, }); diff --git a/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx b/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx index 1c1fdee3f13..fd8932f323c 100644 --- a/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx @@ -14,6 +14,7 @@ import { PromAlertingRuleState } from 'app/types/unified-alerting-dto'; import { useIsRuleEditable } from '../../hooks/useIsRuleEditable'; import { useStateHistoryModal } from '../../hooks/useStateHistoryModal'; import { deleteRuleAction } from '../../state/actions'; +import { getRulesPermissions } from '../../utils/access-control'; import { getAlertmanagerByUid } from '../../utils/alertmanager'; import { Annotation } from '../../utils/constants'; import { getRulesSourceName, isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource'; @@ -22,6 +23,8 @@ import * as ruleId from '../../utils/rule-id'; import { isAlertingRule, isFederatedRuleGroup, isGrafanaRulerRule } from '../../utils/rules'; import { DeclareIncident } from '../bridges/DeclareIncidentButton'; +import { CloneRuleButton } from './CloneRuleButton'; + interface Props { rule: CombinedRule; rulesSource: RulesSource; @@ -78,6 +81,8 @@ export const RuleDetailsActionButtons: FC = ({ rule, rulesSource, isViewM const isFiringRule = isAlertingRule(rule.promRule) && rule.promRule.state === PromAlertingRuleState.Firing; + const rulesPermissions = getRulesPermissions(rulesSourceName); + const hasCreateRulePermission = contextSrv.hasPermission(rulesPermissions.create); const { isEditable, isRemovable } = useIsRuleEditable(rulesSourceName, rulerRule); const returnTo = location.pathname + location.search; @@ -177,17 +182,11 @@ export const RuleDetailsActionButtons: FC = ({ rule, rulesSource, isViewM ); } - if (isViewMode) { - if (isEditable && rulerRule && !isFederated && !isProvisioned) { - const sourceName = getRulesSourceName(rulesSource); - const identifier = ruleId.fromRulerRule(sourceName, namespace.name, group.name, rulerRule); + if (isViewMode && rulerRule) { + const sourceName = getRulesSourceName(rulesSource); + const identifier = ruleId.fromRulerRule(sourceName, namespace.name, group.name, rulerRule); - const editURL = urlUtil.renderUrl( - `${config.appSubUrl}/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/edit`, - { - returnTo, - } - ); + if (isEditable && !isFederated) { rightButtons.push( = ({ rule, rulesSource, isViewM ); + if (!isProvisioned) { + const editURL = urlUtil.renderUrl( + `${config.appSubUrl}/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/edit`, + { + returnTo, + } + ); + + rightButtons.push( + + Edit + + ); + } + } + + if (hasCreateRulePermission && !isFederated) { rightButtons.push( - - Edit - + ); } - if (isRemovable && rulerRule && !isFederated && !isProvisioned) { + if (isRemovable && !isFederated && !isProvisioned) { rightButtons.push(
diff --git a/public/app/features/alerting/unified/components/amroutes/MuteTimingForm.tsx b/public/app/features/alerting/unified/components/amroutes/MuteTimingForm.tsx index e7779c10bbb..1a6b0de3fe7 100644 --- a/public/app/features/alerting/unified/components/amroutes/MuteTimingForm.tsx +++ b/public/app/features/alerting/unified/components/amroutes/MuteTimingForm.tsx @@ -114,7 +114,7 @@ const MuteTimingForm = ({ muteTiming, showError, provenance }: Props) => { pageNav={{ ...defaultPageNav, id: muteTiming ? 'alert-policy-edit' : 'alert-policy-new', - text: muteTiming ? 'Edit mute timing' : 'New mute timing', + text: muteTiming ? 'Edit mute timing' : 'Add mute timing', }} > = ({ alertManagerSourceName, muteTiming variant="primary" href={makeAMLink('alerting/routes/mute-timing/new', alertManagerSourceName)} > - New mute timing + Add mute timing )} diff --git a/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx b/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx index b3654602d8f..ace509fa67d 100644 --- a/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx +++ b/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx @@ -298,9 +298,9 @@ export const ReceiversTable: FC = ({ config, alertManagerName }) => { return ( = ({ config, alertManagerName }) => { return ( diff --git a/public/app/features/alerting/unified/components/receivers/form/ReceiverForm.tsx b/public/app/features/alerting/unified/components/receivers/form/ReceiverForm.tsx index 0223021ea6a..b83c4b71696 100644 --- a/public/app/features/alerting/unified/components/receivers/form/ReceiverForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/ReceiverForm.tsx @@ -159,7 +159,7 @@ export function ReceiverForm({ variant="secondary" onClick={() => append({ ...defaultItem, __id: String(Math.random()) } as R)} > - New contact point integration + Add contact point integration )}
diff --git a/public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx b/public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx index f38727ae1ea..8f3d1d52916 100644 --- a/public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx +++ b/public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx @@ -16,7 +16,7 @@ export const NoRulesSplash = () => { title="You haven`t created any alert rules yet" buttonIcon="bell" buttonLink={'alerting/new'} - buttonTitle="New alert rule" + buttonTitle="Create alert rule" proTip="you can also create alert rules from existing panels and queries." proTipLink="https://grafana.com/docs/" proTipLinkTitle="Learn more" diff --git a/public/app/features/alerting/unified/components/silences/NoSilencesCTA.tsx b/public/app/features/alerting/unified/components/silences/NoSilencesCTA.tsx index 4c142c092a9..b5fa0a877a0 100644 --- a/public/app/features/alerting/unified/components/silences/NoSilencesCTA.tsx +++ b/public/app/features/alerting/unified/components/silences/NoSilencesCTA.tsx @@ -20,7 +20,7 @@ export const NoSilencesSplash: FC = ({ alertManagerSourceName }) => { title="You haven't created any silences yet" buttonIcon="bell-slash" buttonLink={makeAMLink('alerting/silence/new', alertManagerSourceName)} - buttonTitle="New silence" + buttonTitle="Create silence" /> ); } diff --git a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx index 10b080c7cb8..3136835bbe4 100644 --- a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx +++ b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx @@ -71,7 +71,7 @@ const SilencesTable: FC = ({ silences, alertManagerAlerts, alertManagerSo
diff --git a/public/app/features/connections/__mocks__/store.navIndex.mock.ts b/public/app/features/connections/__mocks__/store.navIndex.mock.ts index f9d48a17daa..4ff680ab811 100644 --- a/public/app/features/connections/__mocks__/store.navIndex.mock.ts +++ b/public/app/features/connections/__mocks__/store.navIndex.mock.ts @@ -132,7 +132,7 @@ export const navIndex: NavIndex = { { id: 'receivers', text: 'Contact points', - subTitle: 'Decide how your contacts are notified when an alert fires', + subTitle: 'Choose how to notify your contact points when an alert instance fires', icon: 'comment-alt-share', url: '/alerting/notifications', }, @@ -171,7 +171,7 @@ export const navIndex: NavIndex = { }, { id: 'alert', - text: 'New alert rule', + text: 'Create alert rule', subTitle: 'Create an alert rule', icon: 'plus', url: '/alerting/new', @@ -198,7 +198,7 @@ export const navIndex: NavIndex = { receivers: { id: 'receivers', text: 'Contact points', - subTitle: 'Decide how your contacts are notified when an alert fires', + subTitle: 'Choose how to notify your contact points when an alert instance fires', icon: 'comment-alt-share', url: '/alerting/notifications', }, @@ -231,7 +231,7 @@ export const navIndex: NavIndex = { }, alert: { id: 'alert', - text: 'New alert rule', + text: 'Create alert rule', subTitle: 'Create an alert rule', icon: 'plus', url: '/alerting/new', diff --git a/public/app/features/datasources/__mocks__/store.navIndex.mock.ts b/public/app/features/datasources/__mocks__/store.navIndex.mock.ts index 73374cab1a3..9ceb0484c09 100644 --- a/public/app/features/datasources/__mocks__/store.navIndex.mock.ts +++ b/public/app/features/datasources/__mocks__/store.navIndex.mock.ts @@ -418,7 +418,7 @@ export const navIndex: NavIndex = { }, { id: 'alert', - text: 'New alert rule', + text: 'Create alert rule', subTitle: 'Create an alert rule', icon: 'plus', url: '/alerting/new', @@ -722,7 +722,7 @@ export const navIndex: NavIndex = { }, { id: 'alert', - text: 'New alert rule', + text: 'Create alert rule', subTitle: 'Create an alert rule', icon: 'plus', url: '/alerting/new', @@ -789,7 +789,7 @@ export const navIndex: NavIndex = { }, { id: 'alert', - text: 'New alert rule', + text: 'Create alert rule', subTitle: 'Create an alert rule', icon: 'plus', url: '/alerting/new', @@ -857,7 +857,7 @@ export const navIndex: NavIndex = { }, { id: 'alert', - text: 'New alert rule', + text: 'Create alert rule', subTitle: 'Create an alert rule', icon: 'plus', url: '/alerting/new', @@ -925,7 +925,7 @@ export const navIndex: NavIndex = { }, { id: 'alert', - text: 'New alert rule', + text: 'Create alert rule', subTitle: 'Create an alert rule', icon: 'plus', url: '/alerting/new', @@ -993,7 +993,7 @@ export const navIndex: NavIndex = { }, { id: 'alert', - text: 'New alert rule', + text: 'Create alert rule', subTitle: 'Create an alert rule', icon: 'plus', url: '/alerting/new', @@ -1061,7 +1061,7 @@ export const navIndex: NavIndex = { }, { id: 'alert', - text: 'New alert rule', + text: 'Create alert rule', subTitle: 'Create an alert rule', icon: 'plus', url: '/alerting/new', @@ -1129,7 +1129,7 @@ export const navIndex: NavIndex = { }, { id: 'alert', - text: 'New alert rule', + text: 'Create alert rule', subTitle: 'Create an alert rule', icon: 'plus', url: '/alerting/new', @@ -1141,7 +1141,7 @@ export const navIndex: NavIndex = { }, alert: { id: 'alert', - text: 'New alert rule', + text: 'Create alert rule', subTitle: 'Create an alert rule', icon: 'plus', url: '/alerting/new', @@ -1200,7 +1200,7 @@ export const navIndex: NavIndex = { }, { id: 'alert', - text: 'New alert rule', + text: 'Create alert rule', subTitle: 'Create an alert rule', icon: 'plus', url: '/alerting/new', diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 255415225ee..77a5c56a413 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -160,7 +160,7 @@ "title": "Alert rules" }, "alerting-receivers": { - "subtitle": "Decide how your contacts are notified when an alert fires", + "subtitle": "Choose how to notify your contact points when an alert instance fires", "title": "Contact points" }, "alerting-silences": { @@ -191,7 +191,7 @@ "title": "Create" }, "create-alert": { - "title": "New alert rule" + "title": "Create alert rule" }, "create-dashboard": { "title": "Dashboard" diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index ffbd5154681..4b20c33f43e 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -577,4 +577,4 @@ "option-tooltip": "Cľęäř şęľęčŧįőʼnş" } } -} \ No newline at end of file +} From 7a465f42a699e9a01271dbb790e09bd697e048f8 Mon Sep 17 00:00:00 2001 From: Alex Moreno Date: Mon, 30 Jan 2023 16:29:05 +0100 Subject: [PATCH 112/117] Alerting: Allow pausing alerts from provisioning (#62263) * Allow pausing alerts from provisioning * Update swagger * Add IsPaused to provision export endpoints * Add pause field in sample.yml * Add exception for reset state in first loop iteration of scheduler if rule is paused * Update provision definition and swagger docs * Fix provisioning export tests * Suggestion: Simplify if condition * Add more context to a comment --- conf/provisioning/alerting/sample.yaml | 13 +++-- .../ngalert/api/api_provisioning_test.go | 12 ++-- pkg/services/ngalert/api/tooling/api.json | 15 +++-- .../definitions/provisioning_alert_rules.go | 4 ++ pkg/services/ngalert/api/tooling/post.json | 57 +++++++++++-------- pkg/services/ngalert/api/tooling/spec.json | 14 ++++- pkg/services/ngalert/schedule/schedule.go | 6 +- .../provisioning/alerting/file/rules_types.go | 4 ++ public/api-merged.json | 8 ++- 9 files changed, 89 insertions(+), 44 deletions(-) diff --git a/conf/provisioning/alerting/sample.yaml b/conf/provisioning/alerting/sample.yaml index 2a86bf5ea71..bce60aacd7f 100644 --- a/conf/provisioning/alerting/sample.yaml +++ b/conf/provisioning/alerting/sample.yaml @@ -11,7 +11,7 @@ apiVersion: 1 # folder: my_first_folder # # interval of the rule group evaluation # interval: 60s -# # list of rules that are part of the rule group +# # list of rules that are part of the rule group # rules: # # unique identifier for the rule # - uid: my_id_1 @@ -53,7 +53,7 @@ apiVersion: 1 # # state of the alert rule when no data is returned # # possible values: "NoData", "Alerting", "OK", default = NoData # noDataState: Alerting -# # state of the alert rule when the query execution +# # state of the alert rule when the query execution # # fails - possible values: "Error", "Alerting", "OK" # # default = Alerting # executionErrorState: Alerting @@ -62,10 +62,11 @@ apiVersion: 1 # # > map of strings to attach arbitrary custom data # annotations: # some_key: some_value -# # map of strings to filter and +# # map of strings to filter and # # route alerts # labels: # team: sre_team_1 +# isPaused: false # # List of alert rule UIDs that should be deleted # deleteRules: @@ -103,7 +104,7 @@ apiVersion: 1 # # > The labels by which incoming alerts are grouped together. For example, # # multiple alerts coming in for cluster=A and alertname=LatencyHigh would # # be batched into a single group. -# # +# # # # To aggregate by all possible labels, use the special value '...' as # # the sole label name, for example: # # group_by: ['...'] @@ -127,7 +128,7 @@ apiVersion: 1 # mute_time_intervals: # - abc # # How long to initially wait to send a notification for a group -# # of alerts. Allows to collect more initial alerts for the same group. +# # of alerts. Allows to collect more initial alerts for the same group. # # (Usually ~0s to few minutes), default = 30s # group_wait: 30s # # How long to wait before sending a notification about new alerts that @@ -138,7 +139,7 @@ apiVersion: 1 # # been sent successfully for an alert. (Usually ~3h or more), default = 4h # repeat_interval: 4h # # Zero or more child routes -# routes: +# routes: # ... # # List of orgIds that should be reset to the default policy diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index 183bf0c8d91..2d121d364be 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -480,7 +480,7 @@ func TestProvisioningApi(t *testing.T) { insertRule(t, sut, createTestAlertRule("rule1", 1)) insertRule(t, sut, createTestAlertRule("rule2", 1)) - expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"my-cool-group","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"-100"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s"},{"uid":"rule2","title":"rule2","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"-100"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s"}]}]}` + expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"my-cool-group","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"-100"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false},{"uid":"rule2","title":"rule2","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"-100"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false}]}]}` response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group") @@ -495,7 +495,7 @@ func TestProvisioningApi(t *testing.T) { insertRule(t, sut, createTestAlertRule("rule2", 1)) rc.Context.Req.Header.Add("Accept", "application/yaml") - expectedResponse := "apiVersion: 1\ngroups:\n - orgId: 1\n name: my-cool-group\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule1\n title: rule1\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: \"-100\"\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n - uid: rule2\n title: rule2\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: \"-100\"\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n" + expectedResponse := "apiVersion: 1\ngroups:\n - orgId: 1\n name: my-cool-group\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule1\n title: rule1\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: \"-100\"\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n - uid: rule2\n title: rule2\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: \"-100\"\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n" response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group") @@ -607,7 +607,7 @@ func TestProvisioningApi(t *testing.T) { rc := createTestRequestCtx() insertRule(t, sut, createTestAlertRule("rule1", 1)) - expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"my-cool-group","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"-100"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s"}]}]}` + expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"my-cool-group","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"-100"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false}]}]}` response := sut.RouteGetAlertRuleExport(&rc, "rule1") @@ -621,7 +621,7 @@ func TestProvisioningApi(t *testing.T) { insertRule(t, sut, createTestAlertRule("rule1", 1)) rc.Context.Req.Header.Add("Accept", "application/yaml") - expectedResponse := "apiVersion: 1\ngroups:\n - orgId: 1\n name: my-cool-group\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule1\n title: rule1\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: \"-100\"\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n" + expectedResponse := "apiVersion: 1\ngroups:\n - orgId: 1\n name: my-cool-group\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule1\n title: rule1\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: \"-100\"\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n" response := sut.RouteGetAlertRuleExport(&rc, "rule1") @@ -725,7 +725,7 @@ func TestProvisioningApi(t *testing.T) { insertRule(t, sut, createTestAlertRuleWithFolderAndGroup("rule2", 1, "folder-uid", "groupb")) insertRule(t, sut, createTestAlertRuleWithFolderAndGroup("rule3", 1, "folder-uid2", "groupb")) - expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"groupa","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"-100"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s"}]},{"orgId":1,"name":"groupb","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule2","title":"rule2","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"-100"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s"}]},{"orgId":1,"name":"groupb","folder":"Folder Title2","interval":"1m","rules":[{"uid":"rule3","title":"rule3","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"-100"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s"}]}]}` + expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"groupa","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"-100"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false}]},{"orgId":1,"name":"groupb","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule2","title":"rule2","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"-100"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false}]},{"orgId":1,"name":"groupb","folder":"Folder Title2","interval":"1m","rules":[{"uid":"rule3","title":"rule3","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"-100"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false}]}]}` response := sut.RouteGetAlertRulesExport(&rc) @@ -741,7 +741,7 @@ func TestProvisioningApi(t *testing.T) { insertRule(t, sut, createTestAlertRuleWithFolderAndGroup("rule3", 1, "folder-uid2", "groupb")) rc.Context.Req.Header.Add("Accept", "application/yaml") - expectedResponse := "apiVersion: 1\ngroups:\n - orgId: 1\n name: groupa\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule1\n title: rule1\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: \"-100\"\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n - orgId: 1\n name: groupb\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule2\n title: rule2\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: \"-100\"\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n - orgId: 1\n name: groupb\n folder: Folder Title2\n interval: 1m\n rules:\n - uid: rule3\n title: rule3\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: \"-100\"\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n" + expectedResponse := "apiVersion: 1\ngroups:\n - orgId: 1\n name: groupa\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule1\n title: rule1\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: \"-100\"\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n - orgId: 1\n name: groupb\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule2\n title: rule2\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: \"-100\"\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n - orgId: 1\n name: groupb\n folder: Folder Title2\n interval: 1m\n rules:\n - uid: rule3\n title: rule3\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: \"-100\"\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n" response := sut.RouteGetAlertRulesExport(&rc) diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index 5c39db61c9e..09c49c9cbe3 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -194,6 +194,9 @@ "for": { "$ref": "#/definitions/Duration" }, + "isPaused": { + "type": "boolean" + }, "labels": { "additionalProperties": { "type": "string" @@ -2276,6 +2279,10 @@ "format": "int64", "type": "integer" }, + "isPaused": { + "example": false, + "type": "boolean" + }, "labels": { "additionalProperties": { "type": "string" @@ -3277,6 +3284,7 @@ "type": "object" }, "URL": { + "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.", "properties": { "ForceQuery": { "type": "boolean" @@ -3312,7 +3320,7 @@ "$ref": "#/definitions/Userinfo" } }, - "title": "URL is a custom URL type that allows validation at configuration load time.", + "title": "A URL represents a parsed URL (technically, a URI reference).", "type": "object" }, "Userinfo": { @@ -3616,7 +3624,6 @@ "type": "object" }, "gettableAlert": { - "description": "GettableAlert gettable alert", "properties": { "annotations": { "$ref": "#/definitions/labelSet" @@ -3672,13 +3679,13 @@ "type": "object" }, "gettableAlerts": { - "description": "GettableAlerts gettable alerts", "items": { "$ref": "#/definitions/gettableAlert" }, "type": "array" }, "gettableSilence": { + "description": "GettableSilence gettable silence", "properties": { "comment": { "description": "comment", @@ -3733,6 +3740,7 @@ "type": "array" }, "integration": { + "description": "Integration integration", "properties": { "lastNotifyAttempt": { "description": "A timestamp indicating the last attempt to deliver a notification regardless of the outcome.\nFormat: date-time", @@ -3876,7 +3884,6 @@ "type": "array" }, "postableSilence": { - "description": "PostableSilence postable silence", "properties": { "comment": { "description": "comment", diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go index 74929e8b362..d273de06201 100644 --- a/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go @@ -134,6 +134,8 @@ type ProvisionedAlertRule struct { Labels map[string]string `json:"labels,omitempty"` // readonly: true Provenance models.Provenance `json:"provenance,omitempty"` + // example: false + IsPaused bool `json:"isPaused"` } func (a *ProvisionedAlertRule) UpstreamModel() (models.AlertRule, error) { @@ -152,6 +154,7 @@ func (a *ProvisionedAlertRule) UpstreamModel() (models.AlertRule, error) { For: time.Duration(a.For), Annotations: a.Annotations, Labels: a.Labels, + IsPaused: a.IsPaused, }, nil } @@ -172,6 +175,7 @@ func NewAlertRule(rule models.AlertRule, provenance models.Provenance) Provision Annotations: rule.Annotations, Labels: rule.Labels, Provenance: provenance, + IsPaused: rule.IsPaused, } } diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index b30fb5961d6..12c7b075536 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -194,6 +194,9 @@ "for": { "$ref": "#/definitions/Duration" }, + "isPaused": { + "type": "boolean" + }, "labels": { "additionalProperties": { "type": "string" @@ -219,19 +222,7 @@ "type": "string" } }, - "title": "AlertRuleExport is the provisioned export of models.AlertRule.", - "type": "object" - }, - "AlertRuleFileExport": { - "properties": { - "groups": { - "items": { - "$ref": "#/definitions/AlertRuleGroupExport" - }, - "type": "array" - } - }, - "title": "AlertRuleFileExport is the provisioned export of multiple models.AlertRuleGroup.", + "title": "AlertRuleExport is the provisioned file export of models.AlertRule.", "type": "object" }, "AlertRuleGroup": { @@ -277,7 +268,7 @@ "type": "array" } }, - "title": "AlertRuleGroupExport is the provisioned export of models.AlertRuleGroup.", + "title": "AlertRuleGroupExport is the provisioned file export of AlertRuleGroupV1.", "type": "object" }, "AlertRuleGroupMetadata": { @@ -289,6 +280,22 @@ }, "type": "object" }, + "AlertingFileExport": { + "properties": { + "apiVersion": { + "format": "int64", + "type": "integer" + }, + "groups": { + "items": { + "$ref": "#/definitions/AlertRuleGroupExport" + }, + "type": "array" + } + }, + "title": "AlertingFileExport is the full provisioned file export.", + "type": "object" + }, "AlertingRule": { "description": "adapted from cortex", "properties": { @@ -2272,6 +2279,10 @@ "format": "int64", "type": "integer" }, + "isPaused": { + "example": false, + "type": "boolean" + }, "labels": { "additionalProperties": { "type": "string" @@ -3510,6 +3521,7 @@ "type": "object" }, "alertGroups": { + "description": "AlertGroups alert groups", "items": { "$ref": "#/definitions/alertGroup" }, @@ -3675,7 +3687,6 @@ "type": "array" }, "gettableSilence": { - "description": "GettableSilence gettable silence", "properties": { "comment": { "description": "comment", @@ -3724,13 +3735,13 @@ "type": "object" }, "gettableSilences": { + "description": "GettableSilences gettable silences", "items": { "$ref": "#/definitions/gettableSilence" }, "type": "array" }, "integration": { - "description": "Integration integration", "properties": { "lastNotifyAttempt": { "description": "A timestamp indicating the last attempt to deliver a notification regardless of the outcome.\nFormat: date-time", @@ -3874,6 +3885,7 @@ "type": "array" }, "postableSilence": { + "description": "PostableSilence postable silence", "properties": { "comment": { "description": "comment", @@ -3911,7 +3923,6 @@ "type": "object" }, "receiver": { - "description": "Receiver receiver", "properties": { "active": { "description": "active", @@ -5772,9 +5783,9 @@ ], "responses": { "200": { - "description": "AlertRuleFileExport", + "description": "AlertingFileExport", "schema": { - "$ref": "#/definitions/AlertRuleFileExport" + "$ref": "#/definitions/AlertingFileExport" } }, "404": { @@ -5908,9 +5919,9 @@ ], "responses": { "200": { - "description": "AlertRuleExport", + "description": "AlertingFileExport", "schema": { - "$ref": "#/definitions/AlertRuleExport" + "$ref": "#/definitions/AlertingFileExport" } }, "404": { @@ -6157,9 +6168,9 @@ ], "responses": { "200": { - "description": "AlertRuleGroupExport", + "description": "AlertingFileExport", "schema": { - "$ref": "#/definitions/AlertRuleGroupExport" + "$ref": "#/definitions/AlertingFileExport" } }, "404": { diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index fcae8f39245..14b8d748465 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -2806,6 +2806,9 @@ "for": { "$ref": "#/definitions/Duration" }, + "isPaused": { + "type": "boolean" + }, "labels": { "type": "object", "additionalProperties": { @@ -4903,6 +4906,10 @@ "type": "integer", "format": "int64" }, + "isPaused": { + "type": "boolean", + "example": false + }, "labels": { "type": "object", "additionalProperties": { @@ -5892,8 +5899,9 @@ } }, "URL": { + "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.", "type": "object", - "title": "URL is a custom URL type that allows validation at configuration load time.", + "title": "A URL represents a parsed URL (technically, a URI reference).", "properties": { "ForceQuery": { "type": "boolean" @@ -6129,6 +6137,7 @@ "$ref": "#/definitions/alertGroup" }, "alertGroups": { + "description": "AlertGroups alert groups", "type": "array", "items": { "$ref": "#/definitions/alertGroup" @@ -6297,7 +6306,6 @@ "$ref": "#/definitions/gettableAlerts" }, "gettableSilence": { - "description": "GettableSilence gettable silence", "type": "object", "required": [ "comment", @@ -6347,6 +6355,7 @@ "$ref": "#/definitions/gettableSilence" }, "gettableSilences": { + "description": "GettableSilences gettable silences", "type": "array", "items": { "$ref": "#/definitions/gettableSilence" @@ -6498,6 +6507,7 @@ } }, "postableSilence": { + "description": "PostableSilence postable silence", "type": "object", "required": [ "comment", diff --git a/pkg/services/ngalert/schedule/schedule.go b/pkg/services/ngalert/schedule/schedule.go index 8cce260d003..62a6a391842 100644 --- a/pkg/services/ngalert/schedule/schedule.go +++ b/pkg/services/ngalert/schedule/schedule.go @@ -474,7 +474,11 @@ func (sch *schedule) ruleRoutine(grafanaCtx context.Context, key ngmodels.AlertR isPaused := ctx.rule.IsPaused // fetch latest alert rule version if currentRuleVersion != newVersion { - if currentRuleVersion > 0 { // do not clean up state if the eval loop has just started. + // Do not clean up state if the eval loop has just started. + // We need to reset state if the loop has started and the alert is already paused. It can happen, + // if we have an alert with state and we do file provision with stateful Grafana, that state + // lingers in DB and won't be cleaned up until next alert rule update. + if currentRuleVersion > 0 || isPaused { logger.Debug("Got a new version of alert rule. Clear up the state and refresh extra labels", "version", currentRuleVersion, "newVersion", newVersion) resetState(grafanaCtx, isPaused) } diff --git a/pkg/services/provisioning/alerting/file/rules_types.go b/pkg/services/provisioning/alerting/file/rules_types.go index 8c31876a805..59782b08c71 100644 --- a/pkg/services/provisioning/alerting/file/rules_types.go +++ b/pkg/services/provisioning/alerting/file/rules_types.go @@ -78,6 +78,7 @@ type AlertRuleV1 struct { For values.StringValue `json:"for" yaml:"for"` Annotations values.StringMapValue `json:"annotations" yaml:"annotations"` Labels values.StringMapValue `json:"labels" yaml:"labels"` + IsPaused values.BoolValue `json:"isPaused" yaml:"isPaused"` } func (rule *AlertRuleV1) mapToModel(orgID int64) (models.AlertRule, error) { @@ -134,6 +135,7 @@ func (rule *AlertRuleV1) mapToModel(orgID int64) (models.AlertRule, error) { if len(alertRule.Data) == 0 { return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: no data set", alertRule.Title) } + alertRule.IsPaused = rule.IsPaused.Value() return alertRule, nil } @@ -205,6 +207,7 @@ type AlertRuleExport struct { For model.Duration `json:"for" yaml:"for"` Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"` Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` + IsPaused bool `json:"isPaused" yaml:"isPaused"` } // AlertQueryExport is the provisioned export of models.AlertQuery. @@ -281,6 +284,7 @@ func newAlertRuleExport(rule models.AlertRule) (AlertRuleExport, error) { ExecErrState: rule.ExecErrState, Annotations: rule.Annotations, Labels: rule.Labels, + IsPaused: rule.IsPaused, }, nil } diff --git a/public/api-merged.json b/public/api-merged.json index 2505ee36660..b8b4d81b4d3 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -16003,6 +16003,10 @@ "type": "integer", "format": "int64" }, + "isPaused": { + "type": "boolean", + "example": true + }, "labels": { "type": "object", "additionalProperties": { @@ -18929,6 +18933,7 @@ } }, "alertGroups": { + "description": "AlertGroups alert groups", "type": "array", "items": { "$ref": "#/definitions/alertGroup" @@ -19033,7 +19038,6 @@ } }, "gettableAlert": { - "description": "GettableAlert gettable alert", "type": "object", "required": [ "labels", @@ -19144,6 +19148,7 @@ } }, "gettableSilences": { + "description": "GettableSilences gettable silences", "type": "array", "items": { "$ref": "#/definitions/gettableSilence" @@ -19293,7 +19298,6 @@ } }, "postableSilence": { - "description": "PostableSilence postable silence", "type": "object", "required": [ "comment", From b6f477ae03b90c788df96aeaea11c678a5a099ce Mon Sep 17 00:00:00 2001 From: Kristina Date: Mon, 30 Jan 2023 09:40:36 -0600 Subject: [PATCH 113/117] Consider y coord when determining bottom collision (#62403) --- packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx b/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx index bde5b951c51..04bf348225e 100644 --- a/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx +++ b/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx @@ -33,7 +33,7 @@ export const ContextMenu: React.FC = React.memo( const OFFSET = 5; const collisions = { right: window.innerWidth < x + rect.width, - bottom: window.innerHeight < rect.bottom + rect.height + OFFSET, + bottom: window.innerHeight < y + rect.height + OFFSET, }; setPositionStyles({ From f1a2a768971e64f98697c38da79bf1810ec2ac84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ida=20=C5=A0tambuk?= Date: Mon, 30 Jan 2023 16:45:03 +0100 Subject: [PATCH 114/117] Datasources: Use getDefaultQuery in annotations editors (#61870) + Add Cloudwatch default annotation --- .../grafana-data/src/types/annotations.ts | 5 ++ .../StandardAnnotationQueryEditor.test.tsx | 70 +++++++++++++++++++ .../StandardAnnotationQueryEditor.tsx | 8 ++- .../annotations/executeAnnotationQuery.ts | 6 +- public/app/features/query/state/runRequest.ts | 10 +-- .../cloudwatch/annotationSupport.ts | 8 ++- .../datasource/cloudwatch/datasource.test.ts | 2 +- .../datasource/cloudwatch/defaultQueries.ts | 8 +++ 8 files changed, 106 insertions(+), 11 deletions(-) create mode 100644 public/app/features/annotations/components/StandardAnnotationQueryEditor.test.tsx diff --git a/packages/grafana-data/src/types/annotations.ts b/packages/grafana-data/src/types/annotations.ts index 099ad4fe034..da077daa76e 100644 --- a/packages/grafana-data/src/types/annotations.ts +++ b/packages/grafana-data/src/types/annotations.ts @@ -113,4 +113,9 @@ export interface AnnotationSupport>; + + /** + * Define this method if you want to pre-populate the editor with a default query + */ + getDefaultQuery?(): Partial; } diff --git a/public/app/features/annotations/components/StandardAnnotationQueryEditor.test.tsx b/public/app/features/annotations/components/StandardAnnotationQueryEditor.test.tsx new file mode 100644 index 00000000000..a840ae1641c --- /dev/null +++ b/public/app/features/annotations/components/StandardAnnotationQueryEditor.test.tsx @@ -0,0 +1,70 @@ +import { render } from '@testing-library/react'; +import React from 'react'; + +import { AnnotationQuery, DataSourceApi, DataSourceInstanceSettings } from '@grafana/data/src'; + +import StandardAnnotationQueryEditor, { Props as EditorProps } from './StandardAnnotationQueryEditor'; + +const setup = (customProps: Partial) => { + const props: EditorProps = { + datasource: {} as unknown as DataSourceApi, + datasourceInstanceSettings: {} as DataSourceInstanceSettings, + annotation: {} as AnnotationQuery, + onChange: jest.fn(), + ...customProps, + }; + const { rerender } = render(); + return { rerender, props }; +}; + +jest.mock('app/features/dashboard/services/DashboardSrv', () => ({ + getDashboardSrv: jest.fn().mockReturnValue({ + getCurrent: jest.fn().mockReturnValue(null), + }), +})); + +jest.mock('app/features/dashboard/services/TimeSrv', () => ({ + getTimeSrv: jest.fn().mockReturnValue({ + timeRange: jest.fn().mockReturnValue({}), + }), +})); + +describe('StandardAnnotationQueryEditor', () => { + it('should fill out a default query if it is defined and pass it to the Query Editor', () => { + const { props } = setup({ + annotation: { name: 'initialAnn', target: { refId: 'initialAnnotationRef' } } as AnnotationQuery, + + datasource: { + annotations: { + QueryEditor: jest.fn(() =>
Editor
), + getDefaultQuery: jest.fn().mockImplementation(() => ({ queryType: 'defaultAnnotationsQuery' })), + prepareAnnotation: (annotation: AnnotationQuery) => annotation, + }, + } as unknown as DataSourceApi, + }); + expect(props.datasource?.annotations?.getDefaultQuery).toBeDefined(); + expect(props.datasource?.annotations?.QueryEditor).toHaveBeenCalledWith( + expect.objectContaining({ + query: expect.objectContaining({ queryType: 'defaultAnnotationsQuery', refId: 'initialAnnotationRef' }), + }), + expect.anything() + ); + }); + it('should keep and pass the initial query if the defaultQuery is not defined', () => { + const { props } = setup({ + annotation: { name: 'initialAnn', target: { refId: 'initialAnnotationRef' } } as AnnotationQuery, + datasource: { + annotations: { + QueryEditor: jest.fn(() =>
Editor
), + prepareAnnotation: (annotation: AnnotationQuery) => annotation, + }, + } as unknown as DataSourceApi, + }); + expect(props.datasource?.annotations?.QueryEditor).toHaveBeenCalledWith( + expect.objectContaining({ + query: expect.objectContaining({ refId: 'initialAnnotationRef' }), + }), + expect.anything() + ); + }); +}); diff --git a/public/app/features/annotations/components/StandardAnnotationQueryEditor.tsx b/public/app/features/annotations/components/StandardAnnotationQueryEditor.tsx index 61d335c0642..e6df175dc2e 100644 --- a/public/app/features/annotations/components/StandardAnnotationQueryEditor.tsx +++ b/public/app/features/annotations/components/StandardAnnotationQueryEditor.tsx @@ -22,7 +22,7 @@ import { AnnotationQueryResponse } from '../types'; import { AnnotationFieldMapper } from './AnnotationResultMapper'; -interface Props { +export interface Props { datasource: DataSourceApi; datasourceInstanceSettings: DataSourceInstanceSettings; annotation: AnnotationQuery; @@ -186,7 +186,11 @@ export default class StandardAnnotationQueryEditor extends PureComponentAnnotations are not supported. This datasource needs to export a QueryEditor
; } - const query = annotation.target ?? { refId: 'Anno' }; + const query = { + ...datasource.annotations?.getDefaultQuery?.(), + ...(annotation.target ?? { refId: 'Anno' }), + }; + return ( <> diff --git a/public/app/features/annotations/executeAnnotationQuery.ts b/public/app/features/annotations/executeAnnotationQuery.ts index df9a7e0365c..e3870a75437 100644 --- a/public/app/features/annotations/executeAnnotationQuery.ts +++ b/public/app/features/annotations/executeAnnotationQuery.ts @@ -23,7 +23,11 @@ export function executeAnnotationQuery( ...datasource.annotations, }; - const annotation = processor.prepareAnnotation!(savedJsonAnno); + const annotationWithDefaults = { + ...processor.getDefaultQuery?.(), + ...savedJsonAnno, + }; + const annotation = processor.prepareAnnotation!(annotationWithDefaults); if (!annotation) { return of({}); } diff --git a/public/app/features/query/state/runRequest.ts b/public/app/features/query/state/runRequest.ts index 440bf4f4e6c..fe0a7d120cf 100644 --- a/public/app/features/query/state/runRequest.ts +++ b/public/app/features/query/state/runRequest.ts @@ -24,7 +24,6 @@ import { import { toDataQueryError } from '@grafana/runtime'; import { isExpressionReference } from '@grafana/runtime/src/utils/DataSourceWithBackend'; import { backendSrv } from 'app/core/services/backend_srv'; -import { queryIsEmpty } from 'app/core/utils/query'; import { dataSource as expressionDatasource } from 'app/features/expressions/ExpressionDatasource'; import { ExpressionQuery } from 'app/features/expressions/types'; @@ -176,10 +175,11 @@ export function callQueryMethod( request: DataQueryRequest, queryFunction?: typeof datasource.query ) { - // If the datasource has defined a default query, make sure it's applied if the query is empty - request.targets = request.targets.map((t) => - queryIsEmpty(t) ? { ...datasource?.getDefaultQuery?.(CoreApp.PanelEditor), ...t } : t - ); + // If the datasource has defined a default query, make sure it's applied + request.targets = request.targets.map((t) => ({ + ...datasource?.getDefaultQuery?.(CoreApp.PanelEditor), + ...t, + })); // If its a public datasource, just return the result. Expressions will be handled on the backend. if (datasource.type === 'public-ds') { diff --git a/public/app/plugins/datasource/cloudwatch/annotationSupport.ts b/public/app/plugins/datasource/cloudwatch/annotationSupport.ts index 88a6c3fe9c7..08d4659a707 100644 --- a/public/app/plugins/datasource/cloudwatch/annotationSupport.ts +++ b/public/app/plugins/datasource/cloudwatch/annotationSupport.ts @@ -1,6 +1,7 @@ import { AnnotationQuery } from '@grafana/data'; import { AnnotationQueryEditor } from './components/AnnotationQueryEditor'; +import { DEFAULT_ANNOTATIONS_QUERY } from './defaultQueries'; import { isCloudWatchAnnotation } from './guards'; import { CloudWatchAnnotationQuery, CloudWatchQuery, LegacyAnnotationQuery } from './types'; @@ -24,8 +25,8 @@ export const CloudWatchAnnotationSupport = { target: { ...query.target, ...query, - statistic: query.statistic || 'Average', - region: query.region || 'default', + statistic: query.statistic || DEFAULT_ANNOTATIONS_QUERY.statistic, + region: query.region || DEFAULT_ANNOTATIONS_QUERY.region, queryMode: 'Annotations', refId: query.refId || 'annotationQuery', }, @@ -56,5 +57,8 @@ export const CloudWatchAnnotationSupport = { return undefined; }, + getDefaultQuery() { + return DEFAULT_ANNOTATIONS_QUERY; + }, QueryEditor: AnnotationQueryEditor, }; diff --git a/public/app/plugins/datasource/cloudwatch/datasource.test.ts b/public/app/plugins/datasource/cloudwatch/datasource.test.ts index 92bf5f4f1e8..258023ac047 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.test.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.test.ts @@ -14,10 +14,10 @@ import { setupForLogs } from './__mocks__/logsTestContext'; import { validLogsQuery, validMetricSearchBuilderQuery } from './__mocks__/queries'; import { TimeRangeMock } from './__mocks__/timeRange'; import { + CloudWatchDefaultQuery, CloudWatchLogsQuery, CloudWatchMetricsQuery, CloudWatchQuery, - CloudWatchDefaultQuery, MetricEditorMode, MetricQueryType, } from './types'; diff --git a/public/app/plugins/datasource/cloudwatch/defaultQueries.ts b/public/app/plugins/datasource/cloudwatch/defaultQueries.ts index a3e804861ff..62b72054e99 100644 --- a/public/app/plugins/datasource/cloudwatch/defaultQueries.ts +++ b/public/app/plugins/datasource/cloudwatch/defaultQueries.ts @@ -1,4 +1,5 @@ import { + CloudWatchAnnotationQuery, CloudWatchLogsQuery, CloudWatchMetricsQuery, LogGroup, @@ -24,6 +25,13 @@ export const DEFAULT_METRICS_QUERY: Omit = { matchExact: true, }; +export const DEFAULT_ANNOTATIONS_QUERY: Omit = { + queryMode: 'Annotations', + namespace: '', + region: 'default', + statistic: 'Average', +}; + export const DEFAULT_LOGS_QUERY_STRING = 'fields @timestamp, @message |\n sort @timestamp desc |\n limit 20'; export const getDefaultLogsQuery = ( From 14dd1be24479ad6baf3425b672c89971913fd445 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Mon, 30 Jan 2023 16:01:44 +0000 Subject: [PATCH 115/117] Chore: update `copy-webpack-plugin` and remove from renovate ignore list (#62459) update copy-webpack-plugin and remove from renovate ignore list --- .github/renovate.json5 | 1 - package.json | 2 +- yarn.lock | 45 ++++++++++++++++++++++++++++++------------ 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index d198edf8f75..c9831f8b844 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -14,7 +14,6 @@ "react-redux", // react-beautiful-dnd depends on react-redux 7.x, we need to update that one first "react-router-dom", // we should bump this together with history "systemjs", - "copy-webpack-plugin", // try to upgrade with newer yarn release. Not working with 3.1.1 "ts-loader", // we should remove ts-loader and use babel-loader instead "ora", // we should bump this once we move to esm modules diff --git a/package.json b/package.json index ed2fc0ae7c2..b48be2ee36c 100644 --- a/package.json +++ b/package.json @@ -180,7 +180,7 @@ "browserslist": "^4.21.4", "chance": "^1.0.10", "codeowners": "^5.1.1", - "copy-webpack-plugin": "9.0.1", + "copy-webpack-plugin": "11.0.0", "css-loader": "6.7.1", "css-minimizer-webpack-plugin": "4.2.2", "cypress": "9.5.1", diff --git a/yarn.lock b/yarn.lock index b0815fbdedf..6ffbc66ec44 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16564,20 +16564,19 @@ __metadata: languageName: node linkType: hard -"copy-webpack-plugin@npm:9.0.1": - version: 9.0.1 - resolution: "copy-webpack-plugin@npm:9.0.1" +"copy-webpack-plugin@npm:11.0.0": + version: 11.0.0 + resolution: "copy-webpack-plugin@npm:11.0.0" dependencies: - fast-glob: ^3.2.5 - glob-parent: ^6.0.0 - globby: ^11.0.3 + fast-glob: ^3.2.11 + glob-parent: ^6.0.1 + globby: ^13.1.1 normalize-path: ^3.0.0 - p-limit: ^3.1.0 - schema-utils: ^3.0.0 + schema-utils: ^4.0.0 serialize-javascript: ^6.0.0 peerDependencies: webpack: ^5.1.0 - checksum: f3e69883e173f9a298b63dcf35ba5aafc02e252c67c236029424af19fb2e36fc93de94054bd7a9ada8b4cbcc4e96e9a3a269f972e4a45f4fd1b32a3d199d2cae + checksum: df4f8743f003a29ee7dd3d9b1789998a3a99051c92afb2ba2203d3dacfa696f4e757b275560fafb8f206e520a0aa78af34b990324a0e36c2326cefdeef3ca82e languageName: node linkType: hard @@ -20536,7 +20535,7 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:3.2.7, fast-glob@npm:^3.1.1, fast-glob@npm:^3.2.5": +"fast-glob@npm:3.2.7, fast-glob@npm:^3.1.1": version: 3.2.7 resolution: "fast-glob@npm:3.2.7" dependencies: @@ -20563,7 +20562,7 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:^3.2.12": +"fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.12": version: 3.2.12 resolution: "fast-glob@npm:3.2.12" dependencies: @@ -21706,7 +21705,7 @@ __metadata: languageName: node linkType: hard -"glob-parent@npm:^6.0.0, glob-parent@npm:^6.0.1, glob-parent@npm:^6.0.2": +"glob-parent@npm:^6.0.1, glob-parent@npm:^6.0.2": version: 6.0.2 resolution: "glob-parent@npm:6.0.2" dependencies: @@ -21922,6 +21921,19 @@ __metadata: languageName: node linkType: hard +"globby@npm:^13.1.1": + version: 13.1.3 + resolution: "globby@npm:13.1.3" + dependencies: + dir-glob: ^3.0.1 + fast-glob: ^3.2.11 + ignore: ^5.2.0 + merge2: ^1.4.1 + slash: ^4.0.0 + checksum: 93f06e02002cdf368f7e3d55bd59e7b00784c7cc8fe92c7ee5082cc7171ff6109fda45e1c97a80bb48bc811dedaf7843c7c9186f5f84bde4883ab630e13c43df + languageName: node + linkType: hard + "globby@npm:^9.2.0": version: 9.2.0 resolution: "globby@npm:9.2.0" @@ -22138,7 +22150,7 @@ __metadata: combokeys: ^3.0.0 comlink: 4.3.1 common-tags: 1.8.2 - copy-webpack-plugin: 9.0.1 + copy-webpack-plugin: 11.0.0 core-js: 3.27.1 css-loader: 6.7.1 css-minimizer-webpack-plugin: 4.2.2 @@ -35238,6 +35250,13 @@ __metadata: languageName: node linkType: hard +"slash@npm:^4.0.0": + version: 4.0.0 + resolution: "slash@npm:4.0.0" + checksum: da8e4af73712253acd21b7853b7e0dbba776b786e82b010a5bfc8b5051a1db38ed8aba8e1e8f400dd2c9f373be91eb1c42b66e91abb407ff42b10feece5e1d2d + languageName: node + linkType: hard + "slate-base64-serializer@npm:^0.2.112": version: 0.2.115 resolution: "slate-base64-serializer@npm:0.2.115" From 6d230d95ebadd3516e3cac0787c132baa5742fc6 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Mon, 30 Jan 2023 17:19:03 +0100 Subject: [PATCH 116/117] Azure Monitor: Enable multiple resource queries (#62467) --- .../feature-toggles/index.md | 28 ++-- e2e/cloud-plugins-suite/azure-monitor.spec.ts | 5 +- .../src/types/featureToggles.gen.ts | 2 - pkg/services/featuremgmt/registry.go | 12 -- pkg/services/featuremgmt/toggles_gen.go | 8 - .../LogsQueryEditor/LogsQueryEditor.test.tsx | 45 +++++- .../LogsQueryEditor/LogsQueryEditor.tsx | 11 +- .../MetricsQueryEditor.test.tsx | 96 +++++++++++- .../MetricsQueryEditor/MetricsQueryEditor.tsx | 7 +- .../ResourcePicker/Advanced.test.tsx | 44 ------ .../components/ResourcePicker/Advanced.tsx | 146 ------------------ .../ResourcePicker/ResourcePicker.test.tsx | 79 ---------- .../ResourcePicker/ResourcePicker.tsx | 18 +-- .../VariableEditor/VariableEditor.tsx | 6 +- .../grafanaTemplateVariableFns.ts | 2 +- .../grafanaTemplateVariables.test.ts | 2 +- 16 files changed, 159 insertions(+), 352 deletions(-) delete mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/Advanced.test.tsx delete mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/Advanced.tsx diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index a404c8b6f33..449086347e2 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -98,24 +98,22 @@ Alpha features might be changed or removed without prior notice. | `sessionRemoteCache` | Enable using remote cache for user sessions | | `alertingBacktesting` | Rule backtesting API for alerting | | `editPanelCSVDragAndDrop` | Enables drag and drop for CSV and Excel files | -| `azureMultipleResourcePicker` | Azure multiple resource picker | | `logsContextDatasourceUi` | Allow datasource to provide custom UI for context view | ## Development feature toggles The following toggles require explicitly setting Grafana's [app mode]({{< relref "../_index.md/#app_mode" >}}) to 'development' before you can enable this feature toggle. These features tend to be experimental. -| Feature toggle name | Description | -| -------------------------------------- | ----------------------------------------------------------------------- | -| `dashboardPreviewsAdmin` | Manage the dashboard previews crawler process from the UI | -| `showFeatureFlagsInUI` | Show feature flags in the settings UI | -| `publicDashboardsEmailSharing` | Allows public dashboard sharing to be restricted to only allowed emails | -| `k8s` | Explore native k8s integrations | -| `k8sDashboards` | Save dashboards via k8s | -| `dashboardsFromStorage` | Load dashboards from the generic storage interface | -| `export` | Export grafana instance (to git, etc) | -| `azureMonitorResourcePickerForMetrics` | New UI for Azure Monitor Metrics Query | -| `grpcServer` | Run GRPC server | -| `entityStore` | SQL-based entity store (requires storage flag also) | -| `queryLibrary` | Reusable query library | -| `nestedFolders` | Enable folder nesting | +| Feature toggle name | Description | +| ------------------------------ | ----------------------------------------------------------------------- | +| `dashboardPreviewsAdmin` | Manage the dashboard previews crawler process from the UI | +| `showFeatureFlagsInUI` | Show feature flags in the settings UI | +| `publicDashboardsEmailSharing` | Allows public dashboard sharing to be restricted to only allowed emails | +| `k8s` | Explore native k8s integrations | +| `k8sDashboards` | Save dashboards via k8s | +| `dashboardsFromStorage` | Load dashboards from the generic storage interface | +| `export` | Export grafana instance (to git, etc) | +| `grpcServer` | Run GRPC server | +| `entityStore` | SQL-based entity store (requires storage flag also) | +| `queryLibrary` | Reusable query library | +| `nestedFolders` | Enable folder nesting | diff --git a/e2e/cloud-plugins-suite/azure-monitor.spec.ts b/e2e/cloud-plugins-suite/azure-monitor.spec.ts index 178b3a4bed0..84fb21421ae 100644 --- a/e2e/cloud-plugins-suite/azure-monitor.spec.ts +++ b/e2e/cloud-plugins-suite/azure-monitor.spec.ts @@ -248,6 +248,8 @@ e2e.scenario({ .parent() .find('input') .type('microsoft.storage/storageaccounts{downArrow}{enter}'); + e2e.pages.Dashboard.SubMenu.submenuItemLabels('region').parent().find('button').click(); + e2e.pages.Dashboard.SubMenu.submenuItemLabels('region').parent().find('input').type('uk south{downArrow}{enter}'); e2e.pages.Dashboard.SubMenu.submenuItemLabels('resource').parent().find('button').click(); e2e.pages.Dashboard.SubMenu.submenuItemLabels('resource') .parent() @@ -262,8 +264,7 @@ e2e.scenario({ e2eSelectors.queryEditor.resourcePicker.advanced.subscription.input().find('input').type('$subscription'); e2eSelectors.queryEditor.resourcePicker.advanced.resourceGroup.input().find('input').type('$resourceGroups'); e2eSelectors.queryEditor.resourcePicker.advanced.namespace.input().find('input').type('$namespaces'); - // TODO: Enable this input once multiple resources feature flag is removed - // e2eSelectors.queryEditor.resourcePicker.advanced.region.input().find('input').type('$region'); + e2eSelectors.queryEditor.resourcePicker.advanced.region.input().find('input').type('$region'); e2eSelectors.queryEditor.resourcePicker.advanced.resource.input().find('input').type('$resource'); e2eSelectors.queryEditor.resourcePicker.apply.button().click(); e2eSelectors.queryEditor.metricsQueryEditor.metricName.input().find('input').type('Transactions{enter}'); diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 467ad96208f..126aebe083f 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -47,7 +47,6 @@ export interface FeatureToggles { supportBundles?: boolean; dashboardsFromStorage?: boolean; export?: boolean; - azureMonitorResourcePickerForMetrics?: boolean; exploreMixedDatasource?: boolean; tracing?: boolean; commandPalette?: boolean; @@ -90,7 +89,6 @@ export interface FeatureToggles { alertingBacktesting?: boolean; editPanelCSVDragAndDrop?: boolean; alertingNoNormalState?: boolean; - azureMultipleResourcePicker?: boolean; topNavCommandPalette?: boolean; logsSampleInExplore?: boolean; logsContextDatasourceUi?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index e27478fa809..4910a8c9d3c 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -177,13 +177,6 @@ var ( State: FeatureStateAlpha, RequiresDevMode: true, }, - { - Name: "azureMonitorResourcePickerForMetrics", - Description: "New UI for Azure Monitor Metrics Query", - State: FeatureStateAlpha, - RequiresDevMode: true, - FrontendOnly: true, - }, { Name: "exploreMixedDatasource", Description: "Enable mixed datasource in Explore", @@ -416,11 +409,6 @@ var ( State: FeatureStateBeta, RequiresRestart: false, }, - { - Name: "azureMultipleResourcePicker", - Description: "Azure multiple resource picker", - State: FeatureStateAlpha, - }, { Name: "topNavCommandPalette", Description: "Launch the Command Palette from the top navigation search box", diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 737d8483c0e..7301eb570ba 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -131,10 +131,6 @@ const ( // Export grafana instance (to git, etc) FlagExport = "export" - // FlagAzureMonitorResourcePickerForMetrics - // New UI for Azure Monitor Metrics Query - FlagAzureMonitorResourcePickerForMetrics = "azureMonitorResourcePickerForMetrics" - // FlagExploreMixedDatasource // Enable mixed datasource in Explore FlagExploreMixedDatasource = "exploreMixedDatasource" @@ -303,10 +299,6 @@ const ( // Stop maintaining state of alerts that are not firing FlagAlertingNoNormalState = "alertingNoNormalState" - // FlagAzureMultipleResourcePicker - // Azure multiple resource picker - FlagAzureMultipleResourcePicker = "azureMultipleResourcePicker" - // FlagTopNavCommandPalette // Launch the Command Palette from the top navigation search box FlagTopNavCommandPalette = "topNavCommandPalette" diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.test.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.test.tsx index 32c4b44c92e..5bf10d93801 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.test.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.test.tsx @@ -2,8 +2,6 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import config from 'app/core/config'; - import createMockDatasource from '../../__mocks__/datasource'; import createMockQuery from '../../__mocks__/query'; import { createMockResourcePickerData } from '../MetricsQueryEditor/MetricsQueryEditor.test'; @@ -24,10 +22,6 @@ const variableOptionGroup = { options: [], }; -beforeEach(() => { - config.featureToggles.azureMultipleResourcePicker = true; -}); - describe('LogsQueryEdiutor', () => { const originalScrollIntoView = window.HTMLElement.prototype.scrollIntoView; @@ -151,4 +145,43 @@ describe('LogsQueryEdiutor', () => { expect(await screen.findByText('You may only choose items of the same resource type.')).toBeInTheDocument(); }); + + it('should call onApply with a new subscription uri when a user types it in the selection box', async () => { + const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() }); + const query = createMockQuery(); + delete query?.subscription; + delete query?.azureLogAnalytics?.resources; + const onChange = jest.fn(); + + render( + {}} + /> + ); + + const resourcePickerButton = await screen.findByRole('button', { name: 'Select a resource' }); + resourcePickerButton.click(); + + const advancedSection = screen.getByText('Advanced'); + advancedSection.click(); + + const advancedInput = await screen.findByTestId('input-advanced-resource-picker-1'); + // const advancedInput = await screen.findByLabelText('Resource URI(s)'); + await userEvent.type(advancedInput, '/subscriptions/def-123'); + + const applyButton = screen.getByRole('button', { name: 'Apply' }); + applyButton.click(); + + expect(onChange).toBeCalledWith( + expect.objectContaining({ + azureLogAnalytics: expect.objectContaining({ + resources: ['/subscriptions/def-123'], + }), + }) + ); + }); }); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.tsx index f29bb2c41e8..ed9bedc34f6 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { EditorFieldGroup, EditorRow, EditorRows } from '@grafana/experimental'; -import { config } from '@grafana/runtime'; import { Alert } from '@grafana/ui'; import Datasource from '../../datasource'; @@ -40,10 +39,6 @@ const LogsQueryEditor: React.FC = ({ // Only if there is some resource(s) selected we should disable rows return false; } - // Disable multiple selection until the feature is ready - if (!config.featureToggles.azureMultipleResourcePicker) { - return true; - } const rowResourceNS = parseResourceDetails(row.uri, row.location).metricNamespace?.toLowerCase(); const selectedRowSampleNs = parseResourceDetails( selectedRows[0].uri, @@ -82,11 +77,7 @@ const LogsQueryEditor: React.FC = ({ // eslint-disable-next-line )} - selectionNotice={() => - config.featureToggles.azureMultipleResourcePicker - ? 'You may only choose items of the same resource type.' - : '' - } + selectionNotice={() => 'You may only choose items of the same resource type.'} /> diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx index 01cb3db99cf..debb4c68082 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx @@ -3,8 +3,6 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; import { selectOptionInTest } from 'test/helpers/selectOptionInTest'; -import config from 'app/core/config'; - import createMockDatasource from '../../__mocks__/datasource'; import { createMockInstanceSetttings } from '../../__mocks__/instanceSettings'; import createMockPanelData from '../../__mocks__/panelData'; @@ -33,10 +31,6 @@ const variableOptionGroup = { options: [], }; -beforeEach(() => { - config.featureToggles.azureMultipleResourcePicker = true; -}); - export function createMockResourcePickerData() { const mockDatasource = createMockDatasource(); const mockResourcePicker = new ResourcePickerData( @@ -378,4 +372,94 @@ describe('MetricsQueryEditor', () => { }, }); }); + + it('should show unselect a resource if the value is manually edited', async () => { + const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() }); + const query = createMockQuery(); + delete query?.subscription; + delete query?.azureMonitor?.resources; + delete query?.azureMonitor?.metricNamespace; + const onChange = jest.fn(); + + render( + {}} + /> + ); + + const resourcePickerButton = await screen.findByRole('button', { name: 'Select a resource' }); + resourcePickerButton.click(); + + const subscriptionButton = await screen.findByRole('button', { name: 'Expand Primary Subscription' }); + subscriptionButton.click(); + + const resourceGroupButton = await screen.findByRole('button', { name: 'Expand A Great Resource Group' }); + resourceGroupButton.click(); + + const checkbox = await screen.findByLabelText('web-server'); + await userEvent.click(checkbox); + expect(checkbox).toBeChecked(); + + const advancedSection = screen.getByText('Advanced'); + advancedSection.click(); + + const advancedInput = await screen.findByLabelText('Subscription'); + await userEvent.type(advancedInput, 'def-123'); + + const updatedCheckboxes = await screen.findAllByLabelText('web-server'); + expect(updatedCheckboxes.length).toBe(1); + expect(updatedCheckboxes[0]).not.toBeChecked(); + }); + + it('should call onApply with a new subscription when a user types it in the selection box', async () => { + const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() }); + const query = createMockQuery(); + delete query?.subscription; + delete query?.azureMonitor?.resources; + delete query?.azureMonitor?.metricNamespace; + const onChange = jest.fn(); + + render( + {}} + /> + ); + + const resourcePickerButton = await screen.findByRole('button', { name: 'Select a resource' }); + resourcePickerButton.click(); + + const advancedSection = screen.getByText('Advanced'); + advancedSection.click(); + + const advancedInput = await screen.findByLabelText('Subscription'); + await userEvent.type(advancedInput, 'def-123'); + const nsInput = await screen.findByLabelText('Namespace'); + await userEvent.type(nsInput, 'ns'); + const rgInput = await screen.findByLabelText('Resource Group'); + await userEvent.type(rgInput, 'rg'); + const rnInput = await screen.findByLabelText('Resource Name'); + await userEvent.type(rnInput, 'rn'); + + const applyButton = screen.getByRole('button', { name: 'Apply' }); + applyButton.click(); + + expect(onChange).toBeCalledTimes(1); + expect(onChange).toBeCalledWith( + expect.objectContaining({ + azureMonitor: expect.objectContaining({ + resources: [{ subscription: 'def-123', metricNamespace: 'ns', resourceGroup: 'rg', resourceName: 'rn' }], + }), + }) + ); + }); }); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.tsx index 4358ab8422d..d4ad986b58d 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { PanelData } from '@grafana/data/src/types'; import { EditorRows, EditorRow, EditorFieldGroup } from '@grafana/experimental'; -import { config } from '@grafana/runtime'; import { multiResourceCompatibleTypes } from '../../azureMetadata'; import type Datasource from '../../datasource'; @@ -59,10 +58,6 @@ const MetricsQueryEditor: React.FC = ({ // Only if there is some resource(s) selected we should disable rows return false; } - if (!config.featureToggles.azureMultipleResourcePicker) { - // Disable multiple selection until the feature is ready - return true; - } const rowResource = parseResourceDetails(row.uri, row.location); const selectedRowSample = parseResourceDetails(selectedRows[0].uri, selectedRows[0].location); @@ -80,7 +75,7 @@ const MetricsQueryEditor: React.FC = ({ }; const selectionNotice = (selectedRows: ResourceRowGroup) => { - if (selectedRows.length === 0 || !config.featureToggles.azureMultipleResourcePicker) { + if (selectedRows.length === 0) { return ''; } const selectedRowSample = parseResourceDetails(selectedRows[0].uri, selectedRows[0].location); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/Advanced.test.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/Advanced.test.tsx deleted file mode 100644 index f4f55d70b0d..00000000000 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/Advanced.test.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import React from 'react'; - -import Advanced from './Advanced'; - -describe('AzureMonitor ResourcePicker', () => { - it('should set a parameter as an object', async () => { - const onChange = jest.fn(); - const { rerender } = render(); - const advancedSection = screen.getByText('Advanced'); - advancedSection.click(); - - const subsInput = await screen.findByLabelText('Subscription'); - await userEvent.type(subsInput, 'd'); - expect(onChange).toHaveBeenCalledWith([{ subscription: 'd' }]); - - rerender(); - expect(screen.getByLabelText('Subscription').outerHTML).toMatch('value="def-123"'); - }); - - it('should set a parameter as uri', async () => { - const onChange = jest.fn(); - const { rerender } = render(); - const advancedSection = screen.getByText('Advanced'); - advancedSection.click(); - - const subsInput = await screen.findByLabelText('Resource URI'); - await userEvent.type(subsInput, '/'); - expect(onChange).toHaveBeenCalledWith(['/']); - - rerender(); - expect(screen.getByLabelText('Resource URI').outerHTML).toMatch('value="/subscriptions/sub"'); - }); - - it('should render multiple resources', async () => { - render(); - const advancedSection = screen.getByText('Advanced'); - advancedSection.click(); - - expect(screen.getByDisplayValue('/subscriptions/sub1')).toBeInTheDocument(); - expect(screen.getByDisplayValue('/subscriptions/sub2')).toBeInTheDocument(); - }); -}); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/Advanced.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/Advanced.tsx deleted file mode 100644 index fd12b294a6f..00000000000 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/Advanced.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import React, { useState } from 'react'; - -import { Icon, Input, Tooltip, Collapse, Label, InlineField } from '@grafana/ui'; - -import { selectors } from '../../e2e/selectors'; -import { AzureMetricResource } from '../../types'; -import { Space } from '../Space'; - -interface ResourcePickerProps { - resources: T[]; - onChange: (resources: T[]) => void; -} - -const Advanced = ({ resources, onChange }: ResourcePickerProps) => { - const [isAdvancedOpen, setIsAdvancedOpen] = useState(!!resources.length && JSON.stringify(resources).includes('$')); - - const onResourceChange = (resource: string | AzureMetricResource, index: number) => { - const newResources = [...resources]; - newResources[index] = resource; - onChange(newResources); - }; - - return ( -
- setIsAdvancedOpen(!isAdvancedOpen)} - > - {(resources.length ? resources : [{}]).map((resource, index) => ( -
- {typeof resource === 'string' ? ( - <> - - onResourceChange(event.currentTarget.value, index)} - placeholder="ex: /subscriptions/$subId" - /> - - ) : ( - <> - - - onResourceChange({ ...resource, subscription: event.currentTarget.value }, index) - } - placeholder="aaaaaaaa-bbbb-cccc-dddd-eeeeeeee" - /> - - - - onResourceChange({ ...resource, resourceGroup: event.currentTarget.value }, index) - } - placeholder="resource-group" - /> - - - - onResourceChange({ ...resource, metricNamespace: event.currentTarget.value }, index) - } - placeholder="Microsoft.Insights/metricNamespaces" - /> - - - - onResourceChange({ ...resource, resourceName: event.currentTarget.value }, index) - } - placeholder="name" - /> - - - )} -
- ))} - -
-
- ); -}; - -export default Advanced; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.test.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.test.tsx index 3bad6927b27..6b4b330882e 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.test.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.test.tsx @@ -232,85 +232,6 @@ describe('AzureMonitor ResourcePicker', () => { expect(onApply).toBeCalledWith([]); }); - it('should call onApply with a new subscription uri when a user types it in the selection box', async () => { - const onApply = jest.fn(); - render(); - const subscriptionCheckbox = await screen.findByLabelText('Primary Subscription'); - expect(subscriptionCheckbox).toBeInTheDocument(); - expect(subscriptionCheckbox).not.toBeChecked(); - - const advancedSection = screen.getByText('Advanced'); - advancedSection.click(); - - const advancedInput = await screen.findByLabelText('Resource URI'); - await userEvent.type(advancedInput, '/subscriptions/def-123'); - - const applyButton = screen.getByRole('button', { name: 'Apply' }); - applyButton.click(); - - expect(onApply).toBeCalledTimes(1); - expect(onApply).toBeCalledWith(['/subscriptions/def-123']); - }); - - it('should call onApply with a new subscription when a user types it in the selection box', async () => { - const onApply = jest.fn(); - render(); - const subscriptionCheckbox = await screen.findByLabelText('Primary Subscription'); - expect(subscriptionCheckbox).toBeInTheDocument(); - expect(subscriptionCheckbox).not.toBeChecked(); - - const advancedSection = screen.getByText('Advanced'); - advancedSection.click(); - - const advancedInput = await screen.findByLabelText('Subscription'); - await userEvent.type(advancedInput, 'def-123'); - const nsInput = await screen.findByLabelText('Namespace'); - await userEvent.type(nsInput, 'ns'); - const rgInput = await screen.findByLabelText('Resource Group'); - await userEvent.type(rgInput, 'rg'); - const rnInput = await screen.findByLabelText('Resource Name'); - await userEvent.type(rnInput, 'rn'); - - const applyButton = screen.getByRole('button', { name: 'Apply' }); - applyButton.click(); - - expect(onApply).toBeCalledTimes(1); - expect(onApply).toBeCalledWith([ - { subscription: 'def-123', metricNamespace: 'ns', resourceGroup: 'rg', resourceName: 'rn' }, - ]); - }); - - it('should show unselect a subscription if the value is manually edited', async () => { - render( - - ); - const checkboxes = await screen.findAllByLabelText('web-server'); - expect(checkboxes.length).toBe(2); - expect(checkboxes[0]).toBeChecked(); - expect(checkboxes[1]).toBeChecked(); - - const advancedSection = screen.getByText('Advanced'); - advancedSection.click(); - - const advancedInput = await screen.findByLabelText('Subscription'); - await userEvent.type(advancedInput, 'def-123'); - - const updatedCheckboxes = await screen.findAllByLabelText('web-server'); - expect(updatedCheckboxes.length).toBe(1); - expect(updatedCheckboxes[0]).not.toBeChecked(); - }); - it('renders a search field which show search results when there are results', async () => { render(); const searchRow1 = screen.queryByLabelText('search-result'); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.tsx index 93d04ba6ff6..475215e0383 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.tsx @@ -2,7 +2,6 @@ import { cx } from '@emotion/css'; import React, { useCallback, useEffect, useState } from 'react'; import { useEffectOnce } from 'react-use'; -import { config } from '@grafana/runtime'; import { Alert, Button, LoadingPlaceholder, useStyles2 } from '@grafana/ui'; import { selectors } from '../../e2e/selectors'; @@ -11,7 +10,6 @@ import { AzureMetricResource } from '../../types'; import messageFromError from '../../utils/messageFromError'; import { Space } from '../Space'; -import Advanced from './Advanced'; import AdvancedMulti from './AdvancedMulti'; import NestedRow from './NestedRow'; import Search from './Search'; @@ -123,7 +121,7 @@ const ResourcePicker = ({ if (isSelected) { const newRes = queryType === 'logs' ? row.uri : parseMultipleResourceDetails([row.uri], row.location)[0]; const newSelected = internalSelected ? internalSelected.concat(newRes) : [newRes]; - setInternalSelected(newSelected); + setInternalSelected(newSelected.filter((r) => isValid(r))); } else { const newInternalSelected = internalSelected?.filter((r) => { return !matchURI(resourceToString(r), row.uri); @@ -252,15 +250,11 @@ const ResourcePicker = ({ )} - {config.featureToggles.azureMultipleResourcePicker ? ( - setInternalSelected(r)} - renderAdvanced={renderAdvanced} - /> - ) : ( - setInternalSelected(r)} /> - )} + setInternalSelected(r)} + renderAdvanced={renderAdvanced} + /> diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.tsx index 9ada46d6676..c0716cb2ed9 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.tsx @@ -1,4 +1,4 @@ -import { get } from 'lodash'; +import { get, isEqual } from 'lodash'; import React, { useEffect, useState } from 'react'; import { useEffectOnce } from 'react-use'; @@ -65,7 +65,9 @@ const VariableEditor = (props: Props) => { useEffect(() => { migrateQuery(query, { datasource: datasource }).then((migratedQuery) => { - onChange(migratedQuery); + if (!isEqual(query, migratedQuery)) { + onChange(migratedQuery); + } }); }, [query, datasource, onChange]); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/grafanaTemplateVariableFns.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/grafanaTemplateVariableFns.ts index 0ed9de7dd36..1b7578ee774 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/grafanaTemplateVariableFns.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/grafanaTemplateVariableFns.ts @@ -246,7 +246,7 @@ const createLogAnalyticsTemplateVariableQuery = async ( queryType: AzureQueryType.LogAnalytics, azureLogAnalytics: { query: rawQuery, - resources: [resource], + resources: resource ? [resource] : [], }, subscription: defaultSubscriptionId, }; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/grafanaTemplateVariables.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/grafanaTemplateVariables.test.ts index 07e78821810..88b6dbd5fa3 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/grafanaTemplateVariables.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/grafanaTemplateVariables.test.ts @@ -219,7 +219,7 @@ describe('migrateStringQueriesToObjectQueries', () => { queryType: AzureQueryType.LogAnalytics, azureLogAnalytics: { query: 'some kind of kql query', - resources: [''], + resources: [], }, subscription: 'defaultSubscriptionId', }, From f23be415c5276a4d328bc70b3d2c0b8bd802839f Mon Sep 17 00:00:00 2001 From: Horst Gutmann Date: Mon, 30 Jan 2023 17:24:10 +0100 Subject: [PATCH 117/117] CI: Add `artifacts publish` build command (#62445) * CI: Add `artifacts publish` build command * Lint release.star --- .drone.yml | 52 +++++--- pkg/build/cmd/main.go | 53 ++++++++ pkg/build/cmd/publishartifacts.go | 211 ++++++++++++++++++++++++++++++ scripts/drone/events/release.star | 9 +- 4 files changed, 303 insertions(+), 22 deletions(-) create mode 100644 pkg/build/cmd/publishartifacts.go diff --git a/.drone.yml b/.drone.yml index 1879dd0a72b..00584d6a234 100644 --- a/.drone.yml +++ b/.drone.yml @@ -4322,20 +4322,27 @@ platform: services: [] steps: - commands: - - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl - - chmod +x bin/grabpl - image: byrnedo/alpine-curl:0.1.8 - name: grabpl -- commands: - - ./bin/grabpl artifacts publish --security --tag $${DRONE_TAG} --src-bucket $${PRERELEASE_BUCKET} - depends_on: - - grabpl + - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd + depends_on: [] environment: + CGO_ENABLED: 0 + image: golang:1.19.4 + name: compile-build-cmd +- commands: + - ./bin/build artifacts publish --security --tag $${DRONE_TAG} --src-bucket $${PRERELEASE_BUCKET} + depends_on: + - compile-build-cmd + environment: + ENTERPRISE2_SECURITY_PREFIX: + from_secret: enterprise2_security_prefix GCP_KEY: from_secret: gcp_key PRERELEASE_BUCKET: from_secret: prerelease_bucket + SECURITY_DEST_BUCKET: + from_secret: security_dest_bucket + STATIC_ASSET_EDITIONS: + from_secret: static_asset_editions image: grafana/grafana-ci-deploy:1.3.3 name: publish-artifacts trigger: @@ -4366,20 +4373,27 @@ platform: services: [] steps: - commands: - - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl - - chmod +x bin/grabpl - image: byrnedo/alpine-curl:0.1.8 - name: grabpl -- commands: - - ./bin/grabpl artifacts publish --tag $${DRONE_TAG} --src-bucket $${PRERELEASE_BUCKET} - depends_on: - - grabpl + - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd + depends_on: [] environment: + CGO_ENABLED: 0 + image: golang:1.19.4 + name: compile-build-cmd +- commands: + - ./bin/build artifacts publish --tag $${DRONE_TAG} --src-bucket $${PRERELEASE_BUCKET} + depends_on: + - compile-build-cmd + environment: + ENTERPRISE2_SECURITY_PREFIX: + from_secret: enterprise2_security_prefix GCP_KEY: from_secret: gcp_key PRERELEASE_BUCKET: from_secret: prerelease_bucket + SECURITY_DEST_BUCKET: + from_secret: security_dest_bucket + STATIC_ASSET_EDITIONS: + from_secret: static_asset_editions image: grafana/grafana-ci-deploy:1.3.3 name: publish-artifacts trigger: @@ -6502,6 +6516,6 @@ kind: secret name: aws_secret_access_key --- kind: signature -hmac: 6e76bf175f2c58fd4ffdc42e2120c558345a71a45011279b14092acb67252b28 +hmac: eba6c445aae6d75df0a2963d5e1e90c44474587a9e1d11e21bf2ba4d99f14da8 ... diff --git a/pkg/build/cmd/main.go b/pkg/build/cmd/main.go index 1ee1270a65d..2613c50bff0 100644 --- a/pkg/build/cmd/main.go +++ b/pkg/build/cmd/main.go @@ -190,6 +190,59 @@ func main() { Name: "artifacts", Usage: "Handle Grafana artifacts", Subcommands: cli.Commands{ + { + Name: "publish", + Usage: "Publish Grafana artifacts", + Action: PublishArtifactsAction, + Flags: []cli.Flag{ + &editionFlag, + &cli.BoolFlag{ + Name: "security", + Usage: "Security release", + }, + &cli.StringFlag{ + Name: "security-dest-bucket", + Usage: "Google Cloud Storage bucket for security packages (or $SECURITY_DEST_BUCKET)", + }, + &cli.StringFlag{ + Name: "tag", + Usage: "Grafana version tag", + }, + &cli.StringFlag{ + Name: "src-bucket", + Value: "grafana-prerelease", + Usage: "Google Cloud Storage bucket", + }, + &cli.StringFlag{ + Name: "dest-bucket", + Value: "grafana-downloads", + Usage: "Google Cloud Storage bucket for published packages", + }, + &cli.StringFlag{ + Name: "enterprise2-dest-bucket", + Value: "grafana-downloads-enterprise2", + Usage: "Google Cloud Storage bucket for published packages", + }, + &cli.StringFlag{ + Name: "enterprise2-security-prefix", + Usage: "Bucket path prefix for enterprise2 security releases (or $ENTERPRISE2_SECURITY_PREFIX)", + }, + &cli.StringFlag{ + Name: "static-assets-bucket", + Value: "grafana-static-assets", + Usage: "Google Cloud Storage bucket for static assets", + }, + &cli.StringSliceFlag{ + Name: "static-asset-editions", + Usage: "All the editions of the static assets (or $STATIC_ASSET_EDITIONS)", + }, + &cli.StringFlag{ + Name: "storybook-bucket", + Value: "grafana-storybook", + Usage: "Google Cloud Storage bucket for storybooks", + }, + }, + }, { Name: "docker", Usage: "Handle Grafana Docker images", diff --git a/pkg/build/cmd/publishartifacts.go b/pkg/build/cmd/publishartifacts.go new file mode 100644 index 00000000000..bd158d6d3ad --- /dev/null +++ b/pkg/build/cmd/publishartifacts.go @@ -0,0 +1,211 @@ +package main + +import ( + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/grafana/grafana/pkg/build/gcloud" + "github.com/grafana/grafana/pkg/build/versions" + "github.com/urfave/cli/v2" +) + +type publishConfig struct { + tag string + srcBucket string + destBucket string + enterprise2DestBucket string + enterprise2SecurityPrefix string + staticAssetsBucket string + staticAssetEditions []string + storybookBucket string + security bool +} + +// requireListWithEnvFallback first checks the CLI for a flag with the required +// name. If this is empty, it falls back to taking the environment variable. +// Sadly, we cannot use cli.Flag.EnvVars for this due to it potentially leaking +// environment variables as default values in usage-errors. +func requireListWithEnvFallback(cctx *cli.Context, name string, envName string) ([]string, error) { + result := cctx.StringSlice(name) + if len(result) == 0 { + for _, v := range strings.Split(os.Getenv(envName), ",") { + value := strings.TrimSpace(v) + if value != "" { + result = append(result, value) + } + } + } + if len(result) == 0 { + return nil, cli.Exit(fmt.Sprintf("Required flag (%s) or environment variable (%s) not set", name, envName), 1) + } + return result, nil +} + +func requireStringWithEnvFallback(cctx *cli.Context, name string, envName string) (string, error) { + result := cctx.String(name) + if result == "" { + result = os.Getenv(envName) + } + if result == "" { + return "", cli.Exit(fmt.Sprintf("Required flag (%s) or environment variable (%s) not set", name, envName), 1) + } + return result, nil +} + +// Action implements the sub-command "publish-artifacts". +func PublishArtifactsAction(c *cli.Context) error { + if c.NArg() > 0 { + if err := cli.ShowSubcommandHelp(c); err != nil { + return cli.Exit(err.Error(), 1) + } + return cli.Exit("", 1) + } + + staticAssetEditions, err := requireListWithEnvFallback(c, "static-asset-editions", "STATIC_ASSET_EDITIONS") + if err != nil { + return err + } + securityDestBucket, err := requireStringWithEnvFallback(c, "security-dest-bucket", "SECURITY_DEST_BUCKET") + if err != nil { + return err + } + enterprise2SecurityPrefix, err := requireStringWithEnvFallback(c, "enterprise2-security-prefix", "ENTERPRISE2_SECURITY_PREFIX") + if err != nil { + return err + } + + if err := gcloud.ActivateServiceAccount(); err != nil { + return fmt.Errorf("error connecting to gcp, %q", err) + } + + cfg := publishConfig{ + srcBucket: c.String("src-bucket"), + destBucket: c.String("dest-bucket"), + enterprise2DestBucket: c.String("enterprise2-dest-bucket"), + enterprise2SecurityPrefix: enterprise2SecurityPrefix, + staticAssetsBucket: c.String("static-assets-bucket"), + staticAssetEditions: staticAssetEditions, + storybookBucket: c.String("storybook-bucket"), + security: c.Bool("security"), + tag: strings.TrimPrefix(c.String("tag"), "v"), + } + + if cfg.security { + cfg.destBucket = securityDestBucket + } + + err = copyStaticAssets(cfg) + if err != nil { + return err + } + err = copyStorybook(cfg) + if err != nil { + return err + } + err = copyDownloads(cfg) + if err != nil { + return err + } + err = copyEnterprise2Downloads(cfg) + if err != nil { + return err + } + return nil +} + +func copyStaticAssets(cfg publishConfig) error { + for _, edition := range cfg.staticAssetEditions { + log.Printf("Copying static assets for %s", edition) + srcURL := fmt.Sprintf("%s/artifacts/static-assets/%s/%s/*", cfg.srcBucket, edition, cfg.tag) + destURL := fmt.Sprintf("%s/%s/%s/", cfg.staticAssetsBucket, edition, cfg.tag) + err := gcsCopy("static assets", srcURL, destURL) + if err != nil { + return fmt.Errorf("error copying static assets, %q", err) + } + } + log.Printf("Successfully copied static assets!") + return nil +} + +func copyStorybook(cfg publishConfig) error { + if cfg.security { + log.Printf("skipping storybook copy - not needed for a security release") + return nil + } + log.Printf("Copying storybooks...") + srcURL := fmt.Sprintf("%s/artifacts/storybook/v%s/*", cfg.srcBucket, cfg.tag) + destURL := fmt.Sprintf("%s/%s", cfg.storybookBucket, cfg.tag) + err := gcsCopy("storybook", srcURL, destURL) + if err != nil { + return fmt.Errorf("error copying storybook. %q", err) + } + stableVersion, err := versions.GetLatestVersion(versions.LatestStableVersionURL) + if err != nil { + return err + } + isLatest, err := versions.IsGreaterThanOrEqual(cfg.tag, stableVersion) + if err != nil { + return err + } + if isLatest { + log.Printf("Copying storybooks to latest...") + srcURL := fmt.Sprintf("%s/artifacts/storybook/v%s/*", cfg.srcBucket, cfg.tag) + destURL := fmt.Sprintf("%s/latest", cfg.storybookBucket) + err := gcsCopy("storybook (latest)", srcURL, destURL) + if err != nil { + return fmt.Errorf("error copying storybook to latest. %q", err) + } + } + + log.Printf("Successfully copied storybook!") + return nil +} + +func copyDownloads(cfg publishConfig) error { + for _, edition := range []string{ + "oss", "enterprise", + } { + destURL := fmt.Sprintf("%s/%s/", cfg.destBucket, edition) + srcURL := fmt.Sprintf("%s/artifacts/downloads/v%s/%s/release/*", cfg.srcBucket, cfg.tag, edition) + if !cfg.security { + destURL = filepath.Join(destURL, "release") + } + log.Printf("Copying downloads for %s, from %s bucket to %s bucket", edition, srcURL, destURL) + err := gcsCopy("downloads", srcURL, destURL) + if err != nil { + return fmt.Errorf("error copying downloads, %q", err) + } + } + log.Printf("Successfully copied downloads.") + return nil +} + +func copyEnterprise2Downloads(cfg publishConfig) error { + var prefix string + if cfg.security { + prefix = cfg.enterprise2SecurityPrefix + } + srcURL := fmt.Sprintf("%s/artifacts/downloads-enterprise2/v%s/enterprise2/release/*", cfg.srcBucket, cfg.tag) + destURL := fmt.Sprintf("%s/enterprise2/%srelease", cfg.enterprise2DestBucket, prefix) + log.Printf("Copying downloads for enterprise2, from %s bucket to %s bucket", srcURL, destURL) + err := gcsCopy("enterprise2 downloads", srcURL, destURL) + if err != nil { + return fmt.Errorf("error copying ") + } + return nil +} + +func gcsCopy(desc, src, dest string) error { + args := strings.Split(fmt.Sprintf("-m cp -r gs://%s gs://%s", src, dest), " ") + // nolint:gosec + cmd := exec.Command("gsutil", args...) + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("failed to publish %s: %w\n%s", desc, err, out) + } + return nil +} diff --git a/scripts/drone/events/release.star b/scripts/drone/events/release.star index d9edb1c9bee..8db03e1caec 100644 --- a/scripts/drone/events/release.star +++ b/scripts/drone/events/release.star @@ -532,13 +532,16 @@ def publish_artifacts_step(mode): "environment": { "GCP_KEY": from_secret("gcp_key"), "PRERELEASE_BUCKET": from_secret("prerelease_bucket"), + "ENTERPRISE2_SECURITY_PREFIX": from_secret("enterprise2_security_prefix"), + "SECURITY_DEST_BUCKET": from_secret("security_dest_bucket"), + "STATIC_ASSET_EDITIONS": from_secret("static_asset_editions"), }, "commands": [ - "./bin/grabpl artifacts publish {}--tag $${{DRONE_TAG}} --src-bucket $${{PRERELEASE_BUCKET}}".format( + "./bin/build artifacts publish {}--tag $${{DRONE_TAG}} --src-bucket $${{PRERELEASE_BUCKET}}".format( security, ), ], - "depends_on": ["grabpl"], + "depends_on": ["compile-build-cmd"], } def publish_artifacts_pipelines(mode): @@ -547,7 +550,7 @@ def publish_artifacts_pipelines(mode): "target": [mode], } steps = [ - download_grabpl_step(), + compile_build_cmd(), publish_artifacts_step(mode), ]