From f508a16a43478975d777d157859deebfa47cd0a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Thu, 18 Mar 2021 11:19:41 +0100 Subject: [PATCH] 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 --- .../src/components/Pagination/Pagination.tsx | 8 +- pkg/services/librarypanels/api.go | 2 +- pkg/services/librarypanels/database.go | 110 +++-- .../librarypanels_get_all_test.go | 458 +++++++++++++++--- .../librarypanels_permissions_test.go | 12 +- .../librarypanels/librarypanels_test.go | 17 +- pkg/services/librarypanels/models.go | 8 + .../LibraryPanelCard/LibraryPanelCard.tsx | 6 +- .../LibraryPanelsView/LibraryPanelsView.tsx | 87 ++-- .../components/LibraryPanelsView/actions.ts | 60 +++ .../LibraryPanelsView/reducer.test.ts | 144 ++++++ .../components/LibraryPanelsView/reducer.ts | 59 +++ .../app/features/library-panels/state/api.ts | 24 +- public/app/features/library-panels/types.ts | 7 + 14 files changed, 838 insertions(+), 164 deletions(-) create mode 100644 public/app/features/library-panels/components/LibraryPanelsView/actions.ts create mode 100644 public/app/features/library-panels/components/LibraryPanelsView/reducer.test.ts create mode 100644 public/app/features/library-panels/components/LibraryPanelsView/reducer.ts diff --git a/packages/grafana-ui/src/components/Pagination/Pagination.tsx b/packages/grafana-ui/src/components/Pagination/Pagination.tsx index a40c49e669d..470f90d57af 100644 --- a/packages/grafana-ui/src/components/Pagination/Pagination.tsx +++ b/packages/grafana-ui/src/components/Pagination/Pagination.tsx @@ -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 = ({ currentPage, numberOfPages, onNavigate }) => { +export const Pagination: React.FC = ({ currentPage, numberOfPages, onNavigate, hideWhenSinglePage }) => { const styles = getStyles(); const pages = [...new Array(numberOfPages).keys()]; @@ -71,6 +73,10 @@ export const Pagination: React.FC = ({ currentPage, numberOfPages, onNavi return pagesToRender; }, []); + if (hideWhenSinglePage && numberOfPages <= 1) { + return null; + } + return (
    diff --git a/pkg/services/librarypanels/api.go b/pkg/services/librarypanels/api.go index e39e0589b24..8e07d9e318a 100644 --- a/pkg/services/librarypanels/api.go +++ b/pkg/services/librarypanels/api.go @@ -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") } diff --git a/pkg/services/librarypanels/database.go b/pkg/services/librarypanels/database.go index 7f0d6584e05..4814328a8c7 100644 --- a/pkg/services/librarypanels/database.go +++ b/pkg/services/librarypanels/database.go @@ -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. diff --git a/pkg/services/librarypanels/librarypanels_get_all_test.go b/pkg/services/librarypanels/librarypanels_get_all_test.go index f7f3117176b..d70e85d5f06 100644 --- a/pkg/services/librarypanels/librarypanels_get_all_test.go +++ b/pkg/services/librarypanels/librarypanels_get_all_test.go @@ -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) + } }) } diff --git a/pkg/services/librarypanels/librarypanels_permissions_test.go b/pkg/services/librarypanels/librarypanels_permissions_test.go index a8fdd1374ea..c6f344153cc 100644 --- a/pkg/services/librarypanels/librarypanels_permissions_test.go +++ b/pkg/services/librarypanels/librarypanels_permissions_test.go @@ -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) } }) diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index 861bbc3559a..a5f03d02061 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -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 { diff --git a/pkg/services/librarypanels/models.go b/pkg/services/librarypanels/models.go index aba12b3acf4..cbd97b78c88 100644 --- a/pkg/services/librarypanels/models.go +++ b/pkg/services/librarypanels/models.go @@ -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"` diff --git a/public/app/features/library-panels/components/LibraryPanelCard/LibraryPanelCard.tsx b/public/app/features/library-panels/components/LibraryPanelCard/LibraryPanelCard.tsx index 00f08273d8e..f93940a2db9 100644 --- a/public/app/features/library-panels/components/LibraryPanelCard/LibraryPanelCard.tsx +++ b/public/app/features/library-panels/components/LibraryPanelCard/LibraryPanelCard.tsx @@ -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 { - onDelete?.(); + onDelete?.(libraryPanel); setShowDeletionModal(false); }; diff --git a/public/app/features/library-panels/components/LibraryPanelsView/LibraryPanelsView.tsx b/public/app/features/library-panels/components/LibraryPanelsView/LibraryPanelsView.tsx index 8ffb08393ec..2d66230565f 100644 --- a/public/app/features/library-panels/components/LibraryPanelsView/LibraryPanelsView.tsx +++ b/public/app/features/library-panels/components/LibraryPanelsView/LibraryPanelsView.tsx @@ -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 = ({ 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(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) => + asyncDispatch(changeSearchString({ searchString: event.currentTarget.value })); + const onDelete = ({ uid }: LibraryPanelDTO) => + asyncDispatch(deleteLibraryPanel(uid, { searchString, page, perPage })); + const onPageChange = (page: number) => asyncDispatch(changePage({ page })); return (
    @@ -70,21 +56,21 @@ export const LibraryPanelsView: React.FC = ({ prefix={} value={searchString} autoFocus - onChange={(e) => setSearchString(e.currentTarget.value)} - > + onChange={onSearchChange} + /> {/*