Query history: Create API to delete query from query history (#44653)

* Query history: Add delete and refactor

* Update docs/sources/http_api/query_history.md

Co-authored-by: Piotr Jamróz <pm.jamroz@gmail.com>

Co-authored-by: Piotr Jamróz <pm.jamroz@gmail.com>
This commit is contained in:
Ivana Huckova 2022-02-04 16:14:36 +01:00 committed by GitHub
parent 1680e284e5
commit 0f362f8dfc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 211 additions and 25 deletions

View File

@ -49,11 +49,63 @@ JSON body schema:
HTTP/1.1 200
Content-Type: application/json
{
"message": "Query successfully added to query history",
"result": {
"uid": "Ahg678z",
"datasourceUid": "PE1C5CBDA0504A6A3",
"createdBy": 1,
"createdAt": 1643630762,
"starred": false,
"comment": "",
"queries": [
{
"refId": "A",
"key": "Q-87fed8e3-62ba-4eb2-8d2a-4129979bb4de-0",
"scenarioId": "csv_content",
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
}
}
]
}
}
```
Status codes:
- **200** OK
- **500** Errors (invalid JSON, missing or invalid fields)
- **400** - Errors (invalid JSON, missing or invalid fields)
- **500** Unable to add query to the database
### Delete query from Query history by UID
`DELETE /api/query-history/:uid`
Deletes the query in query history that matches the specified uid. It requires that the user is logged in and that Query history feature is enabled in config file.
**Example Request**:
```http
DELETE /api/query-history/P8zM2I1nz HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
**Example Response**:
```http
HTTP/1.1 200
Content-Type: application/json
{
"message": "Query deleted",
"id": 28
}
```
Status codes:
- **200** OK
- **404** - Query in query history not found
- **500** Unable to delete query from the database

View File

@ -7,12 +7,14 @@ import (
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
)
func (s *QueryHistoryService) registerAPIEndpoints() {
s.RouteRegister.Group("/api/query-history", func(entities routing.RouteRegister) {
entities.Post("/", middleware.ReqSignedIn, routing.Wrap(s.createHandler))
entities.Delete("/:uid", middleware.ReqSignedIn, routing.Wrap(s.deleteHandler))
})
}
@ -22,10 +24,31 @@ func (s *QueryHistoryService) createHandler(c *models.ReqContext) response.Respo
return response.Error(http.StatusBadRequest, "bad request data", err)
}
err := s.CreateQueryInQueryHistory(c.Req.Context(), c.SignedInUser, cmd)
query, err := s.CreateQueryInQueryHistory(c.Req.Context(), c.SignedInUser, cmd)
if err != nil {
return response.Error(500, "Failed to create query history", err)
return response.Error(http.StatusInternalServerError, "Failed to create query history", err)
}
return response.Success("Query successfully added to query history")
return response.JSON(http.StatusOK, QueryHistoryResponse{Result: query})
}
func (s *QueryHistoryService) deleteHandler(c *models.ReqContext) response.Response {
queryUID := web.Params(c.Req)[":uid"]
if len(queryUID) == 0 {
return response.Error(http.StatusNotFound, "Query in query history not found", nil)
}
if !util.IsValidShortUID(queryUID) {
return response.Error(http.StatusNotFound, "Query in query history not found", nil)
}
id, err := s.DeleteQueryFromQueryHistory(c.Req.Context(), c.SignedInUser, queryUID)
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to delete query from query history", err)
}
return response.JSON(http.StatusOK, DeleteQueryFromQueryHistoryResponse{
Message: "Query deleted",
ID: id,
})
}

View File

