mirror of
https://github.com/grafana/grafana.git
synced 2025-08-01 05:31:49 +08:00
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:
@ -49,11 +49,63 @@ JSON body schema:
|
|||||||
`DELETE /api/query-history/: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.
|
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.
|
||||||
"message": "Query successfully added to query history",
|
|
||||||
|
**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
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Status codes:
|
||||||
|
|
||||||
- **200** – OK
|
- **200** – OK
|
||||||
- **404** - Query in query history not found
|
- **404** - Query in query history not found
|
||||||
- **500** – Unable to delete query from the database
|
- **500** – Unable to delete query from the database
|
||||||
|
|
||||||
- **200** – OK
|
- **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
|
||||||
|
@ -7,12 +7,14 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/api/routing"
|
"github.com/grafana/grafana/pkg/api/routing"
|
||||||
"github.com/grafana/grafana/pkg/middleware"
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/util"
|
||||||
"github.com/grafana/grafana/pkg/web"
|
"github.com/grafana/grafana/pkg/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *QueryHistoryService) registerAPIEndpoints() {
|
func (s *QueryHistoryService) registerAPIEndpoints() {
|
||||||
s.RouteRegister.Group("/api/query-history", func(entities routing.RouteRegister) {
|
s.RouteRegister.Group("/api/query-history", func(entities routing.RouteRegister) {
|
||||||
entities.Post("/", middleware.ReqSignedIn, routing.Wrap(s.createHandler))
|
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)
|
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 {
|
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,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -9,12 +9,12 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/util"
|
"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{
|
queryHistory := QueryHistory{
|
||||||
OrgId: user.OrgId,
|
OrgID: user.OrgId,
|
||||||
Uid: util.GenerateShortUID(),
|
UID: util.GenerateShortUID(),
|
||||||
Queries: cmd.Queries,
|
Queries: cmd.Queries,
|
||||||
DatasourceUid: cmd.DatasourceUid,
|
DatasourceUID: cmd.DatasourceUID,
|
||||||
CreatedBy: user.UserId,
|
CreatedBy: user.UserId,
|
||||||
CreatedAt: time.Now().Unix(),
|
CreatedAt: time.Now().Unix(),
|
||||||
Comment: "",
|
Comment: "",
|
||||||
@ -25,8 +25,32 @@ func (s QueryHistoryService) createQuery(ctx context.Context, user *models.Signe
|
|||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
if err != nil {
|
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
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,48 @@
|
|||||||
package queryhistory
|
package queryhistory
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrQueryNotFound = errors.New("query in query history not found")
|
||||||
|
)
|
||||||
|
|
||||||
type QueryHistory struct {
|
type QueryHistory struct {
|
||||||
Id int64 `json:"id"`
|
ID int64 `xorm:"pk autoincr 'id'"`
|
||||||
Uid string `json:"uid"`
|
UID string `xorm:"uid"`
|
||||||
DatasourceUid string `json:"datasourceUid"`
|
DatasourceUID string `xorm:"datasource_uid"`
|
||||||
OrgId int64 `json:"orgId"`
|
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"`
|
CreatedBy int64 `json:"createdBy"`
|
||||||
CreatedAt int64 `json:"createdAt"`
|
CreatedAt int64 `json:"createdAt"`
|
||||||
Comment string `json:"comment"`
|
Comment string `json:"comment"`
|
||||||
Queries *simplejson.Json `json:"queries"`
|
Queries *simplejson.Json `json:"queries"`
|
||||||
|
Starred bool `json:"starred"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateQueryInQueryHistoryCommand struct {
|
// QueryHistoryResponse is a response struct for QueryHistoryDTO
|
||||||
DatasourceUid string `json:"datasourceUid"`
|
type QueryHistoryResponse struct {
|
||||||
Queries *simplejson.Json `json:"queries"`
|
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"`
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,8 @@ func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, routeRegister
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Service interface {
|
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 {
|
type QueryHistoryService struct {
|
||||||
@ -37,6 +38,10 @@ type QueryHistoryService struct {
|
|||||||
log log.Logger
|
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)
|
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)
|
||||||
|
}
|
||||||
|
@ -11,7 +11,7 @@ func TestCreateQueryInQueryHistory(t *testing.T) {
|
|||||||
testScenario(t, "When users tries to create query in query history it should succeed",
|
testScenario(t, "When users tries to create query in query history it should succeed",
|
||||||
func(t *testing.T, sc scenarioContext) {
|
func(t *testing.T, sc scenarioContext) {
|
||||||
command := CreateQueryInQueryHistoryCommand{
|
command := CreateQueryInQueryHistoryCommand{
|
||||||
DatasourceUid: "NCzh67i",
|
DatasourceUID: "NCzh67i",
|
||||||
Queries: simplejson.NewFromAny(map[string]interface{}{
|
Queries: simplejson.NewFromAny(map[string]interface{}{
|
||||||
"expr": "test",
|
"expr": "test",
|
||||||
}),
|
}),
|
||||||
|
23
pkg/services/queryhistory/queryhistory_delete_test.go
Normal file
23
pkg/services/queryhistory/queryhistory_delete_test.go
Normal 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())
|
||||||
|
})
|
||||||
|
}
|
@ -9,6 +9,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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/models"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
@ -22,10 +24,11 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type scenarioContext struct {
|
type scenarioContext struct {
|
||||||
ctx *web.Context
|
ctx *web.Context
|
||||||
service *QueryHistoryService
|
service *QueryHistoryService
|
||||||
reqContext *models.ReqContext
|
reqContext *models.ReqContext
|
||||||
sqlStore *sqlstore.SQLStore
|
sqlStore *sqlstore.SQLStore
|
||||||
|
initialResult QueryHistoryResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioContext)) {
|
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 {
|
func mockRequestBody(v interface{}) io.ReadCloser {
|
||||||
b, _ := json.Marshal(v)
|
b, _ := json.Marshal(v)
|
||||||
return io.NopCloser(bytes.NewReader(b))
|
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
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user