mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Chore: remove querylibrary
feature toggle (#65021)
* chore: remove querylibrary * chore: remove querylibrary * chore: remove querylibrary
This commit is contained in:
parent
68551ac9ca
commit
4274b9414f
@ -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
2
.github/CODEOWNERS
vendored
@ -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
|
||||
|
@ -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 |
|
||||
|
@ -61,7 +61,6 @@ export interface FeatureToggles {
|
||||
redshiftAsyncQueryDataSupport?: boolean;
|
||||
athenaAsyncQueryDataSupport?: boolean;
|
||||
newPanelChromeUI?: boolean;
|
||||
queryLibrary?: boolean;
|
||||
showDashboardValidationWarnings?: boolean;
|
||||
mysqlAnsiQuotes?: boolean;
|
||||
accessControlOnCall?: boolean;
|
||||
|
@ -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"))
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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)),
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
@ -15,7 +15,6 @@ const (
|
||||
WeightSavedItems
|
||||
WeightCreate
|
||||
WeightDashboard
|
||||
WeightQueryLibrary
|
||||
WeightExplore
|
||||
WeightAlerting
|
||||
WeightDataConnections
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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;
|
@ -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;
|
@ -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;
|
||||
`,
|
||||
};
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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};
|
||||
`,
|
||||
};
|
||||
};
|
@ -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%;
|
||||
`,
|
||||
};
|
||||
};
|
@ -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;
|
||||
`,
|
||||
};
|
||||
};
|
@ -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 </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)};
|
||||
`,
|
||||
};
|
||||
};
|
@ -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);
|
||||
`,
|
||||
};
|
||||
};
|
@ -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;
|
||||
`,
|
||||
};
|
||||
};
|
@ -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;
|
||||
`,
|
||||
};
|
||||
};
|
@ -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;
|
@ -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;
|
||||
`,
|
||||
};
|
||||
};
|
@ -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" />
|
||||
|
||||
{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' }}
|
||||
/>
|
||||
{getDsType()}
|
||||
{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}`}
|
||||
/>
|
||||
{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;
|
||||
`,
|
||||
};
|
||||
};
|
@ -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;
|
||||
`,
|
||||
};
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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;
|
||||
`,
|
||||
};
|
||||
};
|
@ -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;
|
||||
`,
|
||||
};
|
||||
};
|
@ -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};
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
@ -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 |
@ -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 |
@ -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 |
@ -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 [];
|
||||
}
|
@ -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[];
|
||||
};
|
@ -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!');
|
||||
};
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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(),
|
||||
|
Loading…
Reference in New Issue
Block a user