mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
LibraryPanels: Improves the Get All experience (#32028)
* LibraryPanels: Improves Get All Api * Refactor: using useReducer instead of useState * Refactor: adds Pagination to UI * Tests: adds reducer tests * Refactor: using Observable instead to avoid flickering * Refactor: moves exclusion to backend instead * Chore: changing back the perPage default value
This commit is contained in:
@@ -13,9 +13,11 @@ interface Props {
|
||||
numberOfPages: number;
|
||||
/** Callback function for fetching the selected page */
|
||||
onNavigate: (toPage: number) => void;
|
||||
/** When set to true and the pagination result is only one page it will not render the pagination at all */
|
||||
hideWhenSinglePage?: boolean;
|
||||
}
|
||||
|
||||
export const Pagination: React.FC<Props> = ({ currentPage, numberOfPages, onNavigate }) => {
|
||||
export const Pagination: React.FC<Props> = ({ currentPage, numberOfPages, onNavigate, hideWhenSinglePage }) => {
|
||||
const styles = getStyles();
|
||||
const pages = [...new Array(numberOfPages).keys()];
|
||||
|
||||
@@ -71,6 +73,10 @@ export const Pagination: React.FC<Props> = ({ currentPage, numberOfPages, onNavi
|
||||
return pagesToRender;
|
||||
}, []);
|
||||
|
||||
if (hideWhenSinglePage && numberOfPages <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<ol>
|
||||
|
||||
@@ -81,7 +81,7 @@ func (lps *LibraryPanelService) getHandler(c *models.ReqContext) response.Respon
|
||||
|
||||
// getAllHandler handles GET /api/library-panels/.
|
||||
func (lps *LibraryPanelService) getAllHandler(c *models.ReqContext) response.Response {
|
||||
libraryPanels, err := lps.getAllLibraryPanels(c, c.QueryInt64("limit"))
|
||||
libraryPanels, err := lps.getAllLibraryPanels(c, c.QueryInt("perPage"), c.QueryInt("page"), c.Query("name"), c.Query("excludeUid"))
|
||||
if err != nil {
|
||||
return toLibraryPanelError(err, "Failed to get library panels")
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package librarypanels
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
@@ -365,60 +366,101 @@ func (lps *LibraryPanelService) getLibraryPanel(c *models.ReqContext, uid string
|
||||
}
|
||||
|
||||
// getAllLibraryPanels gets all library panels.
|
||||
func (lps *LibraryPanelService) getAllLibraryPanels(c *models.ReqContext, limit int64) ([]LibraryPanelDTO, error) {
|
||||
func (lps *LibraryPanelService) getAllLibraryPanels(c *models.ReqContext, perPage int, page int, name string, excludeUID string) (LibraryPanelSearchResult, error) {
|
||||
libraryPanels := make([]LibraryPanelWithMeta, 0)
|
||||
result := LibraryPanelSearchResult{}
|
||||
if perPage <= 0 {
|
||||
perPage = 100
|
||||
}
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
err := lps.SQLStore.WithDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
|
||||
builder := sqlstore.SQLBuilder{}
|
||||
builder.Write(sqlStatmentLibrayPanelDTOWithMeta)
|
||||
builder.Write(` WHERE lp.org_id=? AND lp.folder_id=0`, c.SignedInUser.OrgId)
|
||||
if len(strings.TrimSpace(name)) > 0 {
|
||||
builder.Write(" AND lp.name "+lps.SQLStore.Dialect.LikeStr()+" ?", "%"+name+"%")
|
||||
}
|
||||
if len(strings.TrimSpace(excludeUID)) > 0 {
|
||||
builder.Write(" AND lp.uid <> ?", excludeUID)
|
||||
}
|
||||
builder.Write(" UNION ")
|
||||
builder.Write(sqlStatmentLibrayPanelDTOWithMeta)
|
||||
builder.Write(" INNER JOIN dashboard AS dashboard on lp.folder_id = dashboard.id AND lp.folder_id<>0")
|
||||
builder.Write(` WHERE lp.org_id=?`, c.SignedInUser.OrgId)
|
||||
if len(strings.TrimSpace(name)) > 0 {
|
||||
builder.Write(" AND lp.name "+lps.SQLStore.Dialect.LikeStr()+" ?", "%"+name+"%")
|
||||
}
|
||||
if len(strings.TrimSpace(excludeUID)) > 0 {
|
||||
builder.Write(" AND lp.uid <> ?", excludeUID)
|
||||
}
|
||||
if c.SignedInUser.OrgRole != models.ROLE_ADMIN {
|
||||
builder.WriteDashboardPermissionFilter(c.SignedInUser, models.PERMISSION_VIEW)
|
||||
}
|
||||
if limit == 0 {
|
||||
limit = 1000
|
||||
if perPage != 0 {
|
||||
offset := perPage * (page - 1)
|
||||
builder.Write(lps.SQLStore.Dialect.LimitOffset(int64(perPage), int64(offset)))
|
||||
}
|
||||
builder.Write(lps.SQLStore.Dialect.Limit(limit))
|
||||
if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&libraryPanels); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
retDTOs := make([]LibraryPanelDTO, 0)
|
||||
for _, panel := range libraryPanels {
|
||||
retDTOs = append(retDTOs, LibraryPanelDTO{
|
||||
ID: panel.ID,
|
||||
OrgID: panel.OrgID,
|
||||
FolderID: panel.FolderID,
|
||||
UID: panel.UID,
|
||||
Name: panel.Name,
|
||||
Model: panel.Model,
|
||||
Version: panel.Version,
|
||||
Meta: LibraryPanelDTOMeta{
|
||||
CanEdit: true,
|
||||
ConnectedDashboards: panel.ConnectedDashboards,
|
||||
Created: panel.Created,
|
||||
Updated: panel.Updated,
|
||||
CreatedBy: LibraryPanelDTOMetaUser{
|
||||
ID: panel.CreatedBy,
|
||||
Name: panel.CreatedByName,
|
||||
AvatarUrl: dtos.GetGravatarUrl(panel.CreatedByEmail),
|
||||
},
|
||||
UpdatedBy: LibraryPanelDTOMetaUser{
|
||||
ID: panel.UpdatedBy,
|
||||
Name: panel.UpdatedByName,
|
||||
AvatarUrl: dtos.GetGravatarUrl(panel.UpdatedByEmail),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
var panels []LibraryPanel
|
||||
countBuilder := sqlstore.SQLBuilder{}
|
||||
countBuilder.Write("SELECT * FROM library_panel")
|
||||
countBuilder.Write(` WHERE org_id=?`, c.SignedInUser.OrgId)
|
||||
if len(strings.TrimSpace(name)) > 0 {
|
||||
countBuilder.Write(" AND name "+lps.SQLStore.Dialect.LikeStr()+" ?", "%"+name+"%")
|
||||
}
|
||||
if len(strings.TrimSpace(excludeUID)) > 0 {
|
||||
countBuilder.Write(" AND uid <> ?", excludeUID)
|
||||
}
|
||||
if err := session.SQL(countBuilder.GetSQLString(), countBuilder.GetParams()...).Find(&panels); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result = LibraryPanelSearchResult{
|
||||
TotalCount: int64(len(panels)),
|
||||
LibraryPanels: retDTOs,
|
||||
Page: page,
|
||||
PerPage: perPage,
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
retDTOs := make([]LibraryPanelDTO, 0)
|
||||
for _, panel := range libraryPanels {
|
||||
retDTOs = append(retDTOs, LibraryPanelDTO{
|
||||
ID: panel.ID,
|
||||
OrgID: panel.OrgID,
|
||||
FolderID: panel.FolderID,
|
||||
UID: panel.UID,
|
||||
Name: panel.Name,
|
||||
Model: panel.Model,
|
||||
Version: panel.Version,
|
||||
Meta: LibraryPanelDTOMeta{
|
||||
CanEdit: true,
|
||||
ConnectedDashboards: panel.ConnectedDashboards,
|
||||
Created: panel.Created,
|
||||
Updated: panel.Updated,
|
||||
CreatedBy: LibraryPanelDTOMetaUser{
|
||||
ID: panel.CreatedBy,
|
||||
Name: panel.CreatedByName,
|
||||
AvatarUrl: dtos.GetGravatarUrl(panel.CreatedByEmail),
|
||||
},
|
||||
UpdatedBy: LibraryPanelDTOMetaUser{
|
||||
ID: panel.UpdatedBy,
|
||||
Name: panel.UpdatedByName,
|
||||
AvatarUrl: dtos.GetGravatarUrl(panel.UpdatedByEmail),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return retDTOs, err
|
||||
return result, err
|
||||
}
|
||||
|
||||
// getConnectedDashboards gets all dashboards connected to a Library Panel.
|
||||
|
||||
@@ -16,11 +16,20 @@ func TestGetAllLibraryPanels(t *testing.T) {
|
||||
resp := sc.service.getAllHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
|
||||
var result libraryPanelsResult
|
||||
var result libraryPanelsSearch
|
||||
err := json.Unmarshal(resp.Body(), &result)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result.Result)
|
||||
require.Equal(t, 0, len(result.Result))
|
||||
var expected = libraryPanelsSearch{
|
||||
Result: libraryPanelsSearchResult{
|
||||
TotalCount: 0,
|
||||
LibraryPanels: []libraryPanel{},
|
||||
Page: 1,
|
||||
PerPage: 100,
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist, it should succeed",
|
||||
@@ -32,68 +41,132 @@ func TestGetAllLibraryPanels(t *testing.T) {
|
||||
resp = sc.service.getAllHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
|
||||
var result libraryPanelsResult
|
||||
var result libraryPanelsSearch
|
||||
err := json.Unmarshal(resp.Body(), &result)
|
||||
require.NoError(t, err)
|
||||
var expected = libraryPanelsResult{
|
||||
Result: []libraryPanel{
|
||||
{
|
||||
ID: 1,
|
||||
OrgID: 1,
|
||||
FolderID: 1,
|
||||
UID: result.Result[0].UID,
|
||||
Name: "Text - Library Panel",
|
||||
Model: map[string]interface{}{
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"id": float64(1),
|
||||
"title": "Text - Library Panel",
|
||||
"type": "text",
|
||||
},
|
||||
Version: 1,
|
||||
Meta: LibraryPanelDTOMeta{
|
||||
CanEdit: true,
|
||||
ConnectedDashboards: 0,
|
||||
Created: result.Result[0].Meta.Created,
|
||||
Updated: result.Result[0].Meta.Updated,
|
||||
CreatedBy: LibraryPanelDTOMetaUser{
|
||||
ID: 1,
|
||||
Name: UserInDbName,
|
||||
AvatarUrl: UserInDbAvatar,
|
||||
var expected = libraryPanelsSearch{
|
||||
Result: libraryPanelsSearchResult{
|
||||
TotalCount: 2,
|
||||
Page: 1,
|
||||
PerPage: 100,
|
||||
LibraryPanels: []libraryPanel{
|
||||
{
|
||||
ID: 1,
|
||||
OrgID: 1,
|
||||
FolderID: 1,
|
||||
UID: result.Result.LibraryPanels[0].UID,
|
||||
Name: "Text - Library Panel",
|
||||
Model: map[string]interface{}{
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"id": float64(1),
|
||||
"title": "Text - Library Panel",
|
||||
"type": "text",
|
||||
},
|
||||
UpdatedBy: LibraryPanelDTOMetaUser{
|
||||
ID: 1,
|
||||
Name: UserInDbName,
|
||||
AvatarUrl: UserInDbAvatar,
|
||||
Version: 1,
|
||||
Meta: LibraryPanelDTOMeta{
|
||||
CanEdit: true,
|
||||
ConnectedDashboards: 0,
|
||||
Created: result.Result.LibraryPanels[0].Meta.Created,
|
||||
Updated: result.Result.LibraryPanels[0].Meta.Updated,
|
||||
CreatedBy: LibraryPanelDTOMetaUser{
|
||||
ID: 1,
|
||||
Name: UserInDbName,
|
||||
AvatarUrl: UserInDbAvatar,
|
||||
},
|
||||
UpdatedBy: LibraryPanelDTOMetaUser{
|
||||
ID: 1,
|
||||
Name: UserInDbName,
|
||||
AvatarUrl: UserInDbAvatar,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
OrgID: 1,
|
||||
FolderID: 1,
|
||||
UID: result.Result.LibraryPanels[1].UID,
|
||||
Name: "Text - Library Panel2",
|
||||
Model: map[string]interface{}{
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"id": float64(1),
|
||||
"title": "Text - Library Panel2",
|
||||
"type": "text",
|
||||
},
|
||||
Version: 1,
|
||||
Meta: LibraryPanelDTOMeta{
|
||||
CanEdit: true,
|
||||
ConnectedDashboards: 0,
|
||||
Created: result.Result.LibraryPanels[1].Meta.Created,
|
||||
Updated: result.Result.LibraryPanels[1].Meta.Updated,
|
||||
CreatedBy: LibraryPanelDTOMetaUser{
|
||||
ID: 1,
|
||||
Name: UserInDbName,
|
||||
AvatarUrl: UserInDbAvatar,
|
||||
},
|
||||
UpdatedBy: LibraryPanelDTOMetaUser{
|
||||
ID: 1,
|
||||
Name: UserInDbName,
|
||||
AvatarUrl: UserInDbAvatar,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
OrgID: 1,
|
||||
FolderID: 1,
|
||||
UID: result.Result[1].UID,
|
||||
Name: "Text - Library Panel2",
|
||||
Model: map[string]interface{}{
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"id": float64(1),
|
||||
"title": "Text - Library Panel2",
|
||||
"type": "text",
|
||||
},
|
||||
Version: 1,
|
||||
Meta: LibraryPanelDTOMeta{
|
||||
CanEdit: true,
|
||||
ConnectedDashboards: 0,
|
||||
Created: result.Result[1].Meta.Created,
|
||||
Updated: result.Result[1].Meta.Updated,
|
||||
CreatedBy: LibraryPanelDTOMetaUser{
|
||||
ID: 1,
|
||||
Name: UserInDbName,
|
||||
AvatarUrl: UserInDbAvatar,
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and excludeUID is set, it should succeed and the result should be correct",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
command := getCreateCommand(sc.folder.Id, "Text - Library Panel2")
|
||||
resp := sc.service.createHandler(sc.reqContext, command)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
|
||||
err := sc.reqContext.Req.ParseForm()
|
||||
require.NoError(t, err)
|
||||
sc.reqContext.Req.Form.Add("excludeUid", sc.initialResult.Result.UID)
|
||||
resp = sc.service.getAllHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
|
||||
var result libraryPanelsSearch
|
||||
err = json.Unmarshal(resp.Body(), &result)
|
||||
require.NoError(t, err)
|
||||
var expected = libraryPanelsSearch{
|
||||
Result: libraryPanelsSearchResult{
|
||||
TotalCount: 1,
|
||||
Page: 1,
|
||||
PerPage: 100,
|
||||
LibraryPanels: []libraryPanel{
|
||||
{
|
||||
ID: 2,
|
||||
OrgID: 1,
|
||||
FolderID: 1,
|
||||
UID: result.Result.LibraryPanels[0].UID,
|
||||
Name: "Text - Library Panel2",
|
||||
Model: map[string]interface{}{
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"id": float64(1),
|
||||
"title": "Text - Library Panel2",
|
||||
"type": "text",
|
||||
},
|
||||
UpdatedBy: LibraryPanelDTOMetaUser{
|
||||
ID: 1,
|
||||
Name: UserInDbName,
|
||||
AvatarUrl: UserInDbAvatar,
|
||||
Version: 1,
|
||||
Meta: LibraryPanelDTOMeta{
|
||||
CanEdit: true,
|
||||
ConnectedDashboards: 0,
|
||||
Created: result.Result.LibraryPanels[0].Meta.Created,
|
||||
Updated: result.Result.LibraryPanels[0].Meta.Updated,
|
||||
CreatedBy: LibraryPanelDTOMetaUser{
|
||||
ID: 1,
|
||||
Name: UserInDbName,
|
||||
AvatarUrl: UserInDbAvatar,
|
||||
},
|
||||
UpdatedBy: LibraryPanelDTOMetaUser{
|
||||
ID: 1,
|
||||
Name: UserInDbName,
|
||||
AvatarUrl: UserInDbAvatar,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -104,6 +177,246 @@ func TestGetAllLibraryPanels(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and perPage is 1, it should succeed and the result should be correct",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
command := getCreateCommand(sc.folder.Id, "Text - Library Panel2")
|
||||
resp := sc.service.createHandler(sc.reqContext, command)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
|
||||
err := sc.reqContext.Req.ParseForm()
|
||||
require.NoError(t, err)
|
||||
sc.reqContext.Req.Form.Add("perPage", "1")
|
||||
resp = sc.service.getAllHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
|
||||
var result libraryPanelsSearch
|
||||
err = json.Unmarshal(resp.Body(), &result)
|
||||
require.NoError(t, err)
|
||||
var expected = libraryPanelsSearch{
|
||||
Result: libraryPanelsSearchResult{
|
||||
TotalCount: 2,
|
||||
Page: 1,
|
||||
PerPage: 1,
|
||||
LibraryPanels: []libraryPanel{
|
||||
{
|
||||
ID: 1,
|
||||
OrgID: 1,
|
||||
FolderID: 1,
|
||||
UID: result.Result.LibraryPanels[0].UID,
|
||||
Name: "Text - Library Panel",
|
||||
Model: map[string]interface{}{
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"id": float64(1),
|
||||
"title": "Text - Library Panel",
|
||||
"type": "text",
|
||||
},
|
||||
Version: 1,
|
||||
Meta: LibraryPanelDTOMeta{
|
||||
CanEdit: true,
|
||||
ConnectedDashboards: 0,
|
||||
Created: result.Result.LibraryPanels[0].Meta.Created,
|
||||
Updated: result.Result.LibraryPanels[0].Meta.Updated,
|
||||
CreatedBy: LibraryPanelDTOMetaUser{
|
||||
ID: 1,
|
||||
Name: UserInDbName,
|
||||
AvatarUrl: UserInDbAvatar,
|
||||
},
|
||||
UpdatedBy: LibraryPanelDTOMetaUser{
|
||||
ID: 1,
|
||||
Name: UserInDbName,
|
||||
AvatarUrl: UserInDbAvatar,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and perPage is 1 and page is 2, it should succeed and the result should be correct",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
command := getCreateCommand(sc.folder.Id, "Text - Library Panel2")
|
||||
resp := sc.service.createHandler(sc.reqContext, command)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
|
||||
err := sc.reqContext.Req.ParseForm()
|
||||
require.NoError(t, err)
|
||||
sc.reqContext.Req.Form.Add("perPage", "1")
|
||||
sc.reqContext.Req.Form.Add("page", "2")
|
||||
resp = sc.service.getAllHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
|
||||
var result libraryPanelsSearch
|
||||
err = json.Unmarshal(resp.Body(), &result)
|
||||
require.NoError(t, err)
|
||||
var expected = libraryPanelsSearch{
|
||||
Result: libraryPanelsSearchResult{
|
||||
TotalCount: 2,
|
||||
Page: 2,
|
||||
PerPage: 1,
|
||||
LibraryPanels: []libraryPanel{
|
||||
{
|
||||
ID: 2,
|
||||
OrgID: 1,
|
||||
FolderID: 1,
|
||||
UID: result.Result.LibraryPanels[0].UID,
|
||||
Name: "Text - Library Panel2",
|
||||
Model: map[string]interface{}{
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"id": float64(1),
|
||||
"title": "Text - Library Panel2",
|
||||
"type": "text",
|
||||
},
|
||||
Version: 1,
|
||||
Meta: LibraryPanelDTOMeta{
|
||||
CanEdit: true,
|
||||
ConnectedDashboards: 0,
|
||||
Created: result.Result.LibraryPanels[0].Meta.Created,
|
||||
Updated: result.Result.LibraryPanels[0].Meta.Updated,
|
||||
CreatedBy: LibraryPanelDTOMetaUser{
|
||||
ID: 1,
|
||||
Name: UserInDbName,
|
||||
AvatarUrl: UserInDbAvatar,
|
||||
},
|
||||
UpdatedBy: LibraryPanelDTOMetaUser{
|
||||
ID: 1,
|
||||
Name: UserInDbName,
|
||||
AvatarUrl: UserInDbAvatar,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and perPage is 1 and page is 1 and name is panel2, it should succeed and the result should be correct",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
command := getCreateCommand(sc.folder.Id, "Text - Library Panel2")
|
||||
resp := sc.service.createHandler(sc.reqContext, command)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
|
||||
err := sc.reqContext.Req.ParseForm()
|
||||
require.NoError(t, err)
|
||||
sc.reqContext.Req.Form.Add("perPage", "1")
|
||||
sc.reqContext.Req.Form.Add("page", "1")
|
||||
sc.reqContext.Req.Form.Add("name", "panel2")
|
||||
resp = sc.service.getAllHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
|
||||
var result libraryPanelsSearch
|
||||
err = json.Unmarshal(resp.Body(), &result)
|
||||
require.NoError(t, err)
|
||||
var expected = libraryPanelsSearch{
|
||||
Result: libraryPanelsSearchResult{
|
||||
TotalCount: 1,
|
||||
Page: 1,
|
||||
PerPage: 1,
|
||||
LibraryPanels: []libraryPanel{
|
||||
{
|
||||
ID: 2,
|
||||
OrgID: 1,
|
||||
FolderID: 1,
|
||||
UID: result.Result.LibraryPanels[0].UID,
|
||||
Name: "Text - Library Panel2",
|
||||
Model: map[string]interface{}{
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"id": float64(1),
|
||||
"title": "Text - Library Panel2",
|
||||
"type": "text",
|
||||
},
|
||||
Version: 1,
|
||||
Meta: LibraryPanelDTOMeta{
|
||||
CanEdit: true,
|
||||
ConnectedDashboards: 0,
|
||||
Created: result.Result.LibraryPanels[0].Meta.Created,
|
||||
Updated: result.Result.LibraryPanels[0].Meta.Updated,
|
||||
CreatedBy: LibraryPanelDTOMetaUser{
|
||||
ID: 1,
|
||||
Name: UserInDbName,
|
||||
AvatarUrl: UserInDbAvatar,
|
||||
},
|
||||
UpdatedBy: LibraryPanelDTOMetaUser{
|
||||
ID: 1,
|
||||
Name: UserInDbName,
|
||||
AvatarUrl: UserInDbAvatar,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and perPage is 1 and page is 3 and name is panel, it should succeed and the result should be correct",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
command := getCreateCommand(sc.folder.Id, "Text - Library Panel2")
|
||||
resp := sc.service.createHandler(sc.reqContext, command)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
|
||||
err := sc.reqContext.Req.ParseForm()
|
||||
require.NoError(t, err)
|
||||
sc.reqContext.Req.Form.Add("perPage", "1")
|
||||
sc.reqContext.Req.Form.Add("page", "3")
|
||||
sc.reqContext.Req.Form.Add("name", "panel")
|
||||
resp = sc.service.getAllHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
|
||||
var result libraryPanelsSearch
|
||||
err = json.Unmarshal(resp.Body(), &result)
|
||||
require.NoError(t, err)
|
||||
var expected = libraryPanelsSearch{
|
||||
Result: libraryPanelsSearchResult{
|
||||
TotalCount: 2,
|
||||
Page: 3,
|
||||
PerPage: 1,
|
||||
LibraryPanels: []libraryPanel{},
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and perPage is 1 and page is 3 and name does not exist, it should succeed and the result should be correct",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
command := getCreateCommand(sc.folder.Id, "Text - Library Panel2")
|
||||
resp := sc.service.createHandler(sc.reqContext, command)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
|
||||
err := sc.reqContext.Req.ParseForm()
|
||||
require.NoError(t, err)
|
||||
sc.reqContext.Req.Form.Add("perPage", "1")
|
||||
sc.reqContext.Req.Form.Add("page", "3")
|
||||
sc.reqContext.Req.Form.Add("name", "monkey")
|
||||
resp = sc.service.getAllHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
|
||||
var result libraryPanelsSearch
|
||||
err = json.Unmarshal(resp.Body(), &result)
|
||||
require.NoError(t, err)
|
||||
var expected = libraryPanelsSearch{
|
||||
Result: libraryPanelsSearchResult{
|
||||
TotalCount: 0,
|
||||
Page: 3,
|
||||
PerPage: 1,
|
||||
LibraryPanels: []libraryPanel{},
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist but only one is connected, it should succeed and return correct connected dashboards",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
command := getCreateCommand(sc.folder.Id, "Text - Library Panel2")
|
||||
@@ -121,11 +434,11 @@ func TestGetAllLibraryPanels(t *testing.T) {
|
||||
resp = sc.service.getAllHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
|
||||
var results libraryPanelsResult
|
||||
var results libraryPanelsSearch
|
||||
err := json.Unmarshal(resp.Body(), &results)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(0), results.Result[0].Meta.ConnectedDashboards)
|
||||
require.Equal(t, int64(2), results.Result[1].Meta.ConnectedDashboards)
|
||||
require.Equal(t, int64(0), results.Result.LibraryPanels[0].Meta.ConnectedDashboards)
|
||||
require.Equal(t, int64(2), results.Result.LibraryPanels[1].Meta.ConnectedDashboards)
|
||||
})
|
||||
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to get all library panels in a different org, none should be returned",
|
||||
@@ -133,22 +446,31 @@ func TestGetAllLibraryPanels(t *testing.T) {
|
||||
resp := sc.service.getAllHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
|
||||
var result libraryPanelsResult
|
||||
var result libraryPanelsSearch
|
||||
err := json.Unmarshal(resp.Body(), &result)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(result.Result))
|
||||
require.Equal(t, int64(1), result.Result[0].FolderID)
|
||||
require.Equal(t, "Text - Library Panel", result.Result[0].Name)
|
||||
require.Equal(t, 1, len(result.Result.LibraryPanels))
|
||||
require.Equal(t, int64(1), result.Result.LibraryPanels[0].FolderID)
|
||||
require.Equal(t, "Text - Library Panel", result.Result.LibraryPanels[0].Name)
|
||||
|
||||
sc.reqContext.SignedInUser.OrgId = 2
|
||||
sc.reqContext.SignedInUser.OrgRole = models.ROLE_ADMIN
|
||||
resp = sc.service.getAllHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
|
||||
result = libraryPanelsResult{}
|
||||
result = libraryPanelsSearch{}
|
||||
err = json.Unmarshal(resp.Body(), &result)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result.Result)
|
||||
require.Equal(t, 0, len(result.Result))
|
||||
var expected = libraryPanelsSearch{
|
||||
Result: libraryPanelsSearchResult{
|
||||
TotalCount: 0,
|
||||
LibraryPanels: []libraryPanel{},
|
||||
Page: 1,
|
||||
PerPage: 100,
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -343,10 +343,10 @@ func TestLibraryPanelPermissions(t *testing.T) {
|
||||
|
||||
resp := sc.service.getAllHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
var actual libraryPanelsResult
|
||||
var actual libraryPanelsSearch
|
||||
err := json.Unmarshal(resp.Body(), &actual)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, testCase.panels, len(actual.Result))
|
||||
require.Equal(t, testCase.panels, len(actual.Result.LibraryPanels))
|
||||
for _, folderIndex := range testCase.folderIndexes {
|
||||
var folderID = int64(folderIndex + 2) // testScenario creates one folder and general folder doesn't count
|
||||
var foundResult libraryPanel
|
||||
@@ -359,7 +359,7 @@ func TestLibraryPanelPermissions(t *testing.T) {
|
||||
}
|
||||
require.NotEmpty(t, foundResult)
|
||||
|
||||
for _, result := range actual.Result {
|
||||
for _, result := range actual.Result.LibraryPanels {
|
||||
if result.FolderID == folderID {
|
||||
actualResult = result
|
||||
break
|
||||
@@ -386,11 +386,11 @@ func TestLibraryPanelPermissions(t *testing.T) {
|
||||
|
||||
resp = sc.service.getAllHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
var actual libraryPanelsResult
|
||||
var actual libraryPanelsSearch
|
||||
err := json.Unmarshal(resp.Body(), &actual)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(actual.Result))
|
||||
if diff := cmp.Diff(result.Result, actual.Result[0], getCompareOptions()...); diff != "" {
|
||||
require.Equal(t, 1, len(actual.Result.LibraryPanels))
|
||||
if diff := cmp.Diff(result.Result, actual.Result.LibraryPanels[0], getCompareOptions()...); diff != "" {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -646,11 +646,11 @@ func TestDeleteLibraryPanelsInFolder(t *testing.T) {
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
resp := sc.service.getAllHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
var result libraryPanelsResult
|
||||
var result libraryPanelsSearch
|
||||
err := json.Unmarshal(resp.Body(), &result)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result.Result)
|
||||
require.Equal(t, 1, len(result.Result))
|
||||
require.Equal(t, 1, len(result.Result.LibraryPanels))
|
||||
|
||||
err = sc.service.DeleteLibraryPanelsInFolder(sc.reqContext, sc.folder.Uid)
|
||||
require.NoError(t, err)
|
||||
@@ -659,7 +659,7 @@ func TestDeleteLibraryPanelsInFolder(t *testing.T) {
|
||||
err = json.Unmarshal(resp.Body(), &result)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result.Result)
|
||||
require.Equal(t, 0, len(result.Result))
|
||||
require.Equal(t, 0, len(result.Result.LibraryPanels))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -678,8 +678,15 @@ type libraryPanelResult struct {
|
||||
Result libraryPanel `json:"result"`
|
||||
}
|
||||
|
||||
type libraryPanelsResult struct {
|
||||
Result []libraryPanel `json:"result"`
|
||||
type libraryPanelsSearch struct {
|
||||
Result libraryPanelsSearchResult `json:"result"`
|
||||
}
|
||||
|
||||
type libraryPanelsSearchResult struct {
|
||||
TotalCount int64 `json:"totalCount"`
|
||||
LibraryPanels []libraryPanel `json:"libraryPanels"`
|
||||
Page int `json:"page"`
|
||||
PerPage int `json:"perPage"`
|
||||
}
|
||||
|
||||
type libraryPanelDashboardsResult struct {
|
||||
|
||||
@@ -58,6 +58,14 @@ type LibraryPanelDTO struct {
|
||||
Meta LibraryPanelDTOMeta `json:"meta"`
|
||||
}
|
||||
|
||||
// LibraryPanelSearchResult is the search result for library panels.
|
||||
type LibraryPanelSearchResult struct {
|
||||
TotalCount int64 `json:"totalCount"`
|
||||
LibraryPanels []LibraryPanelDTO `json:"libraryPanels"`
|
||||
Page int `json:"page"`
|
||||
PerPage int `json:"perPage"`
|
||||
}
|
||||
|
||||
// LibraryPanelDTOMeta is the meta information for LibraryPanelDTO.
|
||||
type LibraryPanelDTOMeta struct {
|
||||
CanEdit bool `json:"canEdit"`
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Icon, IconButton, ConfirmModal, Tooltip, useStyles, Card } from '@grafana/ui';
|
||||
import { Card, ConfirmModal, Icon, IconButton, Tooltip, useStyles } from '@grafana/ui';
|
||||
import { css } from 'emotion';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { LibraryPanelDTO } from '../../types';
|
||||
@@ -7,7 +7,7 @@ import { LibraryPanelDTO } from '../../types';
|
||||
export interface LibraryPanelCardProps {
|
||||
libraryPanel: LibraryPanelDTO;
|
||||
onClick?: (panel: LibraryPanelDTO) => void;
|
||||
onDelete?: () => void;
|
||||
onDelete?: (panel: LibraryPanelDTO) => void;
|
||||
showSecondaryActions?: boolean;
|
||||
formatDate?: (dateString: string) => string;
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export const LibraryPanelCard: React.FC<LibraryPanelCardProps & { children?: JSX
|
||||
const [showDeletionModal, setShowDeletionModal] = useState(false);
|
||||
|
||||
const onDeletePanel = () => {
|
||||
onDelete?.();
|
||||
onDelete?.(libraryPanel);
|
||||
setShowDeletionModal(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { FormEvent, useMemo, useReducer } from 'react';
|
||||
import { useDebounce } from 'react-use';
|
||||
import { css, cx } from 'emotion';
|
||||
import { Button, Icon, Input, stylesFactory, useStyles } from '@grafana/ui';
|
||||
import { DateTimeInput, GrafanaTheme } from '@grafana/data';
|
||||
import { Button, Icon, Input, Pagination, stylesFactory, useStyles } from '@grafana/ui';
|
||||
import { DateTimeInput, GrafanaTheme, LoadingState } from '@grafana/data';
|
||||
|
||||
import { LibraryPanelCard } from '../LibraryPanelCard/LibraryPanelCard';
|
||||
import { deleteLibraryPanel, getLibraryPanels } from '../../state/api';
|
||||
import { LibraryPanelDTO } from '../../types';
|
||||
import { changePage, changeSearchString, initialLibraryPanelsViewState, libraryPanelsViewReducer } from './reducer';
|
||||
import { asyncDispatcher, deleteLibraryPanel, searchForLibraryPanels } from './actions';
|
||||
|
||||
interface LibraryPanelViewProps {
|
||||
className?: string;
|
||||
@@ -28,39 +29,24 @@ export const LibraryPanelsView: React.FC<LibraryPanelViewProps> = ({
|
||||
currentPanelId: currentPanel,
|
||||
}) => {
|
||||
const styles = useStyles(getPanelViewStyles);
|
||||
const [searchString, setSearchString] = useState('');
|
||||
|
||||
// Deliberately not using useAsync here as we want to be able to update libraryPanels without
|
||||
// making an additional API request (for example when a user deletes a library panel and we want to update the view to reflect that)
|
||||
const [libraryPanels, setLibraryPanels] = useState<LibraryPanelDTO[] | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
getLibraryPanels().then((panels) => {
|
||||
setLibraryPanels(panels);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const [filteredItems, setFilteredItems] = useState(libraryPanels);
|
||||
useDebounce(
|
||||
() => {
|
||||
setFilteredItems(
|
||||
libraryPanels?.filter(
|
||||
(v) => v.name.toLowerCase().includes(searchString.toLowerCase()) && v.uid !== currentPanel
|
||||
)
|
||||
);
|
||||
},
|
||||
300,
|
||||
[searchString, libraryPanels, currentPanel]
|
||||
);
|
||||
|
||||
const onDeletePanel = async (uid: string) => {
|
||||
try {
|
||||
await deleteLibraryPanel(uid);
|
||||
const panelIndex = libraryPanels!.findIndex((panel) => panel.uid === uid);
|
||||
setLibraryPanels([...libraryPanels!.slice(0, panelIndex), ...libraryPanels!.slice(panelIndex + 1)]);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
const [
|
||||
{ libraryPanels, searchString, page, perPage, numberOfPages, loadingState, currentPanelId },
|
||||
dispatch,
|
||||
] = useReducer(libraryPanelsViewReducer, {
|
||||
...initialLibraryPanelsViewState,
|
||||
currentPanelId: currentPanel,
|
||||
});
|
||||
const asyncDispatch = useMemo(() => asyncDispatcher(dispatch), [dispatch]);
|
||||
useDebounce(() => asyncDispatch(searchForLibraryPanels({ searchString, page, perPage, currentPanelId })), 300, [
|
||||
searchString,
|
||||
page,
|
||||
asyncDispatch,
|
||||
]);
|
||||
const onSearchChange = (event: FormEvent<HTMLInputElement>) =>
|
||||
asyncDispatch(changeSearchString({ searchString: event.currentTarget.value }));
|
||||
const onDelete = ({ uid }: LibraryPanelDTO) =>
|
||||
asyncDispatch(deleteLibraryPanel(uid, { searchString, page, perPage }));
|
||||
const onPageChange = (page: number) => asyncDispatch(changePage({ page }));
|
||||
|
||||
return (
|
||||
<div className={cx(styles.container, className)}>
|
||||
@@ -70,21 +56,21 @@ export const LibraryPanelsView: React.FC<LibraryPanelViewProps> = ({
|
||||
prefix={<Icon name="search" />}
|
||||
value={searchString}
|
||||
autoFocus
|
||||
onChange={(e) => setSearchString(e.currentTarget.value)}
|
||||
></Input>
|
||||
onChange={onSearchChange}
|
||||
/>
|
||||
{/* <Select placeholder="Filter by" onChange={() => {}} width={35} /> */}
|
||||
</div>
|
||||
<div className={styles.libraryPanelList}>
|
||||
{libraryPanels === undefined ? (
|
||||
{loadingState === LoadingState.Loading ? (
|
||||
<p>Loading library panels...</p>
|
||||
) : filteredItems?.length! < 1 ? (
|
||||
) : libraryPanels.length < 1 ? (
|
||||
<p>No library panels found.</p>
|
||||
) : (
|
||||
filteredItems?.map((item, i) => (
|
||||
libraryPanels?.map((item, i) => (
|
||||
<LibraryPanelCard
|
||||
key={`shared-panel=${i}`}
|
||||
libraryPanel={item}
|
||||
onDelete={() => onDeletePanel(item.uid)}
|
||||
onDelete={onDelete}
|
||||
onClick={onClickCard}
|
||||
formatDate={formatDate}
|
||||
showSecondaryActions={showSecondaryActions}
|
||||
@@ -94,6 +80,17 @@ export const LibraryPanelsView: React.FC<LibraryPanelViewProps> = ({
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
{libraryPanels.length ? (
|
||||
<div className={styles.pagination}>
|
||||
<Pagination
|
||||
currentPage={page}
|
||||
numberOfPages={numberOfPages}
|
||||
onNavigate={onPageChange}
|
||||
hideWhenSinglePage={true}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{onCreateNewPanel && (
|
||||
<Button icon="plus" className={styles.newPanelButton} onClick={onCreateNewPanel}>
|
||||
Create a new reusable panel
|
||||
@@ -111,6 +108,7 @@ const getPanelViewStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
flex-wrap: nowrap;
|
||||
gap: ${theme.spacing.sm};
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
`,
|
||||
libraryPanelList: css`
|
||||
display: flex;
|
||||
@@ -124,5 +122,8 @@ const getPanelViewStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
margin-top: 10px;
|
||||
align-self: flex-start;
|
||||
`,
|
||||
pagination: css`
|
||||
align-self: center;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Dispatch } from 'react';
|
||||
import { AnyAction } from '@reduxjs/toolkit';
|
||||
import { from, merge, of, Subscription, timer } from 'rxjs';
|
||||
import { catchError, finalize, mapTo, mergeMap, share, takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { deleteLibraryPanel as apiDeleteLibraryPanel, getLibraryPanels } from '../../state/api';
|
||||
import { initialLibraryPanelsViewState, initSearch, LibraryPanelsViewState, searchCompleted } from './reducer';
|
||||
|
||||
type DispatchResult = (dispatch: Dispatch<AnyAction>) => void;
|
||||
type SearchArgs = Pick<LibraryPanelsViewState, 'searchString' | 'perPage' | 'page' | 'currentPanelId'>;
|
||||
|
||||
export function searchForLibraryPanels(args: SearchArgs): DispatchResult {
|
||||
return function (dispatch) {
|
||||
const subscription = new Subscription();
|
||||
const dataObservable = from(
|
||||
getLibraryPanels({
|
||||
name: args.searchString,
|
||||
perPage: args.perPage,
|
||||
page: args.page,
|
||||
})
|
||||
).pipe(
|
||||
mergeMap(({ perPage, libraryPanels, page, totalCount }) =>
|
||||
of(searchCompleted({ libraryPanels, page, perPage, totalCount }))
|
||||
),
|
||||
catchError((err) => {
|
||||
console.error(err);
|
||||
return of(searchCompleted({ ...initialLibraryPanelsViewState, page: args.page, perPage: args.perPage }));
|
||||
}),
|
||||
finalize(() => subscription.unsubscribe()), // make sure we unsubscribe
|
||||
share()
|
||||
);
|
||||
|
||||
subscription.add(
|
||||
// If 50ms without a response dispatch a loading state
|
||||
// mapTo will translate the timer event into a loading state
|
||||
// takeUntil will cancel the timer emit when first response is received on the dataObservable
|
||||
merge(timer(50).pipe(mapTo(initSearch()), takeUntil(dataObservable)), dataObservable).subscribe(dispatch)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteLibraryPanel(uid: string, args: SearchArgs): DispatchResult {
|
||||
return async function (dispatch) {
|
||||
try {
|
||||
await apiDeleteLibraryPanel(uid);
|
||||
searchForLibraryPanels(args)(dispatch);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function asyncDispatcher(dispatch: Dispatch<AnyAction>) {
|
||||
return function (action: any) {
|
||||
if (action instanceof Function) {
|
||||
return action(dispatch);
|
||||
}
|
||||
return dispatch(action);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import { LoadingState } from '@grafana/data';
|
||||
|
||||
import { reducerTester } from '../../../../../test/core/redux/reducerTester';
|
||||
import {
|
||||
changePage,
|
||||
changeSearchString,
|
||||
initialLibraryPanelsViewState,
|
||||
initSearch,
|
||||
libraryPanelsViewReducer,
|
||||
LibraryPanelsViewState,
|
||||
searchCompleted,
|
||||
} from './reducer';
|
||||
import { LibraryPanelDTO } from '../../types';
|
||||
|
||||
describe('libraryPanelsViewReducer', () => {
|
||||
describe('when initSearch is dispatched', () => {
|
||||
it('then the state should be correct', () => {
|
||||
reducerTester<LibraryPanelsViewState>()
|
||||
.givenReducer(libraryPanelsViewReducer, { ...initialLibraryPanelsViewState })
|
||||
.whenActionIsDispatched(initSearch())
|
||||
.thenStateShouldEqual({
|
||||
...initialLibraryPanelsViewState,
|
||||
loadingState: LoadingState.Loading,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when searchCompleted is dispatched', () => {
|
||||
it('then the state should be correct', () => {
|
||||
const payload = {
|
||||
perPage: 10,
|
||||
page: 3,
|
||||
libraryPanels: getLibraryPanelMocks(2),
|
||||
totalCount: 200,
|
||||
};
|
||||
|
||||
reducerTester<LibraryPanelsViewState>()
|
||||
.givenReducer(libraryPanelsViewReducer, { ...initialLibraryPanelsViewState })
|
||||
.whenActionIsDispatched(searchCompleted(payload))
|
||||
.thenStateShouldEqual({
|
||||
...initialLibraryPanelsViewState,
|
||||
perPage: 10,
|
||||
page: 3,
|
||||
libraryPanels: payload.libraryPanels,
|
||||
totalCount: 200,
|
||||
loadingState: LoadingState.Done,
|
||||
numberOfPages: 20,
|
||||
});
|
||||
});
|
||||
|
||||
describe('and page is greater than the current number of pages', () => {
|
||||
it('then the state should be correct', () => {
|
||||
const payload = {
|
||||
perPage: 10,
|
||||
page: 21,
|
||||
libraryPanels: getLibraryPanelMocks(2),
|
||||
totalCount: 200,
|
||||
};
|
||||
|
||||
reducerTester<LibraryPanelsViewState>()
|
||||
.givenReducer(libraryPanelsViewReducer, { ...initialLibraryPanelsViewState })
|
||||
.whenActionIsDispatched(searchCompleted(payload))
|
||||
.thenStateShouldEqual({
|
||||
...initialLibraryPanelsViewState,
|
||||
perPage: 10,
|
||||
page: 20,
|
||||
libraryPanels: payload.libraryPanels,
|
||||
totalCount: 200,
|
||||
loadingState: LoadingState.Done,
|
||||
numberOfPages: 20,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when changeSearchString is dispatched', () => {
|
||||
it('then the state should be correct', () => {
|
||||
reducerTester<LibraryPanelsViewState>()
|
||||
.givenReducer(libraryPanelsViewReducer, { ...initialLibraryPanelsViewState })
|
||||
.whenActionIsDispatched(changeSearchString({ searchString: 'a search string' }))
|
||||
.thenStateShouldEqual({
|
||||
...initialLibraryPanelsViewState,
|
||||
searchString: 'a search string',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when changePage is dispatched', () => {
|
||||
it('then the state should be correct', () => {
|
||||
reducerTester<LibraryPanelsViewState>()
|
||||
.givenReducer(libraryPanelsViewReducer, { ...initialLibraryPanelsViewState })
|
||||
.whenActionIsDispatched(changePage({ page: 42 }))
|
||||
.thenStateShouldEqual({
|
||||
...initialLibraryPanelsViewState,
|
||||
page: 42,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getLibraryPanelMocks(count: number): LibraryPanelDTO[] {
|
||||
const mocks: LibraryPanelDTO[] = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
mocks.push(
|
||||
mockLibraryPanel({
|
||||
uid: i.toString(10),
|
||||
id: i,
|
||||
name: `Test Panel ${i}`,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return mocks;
|
||||
}
|
||||
|
||||
function mockLibraryPanel({
|
||||
uid = '1',
|
||||
id = 1,
|
||||
orgId = 1,
|
||||
folderId = 0,
|
||||
name = 'Test Panel',
|
||||
model = { type: 'text', title: 'Test Panel' },
|
||||
meta = {
|
||||
canEdit: true,
|
||||
connectedDashboards: 0,
|
||||
created: '2021-01-01T00:00:00',
|
||||
createdBy: { id: 1, name: 'User X', avatarUrl: '/avatar/abc' },
|
||||
updated: '2021-01-02T00:00:00',
|
||||
updatedBy: { id: 2, name: 'User Y', avatarUrl: '/avatar/xyz' },
|
||||
},
|
||||
version = 1,
|
||||
}: Partial<LibraryPanelDTO> = {}): LibraryPanelDTO {
|
||||
return {
|
||||
uid,
|
||||
id,
|
||||
orgId,
|
||||
folderId,
|
||||
name,
|
||||
model,
|
||||
version,
|
||||
meta,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { LoadingState } from '@grafana/data';
|
||||
|
||||
import { LibraryPanelDTO } from '../../types';
|
||||
|
||||
export interface LibraryPanelsViewState {
|
||||
loadingState: LoadingState;
|
||||
libraryPanels: LibraryPanelDTO[];
|
||||
searchString: string;
|
||||
totalCount: number;
|
||||
perPage: number;
|
||||
page: number;
|
||||
numberOfPages: number;
|
||||
currentPanelId?: string;
|
||||
}
|
||||
|
||||
export const initialLibraryPanelsViewState: LibraryPanelsViewState = {
|
||||
loadingState: LoadingState.NotStarted,
|
||||
libraryPanels: [],
|
||||
searchString: '',
|
||||
totalCount: 0,
|
||||
perPage: 10,
|
||||
page: 1,
|
||||
numberOfPages: 0,
|
||||
currentPanelId: undefined,
|
||||
};
|
||||
|
||||
const libraryPanelsViewSlice = createSlice({
|
||||
name: 'libraryPanels/view',
|
||||
initialState: initialLibraryPanelsViewState,
|
||||
reducers: {
|
||||
initSearch: (state) => {
|
||||
state.loadingState = LoadingState.Loading;
|
||||
},
|
||||
searchCompleted: (
|
||||
state,
|
||||
action: PayloadAction<
|
||||
Omit<LibraryPanelsViewState, 'currentPanelId' | 'searchString' | 'loadingState' | 'numberOfPages'>
|
||||
>
|
||||
) => {
|
||||
const { libraryPanels, page, perPage, totalCount } = action.payload;
|
||||
state.libraryPanels = libraryPanels;
|
||||
state.perPage = perPage;
|
||||
state.totalCount = totalCount;
|
||||
state.loadingState = LoadingState.Done;
|
||||
state.numberOfPages = Math.ceil(totalCount / perPage);
|
||||
state.page = page > state.numberOfPages ? page - 1 : page;
|
||||
},
|
||||
changeSearchString: (state, action: PayloadAction<Pick<LibraryPanelsViewState, 'searchString'>>) => {
|
||||
state.searchString = action.payload.searchString;
|
||||
},
|
||||
changePage: (state, action: PayloadAction<Pick<LibraryPanelsViewState, 'page'>>) => {
|
||||
state.page = action.payload.page;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const libraryPanelsViewReducer = libraryPanelsViewSlice.reducer;
|
||||
export const { initSearch, searchCompleted, changeSearchString, changePage } = libraryPanelsViewSlice.actions;
|
||||
@@ -1,8 +1,26 @@
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { LibraryPanelDTO, PanelModelWithLibraryPanel } from '../types';
|
||||
import { LibraryPanelDTO, LibraryPanelSearchResult, PanelModelWithLibraryPanel } from '../types';
|
||||
|
||||
export async function getLibraryPanels(): Promise<LibraryPanelDTO[]> {
|
||||
const { result } = await getBackendSrv().get(`/api/library-panels`);
|
||||
export interface GetLibraryPanelsOptions {
|
||||
name?: string;
|
||||
perPage?: number;
|
||||
page?: number;
|
||||
excludeUid?: string;
|
||||
}
|
||||
|
||||
export async function getLibraryPanels({
|
||||
name = '',
|
||||
perPage = 100,
|
||||
page = 1,
|
||||
excludeUid = '',
|
||||
}: GetLibraryPanelsOptions = {}): Promise<LibraryPanelSearchResult> {
|
||||
const params = new URLSearchParams();
|
||||
params.append('name', name);
|
||||
params.append('excludeUid', excludeUid);
|
||||
params.append('perPage', perPage.toString(10));
|
||||
params.append('page', page.toString(10));
|
||||
|
||||
const { result } = await getBackendSrv().get(`/api/library-panels?${params.toString()}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { PanelModel } from '../dashboard/state';
|
||||
|
||||
export interface LibraryPanelSearchResult {
|
||||
totalCount: number;
|
||||
libraryPanels: LibraryPanelDTO[];
|
||||
perPage: number;
|
||||
page: number;
|
||||
}
|
||||
|
||||
export interface LibraryPanelDTO {
|
||||
id: number;
|
||||
orgId: number;
|
||||
|
||||
Reference in New Issue
Block a user