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:
Hugo Häggmark
2021-03-18 11:19:41 +01:00
committed by GitHub
parent 0fbe7f7f52
commit f508a16a43
14 changed files with 838 additions and 164 deletions

View File

@@ -13,9 +13,11 @@ interface Props {
numberOfPages: number; numberOfPages: number;
/** Callback function for fetching the selected page */ /** Callback function for fetching the selected page */
onNavigate: (toPage: number) => void; 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 styles = getStyles();
const pages = [...new Array(numberOfPages).keys()]; const pages = [...new Array(numberOfPages).keys()];
@@ -71,6 +73,10 @@ export const Pagination: React.FC<Props> = ({ currentPage, numberOfPages, onNavi
return pagesToRender; return pagesToRender;
}, []); }, []);
if (hideWhenSinglePage && numberOfPages <= 1) {
return null;
}
return ( return (
<div className={styles.container}> <div className={styles.container}>
<ol> <ol>

View File

@@ -81,7 +81,7 @@ func (lps *LibraryPanelService) getHandler(c *models.ReqContext) response.Respon
// getAllHandler handles GET /api/library-panels/. // getAllHandler handles GET /api/library-panels/.
func (lps *LibraryPanelService) getAllHandler(c *models.ReqContext) response.Response { 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 { if err != nil {
return toLibraryPanelError(err, "Failed to get library panels") return toLibraryPanelError(err, "Failed to get library panels")
} }

View File

@@ -3,6 +3,7 @@ package librarypanels
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"strings"
"time" "time"
"github.com/grafana/grafana/pkg/api/dtos" "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. // 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) 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 { err := lps.SQLStore.WithDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
builder := sqlstore.SQLBuilder{} builder := sqlstore.SQLBuilder{}
builder.Write(sqlStatmentLibrayPanelDTOWithMeta) builder.Write(sqlStatmentLibrayPanelDTOWithMeta)
builder.Write(` WHERE lp.org_id=? AND lp.folder_id=0`, c.SignedInUser.OrgId) 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(" UNION ")
builder.Write(sqlStatmentLibrayPanelDTOWithMeta) builder.Write(sqlStatmentLibrayPanelDTOWithMeta)
builder.Write(" INNER JOIN dashboard AS dashboard on lp.folder_id = dashboard.id AND lp.folder_id<>0") 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) 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 { if c.SignedInUser.OrgRole != models.ROLE_ADMIN {
builder.WriteDashboardPermissionFilter(c.SignedInUser, models.PERMISSION_VIEW) builder.WriteDashboardPermissionFilter(c.SignedInUser, models.PERMISSION_VIEW)
} }
if limit == 0 { if perPage != 0 {
limit = 1000 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 { if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&libraryPanels); err != nil {
return err 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 return nil
}) })
retDTOs := make([]LibraryPanelDTO, 0) return result, err
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
} }
// getConnectedDashboards gets all dashboards connected to a Library Panel. // getConnectedDashboards gets all dashboards connected to a Library Panel.

View File

