From 65a0a8cd3c33d115ccd6b29636bf36d0f4892efc Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Tue, 13 Feb 2024 15:56:59 +0100 Subject: [PATCH 01/19] Loki: Update `@grafana/lezer-logql` to `0.2.3` containing fix for ip label name (#82378) Loki: Update @grafana/lezer-logql to 0.2.3 containing fix for ip label name --- package.json | 2 +- yarn.lock | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 5a46e33cfac..49663e1c2d5 100644 --- a/package.json +++ b/package.json @@ -241,7 +241,7 @@ "@grafana/faro-web-sdk": "^1.3.6", "@grafana/flamegraph": "workspace:*", "@grafana/google-sdk": "0.1.2", - "@grafana/lezer-logql": "0.2.2", + "@grafana/lezer-logql": "0.2.3", "@grafana/monaco-logql": "^0.0.7", "@grafana/o11y-ds-frontend": "workspace:*", "@grafana/prometheus": "workspace:*", diff --git a/yarn.lock b/yarn.lock index d42266b7a73..edb666051bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3791,6 +3791,15 @@ __metadata: languageName: node linkType: hard +"@grafana/lezer-logql@npm:0.2.3": + version: 0.2.3 + resolution: "@grafana/lezer-logql@npm:0.2.3" + peerDependencies: + "@lezer/lr": ^1.0.0 + checksum: 10/a7f2f07d328d3e8c3d5cd31c5a0ea9379beeb7d65def078bae159298814d92881d978b088f05eff5148c8e26528de84a6fb6add8edf7bb383863fa51e9bc5f56 + languageName: node + linkType: hard + "@grafana/lezer-traceql@npm:0.0.14": version: 0.0.14 resolution: "@grafana/lezer-traceql@npm:0.0.14" @@ -18012,7 +18021,7 @@ __metadata: "@grafana/faro-web-sdk": "npm:^1.3.6" "@grafana/flamegraph": "workspace:*" "@grafana/google-sdk": "npm:0.1.2" - "@grafana/lezer-logql": "npm:0.2.2" + "@grafana/lezer-logql": "npm:0.2.3" "@grafana/monaco-logql": "npm:^0.0.7" "@grafana/o11y-ds-frontend": "workspace:*" "@grafana/prometheus": "workspace:*" From dcbc3aa46a509951bc0c6c2cf26a4a02da0cb657 Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Tue, 13 Feb 2024 15:39:41 +0000 Subject: [PATCH 02/19] Chore: Update `grabpl` to `v3.0.50` (#82379) Bump grabpl version --- .drone.yml | 18 +++++++++--------- scripts/drone/variables.star | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.drone.yml b/.drone.yml index 61469de878f..ba8d9612d6a 100644 --- a/.drone.yml +++ b/.drone.yml @@ -499,7 +499,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.47/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.50/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -851,7 +851,7 @@ steps: name: clone-enterprise - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.47/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.50/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1774,7 +1774,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.47/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.50/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2223,7 +2223,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.47/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.50/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2425,7 +2425,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.47/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.50/windows/grabpl.exe -OutFile grabpl.exe image: grafana/ci-wix:0.1.1 name: windows-init @@ -2555,7 +2555,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.47/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.50/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3215,7 +3215,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.47/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.50/windows/grabpl.exe -OutFile grabpl.exe image: grafana/ci-wix:0.1.1 name: windows-init @@ -4017,7 +4017,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.47/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.50/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -4800,6 +4800,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: f82455098bcac4c4b46f62ec7bc768660ccce0bb4c869da8bb85026e5845aa49 +hmac: c5243aaa05fbb21c64d03465f079ad710c9dcddb81db19b89ebb61cc9b60bc72 ... diff --git a/scripts/drone/variables.star b/scripts/drone/variables.star index 51b8bb45a20..bc118eedaa3 100644 --- a/scripts/drone/variables.star +++ b/scripts/drone/variables.star @@ -2,7 +2,7 @@ global variables """ -grabpl_version = "v3.0.47" +grabpl_version = "v3.0.50" golang_version = "1.21.6" # nodejs_version should match what's in ".nvmrc", but without the v prefix. From 3f940f4da135320ba82319e527ae9699c4b55446 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Tue, 13 Feb 2024 17:47:00 +0100 Subject: [PATCH 03/19] Chore: Use jsoniter in influxdb from grafana-plugin-sdk-go (#82360) use jsoniter from grafana-plugin-sdk-go --- pkg/tsdb/influxdb/influxql/converter/converter.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/tsdb/influxdb/influxql/converter/converter.go b/pkg/tsdb/influxdb/influxql/converter/converter.go index d81d2ffc9b6..feaeb4b0406 100644 --- a/pkg/tsdb/influxdb/influxql/converter/converter.go +++ b/pkg/tsdb/influxdb/influxql/converter/converter.go @@ -7,11 +7,11 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" + sdkjsoniter "github.com/grafana/grafana-plugin-sdk-go/data/utils/jsoniter" jsoniter "github.com/json-iterator/go" "github.com/grafana/grafana/pkg/tsdb/influxdb/influxql/util" "github.com/grafana/grafana/pkg/tsdb/influxdb/models" - "github.com/grafana/grafana/pkg/util/converter/jsonitere" ) func rspErr(e error) *backend.DataResponse { @@ -19,7 +19,7 @@ func rspErr(e error) *backend.DataResponse { } func ReadInfluxQLStyleResult(jIter *jsoniter.Iterator, query *models.Query) *backend.DataResponse { - iter := jsonitere.NewIterator(jIter) + iter := sdkjsoniter.NewIterator(jIter) var rsp *backend.DataResponse l1Fields: @@ -51,7 +51,7 @@ l1Fields: return rsp } -func readResults(iter *jsonitere.Iterator, query *models.Query) *backend.DataResponse { +func readResults(iter *sdkjsoniter.Iterator, query *models.Query) *backend.DataResponse { rsp := &backend.DataResponse{Frames: make(data.Frames, 0)} l1Fields: for more, err := iter.ReadArray(); more; more, err = iter.ReadArray() { @@ -79,7 +79,7 @@ l1Fields: return rsp } -func readSeries(iter *jsonitere.Iterator, query *models.Query) *backend.DataResponse { +func readSeries(iter *sdkjsoniter.Iterator, query *models.Query) *backend.DataResponse { var ( measurement string tags map[string]string @@ -179,7 +179,7 @@ func readSeries(iter *jsonitere.Iterator, query *models.Query) *backend.DataResp return rsp } -func readTags(iter *jsonitere.Iterator) (map[string]string, error) { +func readTags(iter *sdkjsoniter.Iterator) (map[string]string, error) { tags := make(map[string]string) for l1Field, err := iter.ReadObject(); l1Field != ""; l1Field, err = iter.ReadObject() { if err != nil { @@ -194,7 +194,7 @@ func readTags(iter *jsonitere.Iterator) (map[string]string, error) { return tags, nil } -func readColumns(iter *jsonitere.Iterator) (columns []string, err error) { +func readColumns(iter *sdkjsoniter.Iterator) (columns []string, err error) { for more, err := iter.ReadArray(); more; more, err = iter.ReadArray() { if err != nil { return nil, err @@ -209,7 +209,7 @@ func readColumns(iter *jsonitere.Iterator) (columns []string, err error) { return columns, nil } -func readValues(iter *jsonitere.Iterator, hasTimeColumn bool) (valueFields data.Fields, err error) { +func readValues(iter *sdkjsoniter.Iterator, hasTimeColumn bool) (valueFields data.Fields, err error) { if hasTimeColumn { valueFields = append(valueFields, data.NewField("Time", nil, make([]time.Time, 0))) } From ccb4533a863dbeb3b804ad6d52f5894c11a2c27f Mon Sep 17 00:00:00 2001 From: Alexander Weaver Date: Tue, 13 Feb 2024 10:56:24 -0600 Subject: [PATCH 04/19] Alerting: Remove unused AlertRuleVersionWithPauseStatus (#82383) Remove unused AlertRuleVersionWithPauseStatus --- pkg/services/ngalert/api/persist.go | 2 +- pkg/services/ngalert/models/alert_rule.go | 5 ----- pkg/services/ngalert/store/alert_rule.go | 4 ++-- pkg/services/ngalert/tests/fakes/rules.go | 13 +++++-------- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/pkg/services/ngalert/api/persist.go b/pkg/services/ngalert/api/persist.go index c8f759920d9..59d4930d7d0 100644 --- a/pkg/services/ngalert/api/persist.go +++ b/pkg/services/ngalert/api/persist.go @@ -24,5 +24,5 @@ type RuleStore interface { DeleteAlertRulesByUID(ctx context.Context, orgID int64, ruleUID ...string) error // IncreaseVersionForAllRulesInNamespace Increases version for all rules that have specified namespace. Returns all rules that belong to the namespace - IncreaseVersionForAllRulesInNamespace(ctx context.Context, orgID int64, namespaceUID string) ([]ngmodels.AlertRuleKeyWithVersionAndPauseStatus, error) + IncreaseVersionForAllRulesInNamespace(ctx context.Context, orgID int64, namespaceUID string) ([]ngmodels.AlertRuleKeyWithVersion, error) } diff --git a/pkg/services/ngalert/models/alert_rule.go b/pkg/services/ngalert/models/alert_rule.go index 881fc311d87..f93eb3a5484 100644 --- a/pkg/services/ngalert/models/alert_rule.go +++ b/pkg/services/ngalert/models/alert_rule.go @@ -368,11 +368,6 @@ type AlertRuleKeyWithVersion struct { AlertRuleKey `xorm:"extends"` } -type AlertRuleKeyWithVersionAndPauseStatus struct { - IsPaused bool - AlertRuleKeyWithVersion `xorm:"extends"` -} - type AlertRuleKeyWithId struct { AlertRuleKey ID int64 diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index 585ddf14261..71c97b6db78 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -72,8 +72,8 @@ func (st DBstore) DeleteAlertRulesByUID(ctx context.Context, orgID int64, ruleUI } // IncreaseVersionForAllRulesInNamespace Increases version for all rules that have specified namespace. Returns all rules that belong to the namespace -func (st DBstore) IncreaseVersionForAllRulesInNamespace(ctx context.Context, orgID int64, namespaceUID string) ([]ngmodels.AlertRuleKeyWithVersionAndPauseStatus, error) { - var keys []ngmodels.AlertRuleKeyWithVersionAndPauseStatus +func (st DBstore) IncreaseVersionForAllRulesInNamespace(ctx context.Context, orgID int64, namespaceUID string) ([]ngmodels.AlertRuleKeyWithVersion, error) { + var keys []ngmodels.AlertRuleKeyWithVersion err := st.SQLStore.WithTransactionalDbSession(ctx, func(sess *db.Session) error { now := TimeNow() _, err := sess.Exec("UPDATE alert_rule SET version = version + 1, updated = ? WHERE namespace_uid = ? AND org_id = ?", now, namespaceUID, orgID) diff --git a/pkg/services/ngalert/tests/fakes/rules.go b/pkg/services/ngalert/tests/fakes/rules.go index 6c8c28161d0..9185047c989 100644 --- a/pkg/services/ngalert/tests/fakes/rules.go +++ b/pkg/services/ngalert/tests/fakes/rules.go @@ -315,7 +315,7 @@ func (f *RuleStore) UpdateRuleGroup(ctx context.Context, orgID int64, namespaceU return nil } -func (f *RuleStore) IncreaseVersionForAllRulesInNamespace(_ context.Context, orgID int64, namespaceUID string) ([]models.AlertRuleKeyWithVersionAndPauseStatus, error) { +func (f *RuleStore) IncreaseVersionForAllRulesInNamespace(_ context.Context, orgID int64, namespaceUID string) ([]models.AlertRuleKeyWithVersion, error) { f.mtx.Lock() defer f.mtx.Unlock() @@ -324,18 +324,15 @@ func (f *RuleStore) IncreaseVersionForAllRulesInNamespace(_ context.Context, org Params: []any{orgID, namespaceUID}, }) - var result []models.AlertRuleKeyWithVersionAndPauseStatus + var result []models.AlertRuleKeyWithVersion for _, rule := range f.Rules[orgID] { if rule.NamespaceUID == namespaceUID && rule.OrgID == orgID { rule.Version++ rule.Updated = time.Now() - result = append(result, models.AlertRuleKeyWithVersionAndPauseStatus{ - IsPaused: rule.IsPaused, - AlertRuleKeyWithVersion: models.AlertRuleKeyWithVersion{ - Version: rule.Version, - AlertRuleKey: rule.GetKey(), - }, + result = append(result, models.AlertRuleKeyWithVersion{ + Version: rule.Version, + AlertRuleKey: rule.GetKey(), }) } } From dbde08b03c05356c740d3adb02276651a085748b Mon Sep 17 00:00:00 2001 From: Ezequiel Victorero Date: Tue, 13 Feb 2024 14:15:55 -0300 Subject: [PATCH 05/19] Scenes: Refactor original snapshot button in a new component (#82199) --- .../kinds/core/dashboard/schema-reference.md | 1 + kinds/dashboard/dashboard_kind.cue | 2 + .../raw/dashboard/x/dashboard_types.gen.ts | 4 ++ pkg/kinds/dashboard/dashboard_spec_gen.go | 3 + pkg/kindsysreport/codegen/report.json | 2 +- .../scene/DashboardScene.test.tsx | 60 ------------------ .../dashboard-scene/scene/DashboardScene.tsx | 50 +-------------- .../scene/GoToSnapshotOriginButton.test.tsx | 60 ++++++++++++++++++ .../scene/GoToSnapshotOriginButton.tsx | 62 +++++++++++++++++++ .../scene/NavToolbarActions.tsx | 10 +-- 10 files changed, 138 insertions(+), 116 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/GoToSnapshotOriginButton.test.tsx create mode 100644 public/app/features/dashboard-scene/scene/GoToSnapshotOriginButton.tsx diff --git a/docs/sources/developers/kinds/core/dashboard/schema-reference.md b/docs/sources/developers/kinds/core/dashboard/schema-reference.md index eb287875f5c..561580fd2fd 100644 --- a/docs/sources/developers/kinds/core/dashboard/schema-reference.md +++ b/docs/sources/developers/kinds/core/dashboard/schema-reference.md @@ -186,6 +186,7 @@ Sensitive information stripped: queries (metric, template,annotation) and panel | `key` | string | **Yes** | | Optional, defined the unique key of the snapshot, required if external is true | | `name` | string | **Yes** | | Optional, name of the snapshot | | `orgId` | uint32 | **Yes** | | org id of the snapshot | +| `originalUrl` | string | **Yes** | | original url, url of the dashboard that was snapshotted | | `updated` | string | **Yes** | | last time when the snapshot was updated | | `userId` | uint32 | **Yes** | | user id of the snapshot creator | | `url` | string | No | | url of the snapshot, if snapshot was shared internally | diff --git a/kinds/dashboard/dashboard_kind.cue b/kinds/dashboard/dashboard_kind.cue index 6cace8d26ff..fe963ef5bab 100644 --- a/kinds/dashboard/dashboard_kind.cue +++ b/kinds/dashboard/dashboard_kind.cue @@ -494,6 +494,8 @@ lineage: schemas: [{ external: bool @grafanamaturity(NeedsExpertReview) // external url, if snapshot was shared in external grafana instance externalUrl: string @grafanamaturity(NeedsExpertReview) + // original url, url of the dashboard that was snapshotted + originalUrl: string @grafanamaturity(NeedsExpertReview) // Unique identifier of the snapshot id: uint32 @grafanamaturity(NeedsExpertReview) // Optional, defined the unique key of the snapshot, required if external is true 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 c34707a521c..3e7d114de76 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 @@ -1088,6 +1088,10 @@ export interface Dashboard { * external url, if snapshot was shared in external grafana instance */ externalUrl: string; + /** + * original url, url of the dashboard that was snapshotted + */ + originalUrl: string; /** * Unique identifier of the snapshot */ diff --git a/pkg/kinds/dashboard/dashboard_spec_gen.go b/pkg/kinds/dashboard/dashboard_spec_gen.go index dfd9bf19629..3014bba97ef 100644 --- a/pkg/kinds/dashboard/dashboard_spec_gen.go +++ b/pkg/kinds/dashboard/dashboard_spec_gen.go @@ -694,6 +694,9 @@ type Snapshot struct { // OrgId org id of the snapshot OrgId int `json:"orgId"` + // OriginalUrl original url, url of the dashboard that was snapshotted + OriginalUrl string `json:"originalUrl"` + // Updated last time when the snapshot was updated Updated time.Time `json:"updated"` diff --git a/pkg/kindsysreport/codegen/report.json b/pkg/kindsysreport/codegen/report.json index 015db1369bd..a44e34a9472 100644 --- a/pkg/kindsysreport/codegen/report.json +++ b/pkg/kindsysreport/codegen/report.json @@ -374,7 +374,7 @@ 0 ], "description": "A Grafana dashboard.", - "grafanaMaturityCount": 103, + "grafanaMaturityCount": 105, "lineageIsGroup": false, "links": { "docs": "https://grafana.com/docs/grafana/next/developers/kinds/core/dashboard/schema-reference", diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index c41f368b4dd..85c0986fcf9 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -1,5 +1,4 @@ import { CoreApp } from '@grafana/data'; -import { config, locationService } from '@grafana/runtime'; import { sceneGraph, SceneGridItem, @@ -12,12 +11,10 @@ import { VizPanel, } from '@grafana/scenes'; import { Dashboard } from '@grafana/schema'; -import { ConfirmModal } from '@grafana/ui'; import appEvents from 'app/core/app_events'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { VariablesChanged } from 'app/features/variables/types'; -import { ShowModalReactEvent } from '../../../types/events'; import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; import { DecoratedRevisionModel } from '../settings/VersionsEditView'; import { historySrv } from '../settings/version-history/HistorySrv'; @@ -208,63 +205,6 @@ describe('DashboardScene', () => { } }); }); - - describe('when opening a dashboard from a snapshot', () => { - let scene: DashboardScene; - beforeEach(async () => { - scene = buildTestScene(); - locationService.push('/'); - // mockLocationHref('http://snapshots.grafana.com/snapshots/dashboard/abcdefghi/my-dash'); - const location = window.location; - - //@ts-ignore - delete window.location; - window.location = { - ...location, - href: 'http://snapshots.grafana.com/snapshots/dashboard/abcdefghi/my-dash', - }; - jest.spyOn(appEvents, 'publish'); - }); - - config.appUrl = 'http://snapshots.grafana.com/'; - - it('redirects to the original dashboard', () => { - scene.setInitialSaveModel({ - // @ts-ignore - snapshot: { originalUrl: '/d/c0d2742f-b827-466d-9269-fb34d6af24ff' }, - }); - - // Call the function - scene.onOpenSnapshotOriginalDashboard(); - - // Assertions - expect(appEvents.publish).toHaveBeenCalledTimes(0); - expect(locationService.getLocation().pathname).toEqual('/d/c0d2742f-b827-466d-9269-fb34d6af24ff'); - expect(window.location.href).toBe('http://snapshots.grafana.com/snapshots/dashboard/abcdefghi/my-dash'); - }); - - it('opens a confirmation modal', () => { - scene.setInitialSaveModel({ - // @ts-ignore - snapshot: { originalUrl: 'http://www.anotherdomain.com/' }, - }); - - // Call the function - scene.onOpenSnapshotOriginalDashboard(); - - // Assertions - expect(appEvents.publish).toHaveBeenCalledTimes(1); - expect(appEvents.publish).toHaveBeenCalledWith( - new ShowModalReactEvent( - expect.objectContaining({ - component: ConfirmModal, - }) - ) - ); - expect(locationService.getLocation().pathname).toEqual('/'); - expect(window.location.href).toBe('http://snapshots.grafana.com/snapshots/dashboard/abcdefghi/my-dash'); - }); - }); }); function buildTestScene(overrides?: Partial) { diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 7f1c7706f1a..11402d317ae 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -1,10 +1,8 @@ -import { css } from '@emotion/css'; import * as H from 'history'; -import React from 'react'; import { Unsubscribable } from 'rxjs'; -import { AppEvents, CoreApp, DataQueryRequest, NavIndex, NavModelItem, locationUtil, textUtil } from '@grafana/data'; -import { locationService, config } from '@grafana/runtime'; +import { AppEvents, CoreApp, DataQueryRequest, NavIndex, NavModelItem, locationUtil } from '@grafana/data'; +import { locationService } from '@grafana/runtime'; import { dataLayers, getUrlSyncManager, @@ -25,7 +23,6 @@ import { VizPanel, } from '@grafana/scenes'; import { Dashboard, DashboardLink } from '@grafana/schema'; -import { ConfirmModal } from '@grafana/ui'; import appEvents from 'app/core/app_events'; import { LS_PANEL_COPY_KEY } from 'app/core/constants'; import { getNavModel } from 'app/core/selectors/navModel'; @@ -35,7 +32,7 @@ import { DashboardModel } from 'app/features/dashboard/state'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; import { VariablesChanged } from 'app/features/variables/types'; import { DashboardDTO, DashboardMeta, SaveDashboardResponseDTO } from 'app/types'; -import { ShowModalReactEvent, ShowConfirmModalEvent } from 'app/types/events'; +import { ShowConfirmModalEvent } from 'app/types/events'; import { PanelEditor } from '../panel-edit/PanelEditor'; import { SaveDashboardDrawer } from '../saving/SaveDashboardDrawer'; @@ -503,47 +500,6 @@ export class DashboardScene extends SceneObjectBase { } } - public onOpenSnapshotOriginalDashboard = () => { - // @ts-ignore - const relativeURL = this.getInitialSaveModel()?.snapshot?.originalUrl ?? ''; - const sanitizedRelativeURL = textUtil.sanitizeUrl(relativeURL); - try { - const sanitizedAppUrl = new URL(sanitizedRelativeURL, config.appUrl); - const appUrl = new URL(config.appUrl); - if (sanitizedAppUrl.host !== appUrl.host) { - appEvents.publish( - new ShowModalReactEvent({ - component: ConfirmModal, - props: { - title: 'Proceed to external site?', - modalClass: css({ - width: 'max-content', - maxWidth: '80vw', - }), - body: ( - <> -

- {`This link connects to an external website at`} {relativeURL} -

-

{"Are you sure you'd like to proceed?"}

- - ), - confirmVariant: 'primary', - confirmText: 'Proceed', - onConfirm: () => { - window.location.href = sanitizedAppUrl.href; - }, - }, - }) - ); - } else { - locationService.push(sanitizedRelativeURL); - } - } catch (err) { - console.error('Failed to open original dashboard', err); - } - }; - public onOpenSettings = () => { locationService.partial({ editview: 'settings' }); }; diff --git a/public/app/features/dashboard-scene/scene/GoToSnapshotOriginButton.test.tsx b/public/app/features/dashboard-scene/scene/GoToSnapshotOriginButton.test.tsx new file mode 100644 index 00000000000..6e55b4d0ab0 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/GoToSnapshotOriginButton.test.tsx @@ -0,0 +1,60 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import React from 'react'; + +import { config, locationService } from '@grafana/runtime'; +import { ConfirmModal } from '@grafana/ui'; + +import appEvents from '../../../core/app_events'; +import { ShowModalReactEvent } from '../../../types/events'; + +import { GoToSnapshotOriginButton } from './GoToSnapshotOriginButton'; + +describe('GoToSnapshotOriginButton component', () => { + beforeEach(async () => { + locationService.push('/'); + const location = window.location; + //@ts-ignore + delete window.location; + window.location = { + ...location, + href: 'http://snapshots.grafana.com/snapshots/dashboard/abcdefghi/my-dash', + }; + jest.spyOn(appEvents, 'publish'); + }); + config.appUrl = 'http://snapshots.grafana.com/'; + + it('renders button and triggers onClick redirects to the original dashboard', () => { + render(); + + // Check if the button renders with the correct testid + expect(screen.getByTestId('button-snapshot')).toBeInTheDocument(); + + // Simulate a button click + fireEvent.click(screen.getByTestId('button-snapshot')); + + expect(appEvents.publish).toHaveBeenCalledTimes(0); + expect(locationService.getLocation().pathname).toEqual('/d/c0d2742f-b827-466d-9269-fb34d6af24ff'); + expect(window.location.href).toBe('http://snapshots.grafana.com/snapshots/dashboard/abcdefghi/my-dash'); + }); + + it('renders button and triggers onClick opens a confirmation modal', () => { + render(); + + // Check if the button renders with the correct testid + expect(screen.getByTestId('button-snapshot')).toBeInTheDocument(); + + // Simulate a button click + fireEvent.click(screen.getByTestId('button-snapshot')); + + expect(appEvents.publish).toHaveBeenCalledTimes(1); + expect(appEvents.publish).toHaveBeenCalledWith( + new ShowModalReactEvent( + expect.objectContaining({ + component: ConfirmModal, + }) + ) + ); + expect(locationService.getLocation().pathname).toEqual('/'); + expect(window.location.href).toBe('http://snapshots.grafana.com/snapshots/dashboard/abcdefghi/my-dash'); + }); +}); diff --git a/public/app/features/dashboard-scene/scene/GoToSnapshotOriginButton.tsx b/public/app/features/dashboard-scene/scene/GoToSnapshotOriginButton.tsx new file mode 100644 index 00000000000..28c5c802f47 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/GoToSnapshotOriginButton.tsx @@ -0,0 +1,62 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { textUtil } from '@grafana/data'; +import { config, locationService } from '@grafana/runtime'; +import { ConfirmModal, ToolbarButton } from '@grafana/ui'; + +import appEvents from '../../../core/app_events'; +import { t } from '../../../core/internationalization'; +import { ShowModalReactEvent } from '../../../types/events'; + +export function GoToSnapshotOriginButton(props: { originalURL: string }) { + return ( + onOpenSnapshotOriginalDashboard(props.originalURL)} + /> + ); +} + +const onOpenSnapshotOriginalDashboard = (originalUrl: string) => { + const relativeURL = originalUrl ?? ''; + const sanitizedRelativeURL = textUtil.sanitizeUrl(relativeURL); + try { + const sanitizedAppUrl = new URL(sanitizedRelativeURL, config.appUrl); + const appUrl = new URL(config.appUrl); + if (sanitizedAppUrl.host !== appUrl.host) { + appEvents.publish( + new ShowModalReactEvent({ + component: ConfirmModal, + props: { + title: 'Proceed to external site?', + modalClass: css({ + width: 'max-content', + maxWidth: '80vw', + }), + body: ( + <> +

+ {`This link connects to an external website at`} {relativeURL} +

+

{"Are you sure you'd like to proceed?"}

+ + ), + confirmVariant: 'primary', + confirmText: 'Proceed', + onConfirm: () => { + window.location.href = sanitizedAppUrl.href; + }, + }, + }) + ); + } else { + locationService.push(sanitizedRelativeURL); + } + } catch (err) { + console.error('Failed to open original dashboard', err); + } +}; diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx index 8b1e01af6b7..540bc7fefb0 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx @@ -15,6 +15,7 @@ import { DashboardInteractions } from '../utils/interactions'; import { dynamicDashNavActions } from '../utils/registerDynamicDashNavAction'; import { DashboardScene } from './DashboardScene'; +import { GoToSnapshotOriginButton } from './GoToSnapshotOriginButton'; interface Props { dashboard: DashboardScene; @@ -89,14 +90,7 @@ export function ToolbarActions({ dashboard }: Props) { group: 'icon-actions', condition: meta.isSnapshot && !isEditing, render: () => ( - { - dashboard.onOpenSnapshotOriginalDashboard(); - }} - /> + ), }); From e6e9d6a782ac25b7967252815d25d5b8df91bb0c Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Tue, 13 Feb 2024 18:47:26 +0100 Subject: [PATCH 06/19] Changelog: Updated changelog for 9.5.16 (#82386) Co-authored-by: grafanabot --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dcc0430c51..1235a65552b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1789,6 +1789,15 @@ The `database` field has been deprecated in the Elasticsearch datasource provisi - **InteractiveTable:** Updated design and minor tweak to Correlactions page. [#66443](https://github.com/grafana/grafana/issues/66443), [@torkelo](https://github.com/torkelo) + + +# 9.5.16 (2024-01-29) + +### Bug fixes + +- **Annotations:** Split cleanup into separate queries and deletes to avoid deadlocks on MySQL. [#80682](https://github.com/grafana/grafana/issues/80682), [@alexweav](https://github.com/alexweav) + + # 9.5.15 (2023-12-18) From 28de94f6a2a4f232dd1f4dcf460d35065f4792af Mon Sep 17 00:00:00 2001 From: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> Date: Tue, 13 Feb 2024 19:47:46 +0200 Subject: [PATCH 07/19] Folders: Modify folder service Get() to optionally return fullpath (#81972) * Folders: Modify Get() to optionally return fullpath * Set FullPath to folder title if feature flag is off * Apply suggestion from code review --- pkg/services/folder/folderimpl/folder.go | 6 ++ pkg/services/folder/folderimpl/folder_test.go | 102 +++++++++++++++++- pkg/services/folder/folderimpl/sqlstore.go | 56 +++++++--- .../folder/folderimpl/sqlstore_test.go | 25 ++++- pkg/services/folder/model.go | 9 +- pkg/services/folder/service.go | 1 + 6 files changed, 176 insertions(+), 23 deletions(-) diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index 92d599091ac..79ad5718aa2 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -227,8 +227,10 @@ func (s *Service) Get(ctx context.Context, q *folder.GetFolderQuery) (*folder.Fo } if !s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) { + dashFolder.Fullpath = dashFolder.Title return dashFolder, nil } + metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Folder).Inc() // nolint:staticcheck if q.ID != nil { @@ -247,6 +249,10 @@ func (s *Service) Get(ctx context.Context, q *folder.GetFolderQuery) (*folder.Fo f.ID = dashFolder.ID f.Version = dashFolder.Version + if !s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) { + f.Fullpath = f.Title // set full path to the folder title (unescaped) + } + return f, err } diff --git a/pkg/services/folder/folderimpl/folder_test.go b/pkg/services/folder/folderimpl/folder_test.go index bb6c8f2c758..dfa61a2444e 100644 --- a/pkg/services/folder/folderimpl/folder_test.go +++ b/pkg/services/folder/folderimpl/folder_test.go @@ -1611,6 +1611,106 @@ func TestIntegrationNestedFolderSharedWithMe(t *testing.T) { }) } +func TestFolderServiceGetFolder(t *testing.T) { + db := sqlstore.InitTestDB(t) + + signedInAdminUser := user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{ + orgID: { + dashboards.ActionFoldersCreate: {}, + dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll}, + dashboards.ActionFoldersRead: {dashboards.ScopeFoldersAll}, + }, + }} + + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ + CanSaveValue: true, + CanViewValue: true, + }) + + getSvc := func(features featuremgmt.FeatureToggles) Service { + quotaService := quotatest.New(false, nil) + folderStore := ProvideDashboardFolderStore(db) + + cfg := setting.NewCfg() + + featuresFlagOff := featuremgmt.WithFeatures() + dashStore, err := database.ProvideDashboardStore(db, db.Cfg, featuresFlagOff, tagimpl.ProvideService(db), quotaService) + require.NoError(t, err) + nestedFolderStore := ProvideStore(db, db.Cfg) + + b := bus.ProvideBus(tracing.InitializeTracerForTest()) + ac := acimpl.ProvideAccessControl(cfg) + + return Service{ + cfg: cfg, + log: log.New("test-folder-service"), + dashboardStore: dashStore, + dashboardFolderStore: folderStore, + store: nestedFolderStore, + features: features, + bus: b, + db: db, + accessControl: ac, + registry: make(map[string]folder.RegistryService), + metrics: newFoldersMetrics(nil), + } + } + + folderSvcOn := getSvc(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)) + folderSvcOff := getSvc(featuremgmt.WithFeatures()) + + createCmd := folder.CreateFolderCommand{ + OrgID: orgID, + ParentUID: "", + SignedInUser: &signedInAdminUser, + } + + depth := 3 + folders := CreateSubtreeInStore(t, folderSvcOn.store, &folderSvcOn, depth, "get/folder-", createCmd) + f := folders[1] + + testCases := []struct { + name string + svc *Service + WithFullpath bool + expectedFullpath string + }{ + { + name: "when flag is off", + svc: &folderSvcOff, + expectedFullpath: f.Title, + }, + { + name: "when flag is on and WithFullpath is false", + svc: &folderSvcOn, + WithFullpath: false, + expectedFullpath: "", + }, + { + name: "when flag is on and WithFullpath is true", + svc: &folderSvcOn, + WithFullpath: true, + expectedFullpath: "get\\/folder-folder-0/get\\/folder-folder-1", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + q := folder.GetFolderQuery{ + OrgID: orgID, + UID: &f.UID, + WithFullpath: tc.WithFullpath, + SignedInUser: &signedInAdminUser, + } + fldr, err := tc.svc.Get(context.Background(), &q) + require.NoError(t, err) + require.Equal(t, f.UID, fldr.UID) + + require.Equal(t, tc.expectedFullpath, fldr.Fullpath) + }) + } +} + func TestFolderServiceGetFolders(t *testing.T) { db := sqlstore.InitTestDB(t) quotaService := quotatest.New(false, nil) @@ -1687,7 +1787,7 @@ func TestFolderServiceGetFolders(t *testing.T) { }) } -func CreateSubtreeInStore(t *testing.T, store *sqlStore, service *Service, depth int, prefix string, cmd folder.CreateFolderCommand) []*folder.Folder { +func CreateSubtreeInStore(t *testing.T, store store, service *Service, depth int, prefix string, cmd folder.CreateFolderCommand) []*folder.Folder { t.Helper() folders := make([]*folder.Folder, 0, depth) diff --git a/pkg/services/folder/folderimpl/sqlstore.go b/pkg/services/folder/folderimpl/sqlstore.go index fe758499557..7b09069eccc 100644 --- a/pkg/services/folder/folderimpl/sqlstore.go +++ b/pkg/services/folder/folderimpl/sqlstore.go @@ -171,27 +171,55 @@ func (ss *sqlStore) Update(ctx context.Context, cmd folder.UpdateFolderCommand) return foldr.WithURL(), err } +// If WithFullpath is true it computes also the full path of a folder. +// The full path is a string that contains the titles of all parent folders separated by a slash. +// For example, if the folder structure is: +// +// A +// └── B +// └── C +// +// The full path of C is "A/B/C". +// The full path of B is "A/B". +// The full path of A is "A". +// If a folder contains a slash in its title, it is escaped with a backslash. +// For example, if the folder structure is: +// +// A +// └── B/C +// +// The full path of C is "A/B\/C". func (ss *sqlStore) Get(ctx context.Context, q folder.GetFolderQuery) (*folder.Folder, error) { foldr := &folder.Folder{} err := ss.db.WithDbSession(ctx, func(sess *db.Session) error { exists := false var err error + s := strings.Builder{} + s.WriteString("SELECT *") + if q.WithFullpath { + s.WriteString(fmt.Sprintf(`, %s AS fullpath`, getFullpathSQL(ss.db.GetDialect()))) + } + s.WriteString(" FROM folder f0") + if q.WithFullpath { + s.WriteString(getFullpathJoinsSQL()) + } switch { case q.UID != nil: - exists, err = sess.SQL("SELECT * FROM folder WHERE uid = ? AND org_id = ?", q.UID, q.OrgID).Get(foldr) + s.WriteString(" WHERE f0.uid = ? AND f0.org_id = ?") + exists, err = sess.SQL(s.String(), q.UID, q.OrgID).Get(foldr) // nolint:staticcheck case q.ID != nil: + s.WriteString(" WHERE f0.id = ?") metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Folder).Inc() - exists, err = sess.SQL("SELECT * FROM folder WHERE id = ?", q.ID).Get(foldr) + exists, err = sess.SQL(s.String(), q.ID).Get(foldr) case q.Title != nil: - s := strings.Builder{} - s.WriteString("SELECT * FROM folder WHERE title = ? AND org_id = ?") + s.WriteString(" WHERE f0.title = ? AND f0.org_id = ?") args := []any{*q.Title, q.OrgID} if q.ParentUID != nil { - s.WriteString(" AND parent_uid = ?") + s.WriteString(" AND f0.parent_uid = ?") args = append(args, *q.ParentUID) } else { - s.WriteString(" AND parent_uid IS NULL") + s.WriteString(" AND f0.parent_uid IS NULL") } exists, err = sess.SQL(s.String(), args...).Get(foldr) default: @@ -207,6 +235,7 @@ func (ss *sqlStore) Get(ctx context.Context, q folder.GetFolderQuery) (*folder.F return nil }) + foldr.Fullpath = strings.TrimLeft(foldr.Fullpath, "/") return foldr.WithURL(), err } @@ -274,15 +303,16 @@ func (ss *sqlStore) GetChildren(ctx context.Context, q folder.GetChildrenQuery) args = append(args, q.UID, q.OrgID) } - if q.FolderUIDs != nil { - sql.WriteString(" AND uid IN (?") - for range q.FolderUIDs[1:] { - sql.WriteString(", ?") - } - sql.WriteString(")") - for _, uid := range q.FolderUIDs { + if len(q.FolderUIDs) > 0 { + sql.WriteString(" AND uid IN (") + for i, uid := range q.FolderUIDs { + if i > 0 { + sql.WriteString(", ") + } + sql.WriteString("?") args = append(args, uid) } + sql.WriteString(")") } sql.WriteString(" ORDER BY title ASC") diff --git a/pkg/services/folder/folderimpl/sqlstore_test.go b/pkg/services/folder/folderimpl/sqlstore_test.go index 59e354b3e8a..08df34d0568 100644 --- a/pkg/services/folder/folderimpl/sqlstore_test.go +++ b/pkg/services/folder/folderimpl/sqlstore_test.go @@ -3,6 +3,7 @@ package folderimpl import ( "context" "fmt" + "path" "slices" "sort" "testing" @@ -391,11 +392,7 @@ func TestIntegrationGet(t *testing.T) { UID: util.GenerateShortUID(), ParentUID: f.UID, }) - - t.Cleanup(func() { - err := folderStore.Delete(context.Background(), []string{f.UID}, orgID) - require.NoError(t, err) - }) + require.NoError(t, err) t.Run("should gently fail in case of bad request", func(t *testing.T) { _, err = folderStore.Get(context.Background(), folder.GetFolderQuery{}) @@ -466,6 +463,24 @@ func TestIntegrationGet(t *testing.T) { assert.NotEmpty(t, ff.Updated) assert.NotEmpty(t, ff.URL) }) + + t.Run("get folder with fullpath should set fullpath as expected", func(t *testing.T) { + ff, err := folderStore.Get(context.Background(), folder.GetFolderQuery{ + UID: &subfolderWithSameName.UID, + OrgID: orgID, + WithFullpath: true, + }) + require.NoError(t, err) + assert.Equal(t, subfolderWithSameName.UID, ff.UID) + assert.Equal(t, subfolderWithSameName.OrgID, ff.OrgID) + assert.Equal(t, subfolderWithSameName.Title, ff.Title) + assert.Equal(t, subfolderWithSameName.Description, ff.Description) + assert.Equal(t, path.Join(f.Title, subfolderWithSameName.Title), ff.Fullpath) + assert.Equal(t, f.UID, ff.ParentUID) + assert.NotEmpty(t, ff.Created) + assert.NotEmpty(t, ff.Updated) + assert.NotEmpty(t, ff.URL) + }) } func TestIntegrationGetParents(t *testing.T) { diff --git a/pkg/services/folder/model.go b/pkg/services/folder/model.go index 09f8e0cafaa..e111e90fb3b 100644 --- a/pkg/services/folder/model.go +++ b/pkg/services/folder/model.go @@ -147,10 +147,11 @@ type DeleteFolderCommand struct { type GetFolderQuery struct { UID *string // Deprecated: use FolderUID instead - ID *int64 - Title *string - ParentUID *string - OrgID int64 + ID *int64 + Title *string + ParentUID *string + OrgID int64 + WithFullpath bool SignedInUser identity.Requester `json:"-"` } diff --git a/pkg/services/folder/service.go b/pkg/services/folder/service.go index a7d09a8393d..50a1bec3391 100644 --- a/pkg/services/folder/service.go +++ b/pkg/services/folder/service.go @@ -18,6 +18,7 @@ type Service interface { // specificity (UID, ID, Title). // When fetching a folder by Title, callers can optionally define a ParentUID. // If ParentUID is not set then the folder will be fetched from the root level. + // If WithFullpath is true it computes also the full path of a folder. Get(ctx context.Context, q *GetFolderQuery) (*Folder, error) // Update is used to update a folder's UID, Title and Description. To change From f2ac24f9a044805c936aa407314345d5e3ce1ff5 Mon Sep 17 00:00:00 2001 From: Lucy Chen <140550297+lucychen-grafana@users.noreply.github.com> Date: Tue, 13 Feb 2024 13:08:49 -0500 Subject: [PATCH 08/19] Documentation: Incorrect API example for Public Dashboard (#82335) fix doc --- docs/sources/developers/http_api/dashboard_public.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/developers/http_api/dashboard_public.md b/docs/sources/developers/http_api/dashboard_public.md index 6093ac42341..d0f1980c6b0 100644 --- a/docs/sources/developers/http_api/dashboard_public.md +++ b/docs/sources/developers/http_api/dashboard_public.md @@ -207,7 +207,7 @@ See note in the [introduction](#public-dashboard-api) for an explanation. **Example Request**: ```http -GET /api/dashboards/uid/xCpsVuc4z HTTP/1.1 +GET /api/dashboards/uid/xCpsVuc4z/public-dashboards/ HTTP/1.1 Accept: application/json Content-Type: application/json Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk From 558dc74b4d89c956fb8537288e3fca0f1633701a Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Tue, 13 Feb 2024 19:10:06 +0000 Subject: [PATCH 09/19] Changelog: Updated changelog for 10.0.11 (#82392) Co-authored-by: grafanabot --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1235a65552b..17ebe4eca82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1257,6 +1257,15 @@ Starting with 10.0, changing the folder UID is deprecated. It will be removed in - **Grafana/ui:** Fix margin in RadioButtonGroup option when only icon is present. [#68899](https://github.com/grafana/grafana/issues/68899), [@aocenas](https://github.com/aocenas) + + +# 10.0.11 (2024-01-29) + +### Bug fixes + +- **Annotations:** Split cleanup into separate queries and deletes to avoid deadlocks on MySQL. [#80681](https://github.com/grafana/grafana/issues/80681), [@alexweav](https://github.com/alexweav) + + # 10.0.10 (2023-12-18) From 63670b7adc5e9062a0cf96632f729c2a8cf7f26d Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Tue, 13 Feb 2024 21:19:13 +0100 Subject: [PATCH 10/19] Chore: Use jsoniter in prometheus from grafana-plugin-sdk-go (#82359) use jsoniter from grafana-plugin-sdk-go --- pkg/util/converter/jsonitere/jsonitere.go | 66 ----------------------- pkg/util/converter/prom.go | 56 +++++++++---------- 2 files changed, 28 insertions(+), 94 deletions(-) delete mode 100644 pkg/util/converter/jsonitere/jsonitere.go diff --git a/pkg/util/converter/jsonitere/jsonitere.go b/pkg/util/converter/jsonitere/jsonitere.go deleted file mode 100644 index 4bc0c2ed55f..00000000000 --- a/pkg/util/converter/jsonitere/jsonitere.go +++ /dev/null @@ -1,66 +0,0 @@ -// Package jsonitere wraps json-iterator/go's Iterator methods with error returns -// so linting can catch unchecked errors. -// The underlying iterator's Error property is returned and not reset. -// See json-iterator/go for method documentation and additional methods that -// can be added to this library. -package jsonitere - -import ( - j "github.com/json-iterator/go" -) - -type Iterator struct { - // named property instead of embedded so there is no - // confusion about which method or property is called - i *j.Iterator -} - -func NewIterator(i *j.Iterator) *Iterator { - return &Iterator{i} -} - -func (iter *Iterator) Read() (any, error) { - return iter.i.Read(), iter.i.Error -} - -func (iter *Iterator) ReadAny() (j.Any, error) { - return iter.i.ReadAny(), iter.i.Error -} - -func (iter *Iterator) ReadArray() (bool, error) { - return iter.i.ReadArray(), iter.i.Error -} - -func (iter *Iterator) ReadObject() (string, error) { - return iter.i.ReadObject(), iter.i.Error -} - -func (iter *Iterator) ReadString() (string, error) { - return iter.i.ReadString(), iter.i.Error -} - -func (iter *Iterator) WhatIsNext() (j.ValueType, error) { - return iter.i.WhatIsNext(), iter.i.Error -} - -func (iter *Iterator) Skip() error { - iter.i.Skip() - return iter.i.Error -} - -func (iter *Iterator) SkipAndReturnBytes() []byte { - return iter.i.SkipAndReturnBytes() -} - -func (iter *Iterator) ReadVal(obj any) error { - iter.i.ReadVal(obj) - return iter.i.Error -} - -func (iter *Iterator) ReadFloat64() (float64, error) { - return iter.i.ReadFloat64(), iter.i.Error -} - -func (iter *Iterator) ReadInt8() (int8, error) { - return iter.i.ReadInt8(), iter.i.Error -} diff --git a/pkg/util/converter/prom.go b/pkg/util/converter/prom.go index 60215e5cff9..7b5cd6b95f6 100644 --- a/pkg/util/converter/prom.go +++ b/pkg/util/converter/prom.go @@ -8,15 +8,15 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" + sdkjsoniter "github.com/grafana/grafana-plugin-sdk-go/data/utils/jsoniter" jsoniter "github.com/json-iterator/go" - "golang.org/x/exp/slices" - "github.com/grafana/grafana/pkg/util/converter/jsonitere" + "golang.org/x/exp/slices" ) // helpful while debugging all the options that may appear func logf(format string, a ...any) { - //fmt.Printf(format, a...) + // fmt.Printf(format, a...) } type Options struct { @@ -29,7 +29,7 @@ func rspErr(e error) backend.DataResponse { // ReadPrometheusStyleResult will read results from a prometheus or loki server and return data frames func ReadPrometheusStyleResult(jIter *jsoniter.Iterator, opt Options) backend.DataResponse { - iter := jsonitere.NewIterator(jIter) + iter := sdkjsoniter.NewIterator(jIter) var rsp backend.DataResponse status := "unknown" errorType := "" @@ -102,14 +102,14 @@ l1Fields: return rsp } -func readWarnings(iter *jsonitere.Iterator) ([]data.Notice, error) { +func readWarnings(iter *sdkjsoniter.Iterator) ([]data.Notice, error) { warnings := []data.Notice{} next, err := iter.WhatIsNext() if err != nil { return nil, err } - if next != jsoniter.ArrayValue { + if next != sdkjsoniter.ArrayValue { return warnings, nil } @@ -121,7 +121,7 @@ func readWarnings(iter *jsonitere.Iterator) ([]data.Notice, error) { if err != nil { return nil, err } - if next == jsoniter.StringValue { + if next == sdkjsoniter.StringValue { s, err := iter.ReadString() if err != nil { return nil, err @@ -137,18 +137,18 @@ func readWarnings(iter *jsonitere.Iterator) ([]data.Notice, error) { return warnings, nil } -func readPrometheusData(iter *jsonitere.Iterator, opt Options) backend.DataResponse { +func readPrometheusData(iter *sdkjsoniter.Iterator, opt Options) backend.DataResponse { var rsp backend.DataResponse t, err := iter.WhatIsNext() if err != nil { return rspErr(err) } - if t == jsoniter.ArrayValue { + if t == sdkjsoniter.ArrayValue { return readArrayData(iter) } - if t != jsoniter.ObjectValue { + if t != sdkjsoniter.ObjectValue { return backend.DataResponse{ Error: fmt.Errorf("expected object type"), } @@ -190,7 +190,7 @@ l1Fields: // if we have saved resultBytes we will parse them here // we saved them because when we had them we don't know the resultType if len(resultBytes) > 0 { - ji := jsonitere.NewIterator(jsoniter.ParseBytes(jsoniter.ConfigDefault, resultBytes)) + ji := sdkjsoniter.NewIterator(jsoniter.ParseBytes(sdkjsoniter.ConfigDefault, resultBytes)) rsp = readResult(resultType, rsp, ji, opt, encodingFlags) } case "result": @@ -200,7 +200,7 @@ l1Fields: if resultTypeFound { rsp = readResult(resultType, rsp, iter, opt, encodingFlags) } else { - resultBytes = iter.SkipAndReturnBytes() + resultBytes, _ = iter.SkipAndReturnBytes() } case "stats": @@ -241,7 +241,7 @@ l1Fields: } // will read the result object based on the resultType and return a DataResponse -func readResult(resultType string, rsp backend.DataResponse, iter *jsonitere.Iterator, opt Options, encodingFlags []string) backend.DataResponse { +func readResult(resultType string, rsp backend.DataResponse, iter *sdkjsoniter.Iterator, opt Options, encodingFlags []string) backend.DataResponse { switch resultType { case "matrix", "vector": rsp = readMatrixOrVectorMulti(iter, resultType, opt) @@ -279,7 +279,7 @@ func readResult(resultType string, rsp backend.DataResponse, iter *jsonitere.Ite } // will return strings or exemplars -func readArrayData(iter *jsonitere.Iterator) backend.DataResponse { +func readArrayData(iter *sdkjsoniter.Iterator) backend.DataResponse { lookup := make(map[string]*data.Field) var labelFrame *data.Frame @@ -298,7 +298,7 @@ func readArrayData(iter *jsonitere.Iterator) backend.DataResponse { } switch next { - case jsoniter.StringValue: + case sdkjsoniter.StringValue: s, err := iter.ReadString() if err != nil { return rspErr(err) @@ -306,7 +306,7 @@ func readArrayData(iter *jsonitere.Iterator) backend.DataResponse { stringField.Append(s) // Either label or exemplars - case jsoniter.ObjectValue: + case sdkjsoniter.ObjectValue: exemplar, labelPairs, err := readLabelsOrExemplars(iter) if err != nil { rspErr(err) @@ -365,7 +365,7 @@ func readArrayData(iter *jsonitere.Iterator) backend.DataResponse { } // For consistent ordering read values to an array not a map -func readLabelsAsPairs(iter *jsonitere.Iterator) ([][2]string, error) { +func readLabelsAsPairs(iter *sdkjsoniter.Iterator) ([][2]string, error) { pairs := make([][2]string, 0, 10) for k, err := iter.ReadObject(); k != ""; k, err = iter.ReadObject() { if err != nil { @@ -380,7 +380,7 @@ func readLabelsAsPairs(iter *jsonitere.Iterator) ([][2]string, error) { return pairs, nil } -func readLabelsOrExemplars(iter *jsonitere.Iterator) (*data.Frame, [][2]string, error) { +func readLabelsOrExemplars(iter *sdkjsoniter.Iterator) (*data.Frame, [][2]string, error) { pairs := make([][2]string, 0, 10) labels := data.Labels{} var frame *data.Frame @@ -496,7 +496,7 @@ l1Fields: return frame, pairs, nil } -func readString(iter *jsonitere.Iterator) backend.DataResponse { +func readString(iter *sdkjsoniter.Iterator) backend.DataResponse { timeField := data.NewFieldFromFieldType(data.FieldTypeTime, 0) timeField.Name = data.TimeSeriesTimeFieldName valueField := data.NewFieldFromFieldType(data.FieldTypeString, 0) @@ -541,7 +541,7 @@ func readString(iter *jsonitere.Iterator) backend.DataResponse { } } -func readScalar(iter *jsonitere.Iterator, dataPlane bool) backend.DataResponse { +func readScalar(iter *sdkjsoniter.Iterator, dataPlane bool) backend.DataResponse { rsp := backend.DataResponse{} timeField := data.NewFieldFromFieldType(data.FieldTypeTime, 0) @@ -573,7 +573,7 @@ func readScalar(iter *jsonitere.Iterator, dataPlane bool) backend.DataResponse { } } -func readMatrixOrVectorMulti(iter *jsonitere.Iterator, resultType string, opt Options) backend.DataResponse { +func readMatrixOrVectorMulti(iter *sdkjsoniter.Iterator, resultType string, opt Options) backend.DataResponse { rsp := backend.DataResponse{} for more, err := iter.ReadArray(); more; more, err = iter.ReadArray() { @@ -679,7 +679,7 @@ func readMatrixOrVectorMulti(iter *jsonitere.Iterator, resultType string, opt Op return rsp } -func readTimeValuePair(iter *jsonitere.Iterator) (time.Time, float64, error) { +func readTimeValuePair(iter *sdkjsoniter.Iterator) (time.Time, float64, error) { if _, err := iter.ReadArray(); err != nil { return time.Time{}, 0, err } @@ -708,7 +708,7 @@ func readTimeValuePair(iter *jsonitere.Iterator) (time.Time, float64, error) { } type histogramInfo struct { - //XMax (time) YMin Ymax Count YLayout + // XMax (time) YMin Ymax Count YLayout time *data.Field yMin *data.Field // will have labels? yMax *data.Field @@ -734,7 +734,7 @@ func newHistogramInfo() *histogramInfo { // This will read a single sparse histogram // [ time, { count, sum, buckets: [...] }] -func readHistogram(iter *jsonitere.Iterator, hist *histogramInfo) error { +func readHistogram(iter *sdkjsoniter.Iterator, hist *histogramInfo) error { // first element if _, err := iter.ReadArray(); err != nil { return err @@ -834,7 +834,7 @@ func readHistogram(iter *jsonitere.Iterator, hist *histogramInfo) error { return nil } -func appendValueFromString(iter *jsonitere.Iterator, field *data.Field) error { +func appendValueFromString(iter *sdkjsoniter.Iterator, field *data.Field) error { var err error var s string if s, err = iter.ReadString(); err != nil { @@ -850,7 +850,7 @@ func appendValueFromString(iter *jsonitere.Iterator, field *data.Field) error { return nil } -func readStream(iter *jsonitere.Iterator) backend.DataResponse { +func readStream(iter *sdkjsoniter.Iterator) backend.DataResponse { rsp := backend.DataResponse{} labelsField := data.NewFieldFromFieldType(data.FieldTypeJSON, 0) @@ -950,7 +950,7 @@ func readStream(iter *jsonitere.Iterator) backend.DataResponse { return rsp } -func readCategorizedStream(iter *jsonitere.Iterator) backend.DataResponse { +func readCategorizedStream(iter *sdkjsoniter.Iterator) backend.DataResponse { rsp := backend.DataResponse{} labelsField := data.NewFieldFromFieldType(data.FieldTypeJSON, 0) @@ -1084,7 +1084,7 @@ func readCategorizedStream(iter *jsonitere.Iterator) backend.DataResponse { return rsp } -func readCategorizedStreamField(iter *jsonitere.Iterator) (map[string]interface{}, map[string]interface{}, error) { +func readCategorizedStreamField(iter *sdkjsoniter.Iterator) (map[string]interface{}, map[string]interface{}, error) { parsedLabels := data.Labels{} structuredMetadata := data.Labels{} var parsedLabelsMap map[string]interface{} From 3d86d101b7d4737d44b432911689356b392d2827 Mon Sep 17 00:00:00 2001 From: Ivan Ortega Alba Date: Tue, 13 Feb 2024 21:38:26 +0100 Subject: [PATCH 11/19] Dashboards: Use `auto` and only use `AdHocFiltersVariable` to manage filters (#81318) --- package.json | 2 +- .../src/selectors/pages.ts | 4 + .../scene/setDashboardPanelContext.test.ts | 20 +-- .../scene/setDashboardPanelContext.ts | 46 ++++--- .../transformSceneToSaveModel.test.ts.snap | 36 +++--- .../sceneVariablesSetToVariables.test.ts | 57 ++++++++ .../sceneVariablesSetToVariables.ts | 10 ++ .../transformSaveModelToScene.test.ts | 69 +++++++++- .../transformSaveModelToScene.ts | 33 +++-- .../transformSceneToSaveModel.ts | 12 -- .../settings/VariablesEditView.test.tsx | 47 +++++-- .../settings/VariablesEditView.tsx | 34 ++--- .../components/AdHocVariableForm.test.tsx | 75 +++++++++++ .../components/AdHocVariableForm.tsx | 34 +++++ .../AdHocFiltersVariableEditor.test.tsx | 122 ++++++++++++++++++ .../editors/AdHocFiltersVariableEditor.tsx | 32 ++++- .../settings/variables/utils.test.ts | 6 +- .../settings/variables/utils.ts | 5 +- .../utils/getVariablesCompatibility.ts | 2 +- .../ActionTabs/AddToFiltersGraphAction.tsx | 4 +- .../app/features/trails/ActionTabs/utils.ts | 2 +- public/app/features/trails/DataTrail.tsx | 4 +- public/app/features/trails/DataTrailCard.tsx | 2 +- .../trails/TrailStore/TrailStore.test.ts | 7 + .../adhoc/AdHocVariableEditor.test.tsx | 90 ------------- .../variables/adhoc/AdHocVariableEditor.tsx | 24 +--- yarn.lock | 10 +- 27 files changed, 538 insertions(+), 251 deletions(-) create mode 100644 public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.test.tsx create mode 100644 public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.tsx create mode 100644 public/app/features/dashboard-scene/settings/variables/editors/AdHocFiltersVariableEditor.test.tsx delete mode 100644 public/app/features/variables/adhoc/AdHocVariableEditor.test.tsx diff --git a/package.json b/package.json index 49663e1c2d5..ccac6bcdd89 100644 --- a/package.json +++ b/package.json @@ -246,7 +246,7 @@ "@grafana/o11y-ds-frontend": "workspace:*", "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", - "@grafana/scenes": "^2.6.5", + "@grafana/scenes": "^3.2.1", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index 4acc843136e..f456dc70576 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -182,6 +182,10 @@ export const Pages = { stepCountIntervalSelect: 'data-testid interval variable step count input', minIntervalInput: 'data-testid interval variable mininum interval input', }, + AdHocFiltersVariable: { + datasourceSelect: Components.DataSourcePicker.inputV2, + infoText: 'data-testid ad-hoc filters variable info text', + }, }, }, }, diff --git a/public/app/features/dashboard-scene/scene/setDashboardPanelContext.test.ts b/public/app/features/dashboard-scene/scene/setDashboardPanelContext.test.ts index 149dce0d119..e0d543c73e2 100644 --- a/public/app/features/dashboard-scene/scene/setDashboardPanelContext.test.ts +++ b/public/app/features/dashboard-scene/scene/setDashboardPanelContext.test.ts @@ -5,7 +5,7 @@ import { PanelContext } from '@grafana/ui'; import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; import { findVizPanelByKey } from '../utils/utils'; -import { getAdHocFilterSetFor, setDashboardPanelContext } from './setDashboardPanelContext'; +import { getAdHocFilterVariableFor, setDashboardPanelContext } from './setDashboardPanelContext'; const postFn = jest.fn(); const putFn = jest.fn(); @@ -132,26 +132,26 @@ describe('setDashboardPanelContext', () => { context.onAddAdHocFilter!({ key: 'hello', value: 'world', operator: '=' }); - const set = getAdHocFilterSetFor(scene, { uid: 'my-ds-uid' }); + const variable = getAdHocFilterVariableFor(scene, { uid: 'my-ds-uid' }); - expect(set.state.filters).toEqual([{ key: 'hello', value: 'world', operator: '=' }]); + expect(variable.state.filters).toEqual([{ key: 'hello', value: 'world', operator: '=' }]); }); it('Should update and add filter to existing set', () => { - const { scene, context } = buildTestScene({ existingFilterSet: true }); + const { scene, context } = buildTestScene({ existingFilterVariable: true }); - const set = getAdHocFilterSetFor(scene, { uid: 'my-ds-uid' }); + const variable = getAdHocFilterVariableFor(scene, { uid: 'my-ds-uid' }); - set.setState({ filters: [{ key: 'existing', value: 'world', operator: '=' }] }); + variable.setState({ filters: [{ key: 'existing', value: 'world', operator: '=' }] }); context.onAddAdHocFilter!({ key: 'hello', value: 'world', operator: '=' }); - expect(set.state.filters.length).toBe(2); + expect(variable.state.filters.length).toBe(2); // Can update existing filter value without adding a new filter context.onAddAdHocFilter!({ key: 'hello', value: 'world2', operator: '=' }); // Verify existing filter value updated - expect(set.state.filters[1].value).toBe('world2'); + expect(variable.state.filters[1].value).toBe('world2'); }); }); }); @@ -163,7 +163,7 @@ interface SceneOptions { canEdit?: boolean; canDelete?: boolean; orgCanEdit?: boolean; - existingFilterSet?: boolean; + existingFilterVariable?: boolean; } function buildTestScene(options: SceneOptions) { @@ -198,7 +198,7 @@ function buildTestScene(options: SceneOptions) { }, ], templating: { - list: options.existingFilterSet + list: options.existingFilterVariable ? [ { type: 'adhoc', diff --git a/public/app/features/dashboard-scene/scene/setDashboardPanelContext.ts b/public/app/features/dashboard-scene/scene/setDashboardPanelContext.ts index 5b988c550a8..bb10455fd72 100644 --- a/public/app/features/dashboard-scene/scene/setDashboardPanelContext.ts +++ b/public/app/features/dashboard-scene/scene/setDashboardPanelContext.ts @@ -1,5 +1,5 @@ import { AnnotationChangeEvent, AnnotationEventUIModel, CoreApp, DataFrame } from '@grafana/data'; -import { AdHocFilterSet, dataLayers, SceneDataLayers, VizPanel } from '@grafana/scenes'; +import { AdHocFiltersVariable, dataLayers, SceneDataLayers, sceneGraph, sceneUtils, VizPanel } from '@grafana/scenes'; import { DataSourceRef } from '@grafana/schema'; import { AdHocFilterItem, PanelContext } from '@grafana/ui'; import { deleteAnnotation, saveAnnotation, updateAnnotation } from 'app/features/annotations/api'; @@ -111,8 +111,8 @@ export function setDashboardPanelContext(vizPanel: VizPanel, context: PanelConte return; } - const filterSet = getAdHocFilterSetFor(dashboard, queryRunner.state.datasource); - updateAdHocFilterSet(filterSet, newFilter); + const filterVar = getAdHocFilterVariableFor(dashboard, queryRunner.state.datasource); + updateAdHocFilterVariable(filterVar, newFilter); }; context.onUpdateData = (frames: DataFrame[]): Promise => { @@ -149,33 +149,37 @@ function reRunBuiltInAnnotationsLayer(scene: DashboardScene) { } } -export function getAdHocFilterSetFor(scene: DashboardScene, ds: DataSourceRef | null | undefined) { - const controls = scene.state.controls ?? []; +export function getAdHocFilterVariableFor(scene: DashboardScene, ds: DataSourceRef | null | undefined) { + const variables = sceneGraph.getVariables(scene); - for (const control of controls) { - if (control instanceof AdHocFilterSet) { - if (control.state.datasource === ds || control.state.datasource?.uid === ds?.uid) { - return control; + for (const variable of variables.state.variables) { + if (sceneUtils.isAdHocVariable(variable)) { + const filtersDs = variable.state.datasource; + if (filtersDs === ds || filtersDs?.uid === ds?.uid) { + return variable; } } } - const newSet = new AdHocFilterSet({ datasource: ds }); - - // Add it to the scene - scene.setState({ - controls: [controls[0], newSet, ...controls.slice(1)], + const newVariable = new AdHocFiltersVariable({ + name: 'Filters', + datasource: ds, }); - return newSet; + // Add it to the scene + variables.setState({ + variables: [...variables.state.variables, newVariable], + }); + + return newVariable; } -function updateAdHocFilterSet(filterSet: AdHocFilterSet, newFilter: AdHocFilterItem) { +function updateAdHocFilterVariable(filterVar: AdHocFiltersVariable, newFilter: AdHocFilterItem) { // Check if we need to update an existing filter - for (const filter of filterSet.state.filters) { + for (const filter of filterVar.state.filters) { if (filter.key === newFilter.key) { - filterSet.setState({ - filters: filterSet.state.filters.map((f) => { + filterVar.setState({ + filters: filterVar.state.filters.map((f) => { if (f.key === newFilter.key) { return newFilter; } @@ -187,7 +191,7 @@ function updateAdHocFilterSet(filterSet: AdHocFilterSet, newFilter: AdHocFilterI } // Add new filter - filterSet.setState({ - filters: [...filterSet.state.filters, newFilter], + filterVar.setState({ + filters: [...filterVar.state.filters, newFilter], }); } diff --git a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap index 2dcb29ae4db..f43d073b626 100644 --- a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap +++ b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap @@ -455,6 +455,16 @@ exports[`transformSceneToSaveModel Given a simple scene with custom settings Sho ], "templating": { "list": [ + { + "baseFilters": [], + "datasource": { + "type": "prometheus", + "uid": "wc2AL7L7k", + }, + "filters": [], + "name": "Filters", + "type": "adhoc", + }, { "auto": true, "auto_count": 30, @@ -525,14 +535,6 @@ exports[`transformSceneToSaveModel Given a simple scene with custom settings Sho "skipUrlSync": true, "type": "constant", }, - { - "datasource": { - "type": "prometheus", - "uid": "wc2AL7L7k", - }, - "name": "Filters", - "type": "adhoc", - }, ], }, "time": { @@ -757,6 +759,16 @@ exports[`transformSceneToSaveModel Given a simple scene with variables Should tr ], "templating": { "list": [ + { + "baseFilters": [], + "datasource": { + "type": "prometheus", + "uid": "wc2AL7L7k", + }, + "filters": [], + "name": "Filters", + "type": "adhoc", + }, { "auto": true, "auto_count": 30, @@ -827,14 +839,6 @@ exports[`transformSceneToSaveModel Given a simple scene with variables Should tr "skipUrlSync": true, "type": "constant", }, - { - "datasource": { - "type": "prometheus", - "uid": "wc2AL7L7k", - }, - "name": "Filters", - "type": "adhoc", - }, ], }, "time": { diff --git a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts index 112e75b419d..3bb0cc046cc 100644 --- a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts +++ b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts @@ -13,6 +13,7 @@ import { } from '@grafana/data'; import { setRunRequest } from '@grafana/runtime'; import { + AdHocFiltersVariable, ConstantVariable, CustomVariable, DataSourceVariable, @@ -344,4 +345,60 @@ describe('sceneVariablesSetToVariables', () => { } `); }); + + it('should handle AdHocFiltersVariable', () => { + const variable = new AdHocFiltersVariable({ + name: 'test', + label: 'test-label', + description: 'test-desc', + datasource: { uid: 'fake-std', type: 'fake-std' }, + filters: [ + { + key: 'filterTest', + operator: '=', + value: 'test', + }, + ], + baseFilters: [ + { + key: 'baseFilterTest', + operator: '=', + value: 'test', + }, + ], + }); + const set = new SceneVariableSet({ + variables: [variable], + }); + + const result = sceneVariablesSetToVariables(set); + + expect(result).toHaveLength(1); + expect(result[0]).toMatchInlineSnapshot(` + { + "baseFilters": [ + { + "key": "baseFilterTest", + "operator": "=", + "value": "test", + }, + ], + "datasource": { + "type": "fake-std", + "uid": "fake-std", + }, + "description": "test-desc", + "filters": [ + { + "key": "filterTest", + "operator": "=", + "value": "test", + }, + ], + "label": "test-label", + "name": "test", + "type": "adhoc", + } + `); + }); }); diff --git a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts index a2145c75777..928afaa5e9b 100644 --- a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts +++ b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts @@ -104,6 +104,16 @@ export function sceneVariablesSetToVariables(set: SceneVariables) { }, query: variable.state.value, }); + } else if (sceneUtils.isAdHocVariable(variable)) { + variables.push({ + ...commonProperties, + name: variable.state.name!, + type: 'adhoc', + datasource: variable.state.datasource, + // @ts-expect-error + baseFilters: variable.state.baseFilters, + filters: variable.state.filters, + }); } else { throw new Error('Unsupported variable type'); } diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts index 8e288c8b429..1a6f59fa4d7 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts @@ -11,8 +11,9 @@ import { import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; import { config } from '@grafana/runtime'; import { - AdHocFilterSet, + AdHocFiltersVariable, behaviors, + ConstantVariable, CustomVariable, DataSourceVariable, QueryVariable, @@ -120,11 +121,11 @@ describe('transformSaveModelToScene', () => { expect(scene.state?.$timeRange?.state.timeZone).toEqual('America/New_York'); expect(scene.state?.$timeRange?.state.weekStart).toEqual('saturday'); - expect(scene.state?.$variables?.state.variables).toHaveLength(1); + expect(scene.state?.$variables?.state.variables).toHaveLength(2); + expect(scene.state?.$variables?.getByName('constant')).toBeInstanceOf(ConstantVariable); + expect(scene.state?.$variables?.getByName('CoolFilters')).toBeInstanceOf(AdHocFiltersVariable); expect(dashboardControls).toBeDefined(); expect(dashboardControls).toBeInstanceOf(DashboardControls); - expect(dashboardControls.state.variableControls[1]).toBeInstanceOf(AdHocFilterSet); - expect((dashboardControls.state.variableControls[1] as AdHocFilterSet).state.name).toBe('CoolFilters'); expect(dashboardControls.state.timeControls).toHaveLength(2); expect(dashboardControls.state.timeControls[0]).toBeInstanceOf(SceneTimePicker); expect(dashboardControls.state.timeControls[1]).toBeInstanceOf(SceneRefreshPicker); @@ -789,6 +790,60 @@ describe('transformSaveModelToScene', () => { }); }); + it('should migrate adhoc variable', () => { + const variable: TypedVariableModel = { + id: 'adhoc', + global: false, + index: 0, + state: LoadingState.Done, + error: null, + name: 'adhoc', + label: 'Adhoc Label', + description: 'Adhoc Description', + type: 'adhoc', + rootStateKey: 'N4XLmH5Vz', + datasource: { + uid: 'gdev-prometheus', + type: 'prometheus', + }, + filters: [ + { + key: 'filterTest', + operator: '=', + value: 'test', + }, + ], + baseFilters: [ + { + key: 'baseFilterTest', + operator: '=', + value: 'test', + }, + ], + hide: 0, + skipUrlSync: false, + }; + + const migrated = createSceneVariableFromVariableModel(variable) as AdHocFiltersVariable; + const filterVarState = migrated.state; + + expect(migrated).toBeInstanceOf(AdHocFiltersVariable); + expect(filterVarState).toEqual({ + key: expect.any(String), + description: 'Adhoc Description', + hide: 0, + label: 'Adhoc Label', + name: 'adhoc', + skipUrlSync: false, + type: 'adhoc', + filterExpression: 'filterTest="test"', + filters: [{ key: 'filterTest', operator: '=', value: 'test' }], + baseFilters: [{ key: 'baseFilterTest', operator: '=', value: 'test' }], + datasource: { uid: 'gdev-prometheus', type: 'prometheus' }, + applyMode: 'auto', + }); + }); + it.each(['system'])('should throw for unsupported (yet) variables', (type) => { const variable = { name: 'query0', @@ -856,7 +911,7 @@ describe('transformSaveModelToScene', () => { const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} }); expect(scene.state.$data).toBeInstanceOf(SceneDataLayers); - expect((scene.state.controls![0] as DashboardControls)!.state.variableControls[2]).toBeInstanceOf( + expect((scene.state.controls![0] as DashboardControls)!.state.variableControls[1]).toBeInstanceOf( SceneDataLayerControls ); @@ -886,7 +941,7 @@ describe('transformSaveModelToScene', () => { const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} }); expect(scene.state.$data).toBeInstanceOf(SceneDataLayers); - expect((scene.state.controls![0] as DashboardControls)!.state.variableControls[2]).toBeInstanceOf( + expect((scene.state.controls![0] as DashboardControls)!.state.variableControls[1]).toBeInstanceOf( SceneDataLayerControls ); @@ -902,7 +957,7 @@ describe('transformSaveModelToScene', () => { const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} }); expect(scene.state.$data).toBeInstanceOf(SceneDataLayers); - expect((scene.state.controls![0] as DashboardControls)!.state.variableControls[2]).toBeInstanceOf( + expect((scene.state.controls![0] as DashboardControls)!.state.variableControls[1]).toBeInstanceOf( SceneDataLayerControls ); diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index b1dd16f8499..cb3d3795605 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -1,4 +1,4 @@ -import { AdHocVariableModel, TypedVariableModel, VariableModel } from '@grafana/data'; +import { TypedVariableModel } from '@grafana/data'; import { config } from '@grafana/runtime'; import { VizPanel, @@ -24,9 +24,9 @@ import { SceneDataLayers, SceneDataLayerProvider, SceneDataLayerControls, - AdHocFilterSet, TextBoxVariable, UserActionEvent, + AdHocFiltersVariable, } from '@grafana/scenes'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { trackDashboardLoaded } from 'app/features/dashboard/utils/tracking'; @@ -173,24 +173,11 @@ function createRowFromPanelModel(row: PanelModel, content: SceneGridItemLike[]): export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel) { let variables: SceneVariableSet | undefined = undefined; let layers: SceneDataLayerProvider[] = []; - let filtersSets: AdHocFilterSet[] = []; if (oldModel.templating?.list?.length) { const variableObjects = oldModel.templating.list .map((v) => { try { - if (isAdhocVariable(v)) { - filtersSets.push( - new AdHocFilterSet({ - name: v.name, - datasource: v.datasource, - filters: v.filters ?? [], - baseFilters: v.baseFilters ?? [], - }) - ); - return null; - } - return createSceneVariableFromVariableModel(v); } catch (err) { console.error(err); @@ -282,7 +269,7 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel) : undefined, controls: [ new DashboardControls({ - variableControls: [new VariableValueSelectors({}), ...filtersSets, new SceneDataLayerControls()], + variableControls: [new VariableValueSelectors({}), new SceneDataLayerControls()], timeControls: [ new SceneTimePicker({}), new SceneRefreshPicker({ @@ -305,6 +292,18 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode name: variable.name, label: variable.label, }; + if (variable.type === 'adhoc') { + return new AdHocFiltersVariable({ + ...commonProperties, + description: variable.description, + skipUrlSync: variable.skipUrlSync, + hide: variable.hide, + datasource: variable.datasource, + applyMode: 'auto', + filters: variable.filters ?? [], + baseFilters: variable.baseFilters ?? [], + }); + } if (variable.type === 'custom') { return new CustomVariable({ ...commonProperties, @@ -480,8 +479,6 @@ export function buildGridItemForPanel(panel: PanelModel): SceneGridItemLike { }); } -const isAdhocVariable = (v: VariableModel): v is AdHocVariableModel => v.type === 'adhoc'; - const getLimitedDescriptionReporter = () => { const reportedPanels: string[] = []; diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index 1b9e94cd8a3..84442df1153 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -11,7 +11,6 @@ import { VizPanel, SceneDataTransformer, SceneVariableSet, - AdHocFilterSet, LocalValueVariable, SceneRefreshPicker, } from '@grafana/scenes'; @@ -101,17 +100,6 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa refreshIntervals = control.state.intervals; } } - - const variableControls = state.controls[0].state.variableControls; - for (const control of variableControls) { - if (control instanceof AdHocFilterSet) { - variables.push({ - name: control.state.name!, - type: 'adhoc', - datasource: control.state.datasource, - }); - } - } } if (state.$behaviors) { diff --git a/public/app/features/dashboard-scene/settings/VariablesEditView.test.tsx b/public/app/features/dashboard-scene/settings/VariablesEditView.test.tsx index 0e71277c26a..8f67633d5a5 100644 --- a/public/app/features/dashboard-scene/settings/VariablesEditView.test.tsx +++ b/public/app/features/dashboard-scene/settings/VariablesEditView.test.tsx @@ -10,7 +10,14 @@ import { } from '@grafana/data'; import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; import { setPluginImportUtils, setRunRequest } from '@grafana/runtime'; -import { SceneVariableSet, CustomVariable, SceneGridItem, SceneGridLayout, VizPanel } from '@grafana/scenes'; +import { + SceneVariableSet, + CustomVariable, + SceneGridItem, + SceneGridLayout, + VizPanel, + AdHocFiltersVariable, +} from '@grafana/scenes'; import { mockDataSource } from 'app/features/alerting/unified/mocks'; import { LegacyVariableQueryEditor } from 'app/features/variables/editor/LegacyVariableQueryEditor'; @@ -97,11 +104,16 @@ describe('VariablesEditView', () => { query: 'test3, test4, $customVar', value: 'test3', }, + { + type: 'adhoc', + name: 'adhoc', + }, ]; const variables = variableView.getVariables(); - expect(variables).toHaveLength(2); + expect(variables).toHaveLength(3); expect(variables[0].state).toMatchObject(expectedVariables[0]); expect(variables[1].state).toMatchObject(expectedVariables[1]); + expect(variables[2].state).toMatchObject(expectedVariables[2]); }); }); @@ -117,7 +129,7 @@ describe('VariablesEditView', () => { const variables = variableView.getVariables(); const variable = variables[0]; variableView.onDuplicated(variable.state.name); - expect(variableView.getVariables()).toHaveLength(3); + expect(variableView.getVariables()).toHaveLength(4); expect(variableView.getVariables()[1].state.name).toBe('copy_of_customVar'); }); @@ -125,7 +137,7 @@ describe('VariablesEditView', () => { const variableIdentifier = 'customVar'; variableView.onDuplicated(variableIdentifier); variableView.onDuplicated(variableIdentifier); - expect(variableView.getVariables()).toHaveLength(4); + expect(variableView.getVariables()).toHaveLength(5); expect(variableView.getVariables()[1].state.name).toBe('copy_of_customVar_1'); expect(variableView.getVariables()[2].state.name).toBe('copy_of_customVar'); }); @@ -133,7 +145,7 @@ describe('VariablesEditView', () => { it('should delete a variable', () => { const variableIdentifier = 'customVar'; variableView.onDelete(variableIdentifier); - expect(variableView.getVariables()).toHaveLength(1); + expect(variableView.getVariables()).toHaveLength(2); expect(variableView.getVariables()[0].state.name).toBe('customVar2'); }); @@ -147,7 +159,7 @@ describe('VariablesEditView', () => { it('should keep the same order of variables with invalid indexes', () => { const fromIndex = 0; - const toIndex = 2; + const toIndex = 3; const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); @@ -163,11 +175,11 @@ describe('VariablesEditView', () => { const previousVariable = variableView.getVariables()[1] as CustomVariable; variableView.onEdit('customVar2'); - variableView.onTypeChange('constant'); - expect(variableView.getVariables()).toHaveLength(2); + variableView.onTypeChange('adhoc'); + expect(variableView.getVariables()).toHaveLength(3); const variable = variableView.getVariables()[1]; expect(variable).not.toBe(previousVariable); - expect(variable.state.type).toBe('constant'); + expect(variable.state.type).toBe('adhoc'); // Values to be kept between the old and new variable expect(variable.state.name).toEqual(previousVariable.state.name); @@ -184,9 +196,9 @@ describe('VariablesEditView', () => { it('should add default new query variable when onAdd is called', () => { variableView.onAdd(); - expect(variableView.getVariables()).toHaveLength(3); - expect(variableView.getVariables()[2].state.name).toBe('query0'); - expect(variableView.getVariables()[2].state.type).toBe('query'); + expect(variableView.getVariables()).toHaveLength(4); + expect(variableView.getVariables()[3].state.name).toBe('query0'); + expect(variableView.getVariables()[3].state.type).toBe('query'); }); afterEach(() => { @@ -266,6 +278,17 @@ async function buildTestScene() { value: '$customVar', text: '$customVar', }), + new AdHocFiltersVariable({ + type: 'adhoc', + name: 'adhoc', + filters: [ + { + key: 'test', + operator: '=', + value: 'testValue', + }, + ], + }), ], }), body: new SceneGridLayout({ diff --git a/public/app/features/dashboard-scene/settings/VariablesEditView.tsx b/public/app/features/dashboard-scene/settings/VariablesEditView.tsx index d574cbdd5c5..84fe03a8f93 100644 --- a/public/app/features/dashboard-scene/settings/VariablesEditView.tsx +++ b/public/app/features/dashboard-scene/settings/VariablesEditView.tsx @@ -1,14 +1,7 @@ import React from 'react'; import { NavModel, NavModelItem, PageLayoutType } from '@grafana/data'; -import { - SceneComponentProps, - SceneObjectBase, - SceneVariable, - SceneVariables, - sceneGraph, - AdHocFilterSet, -} from '@grafana/scenes'; +import { SceneComponentProps, SceneObjectBase, SceneVariable, SceneVariables, sceneGraph } from '@grafana/scenes'; import { Page } from 'app/core/components/Page/Page'; import { DashboardScene } from '../scene/DashboardScene'; @@ -46,7 +39,7 @@ export class VariablesEditView extends SceneObjectBase i return variables.findIndex((variable) => variable.state.name === identifier); }; - private replaceEditVariable = (newVariable: SceneVariable | AdHocFilterSet) => { + private replaceEditVariable = (newVariable: SceneVariable) => { // Find the index of the variable to be deleted const variableIndex = this.state.editIndex ?? -1; const { variables } = this.getVariableSet().state; @@ -58,18 +51,10 @@ export class VariablesEditView extends SceneObjectBase i return; } - if (newVariable instanceof AdHocFilterSet) { - // TODO: Update controls in adding this fiter set to the dashboard - } else { - const updatedVariables = [ - ...variables.slice(0, variableIndex), - newVariable, - ...variables.slice(variableIndex + 1), - ]; + const updatedVariables = [...variables.slice(0, variableIndex), newVariable, ...variables.slice(variableIndex + 1)]; - // Update the state or the variables array - this.getVariableSet().setState({ variables: updatedVariables }); - } + // Update the state or the variables array + this.getVariableSet().setState({ variables: updatedVariables }); }; public onDelete = (identifier: string) => { @@ -158,12 +143,9 @@ export class VariablesEditView extends SceneObjectBase i const variableIndex = variables.length; //add the new variable to the end of the array const defaultNewVariable = getVariableDefault(variables); - if (defaultNewVariable instanceof AdHocFilterSet) { - // TODO: Update controls in adding this fiter set to the dashboard - } else { - this.getVariableSet().setState({ variables: [...this.getVariables(), defaultNewVariable] }); - this.setState({ editIndex: variableIndex }); - } + + this.getVariableSet().setState({ variables: [...this.getVariables(), defaultNewVariable] }); + this.setState({ editIndex: variableIndex }); }; public onTypeChange = (type: EditableVariableType) => { diff --git a/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.test.tsx b/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.test.tsx new file mode 100644 index 00000000000..498483266d4 --- /dev/null +++ b/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.test.tsx @@ -0,0 +1,75 @@ +import { act, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import { selectors } from '@grafana/e2e-selectors'; +import { mockDataSource } from 'app/features/alerting/unified/mocks'; + +import { AdHocVariableForm } from './AdHocVariableForm'; + +const defaultDatasource = mockDataSource({ + name: 'Default Test Data Source', + uid: 'test-ds', + type: 'test', +}); + +const promDatasource = mockDataSource({ + name: 'Prometheus', + uid: 'prometheus', + type: 'prometheus', +}); + +jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => ({ + ...jest.requireActual('@grafana/runtime/src/services/dataSourceSrv'), + getDataSourceSrv: () => ({ + get: async () => defaultDatasource, + getList: () => [defaultDatasource, promDatasource], + getInstanceSettings: () => ({ ...defaultDatasource }), + }), +})); + +describe('AdHocVariableForm', () => { + it('should render the form with the provided data source', async () => { + const onDataSourceChange = jest.fn(); + const { renderer } = await setup({ + datasource: defaultDatasource, + onDataSourceChange, + infoText: 'Test Info', + }); + + const dataSourcePicker = renderer.getByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.AdHocFiltersVariable.datasourceSelect + ); + const infoText = renderer.getByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.AdHocFiltersVariable.infoText + ); + + expect(dataSourcePicker).toBeInTheDocument(); + expect(dataSourcePicker.getAttribute('placeholder')).toBe('Default Test Data Source'); + expect(infoText).toBeInTheDocument(); + expect(infoText).toHaveTextContent('Test Info'); + }); + + it('should call the onDataSourceChange callback when the data source is changed', async () => { + const onDataSourceChange = jest.fn(); + const { renderer, user } = await setup({ + datasource: defaultDatasource, + onDataSourceChange, + infoText: 'Test Info', + }); + + // Simulate changing the data source + await user.click(renderer.getByTestId(selectors.components.DataSourcePicker.inputV2)); + await user.click(renderer.getByText(/prom/i)); + + expect(onDataSourceChange).toHaveBeenCalledTimes(1); + expect(onDataSourceChange).toHaveBeenCalledWith(promDatasource, undefined); + }); +}); + +async function setup(props?: React.ComponentProps) { + return { + renderer: await act(() => render()), + user: userEvent.setup(), + }; +} diff --git a/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.tsx b/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.tsx new file mode 100644 index 00000000000..fe093a380b5 --- /dev/null +++ b/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.tsx @@ -0,0 +1,34 @@ +import React from 'react'; + +import { DataSourceInstanceSettings } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; +import { DataSourceRef } from '@grafana/schema'; +import { Alert, Field } from '@grafana/ui'; +import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; + +import { VariableLegend } from './VariableLegend'; + +interface AdHocVariableFormProps { + datasource?: DataSourceRef; + onDataSourceChange: (dsSettings: DataSourceInstanceSettings) => void; + infoText?: string; +} + +export function AdHocVariableForm({ datasource, infoText, onDataSourceChange }: AdHocVariableFormProps) { + return ( + <> + Ad-hoc options + + + + + {infoText ? ( + + ) : null} + + ); +} diff --git a/public/app/features/dashboard-scene/settings/variables/editors/AdHocFiltersVariableEditor.test.tsx b/public/app/features/dashboard-scene/settings/variables/editors/AdHocFiltersVariableEditor.test.tsx new file mode 100644 index 00000000000..8fae3e3fe2a --- /dev/null +++ b/public/app/features/dashboard-scene/settings/variables/editors/AdHocFiltersVariableEditor.test.tsx @@ -0,0 +1,122 @@ +import { render, act } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { of } from 'rxjs'; + +import { + FieldType, + LoadingState, + PanelData, + VariableSupportType, + getDefaultTimeRange, + toDataFrame, +} from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; +import { setRunRequest } from '@grafana/runtime/src'; +import { AdHocFiltersVariable } from '@grafana/scenes'; +import { mockDataSource } from 'app/features/alerting/unified/mocks'; +import { LegacyVariableQueryEditor } from 'app/features/variables/editor/LegacyVariableQueryEditor'; + +import { AdHocFiltersVariableEditor } from './AdHocFiltersVariableEditor'; + +const defaultDatasource = mockDataSource({ + name: 'Default Test Data Source', + uid: 'test-ds', + type: 'test', +}); + +const promDatasource = mockDataSource({ + name: 'Prometheus', + uid: 'prometheus', + type: 'prometheus', +}); + +jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => ({ + ...jest.requireActual('@grafana/runtime/src/services/dataSourceSrv'), + getDataSourceSrv: () => ({ + get: async () => ({ + ...defaultDatasource, + variables: { + getType: () => VariableSupportType.Custom, + query: jest.fn(), + editor: jest.fn().mockImplementation(LegacyVariableQueryEditor), + }, + }), + getList: () => [defaultDatasource, promDatasource], + getInstanceSettings: () => ({ ...defaultDatasource }), + }), +})); + +const runRequestMock = jest.fn().mockReturnValue( + of({ + state: LoadingState.Done, + series: [ + toDataFrame({ + fields: [{ name: 'text', type: FieldType.string, values: ['val1', 'val2', 'val11'] }], + }), + ], + timeRange: getDefaultTimeRange(), + }) +); + +setRunRequest(runRequestMock); + +describe('AdHocFiltersVariableEditor', () => { + it('renders AdHocVariableForm with correct props', async () => { + const { renderer } = await setup(); + const dataSourcePicker = renderer.getByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.AdHocFiltersVariable.datasourceSelect + ); + const infoText = renderer.getByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.AdHocFiltersVariable.infoText + ); + + expect(dataSourcePicker).toBeInTheDocument(); + expect(dataSourcePicker.getAttribute('placeholder')).toBe('Default Test Data Source'); + expect(infoText).toBeInTheDocument(); + expect(infoText).toHaveTextContent('This data source does not support ad hoc filters yet.'); + }); + + it('should update the variable data source when data source picker is changed', async () => { + const { renderer, variable, user } = await setup(); + + // Simulate changing the data source + await user.click(renderer.getByTestId(selectors.components.DataSourcePicker.inputV2)); + await user.click(renderer.getByText(/prom/i)); + + expect(variable.state.datasource).toEqual({ uid: 'prometheus', type: 'prometheus' }); + }); +}); + +async function setup(props?: React.ComponentProps) { + const onRunQuery = jest.fn(); + const variable = new AdHocFiltersVariable({ + name: 'adhocVariable', + type: 'adhoc', + label: 'Ad hoc filters', + description: 'Ad hoc filters are applied automatically to all queries that target this data source', + datasource: { uid: defaultDatasource.uid, type: defaultDatasource.type }, + filters: [ + { + key: 'test', + operator: '=', + value: 'testValue', + }, + ], + baseFilters: [ + { + key: 'baseTest', + operator: '=', + value: 'baseTestValue', + }, + ], + }); + return { + renderer: await act(() => + render() + ), + variable, + user: userEvent.setup(), + mocks: { onRunQuery }, + }; +} diff --git a/public/app/features/dashboard-scene/settings/variables/editors/AdHocFiltersVariableEditor.tsx b/public/app/features/dashboard-scene/settings/variables/editors/AdHocFiltersVariableEditor.tsx index 15a50cffbfc..37daff7cb56 100644 --- a/public/app/features/dashboard-scene/settings/variables/editors/AdHocFiltersVariableEditor.tsx +++ b/public/app/features/dashboard-scene/settings/variables/editors/AdHocFiltersVariableEditor.tsx @@ -1,12 +1,40 @@ import React from 'react'; +import { useAsync } from 'react-use'; +import { DataSourceInstanceSettings } from '@grafana/data'; +import { getDataSourceSrv } from '@grafana/runtime'; import { AdHocFiltersVariable } from '@grafana/scenes'; +import { DataSourceRef } from '@grafana/schema'; + +import { AdHocVariableForm } from '../components/AdHocVariableForm'; interface AdHocFiltersVariableEditorProps { variable: AdHocFiltersVariable; - onChange: (variable: AdHocFiltersVariable) => void; + onRunQuery: (variable: AdHocFiltersVariable) => void; } export function AdHocFiltersVariableEditor(props: AdHocFiltersVariableEditorProps) { - return
AdHocFiltersVariableEditor
; + const { variable } = props; + const datasourceRef = variable.useState().datasource ?? undefined; + + const { value: datasourceSettings } = useAsync(async () => { + return await getDataSourceSrv().get(datasourceRef); + }, [datasourceRef]); + + const message = datasourceSettings?.getTagKeys + ? 'Ad hoc filters are applied automatically to all queries that target this data source' + : 'This data source does not support ad hoc filters yet.'; + + const onDataSourceChange = (ds: DataSourceInstanceSettings) => { + const dsRef: DataSourceRef = { + uid: ds.uid, + type: ds.type, + }; + + variable.setState({ + datasource: dsRef, + }); + }; + + return ; } diff --git a/public/app/features/dashboard-scene/settings/variables/utils.test.ts b/public/app/features/dashboard-scene/settings/variables/utils.test.ts index f34dcdc3ec1..2b2c42d1ca8 100644 --- a/public/app/features/dashboard-scene/settings/variables/utils.test.ts +++ b/public/app/features/dashboard-scene/settings/variables/utils.test.ts @@ -172,10 +172,10 @@ describe('getVariableScene', () => { ['adhoc', AdHocFiltersVariable], ['groupby', GroupByVariable], ['textbox', TextBoxVariable], - ])('should return the scene variable instance for the given editable variable type', () => { + ])('should return the scene variable instance for the given editable variable type', (type, instanceType) => { const initialState = { name: 'MyVariable' }; - const sceneVariable = getVariableScene('custom', initialState); - expect(sceneVariable).toBeInstanceOf(CustomVariable); + const sceneVariable = getVariableScene(type as EditableVariableType, initialState); + expect(sceneVariable).toBeInstanceOf(instanceType); expect(sceneVariable.state.name).toBe(initialState.name); }); }); diff --git a/public/app/features/dashboard-scene/settings/variables/utils.ts b/public/app/features/dashboard-scene/settings/variables/utils.ts index f1ed0380100..1d61b445b6b 100644 --- a/public/app/features/dashboard-scene/settings/variables/utils.ts +++ b/public/app/features/dashboard-scene/settings/variables/utils.ts @@ -9,10 +9,10 @@ import { IntervalVariable, TextBoxVariable, QueryVariable, - AdHocFilterSet, GroupByVariable, SceneVariable, MultiValueVariable, + AdHocFiltersVariable, SceneVariableState, } from '@grafana/scenes'; import { VariableType } from '@grafana/schema'; @@ -124,8 +124,7 @@ export function getVariableScene(type: EditableVariableType, initialState: Commo case 'datasource': return new DataSourceVariable(initialState); case 'adhoc': - // TODO: Initialize properly AdHocFilterSet with initialState - return new AdHocFilterSet({ name: initialState.name }); + return new AdHocFiltersVariable(initialState); case 'groupby': return new GroupByVariable(initialState); case 'textbox': diff --git a/public/app/features/dashboard-scene/utils/getVariablesCompatibility.ts b/public/app/features/dashboard-scene/utils/getVariablesCompatibility.ts index ed26220201f..54c47284639 100644 --- a/public/app/features/dashboard-scene/utils/getVariablesCompatibility.ts +++ b/public/app/features/dashboard-scene/utils/getVariablesCompatibility.ts @@ -10,7 +10,7 @@ export function getVariablesCompatibility(sceneObject: SceneObject): TypedVariab // Sadly templateSrv.getVariables returns TypedVariableModel but sceneVariablesSetToVariables return persisted schema model // They look close to identical (differ in what is optional in some places). // The way templateSrv.getVariables is used it should not matter. it is mostly used to get names of all variables (for query editors). - // So type and name are important. Maybe some external data sourcess also check current value so that is also important. + // So type and name are important. Maybe some external data sources also check current value so that is also important. // @ts-expect-error return legacyModels; } diff --git a/public/app/features/trails/ActionTabs/AddToFiltersGraphAction.tsx b/public/app/features/trails/ActionTabs/AddToFiltersGraphAction.tsx index f92ef303f92..9a71488e30f 100644 --- a/public/app/features/trails/ActionTabs/AddToFiltersGraphAction.tsx +++ b/public/app/features/trails/ActionTabs/AddToFiltersGraphAction.tsx @@ -28,9 +28,9 @@ export class AddToFiltersGraphAction extends SceneObjectBase ({ + ...jest.requireActual('@grafana/runtime'), + getTemplateSrv: () => ({ + getAdhocFilters: jest.fn().mockReturnValue([{ key: 'origKey', operator: '=', value: '' }]), + }), +})); + describe('TrailStore', () => { beforeAll(() => { let localStore: Record = {}; diff --git a/public/app/features/variables/adhoc/AdHocVariableEditor.test.tsx b/public/app/features/variables/adhoc/AdHocVariableEditor.test.tsx deleted file mode 100644 index 87e640539ac..00000000000 --- a/public/app/features/variables/adhoc/AdHocVariableEditor.test.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import React, { ComponentProps } from 'react'; - -import { selectors } from '@grafana/e2e-selectors'; -import { mockDataSource } from 'app/features/alerting/unified/mocks'; -import { DataSourceType } from 'app/features/alerting/unified/utils/datasource'; - -import { adHocBuilder } from '../shared/testing/builders'; - -import { AdHocVariableEditorUnConnected as AdHocVariableEditor } from './AdHocVariableEditor'; - -const promDsMock = mockDataSource({ - name: 'Prometheus', - type: DataSourceType.Prometheus, -}); - -const lokiDsMock = mockDataSource({ - name: 'Loki', - type: DataSourceType.Loki, -}); - -jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => { - return { - getDataSourceSrv: () => ({ - get: () => { - return Promise.resolve(promDsMock); - }, - getList: () => [promDsMock, lokiDsMock], - getInstanceSettings: (v: string) => { - if (v === 'Prometheus') { - return promDsMock; - } - return lokiDsMock; - }, - }), - }; -}); - -const props = { - extended: { - dataSources: [ - { text: 'Prometheus', value: null }, // default datasource - { text: 'Loki', value: { type: 'loki-ds', uid: 'abc' } }, - ], - } as ComponentProps['extended'], - variable: adHocBuilder().withId('adhoc').withRootStateKey('key').withName('adhoc').build(), - onPropChange: jest.fn(), - - // connected actions - initAdHocVariableEditor: jest.fn(), - changeVariableDatasource: jest.fn(), -}; - -describe('AdHocVariableEditor', () => { - beforeEach(() => { - props.changeVariableDatasource.mockReset(); - }); - - it('has a datasource select menu', async () => { - render(); - - expect(await screen.getByTestId(selectors.components.DataSourcePicker.container)).toBeInTheDocument(); - }); - - it('calls the callback when changing the datasource', async () => { - render(); - const selectEl = screen - .getByTestId(selectors.components.DataSourcePicker.container) - .getElementsByTagName('input')[0]; - await userEvent.click(selectEl); - await userEvent.click(screen.getByText('Loki')); - - expect(props.changeVariableDatasource).toBeCalledWith( - { type: 'adhoc', id: 'adhoc', rootStateKey: 'key' }, - { type: 'loki', uid: 'mock-ds-3' } - ); - }); - - it('renders informational text', () => { - const extended = { - ...props.extended, - infoText: "Here's a message that should help you", - }; - render(); - - const alert = screen.getByText("Here's a message that should help you"); - expect(alert).toBeInTheDocument(); - }); -}); diff --git a/public/app/features/variables/adhoc/AdHocVariableEditor.tsx b/public/app/features/variables/adhoc/AdHocVariableEditor.tsx index ac079a2d787..8bd63d44309 100644 --- a/public/app/features/variables/adhoc/AdHocVariableEditor.tsx +++ b/public/app/features/variables/adhoc/AdHocVariableEditor.tsx @@ -2,11 +2,9 @@ import React, { PureComponent } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { DataSourceInstanceSettings, getDataSourceRef } from '@grafana/data'; -import { Alert, Field } from '@grafana/ui'; -import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; +import { AdHocVariableForm } from 'app/features/dashboard-scene/settings/variables/components/AdHocVariableForm'; import { StoreState } from 'app/types'; -import { VariableLegend } from '../../dashboard-scene/settings/variables/components/VariableLegend'; import { initialVariableEditorState } from '../editor/reducer'; import { getAdhocVariableEditorState } from '../editor/selectors'; import { VariableEditorProps } from '../editor/types'; @@ -58,23 +56,13 @@ export class AdHocVariableEditorUnConnected extends PureComponent { render() { const { variable, extended } = this.props; - const infoText = extended?.infoText ?? null; return ( - <> - Ad-hoc options - - - - - {infoText ? : null} - + ); } } diff --git a/yarn.lock b/yarn.lock index edb666051bf..a3bfcbd3626 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4020,9 +4020,9 @@ __metadata: languageName: unknown linkType: soft -"@grafana/scenes@npm:^2.6.5": - version: 2.6.5 - resolution: "@grafana/scenes@npm:2.6.5" +"@grafana/scenes@npm:^3.2.1": + version: 3.2.1 + resolution: "@grafana/scenes@npm:3.2.1" dependencies: "@grafana/e2e-selectors": "npm:10.0.2" react-grid-layout: "npm:1.3.4" @@ -4034,7 +4034,7 @@ __metadata: "@grafana/runtime": 10.0.3 "@grafana/schema": 10.0.3 "@grafana/ui": 10.0.3 - checksum: 10/68fe91a5a0c8f80b679126f3525b74b29ce3f9ad92bc558eaaf39693235137348b90796c2b02aa7c1c7929586e60df6024e139993a416ef37d4c875e548dc855 + checksum: 10/5e93c0dcdfbd7cfed977d650fb0744c9b8107b1c92af2ae6d6e9f2f61bb9ba7f3cb5ad394fa3389aaef0d892a18c6a12fedf5454a4210bdcc6797a272ddbc625 languageName: node linkType: hard @@ -18026,7 +18026,7 @@ __metadata: "@grafana/o11y-ds-frontend": "workspace:*" "@grafana/prometheus": "workspace:*" "@grafana/runtime": "workspace:*" - "@grafana/scenes": "npm:^2.6.5" + "@grafana/scenes": "npm:^3.2.1" "@grafana/schema": "workspace:*" "@grafana/sql": "workspace:*" "@grafana/tsconfig": "npm:^1.3.0-rc1" From ff5b0f7bd8605f8e0f347c64c96e7953842cea8e Mon Sep 17 00:00:00 2001 From: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com> Date: Tue, 13 Feb 2024 15:04:09 -0600 Subject: [PATCH 12/19] Logs panel: Table UI - remove beta badge (#82395) * remove beta badge --- public/app/features/explore/Logs/Logs.tsx | 24 +++++++---------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index c927254a478..b53337db900 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -13,7 +13,6 @@ import { EventBus, ExploreLogsPanelState, ExplorePanelsState, - FeatureState, Field, GrafanaTheme2, LinkModel, @@ -36,7 +35,6 @@ import { config, reportInteraction } from '@grafana/runtime'; import { DataQuery, TimeZone } from '@grafana/schema'; import { Button, - FeatureBadge, InlineField, InlineFieldRow, InlineSwitch, @@ -654,21 +652,13 @@ class UnthemedLogs extends PureComponent { - - , - - - , - ] - : null, + config.featureToggles.logsExploreTableVisualisation ? ( + this.state.visualisationType === 'logs' ? null : ( + + + + ) + ) : null, ]} title={'Logs'} actions={ From b3663eafeef83474e07efc7f680e30a5ca7a6097 Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Tue, 13 Feb 2024 14:47:17 -0700 Subject: [PATCH 13/19] Changelog: Updated changelog for 10.3.3 (#82399) Co-authored-by: grafanabot --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17ebe4eca82..8937a766a15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ + + +# 10.3.3 (2024-02-02) + +### Bug fixes + +- **Elasticsearch:** Fix creating of legend so it is backward compatible with frontend produced frames. [#81786](https://github.com/grafana/grafana/issues/81786), [@ivanahuckova](https://github.com/ivanahuckova) +- **ShareModal:** Fixes url sync issue that caused issue with save drawer. [#81721](https://github.com/grafana/grafana/issues/81721), [@ivanortegaalba](https://github.com/ivanortegaalba) + + # 10.3.1 (2024-01-22) From cb47177df92a9db194d80fccf77b8ce5db5e6e61 Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Tue, 13 Feb 2024 17:00:27 -0500 Subject: [PATCH 14/19] Changelog: Updated changelog for 10.1.7 (#82401) Co-authored-by: grafanabot --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8937a766a15..e894eb0ce7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -766,6 +766,15 @@ Starting with 10.2, `parentRowIndex` is deprecated. It will be removed in a futu - **Drawer:** Make content scroll by default. [#75287](https://github.com/grafana/grafana/issues/75287), [@ashharrison90](https://github.com/ashharrison90) + + +# 10.1.7 (2024-01-29) + +### Bug fixes + +- **Annotations:** Split cleanup into separate queries and deletes to avoid deadlocks on MySQL. [#80678](https://github.com/grafana/grafana/issues/80678), [@alexweav](https://github.com/alexweav) + + # 10.1.6 (2023-12-18) From a1ff439a20bf35bfc7dbb1b9f5e89dbd4207d575 Mon Sep 17 00:00:00 2001 From: Victor Marin <36818606+mdvictor@users.noreply.github.com> Date: Wed, 14 Feb 2024 10:00:16 +0200 Subject: [PATCH 15/19] Fix query inspector in explore to scroll to bottom (#82369) --- .../src/components/TabbedContainer/TabbedContainer.tsx | 10 ++++++---- .../explore/RichHistory/RichHistoryQueriesTab.tsx | 2 +- .../explore/RichHistory/RichHistoryStarredTab.tsx | 2 +- public/app/features/inspector/QueryInspector.tsx | 4 ++-- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/grafana-ui/src/components/TabbedContainer/TabbedContainer.tsx b/packages/grafana-ui/src/components/TabbedContainer/TabbedContainer.tsx index 1794b1a96da..7a527bad325 100644 --- a/packages/grafana-ui/src/components/TabbedContainer/TabbedContainer.tsx +++ b/packages/grafana-ui/src/components/TabbedContainer/TabbedContainer.tsx @@ -5,7 +5,7 @@ import { SelectableValue, GrafanaTheme2 } from '@grafana/data'; import { IconButton } from '../../components/IconButton/IconButton'; import { TabsBar, Tab, TabContent } from '../../components/Tabs'; -import { useStyles2 } from '../../themes'; +import { useStyles2, useTheme2 } from '../../themes'; import { IconName } from '../../types/icon'; import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar'; @@ -25,12 +25,14 @@ export interface TabbedContainerProps { export function TabbedContainer({ tabs, defaultTab, closeIconTooltip, onClose }: TabbedContainerProps) { const [activeTab, setActiveTab] = useState(tabs.some((tab) => tab.value === defaultTab) ? defaultTab : tabs[0].value); + const styles = useStyles2(getStyles); + const theme = useTheme2(); const onSelectTab = (item: SelectableValue) => { setActiveTab(item.value!); }; - const styles = useStyles2(getStyles); + const autoHeight = `calc(100% - (${theme.components.menuTabs.height}px + ${theme.spacing(1)}))`; return (
@@ -46,7 +48,7 @@ export function TabbedContainer({ tabs, defaultTab, closeIconTooltip, onClose }: ))} - + {tabs.find((t) => t.value === activeTab)?.content}
@@ -60,7 +62,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ tabContent: css({ padding: theme.spacing(2), backgroundColor: theme.colors.background.primary, - height: `calc(100% - ${theme.components.menuTabs.height}px)`, + height: `100%`, }), close: css({ position: 'absolute', diff --git a/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx b/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx index cdafb001620..e118d226356 100644 --- a/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx @@ -94,9 +94,9 @@ const getStyles = (theme: GrafanaTheme2, height: number) => { `, footer: css` height: 60px; - margin: ${theme.spacing(3)} auto; display: flex; justify-content: center; + align-items: center; font-weight: ${theme.typography.fontWeightLight}; font-size: ${theme.typography.bodySmall.fontSize}; a { diff --git a/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx b/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx index 7005ae94c90..c8b824da52d 100644 --- a/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx @@ -54,9 +54,9 @@ const getStyles = (theme: GrafanaTheme2) => { `, footer: css` height: 60px; - margin-top: ${theme.spacing(3)}; display: flex; justify-content: center; + align-items: center; font-weight: ${theme.typography.fontWeightLight}; font-size: ${theme.typography.bodySmall.fontSize}; a { diff --git a/public/app/features/inspector/QueryInspector.tsx b/public/app/features/inspector/QueryInspector.tsx index ad857599173..23e1fac5290 100644 --- a/public/app/features/inspector/QueryInspector.tsx +++ b/public/app/features/inspector/QueryInspector.tsx @@ -9,7 +9,7 @@ import { Button, ClipboardButton, JSONFormatter, LoadingPlaceholder, Stack } fro import { Trans } from 'app/core/internationalization'; import { backendSrv } from 'app/core/services/backend_srv'; -import { getPanelInspectorStyles } from './styles'; +import { getPanelInspectorStyles2 } from './styles'; interface ExecutedQueryInfo { refId: string; @@ -212,7 +212,7 @@ export class QueryInspector extends PureComponent { const { allNodesExpanded, executedQueries, response } = this.state; const { onRefreshQuery, data } = this.props; const openNodes = this.getNrOfOpenNodes(); - const styles = getPanelInspectorStyles(); + const styles = getPanelInspectorStyles2(config.theme2); const haveData = Object.keys(response).length > 0; const isLoading = data.state === LoadingState.Loading; From 37c860daf0e6973f4ee1cb22144e4370d9fa75d9 Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Wed, 14 Feb 2024 09:56:28 +0100 Subject: [PATCH 16/19] Changelog: Updated changelog for 10.2.4 (#82411) Co-authored-by: grafanabot --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e894eb0ce7a..ee66a614138 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -113,6 +113,29 @@ Users who have InfluxDB datasource configured with SQL querying language must up Removes `NamespaceID` from responses of all GET routes underneath the path `/api/ruler/grafana/api/v1/rules` - 3 affected endpoints. All affected routes are not in the publicly documented or `stable` marked portion of the ngalert API. This only breaks clients who are directly using the unstable portion of the API. Such clients should use `NamespaceUID` rather than `NamespaceID` to identify namespaces. Issue [#79359](https://github.com/grafana/grafana/issues/79359) + + +# 10.2.4 (2024-01-29) + +### Features and enhancements + +- **Chore:** Upgrade Go to 1.21.5. [#79560](https://github.com/grafana/grafana/issues/79560), [@tolzhabayev](https://github.com/tolzhabayev) + +### Bug fixes + +- **Field:** Fix perf regression in getUniqueFieldName(). [#81417](https://github.com/grafana/grafana/issues/81417), [@leeoniya](https://github.com/leeoniya) +- **Alerting:** Fix Graphite subqueries. [#80816](https://github.com/grafana/grafana/issues/80816), [@gillesdemey](https://github.com/gillesdemey) +- **Alerting:** Fix Graphite subqueries. [#80744](https://github.com/grafana/grafana/issues/80744), [@gillesdemey](https://github.com/gillesdemey) +- **Annotations:** Split cleanup into separate queries and deletes to avoid deadlocks on MySQL. [#80485](https://github.com/grafana/grafana/issues/80485), [@alexweav](https://github.com/alexweav) +- **Loki:** Fix bug duplicating parsed labels across multiple log lines. [#80368](https://github.com/grafana/grafana/issues/80368), [@svennergr](https://github.com/svennergr) +- **Alerting:** Fix NoData & Error alerts not resolving when rule is reset. [#80241](https://github.com/grafana/grafana/issues/80241), [@JacobsonMT](https://github.com/JacobsonMT) +- **Auth:** Fix a panic during logout when OAuth provider is not set. [#80221](https://github.com/grafana/grafana/issues/80221), [@dmihai](https://github.com/dmihai) +- **Gauges:** Fixing broken auto sizing. [#79940](https://github.com/grafana/grafana/issues/79940), [@torkelo](https://github.com/torkelo) +- **Templating:** Json interpolation of single-value default selection does not create valid json. [#79503](https://github.com/grafana/grafana/issues/79503), [@kaydelaney](https://github.com/kaydelaney) +- **Tempo:** Fix cache in TraceQL editor. [#79471](https://github.com/grafana/grafana/issues/79471), [@adrapereira](https://github.com/adrapereira) +- **Alerting:** Fix for data source filter on cloud rules. (#79327). [#79350](https://github.com/grafana/grafana/issues/79350), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) + + # 10.2.3 (2023-12-18) From 92eb2c900e9dc67b369e796bbe0e3652062402ac Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Wed, 14 Feb 2024 11:43:46 +0100 Subject: [PATCH 17/19] Alerting: Use VirtualizedSelect for ContactPointSelector (#82345) * Use VirtualizedSelect for ContactPointSelector * Conditionally use virtualized to keep description when having less than 500 contact points --- .../contactPoint/ContactPointSelector.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx index 5788b9dd44a..1e7d178496b 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx @@ -30,6 +30,8 @@ export interface ContactPointSelectorProps { refetchReceivers: () => Promise; } +const MAX_CONTACT_POINTS_RENDERED = 500; + export function ContactPointSelector({ alertManager, options, @@ -42,9 +44,10 @@ export function ContactPointSelector({ const contactPointInForm = watch(`contactPoints.${alertManager}.selectedContactPoint`); const selectedContactPointWithMetadata = options.find((option) => option.value.name === contactPointInForm)?.value; - const selectedContactPointSelectableValue = selectedContactPointWithMetadata - ? { value: selectedContactPointWithMetadata, label: selectedContactPointWithMetadata.name } - : undefined; + const selectedContactPointSelectableValue: SelectableValue = + selectedContactPointWithMetadata + ? { value: selectedContactPointWithMetadata, label: selectedContactPointWithMetadata.name } + : { value: undefined, label: '' }; const LOADING_SPINNER_DURATION = 1000; @@ -80,8 +83,8 @@ export function ContactPointSelector({ render={({ field: { onChange, ref, ...field }, fieldState: { error } }) => ( <>
-