@ -9,12 +9,12 @@ import (
"github.com/grafana/grafana/pkg/util"
)
func (s QueryHistoryService) createQuery(ctx context.Context, user *models.SignedInUser, cmd CreateQueryInQueryHistoryCommand) error {
func (s QueryHistoryService) createQuery(ctx context.Context, user *models.SignedInUser, cmd CreateQueryInQueryHistoryCommand) (QueryHistoryDTO, error) {
queryHistory := QueryHistory{
OrgId: user.OrgId,
Uid: util.GenerateShortUID(),
OrgID: user.OrgId,
UID: util.GenerateShortUID(),
Queries: cmd.Queries,
DatasourceUid: cmd.DatasourceUid,
DatasourceUID: cmd.DatasourceUID,
CreatedBy: user.UserId,
CreatedAt: time.Now().Unix(),
Comment: "",
@ -25,8 +25,32 @@ func (s QueryHistoryService) createQuery(ctx context.Context, user *models.Signe
return err
})
if err != nil {
return err
return QueryHistoryDTO{}, err
}
return nil
dto := QueryHistoryDTO{
UID: queryHistory.UID,
DatasourceUID: queryHistory.DatasourceUID,
CreatedBy: queryHistory.CreatedBy,
CreatedAt: queryHistory.CreatedAt,
Comment: queryHistory.Comment,
Queries: queryHistory.Queries,
Starred: false,
}
return dto, nil
}
func (s QueryHistoryService) deleteQuery(ctx context.Context, user *models.SignedInUser, UID string) (int64, error) {
var queryID int64
err := s.SQLStore.WithDbSession(ctx, func(session *sqlstore.DBSession) error {
id, err := session.Where("org_id = ? AND created_by = ? AND uid = ?", user.OrgId, user.UserId, UID).Delete(QueryHistory{})
if id == 0 {
return ErrQueryNotFound
}
queryID = id
return err
})
return queryID, err
}

View File

@ -1,21 +1,48 @@
package queryhistory
import (
"errors"
"github.com/grafana/grafana/pkg/components/simplejson"
)
var (
ErrQueryNotFound = errors.New("query in query history not found")
)
type QueryHistory struct {
Id int64 `json:"id"`
Uid string `json:"uid"`
DatasourceUid string `json:"datasourceUid"`
OrgId int64 `json:"orgId"`
ID int64 `xorm:"pk autoincr 'id'"`
UID string `xorm:"uid"`
DatasourceUID string `xorm:"datasource_uid"`
OrgID int64 `xorm:"org_id"`
CreatedBy int64
CreatedAt int64
Comment string
Queries *simplejson.Json
}
type CreateQueryInQueryHistoryCommand struct {
DatasourceUID string `json:"datasourceUid"`
Queries *simplejson.Json `json:"queries"`
}
type QueryHistoryDTO struct {
UID string `json:"uid"`
DatasourceUID string `json:"datasourceUid"`
CreatedBy int64 `json:"createdBy"`
CreatedAt int64 `json:"createdAt"`
Comment string `json:"comment"`
Queries *simplejson.Json `json:"queries"`
Starred bool `json:"starred"`
}
type CreateQueryInQueryHistoryCommand struct {
DatasourceUid string `json:"datasourceUid"`
Queries *simplejson.Json `json:"queries"`
// QueryHistoryResponse is a response struct for QueryHistoryDTO
type QueryHistoryResponse struct {
Result QueryHistoryDTO `json:"result"`
}
// DeleteQueryFromQueryHistoryResponse is the response struct for deleting a query from query history
type DeleteQueryFromQueryHistoryResponse struct {
ID int64 `json:"id"`
Message string `json:"message"`
}

View File

@ -27,7 +27,8 @@ func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, routeRegister
}
type Service interface {
CreateQueryInQueryHistory(ctx context.Context, user *models.SignedInUser, cmd CreateQueryInQueryHistoryCommand) error
CreateQueryInQueryHistory(ctx context.Context, user *models.SignedInUser, cmd CreateQueryInQueryHistoryCommand) (QueryHistoryDTO, error)
DeleteQueryFromQueryHistory(ctx context.Context, user *models.SignedInUser, UID string) (int64, error)
}
type QueryHistoryService struct {
@ -37,6 +38,10 @@ type QueryHistoryService struct {
log log.Logger
}
func (s QueryHistoryService) CreateQueryInQueryHistory(ctx context.Context, user *models.SignedInUser, cmd CreateQueryInQueryHistoryCommand) error {
func (s QueryHistoryService) CreateQueryInQueryHistory(ctx context.Context, user *models.SignedInUser, cmd CreateQueryInQueryHistoryCommand) (QueryHistoryDTO, error) {
return s.createQuery(ctx, user, cmd)
}
func (s QueryHistoryService) DeleteQueryFromQueryHistory(ctx context.Context, user *models.SignedInUser, UID string) (int64, error) {
return s.deleteQuery(ctx, user, UID)
}

View File

@ -11,7 +11,7 @@ func TestCreateQueryInQueryHistory(t *testing.T) {
testScenario(t, "When users tries to create query in query history it should succeed",
func(t *testing.T, sc scenarioContext) {
command := CreateQueryInQueryHistoryCommand{
DatasourceUid: "NCzh67i",
DatasourceUID: "NCzh67i",
Queries: simplejson.NewFromAny(map[string]interface{}{
"expr": "test",
}),

View File

@ -0,0 +1,23 @@
package queryhistory
import (
"testing"
"github.com/grafana/grafana/pkg/web"
"github.com/stretchr/testify/require"
)
func TestDeleteQueryFromQueryHistory(t *testing.T) {
testScenarioWithQueryInQueryHistory(t, "When users tries to delete query in query history that does not exist, it should fail",
func(t *testing.T, sc scenarioContext) {
resp := sc.service.deleteHandler(sc.reqContext)
require.Equal(t, 404, resp.Status())
})
testScenarioWithQueryInQueryHistory(t, "When users tries to delete query in query history that exists, it should succeed",
func(t *testing.T, sc scenarioContext) {
sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID})
resp := sc.service.deleteHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
})
}

View File

@ -9,6 +9,8 @@ import (
"testing"
"time"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
@ -22,10 +24,11 @@ var (
)
type scenarioContext struct {
ctx *web.Context
service *QueryHistoryService
reqContext *models.ReqContext
sqlStore *sqlstore.SQLStore
ctx *web.Context
service *QueryHistoryService
reqContext *models.ReqContext
sqlStore *sqlstore.SQLStore
initialResult QueryHistoryResponse
}
func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioContext)) {
@ -71,7 +74,36 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
})
}
func testScenarioWithQueryInQueryHistory(t *testing.T, desc string, fn func(t *testing.T, sc scenarioContext)) {
t.Helper()
testScenario(t, desc, func(t *testing.T, sc scenarioContext) {
command := CreateQueryInQueryHistoryCommand{
DatasourceUID: "NCzh67i",
Queries: simplejson.NewFromAny(map[string]interface{}{
"expr": "test",
}),
}
sc.reqContext.Req.Body = mockRequestBody(command)
resp := sc.service.createHandler(sc.reqContext)
sc.initialResult = validateAndUnMarshalResponse(t, resp)
fn(t, sc)
})
}
func mockRequestBody(v interface{}) io.ReadCloser {
b, _ := json.Marshal(v)
return io.NopCloser(bytes.NewReader(b))
}
func validateAndUnMarshalResponse(t *testing.T, resp response.Response) QueryHistoryResponse {
t.Helper()
require.Equal(t, 200, resp.Status())
var result = QueryHistoryResponse{}
err := json.Unmarshal(resp.Body(), &result)
require.NoError(t, err)
return result
}