@@ -16,11 +16,20 @@ func TestGetAllLibraryPanels(t *testing.T) {
resp := sc.service.getAllHandler(sc.reqContext) resp := sc.service.getAllHandler(sc.reqContext)
require.Equal(t, 200, resp.Status()) require.Equal(t, 200, resp.Status())
var result libraryPanelsResult var result libraryPanelsSearch
err := json.Unmarshal(resp.Body(), &result) err := json.Unmarshal(resp.Body(), &result)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, result.Result) var expected = libraryPanelsSearch{
require.Equal(t, 0, len(result.Result)) 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", 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) resp = sc.service.getAllHandler(sc.reqContext)
require.Equal(t, 200, resp.Status()) require.Equal(t, 200, resp.Status())
var result libraryPanelsResult var result libraryPanelsSearch
err := json.Unmarshal(resp.Body(), &result) err := json.Unmarshal(resp.Body(), &result)
require.NoError(t, err) require.NoError(t, err)
var expected = libraryPanelsResult{ var expected = libraryPanelsSearch{
Result: []libraryPanel{ Result: libraryPanelsSearchResult{
{ TotalCount: 2,
ID: 1, Page: 1,
OrgID: 1, PerPage: 100,
FolderID: 1, LibraryPanels: []libraryPanel{
UID: result.Result[0].UID, {
Name: "Text - Library Panel", ID: 1,
Model: map[string]interface{}{ OrgID: 1,
"datasource": "${DS_GDEV-TESTDATA}", FolderID: 1,
"id": float64(1), UID: result.Result.LibraryPanels[0].UID,
"title": "Text - Library Panel", Name: "Text - Library Panel",
"type": "text", Model: map[string]interface{}{
}, "datasource": "${DS_GDEV-TESTDATA}",
Version: 1, "id": float64(1),
Meta: LibraryPanelDTOMeta{ "title": "Text - Library Panel",
CanEdit: true, "type": "text",
ConnectedDashboards: 0,
Created: result.Result[0].Meta.Created,
Updated: result.Result[0].Meta.Updated,
CreatedBy: LibraryPanelDTOMetaUser{
ID: 1,
Name: UserInDbName,
AvatarUrl: UserInDbAvatar,
}, },
UpdatedBy: LibraryPanelDTOMetaUser{ Version: 1,
ID: 1, Meta: LibraryPanelDTOMeta{
Name: UserInDbName, CanEdit: true,
AvatarUrl: UserInDbAvatar, 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, if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" {
FolderID: 1, t.Fatalf("Result mismatch (-want +got):\n%s", diff)
UID: result.Result[1].UID, }
Name: "Text - Library Panel2", })
Model: map[string]interface{}{
"datasource": "${DS_GDEV-TESTDATA}", 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",
"id": float64(1), func(t *testing.T, sc scenarioContext) {
"title": "Text - Library Panel2", command := getCreateCommand(sc.folder.Id, "Text - Library Panel2")
"type": "text", resp := sc.service.createHandler(sc.reqContext, command)
}, require.Equal(t, 200, resp.Status())
Version: 1,
Meta: LibraryPanelDTOMeta{ err := sc.reqContext.Req.ParseForm()
CanEdit: true, require.NoError(t, err)
ConnectedDashboards: 0, sc.reqContext.Req.Form.Add("excludeUid", sc.initialResult.Result.UID)
Created: result.Result[1].Meta.Created, resp = sc.service.getAllHandler(sc.reqContext)
Updated: result.Result[1].Meta.Updated, require.Equal(t, 200, resp.Status())
CreatedBy: LibraryPanelDTOMetaUser{
ID: 1, var result libraryPanelsSearch
Name: UserInDbName, err = json.Unmarshal(resp.Body(), &result)
AvatarUrl: UserInDbAvatar, 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{ Version: 1,
ID: 1, Meta: LibraryPanelDTOMeta{
Name: UserInDbName, CanEdit: true,
AvatarUrl: UserInDbAvatar, 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", 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) { func(t *testing.T, sc scenarioContext) {
command := getCreateCommand(sc.folder.Id, "Text - Library Panel2") command := getCreateCommand(sc.folder.Id, "Text - Library Panel2")
@@ -121,11 +434,11 @@ func TestGetAllLibraryPanels(t *testing.T) {
resp = sc.service.getAllHandler(sc.reqContext) resp = sc.service.getAllHandler(sc.reqContext)
require.Equal(t, 200, resp.Status()) require.Equal(t, 200, resp.Status())
var results libraryPanelsResult var results libraryPanelsSearch
err := json.Unmarshal(resp.Body(), &results) err := json.Unmarshal(resp.Body(), &results)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, int64(0), results.Result[0].Meta.ConnectedDashboards) require.Equal(t, int64(0), results.Result.LibraryPanels[0].Meta.ConnectedDashboards)
require.Equal(t, int64(2), results.Result[1].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", 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) resp := sc.service.getAllHandler(sc.reqContext)
require.Equal(t, 200, resp.Status()) require.Equal(t, 200, resp.Status())
var result libraryPanelsResult var result libraryPanelsSearch
err := json.Unmarshal(resp.Body(), &result) err := json.Unmarshal(resp.Body(), &result)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 1, len(result.Result)) require.Equal(t, 1, len(result.Result.LibraryPanels))
require.Equal(t, int64(1), result.Result[0].FolderID) require.Equal(t, int64(1), result.Result.LibraryPanels[0].FolderID)
require.Equal(t, "Text - Library Panel", result.Result[0].Name) require.Equal(t, "Text - Library Panel", result.Result.LibraryPanels[0].Name)
sc.reqContext.SignedInUser.OrgId = 2 sc.reqContext.SignedInUser.OrgId = 2
sc.reqContext.SignedInUser.OrgRole = models.ROLE_ADMIN sc.reqContext.SignedInUser.OrgRole = models.ROLE_ADMIN
resp = sc.service.getAllHandler(sc.reqContext) resp = sc.service.getAllHandler(sc.reqContext)
require.Equal(t, 200, resp.Status()) require.Equal(t, 200, resp.Status())
result = libraryPanelsResult{} result = libraryPanelsSearch{}
err = json.Unmarshal(resp.Body(), &result) err = json.Unmarshal(resp.Body(), &result)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, result.Result) var expected = libraryPanelsSearch{
require.Equal(t, 0, len(result.Result)) 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)
}
}) })
} }

View File

@@ -343,10 +343,10 @@ func TestLibraryPanelPermissions(t *testing.T) {
resp := sc.service.getAllHandler(sc.reqContext) resp := sc.service.getAllHandler(sc.reqContext)
require.Equal(t, 200, resp.Status()) require.Equal(t, 200, resp.Status())
var actual libraryPanelsResult var actual libraryPanelsSearch
err := json.Unmarshal(resp.Body(), &actual) err := json.Unmarshal(resp.Body(), &actual)
require.NoError(t, err) 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 { for _, folderIndex := range testCase.folderIndexes {
var folderID = int64(folderIndex + 2) // testScenario creates one folder and general folder doesn't count var folderID = int64(folderIndex + 2) // testScenario creates one folder and general folder doesn't count
var foundResult libraryPanel var foundResult libraryPanel
@@ -359,7 +359,7 @@ func TestLibraryPanelPermissions(t *testing.T) {
} }
require.NotEmpty(t, foundResult) require.NotEmpty(t, foundResult)
for _, result := range actual.Result { for _, result := range actual.Result.LibraryPanels {
if result.FolderID == folderID { if result.FolderID == folderID {
actualResult = result actualResult = result
break break
@@ -386,11 +386,11 @@ func TestLibraryPanelPermissions(t *testing.T) {
resp = sc.service.getAllHandler(sc.reqContext) resp = sc.service.getAllHandler(sc.reqContext)
require.Equal(t, 200, resp.Status()) require.Equal(t, 200, resp.Status())
var actual libraryPanelsResult var actual libraryPanelsSearch
err := json.Unmarshal(resp.Body(), &actual) err := json.Unmarshal(resp.Body(), &actual)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 1, len(actual.Result)) require.Equal(t, 1, len(actual.Result.LibraryPanels))
if diff := cmp.Diff(result.Result, actual.Result[0], getCompareOptions()...); diff != "" { if diff := cmp.Diff(result.Result, actual.Result.LibraryPanels[0], getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff) t.Fatalf("Result mismatch (-want +got):\n%s", diff)
} }
}) })

View File

@@ -646,11 +646,11 @@ func TestDeleteLibraryPanelsInFolder(t *testing.T) {
func(t *testing.T, sc scenarioContext) { func(t *testing.T, sc scenarioContext) {
resp := sc.service.getAllHandler(sc.reqContext) resp := sc.service.getAllHandler(sc.reqContext)
require.Equal(t, 200, resp.Status()) require.Equal(t, 200, resp.Status())
var result libraryPanelsResult var result libraryPanelsSearch
err := json.Unmarshal(resp.Body(), &result) err := json.Unmarshal(resp.Body(), &result)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, result.Result) 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) err = sc.service.DeleteLibraryPanelsInFolder(sc.reqContext, sc.folder.Uid)
require.NoError(t, err) require.NoError(t, err)
@@ -659,7 +659,7 @@ func TestDeleteLibraryPanelsInFolder(t *testing.T) {
err = json.Unmarshal(resp.Body(), &result) err = json.Unmarshal(resp.Body(), &result)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, result.Result) 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"` Result libraryPanel `json:"result"`
} }
type libraryPanelsResult struct { type libraryPanelsSearch struct {
Result []libraryPanel `json:"result"` 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 { type libraryPanelDashboardsResult struct {

View File

@@ -58,6 +58,14 @@ type LibraryPanelDTO struct {
Meta LibraryPanelDTOMeta `json:"meta"` 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. // LibraryPanelDTOMeta is the meta information for LibraryPanelDTO.
type LibraryPanelDTOMeta struct { type LibraryPanelDTOMeta struct {
CanEdit bool `json:"canEdit"` CanEdit bool `json:"canEdit"`

View File

@@ -1,5 +1,5 @@
import React, { useState } from 'react'; 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 { css } from 'emotion';
import { GrafanaTheme } from '@grafana/data'; import { GrafanaTheme } from '@grafana/data';
import { LibraryPanelDTO } from '../../types'; import { LibraryPanelDTO } from '../../types';
@@ -7,7 +7,7 @@ import { LibraryPanelDTO } from '../../types';
export interface LibraryPanelCardProps { export interface LibraryPanelCardProps {
libraryPanel: LibraryPanelDTO; libraryPanel: LibraryPanelDTO;
onClick?: (panel: LibraryPanelDTO) => void; onClick?: (panel: LibraryPanelDTO) => void;
onDelete?: () => void; onDelete?: (panel: LibraryPanelDTO) => void;
showSecondaryActions?: boolean; showSecondaryActions?: boolean;
formatDate?: (dateString: string) => string; formatDate?: (dateString: string) => string;
} }
@@ -24,7 +24,7 @@ export const LibraryPanelCard: React.FC<LibraryPanelCardProps & { children?: JSX
const [showDeletionModal, setShowDeletionModal] = useState(false); const [showDeletionModal, setShowDeletionModal] = useState(false);
const onDeletePanel = () => { const onDeletePanel = () => {
onDelete?.(); onDelete?.(libraryPanel);
setShowDeletionModal(false); setShowDeletionModal(false);
}; };

View File

@@ -1,12 +1,13 @@
import React, { useEffect, useState } from 'react'; import React, { FormEvent, useMemo, useReducer } from 'react';
import { useDebounce } from 'react-use'; import { useDebounce } from 'react-use';
import { css, cx } from 'emotion'; import { css, cx } from 'emotion';
import { Button, Icon, Input, stylesFactory, useStyles } from '@grafana/ui'; import { Button, Icon, Input, Pagination, stylesFactory, useStyles } from '@grafana/ui';
import { DateTimeInput, GrafanaTheme } from '@grafana/data'; import { DateTimeInput, GrafanaTheme, LoadingState } from '@grafana/data';
import { LibraryPanelCard } from '../LibraryPanelCard/LibraryPanelCard'; import { LibraryPanelCard } from '../LibraryPanelCard/LibraryPanelCard';
import { deleteLibraryPanel, getLibraryPanels } from '../../state/api';
import { LibraryPanelDTO } from '../../types'; import { LibraryPanelDTO } from '../../types';
import { changePage, changeSearchString, initialLibraryPanelsViewState, libraryPanelsViewReducer } from './reducer';
import { asyncDispatcher, deleteLibraryPanel, searchForLibraryPanels } from './actions';
interface LibraryPanelViewProps { interface LibraryPanelViewProps {
className?: string; className?: string;
@@ -28,39 +29,24 @@ export const LibraryPanelsView: React.FC<LibraryPanelViewProps> = ({
currentPanelId: currentPanel, currentPanelId: currentPanel,
}) => { }) => {
const styles = useStyles(getPanelViewStyles); const styles = useStyles(getPanelViewStyles);
const [searchString, setSearchString] = useState(''); const [
{ libraryPanels, searchString, page, perPage, numberOfPages, loadingState, currentPanelId },
// Deliberately not using useAsync here as we want to be able to update libraryPanels without dispatch,
// making an additional API request (for example when a user deletes a library panel and we want to update the view to reflect that) ] = useReducer(libraryPanelsViewReducer, {
const [libraryPanels, setLibraryPanels] = useState<LibraryPanelDTO[] | undefined>(undefined); ...initialLibraryPanelsViewState,
useEffect(() => { currentPanelId: currentPanel,
getLibraryPanels().then((panels) => { });
setLibraryPanels(panels); const asyncDispatch = useMemo(() => asyncDispatcher(dispatch), [dispatch]);
}); useDebounce(() => asyncDispatch(searchForLibraryPanels({ searchString, page, perPage, currentPanelId })), 300, [
}, []); searchString,
page,
const [filteredItems, setFilteredItems] = useState(libraryPanels); asyncDispatch,
useDebounce( ]);
() => { const onSearchChange = (event: FormEvent<HTMLInputElement>) =>
setFilteredItems( asyncDispatch(changeSearchString({ searchString: event.currentTarget.value }));
libraryPanels?.filter( const onDelete = ({ uid }: LibraryPanelDTO) =>
(v) => v.name.toLowerCase().includes(searchString.toLowerCase()) && v.uid !== currentPanel asyncDispatch(deleteLibraryPanel(uid, { searchString, page, perPage }));
) const onPageChange = (page: number) => asyncDispatch(changePage({ page }));
);
},
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;
}
};
return ( return (
<div className={cx(styles.container, className)}> <div className={cx(styles.container, className)}>
@@ -70,21 +56,21 @@ export const LibraryPanelsView: React.FC<LibraryPanelViewProps> = ({
prefix={<Icon name="search" />} prefix={<Icon name="search" />}
value={searchString} value={searchString}
autoFocus autoFocus
onChange={(e) => setSearchString(e.currentTarget.value)} onChange={onSearchChange}
></Input> />
{/* <Select placeholder="Filter by" onChange={() => {}} width={35} /> */} {/* <Select placeholder="Filter by" onChange={() => {}} width={35} /> */}
</div> </div>
<div className={styles.libraryPanelList}> <div className={styles.libraryPanelList}>
{libraryPanels === undefined ? ( {loadingState === LoadingState.Loading ? (
<p>Loading library panels...</p> <p>Loading library panels...</p>
) : filteredItems?.length! < 1 ? ( ) : libraryPanels.length < 1 ? (
<p>No library panels found.</p> <p>No library panels found.</p>
) : ( ) : (
filteredItems?.map((item, i) => ( libraryPanels?.map((item, i) => (
<LibraryPanelCard <LibraryPanelCard
key={`shared-panel=${i}`} key={`shared-panel=${i}`}
libraryPanel={item} libraryPanel={item}
onDelete={() => onDeletePanel(item.uid)} onDelete={onDelete}
onClick={onClickCard} onClick={onClickCard}
formatDate={formatDate} formatDate={formatDate}
showSecondaryActions={showSecondaryActions} showSecondaryActions={showSecondaryActions}
@@ -94,6 +80,17 @@ export const LibraryPanelsView: React.FC<LibraryPanelViewProps> = ({
)) ))
)} )}
</div> </div>
{libraryPanels.length ? (
<div className={styles.pagination}>
<Pagination
currentPage={page}
numberOfPages={numberOfPages}
onNavigate={onPageChange}
hideWhenSinglePage={true}
/>
</div>
) : null}
{onCreateNewPanel && ( {onCreateNewPanel && (
<Button icon="plus" className={styles.newPanelButton} onClick={onCreateNewPanel}> <Button icon="plus" className={styles.newPanelButton} onClick={onCreateNewPanel}>
Create a new reusable panel Create a new reusable panel
@@ -111,6 +108,7 @@ const getPanelViewStyles = stylesFactory((theme: GrafanaTheme) => {
flex-wrap: nowrap; flex-wrap: nowrap;
gap: ${theme.spacing.sm}; gap: ${theme.spacing.sm};
height: 100%; height: 100%;
overflow-y: auto;
`, `,
libraryPanelList: css` libraryPanelList: css`
display: flex; display: flex;
@@ -124,5 +122,8 @@ const getPanelViewStyles = stylesFactory((theme: GrafanaTheme) => {
margin-top: 10px; margin-top: 10px;
align-self: flex-start; align-self: flex-start;
`, `,
pagination: css`
align-self: center;
`,
}; };
}); });

View File

@@ -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);
};
}

View File

@@ -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,
};
}

View File

@@ -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;

View File

@@ -1,8 +1,26 @@
import { getBackendSrv } from '@grafana/runtime'; import { getBackendSrv } from '@grafana/runtime';
import { LibraryPanelDTO, PanelModelWithLibraryPanel } from '../types'; import { LibraryPanelDTO, LibraryPanelSearchResult, PanelModelWithLibraryPanel } from '../types';
export async function getLibraryPanels(): Promise<LibraryPanelDTO[]> { export interface GetLibraryPanelsOptions {
const { result } = await getBackendSrv().get(`/api/library-panels`); 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; return result;
} }

View File

@@ -1,5 +1,12 @@
import { PanelModel } from '../dashboard/state'; import { PanelModel } from '../dashboard/state';
export interface LibraryPanelSearchResult {
totalCount: number;
libraryPanels: LibraryPanelDTO[];
perPage: number;
page: number;
}
export interface LibraryPanelDTO { export interface LibraryPanelDTO {
id: number; id: number;
orgId: number; orgId: number;