wire up unified search from the ui; add basic search support (#94358)
* wire up search from the ui; add basic search support
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
package unifiedSearch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type SearchHTTPService interface {
|
||||
RegisterHTTPRoutes(storageRoute routing.RouteRegister)
|
||||
}
|
||||
|
||||
type searchHTTPService struct {
|
||||
search SearchService
|
||||
}
|
||||
|
||||
func ProvideSearchHTTPService(search SearchService) SearchHTTPService {
|
||||
return &searchHTTPService{search: search}
|
||||
}
|
||||
|
||||
func (s *searchHTTPService) RegisterHTTPRoutes(storageRoute routing.RouteRegister) {
|
||||
storageRoute.Post("/", middleware.ReqSignedIn, routing.Wrap(s.doQuery))
|
||||
}
|
||||
|
||||
func (s *searchHTTPService) doQuery(c *contextmodel.ReqContext) response.Response {
|
||||
searchReadinessCheckResp := s.search.IsReady(c.Req.Context(), c.SignedInUser.GetOrgID())
|
||||
if !searchReadinessCheckResp.IsReady {
|
||||
return response.JSON(http.StatusOK, &backend.DataResponse{
|
||||
Frames: []*data.Frame{{
|
||||
Name: "Loading",
|
||||
}},
|
||||
Error: nil,
|
||||
})
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(c.Req.Body)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusInternalServerError, "error reading bytes", err)
|
||||
}
|
||||
|
||||
query := &Query{}
|
||||
err = json.Unmarshal(body, query)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusBadRequest, "error parsing body", err)
|
||||
}
|
||||
|
||||
resp := s.search.doQuery(c.Req.Context(), c.SignedInUser, c.SignedInUser.GetOrgID(), *query)
|
||||
|
||||
if resp.Error != nil {
|
||||
return response.Error(http.StatusInternalServerError, "error handling search request", resp.Error)
|
||||
}
|
||||
|
||||
if len(resp.Frames) == 0 {
|
||||
msg := "invalid search response"
|
||||
return response.Error(http.StatusInternalServerError, msg, errors.New(msg))
|
||||
}
|
||||
|
||||
bytes, err := resp.MarshalJSON()
|
||||
if err != nil {
|
||||
return response.Error(http.StatusInternalServerError, "error marshalling response", err)
|
||||
}
|
||||
|
||||
return response.JSON(http.StatusOK, bytes)
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package unifiedSearch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"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/store"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
)
|
||||
|
||||
type StandardSearchService struct {
|
||||
registry.BackgroundService
|
||||
cfg *setting.Cfg
|
||||
sql db.DB
|
||||
ac accesscontrol.Service
|
||||
orgService org.Service
|
||||
userService user.Service
|
||||
logger log.Logger
|
||||
reIndexCh chan struct{}
|
||||
features featuremgmt.FeatureToggles
|
||||
resourceClient resource.ResourceClient
|
||||
}
|
||||
|
||||
func (s *StandardSearchService) IsReady(ctx context.Context, orgId int64) IsSearchReadyResponse {
|
||||
return IsSearchReadyResponse{IsReady: true}
|
||||
}
|
||||
|
||||
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, folderStore folder.Store, resourceClient resource.ResourceClient) SearchService {
|
||||
logger := log.New("searchV3")
|
||||
s := &StandardSearchService{
|
||||
cfg: cfg,
|
||||
sql: sql,
|
||||
ac: ac,
|
||||
logger: logger,
|
||||
reIndexCh: make(chan struct{}, 1),
|
||||
orgService: orgService,
|
||||
userService: userService,
|
||||
features: features,
|
||||
resourceClient: resourceClient,
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StandardSearchService) IsDisabled() bool {
|
||||
return !s.features.IsEnabledGlobally(featuremgmt.FlagPanelTitleSearch)
|
||||
}
|
||||
|
||||
func (s *StandardSearchService) Run(ctx context.Context) error {
|
||||
// TODO: implement this? ( copied from pkg/services/searchV2/service.go )
|
||||
// orgQuery := &org.SearchOrgsQuery{}
|
||||
// result, err := s.orgService.Search(ctx, orgQuery)
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("can't get org list: %w", err)
|
||||
// }
|
||||
// orgIDs := make([]int64, 0, len(result))
|
||||
// for _, org := range result {
|
||||
// orgIDs = append(orgIDs, org.ID)
|
||||
// }
|
||||
// TODO: do we need to initialize the bleve index again ( should be initialized on startup )?
|
||||
// return s.dashboardIndex.run(ctx, orgIDs, s.reIndexCh)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StandardSearchService) TriggerReIndex() {
|
||||
select {
|
||||
case s.reIndexCh <- struct{}{}:
|
||||
default:
|
||||
// channel is full => re-index will happen soon anyway.
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StandardSearchService) getUser(ctx context.Context, backendUser *backend.User, orgId int64) (*user.SignedInUser, error) {
|
||||
// TODO: get user & user's permissions from the request context
|
||||
var usr *user.SignedInUser
|
||||
if s.cfg.AnonymousEnabled && backendUser.Email == "" && backendUser.Login == "" {
|
||||
getOrg := org.GetOrgByNameQuery{Name: s.cfg.AnonymousOrgName}
|
||||
orga, err := s.orgService.GetByName(ctx, &getOrg)
|
||||
if err != nil {
|
||||
s.logger.Error("Anonymous access organization error.", "org_name", s.cfg.AnonymousOrgName, "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
usr = &user.SignedInUser{
|
||||
OrgID: orga.ID,
|
||||
OrgName: orga.Name,
|
||||
OrgRole: org.RoleType(s.cfg.AnonymousOrgRole),
|
||||
IsAnonymous: true,
|
||||
}
|
||||
} else {
|
||||
getSignedInUserQuery := &user.GetSignedInUserQuery{
|
||||
Login: backendUser.Login,
|
||||
Email: backendUser.Email,
|
||||
OrgID: orgId,
|
||||
}
|
||||
var err error
|
||||
usr, err = s.userService.GetSignedInUser(ctx, getSignedInUserQuery)
|
||||
if err != nil {
|
||||
s.logger.Error("Error while retrieving user", "error", err, "email", backendUser.Email, "login", getSignedInUserQuery.Login)
|
||||
return nil, errors.New("auth error")
|
||||
}
|
||||
|
||||
if usr == nil {
|
||||
s.logger.Error("No user found", "email", backendUser.Email)
|
||||
return nil, errors.New("auth error")
|
||||
}
|
||||
}
|
||||
|
||||
if usr.Permissions == nil {
|
||||
usr.Permissions = make(map[int64]map[string][]string)
|
||||
}
|
||||
|
||||
if _, ok := usr.Permissions[orgId]; ok {
|
||||
// permissions as part of the `s.sql.GetSignedInUser` query - return early
|
||||
return usr, nil
|
||||
}
|
||||
|
||||
// TODO: ensure this is cached
|
||||
permissions, err := s.ac.GetUserPermissions(ctx, usr,
|
||||
accesscontrol.Options{ReloadCache: false})
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to retrieve user permissions", "error", err, "email", backendUser.Email)
|
||||
return nil, errors.New("auth error")
|
||||
}
|
||||
|
||||
usr.Permissions[orgId] = accesscontrol.GroupScopesByActionContext(ctx, permissions)
|
||||
return usr, nil
|
||||
}
|
||||
|
||||
func (s *StandardSearchService) DoQuery(ctx context.Context, user *backend.User, orgID int64, q Query) *backend.DataResponse {
|
||||
signedInUser, err := s.getUser(ctx, user, orgID)
|
||||
if err != nil {
|
||||
return &backend.DataResponse{Error: err}
|
||||
}
|
||||
|
||||
query := s.doQuery(ctx, signedInUser, orgID, q)
|
||||
return query
|
||||
}
|
||||
|
||||
func (s *StandardSearchService) doQuery(ctx context.Context, signedInUser *user.SignedInUser, orgID int64, q Query) *backend.DataResponse {
|
||||
response := s.doSearchQuery(ctx, q, s.cfg.AppSubURL)
|
||||
return response
|
||||
}
|
||||
|
||||
func (s *StandardSearchService) doSearchQuery(ctx context.Context, qry Query, _ string) *backend.DataResponse {
|
||||
response := &backend.DataResponse{}
|
||||
|
||||
req := &resource.SearchRequest{Tenant: s.cfg.StackID, Query: qry.Query}
|
||||
res, err := s.resourceClient.Search(ctx, req)
|
||||
if err != nil {
|
||||
response.Error = err
|
||||
return response
|
||||
}
|
||||
|
||||
// TODO: implement this correctly
|
||||
frame := data.NewFrame("results", data.NewField("value", nil, []string{}))
|
||||
frame.Meta = &data.FrameMeta{Notices: []data.Notice{{Text: "TODO"}}}
|
||||
for _, r := range res.Items {
|
||||
frame.AppendRow(string(r.Value))
|
||||
}
|
||||
response.Frames = append(response.Frames, frame)
|
||||
return response
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package unifiedSearch
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
)
|
||||
|
||||
type FacetField struct {
|
||||
Field string `json:"field"`
|
||||
Limit int `json:"limit,omitempty"` // explicit page size
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
Query string `json:"query"`
|
||||
Location string `json:"location,omitempty"` // parent folder ID
|
||||
Sort string `json:"sort,omitempty"` // field ASC/DESC
|
||||
Datasource string `json:"ds_uid,omitempty"` // "datasource" collides with the JSON value at the same level :()
|
||||
DatasourceType string `json:"ds_type,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Kind []string `json:"kind,omitempty"`
|
||||
PanelType string `json:"panel_type,omitempty"`
|
||||
UIDs []string `json:"uid,omitempty"`
|
||||
Explain bool `json:"explain,omitempty"` // adds details on why document matched
|
||||
WithAllowedActions bool `json:"withAllowedActions,omitempty"` // adds allowed actions per entity
|
||||
Facet []FacetField `json:"facet,omitempty"`
|
||||
SkipLocation bool `json:"skipLocation,omitempty"`
|
||||
HasPreview string `json:"hasPreview,omitempty"` // the light|dark theme
|
||||
Limit int `json:"limit,omitempty"` // explicit page size
|
||||
From int `json:"from,omitempty"` // for paging
|
||||
}
|
||||
|
||||
type IsSearchReadyResponse struct {
|
||||
IsReady bool
|
||||
Reason string // initial-indexing-ongoing, org-indexing-ongoing
|
||||
}
|
||||
|
||||
type SearchService interface {
|
||||
registry.CanBeDisabled
|
||||
registry.BackgroundService
|
||||
DoQuery(ctx context.Context, user *backend.User, orgId int64, query Query) *backend.DataResponse
|
||||
doQuery(ctx context.Context, user *user.SignedInUser, orgId int64, query Query) *backend.DataResponse
|
||||
IsReady(ctx context.Context, orgId int64) IsSearchReadyResponse
|
||||
// RegisterDashboardIndexExtender(ext DashboardIndexExtender)
|
||||
TriggerReIndex()
|
||||
}
|
||||
Reference in New Issue
Block a user