From 7bc6d32eb9aa60ce215e1ab390f89b45ed20b0c2 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Fri, 4 Aug 2023 09:43:47 +0000 Subject: [PATCH] Search API: Search by folder UID (#65040) * Search: Attempt to support folderUID filter * Search: Use folder UID instead of ID for searching folders * Update swagger * Fix JSON property casing * Add integration test * Remove redundant query condition * Fix frontend test * Fix listing dashboards in General/root * Add support for fetching top level folders using `folderUIDs=` (empty string) query parameter * Add deprecation notice * Send uid of general in sql.ts * Use 'general' for query folderUIDs query param for fetching folder * Add tests * Fix FolderUIDFilter --------- Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> --- pkg/api/search.go | 18 +- pkg/services/dashboards/database/database.go | 11 +- .../dashboards/database/database_test.go | 133 ++++++++ pkg/services/dashboards/models.go | 1 + pkg/services/search/service.go | 2 + pkg/services/sqlstore/searchstore/filters.go | 47 +++ .../sqlstore/searchstore/filters_test.go | 59 ++++ public/api-merged.json | 274 +++++++++++++-- .../app/features/search/service/sql.test.ts | 6 +- public/app/features/search/service/sql.ts | 9 +- public/openapi3.json | 312 ++++++++++++++++-- 11 files changed, 809 insertions(+), 63 deletions(-) create mode 100644 pkg/services/sqlstore/searchstore/filters_test.go diff --git a/pkg/api/search.go b/pkg/api/search.go index 6602a002235..1abe05b7d14 100644 --- a/pkg/api/search.go +++ b/pkg/api/search.go @@ -60,7 +60,12 @@ func (hs *HTTPServer) Search(c *contextmodel.ReqContext) response.Response { } } - if len(dbIDs) > 0 && len(dbUIDs) > 0 { + folderUIDs := c.QueryStrings("folderUIDs") + + bothDashboardIds := len(dbIDs) > 0 && len(dbUIDs) > 0 + bothFolderIds := len(folderIDs) > 0 && len(folderUIDs) > 0 + + if bothDashboardIds || bothFolderIds { return response.Error(400, "search supports UIDs or IDs, not both", nil) } @@ -76,6 +81,7 @@ func (hs *HTTPServer) Search(c *contextmodel.ReqContext) response.Response { DashboardUIDs: dbUIDs, Type: dashboardType, FolderIds: folderIDs, + FolderUIDs: folderUIDs, Permission: permission, Sort: sort, } @@ -136,17 +142,27 @@ type SearchParams struct { // Enum: dash-folder,dash-db Type string `json:"type"` // List of dashboard id’s to search for + // This is deprecated: users should use the `dashboardUIDs` query parameter instead // in:query // required: false + // deprecated: true DashboardIds []int64 `json:"dashboardIds"` // List of dashboard uid’s to search for // in:query // required: false DashboardUIDs []string `json:"dashboardUIDs"` // List of folder id’s to search in for dashboards + // If it's `0` then it will query for the top level folders + // This is deprecated: users should use the `folderUIDs` query parameter instead // in:query // required: false + // deprecated: true FolderIds []int64 `json:"folderIds"` + // List of folder UID’s to search in for dashboards + // If it's an empty string then it will query for the top level folders + // in:query + // required: false + FolderUIDs []string `json:"folderUIDs"` // Flag indicating if only starred Dashboards should be returned // in:query // required: false diff --git a/pkg/services/dashboards/database/database.go b/pkg/services/dashboards/database/database.go index fddcfea6c78..9529a974426 100644 --- a/pkg/services/dashboards/database/database.go +++ b/pkg/services/dashboards/database/database.go @@ -975,10 +975,13 @@ func (d *dashboardStore) FindDashboards(ctx context.Context, query *dashboards.F filters = append(filters, query.Filters...) + var orgID int64 if query.OrgId != 0 { - filters = append(filters, searchstore.OrgFilter{OrgId: query.OrgId}) + orgID = query.OrgId + filters = append(filters, searchstore.OrgFilter{OrgId: orgID}) } else if query.SignedInUser.OrgID != 0 { - filters = append(filters, searchstore.OrgFilter{OrgId: query.SignedInUser.OrgID}) + orgID = query.SignedInUser.OrgID + filters = append(filters, searchstore.OrgFilter{OrgId: orgID}) } if len(query.Tags) > 0 { @@ -1003,6 +1006,10 @@ func (d *dashboardStore) FindDashboards(ctx context.Context, query *dashboards.F filters = append(filters, searchstore.FolderFilter{IDs: query.FolderIds}) } + if len(query.FolderUIDs) > 0 { + filters = append(filters, searchstore.FolderUIDFilter{Dialect: d.store.GetDialect(), OrgID: orgID, UIDs: query.FolderUIDs}) + } + var res []dashboards.DashboardSearchProjection sb := &searchstore.Builder{Dialect: d.store.GetDialect(), Filters: filters} diff --git a/pkg/services/dashboards/database/database_test.go b/pkg/services/dashboards/database/database_test.go index 3f83e06c8f9..88de8d1460f 100644 --- a/pkg/services/dashboards/database/database_test.go +++ b/pkg/services/dashboards/database/database_test.go @@ -15,6 +15,7 @@ import ( "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/search/model" @@ -451,6 +452,33 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { require.Equal(t, hit.FolderURL, fmt.Sprintf("/dashboards/f/%s/%s", savedFolder.UID, savedFolder.Slug)) }) + t.Run("Should be able to find a dashboard folder's children by UID", func(t *testing.T) { + setup() + query := dashboards.FindPersistedDashboardsQuery{ + OrgId: 1, + FolderUIDs: []string{savedFolder.UID}, + SignedInUser: &user.SignedInUser{ + OrgID: 1, + OrgRole: org.RoleEditor, + Permissions: map[int64]map[string][]string{ + 1: {dashboards.ActionDashboardsRead: []string{dashboards.ScopeDashboardsAll}}, + }, + }, + } + + hits, err := testSearchDashboards(dashboardStore, &query) + require.NoError(t, err) + + require.Equal(t, len(hits), 2) + hit := hits[0] + require.Equal(t, hit.ID, savedDash.ID) + require.Equal(t, hit.URL, fmt.Sprintf("/d/%s/%s", savedDash.UID, savedDash.Slug)) + require.Equal(t, hit.FolderID, savedFolder.ID) + require.Equal(t, hit.FolderUID, savedFolder.UID) + require.Equal(t, hit.FolderTitle, savedFolder.Title) + require.Equal(t, hit.FolderURL, fmt.Sprintf("/dashboards/f/%s/%s", savedFolder.UID, savedFolder.Slug)) + }) + t.Run("Should be able to find dashboards by ids", func(t *testing.T) { setup() query := dashboards.FindPersistedDashboardsQuery{ @@ -674,6 +702,111 @@ func TestGetExistingDashboardByTitleAndFolder(t *testing.T) { }) } +func TestIntegrationFindDashboardsByFolder(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + sqlStore := db.InitTestDB(t) + cfg := setting.NewCfg() + cfg.IsFeatureToggleEnabled = func(key string) bool { return false } + quotaService := quotatest.New(false, nil) + dashboardStore, err := ProvideDashboardStore(sqlStore, cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, cfg), quotaService) + require.NoError(t, err) + + orgID := int64(1) + insertTestDashboard(t, dashboardStore, "dashboard under general", orgID, 0, false) + + f0 := insertTestDashboard(t, dashboardStore, "f0", orgID, 0, true) + insertTestDashboard(t, dashboardStore, "dashboard under f0", orgID, f0.ID, false) + + f1 := insertTestDashboard(t, dashboardStore, "f1", orgID, 0, true) + insertTestDashboard(t, dashboardStore, "dashboard under f1", orgID, f1.ID, false) + + testCases := []struct { + desc string + folderIDs []int64 + folderUIDs []string + expectedResult []string + }{ + { + desc: "find dashboard under general using folder id", + folderIDs: []int64{0}, + expectedResult: []string{"dashboard under general"}, + }, + { + desc: "find dashboard under f0 using folder id", + folderIDs: []int64{f0.ID}, + expectedResult: []string{"dashboard under f0"}, + }, + { + desc: "find dashboard under f0 or f1 using folder id", + folderIDs: []int64{f0.ID, f1.ID}, + expectedResult: []string{"dashboard under f0", "dashboard under f1"}, + }, + { + desc: "find dashboard under general using folder UID", + folderUIDs: []string{folder.GeneralFolderUID}, + expectedResult: []string{"dashboard under general"}, + }, + { + desc: "find dashboard under f0 using folder UID", + folderUIDs: []string{f0.UID}, + expectedResult: []string{"dashboard under f0"}, + }, + { + desc: "find dashboard under f0 or f1 using folder UID", + folderUIDs: []string{f0.UID, f1.UID}, + expectedResult: []string{"dashboard under f0", "dashboard under f1"}, + }, + { + desc: "find dashboard under general or f0 using folder id", + folderIDs: []int64{0, f0.ID}, + expectedResult: []string{"dashboard under f0", "dashboard under general"}, + }, + { + desc: "find dashboard under general or f0 or f1 using folder id", + folderIDs: []int64{0, f0.ID, f1.ID}, + expectedResult: []string{"dashboard under f0", "dashboard under f1", "dashboard under general"}, + }, + { + desc: "find dashboard under general or f0 using folder UID", + folderUIDs: []string{folder.GeneralFolderUID, f0.UID}, + expectedResult: []string{"dashboard under f0", "dashboard under general"}, + }, + { + desc: "find dashboard under general or f0 or f1 using folder UID", + folderUIDs: []string{folder.GeneralFolderUID, f0.UID, f1.UID}, + expectedResult: []string{"dashboard under f0", "dashboard under f1", "dashboard under general"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + res, err := dashboardStore.FindDashboards(context.Background(), &dashboards.FindPersistedDashboardsQuery{ + SignedInUser: &user.SignedInUser{ + OrgID: 1, + Permissions: map[int64]map[string][]string{ + orgID: { + dashboards.ActionDashboardsRead: []string{dashboards.ScopeDashboardsAll}, + dashboards.ActionFoldersRead: []string{dashboards.ScopeFoldersAll}, + }, + }, + }, + Type: searchstore.TypeDashboard, + FolderIds: tc.folderIDs, + FolderUIDs: tc.folderUIDs, + }) + require.NoError(t, err) + require.Equal(t, len(tc.expectedResult), len(res)) + + for i, r := range tc.expectedResult { + assert.Equal(t, r, res[i].Title) + } + }) + } +} + func insertTestRule(t *testing.T, sqlStore db.DB, foderOrgID int64, folderUID string) { err := sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { type alertQuery struct { diff --git a/pkg/services/dashboards/models.go b/pkg/services/dashboards/models.go index b8a10de177f..7ce7d14a284 100644 --- a/pkg/services/dashboards/models.go +++ b/pkg/services/dashboards/models.go @@ -480,6 +480,7 @@ type FindPersistedDashboardsQuery struct { DashboardUIDs []string Type string FolderIds []int64 + FolderUIDs []string Tags []string Limit int64 Page int64 diff --git a/pkg/services/search/service.go b/pkg/services/search/service.go index 838f61e6e59..a30f4008faf 100644 --- a/pkg/services/search/service.go +++ b/pkg/services/search/service.go @@ -38,6 +38,7 @@ type Query struct { DashboardUIDs []string DashboardIds []int64 FolderIds []int64 + FolderUIDs []string Permission dashboards.PermissionType Sort string } @@ -83,6 +84,7 @@ func (s *SearchService) SearchHandler(ctx context.Context, query *Query) (model. DashboardIds: query.DashboardIds, Type: query.Type, FolderIds: query.FolderIds, + FolderUIDs: query.FolderUIDs, Tags: query.Tags, Limit: query.Limit, Page: query.Page, diff --git a/pkg/services/sqlstore/searchstore/filters.go b/pkg/services/sqlstore/searchstore/filters.go index 170d6a2cf4d..7b5f49dd0df 100644 --- a/pkg/services/sqlstore/searchstore/filters.go +++ b/pkg/services/sqlstore/searchstore/filters.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" ) @@ -91,6 +92,52 @@ func (f FolderFilter) Where() (string, []interface{}) { return sqlIDin("dashboard.folder_id", f.IDs) } +type FolderUIDFilter struct { + Dialect migrator.Dialect + OrgID int64 + UIDs []string +} + +func (f FolderUIDFilter) Where() (string, []interface{}) { + if len(f.UIDs) < 1 { + return "", nil + } + + params := []interface{}{} + includeGeneral := false + for _, uid := range f.UIDs { + if uid == folder.GeneralFolderUID { + includeGeneral = true + continue + } + params = append(params, uid) + } + + q := "" + switch { + case len(params) < 1: + // do nothing + case len(params) == 1: + q = "dashboard.folder_id IN (SELECT id FROM dashboard WHERE org_id = ? AND uid = ?)" + params = append([]interface{}{f.OrgID}, params...) + default: + sqlArray := "(?" + strings.Repeat(",?", len(params)-1) + ")" + q = "dashboard.folder_id IN (SELECT id FROM dashboard WHERE org_id = ? AND uid IN " + sqlArray + ")" + params = append([]interface{}{f.OrgID}, params...) + } + + if includeGeneral { + if q == "" { + q = "dashboard.folder_id = ? " + } else { + q = "(" + q + " OR dashboard.folder_id = ?)" + } + params = append(params, 0) + } + + return q, params +} + type DashboardIDFilter struct { IDs []int64 } diff --git a/pkg/services/sqlstore/searchstore/filters_test.go b/pkg/services/sqlstore/searchstore/filters_test.go new file mode 100644 index 00000000000..00a126e576c --- /dev/null +++ b/pkg/services/sqlstore/searchstore/filters_test.go @@ -0,0 +1,59 @@ +package searchstore_test + +import ( + "testing" + + "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" + "github.com/stretchr/testify/assert" +) + +func TestFolderUIDFilter(t *testing.T) { + testCases := []struct { + description string + uids []string + expectedSql string + expectedParams []interface{} + }{ + { + description: "searching general folder", + uids: []string{"general"}, + expectedSql: "dashboard.folder_id = ? ", + expectedParams: []interface{}{0}, + }, + { + description: "searching a specific folder", + uids: []string{"abc-123"}, + expectedSql: "dashboard.folder_id IN (SELECT id FROM dashboard WHERE org_id = ? AND uid = ?)", + expectedParams: []interface{}{int64(1), "abc-123"}, + }, + { + description: "searching a specific folders", + uids: []string{"abc-123", "def-456"}, + expectedSql: "dashboard.folder_id IN (SELECT id FROM dashboard WHERE org_id = ? AND uid IN (?,?))", + expectedParams: []interface{}{int64(1), "abc-123", "def-456"}, + }, + { + description: "searching a specific folders or general", + uids: []string{"general", "abc-123", "def-456"}, + expectedSql: "(dashboard.folder_id IN (SELECT id FROM dashboard WHERE org_id = ? AND uid IN (?,?)) OR dashboard.folder_id = ?)", + expectedParams: []interface{}{int64(1), "abc-123", "def-456", 0}, + }, + } + + store := setupTestEnvironment(t) + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + f := searchstore.FolderUIDFilter{ + Dialect: store.GetDialect(), + OrgID: 1, + UIDs: tc.uids, + } + + sql, params := f.Where() + + assert.Equal(t, tc.expectedSql, sql) + assert.Equal(t, tc.expectedParams, params) + }) + } +} diff --git a/public/api-merged.json b/public/api-merged.json index dbdfc846a98..134364a792f 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -2664,13 +2664,6 @@ "summary": "Export an alert rule in provisioning file format.", "operationId": "RouteGetAlertRuleExport", "parameters": [ - { - "type": "string", - "description": "Alert rule UID", - "name": "UID", - "in": "path", - "required": true - }, { "type": "boolean", "default": false, @@ -2684,6 +2677,13 @@ "description": "Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence.", "name": "format", "in": "query" + }, + { + "type": "string", + "description": "Alert rule UID", + "name": "UID", + "in": "path", + "required": true } ], "responses": { @@ -2757,6 +2757,58 @@ } } }, + "/api/v1/provisioning/contact-points/export": { + "get": { + "tags": [ + "provisioning" + ], + "summary": "Export all contact points in provisioning file format.", + "operationId": "RouteGetContactpointsExport", + "parameters": [ + { + "type": "boolean", + "default": false, + "description": "Whether to initiate a download of the file or not.", + "name": "download", + "in": "query" + }, + { + "type": "string", + "default": "yaml", + "description": "Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence.", + "name": "format", + "in": "query" + }, + { + "type": "boolean", + "default": false, + "description": "Whether any contained secure settings should be decrypted or left redacted. Redacted settings will contain RedactedValue instead. Currently, only org admin can view decrypted secure settings.", + "name": "decrypt", + "in": "query" + }, + { + "type": "string", + "description": "Filter by name", + "name": "name", + "in": "query" + } + ], + "responses": { + "200": { + "description": "AlertingFileExport", + "schema": { + "$ref": "#/definitions/AlertingFileExport" + } + }, + "403": { + "description": "PermissionDenied", + "schema": { + "$ref": "#/definitions/PermissionDenied" + } + } + } + } + }, "/api/v1/provisioning/contact-points/{UID}": { "put": { "consumes": [ @@ -2915,18 +2967,6 @@ "summary": "Export an alert rule group in provisioning file format.", "operationId": "RouteGetAlertRuleGroupExport", "parameters": [ - { - "type": "string", - "name": "FolderUID", - "in": "path", - "required": true - }, - { - "type": "string", - "name": "Group", - "in": "path", - "required": true - }, { "type": "boolean", "default": false, @@ -2940,6 +2980,18 @@ "description": "Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence.", "name": "format", "in": "query" + }, + { + "type": "string", + "name": "FolderUID", + "in": "path", + "required": true + }, + { + "type": "string", + "name": "Group", + "in": "path", + "required": true } ], "responses": { @@ -3164,6 +3216,29 @@ } } }, + "/api/v1/provisioning/policies/export": { + "get": { + "tags": [ + "provisioning" + ], + "summary": "Export the notification policy tree in provisioning file format.", + "operationId": "RouteGetPolicyTreeExport", + "responses": { + "200": { + "description": "AlertingFileExport", + "schema": { + "$ref": "#/definitions/AlertingFileExport" + } + }, + "404": { + "description": "NotFound", + "schema": { + "$ref": "#/definitions/NotFound" + } + } + } + } + }, "/api/v1/provisioning/templates": { "get": { "tags": [ @@ -8748,7 +8823,7 @@ "type": "integer", "format": "int64" }, - "description": "List of dashboard id’s to search for", + "description": "List of dashboard id’s to search for\nThis is deprecated: users should use the `dashboardUIDs` query parameter instead", "name": "dashboardIds", "in": "query" }, @@ -8767,10 +8842,19 @@ "type": "integer", "format": "int64" }, - "description": "List of folder id’s to search in for dashboards", + "description": "List of folder id’s to search in for dashboards\nIf it's `0` then it will query for the top level folders\nThis is deprecated: users should use the `folderUIDs` query parameter instead", "name": "folderIds", "in": "query" }, + { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of folder UID’s to search in for dashboards\nIf it's an empty string then it will query for the top level folders", + "name": "folderUIDs", + "in": "query" + }, { "type": "boolean", "description": "Flag indicating if only starred Dashboards should be returned", @@ -11584,11 +11668,23 @@ "type": "integer", "format": "int64" }, + "contactPoints": { + "type": "array", + "items": { + "$ref": "#/definitions/ContactPointExport" + } + }, "groups": { "type": "array", "items": { "$ref": "#/definitions/AlertRuleGroupExport" } + }, + "policies": { + "type": "array", + "items": { + "$ref": "#/definitions/NotificationPolicyExport" + } } } }, @@ -12019,12 +12115,49 @@ } } }, + "ContactPointExport": { + "type": "object", + "title": "ContactPointExport is the provisioned file export of alerting.ContactPointV1.", + "properties": { + "name": { + "type": "string" + }, + "orgId": { + "type": "integer", + "format": "int64" + }, + "receivers": { + "type": "array", + "items": { + "$ref": "#/definitions/ReceiverExport" + } + } + } + }, "ContactPoints": { "type": "array", "items": { "$ref": "#/definitions/EmbeddedContactPoint" } }, + "CookiePreferences": { + "type": "object", + "title": "CookiePreferences defines model for CookiePreferences.", + "properties": { + "analytics": { + "type": "object", + "additionalProperties": false + }, + "functional": { + "type": "object", + "additionalProperties": false + }, + "performance": { + "type": "object", + "additionalProperties": false + } + } + }, "CookieType": { "type": "string" }, @@ -12531,6 +12664,9 @@ "type": "integer", "format": "int64" }, + "folderUid": { + "type": "string" + }, "inherited": { "type": "boolean" }, @@ -13763,6 +13899,10 @@ "description": "PathSeparator defines the separator pattern to decode a hierarchy. The default separator is '/'.", "type": "string" }, + "preferredVisualisationPluginId": { + "description": "PreferredVisualizationPluginId sets the panel plugin id to use to render the data when using Explore. If\nthe plugin cannot be found will fall back to PreferredVisualization.", + "type": "string" + }, "preferredVisualisationType": { "$ref": "#/definitions/VisType" }, @@ -15216,6 +15356,19 @@ "format": "int64", "title": "NoticeSeverity is a type for the Severity property of a Notice." }, + "NotificationPolicyExport": { + "type": "object", + "title": "NotificationPolicyExport is the provisioned file export of alerting.NotificiationPolicyV1.", + "properties": { + "Policy": { + "$ref": "#/definitions/RouteExport" + }, + "orgId": { + "type": "integer", + "format": "int64" + } + } + }, "NotificationTemplate": { "type": "object", "properties": { @@ -16748,6 +16901,24 @@ } } }, + "ReceiverExport": { + "type": "object", + "title": "ReceiverExport is the provisioned file export of alerting.ReceiverV1.", + "properties": { + "disableResolveMessage": { + "type": "boolean" + }, + "settings": { + "$ref": "#/definitions/RawMessage" + }, + "type": { + "type": "string" + }, + "uid": { + "type": "string" + } + } + }, "RecordingRuleJSON": { "description": "RecordingRuleJSON is the external representation of a recording rule", "type": "object", @@ -17010,6 +17181,61 @@ } } }, + "RouteExport": { + "description": "RouteExport is the provisioned file export of definitions.Route. This is needed to hide fields that aren't useable in\nprovisioning file format. An alternative would be to define a custom MarshalJSON and MarshalYAML that excludes them.", + "type": "object", + "properties": { + "continue": { + "type": "boolean" + }, + "group_by": { + "type": "array", + "items": { + "type": "string" + } + }, + "group_interval": { + "type": "string" + }, + "group_wait": { + "type": "string" + }, + "match": { + "description": "Deprecated. Remove before v1.0 release.", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "match_re": { + "$ref": "#/definitions/MatchRegexps" + }, + "matchers": { + "$ref": "#/definitions/Matchers" + }, + "mute_time_intervals": { + "type": "array", + "items": { + "type": "string" + } + }, + "object_matchers": { + "$ref": "#/definitions/ObjectMatchers" + }, + "receiver": { + "type": "string" + }, + "repeat_interval": { + "type": "string" + }, + "routes": { + "type": "array", + "items": { + "$ref": "#/definitions/RouteExport" + } + } + } + }, "Rule": { "description": "adapted from cortex", "type": "object", @@ -17814,6 +18040,9 @@ "type": "object", "title": "Spec defines model for Spec.", "properties": { + "cookiePreferences": { + "$ref": "#/definitions/CookiePreferences" + }, "homeDashboardUID": { "description": "UID for the home dashboard", "type": "string" @@ -19568,6 +19797,7 @@ } }, "gettableAlert": { + "description": "GettableAlert gettable alert", "type": "object", "required": [ "labels", @@ -19828,7 +20058,6 @@ } }, "postableSilence": { - "description": "PostableSilence postable silence", "type": "object", "required": [ "comment", @@ -19866,6 +20095,7 @@ } }, "receiver": { + "description": "Receiver receiver", "type": "object", "required": [ "active", diff --git a/public/app/features/search/service/sql.test.ts b/public/app/features/search/service/sql.test.ts index c788381f4e6..4298c7f9216 100644 --- a/public/app/features/search/service/sql.test.ts +++ b/public/app/features/search/service/sql.test.ts @@ -28,7 +28,7 @@ describe('SQLSearcher', () => { sort: query.sort, tag: undefined, type: DashboardSearchItemType.DashDB, - folderIds: [0], + folderUIDs: ['General'], }); }); @@ -49,7 +49,7 @@ describe('SQLSearcher', () => { sort: query.sort, tag: undefined, type: DashboardSearchItemType.DashFolder, - folderIds: [0], + folderUIDs: ['any'], }); }); @@ -71,7 +71,7 @@ describe('SQLSearcher', () => { query: query.query, tag: undefined, type: DashboardSearchItemType.DashFolder, - folderIds: [0], + folderUIDs: ['any'], }); }); diff --git a/public/app/features/search/service/sql.ts b/public/app/features/search/service/sql.ts index 245d6159a72..372883537cc 100644 --- a/public/app/features/search/service/sql.ts +++ b/public/app/features/search/service/sql.ts @@ -20,6 +20,7 @@ interface APIQuery { // DashboardIds []int64 dashboardUID?: string[]; folderIds?: number[]; + folderUIDs?: string[]; sort?: string; starred?: boolean; } @@ -54,13 +55,7 @@ export class SQLSearcher implements GrafanaSearcher { if (query.uid) { apiQuery.dashboardUID = query.uid; } else if (query.location?.length) { - let info = this.locationInfo[query.location]; - if (!info) { - // This will load all folder folders - await this.doAPIQuery({ type: DashboardSearchItemType.DashFolder, limit: 999 }); - info = this.locationInfo[query.location]; - } - apiQuery.folderIds = [info?.folderId ?? 0]; + apiQuery.folderUIDs = [query.location]; } return apiQuery; diff --git a/public/openapi3.json b/public/openapi3.json index da61c26763b..be89f9fd356 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -2680,11 +2680,23 @@ "format": "int64", "type": "integer" }, + "contactPoints": { + "items": { + "$ref": "#/components/schemas/ContactPointExport" + }, + "type": "array" + }, "groups": { "items": { "$ref": "#/components/schemas/AlertRuleGroupExport" }, "type": "array" + }, + "policies": { + "items": { + "$ref": "#/components/schemas/NotificationPolicyExport" + }, + "type": "array" } }, "title": "AlertingFileExport is the full provisioned file export.", @@ -3117,12 +3129,49 @@ }, "type": "object" }, + "ContactPointExport": { + "properties": { + "name": { + "type": "string" + }, + "orgId": { + "format": "int64", + "type": "integer" + }, + "receivers": { + "items": { + "$ref": "#/components/schemas/ReceiverExport" + }, + "type": "array" + } + }, + "title": "ContactPointExport is the provisioned file export of alerting.ContactPointV1.", + "type": "object" + }, "ContactPoints": { "items": { "$ref": "#/components/schemas/EmbeddedContactPoint" }, "type": "array" }, + "CookiePreferences": { + "properties": { + "analytics": { + "additionalProperties": false, + "type": "object" + }, + "functional": { + "additionalProperties": false, + "type": "object" + }, + "performance": { + "additionalProperties": false, + "type": "object" + } + }, + "title": "CookiePreferences defines model for CookiePreferences.", + "type": "object" + }, "CookieType": { "type": "string" }, @@ -3628,6 +3677,9 @@ "format": "int64", "type": "integer" }, + "folderUid": { + "type": "string" + }, "inherited": { "type": "boolean" }, @@ -4859,6 +4911,10 @@ "description": "PathSeparator defines the separator pattern to decode a hierarchy. The default separator is '/'.", "type": "string" }, + "preferredVisualisationPluginId": { + "description": "PreferredVisualizationPluginId sets the panel plugin id to use to render the data when using Explore. If\nthe plugin cannot be found will fall back to PreferredVisualization.", + "type": "string" + }, "preferredVisualisationType": { "$ref": "#/components/schemas/VisType" }, @@ -6314,6 +6370,19 @@ "title": "NoticeSeverity is a type for the Severity property of a Notice.", "type": "integer" }, + "NotificationPolicyExport": { + "properties": { + "Policy": { + "$ref": "#/components/schemas/RouteExport" + }, + "orgId": { + "format": "int64", + "type": "integer" + } + }, + "title": "NotificationPolicyExport is the provisioned file export of alerting.NotificiationPolicyV1.", + "type": "object" + }, "NotificationTemplate": { "properties": { "name": { @@ -7845,6 +7914,24 @@ "title": "Receiver configuration provides configuration on how to contact a receiver.", "type": "object" }, + "ReceiverExport": { + "properties": { + "disableResolveMessage": { + "type": "boolean" + }, + "settings": { + "$ref": "#/components/schemas/RawMessage" + }, + "type": { + "type": "string" + }, + "uid": { + "type": "string" + } + }, + "title": "ReceiverExport is the provisioned file export of alerting.ReceiverV1.", + "type": "object" + }, "RecordingRuleJSON": { "description": "RecordingRuleJSON is the external representation of a recording rule", "properties": { @@ -8107,6 +8194,61 @@ }, "type": "object" }, + "RouteExport": { + "description": "RouteExport is the provisioned file export of definitions.Route. This is needed to hide fields that aren't useable in\nprovisioning file format. An alternative would be to define a custom MarshalJSON and MarshalYAML that excludes them.", + "properties": { + "continue": { + "type": "boolean" + }, + "group_by": { + "items": { + "type": "string" + }, + "type": "array" + }, + "group_interval": { + "type": "string" + }, + "group_wait": { + "type": "string" + }, + "match": { + "additionalProperties": { + "type": "string" + }, + "description": "Deprecated. Remove before v1.0 release.", + "type": "object" + }, + "match_re": { + "$ref": "#/components/schemas/MatchRegexps" + }, + "matchers": { + "$ref": "#/components/schemas/Matchers" + }, + "mute_time_intervals": { + "items": { + "type": "string" + }, + "type": "array" + }, + "object_matchers": { + "$ref": "#/components/schemas/ObjectMatchers" + }, + "receiver": { + "type": "string" + }, + "repeat_interval": { + "type": "string" + }, + "routes": { + "items": { + "$ref": "#/components/schemas/RouteExport" + }, + "type": "array" + } + }, + "type": "object" + }, "Rule": { "description": "adapted from cortex", "properties": { @@ -8908,6 +9050,9 @@ }, "Spec": { "properties": { + "cookiePreferences": { + "$ref": "#/components/schemas/CookiePreferences" + }, "homeDashboardUID": { "description": "UID for the home dashboard", "type": "string" @@ -10664,6 +10809,7 @@ "type": "object" }, "gettableAlert": { + "description": "GettableAlert gettable alert", "properties": { "annotations": { "$ref": "#/components/schemas/labelSet" @@ -10924,7 +11070,6 @@ "type": "array" }, "postableSilence": { - "description": "PostableSilence postable silence", "properties": { "comment": { "description": "comment", @@ -10962,6 +11107,7 @@ "type": "object" }, "receiver": { + "description": "Receiver receiver", "properties": { "active": { "description": "active", @@ -13954,15 +14100,6 @@ "get": { "operationId": "RouteGetAlertRuleExport", "parameters": [ - { - "description": "Alert rule UID", - "in": "path", - "name": "UID", - "required": true, - "schema": { - "type": "string" - } - }, { "description": "Whether to initiate a download of the file or not.", "in": "query", @@ -13980,6 +14117,15 @@ "default": "yaml", "type": "string" } + }, + { + "description": "Alert rule UID", + "in": "path", + "name": "UID", + "required": true, + "schema": { + "type": "string" + } } ], "responses": { @@ -14083,6 +14229,74 @@ ] } }, + "/api/v1/provisioning/contact-points/export": { + "get": { + "operationId": "RouteGetContactpointsExport", + "parameters": [ + { + "description": "Whether to initiate a download of the file or not.", + "in": "query", + "name": "download", + "schema": { + "default": false, + "type": "boolean" + } + }, + { + "description": "Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence.", + "in": "query", + "name": "format", + "schema": { + "default": "yaml", + "type": "string" + } + }, + { + "description": "Whether any contained secure settings should be decrypted or left redacted. Redacted settings will contain RedactedValue instead. Currently, only org admin can view decrypted secure settings.", + "in": "query", + "name": "decrypt", + "schema": { + "default": false, + "type": "boolean" + } + }, + { + "description": "Filter by name", + "in": "query", + "name": "name", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AlertingFileExport" + } + } + }, + "description": "AlertingFileExport" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PermissionDenied" + } + } + }, + "description": "PermissionDenied" + } + }, + "summary": "Export all contact points in provisioning file format.", + "tags": [ + "provisioning" + ] + } + }, "/api/v1/provisioning/contact-points/{UID}": { "delete": { "operationId": "RouteDeleteContactpoints", @@ -14261,22 +14475,6 @@ "get": { "operationId": "RouteGetAlertRuleGroupExport", "parameters": [ - { - "in": "path", - "name": "FolderUID", - "required": true, - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "Group", - "required": true, - "schema": { - "type": "string" - } - }, { "description": "Whether to initiate a download of the file or not.", "in": "query", @@ -14294,6 +14492,22 @@ "default": "yaml", "type": "string" } + }, + { + "in": "path", + "name": "FolderUID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "Group", + "required": true, + "schema": { + "type": "string" + } } ], "responses": { @@ -14575,6 +14789,37 @@ ] } }, + "/api/v1/provisioning/policies/export": { + "get": { + "operationId": "RouteGetPolicyTreeExport", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AlertingFileExport" + } + } + }, + "description": "AlertingFileExport" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFound" + } + } + }, + "description": "NotFound" + } + }, + "summary": "Export the notification policy tree in provisioning file format.", + "tags": [ + "provisioning" + ] + } + }, "/api/v1/provisioning/templates": { "get": { "operationId": "RouteGetTemplates", @@ -20566,7 +20811,7 @@ } }, { - "description": "List of dashboard id’s to search for", + "description": "List of dashboard id’s to search for\nThis is deprecated: users should use the `dashboardUIDs` query parameter instead", "in": "query", "name": "dashboardIds", "schema": { @@ -20589,7 +20834,7 @@ } }, { - "description": "List of folder id’s to search in for dashboards", + "description": "List of folder id’s to search in for dashboards\nIf it's `0` then it will query for the top level folders\nThis is deprecated: users should use the `folderUIDs` query parameter instead", "in": "query", "name": "folderIds", "schema": { @@ -20600,6 +20845,17 @@ "type": "array" } }, + { + "description": "List of folder UID’s to search in for dashboards\nIf it's an empty string then it will query for the top level folders", + "in": "query", + "name": "folderUIDs", + "schema": { + "items": { + "type": "string" + }, + "type": "array" + } + }, { "description": "Flag indicating if only starred Dashboards should be returned", "in": "query",