Chore: remove querylibrary feature toggle (#65021)

* chore: remove querylibrary

* chore: remove querylibrary

* chore: remove querylibrary
This commit is contained in:
Artur Wierzbicki 2023-03-20 20:00:14 +04:00 committed by GitHub
parent 68551ac9ca
commit 4274b9414f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 6 additions and 3527 deletions

View File

@ -3751,13 +3751,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Do not use any type assertions.", "3"]
],
"public/app/features/query-library/components/DatasourceTypePicker.tsx:5381": [
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"],
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "1"]
],
"public/app/features/query-library/components/SaveQueryWorkflowModal.tsx:5381": [
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
],
"public/app/features/query/components/QueryEditorRow.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],

2
.github/CODEOWNERS vendored
View File

@ -255,7 +255,6 @@
/pkg/services/live/ @grafana/grafana-app-platform-squad
/pkg/services/searchV2/ @grafana/grafana-app-platform-squad
/pkg/services/store/ @grafana/grafana-app-platform-squad
/pkg/services/querylibrary/ @grafana/grafana-app-platform-squad
/pkg/infra/filestorage/ @grafana/grafana-app-platform-squad
/pkg/util/converter/ @grafana/grafana-app-platform-squad
/pkg/modules/ @grafana/grafana-app-platform-squad
@ -373,7 +372,6 @@ lerna.json @grafana/frontend-ops
/public/app/features/profile/ @grafana/user-essentials
/public/app/features/runtime/ @ryantxu
/public/app/features/query/ @grafana/dashboards-squad
/public/app/features/query-library/ @grafana/dataviz-squad
/public/app/features/sandbox/ @grafana/user-essentials
/public/app/features/scenes/ @grafana/dashboards-squad
/public/app/features/search/ @grafana/user-essentials

View File

@ -106,5 +106,4 @@ The following toggles require explicitly setting Grafana's [app mode]({{< relref
| `dashboardsFromStorage` | Load dashboards from the generic storage interface |
| `grpcServer` | Run GRPC server |
| `entityStore` | SQL-based entity store (requires storage flag also) |
| `queryLibrary` | Reusable query library |
| `nestedFolders` | Enable folder nesting |

View File

