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>
pull/72894/head
Josh Hunt 2 years ago committed by GitHub
parent 64ed77ddce
commit 7bc6d32eb9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 18
      pkg/api/search.go
  2. 11
      pkg/services/dashboards/database/database.go
  3. 133
      pkg/services/dashboards/database/database_test.go
  4. 1
      pkg/services/dashboards/models.go
  5. 2
      pkg/services/search/service.go
  6. 47
      pkg/services/sqlstore/searchstore/filters.go
  7. 59
      pkg/services/sqlstore/searchstore/filters_test.go
  8. 274
      public/api-merged.json
  9. 6
      public/app/features/search/service/sql.test.ts
  10. 9
      public/app/features/search/service/sql.ts
  11. 312
      public/openapi3.json

@ -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) 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, DashboardUIDs: dbUIDs,
Type: dashboardType, Type: dashboardType,
FolderIds: folderIDs, FolderIds: folderIDs,
FolderUIDs: folderUIDs,
Permission: permission, Permission: permission,
Sort: sort, Sort: sort,
} }
@ -136,17 +142,27 @@ type SearchParams struct {
// Enum: dash-folder,dash-db // Enum: dash-folder,dash-db
Type string `json:"type"` Type string `json:"type"`
// List of dashboard id’s to search for // List of dashboard id’s to search for
// This is deprecated: users should use the `dashboardUIDs` query parameter instead
// in:query // in:query
// required: false // required: false
// deprecated: true
DashboardIds []int64 `json:"dashboardIds"` DashboardIds []int64 `json:"dashboardIds"`
// List of dashboard uid’s to search for // List of dashboard uid’s to search for
// in:query // in:query
// required: false // required: false
DashboardUIDs []string `json:"dashboardUIDs"` DashboardUIDs []string `json:"dashboardUIDs"`
// List of folder id’s to search in for dashboards // 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 // in:query
// required: false // required: false
// deprecated: true
FolderIds []int64 `json:"folderIds"` 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 // Flag indicating if only starred Dashboards should be returned
// in:query // in:query
// required: false // required: false

@ -975,10 +975,13 @@ func (d *dashboardStore) FindDashboards(ctx context.Context, query *dashboards.F
filters = append(filters, query.Filters...) filters = append(filters, query.Filters...)
var orgID int64
if query.OrgId != 0 { 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 { } 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 { 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}) 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 var res []dashboards.DashboardSearchProjection
sb := &searchstore.Builder{Dialect: d.store.GetDialect(), Filters: filters} sb := &searchstore.Builder{Dialect: d.store.GetDialect(), Filters: filters}

@ -15,6 +15,7 @@ import (
"github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/expr"
"github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/dashboards" "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/org"
"github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/search/model" "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)) 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) { t.Run("Should be able to find dashboards by ids", func(t *testing.T) {
setup() setup()
query := dashboards.FindPersistedDashboardsQuery{ 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) { func insertTestRule(t *testing.T, sqlStore db.DB, foderOrgID int64, folderUID string) {
err := sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { err := sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error {
type alertQuery struct { type alertQuery struct {

@ -480,6 +480,7 @@ type FindPersistedDashboardsQuery struct {
DashboardUIDs []string DashboardUIDs []string
Type string Type string
FolderIds []int64 FolderIds []int64
FolderUIDs []string
Tags []string Tags []string
Limit int64 Limit int64
Page int64 Page int64

@ -38,6 +38,7 @@ type Query struct {
DashboardUIDs []string DashboardUIDs []string
DashboardIds []int64 DashboardIds []int64
FolderIds []int64 FolderIds []int64
FolderUIDs []string
Permission dashboards.PermissionType Permission dashboards.PermissionType
Sort string Sort string
} }
@ -83,6 +84,7 @@ func (s *SearchService) SearchHandler(ctx context.Context, query *Query) (model.
DashboardIds: query.DashboardIds, DashboardIds: query.DashboardIds,
Type: query.Type, Type: query.Type,
FolderIds: query.FolderIds, FolderIds: query.FolderIds,
FolderUIDs: query.FolderUIDs,
Tags: query.Tags, Tags: query.Tags,
Limit: query.Limit, Limit: query.Limit,
Page: query.Page, Page: query.Page,

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator" "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) 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 { type DashboardIDFilter struct {
IDs []int64 IDs []int64
} }

@ -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)
})
}
}

@ -2664,13 +2664,6 @@
"summary": "Export an alert rule in provisioning file format.", "summary": "Export an alert rule in provisioning file format.",
"operationId": "RouteGetAlertRuleExport", "operationId": "RouteGetAlertRuleExport",
"parameters": [ "parameters": [
{
"type": "string",
"description": "Alert rule UID",
"name": "UID",
"in": "path",
"required": true
},
{ {
"type": "boolean", "type": "boolean",
"default": false, "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.", "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", "name": "format",
"in": "query" "in": "query"
},
{
"type": "string",
"description": "Alert rule UID",
"name": "UID",
"in": "path",
"required": true
} }
], ],
"responses": { "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}": { "/api/v1/provisioning/contact-points/{UID}": {
"put": { "put": {
"consumes": [ "consumes": [
@ -2915,18 +2967,6 @@
"summary": "Export an alert rule group in provisioning file format.", "summary": "Export an alert rule group in provisioning file format.",
"operationId": "RouteGetAlertRuleGroupExport", "operationId": "RouteGetAlertRuleGroupExport",
"parameters": [ "parameters": [
{
"type": "string",
"name": "FolderUID",
"in": "path",
"required": true
},
{
"type": "string",
"name": "Group",
"in": "path",
"required": true
},
{ {
"type": "boolean", "type": "boolean",
"default": false, "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.", "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", "name": "format",
"in": "query" "in": "query"
},
{
"type": "string",
"name": "FolderUID",
"in": "path",
"required": true
},
{
"type": "string",
"name": "Group",
"in": "path",
"required": true
} }
], ],
"responses": { "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": { "/api/v1/provisioning/templates": {
"get": { "get": {
"tags": [ "tags": [
@ -8748,7 +8823,7 @@
"type": "integer", "type": "integer",
"format": "int64" "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", "name": "dashboardIds",
"in": "query" "in": "query"
}, },
@ -8767,10 +8842,19 @@
"type": "integer", "type": "integer",
"format": "int64" "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", "name": "folderIds",
"in": "query" "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", "type": "boolean",
"description": "Flag indicating if only starred Dashboards should be returned", "description": "Flag indicating if only starred Dashboards should be returned",
@ -11584,11 +11668,23 @@
"type": "integer", "type": "integer",
"format": "int64" "format": "int64"
}, },
"contactPoints": {
"type": "array",
"items": {
"$ref": "#/definitions/ContactPointExport"
}
},
"groups": { "groups": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/AlertRuleGroupExport" "$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": { "ContactPoints": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/EmbeddedContactPoint" "$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": { "CookieType": {
"type": "string" "type": "string"
}, },
@ -12531,6 +12664,9 @@
"type": "integer", "type": "integer",
"format": "int64" "format": "int64"
}, },
"folderUid": {
"type": "string"
},
"inherited": { "inherited": {
"type": "boolean" "type": "boolean"
}, },
@ -13763,6 +13899,10 @@
"description": "PathSeparator defines the separator pattern to decode a hierarchy. The default separator is '/'.", "description": "PathSeparator defines the separator pattern to decode a hierarchy. The default separator is '/'.",
"type": "string" "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": { "preferredVisualisationType": {
"$ref": "#/definitions/VisType" "$ref": "#/definitions/VisType"
}, },
@ -15216,6 +15356,19 @@
"format": "int64", "format": "int64",
"title": "NoticeSeverity is a type for the Severity property of a Notice." "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": { "NotificationTemplate": {
"type": "object", "type": "object",
"properties": { "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": { "RecordingRuleJSON": {
"description": "RecordingRuleJSON is the external representation of a recording rule", "description": "RecordingRuleJSON is the external representation of a recording rule",
"type": "object", "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": { "Rule": {
"description": "adapted from cortex", "description": "adapted from cortex",
"type": "object", "type": "object",
@ -17814,6 +18040,9 @@
"type": "object", "type": "object",
"title": "Spec defines model for Spec.", "title": "Spec defines model for Spec.",
"properties": { "properties": {
"cookiePreferences": {
"$ref": "#/definitions/CookiePreferences"
},
"homeDashboardUID": { "homeDashboardUID": {
"description": "UID for the home dashboard", "description": "UID for the home dashboard",
"type": "string" "type": "string"
@ -19568,6 +19797,7 @@
} }
}, },
"gettableAlert": { "gettableAlert": {
"description": "GettableAlert gettable alert",
"type": "object", "type": "object",
"required": [ "required": [
"labels", "labels",
@ -19828,7 +20058,6 @@
} }
}, },
"postableSilence": { "postableSilence": {
"description": "PostableSilence postable silence",
"type": "object", "type": "object",
"required": [ "required": [
"comment", "comment",
@ -19866,6 +20095,7 @@
} }
}, },
"receiver": { "receiver": {
"description": "Receiver receiver",
"type": "object", "type": "object",
"required": [ "required": [
"active", "active",

@ -28,7 +28,7 @@ describe('SQLSearcher', () => {
sort: query.sort, sort: query.sort,
tag: undefined, tag: undefined,
type: DashboardSearchItemType.DashDB, type: DashboardSearchItemType.DashDB,
folderIds: [0], folderUIDs: ['General'],
}); });
}); });
@ -49,7 +49,7 @@ describe('SQLSearcher', () => {
sort: query.sort, sort: query.sort,
tag: undefined, tag: undefined,
type: DashboardSearchItemType.DashFolder, type: DashboardSearchItemType.DashFolder,
folderIds: [0], folderUIDs: ['any'],
}); });
}); });
@ -71,7 +71,7 @@ describe('SQLSearcher', () => {
query: query.query, query: query.query,
tag: undefined, tag: undefined,
type: DashboardSearchItemType.DashFolder, type: DashboardSearchItemType.DashFolder,
folderIds: [0], folderUIDs: ['any'],
}); });
}); });

@ -20,6 +20,7 @@ interface APIQuery {
// DashboardIds []int64 // DashboardIds []int64
dashboardUID?: string[]; dashboardUID?: string[];
folderIds?: number[]; folderIds?: number[];
folderUIDs?: string[];
sort?: string; sort?: string;
starred?: boolean; starred?: boolean;
} }
@ -54,13 +55,7 @@ export class SQLSearcher implements GrafanaSearcher {
if (query.uid) { if (query.uid) {
apiQuery.dashboardUID = query.uid; apiQuery.dashboardUID = query.uid;
} else if (query.location?.length) { } else if (query.location?.length) {
let info = this.locationInfo[query.location]; apiQuery.folderUIDs = [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];
} }
return apiQuery; return apiQuery;

@ -2680,11 +2680,23 @@
"format": "int64", "format": "int64",
"type": "integer" "type": "integer"
}, },
"contactPoints": {
"items": {
"$ref": "#/components/schemas/ContactPointExport"
},
"type": "array"
},
"groups": { "groups": {
"items": { "items": {
"$ref": "#/components/schemas/AlertRuleGroupExport" "$ref": "#/components/schemas/AlertRuleGroupExport"
}, },
"type": "array" "type": "array"
},
"policies": {
"items": {
"$ref": "#/components/schemas/NotificationPolicyExport"
},
"type": "array"
} }
}, },
"title": "AlertingFileExport is the full provisioned file export.", "title": "AlertingFileExport is the full provisioned file export.",
@ -3117,12 +3129,49 @@
}, },
"type": "object" "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": { "ContactPoints": {
"items": { "items": {
"$ref": "#/components/schemas/EmbeddedContactPoint" "$ref": "#/components/schemas/EmbeddedContactPoint"
}, },
"type": "array" "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": { "CookieType": {
"type": "string" "type": "string"
}, },
@ -3628,6 +3677,9 @@
"format": "int64", "format": "int64",
"type": "integer" "type": "integer"
}, },
"folderUid": {
"type": "string"
},
"inherited": { "inherited": {
"type": "boolean" "type": "boolean"
}, },
@ -4859,6 +4911,10 @@
"description": "PathSeparator defines the separator pattern to decode a hierarchy. The default separator is '/'.", "description": "PathSeparator defines the separator pattern to decode a hierarchy. The default separator is '/'.",
"type": "string" "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": { "preferredVisualisationType": {
"$ref": "#/components/schemas/VisType" "$ref": "#/components/schemas/VisType"
}, },
@ -6314,6 +6370,19 @@
"title": "NoticeSeverity is a type for the Severity property of a Notice.", "title": "NoticeSeverity is a type for the Severity property of a Notice.",
"type": "integer" "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": { "NotificationTemplate": {
"properties": { "properties": {
"name": { "name": {
@ -7845,6 +7914,24 @@
"title": "Receiver configuration provides configuration on how to contact a receiver.", "title": "Receiver configuration provides configuration on how to contact a receiver.",
"type": "object" "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": { "RecordingRuleJSON": {
"description": "RecordingRuleJSON is the external representation of a recording rule", "description": "RecordingRuleJSON is the external representation of a recording rule",
"properties": { "properties": {
@ -8107,6 +8194,61 @@
}, },
"type": "object" "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": { "Rule": {
"description": "adapted from cortex", "description": "adapted from cortex",
"properties": { "properties": {
@ -8908,6 +9050,9 @@
}, },
"Spec": { "Spec": {
"properties": { "properties": {
"cookiePreferences": {
"$ref": "#/components/schemas/CookiePreferences"
},
"homeDashboardUID": { "homeDashboardUID": {
"description": "UID for the home dashboard", "description": "UID for the home dashboard",
"type": "string" "type": "string"
@ -10664,6 +10809,7 @@
"type": "object" "type": "object"
}, },
"gettableAlert": { "gettableAlert": {
"description": "GettableAlert gettable alert",
"properties": { "properties": {
"annotations": { "annotations": {
"$ref": "#/components/schemas/labelSet" "$ref": "#/components/schemas/labelSet"
@ -10924,7 +11070,6 @@
"type": "array" "type": "array"
}, },
"postableSilence": { "postableSilence": {
"description": "PostableSilence postable silence",
"properties": { "properties": {
"comment": { "comment": {
"description": "comment", "description": "comment",
@ -10962,6 +11107,7 @@
"type": "object" "type": "object"
}, },
"receiver": { "receiver": {
"description": "Receiver receiver",
"properties": { "properties": {
"active": { "active": {
"description": "active", "description": "active",
@ -13954,15 +14100,6 @@
"get": { "get": {
"operationId": "RouteGetAlertRuleExport", "operationId": "RouteGetAlertRuleExport",
"parameters": [ "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.", "description": "Whether to initiate a download of the file or not.",
"in": "query", "in": "query",
@ -13980,6 +14117,15 @@
"default": "yaml", "default": "yaml",
"type": "string" "type": "string"
} }
},
{
"description": "Alert rule UID",
"in": "path",
"name": "UID",
"required": true,
"schema": {
"type": "string"
}
} }
], ],
"responses": { "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}": { "/api/v1/provisioning/contact-points/{UID}": {
"delete": { "delete": {
"operationId": "RouteDeleteContactpoints", "operationId": "RouteDeleteContactpoints",
@ -14261,22 +14475,6 @@
"get": { "get": {
"operationId": "RouteGetAlertRuleGroupExport", "operationId": "RouteGetAlertRuleGroupExport",
"parameters": [ "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.", "description": "Whether to initiate a download of the file or not.",
"in": "query", "in": "query",
@ -14294,6 +14492,22 @@
"default": "yaml", "default": "yaml",
"type": "string" "type": "string"
} }
},
{
"in": "path",
"name": "FolderUID",
"required": true,
"schema": {
"type": "string"
}
},
{
"in": "path",
"name": "Group",
"required": true,
"schema": {
"type": "string"
}
} }
], ],
"responses": { "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": { "/api/v1/provisioning/templates": {
"get": { "get": {
"operationId": "RouteGetTemplates", "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", "in": "query",
"name": "dashboardIds", "name": "dashboardIds",
"schema": { "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", "in": "query",
"name": "folderIds", "name": "folderIds",
"schema": { "schema": {
@ -20600,6 +20845,17 @@
"type": "array" "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", "description": "Flag indicating if only starred Dashboards should be returned",
"in": "query", "in": "query",

Loading…
Cancel
Save