@ -61,7 +61,6 @@ export interface FeatureToggles {
redshiftAsyncQueryDataSupport?: boolean;
athenaAsyncQueryDataSupport?: boolean;
newPanelChromeUI?: boolean;
queryLibrary?: boolean;
showDashboardValidationWarnings?: boolean;
mysqlAnsiQuotes?: boolean;
accessControlOnCall?: boolean;

View File

@ -309,10 +309,6 @@ func (hs *HTTPServer) registerRoutes() {
apiRoute.Group("/search-v2", hs.SearchV2HTTPService.RegisterHTTPRoutes)
}
if hs.QueryLibraryHTTPService != nil && !hs.QueryLibraryHTTPService.IsDisabled() {
apiRoute.Group("/query-library", hs.QueryLibraryHTTPService.RegisterHTTPRoutes)
}
// current org
apiRoute.Group("/org", func(orgRoute routing.RouteRegister) {
userIDScope := ac.Scope("users", "id", ac.Parameter(":userId"))

View File

@ -227,12 +227,6 @@ func (hs *HTTPServer) GetDashboard(c *contextmodel.ReqContext) response.Response
// make sure db version is in sync with json model version
dash.Data.Set("version", dash.Version)
if hs.QueryLibraryService != nil && !hs.QueryLibraryService.IsDisabled() {
if err := hs.QueryLibraryService.UpdateDashboardQueries(c.Req.Context(), c.SignedInUser, dash); err != nil {
return response.Error(500, "Error while loading saved queries", err)
}
}
dto := dtos.DashboardFullWithMeta{
Dashboard: dash.Data,
Meta: meta,

View File

@ -74,7 +74,6 @@ import (
publicdashboardsApi "github.com/grafana/grafana/pkg/services/publicdashboards/api"
"github.com/grafana/grafana/pkg/services/query"
"github.com/grafana/grafana/pkg/services/queryhistory"
"github.com/grafana/grafana/pkg/services/querylibrary"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/search"
@ -147,8 +146,6 @@ type HTTPServer struct {
StorageService store.StorageService
httpEntityStore httpentitystore.HTTPEntityStore
SearchV2HTTPService searchV2.SearchHTTPService
QueryLibraryHTTPService querylibrary.HTTPService
QueryLibraryService querylibrary.Service
ContextHandler *contexthandler.ContextHandler
SQLStore db.DB
AlertEngine *alerting.AlertEngine
@ -250,8 +247,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
publicDashboardsApi *publicdashboardsApi.Api, userService user.Service, tempUserService tempUser.Service,
loginAttemptService loginAttempt.Service, orgService org.Service, teamService team.Service,
accesscontrolService accesscontrol.Service, dashboardThumbsService thumbs.DashboardThumbService, navTreeService navtree.Service,
annotationRepo annotations.Repository, tagService tag.Service, searchv2HTTPService searchV2.SearchHTTPService,
queryLibraryHTTPService querylibrary.HTTPService, queryLibraryService querylibrary.Service, oauthTokenService oauthtoken.OAuthTokenService,
annotationRepo annotations.Repository, tagService tag.Service, searchv2HTTPService searchV2.SearchHTTPService, oauthTokenService oauthtoken.OAuthTokenService,
statsService stats.Service, authnService authn.Service, pluginsCDNService *pluginscdn.Service,
starApi *starApi.API,
) (*HTTPServer, error) {
@ -352,8 +348,6 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
accesscontrolService: accesscontrolService,
annotationsRepo: annotationRepo,
tagService: tagService,
QueryLibraryHTTPService: queryLibraryHTTPService,
QueryLibraryService: queryLibraryService,
oauthTokenService: oauthTokenService,
statsService: statsService,
authnService: authnService,

View File

@ -104,7 +104,7 @@ func TestIntegrationPluginManager(t *testing.T) {
pg := postgres.ProvideService(cfg)
my := mysql.ProvideService(cfg, hcp)
ms := mssql.ProvideService(cfg)
sv2 := searchV2.ProvideService(cfg, db.InitTestDB(t), nil, nil, tracer, features, nil, nil, nil, nil)
sv2 := searchV2.ProvideService(cfg, db.InitTestDB(t), nil, nil, tracer, features, nil, nil, nil)
graf := grafanads.ProvideService(sv2, nil)
phlare := phlare.ProvideService(hcp)
parca := parca.ProvideService(hcp)

View File

@ -97,7 +97,6 @@ import (
publicdashboardsService "github.com/grafana/grafana/pkg/services/publicdashboards/service"
"github.com/grafana/grafana/pkg/services/query"
"github.com/grafana/grafana/pkg/services/queryhistory"
"github.com/grafana/grafana/pkg/services/querylibrary/querylibraryimpl"
"github.com/grafana/grafana/pkg/services/quota/quotaimpl"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/search"
@ -263,8 +262,6 @@ var wireBasicSet = wire.NewSet(
secretsManager.ProvideSecretsService,
wire.Bind(new(secrets.Service), new(*secretsManager.SecretsService)),
secretsDatabase.ProvideSecretsStore,
querylibraryimpl.ProvideService,
querylibraryimpl.ProvideHTTPService,
wire.Bind(new(secrets.Store), new(*secretsDatabase.SecretsStoreImpl)),
secretsMigrator.ProvideSecretsMigrator,
wire.Bind(new(secrets.Migrator), new(*secretsMigrator.SecretsMigrator)),

View File

@ -289,13 +289,6 @@ var (
FrontendOnly: true,
Owner: grafanaDashboardsSquad,
},
{
Name: "queryLibrary",
Description: "Reusable query library",
State: FeatureStateAlpha,
RequiresDevMode: true,
Owner: grafanaAppPlatformSquad,
},
{
Name: "showDashboardValidationWarnings",
Description: "Show warnings when dashboards do not validate against the schema",

View File

@ -42,7 +42,6 @@ cloudWatchCrossAccountQuerying,stable,@grafana/aws-plugins,false,false,false,fal
redshiftAsyncQueryDataSupport,alpha,@grafana/aws-plugins,false,false,false,true
athenaAsyncQueryDataSupport,alpha,@grafana/aws-plugins,false,false,false,true
newPanelChromeUI,alpha,@grafana/dashboards-squad,false,false,false,true
queryLibrary,alpha,@grafana/grafana-app-platform-squad,true,false,false,false
showDashboardValidationWarnings,alpha,@grafana/dashboards-squad,false,false,false,false
mysqlAnsiQuotes,alpha,@grafana/backend-platform,false,false,false,false
accessControlOnCall,beta,@grafana/grafana-authnz-team,false,false,false,false

1 Name State Owner requiresDevMode RequiresLicense RequiresRestart FrontendOnly
42 redshiftAsyncQueryDataSupport alpha @grafana/aws-plugins false false false true
43 athenaAsyncQueryDataSupport alpha @grafana/aws-plugins false false false true
44 newPanelChromeUI alpha @grafana/dashboards-squad false false false true
queryLibrary alpha @grafana/grafana-app-platform-squad true false false false
45 showDashboardValidationWarnings alpha @grafana/dashboards-squad false false false false
46 mysqlAnsiQuotes alpha @grafana/backend-platform false false false false
47 accessControlOnCall beta @grafana/grafana-authnz-team false false false false

View File

@ -179,10 +179,6 @@ const (
// Show updated look and feel of grafana-ui PanelChrome: panel header, icons, and menu
FlagNewPanelChromeUI = "newPanelChromeUI"
// FlagQueryLibrary
// Reusable query library
FlagQueryLibrary = "queryLibrary"
// FlagShowDashboardValidationWarnings
// Show warnings when dashboards do not validate against the schema
FlagShowDashboardValidationWarnings = "showDashboardValidationWarnings"

View File

@ -15,7 +15,6 @@ const (
WeightSavedItems
WeightCreate
WeightDashboard
WeightQueryLibrary
WeightExplore
WeightAlerting
WeightDataConnections

View File

@ -18,7 +18,6 @@ import (
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
pref "github.com/grafana/grafana/pkg/services/preference"
"github.com/grafana/grafana/pkg/services/querylibrary"
"github.com/grafana/grafana/pkg/services/star"
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlesimpl"
"github.com/grafana/grafana/pkg/setting"
@ -36,7 +35,6 @@ type ServiceImpl struct {
accesscontrolService ac.Service
kvStore kvstore.KVStore
apiKeyService apikey.Service
queryLibraryService querylibrary.HTTPService
// Navigation
navigationAppConfig map[string]NavigationAppConfig
@ -50,7 +48,7 @@ type NavigationAppConfig struct {
Icon string
}
func ProvideService(cfg *setting.Cfg, accessControl ac.AccessControl, pluginStore plugins.Store, pluginSettings pluginsettings.Service, starService star.Service, features *featuremgmt.FeatureManager, dashboardService dashboards.DashboardService, accesscontrolService ac.Service, kvStore kvstore.KVStore, apiKeyService apikey.Service, queryLibraryService querylibrary.HTTPService) navtree.Service {
func ProvideService(cfg *setting.Cfg, accessControl ac.AccessControl, pluginStore plugins.Store, pluginSettings pluginsettings.Service, starService star.Service, features *featuremgmt.FeatureManager, dashboardService dashboards.DashboardService, accesscontrolService ac.Service, kvStore kvstore.KVStore, apiKeyService apikey.Service) navtree.Service {
service := &ServiceImpl{
cfg: cfg,
log: log.New("navtree service"),
@ -63,7 +61,6 @@ func ProvideService(cfg *setting.Cfg, accessControl ac.AccessControl, pluginStor
accesscontrolService: accesscontrolService,
kvStore: kvStore,
apiKeyService: apiKeyService,
queryLibraryService: queryLibraryService,
}
service.readNavigationSettings()
@ -129,18 +126,6 @@ func (s *ServiceImpl) GetNavTree(c *contextmodel.ReqContext, hasEditPerm bool, p
})
}
if !s.queryLibraryService.IsDisabled() {
treeRoot.AddSection(&navtree.NavLink{
Text: "Query Library",
Id: "query",
SubTitle: "Store, import, export and manage your team queries in an easy way.",
Icon: "file-search-alt",
SortWeight: navtree.WeightQueryLibrary,
Section: navtree.NavSectionCore,
Url: s.cfg.AppSubURL + "/query-library",
})
}
if setting.ProfileEnabled && c.IsSignedIn {
treeRoot.AddSection(s.getProfileNode(c))
}

View File

@ -1,87 +0,0 @@
package querylibraryimpl
import (
"encoding/json"
"fmt"
"io"
"strings"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/middleware"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/querylibrary"
)
type queriesServiceHTTPHandler struct {
service querylibrary.Service
}
func (s *queriesServiceHTTPHandler) IsDisabled() bool {
return s.service.IsDisabled()
}
func (s *queriesServiceHTTPHandler) delete(c *contextmodel.ReqContext) response.Response {
uid := c.Query("uid")
err := s.service.Delete(c.Req.Context(), c.SignedInUser, uid)
if err != nil {
return response.Error(500, fmt.Sprintf("error deleting query with id %s", uid), err)
}
return response.JSON(200, map[string]interface{}{
"success": true,
})
}
func (s *queriesServiceHTTPHandler) RegisterHTTPRoutes(routes routing.RouteRegister) {
reqSignedIn := middleware.ReqSignedIn
routes.Get("/", reqSignedIn, routing.Wrap(s.getBatch))
routes.Post("/", reqSignedIn, routing.Wrap(s.update))
routes.Delete("/", reqSignedIn, routing.Wrap(s.delete))
}
func (s *queriesServiceHTTPHandler) getBatch(c *contextmodel.ReqContext) response.Response {
uids := c.QueryStrings("uid")
queries, err := s.service.GetBatch(c.Req.Context(), c.SignedInUser, uids)
if err != nil {
return response.Error(500, fmt.Sprintf("error retrieving queries: [%s]", strings.Join(uids, ",")), err)
}
return response.JSON(200, queries)
}
func (s *queriesServiceHTTPHandler) update(c *contextmodel.ReqContext) response.Response {
body, err := io.ReadAll(c.Req.Body)
if err != nil {
return response.Error(500, "error reading bytes", err)
}
query := &querylibrary.Query{}
err = json.Unmarshal(body, query)
if err != nil {
return response.Error(400, "error parsing body", err)
}
if err := s.service.Update(c.Req.Context(), c.SignedInUser, query); err != nil {
var msg string
if len(query.UID) > 0 {
msg = fmt.Sprintf("error updating query with UID %s: %s", query.UID, err.Error())
} else {
msg = fmt.Sprintf("error updating query with: %s", err.Error())
}
return response.Error(500, msg, err)
}
return response.JSON(200, map[string]interface{}{
"success": true,
})
}
func ProvideHTTPService(
queriesService querylibrary.Service,
) querylibrary.HTTPService {
return &queriesServiceHTTPHandler{
service: queriesService,
}
}

View File

@ -1,298 +0,0 @@
package querylibraryimpl
import (
"context"
"fmt"
"strings"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/expr"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/x/persistentcollection"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/querylibrary"
"github.com/grafana/grafana/pkg/services/store/kind/dashboard"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles) querylibrary.Service {
return &service{
cfg: cfg,
log: log.New("queryLibraryService"),
features: features,
collection: persistentcollection.NewLocalFSPersistentCollection[*querylibrary.Query]("query-library", cfg.DataPath, 1),
}
}
type service struct {
cfg *setting.Cfg
features featuremgmt.FeatureToggles
log log.Logger
collection persistentcollection.PersistentCollection[*querylibrary.Query]
}
type perRequestQueryLoader struct {
service querylibrary.Service
queries map[string]*querylibrary.Query
ctx context.Context
user *user.SignedInUser
}
func (q *perRequestQueryLoader) byUID(uid string) (*querylibrary.Query, error) {
if q, ok := q.queries[uid]; ok {
return q, nil
}
queries, err := q.service.GetBatch(q.ctx, q.user, []string{uid})
if err != nil {
return nil, err
}
if len(queries) != 1 {
return nil, err
}
q.queries[uid] = queries[0]
return queries[0], nil
}
func newPerRequestQueryLoader(ctx context.Context, user *user.SignedInUser, service querylibrary.Service) queryLoader {
return &perRequestQueryLoader{queries: make(map[string]*querylibrary.Query), ctx: ctx, user: user, service: service}
}
type queryLoader interface {
byUID(uid string) (*querylibrary.Query, error)
}
func (s *service) UpdateDashboardQueries(ctx context.Context, user *user.SignedInUser, dash *dashboards.Dashboard) error {
queryLoader := newPerRequestQueryLoader(ctx, user, s)
return s.updateQueriesRecursively(queryLoader, dash.Data)
}
func (s *service) updateQueriesRecursively(loader queryLoader, parent *simplejson.Json) error {
panels := parent.Get("panels").MustArray()
for i := range panels {
panelAsJSON := simplejson.NewFromAny(panels[i])
panelType := panelAsJSON.Get("type").MustString()
if panelType == "row" {
err := s.updateQueriesRecursively(loader, panelAsJSON)
if err != nil {
return err
}
continue
}
queryUID := panelAsJSON.GetPath("savedQueryLink", "ref", "uid").MustString()
if queryUID == "" {
continue
}
query, err := loader.byUID(queryUID)
if err != nil {
return err
}
if query == nil {
// query deleted - unlink
panelAsJSON.Set("savedQueryLink", nil)
continue
}
queriesAsMap := make([]interface{}, 0)
for idx := range query.Queries {
queriesAsMap = append(queriesAsMap, query.Queries[idx].MustMap())
}
panelAsJSON.Set("targets", queriesAsMap)
isMixed, firstDsRef := isQueryWithMixedDataSource(query)
if isMixed {
panelAsJSON.Set("datasource", map[string]interface{}{
"uid": "-- Mixed --",
"type": "datasource",
})
} else {
panelAsJSON.Set("datasource", map[string]interface{}{
"uid": firstDsRef.UID,
"type": firstDsRef.Type,
})
}
}
return nil
}
func (s *service) IsDisabled() bool {
return !s.features.IsEnabled(featuremgmt.FlagQueryLibrary) || !s.features.IsEnabled(featuremgmt.FlagPanelTitleSearch)
}
func namespaceFromUser(user *user.SignedInUser) string {
return fmt.Sprintf("orgId-%d", user.OrgID)
}
func (s *service) Search(ctx context.Context, user *user.SignedInUser, options querylibrary.QuerySearchOptions) ([]querylibrary.QueryInfo, error) {
queries, err := s.collection.Find(ctx, namespaceFromUser(user), func(_ *querylibrary.Query) (bool, error) { return true, nil })
if err != nil {
return nil, err
}
queryInfo := asQueryInfo(queries)
filteredQueryInfo := make([]querylibrary.QueryInfo, 0)
for _, q := range queryInfo {
if len(options.Query) > 0 {
lowerTitle := strings.ReplaceAll(strings.ToLower(q.Title), " ", "")
lowerQuery := strings.ReplaceAll(strings.ToLower(options.Query), " ", "")
if !strings.Contains(lowerTitle, lowerQuery) {
continue
}
}
if len(options.DatasourceUID) > 0 || len(options.DatasourceType) > 0 {
dsUids := make(map[string]bool)
dsTypes := make(map[string]bool)
for _, ds := range q.Datasource {
dsUids[ds.UID] = true
dsTypes[ds.Type] = true
}
if len(options.DatasourceType) > 0 && !dsTypes[options.DatasourceType] {
continue
}
if len(options.DatasourceUID) > 0 && !dsUids[options.DatasourceUID] {
continue
}
}
filteredQueryInfo = append(filteredQueryInfo, q)
}
return filteredQueryInfo, nil
}
func asQueryInfo(queries []*querylibrary.Query) []querylibrary.QueryInfo {
res := make([]querylibrary.QueryInfo, 0)
for _, query := range queries {
res = append(res, querylibrary.QueryInfo{
UID: query.UID,
Title: query.Title,
Description: query.Description,
Tags: query.Tags,
TimeFrom: query.Time.From,
TimeTo: query.Time.To,
SchemaVersion: query.SchemaVersion,
Datasource: extractDataSources(query),
})
}
return res
}
func getDatasourceUID(q *simplejson.Json) string {
uid := q.Get("datasource").Get("uid").MustString()
if uid == "" {
uid = q.Get("datasource").MustString()
}
if expr.IsDataSource(uid) {
return expr.DatasourceUID
}
return uid
}
func isQueryWithMixedDataSource(q *querylibrary.Query) (isMixed bool, firstDsRef dashboard.DataSourceRef) {
dsRefs := extractDataSources(q)
for _, dsRef := range dsRefs {
if dsRef.Type == expr.DatasourceType {
continue
}
if firstDsRef.UID == "" {
firstDsRef = dsRef
continue
}
if firstDsRef.UID != dsRef.UID || firstDsRef.Type != dsRef.Type {
return true, firstDsRef
}
}
return false, firstDsRef
}
func extractDataSources(query *querylibrary.Query) []dashboard.DataSourceRef {
ds := make([]dashboard.DataSourceRef, 0)
for _, q := range query.Queries {
dsUid := getDatasourceUID(q)
dsType := q.Get("datasource").Get("type").MustString()
if expr.IsDataSource(dsUid) {
dsType = expr.DatasourceType
}
ds = append(ds, dashboard.DataSourceRef{
UID: dsUid,
Type: dsType,
})
}
return ds
}
func (s *service) GetBatch(ctx context.Context, user *user.SignedInUser, uids []string) ([]*querylibrary.Query, error) {
uidMap := make(map[string]bool)
for _, uid := range uids {
uidMap[uid] = true
}
return s.collection.Find(ctx, namespaceFromUser(user), func(q *querylibrary.Query) (bool, error) {
if _, ok := uidMap[q.UID]; ok {
return true, nil
}
return false, nil
})
}
func (s *service) Update(ctx context.Context, user *user.SignedInUser, query *querylibrary.Query) error {
if query.UID == "" {
queriesWithTheSameTitle, err := s.Search(ctx, user, querylibrary.QuerySearchOptions{Query: query.Title})
if err != nil {
return err
}
if len(queriesWithTheSameTitle) != 0 {
return fmt.Errorf("can't create query with title '%s'. existing query with similar name: '%s'", query.Title, queriesWithTheSameTitle[0].Title)
}
query.UID = util.GenerateShortUID()
return s.collection.Insert(ctx, namespaceFromUser(user), query)
}
_, err := s.collection.Update(ctx, namespaceFromUser(user), func(q *querylibrary.Query) (updated bool, updatedItem *querylibrary.Query, err error) {
if q.UID == query.UID {
return true, query, nil
}
return false, nil, nil
})
return err
}
func (s *service) Delete(ctx context.Context, user *user.SignedInUser, uid string) error {
_, err := s.collection.Delete(ctx, namespaceFromUser(user), func(q *querylibrary.Query) (bool, error) {
if q.UID == uid {
return true, nil
}
return false, nil
})
return err
}

View File

@ -1,286 +0,0 @@
package querylibrary_tests
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/querylibrary"
"github.com/grafana/grafana/pkg/services/user"
)
type queryLibraryAPIClient struct {
token string
url string
user *user.SignedInUser
sqlStore db.DB
}
func newQueryLibraryAPIClient(token string, baseUrl string, user *user.SignedInUser, sqlStore db.DB) *queryLibraryAPIClient {
return &queryLibraryAPIClient{
token: token,
url: baseUrl,
user: user,
sqlStore: sqlStore,
}
}
func (q *queryLibraryAPIClient) update(ctx context.Context, query *querylibrary.Query) error {
buf := bytes.Buffer{}
enc := json.NewEncoder(&buf)
err := enc.Encode(query)
if err != nil {
return err
}
url := fmt.Sprintf("%s/query-library", q.url)
req, err := http.NewRequestWithContext(ctx, "POST", url, &buf)
if err != nil {
return err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", q.token))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
_ = resp.Body.Close()
return nil
}
func (q *queryLibraryAPIClient) delete(ctx context.Context, uid string) error {
url := fmt.Sprintf("%s/query-library?uid=%s", q.url, uid)
req, err := http.NewRequestWithContext(ctx, "DELETE", url, bytes.NewBuffer([]byte("")))
if err != nil {
return err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", q.token))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
defer func() {
_ = resp.Body.Close()
}()
return err
}
func (q *queryLibraryAPIClient) get(ctx context.Context, uid string) (*querylibrary.Query, error) {
url := fmt.Sprintf("%s/query-library?uid=%s", q.url, uid)
req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer([]byte("")))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", q.token))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
query := make([]*querylibrary.Query, 0)
err = json.Unmarshal(b, &query)
if len(query) > 0 {
return query[0], err
}
return nil, err
}
type querySearchInfo struct {
kind string
uid string
name string
dsUIDs []string
location string
}
func (q *queryLibraryAPIClient) search(ctx context.Context, options querylibrary.QuerySearchOptions) ([]*querySearchInfo, error) {
return q.searchRetry(ctx, options, 1)
}
func (q *queryLibraryAPIClient) searchRetry(ctx context.Context, options querylibrary.QuerySearchOptions, attempt int) ([]*querySearchInfo, error) {
if attempt >= 3 {
return nil, errors.New("max attempts")
}
url := fmt.Sprintf("%s/search-v2", q.url)
text := "*"
if options.Query != "" {
text = options.Query
}
searchReq := map[string]interface{}{
"query": text,
"sort": "name_sort",
"kind": []string{"query"},
"limit": 50,
}
searchReqJson, err := simplejson.NewFromAny(searchReq).MarshalJSON()
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(searchReqJson))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", q.token))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
r := &backend.DataResponse{}
err = json.Unmarshal(b, r)
if len(r.Frames) != 1 {
return nil, fmt.Errorf("expected a single frame, received %s", string(b))
}
frame := r.Frames[0]
if frame.Name == "Loading" {
time.Sleep(100 * time.Millisecond)
return q.searchRetry(ctx, options, attempt+1)
}
res := make([]*querySearchInfo, 0)
frameLen, _ := frame.RowLen()
for i := 0; i < frameLen; i++ {
fKind, _ := frame.FieldByName("kind")
fUid, _ := frame.FieldByName("uid")
fName, _ := frame.FieldByName("name")
dsUID, _ := frame.FieldByName("ds_uid")
fLocation, _ := frame.FieldByName("location")
rawValue, ok := dsUID.At(i).(json.RawMessage)
if !ok || rawValue == nil {
return nil, errors.New("invalid ds_uid field")
}
jsonValue, err := rawValue.MarshalJSON()
if err != nil {
return nil, err
}
var uids []string
err = json.Unmarshal(jsonValue, &uids)
if err != nil {
return nil, err
}
res = append(res, &querySearchInfo{
kind: fKind.At(i).(string),
uid: fUid.At(i).(string),
name: fName.At(i).(string),
dsUIDs: uids,
location: fLocation.At(i).(string),
})
}
return res, err
}
func (q *queryLibraryAPIClient) getDashboard(ctx context.Context, uid string) (*dtos.DashboardFullWithMeta, error) {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/dashboards/uid/%s", q.url, uid), bytes.NewBuffer([]byte("")))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", q.token))
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
res := &dtos.DashboardFullWithMeta{}
err = json.Unmarshal(b, res)
if err != nil {
return nil, err
}
return res, nil
}
func (q *queryLibraryAPIClient) createDashboard(ctx context.Context, dash *simplejson.Json) (string, error) {
buf := bytes.Buffer{}
enc := json.NewEncoder(&buf)
dashMap, err := dash.Map()
if err != nil {
return "", err
}
err = enc.Encode(dashMap)
if err != nil {
return "", err
}
url := fmt.Sprintf("%s/dashboards/db", q.url)
req, err := http.NewRequestWithContext(ctx, "POST", url, &buf)
if err != nil {
return "", err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", q.token))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
defer func() {
_ = resp.Body.Close()
}()
if err != nil {
return "", err
}
jsonResp, err := simplejson.NewFromReader(resp.Body)
if err != nil {
return "", err
}
return jsonResp.Get("uid").MustString(), nil
}

View File

@ -1,74 +0,0 @@
package querylibrary_tests
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
apikeygenprefix "github.com/grafana/grafana/pkg/components/apikeygenprefixed"
"github.com/grafana/grafana/pkg/server"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
saAPI "github.com/grafana/grafana/pkg/services/serviceaccounts/api"
saTests "github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/tests/testinfra"
)
func createServiceAccountAdminToken(t *testing.T, name string, env *server.TestEnv) (string, *user.SignedInUser) {
t.Helper()
account := saTests.SetupUserServiceAccount(t, env.SQLStore, saTests.TestUser{
Name: name,
Role: string(org.RoleAdmin),
Login: name,
IsServiceAccount: true,
})
keyGen, err := apikeygenprefix.New(saAPI.ServiceID)
require.NoError(t, err)
_ = saTests.SetupApiKey(t, env.SQLStore, saTests.TestApiKey{
Name: name,
Role: org.RoleAdmin,
OrgId: account.OrgID,
Key: keyGen.HashedKey,
ServiceAccountID: &account.ID,
})
return keyGen.ClientSecret, &user.SignedInUser{
UserID: account.ID,
Email: account.Email,
Name: account.Name,
Login: account.Login,
OrgID: account.OrgID,
IsServiceAccount: true,
}
}
type testContext struct {
authToken string
client *queryLibraryAPIClient
user *user.SignedInUser
}
func createTestContext(t *testing.T) testContext {
t.Helper()
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
EnableFeatureToggles: []string{featuremgmt.FlagPanelTitleSearch, featuremgmt.FlagQueryLibrary},
QueryRetries: 3,
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
authToken, serviceAccountUser := createServiceAccountAdminToken(t, "query-library", env)
client := newQueryLibraryAPIClient(authToken, fmt.Sprintf("http://%s/api", grafanaListedAddr), serviceAccountUser, env.SQLStore)
return testContext{
authToken: authToken,
client: client,
user: serviceAccountUser,
}
}

View File

@ -1,299 +0,0 @@
package querylibrary_tests
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/services/querylibrary"
"github.com/grafana/grafana/pkg/tsdb/grafanads"
)
func TestIntegrationCreateAndDelete(t *testing.T) {
if true {
// TODO: re-enable after fixing its flakiness
t.Skip()
}
if testing.Short() {
t.Skip("skipping integration test")
}
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()
testCtx := createTestContext(t)
err := testCtx.client.update(ctx, &querylibrary.Query{
UID: "",
Title: "first query",
Tags: []string{},
Description: "",
Time: querylibrary.Time{
From: "now-15m",
To: "now-30m",
},
Queries: []*simplejson.Json{
simplejson.NewFromAny(map[string]interface{}{
"datasource": map[string]string{
"uid": grafanads.DatasourceUID,
"type": "datasource",
},
"queryType": "randomWalk",
"refId": "A",
}),
simplejson.NewFromAny(map[string]interface{}{
"datasource": map[string]string{
"uid": grafanads.DatasourceUID,
"type": "datasource",
},
"queryType": "list",
"path": "img",
"refId": "B",
}),
},
Variables: []*simplejson.Json{},
})
require.NoError(t, err)
search, err := testCtx.client.search(ctx, querylibrary.QuerySearchOptions{
Query: "",
})
require.NoError(t, err)
require.Len(t, search, 1)
info := search[0]
require.Equal(t, "query", info.kind)
require.Equal(t, "first query", info.name)
require.Equal(t, "General", info.location)
require.Equal(t, []string{grafanads.DatasourceUID, grafanads.DatasourceUID}, info.dsUIDs)
err = testCtx.client.delete(ctx, info.uid)
require.NoError(t, err)
search, err = testCtx.client.search(ctx, querylibrary.QuerySearchOptions{
Query: "",
})
require.NoError(t, err)
require.Len(t, search, 0)
query, err := testCtx.client.get(ctx, info.uid)
require.NoError(t, err)
require.Nil(t, query)
}
func createQuery(t *testing.T, ctx context.Context, testCtx testContext) string {
t.Helper()
err := testCtx.client.update(ctx, &querylibrary.Query{
UID: "",
Title: "first query",
Tags: []string{},
Description: "",
Time: querylibrary.Time{
From: "now-15m",
To: "now-30m",
},
Queries: []*simplejson.Json{
simplejson.NewFromAny(map[string]interface{}{
"datasource": map[string]string{
"uid": grafanads.DatasourceUID,
"type": "datasource",
},
"queryType": "randomWalk",
"refId": "A",
}),
simplejson.NewFromAny(map[string]interface{}{
"datasource": map[string]string{
"uid": grafanads.DatasourceUID,
"type": "datasource",
},
"queryType": "list",
"path": "img",
"refId": "B",
}),
},
Variables: []*simplejson.Json{},
})
require.NoError(t, err)
search, err := testCtx.client.search(ctx, querylibrary.QuerySearchOptions{
Query: "",
})
require.NoError(t, err)
require.Len(t, search, 1)
return search[0].uid
}
func TestIntegrationDashboardGetWithLatestSavedQueries(t *testing.T) {
if true {
// TODO: re-enable after fixing its flakiness
t.Skip()
}
if testing.Short() {
t.Skip("skipping integration test")
}
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()
testCtx := createTestContext(t)
queryUID := createQuery(t, ctx, testCtx)
dashUID, err := testCtx.client.createDashboard(ctx, simplejson.NewFromAny(map[string]interface{}{
"dashboard": map[string]interface{}{
"title": "my-new-dashboard",
"panels": []interface{}{
map[string]interface{}{
"id": int64(1),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
},
map[string]interface{}{
"id": int64(2),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 6,
"y": 0,
},
"savedQueryLink": map[string]interface{}{
"ref": map[string]string{
"uid": queryUID,
},
},
},
},
},
"folderId": 0,
"message": "",
"overwrite": true,
}))
require.NoError(t, err)
dashboard, err := testCtx.client.getDashboard(ctx, dashUID)
require.NoError(t, err)
panelsAsArray, err := dashboard.Dashboard.Get("panels").Array()
require.NoError(t, err)
require.Len(t, panelsAsArray, 2)
secondPanel := simplejson.NewFromAny(panelsAsArray[1])
require.Equal(t, []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"uid": grafanads.DatasourceUID,
"type": "datasource",
},
"queryType": "randomWalk",
"refId": "A",
},
map[string]interface{}{
"datasource": map[string]interface{}{
"uid": grafanads.DatasourceUID,
"type": "datasource",
},
"queryType": "list",
"path": "img",
"refId": "B",
},
}, secondPanel.Get("targets").MustArray())
require.Equal(t, map[string]interface{}{
"uid": grafanads.DatasourceUID,
"type": "datasource",
}, secondPanel.Get("datasource").MustMap())
// update, expect changes when getting dashboards
err = testCtx.client.update(ctx, &querylibrary.Query{
UID: queryUID,
Title: "first query",
Tags: []string{},
Description: "",
Time: querylibrary.Time{
From: "now-15m",
To: "now-30m",
},
Queries: []*simplejson.Json{
simplejson.NewFromAny(map[string]interface{}{
"datasource": map[string]interface{}{
"uid": grafanads.DatasourceUID,
"type": "datasource",
},
"queryType": "randomWalk",
"refId": "A",
}),
simplejson.NewFromAny(map[string]interface{}{
"datasource": map[string]interface{}{
"uid": "different-datasource-uid",
"type": "datasource",
},
"queryType": "randomWalk",
"path": "img",
"refId": "B",
}),
simplejson.NewFromAny(map[string]interface{}{
"datasource": map[string]interface{}{
"uid": "different-datasource-uid-2",
"type": "datasource",
},
"queryType": "randomWalk",
"path": "img",
"refId": "C",
}),
},
Variables: []*simplejson.Json{},
})
require.NoError(t, err)
dashboard, err = testCtx.client.getDashboard(ctx, dashUID)
require.NoError(t, err)
panelsAsArray, err = dashboard.Dashboard.Get("panels").Array()
require.NoError(t, err)
require.Len(t, panelsAsArray, 2)
secondPanel = simplejson.NewFromAny(panelsAsArray[1])
require.Equal(t, []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"uid": grafanads.DatasourceUID,
"type": "datasource",
},
"queryType": "randomWalk",
"refId": "A",
},
map[string]interface{}{
"datasource": map[string]interface{}{
"uid": "different-datasource-uid",
"type": "datasource",
},
"queryType": "randomWalk",
"path": "img",
"refId": "B",
},
map[string]interface{}{
"datasource": map[string]interface{}{
"uid": "different-datasource-uid-2",
"type": "datasource",
},
"queryType": "randomWalk",
"path": "img",
"refId": "C",
},
}, secondPanel.Get("targets").MustArray())
require.Equal(t, map[string]interface{}{
"uid": "-- Mixed --",
"type": "datasource",
}, secondPanel.Get("datasource").MustMap())
}

View File

@ -1,88 +0,0 @@
package querylibrary
import (
"context"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/store/kind/dashboard"
"github.com/grafana/grafana/pkg/services/user"
)
type Time struct {
// From Start time in epoch timestamps in milliseconds or relative using Grafana time units.
// required: true
// example: now-1h
From string `json:"from"`
// To End time in epoch timestamps in milliseconds or relative using Grafana time units.
// required: true
// example: now
To string `json:"to"`
}
type Query struct {
UID string `json:"uid"`
Title string `json:"title"`
Tags []string `json:"tags"`
Description string `json:"description"`
SchemaVersion int64 `json:"schemaVersion"`
Time Time `json:"time"`
// queries.refId Specifies an identifier of the query. Is optional and default to “A”.
// queries.datasourceId Specifies the data source to be queried. Each query in the request must have an unique datasourceId.
// queries.maxDataPoints - Species maximum amount of data points that dashboard panel can render. Is optional and default to 100.
// queries.intervalMs - Specifies the time interval in milliseconds of time series. Is optional and defaults to 1000.
// required: true
// example: [ { "refId": "A", "intervalMs": 86400000, "maxDataPoints": 1092, "datasource":{ "uid":"PD8C576611E62080A" }, "rawSql": "SELECT 1 as valueOne, 2 as valueTwo", "format": "table" } ]
Queries []*simplejson.Json `json:"queries"`
Variables []*simplejson.Json `json:"variables"`
}
type SavedQueryRef struct {
UID string `json:"uid"`
}
type SavedQueryLink struct {
Ref SavedQueryRef `json:"ref"`
}
type QueryInfo struct {
UID string `json:"uid"`
Title string `json:"title"`
Description string `json:"description"`
Tags []string `json:"tags"`
TimeFrom string `json:"timeFrom"`
TimeTo string `json:"timeTo"`
SchemaVersion int64 `json:"schemaVersion"`
Datasource []dashboard.DataSourceRef `json:"datasource,omitempty"` // UIDs
}
type QuerySearchOptions struct {
DatasourceUID string
Query string
DatasourceType string
}
type Service interface {
Search(ctx context.Context, user *user.SignedInUser, options QuerySearchOptions) ([]QueryInfo, error)
GetBatch(ctx context.Context, user *user.SignedInUser, uids []string) ([]*Query, error)
Update(ctx context.Context, user *user.SignedInUser, query *Query) error
Delete(ctx context.Context, user *user.SignedInUser, uid string) error
UpdateDashboardQueries(ctx context.Context, user *user.SignedInUser, dash *dashboards.Dashboard) error
registry.CanBeDisabled
}
type HTTPService interface {
registry.CanBeDisabled
RegisterHTTPRoutes(routes routing.RouteRegister)
}

View File

@ -86,7 +86,7 @@ var (
func service(t *testing.T) *StandardSearchService {
service, ok := ProvideService(&setting.Cfg{Search: setting.SearchSettings{}},
nil, nil, accesscontrolmock.New(), tracing.InitializeTracerForTest(), featuremgmt.WithFeatures(),
nil, nil, nil, nil).(*StandardSearchService)
nil, nil, nil).(*StandardSearchService)
require.True(t, ok)
return service
}

View File

@ -1,113 +0,0 @@
package searchV2
import (
"context"
"encoding/json"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/services/querylibrary"
"github.com/grafana/grafana/pkg/services/user"
)
// TEMPORARY FILE
func (s *StandardSearchService) searchQueries(ctx context.Context, user *user.SignedInUser, q DashboardQuery) *backend.DataResponse {
queryText := q.Query
if queryText == "*" {
queryText = ""
}
queryInfo, err := s.queries.Search(ctx, user, querylibrary.QuerySearchOptions{
Query: queryText,
DatasourceUID: q.Datasource,
DatasourceType: q.DatasourceType,
})
if err != nil {
return &backend.DataResponse{Error: err}
}
header := &customMeta{
SortBy: q.Sort,
Count: uint64(len(queryInfo)),
}
fScore := data.NewFieldFromFieldType(data.FieldTypeFloat64, 0)
fUID := data.NewFieldFromFieldType(data.FieldTypeString, 0)
fKind := data.NewFieldFromFieldType(data.FieldTypeString, 0)
fPType := data.NewFieldFromFieldType(data.FieldTypeString, 0)
fName := data.NewFieldFromFieldType(data.FieldTypeString, 0)
fURL := data.NewFieldFromFieldType(data.FieldTypeString, 0)
fLocation := data.NewFieldFromFieldType(data.FieldTypeString, 0)
fTags := data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0)
fDSUIDs := data.NewFieldFromFieldType(data.FieldTypeJSON, 0)
fExplain := data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0)
fScore.Name = "score"
fUID.Name = "uid"
fKind.Name = "kind"
fName.Name = "name"
fLocation.Name = "location"
fURL.Name = "url"
fURL.Config = &data.FieldConfig{
Links: []data.DataLink{
{Title: "link", URL: "${__value.text}"},
},
}
fPType.Name = "panel_type"
fDSUIDs.Name = "ds_uid"
fTags.Name = "tags"
fExplain.Name = "explain"
frame := data.NewFrame("Query results", fKind, fUID, fName, fPType, fURL, fTags, fDSUIDs, fLocation)
if q.Explain {
frame.Fields = append(frame.Fields, fScore, fExplain)
}
frame.SetMeta(&data.FrameMeta{
Type: "search-results",
Custom: header,
})
fieldLen := 0
for _, q := range queryInfo {
fKind.Append(string(entityKindQuery))
fUID.Append(q.UID)
fPType.Append("")
fName.Append(q.Title)
fURL.Append("")
fLocation.Append("General")
tags := q.Tags
if tags == nil {
tags = make([]string, 0)
}
tagsJson := mustJsonRawMessage(tags)
fTags.Append(&tagsJson)
dsUids := make([]string, 0)
for _, dsRef := range q.Datasource {
dsUids = append(dsUids, dsRef.UID)
}
fDSUIDs.Append(mustJsonRawMessage(dsUids))
// extend fields to match the longest field
fieldLen++
for _, f := range frame.Fields {
if fieldLen > f.Len() {
f.Extend(fieldLen - f.Len())
}
}
}
return &backend.DataResponse{
Frames: data.Frames{frame},
}
}
func mustJsonRawMessage(arr []string) json.RawMessage {
js, _ := json.Marshal(arr)
return js
}

View File

@ -18,7 +18,6 @@ import (
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/querylibrary"
"github.com/grafana/grafana/pkg/services/store"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
@ -75,7 +74,6 @@ type StandardSearchService struct {
dashboardIndex *searchIndex
extender DashboardIndexExtender
reIndexCh chan struct{}
queries querylibrary.Service
features featuremgmt.FeatureToggles
}
@ -85,7 +83,7 @@ func (s *StandardSearchService) IsReady(ctx context.Context, orgId int64) IsSear
func ProvideService(cfg *setting.Cfg, sql db.DB, entityEventStore store.EntityEventsService,
ac accesscontrol.Service, tracer tracing.Tracer, features featuremgmt.FeatureToggles, orgService org.Service,
userService user.Service, queries querylibrary.Service, folderService folder.Service) SearchService {
userService user.Service, folderService folder.Service) SearchService {
extender := &NoopExtender{}
logger := log.New("searchV2")
s := &StandardSearchService{
@ -112,7 +110,6 @@ func ProvideService(cfg *setting.Cfg, sql db.DB, entityEventStore store.EntityEv
reIndexCh: make(chan struct{}, 1),
orgService: orgService,
userService: userService,
queries: queries,
features: features,
}
return s
@ -242,10 +239,6 @@ func (s *StandardSearchService) DoDashboardQuery(ctx context.Context, user *back
}
func (s *StandardSearchService) doDashboardQuery(ctx context.Context, signedInUser *user.SignedInUser, orgID int64, q DashboardQuery) *backend.DataResponse {
if !s.queries.IsDisabled() && len(q.Kind) == 1 && q.Kind[0] == string(entityKindQuery) {
return s.searchQueries(ctx, signedInUser, q)
}
rsp := &backend.DataResponse{}
filter, err := s.auth.GetDashboardReadFilter(ctx, orgID, signedInUser)

View File

@ -15,7 +15,6 @@ import (
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/org/orgtest"
"github.com/grafana/grafana/pkg/services/querylibrary/querylibraryimpl"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/store"
"github.com/grafana/grafana/pkg/services/user"
@ -37,9 +36,8 @@ func setupBenchEnv(b *testing.B, folderCount, dashboardsPerFolder int) (*Standar
orgSvc := &orgtest.FakeOrgService{
ExpectedOrgs: []*org.OrgDTO{{ID: 1}},
}
querySvc := querylibraryimpl.ProvideService(cfg, features)
searchService, ok := ProvideService(cfg, sqlStore, store.NewDummyEntityEventsService(), actest.FakeService{},
tracing.InitializeTracerForTest(), features, orgSvc, nil, querySvc, nil).(*StandardSearchService)
tracing.InitializeTracerForTest(), features, orgSvc, nil, nil).(*StandardSearchService)
require.True(b, ok)
err = runSearchService(searchService)

View File

@ -37,7 +37,6 @@ export class PanelEditorQueries extends PureComponent<Props> {
queries: panel.targets,
maxDataPoints: panel.maxDataPoints,
minInterval: panel.interval,
savedQueryUid: panel.savedQueryLink?.ref.uid ?? null, // Used by experimental feature queryLibrary
timeRange: {
from: panel.timeFrom,
shift: panel.timeShift,

View File

@ -27,7 +27,6 @@ import { LibraryPanel, LibraryPanelRef } from '@grafana/schema';
import config from 'app/core/config';
import { safeStringifyValue } from 'app/core/utils/explore';
import { getNextRefIdChar } from 'app/core/utils/query';
import { SavedQueryLink } from 'app/features/query-library/types';
import { QueryGroupOptions } from 'app/types';
import {
PanelOptionsChangedEvent,
@ -131,7 +130,6 @@ const defaults: any = {
overrides: [],
},
title: '',
savedQueryLink: null,
};
export class PanelModel implements DataConfigSource, IPanelModel {
@ -156,8 +154,6 @@ export class PanelModel implements DataConfigSource, IPanelModel {
datasource: DataSourceRef | null = null;
thresholds?: any;
pluginVersion?: string;
savedQueryLink: SavedQueryLink | null = null; // Used by the experimental feature queryLibrary
snapshotData?: DataFrameDTO[];
timeFrom?: any;
timeShift?: any;
@ -524,17 +520,6 @@ export class PanelModel implements DataConfigSource, IPanelModel {
type: dataSource.type,
};
if (options.savedQueryUid) {
this.savedQueryLink = {
ref: {
uid: options.savedQueryUid,
},
variables: [],
};
} else {
this.savedQueryLink = null;
}
this.cacheTimeout = options.cacheTimeout;
this.queryCachingTTL = options.queryCachingTTL;
this.timeFrom = options.timeRange?.from;

View File

@ -1,72 +0,0 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { DataQuery } from '@grafana/data/src';
import { SavedQueryUpdateOpts } from '../components/QueryEditorDrawer';
import { getSavedQuerySrv } from './SavedQueriesSrv';
export type SavedQueryRef = {
uid?: string;
};
export type Variable = {
name: string;
type?: string;
current: {
value: string | number;
};
};
type SavedQueryMeta = {
title: string;
description?: string;
tags?: string[];
schemaVersion?: number;
variables: Variable[];
};
type SavedQueryData<TQuery extends DataQuery = DataQuery> = {
queries: TQuery[];
};
export type SavedQuery<TQuery extends DataQuery = DataQuery> = SavedQueryMeta & SavedQueryData<TQuery> & SavedQueryRef;
export const isQueryWithMixedDatasource = (savedQuery: SavedQuery): boolean => {
if (!savedQuery?.queries?.length) {
return false;
}
const firstDs = savedQuery.queries[0].datasource;
return savedQuery.queries.some((q) => q.datasource?.uid !== firstDs?.uid || q.datasource?.type !== firstDs?.type);
};
const api = createApi({
reducerPath: 'savedQueries',
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
getSavedQueryByUids: build.query<SavedQuery[] | null, SavedQueryRef[]>({
async queryFn(arg, queryApi, extraOptions, baseQuery) {
return { data: await getSavedQuerySrv().getSavedQueries(arg) };
},
}),
deleteSavedQuery: build.mutation<null, SavedQueryRef>({
async queryFn(arg) {
await getSavedQuerySrv().deleteSavedQuery(arg);
return {
data: null,
};
},
}),
updateSavedQuery: build.mutation<null, { query: SavedQuery; opts: SavedQueryUpdateOpts }>({
async queryFn(arg) {
await getSavedQuerySrv().updateSavedQuery(arg.query, arg.opts);
return {
data: null,
};
},
}),
}),
});
export const { useUpdateSavedQueryMutation } = api;

View File

@ -1,26 +0,0 @@
import { getBackendSrv } from 'app/core/services/backend_srv';
import { SavedQueryUpdateOpts } from 'app/features/query-library/components/QueryEditorDrawer';
import { SavedQuery, SavedQueryRef } from './SavedQueriesApi';
export class SavedQuerySrv {
getSavedQueries = async (refs: SavedQueryRef[]): Promise<SavedQuery[]> => {
if (!refs.length) {
return [];
}
const uidParams = refs.map((r) => `uid=${r.uid}`).join('&');
return getBackendSrv().get<SavedQuery[]>(`/api/query-library?${uidParams}`);
};
deleteSavedQuery = async (ref: SavedQueryRef): Promise<void> => {
return getBackendSrv().delete(`/api/query-library?uid=${ref.uid}`);
};
updateSavedQuery = async (query: SavedQuery, options: SavedQueryUpdateOpts): Promise<void> => {
return getBackendSrv().post(`/api/query-library`, query);
};
}
const savedQuerySrv = new SavedQuerySrv();
export const getSavedQuerySrv = () => savedQuerySrv;

View File

@ -1,123 +0,0 @@
import { css } from '@emotion/css';
import React, { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Button, CodeEditor, useStyles2 } from '@grafana/ui';
import { SavedQuery, useUpdateSavedQueryMutation } from '../api/SavedQueriesApi';
import { SavedQueryUpdateOpts } from './QueryEditorDrawer';
type Props = {
options: SavedQueryUpdateOpts;
onDismiss: () => void;
updateComponent?: () => void;
};
interface QueryForm {
val: SavedQuery;
}
const initialForm: QueryForm = {
val: {
title: 'ds-variables',
tags: [],
description: 'example description',
schemaVersion: 1,
time: {
from: 'now-6h',
to: 'now',
},
variables: [
{
name: 'var1',
type: 'text',
current: {
value: 'hello world',
},
},
],
queries: [
{
// @ts-ignore
channel: 'plugin/testdata/random-flakey-stream',
datasource: {
type: 'datasource',
uid: 'grafana',
},
filter: {
fields: ['Time', 'Value'],
},
queryType: 'measurements',
refId: 'A',
search: {
query: '',
},
},
{
// @ts-ignore
alias: 'my-alias',
datasource: {
type: 'testdata',
uid: 'PD8C576611E62080A',
},
drop: 11,
hide: false,
max: 1000,
min: 10,
noise: 5,
refId: 'B',
scenarioId: 'random_walk',
startValue: 10,
},
],
},
};
export const CreateNewQuery = ({ onDismiss, updateComponent, options }: Props) => {
const styles = useStyles2(getStyles);
const [updateSavedQuery] = useUpdateSavedQueryMutation();
const [query, setQuery] = useState(initialForm);
return (
<>
<CodeEditor
containerStyles={styles.editor}
width="80%"
height="70vh"
language="json"
showLineNumbers={false}
showMiniMap={true}
value={JSON.stringify(query.val, null, 2)}
onBlur={(val) => setQuery(() => ({ val: JSON.parse(val) }))}
onSave={(val) => setQuery(() => ({ val: JSON.parse(val) }))}
readOnly={false}
/>
<Button
type="submit"
className={styles.submitButton}
onClick={async () => {
await updateSavedQuery({ query: query.val, opts: options });
onDismiss();
updateComponent?.();
}}
>
Save query
</Button>
</>
);
};
export const getStyles = (theme: GrafanaTheme2) => {
return {
editor: css``,
submitButton: css`
align-self: flex-end;
margin-bottom: 25px;
margin-top: 25px;
`,
};
};

View File

@ -1,113 +0,0 @@
// Libraries
import { uniqBy } from 'lodash';
import React from 'react';
// Components
import { DataSourceInstanceSettings, isUnsignedPluginSignature } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { getDataSourceSrv } from '@grafana/runtime/src';
import { HorizontalGroup, PluginSignatureBadge, Select } from '@grafana/ui';
export type DatasourceTypePickerProps = {
onChange: (ds: string | null) => void;
current: string | null; // type
hideTextValue?: boolean;
onBlur?: () => void;
autoFocus?: boolean;
openMenuOnFocus?: boolean;
placeholder?: string;
tracing?: boolean;
mixed?: boolean;
dashboard?: boolean;
metrics?: boolean;
type?: string | string[];
annotations?: boolean;
variables?: boolean;
alerting?: boolean;
pluginId?: string;
/** If true,we show only DSs with logs; and if true, pluginId shouldnt be passed in */
logs?: boolean;
width?: number;
inputId?: string;
filter?: (dataSource: DataSourceInstanceSettings) => boolean;
onClear?: () => void;
};
const getDataSourceTypeOptions = (props: DatasourceTypePickerProps) => {
const { alerting, tracing, metrics, mixed, dashboard, variables, annotations, pluginId, type, filter, logs } = props;
return uniqBy(
getDataSourceSrv()
.getList({
alerting,
tracing,
metrics,
logs,
dashboard,
mixed,
variables,
annotations,
pluginId,
filter,
type,
})
.map((ds) => {
if (ds.type === 'datasource') {
return {
value: ds.type,
label: ds.type,
imgUrl: ds.meta.info.logos.small,
meta: ds.meta,
};
}
return {
value: ds.type,
label: ds.type,
imgUrl: ds.meta.info.logos.small,
meta: ds.meta,
};
}),
(opt) => opt.value
);
};
export const DatasourceTypePicker = (props: DatasourceTypePickerProps) => {
const { autoFocus, onBlur, onChange, current, openMenuOnFocus, placeholder, width, inputId } = props;
const options = getDataSourceTypeOptions(props);
return (
<div aria-label={selectors.components.DataSourcePicker.container}>
<Select
aria-label={selectors.components.DataSourcePicker.inputV2}
inputId={inputId || 'data-source-picker'}
className="ds-picker select-container"
isMulti={false}
isClearable={true}
backspaceRemovesValue={true}
options={options}
autoFocus={autoFocus}
onBlur={onBlur}
width={width}
value={current}
onChange={(newValue) => {
onChange(newValue?.value ?? null);
}}
openMenuOnFocus={openMenuOnFocus}
maxMenuHeight={500}
placeholder={placeholder ?? 'Select datasource type'}
noOptionsMessage="No datasources found"
getOptionLabel={(o) => {
if (o.meta && isUnsignedPluginSignature(o.meta.signature)) {
return (
<HorizontalGroup align="center" justify="space-between">
<span>{o.label}</span> <PluginSignatureBadge status={o.meta.signature} />
</HorizontalGroup>
);
}
return o.label || '';
}}
/>
</div>
);
};

View File

@ -1,27 +0,0 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
export const HistoryTab = () => {
const styles = useStyles2(getStyles);
// @TODO Implement history
return (
<div className={styles.wrap}>
<p className={styles.tabDescription}>No history.</p>
</div>
);
};
export const getStyles = (theme: GrafanaTheme2) => {
return {
wrap: css`
padding: 20px 5px 5px 5px;
`,
tabDescription: css`
color: ${theme.colors.text.secondary};
`,
};
};

View File

@ -1,25 +0,0 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data/src';
import { useStyles2 } from '@grafana/ui/src';
import QueryLibrarySearchTable from './QueryLibrarySearchTable';
export const Queries = () => {
const styles = useStyles2(getStyles);
return (
<div className={styles.tableWrapper}>
<QueryLibrarySearchTable />
</div>
);
};
export const getStyles = (theme: GrafanaTheme2) => {
return {
tableWrapper: css`
height: 100%;
`,
};
};

View File

@ -1,118 +0,0 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Button, Card, Drawer, Icon, ModalsController, useStyles2 } from '@grafana/ui';
import { SavedQuery } from '../api/SavedQueriesApi';
import { QueryEditorDrawer, SavedQueryUpdateOpts } from './QueryEditorDrawer';
import { QueryImportDrawer } from './QueryImportDrawer';
type Props = {
onDismiss: () => void;
updateComponent: () => void;
};
export const QueryCreateDrawer = ({ onDismiss, updateComponent }: Props) => {
const styles = useStyles2(getStyles);
const type: SavedQueryUpdateOpts['type'] = 'create-new';
const closeDrawer = () => {
onDismiss();
updateComponent();
};
return (
<Drawer
title="Add new query"
subtitle="You can create a new query from builder or import from file"
onClose={onDismiss}
width={'1000px'}
expandable
scrollableContent
>
<div>
<Card>
<Card.Heading>Create by query builder</Card.Heading>
<Card.Description></Card.Description>
<Card.Figure>
<Icon name={'list-ui-alt'} className={styles.cardIcon} />
</Card.Figure>
<Card.Tags>
<ModalsController>
{({ showModal, hideModal }) => {
return (
<Button
icon="plus"
size="md"
onClick={() => {
const savedQuery: SavedQuery = {
title: 'New Query',
variables: [],
queries: [
{
refId: 'A',
datasource: {
type: 'datasource',
uid: 'grafana',
},
queryType: 'randomWalk',
},
],
};
showModal(QueryEditorDrawer, {
onDismiss: closeDrawer,
options: { type },
savedQuery,
});
}}
>
Create query
</Button>
);
}}
</ModalsController>
</Card.Tags>
</Card>
<Card>
<Card.Heading>Import from file</Card.Heading>
<Card.Description>Supported formats: JSON</Card.Description>
<Card.Figure>
<Icon name={'import'} className={styles.cardIcon} />
</Card.Figure>
<Card.Tags>
<ModalsController>
{({ showModal, hideModal }) => {
return (
<Button
icon="arrow-right"
size="md"
onClick={() => {
showModal(QueryImportDrawer, {
onDismiss: closeDrawer,
options: { type },
});
}}
>
Next
</Button>
);
}}
</ModalsController>
</Card.Tags>
</Card>
</div>
</Drawer>
);
};
export const getStyles = (theme: GrafanaTheme2) => {
return {
cardIcon: css`
width: 30px;
height: 30px;
`,
};
};

View File

@ -1,143 +0,0 @@
import { css } from '@emotion/css';
import React, { useState } from 'react';
import {
CoreApp,
DataQuery,
DataSourceApi,
DataSourceInstanceSettings,
getDefaultTimeRange,
GrafanaTheme2,
LoadingState,
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors/src';
import { DataSourcePicker, getDataSourceSrv } from '@grafana/runtime';
import { Button, HorizontalGroup, useStyles2 } from '@grafana/ui';
import { QueryEditorRows } from 'app/features/query/components/QueryEditorRows';
import { addQuery } from '../../../core/utils/query';
import { dataSource as expressionDatasource } from '../../expressions/ExpressionDatasource';
import { updateQueries } from '../../query/state/updateQueries';
import { isQueryWithMixedDatasource, SavedQuery } from '../api/SavedQueriesApi';
import { defaultQuery } from '../utils';
type Props = {
savedQuery: SavedQuery;
onSavedQueryChange: (newQuery: SavedQuery) => void;
};
export const QueryEditor = ({ savedQuery, onSavedQueryChange }: Props) => {
const styles = useStyles2(getStyles);
const [queries, setQueries] = useState<DataQuery[]>(savedQuery.queries ?? [defaultQuery]);
const dsRef = isQueryWithMixedDatasource(savedQuery)
? { uid: '-- Mixed --', type: 'datasource' }
: queries[0].datasource;
const [dsSettings, setDsSettings] = useState(getDataSourceSrv().getInstanceSettings(dsRef));
const data = {
state: LoadingState.NotStarted,
series: [],
timeRange: getDefaultTimeRange(),
};
const onQueriesChange = (newQueries: DataQuery[]) => {
setQueries(newQueries);
onSavedQueryChange({
...savedQuery,
queries: newQueries,
});
};
const onDsChange = async (newDsSettings: DataSourceInstanceSettings) => {
const newDs = await getDataSourceSrv().get(newDsSettings.uid);
const currentDS = dsSettings ? await getDataSourceSrv().get(dsSettings.uid) : undefined;
const newQueries = await updateQueries(newDs, newDs.uid, queries, currentDS);
onQueriesChange(newQueries);
setDsSettings(newDsSettings);
};
const newQuery = async (): Promise<Partial<DataQuery>> => {
const ds: DataSourceApi = !dsSettings?.meta.mixed // TODO remove the asyncs and use prefetched ds apis
? await getDataSourceSrv().get(dsSettings!.uid)
: await getDataSourceSrv().get();
return {
...ds?.getDefaultQuery?.(CoreApp.PanelEditor),
datasource: { uid: ds?.uid, type: ds?.type },
};
};
const onAddQueryClick = async () => {
const newQ = await newQuery();
onQueriesChange(addQuery(queries, newQ));
};
const onAddExpressionClick = () => {
const newExpr = expressionDatasource.newQuery();
onQueriesChange(addQuery(queries, newExpr));
};
return (
<div>
<HorizontalGroup>
<div className={styles.dataSourceHeader}>Data source</div>
<div className={styles.dataSourcePickerWrapper}>
<DataSourcePicker
onChange={onDsChange}
current={dsSettings}
metrics={true}
mixed={true}
dashboard={true}
variables={true}
/>
</div>
</HorizontalGroup>
<QueryEditorRows
queries={queries}
dsSettings={dsSettings!}
onQueriesChange={onQueriesChange}
onAddQuery={onAddQueryClick}
onRunQueries={() => {}}
data={data}
/>
<HorizontalGroup spacing="md" align="flex-start">
{
<Button
disabled={false}
icon="plus"
onClick={onAddQueryClick}
variant="secondary"
aria-label={selectors.components.QueryTab.addQuery}
>
Query
</Button>
}
{(dsSettings?.meta.alerting || dsSettings?.meta.mixed) && (
<Button icon="plus" onClick={onAddExpressionClick} variant="secondary" className={styles.expressionButton}>
<span>Expression&nbsp;</span>
</Button>
)}
</HorizontalGroup>
</div>
);
};
export const getStyles = (theme: GrafanaTheme2) => {
return {
dataSourceHeader: css`
font-size: ${theme.typography.size.sm};
margin-top: 5px;
margin-bottom: 20px;
`,
dataSourcePickerWrapper: css`
margin-top: 5px;
margin-bottom: 20px;
`,
expressionButton: css`
margin-right: ${theme.spacing(2)};
`,
};
};

View File

@ -1,108 +0,0 @@
import { css } from '@emotion/css';
import React, { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { DataQuery } from '@grafana/data/src/types/query';
import { Drawer, IconName, Tab, TabContent, TabsBar, useStyles2 } from '@grafana/ui';
import { SavedQuery } from '../api/SavedQueriesApi';
import { HistoryTab } from './HistoryTab';
import { QueryEditor } from './QueryEditor';
import { QueryEditorDrawerHeader } from './QueryEditorDrawerHeader';
import { UsagesTab } from './UsagesTab';
import { VariablesTab } from './VariablesTab';
export type SavedQueryUpdateOpts = { message?: string } & (
| {
type: 'create-new';
}
| {
type: 'edit';
}
);
type Props = {
onDismiss: () => void;
savedQuery: SavedQuery<DataQuery>;
options: SavedQueryUpdateOpts;
};
type tab = {
label: string;
active: boolean;
icon: IconName;
};
const initialTabs: tab[] = [
{
label: 'Usages',
active: true,
icon: 'link',
},
{
label: 'Variables',
active: false,
icon: 'info-circle',
},
{
label: 'History',
active: false,
icon: 'history',
},
];
export const QueryEditorDrawer = (props: Props) => {
const { onDismiss, options } = props;
const styles = useStyles2(getStyles);
const [tabs, setTabs] = useState(initialTabs);
const [query, setSavedQuery] = useState(props.savedQuery);
return (
<Drawer onClose={onDismiss} width={'1000px'} expandable scrollableContent>
<div>
<QueryEditorDrawerHeader
options={options}
onSavedQueryChange={setSavedQuery}
savedQuery={query}
onDismiss={onDismiss}
/>
<div className={styles.queryWrapper}>
<QueryEditor onSavedQueryChange={setSavedQuery} savedQuery={query} />
</div>
<TabsBar>
{tabs.map((tab, index) => (
<Tab
key={index}
label={tab.label}
active={tab.active}
icon={tab.icon}
onChangeTab={() => setTabs(tabs.map((tab, idx) => ({ ...tab, active: idx === index })))}
/>
))}
</TabsBar>
<TabContent>
<div className={styles.tabWrapper}>
{tabs[0].active && <UsagesTab savedQuery={query} />}
{tabs[1].active && <VariablesTab savedQuery={query} options={options} />}
{tabs[2].active && <HistoryTab />}
</div>
</TabContent>
</div>
</Drawer>
);
};
export const getStyles = (theme: GrafanaTheme2) => {
return {
queryWrapper: css`
max-height: calc(60vh);
overflow-y: scroll;
margin-bottom: 50px;
`,
tabWrapper: css`
overflow-y: scroll;
max-height: calc(27vh);
`,
};
};

View File

@ -1,198 +0,0 @@
import { css, cx } from '@emotion/css';
import React, { useEffect, useRef, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data/src';
import { Button, HorizontalGroup, Icon, IconName, useStyles2 } from '@grafana/ui';
import { SanitizedSVG } from 'app/core/components/SVG/SanitizedSVG';
import { useAppNotification } from '../../../core/copy/appNotification';
import { SavedQuery } from '../api/SavedQueriesApi';
import { getSavedQuerySrv } from '../api/SavedQueriesSrv';
import { implementationComingSoonAlert } from '../utils';
import { SavedQueryUpdateOpts } from './QueryEditorDrawer';
import { QueryName } from './QueryName';
type Props = {
onSavedQueryChange: (newQuery: SavedQuery) => void;
savedQuery: SavedQuery;
onDismiss: () => void;
options: SavedQueryUpdateOpts;
};
export const QueryEditorDrawerHeader = ({ savedQuery, onDismiss, onSavedQueryChange, options }: Props) => {
const notifyApp = useAppNotification();
const styles = useStyles2(getStyles);
const dropdownRef = useRef(null);
const [queryName, setQueryName] = useState(savedQuery.title);
const [showUseQueryOptions, setShowUseQueryOptions] = useState(false);
const nameEditingEnabled = !Boolean(savedQuery?.uid?.length);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current !== event.target) {
setShowUseQueryOptions(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
}, [dropdownRef]);
const deleteQuery = async () => {
await getSavedQuerySrv().deleteSavedQuery({ uid: savedQuery.uid });
onDismiss();
};
type queryOption = {
label: string;
value: string;
icon: IconName;
src?: string;
};
const useQueryOptions: queryOption[] = [
{ label: 'Add to dashboard', value: 'dashboard-panel', icon: 'apps' },
{ label: 'Create alert rule', value: 'alert-rule', icon: 'bell' },
{ label: 'View in explore', value: 'explore', icon: 'compass' },
{
label: 'Create recorded query',
value: 'recorded-query',
icon: 'record-audio',
},
{ label: 'Create SLO', value: 'slo', icon: 'crosshair' },
{
label: 'Add to incident in Grafana OnCall',
value: 'incident-oncall',
icon: 'record-audio',
src: 'public/app/features/query-library/img/grafana_incident.svg',
},
{
label: 'Create incident in Grafana Incident',
value: 'incident-grafana',
icon: 'heart-break',
src: 'public/app/features/query-library/img/grafana_oncall.svg',
},
{
label: 'Create forecast in Grafana ML',
value: 'grafana-ml',
icon: 'grafana-ml',
src: 'public/app/features/query-library/img/grafana_ml.svg',
},
];
const onQueryNameChange = (name: string) => {
setQueryName(name);
onSavedQueryChange({
...savedQuery,
title: name,
});
};
const onQuerySave = async (options: SavedQueryUpdateOpts) => {
await getSavedQuerySrv()
.updateSavedQuery(savedQuery, options)
.then(() => notifyApp.success('Query updated'))
.catch((err) => {
const msg = err.data?.message || err;
notifyApp.warning(msg);
});
onDismiss();
};
return (
<>
<div className={styles.header}>
<HorizontalGroup justify={'space-between'}>
<QueryName name={queryName} onChange={onQueryNameChange} editingEnabled={nameEditingEnabled} />
<HorizontalGroup>
<Button icon="times" size="md" variant={'secondary'} onClick={onDismiss} autoFocus={false}>
Close
</Button>
<Button
icon={'grafana'}
variant="secondary"
size="md"
onClick={() => {
setShowUseQueryOptions(!showUseQueryOptions);
}}
>
Use query
</Button>
<Button icon="sync" size="md" variant={'secondary'} onClick={implementationComingSoonAlert}>
Run
</Button>
{/*<Button icon="share-alt" size="sm" variant={'secondary'}>Export</Button>*/}
<Button icon="lock" size="md" variant={'secondary'} onClick={implementationComingSoonAlert} />
<Button size="md" variant={'primary'} onClick={() => onQuerySave(options)}>
Save
</Button>
<Button icon="trash-alt" size="md" variant={'destructive'} onClick={() => deleteQuery()} />
</HorizontalGroup>
</HorizontalGroup>
{/*@TODO Nicer submenu*/}
<HorizontalGroup>
{showUseQueryOptions && (
<div
className="panel-menu-container dropdown open"
style={{ height: 0 }}
ref={dropdownRef}
onClick={() => {
setShowUseQueryOptions(false);
}}
>
<ul className={cx('dropdown-menu dropdown-menu--menu panel-menu', styles.dropdown)}>
{useQueryOptions.map((option, key) => {
return (
<li key={key}>
{/*eslint-disable-next-line jsx-a11y/anchor-is-valid*/}
<a onClick={implementationComingSoonAlert}>
<div>
{option.src ? (
<SanitizedSVG src={option.src} className={styles.optionSvg} />
) : (
<Icon name={option.icon} className={styles.menuIconClassName} />
)}
</div>
<span className="dropdown-item-text">{option.label}</span>
<span className="dropdown-menu-item-shortcut" />
</a>
</li>
);
})}
</ul>
</div>
)}
</HorizontalGroup>
</div>
</>
);
};
export const getStyles = (theme: GrafanaTheme2) => {
return {
cascaderButton: css`
height: 24px;
`,
header: css`
padding-top: 5px;
padding-bottom: 15px;
`,
menuIconClassName: css`
margin-right: ${theme.v1.spacing.sm};
a::after {
display: none;
}
`,
optionSvg: css`
margin-right: 8px;
width: 16px;
height: 16px;
`,
dropdown: css`
left: 609px;
top: 2px;
`,
};
};

View File

@ -1,55 +0,0 @@
import { css } from '@emotion/css';
import React, { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Drawer, FileDropzone, useStyles2 } from '@grafana/ui';
import { CreateNewQuery } from './CreateNewQuery';
import { SavedQueryUpdateOpts } from './QueryEditorDrawer';
type Props = {
options: SavedQueryUpdateOpts;
onDismiss: () => void;
};
export const QueryImportDrawer = ({ onDismiss, options }: Props) => {
const styles = useStyles2(getStyles);
const [file, setFile] = useState<File | undefined>(undefined);
return (
<Drawer title="Import query" onClose={onDismiss} width={'1000px'} expandable scrollableContent>
<FileDropzone
readAs="readAsBinaryString"
onFileRemove={() => {
setFile(undefined);
}}
options={{
accept: '.json',
multiple: false,
onDrop: (acceptedFiles: File[]) => {
setFile(acceptedFiles[0]);
},
}}
>
<div>Drag and drop here or browse</div>
</FileDropzone>
{Boolean(file) && (
<div className={styles.queryPreview}>
<CreateNewQuery options={options} onDismiss={onDismiss} />
</div>
)}
</Drawer>
);
};
export const getStyles = (theme: GrafanaTheme2) => {
return {
queryPreview: css`
margin-top: 20px;
margin-bottom: 20px;
margin-left: 170px;
`,
};
};

View File

@ -1,46 +0,0 @@
import React, { useState } from 'react';
import { config } from '@grafana/runtime/src';
import { Alert, Tab, TabsBar, TabContent } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
import { useNavModel } from '../../../core/hooks/useNavModel';
import { Queries } from './Queries';
const initialTabs = [
{
label: 'Queries',
active: true,
},
];
const QueryLibraryPage = () => {
const navModel = useNavModel('query');
const [tabs, setTabs] = useState(initialTabs);
if (!config.featureToggles.panelTitleSearch) {
return <Alert title="Missing feature toggle: panelTitleSearch">Query library requires searchV2</Alert>;
}
return (
<Page navModel={navModel}>
<Page.Contents>
<TabsBar>
{tabs.map((tab, index) => (
<Tab
key={index}
label={tab.label}
active={tab.active}
onChangeTab={() => setTabs(tabs.map((tab, idx) => ({ ...tab, active: idx === index })))}
/>
))}
</TabsBar>
<TabContent>{tabs[0].active && <Queries />}</TabContent>
</Page.Contents>
</Page>
);
};
export default QueryLibraryPage;

View File

@ -1,219 +0,0 @@
import { css, cx } from '@emotion/css';
import { Global } from '@emotion/react';
import React, { useEffect, useMemo, useState } from 'react';
import { useAsync } from 'react-use';
import AutoSizer from 'react-virtualized-auto-sizer';
import { GrafanaTheme2 } from '@grafana/data';
import { Button, FilterInput, HorizontalGroup, ModalsController, useStyles2, useTheme2 } from '@grafana/ui';
import { getGrafanaSearcher, SearchQuery } from '../../search/service';
import { getGlobalStyles } from '../globalStyles';
import { QueryItem } from '../types';
import { DatasourceTypePicker } from './DatasourceTypePicker';
import { QueryCreateDrawer } from './QueryCreateDrawer';
import { QueryListItem } from './QueryListItem';
const QueryLibrarySearchTable = () => {
const styles = useStyles2(getStyles);
const [datasourceType, setDatasourceType] = useState<string | null>(null);
const [searchQueryBy, setSearchByQuery] = useState<string>('');
const [reload, setReload] = useState(0);
const theme = useTheme2();
const globalCSS = getGlobalStyles(theme);
// @TODO update with real data
const authors = ['Artur Wierzbicki', 'Drew Slobodnjak', 'Nathan Marrs', 'Raphael Batyrbaev', 'Adela Almasan'];
const dates = [
'August 17, 2022, 2:32pm',
'August 17, 2022, 4:10pm',
'August 18, 2022, 1:00am',
'August 18, 2022, 12:00pm',
'August 19, 2022, 2:33pm',
];
const searchQuery = useMemo<SearchQuery>(() => {
const query: SearchQuery = {
query: '*',
sort: 'name_sort',
explain: true,
kind: ['query'],
};
if (datasourceType?.length) {
query.ds_type = datasourceType;
}
if (searchQueryBy) {
query.query = searchQueryBy;
}
return query;
}, [datasourceType, searchQueryBy]);
useEffect(() => {}, [reload]);
const results = useAsync(async () => {
const raw = await getGrafanaSearcher().search(searchQuery);
return raw.view.map<QueryItem>((item) => ({
uid: item.uid,
title: item.name,
url: item.url,
uri: item.url,
type: item.kind,
id: 123, // do not use me!
tags: item.tags ?? [],
ds_uid: item.ds_uid,
}));
}, [searchQuery, reload]);
const found = results.value;
return (
<>
<Global styles={globalCSS} />
<div className={styles.tableWrapper}>
<HorizontalGroup width="100%" justify="space-between" spacing={'md'} height={25}>
<HorizontalGroup>
<FilterInput
placeholder="Search queries by name, source, or variable"
autoFocus={true}
value={searchQueryBy}
onChange={setSearchByQuery}
width={50}
className={styles.searchBy}
/>
Filter by datasource type
<DatasourceTypePicker
current={datasourceType}
onChange={(newDsType) => {
setDatasourceType(() => newDsType);
}}
/>
</HorizontalGroup>
<ModalsController>
{({ showModal, hideModal }) => {
return (
<div className={styles.createQueryButton}>
<Button
icon="plus"
size="md"
onClick={() => {
showModal(QueryCreateDrawer, {
onDismiss: hideModal,
updateComponent: () => {
setReload(reload + 1);
},
});
}}
>
Create query
</Button>
</div>
);
}}
</ModalsController>
</HorizontalGroup>
<ModalsController>
{({ showModal, hideModal }) => {
return (
<AutoSizer className={styles.autosizer} style={{ width: '100%', height: '100%' }}>
{({ width, height }) => {
return (
<table className={cx('filter-table form-inline filter-table--hover', styles.table)}>
<thead>
<tr>
<th />
<th>Status</th>
<th>Name and raw query</th>
<th>Data Source</th>
<th>User</th>
<th>Date</th>
<th />
</tr>
</thead>
<tbody>
{!Boolean(found?.length) && (
<tr className={styles.transparentBg}>
<td />
<td />
<td />
<td>
<div className={styles.noData}>No data</div>
</td>
<td />
<td />
<th />
</tr>
)}
{Boolean(found?.length) &&
found!.map((item, key) => {
return (
<QueryListItem
query={item}
key={item.uid}
showModal={showModal}
hideModal={hideModal}
updateComponent={() => setReload(reload + 1)}
author={key < authors.length ? authors[key] : authors[key - authors.length]}
date={key < dates.length ? dates[key] : dates[key - dates.length]}
/>
);
})}
</tbody>
</table>
);
}}
</AutoSizer>
);
}}
</ModalsController>
</div>
</>
);
};
export default QueryLibrarySearchTable;
export const getStyles = (theme: GrafanaTheme2) => {
return {
tableWrapper: css`
height: 100%;
margin-top: 20px;
margin-bottom: 20px;
display: flex;
flex-direction: column;
align-items: flex-start;
`,
autosizer: css`
margin-top: 40px;
`,
createQueryButton: css`
text-align: center;
`,
filtersGroup: css`
padding-top: 10px;
margin-top: 30px;
`,
searchBy: css`
margin-right: 15px;
`,
table: css`
font-size: 14px;
&tbody {
&tr: {
background: ${theme.colors.background.secondary};
}
}
`,
noData: css`
color: ${theme.colors.text.secondary};
`,
transparentBg: css`
background: transparent !important;
`,
};
};

View File

@ -1,198 +0,0 @@
import { css, cx } from '@emotion/css';
import { uniq } from 'lodash';
import React, { memo, useEffect, useState } from 'react';
import { DataSourceApi, GrafanaTheme2 } from '@grafana/data/src';
import { getDataSourceSrv } from '@grafana/runtime/src';
import { Icon, Tooltip } from '@grafana/ui';
import { Badge, IconButton, useStyles2 } from '@grafana/ui/src';
import { useAppNotification } from '../../../core/copy/appNotification';
import { getSavedQuerySrv } from '../api/SavedQueriesSrv';
import { QueryItem } from '../types';
import { implementationComingSoonAlert } from '../utils';
import { QueryEditorDrawer } from './QueryEditorDrawer';
type QueryListItemProps = {
query: QueryItem;
showModal: <T>(component: React.ComponentType<T>, props: T) => void;
hideModal: () => void;
updateComponent: () => void;
author: string;
date: string;
};
const options = {
type: 'edit',
} as const;
export const QueryListItem = memo(
({ query, showModal, hideModal, updateComponent, author, date }: QueryListItemProps) => {
const notifyApp = useAppNotification();
const styles = useStyles2(getStyles);
const [dsInfo, setDsInfo] = useState<DataSourceApi[]>([]);
useEffect(() => {
const getQueryDsInstance = async () => {
const uniqueUids = uniq(query?.ds_uid ?? []);
setDsInfo((await Promise.all(uniqueUids.map((dsUid) => getDataSourceSrv().get(dsUid)))).filter(Boolean));
};
getQueryDsInstance();
}, [query.ds_uid]);
const closeDrawer = () => {
hideModal();
updateComponent();
};
const openDrawer = async () => {
const result = await getSavedQuerySrv().getSavedQueries([{ uid: query.uid }]);
const savedQuery = result[0];
showModal(QueryEditorDrawer, { onDismiss: closeDrawer, savedQuery: savedQuery, options });
};
const deleteQuery = async () => {
await getSavedQuerySrv().deleteSavedQuery({ uid: query.uid });
updateComponent();
};
const getDsType = () => {
const dsType = dsInfo?.length > 1 ? 'mixed' : dsInfo?.[0]?.type ?? 'datasource';
return startWithUpperCase(dsType);
};
const startWithUpperCase = (dsType: string) => {
return dsType.charAt(0).toUpperCase() + dsType.slice(1);
};
const getTooltip = () => {
return (
<div>
<ul className={styles.dsTooltipList}>
{dsInfo.map((dsI, key) => {
return (
<li key={key}>
<img className={styles.dsTooltipIcon} src={dsI?.meta?.info.logos.small} alt="datasource" />
&nbsp;
{startWithUpperCase(dsI.type)}
</li>
);
})}
</ul>
</div>
);
};
const copyToClipboard = async () => {
const models = await getSavedQuerySrv().getSavedQueries([{ uid: query.uid }]);
if (!models?.length) {
implementationComingSoonAlert();
return;
}
await navigator.clipboard.writeText(
JSON.stringify(
{
...models[0],
uid: undefined,
storageOptions: undefined,
},
null,
2
)
);
notifyApp.success('Query JSON copied to clipboard!');
};
return (
<tr key={query.uid} className={styles.row}>
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions*/}
<td onClick={implementationComingSoonAlert}>
<Icon name={'lock'} className={styles.disabled} title={'Implementation coming soon!'} />
</td>
<td>
<Badge color={'green'} text={'1'} icon={'link'} tooltip={'Implementation coming soon!'} />
</td>
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions*/}
<td onClick={openDrawer}>{query.title}</td>
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions*/}
<td onClick={openDrawer}>
<img
className={styles.dsIcon}
src={getDsType() === 'Mixed' ? 'public/img/icn-datasource.svg' : dsInfo[0]?.meta?.info.logos.small}
alt="datasource"
style={{ width: '16px', height: '16px' }}
/>
&nbsp;&nbsp;{getDsType()}&nbsp;
{getDsType() === 'Mixed' && (
<Tooltip content={getTooltip()}>
<Icon name={'question-circle'} className={styles.infoIcon} />
</Tooltip>
)}
</td>
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions*/}
<td onClick={openDrawer}>
<img
className={cx('filter-table__avatar', styles.dsIcon)}
src={'/avatar/46d229b033af06a191ff2267bca9ae56'}
alt={`Avatar for ${author}`}
/>
&nbsp;&nbsp;{author}
</td>
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions*/}
<td onClick={openDrawer}>{date}</td>
<td className={styles.tableTr}>
<IconButton name="share-alt" tooltip={'Share'} onClick={implementationComingSoonAlert} />
<IconButton name="copy" tooltip={'Copy'} onClick={copyToClipboard} />
<IconButton name="upload" tooltip={'Upload'} onClick={implementationComingSoonAlert} />
<IconButton name="cog" tooltip={'Settings'} onClick={implementationComingSoonAlert} />
<IconButton name="trash-alt" tooltip={'Delete'} onClick={deleteQuery} />
</td>
</tr>
);
}
);
QueryListItem.displayName = 'QueryListItem';
const getStyles = (theme: GrafanaTheme2) => {
return {
row: css`
height: 70px;
cursor: pointer;
`,
tableTr: css`
display: flex;
justify-content: space-between;
margin-top: 22px;
`,
disabled: css`
color: ${theme.colors.text.secondary};
`,
gitIcon: css`
width: 30px;
height: 30px;
margin-left: 10px;
margin-top: 1px;
opacity: 0.8;
`,
infoIcon: css`
margin-top: -2px;
`,
dsTooltipIcon: css`
width: 11px;
height: 11px;
`,
dsIcon: css`
width: 16px !important;
height: 16px !important;
`,
dsTooltipList: css`
list-style-type: none;
`,
};
};

View File

@ -1,119 +0,0 @@
import { css } from '@emotion/css';
import React, { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Icon, Input, FieldValidationMessage, HorizontalGroup, useStyles2 } from '@grafana/ui';
export interface QueryNameProps {
name: string;
editingEnabled: boolean;
onChange: (v: string) => void;
}
export const QueryName = ({ name, onChange, editingEnabled }: QueryNameProps) => {
const styles = useStyles2(getStyles);
const [isEditing, setIsEditing] = useState<boolean>(false);
const [validationError, setValidationError] = useState<string | null>(null);
const onEditQueryName = (event: React.SyntheticEvent) => {
setIsEditing(true);
};
const onEndEditName = (newName: string) => {
setIsEditing(false);
if (validationError) {
setValidationError(null);
return;
}
if (name !== newName) {
onChange(newName);
}
};
const onInputChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
const newName = event.currentTarget.value.trim();
if (newName.length === 0) {
setValidationError('An empty name is not allowed');
return;
}
if (validationError) {
setValidationError(null);
}
};
const onEditLayerBlur = (event: React.SyntheticEvent<HTMLInputElement>) => {
onEndEditName(event.currentTarget.value.trim());
};
const onFocus = (event: React.FocusEvent<HTMLInputElement>) => {
event.target.select();
};
const onKeyDown = (event: React.KeyboardEvent) => {
if (event.key === 'Enter') {
if (!(event.target instanceof HTMLInputElement)) {
return;
}
onEndEditName(event.target.value);
}
};
return (
<>
<div className={styles.wrapper}>
{!isEditing && (
<HorizontalGroup>
<h2 className={styles.h2Style}>{name}</h2>
{editingEnabled && <Icon name="pen" className={styles.nameEditIcon} size="md" onClick={onEditQueryName} />}
</HorizontalGroup>
)}
{isEditing && (
<>
<Input
type="text"
defaultValue={name}
onBlur={onEditLayerBlur}
onFocus={onFocus}
autoFocus={true}
onKeyDown={onKeyDown}
invalid={validationError !== null}
onChange={onInputChange}
className={styles.nameInput}
/>
{validationError && <FieldValidationMessage horizontal>{validationError}</FieldValidationMessage>}
</>
)}
</div>
</>
);
};
const getStyles = (theme: GrafanaTheme2) => {
return {
wrapper: css`
display: flex;
align-items: center;
margin-left: ${theme.v1.spacing.xs};
`,
nameEditIcon: css`
cursor: pointer;
color: ${theme.colors.text.secondary};
width: 12px;
height: 12px;
`,
nameInput: css`
max-width: 300px;
margin: -8px 0;
`,
h2Style: css`
margin-bottom: 0;
`,
};
};

View File

@ -1,79 +0,0 @@
import { css } from '@emotion/css';
import React, { useState } from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { Button, Form, Modal, VerticalGroup, TextArea } from '@grafana/ui';
import { WorkflowID } from '../../storage/types';
import { SavedQuery } from '../api/SavedQueriesApi';
interface FormDTO {
message: string;
}
export interface SaveQueryOptions {
savedQuery: SavedQuery;
workflow: WorkflowID;
message?: string;
}
export type SaveProps = {
onCancel: () => void;
onSuccess: () => void;
onSubmit?: (options: SaveQueryOptions) => Promise<{ success: boolean }>;
options: SaveQueryOptions;
onOptionsChange: (opts: SaveQueryOptions) => void;
};
export const SaveQueryWorkflowModal = ({ options, onSubmit, onCancel, onSuccess }: SaveProps) => {
const [saving, setSaving] = useState(false);
return (
<Modal
isOpen={true}
title={options.workflow === WorkflowID.PR ? 'Create a Pull Request' : 'Push changes'}
onDismiss={onCancel}
icon="exclamation-triangle"
className={css`
width: 500px;
`}
>
<Form
onSubmit={async (data: FormDTO) => {
console.log('hello submitting!');
if (!onSubmit) {
return;
}
setSaving(true);
options = { ...options, message: data.message };
const result = await onSubmit(options);
if (result.success) {
onSuccess();
} else {
setSaving(false);
}
}}
>
{({ register, errors }) => (
<VerticalGroup>
<TextArea {...register('message')} placeholder="Add a note to describe your changes." autoFocus rows={5} />
<VerticalGroup>
<Button variant="secondary" onClick={onCancel} fill="outline">
Cancel
</Button>
<Button
type="submit"
disabled={false}
icon={saving ? 'fa fa-spinner' : undefined}
aria-label={selectors.pages.SaveDashboardModal.save}
>
{options.workflow === WorkflowID.PR ? 'Submit PR' : 'Push'}
</Button>
</VerticalGroup>
</VerticalGroup>
)}
</Form>
</Modal>
);
};

View File

@ -1,166 +0,0 @@
import { css } from '@emotion/css';
import React, { useMemo } from 'react';
import { useAsync } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data/src';
import { Button, Card, Icon, IconName, Spinner, useStyles2 } from '@grafana/ui/src';
import { HorizontalGroup } from '../../plugins/admin/components/HorizontalGroup';
import { getGrafanaSearcher, SearchQuery } from '../../search/service';
import { SavedQuery } from '../api/SavedQueriesApi';
import { QueryItem } from '../types';
type Props = {
savedQuery: SavedQuery;
};
export const UsagesTab = ({ savedQuery }: Props) => {
const styles = useStyles2(getStyles);
const searchQuery = useMemo<SearchQuery>(() => {
const query: SearchQuery = {
query: '*',
kind: savedQuery.uid ? ['dashboard', 'alert'] : ['newQuery'], // workaround for new queries
saved_query_uid: savedQuery.uid,
};
return query;
}, [savedQuery.uid]);
const results = useAsync(async () => {
const raw = await getGrafanaSearcher().search(searchQuery);
return raw.view.map<QueryItem>((item) => ({
uid: item.uid,
title: item.name,
url: item.url,
uri: item.url,
type: item.kind,
id: 321, // do not use me!
tags: item.tags ?? [],
ds_uid: item.ds_uid,
location: item.location,
panel_type: item.panel_type,
}));
}, [searchQuery]);
if (results.loading) {
return <Spinner />;
}
const found = results.value;
const getIconForKind = (kind: string): IconName => {
let icon: IconName = 'question-circle';
switch (kind) {
case 'dashboard':
icon = 'apps';
break;
case 'folder':
icon = 'folder';
break;
case 'alert':
icon = 'bell';
break;
default:
icon = 'question-circle';
break;
}
return icon;
};
if (found?.length === 0) {
return (
<div className={styles.wrap}>
<p className={styles.usagesDescription}>This query is not used anywhere.</p>
</div>
);
}
return (
<div className={styles.wrap}>
<p className={styles.usagesDescription}>
This query is used in the places below. Modifying will affect all its usages.
</p>
{found?.map((item) => {
return (
<div key={item.uid}>
<Card>
<Card.Heading>
<span className={styles.cardHeading}>
{item.title}
<a
href={item.url}
title={'Open in new tab'}
target="_blank"
rel="noopener noreferrer"
className={styles.externalLink}
>
<Icon name="external-link-alt" className={styles.cardHeadingIcon} />
</a>
</span>
</Card.Heading>
<Card.Description>
<a href={'dashboards'} target="_blank" rel="noopener noreferrer" className={styles.externalLink}>
<Icon name="folder" className={styles.cardDescriptionIcon} />
</a>
{item.location}
</Card.Description>
<Card.Figure className={styles.cardFigure}>
<Icon name={getIconForKind(item.type)} />
</Card.Figure>
<Card.Tags>
<HorizontalGroup>
<Button icon="eye" size="sm" variant={'secondary'} />
<Button icon="link" size="sm" variant={'secondary'}>
Unlink
</Button>
</HorizontalGroup>
</Card.Tags>
</Card>
</div>
);
})}
</div>
);
};
export const getStyles = (theme: GrafanaTheme2) => {
return {
wrap: css`
padding: 20px 5px 5px 5px;
`,
info: css`
padding-bottom: 30px;
`,
folderIcon: css`
margin-right: 5px;
`,
cardFigure: css`
margin-right: 0;
margin-top: 15px;
`,
externalLink: css`
margin-left: 5px;
`,
cardHeading: css`
display: flex;
`,
cardHeadingIcon: css`
width: 13px;
height: 13px;
color: ${theme.colors.text.secondary};
display: flex;
align-self: center;
`,
usagesDescription: css`
color: ${theme.colors.text.secondary};
`,
cardDescriptionIcon: css`
width: 16px;
height: 16px;
color: ${theme.colors.text.secondary};
margin-right: 5px;
`,
};
};

View File

@ -1,166 +0,0 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Button, Card, HorizontalGroup, useStyles2 } from '@grafana/ui';
import { LayerName } from 'app/core/components/Layers/LayerName';
import { SavedQuery, useUpdateSavedQueryMutation, Variable } from '../api/SavedQueriesApi';
import { SavedQueryUpdateOpts } from './QueryEditorDrawer';
type Props = {
savedQuery: SavedQuery;
options: SavedQueryUpdateOpts;
};
export const VariablesTab = ({ savedQuery, options }: Props) => {
const styles = useStyles2(getStyles);
const [updateSavedQuery] = useUpdateSavedQueryMutation();
const onVariableNameChange = (variable: Variable, newName: string) => {
const newVariables = savedQuery.variables.map((v: Variable) => {
if (v.name === variable.name) {
v.name = newName;
}
return v;
});
updateSavedQuery({
query: {
...savedQuery,
variables: newVariables,
},
opts: options,
});
};
const onVariableValueChange = (variable: Variable, newValue: string) => {
const newVariables = savedQuery.variables.map((v: Variable) => {
if (v.name === variable.name) {
v.current.value = newValue;
}
return v;
});
updateSavedQuery({
query: {
...savedQuery,
variables: newVariables,
},
opts: options,
});
};
const onAddVariable = () => {
// NOTE: doing mutation to force re-render
savedQuery.variables.unshift({
name: 'New variable',
current: {
value: 'General',
},
});
updateSavedQuery({ query: savedQuery, opts: options });
};
const onRemoveVariable = (variable: Variable) => {
const varIndex = savedQuery.variables.map((v: Variable, index: number) => {
if (v.name === variable.name) {
return index;
}
return;
});
if (typeof varIndex === 'number') {
// NOTE: doing mutation vs filter to force re-render
savedQuery.variables.splice(varIndex, 1);
updateSavedQuery({ query: savedQuery, opts: options });
}
};
return (
<div className={styles.tabWrapper}>
<div className={styles.variablesHeader}>
<HorizontalGroup width="100%" justify="space-between" spacing={'md'} height={25}>
<div className={styles.tabDescription}>
Variables enable more interactive and dynamic queries. Instead of hard-coding things like server or sensor
names in your metric queries you can use variables in their place. <br />
<b>Variable support is coming soon!</b>
</div>
<Button icon="plus" size="md" className={styles.addVariableButton} onClick={onAddVariable}>
Add variable
</Button>
</HorizontalGroup>
</div>
<div className={styles.variableList}>
<ul>
{savedQuery &&
savedQuery.variables &&
savedQuery.variables.map((variable: Variable) => (
<li key={variable && variable.name} className={styles.variableListItem}>
<Card>
<Card.Heading>
<LayerName
name={variable && variable.name}
onChange={(v) => onVariableNameChange(variable, v)}
overrideStyles
/>
</Card.Heading>
<Card.Description>
<LayerName
name={variable && variable.current.value.toString()}
onChange={(v) => onVariableValueChange(variable, v)}
overrideStyles
/>
</Card.Description>
<Card.Tags>
<Button
icon="trash-alt"
size="sm"
variant={'secondary'}
tooltip="Delete this variable"
onClick={() => onRemoveVariable(variable)}
>
Delete
</Button>
</Card.Tags>
</Card>
</li>
))}
</ul>
</div>
</div>
);
};
export const getStyles = (theme: GrafanaTheme2) => {
return {
tabWrapper: css`
flex: 1;
padding: 20px 5px 5px 5px;
`,
tabDescription: css`
color: ${theme.colors.text.secondary};
`,
variableList: css`
padding-bottom: 20px;
`,
variableListItem: css`
list-style: none;
`,
addVariableButton: css`
display: flex;
align-self: center;
margin: auto;
margin-bottom: 15px;
`,
variablesHeader: css`
margin-top: 15px;
margin-bottom: 20px;
`,
};
};

View File

@ -1,28 +0,0 @@
import { css } from '@emotion/react';
import { GrafanaTheme2 } from '@grafana/data';
export function getGlobalStyles(theme: GrafanaTheme2) {
return css`
.filter-table {
border-collapse: separate;
border-spacing: 0 5px;
tbody {
tr:nth-child(odd) {
background: ${theme.colors.background.secondary};
}
tr {
background: ${theme.colors.background.secondary};
}
}
&--hover {
tbody tr:hover {
background: ${theme.colors.background.primary};
}
}
}
`;
}

View File

@ -1,10 +0,0 @@
<svg width="1024" height="1024" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.25 2.5V1M2.5 10H1m1.5-7.5L4 4m18 6h1.5M22 2.5 20.5 4" stroke="#F3C90E" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.682 4.75h5.143a3.75 3.75 0 0 1 3.7 3.338l1.133 10.22A3.75 3.75 0 0 1 22.75 22a2.25 2.25 0 0 1-2.25 2.25H4A2.25 2.25 0 0 1 1.75 22a3.75 3.75 0 0 1 3.051-3.684L5.935 8.088A3.75 3.75 0 0 1 9.682 4.75Zm9.298 15H5.484A2.25 2.25 0 0 0 3.25 22a.75.75 0 0 0 .75.75h16.5a.75.75 0 0 0 .75-.75A2.25 2.25 0 0 0 19 19.75h-.02ZM17.035 8.253l1.107 9.997H6.318l1.107-9.997a2.25 2.25 0 0 1 2.25-2.003h5.142a2.25 2.25 0 0 1 2.218 2.003Zm-1.382 2.115a.75.75 0 0 0-1.306-.736l-3.145 5.574v.002l-1.603-2.123a.75.75 0 1 0-1.198.904l1.627 2.155c.144.193.334.36.564.47.227.11.48.156.737.128a1.41 1.41 0 0 0 .69-.253c.204-.143.366-.33.484-.536l.004-.006 3.146-5.579Z" fill="url(#a)"/>
<defs>
<linearGradient id="a" x1="12.25" y1="5" x2="12.25" y2="24.25" gradientUnits="userSpaceOnUse">
<stop stop-color="#FAC00E"/>
<stop offset="1" stop-color="#F26526"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 135.46 118.24"><defs><style>.cls-1{fill:url(#linear-gradient);}</style><linearGradient id="linear-gradient" x1="67.73" y1="137.67" x2="67.73" y2="-0.38" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f9ea1c"/><stop offset="1" stop-color="#ed5a29"/></linearGradient></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M135.37,64.86l-5.6-26.43a4.05,4.05,0,0,0-.9-1.82L113.19,18.24a4.11,4.11,0,0,0-1-.87L83.71.57A4.18,4.18,0,0,0,81.62,0H58.54a4.19,4.19,0,0,0-1,.12L26,8a4.16,4.16,0,0,0-2,1.17L1.12,33.32A4.1,4.1,0,0,0,0,36.14v24.2A4.1,4.1,0,0,0,2.46,64.1l22.85,10L39,95.07a4.13,4.13,0,0,0,3.45,1.86H68.66L81.59,116.4A4.09,4.09,0,0,0,85,118.24h0l6,0a4.11,4.11,0,0,0,4.09-4.11V90.21H117a4.09,4.09,0,0,0,3.36-1.75l14.34-20.38A4.1,4.1,0,0,0,135.37,64.86ZM86.12,34.1a3.29,3.29,0,0,0-.67.77l0,.05H59.83L57,30.53A3.38,3.38,0,0,0,54.24,29a3.45,3.45,0,0,0-2.86,1.45l-3.15,4.51H10.92L21.64,23.58H98.3Zm-3,4.52L72.77,55.23,62.19,38.62ZM37.75,50H8.22V38.62H45.66ZM29.12,15.65,59.05,8.22H80.49l19.74,11.66H25.13Zm-20.9,38h27L26.73,65.78,8.22,57.65ZM114.87,82h-4V46.21a2.3,2.3,0,0,0-2.3-2.3h-1.39a2.3,2.3,0,0,0-2.3,2.3V82h-12V63.92a2.31,2.31,0,0,0-2.3-2.31H89.21a2.31,2.31,0,0,0-2.3,2.31v45.64L74.48,90.84V78.36a2.3,2.3,0,0,0-2.3-2.3H70.79a2.3,2.3,0,0,0-2.3,2.3V88.71H56.23V58.54a2.32,2.32,0,0,0-2.31-2.31H52.54a2.32,2.32,0,0,0-2.31,2.31V88.71H44.64l-12.39-19L53.93,38.62h.21l15.8,24.81A3.32,3.32,0,0,0,72.82,65a3.39,3.39,0,0,0,2.87-1.6L91,38.9l16.73-14.45,14.26,16.7,5,23.66Z"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,9 +0,0 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.3009 29.8017H30.6991C32.0765 29.8017 32.9322 28.313 32.2435 27.1235L25.5443 15.52C24.8557 14.3304 23.1374 14.3304 22.4557 15.52L15.7565 27.1235C15.0678 28.313 15.9304 29.8017 17.3009 29.8017ZM24 0C10.7478 0 0 10.7478 0 24C0 37.2522 10.7478 48 24 48C37.2522 48 48 37.2522 48 24C48 10.7478 37.2522 0 24 0ZM24.0626 10.9774C31.2557 11.0122 37.0574 16.8696 37.0226 24.0626C36.9878 31.2557 31.1304 37.0574 23.9374 37.0226C16.7443 36.9878 10.9426 31.1304 10.9774 23.9374C11.0122 16.7443 16.8696 10.9426 24.0626 10.9774ZM6.94261 31.3809C6.79652 31.4296 6.65739 31.4504 6.5113 31.4504C5.92696 31.4504 5.37739 31.0748 5.18957 30.4904C4.53565 28.48 4.2087 26.3165 4.2087 24.0626C4.2087 22.8104 4.31304 21.5026 4.52174 20.1878C6.09391 12.2017 12.2296 6.03826 20.16 4.4313C20.9183 4.27826 21.6487 4.76522 21.8017 5.51652C21.9548 6.26783 21.4678 7.00522 20.7165 7.15826C13.8922 8.53565 8.61217 13.8435 7.26957 20.6678C7.09565 21.7878 7.00522 22.9496 7.00522 24.0557C7.00522 26.0104 7.29043 27.8887 7.85391 29.6209C8.09044 30.3513 7.69391 31.1374 6.95652 31.3739L6.94261 31.3809ZM39.1791 37.127C35.52 41.4817 30.2539 43.8748 24.3548 43.8748C18.4557 43.8748 12.8626 41.447 9.05739 37.2174C8.54261 36.647 8.5913 35.7635 9.16174 35.2557C9.73217 34.7409 10.6157 34.7896 11.1235 35.36C14.3513 38.9496 19.2904 41.0991 24.3478 41.0991C29.4052 41.0991 33.92 39.0539 37.0435 35.3391C37.5374 34.7548 38.4139 34.6713 39.0052 35.1722C39.5965 35.6661 39.6661 36.5426 39.1722 37.1339L39.1791 37.127ZM42.9426 30.4C42.7409 30.9774 42.2052 31.3322 41.6278 31.3322C41.4748 31.3322 41.3217 31.3043 41.1687 31.2557C40.4452 31.0052 40.0626 30.2122 40.313 29.4817C40.5565 28.7722 40.7791 28.0278 40.96 27.2626C41.1687 26.2122 41.28 25.0852 41.28 23.9722C41.28 15.8052 35.4574 8.73044 27.4296 7.14435C26.6783 6.99826 26.1565 6.26783 26.2957 5.51652C26.4348 4.76522 27.1304 4.26435 27.8748 4.39652C27.8887 4.39652 27.9513 4.41043 27.9652 4.41739C37.287 6.25391 44.0557 14.4835 44.0557 23.9722C44.0557 25.2661 43.9304 26.5739 43.673 27.8539C43.4574 28.7652 43.2139 29.6 42.9357 30.4H42.9426Z" fill="url(#paint0_linear_911_12416)"/>
<defs>
<linearGradient id="paint0_linear_911_12416" x1="24.3556" y1="47.7468" x2="24.3556" y2="0" gradientUnits="userSpaceOnUse">
<stop stop-color="#FAC10D"/>
<stop offset="1" stop-color="#F05A28"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -1,20 +0,0 @@
import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport';
import { config } from 'app/core/config';
import { RouteDescriptor } from 'app/core/navigation/types';
export function getRoutes(): RouteDescriptor[] {
if (config.featureToggles.queryLibrary) {
return [
{
path: `/query-library`,
exact: true,
component: SafeDynamicImport(
() =>
import(/* webpackChunkName: "QueryLibraryPage" */ 'app/features/query-library/components/QueryLibraryPage')
),
},
];
}
return [];
}

View File

@ -1,30 +0,0 @@
import { SavedQueryRef } from './api/SavedQueriesApi';
export interface QueryItem {
id: number;
selected?: boolean;
tags: string[];
title: string;
type: string;
uid: string;
ds_uid: string[];
uri: string;
url: string;
sortMeta?: number;
sortMetaName?: string;
location?: string;
}
type SavedQueryVariable<T = unknown> = {
type: 'text' | 'datasource' | string; // TODO: enumify
name: string;
current: {
// current.value follows the structure from dashboard variables
value: T;
};
};
export type SavedQueryLink = {
ref: SavedQueryRef;
variables: SavedQueryVariable[];
};

View File

@ -1,14 +0,0 @@
import { DataQuery } from '@grafana/data/src';
export const defaultQuery: DataQuery = {
refId: 'A',
datasource: {
type: 'datasource',
uid: 'grafana',
},
queryType: 'measurements',
};
export const implementationComingSoonAlert = () => {
alert('Implementation coming soon!');
};

View File

@ -28,15 +28,12 @@ import { DashboardQueryEditor, isSharedDashboardQuery } from 'app/plugins/dataso
import { GrafanaQuery, GrafanaQueryType } from 'app/plugins/datasource/grafana/types';
import { QueryGroupDataSource, QueryGroupOptions } from 'app/types';
import { isQueryWithMixedDatasource } from '../../query-library/api/SavedQueriesApi';
import { getSavedQuerySrv } from '../../query-library/api/SavedQueriesSrv';
import { PanelQueryRunner } from '../state/PanelQueryRunner';
import { updateQueries } from '../state/updateQueries';
import { GroupActionComponents } from './QueryActionComponent';
import { QueryEditorRows } from './QueryEditorRows';
import { QueryGroupOptionsEditor } from './QueryGroupOptions';
import { SavedQueryPicker } from './SavedQueryPicker';
export interface Props {
queryRunner: PanelQueryRunner;
@ -164,69 +161,6 @@ export class QueryGroup extends PureComponent<Props, State> {
});
};
onChangeSavedQuery = async (savedQueryUid: string | null) => {
if (!savedQueryUid?.length) {
// leave the queries, remove the link
this.onChange({
queries: this.state.queries,
savedQueryUid: null,
dataSource: {
name: this.state.dsSettings?.name,
uid: this.state.dsSettings?.uid,
type: this.state.dsSettings?.meta.id,
default: this.state.dsSettings?.isDefault,
},
});
this.setState({
queries: this.state.queries,
savedQueryUid: null,
dataSource: this.state.dataSource,
dsSettings: this.state.dsSettings,
});
return;
}
const { dsSettings } = this.state;
const currentDS = dsSettings ? await getDataSourceSrv().get(dsSettings.uid) : undefined;
const resp = await getSavedQuerySrv().getSavedQueries([{ uid: savedQueryUid }]);
if (!resp?.length) {
throw new Error('TODO error handling');
}
const savedQuery = resp[0];
const isMixedDatasource = isQueryWithMixedDatasource(savedQuery);
const nextDS = isMixedDatasource
? await getDataSourceSrv().get('-- Mixed --')
: await getDataSourceSrv().get(savedQuery.queries[0].datasource?.uid);
// We need to pass in newSettings.uid as well here as that can be a variable expression and we want to store that in the query model not the current ds variable value
const queries = await updateQueries(nextDS, nextDS.uid, savedQuery.queries, currentDS);
const newDsSettings = await getDataSourceSrv().getInstanceSettings(nextDS.uid);
if (!newDsSettings) {
throw new Error('TODO error handling');
}
this.onChange({
queries,
savedQueryUid: savedQueryUid,
dataSource: {
name: newDsSettings.name,
uid: newDsSettings.uid,
type: newDsSettings.meta.id,
default: newDsSettings.isDefault,
},
});
this.setState({
queries,
savedQueryUid,
dataSource: nextDS,
dsSettings: newDsSettings,
});
};
onAddQueryClick = () => {
const { queries } = this.state;
this.onQueriesChange(addQuery(queries, this.newQuery()));
@ -340,18 +274,6 @@ export class QueryGroup extends PureComponent<Props, State> {
</>
)}
</div>
{config.featureToggles.queryLibrary && (
<>
<div className={styles.dataSourceRow}>
<InlineFormLabel htmlFor="saved-query-picker" width={'auto'}>
Saved query
</InlineFormLabel>
<div className={styles.dataSourceRowItem}>
<SavedQueryPicker current={this.state.savedQueryUid} onChange={this.onChangeSavedQuery} />
</div>
</div>
</>
)}
</div>
);
}

View File

@ -15,7 +15,6 @@ import { getLiveRoutes } from 'app/features/live/pages/routes';
import { getRoutes as getPluginCatalogRoutes } from 'app/features/plugins/admin/routes';
import { getAppPluginRoutes } from 'app/features/plugins/routes';
import { getProfileRoutes } from 'app/features/profile/routes';
import { getRoutes as getQueryLibraryRoutes } from 'app/features/query-library/routes';
import { AccessControlAction, DashboardRoutes } from 'app/types';
import { SafeDynamicImport } from '../core/components/DynamicImports/SafeDynamicImport';
@ -508,7 +507,6 @@ export function getAppRoutes(): RouteDescriptor[] {
...getLiveRoutes(),
...getAlertingRoutes(),
...getProfileRoutes(),
...getQueryLibraryRoutes(),
...extraRoutes,
...getPublicDashboardRoutes(),
...getDataConnectionsRoutes(),