From fc2ce281d0e597203dccc928f01ee4d4ef9847c4 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 3 Nov 2020 11:54:23 -0500 Subject: [PATCH] Paginate mobile apps --- cmd/server/assets/apikeys/index.html | 1 + cmd/server/assets/mobileapps/edit.html | 2 +- cmd/server/assets/mobileapps/index.html | 124 ++++++++++++----------- cmd/server/assets/mobileapps/new.html | 4 +- cmd/server/assets/mobileapps/show.html | 2 +- pkg/controller/mobileapps/create.go | 1 + pkg/controller/mobileapps/create_test.go | 96 ++++++++++++++++++ pkg/controller/mobileapps/index.go | 16 ++- pkg/controller/mobileapps/show.go | 2 + pkg/controller/mobileapps/update.go | 2 + pkg/database/realm.go | 27 +++-- 11 files changed, 199 insertions(+), 78 deletions(-) create mode 100644 pkg/controller/mobileapps/create_test.go diff --git a/cmd/server/assets/apikeys/index.html b/cmd/server/assets/apikeys/index.html index 7d325ce4f..c43e51308 100644 --- a/cmd/server/assets/apikeys/index.html +++ b/cmd/server/assets/apikeys/index.html @@ -16,6 +16,7 @@
+ API keys diff --git a/cmd/server/assets/mobileapps/edit.html b/cmd/server/assets/mobileapps/edit.html index 091925573..6555e650b 100644 --- a/cmd/server/assets/mobileapps/edit.html +++ b/cmd/server/assets/mobileapps/edit.html @@ -8,7 +8,7 @@ {{template "head" .}} - + {{template "navbar" .}}
diff --git a/cmd/server/assets/mobileapps/index.html b/cmd/server/assets/mobileapps/index.html index 63f8619f6..c6bf614ea 100644 --- a/cmd/server/assets/mobileapps/index.html +++ b/cmd/server/assets/mobileapps/index.html @@ -8,74 +8,76 @@ {{template "head" .}} - + {{template "navbar" .}}
{{template "flash" .}} -

Mobile apps

-

- These are the mobile apps for this realm. You - can also create a new mobile app. -

+
+
+ + Mobile apps + + + +
- {{if .apps}} -
- - - - - - - - - - {{range .apps}} - - - - - - - - - - {{end}} - -
AppEnabled
- {{.Name}} - - {{if .DeletedAt}} - Deleted - {{else}} - Enabled - {{end}} - - {{if .DeletedAt}} - - - - {{else}} - - - - {{end}} -
+ {{if $apps}} + + + + + + + + + + {{range $apps}} + + + + + + {{end}} + +
Mobile app
+ {{if .DeletedAt}} + + {{else}} + + {{end}} + + {{.Name}} + + {{if .DeletedAt}} + + + + {{else}} + + + + {{end}} +
+ {{else}} +

+ There are no mobile apps. +

+ {{end}}
- {{else}} -

- There are no mobile apps. -

- {{end}} + + {{template "shared/pagination" .}}
diff --git a/cmd/server/assets/mobileapps/new.html b/cmd/server/assets/mobileapps/new.html index dcaa3ea17..954607f72 100644 --- a/cmd/server/assets/mobileapps/new.html +++ b/cmd/server/assets/mobileapps/new.html @@ -8,7 +8,7 @@ {{template "head" .}} - + {{template "navbar" .}}
@@ -27,7 +27,7 @@

New mobile app

{{template "mobileapps/_app" .}} - +
diff --git a/cmd/server/assets/mobileapps/show.html b/cmd/server/assets/mobileapps/show.html index 676d86f91..f590b554f 100644 --- a/cmd/server/assets/mobileapps/show.html +++ b/cmd/server/assets/mobileapps/show.html @@ -9,7 +9,7 @@ {{template "head" .}} - + {{template "navbar" .}}
diff --git a/pkg/controller/mobileapps/create.go b/pkg/controller/mobileapps/create.go index 4a4b8e050..bc51e245e 100644 --- a/pkg/controller/mobileapps/create.go +++ b/pkg/controller/mobileapps/create.go @@ -100,6 +100,7 @@ func (c *Controller) HandleCreate() http.Handler { // renderNew renders the new page. func (c *Controller) renderNew(ctx context.Context, w http.ResponseWriter, app *database.MobileApp) { m := templateMap(ctx) + m["title"] = fmt.Sprintf("New mobile app - %s", m["title"]) m["app"] = app c.h.RenderHTML(w, "mobileapps/new", m) } diff --git a/pkg/controller/mobileapps/create_test.go b/pkg/controller/mobileapps/create_test.go new file mode 100644 index 000000000..9c301dbeb --- /dev/null +++ b/pkg/controller/mobileapps/create_test.go @@ -0,0 +1,96 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mobileapps_test + +import ( + "context" + "strconv" + "testing" + "time" + + "github.com/google/exposure-notifications-verification-server/internal/browser" + "github.com/google/exposure-notifications-verification-server/internal/envstest" + "github.com/google/exposure-notifications-verification-server/pkg/controller" + "github.com/google/exposure-notifications-verification-server/pkg/database" + + "github.com/chromedp/chromedp" +) + +func TestHandleCreate(t *testing.T) { + t.Parallel() + + harness := envstest.NewServer(t) + + // Get the default realm + realm, err := harness.Database.FindRealm(1) + if err != nil { + t.Fatal(err) + } + + // Create a user + admin := &database.User{ + Email: "admin@example.com", + Name: "Admin", + Realms: []*database.Realm{realm}, + AdminRealms: []*database.Realm{realm}, + } + if err := harness.Database.SaveUser(admin, database.System); err != nil { + t.Fatal(err) + } + + // Log in the user. + session, err := harness.LoggedInSession(nil, admin.Email) + if err != nil { + t.Fatal(err) + } + + // Set the current realm. + controller.StoreSessionRealm(session, realm) + + // Mint a cookie for the session. + cookie, err := harness.SessionCookie(session) + if err != nil { + t.Fatal(err) + } + // Create a browser runner. + browserCtx := browser.New(t) + taskCtx, done := context.WithTimeout(browserCtx, 30*time.Second) + defer done() + + if err := chromedp.Run(taskCtx, + // Pre-authenticate the user. + browser.SetCookie(cookie), + + // Visit /apikeys/new. + chromedp.Navigate(`http://`+harness.Server.Addr()+`/mobile-apps/new`), + + // Wait for render. + chromedp.WaitVisible(`body#mobileapps-new`, chromedp.ByQuery), + + // Fill out the form. + chromedp.SetValue(`input#name`, "Example mobile app", chromedp.ByQuery), + chromedp.SetValue(`input#url`, "https://example.com", chromedp.ByQuery), + chromedp.SetValue(`select#os`, strconv.Itoa(int(database.OSTypeIOS)), chromedp.ByQuery), + chromedp.SetValue(`input#app-id`, "com.example.app", chromedp.ByQuery), + + // Click the submit button. + chromedp.Click(`#submit`, chromedp.ByQuery), + + // Wait for the page to reload. + chromedp.WaitVisible(`body#mobileapps-show`, chromedp.ByQuery), + ); err != nil { + t.Fatal(err) + } +} diff --git a/pkg/controller/mobileapps/index.go b/pkg/controller/mobileapps/index.go index b7051009e..0c01cf5ea 100644 --- a/pkg/controller/mobileapps/index.go +++ b/pkg/controller/mobileapps/index.go @@ -16,10 +16,12 @@ package mobileapps import ( "context" + "fmt" "net/http" "github.com/google/exposure-notifications-verification-server/pkg/controller" "github.com/google/exposure-notifications-verification-server/pkg/database" + "github.com/google/exposure-notifications-verification-server/pkg/pagination" ) func (c *Controller) HandleIndex() http.Handler { @@ -32,20 +34,28 @@ func (c *Controller) HandleIndex() http.Handler { return } + pageParams, err := pagination.FromRequest(r) + if err != nil { + controller.InternalError(w, r, c.h, err) + return + } + // Perform the lazy load on authorized apps for the realm. - apps, err := realm.ListMobileApps(c.db) + apps, paginator, err := realm.ListMobileApps(c.db, pageParams) if err != nil { controller.InternalError(w, r, c.h, err) return } - c.renderIndex(ctx, w, apps) + c.renderIndex(ctx, w, apps, paginator) }) } // renderIndex renders the index page. -func (c *Controller) renderIndex(ctx context.Context, w http.ResponseWriter, apps []*database.MobileApp) { +func (c *Controller) renderIndex(ctx context.Context, w http.ResponseWriter, apps []*database.MobileApp, paginator *pagination.Paginator) { m := templateMap(ctx) + m["title"] = fmt.Sprintf("Mobile apps - %s", m["title"]) m["apps"] = apps + m["paginator"] = paginator c.h.RenderHTML(w, "mobileapps/index", m) } diff --git a/pkg/controller/mobileapps/show.go b/pkg/controller/mobileapps/show.go index 8e535ad5d..66f0bc248 100644 --- a/pkg/controller/mobileapps/show.go +++ b/pkg/controller/mobileapps/show.go @@ -16,6 +16,7 @@ package mobileapps import ( "context" + "fmt" "net/http" "github.com/google/exposure-notifications-verification-server/pkg/controller" @@ -61,6 +62,7 @@ func (c *Controller) HandleShow() http.Handler { // renderShow renders the edit page. func (c *Controller) renderShow(ctx context.Context, w http.ResponseWriter, app *database.MobileApp) { m := templateMap(ctx) + m["title"] = fmt.Sprintf("%s - Mobile apps - %s", app.Name, m["title"]) m["app"] = app c.h.RenderHTML(w, "mobileapps/show", m) } diff --git a/pkg/controller/mobileapps/update.go b/pkg/controller/mobileapps/update.go index ea8ca03da..96264c824 100644 --- a/pkg/controller/mobileapps/update.go +++ b/pkg/controller/mobileapps/update.go @@ -16,6 +16,7 @@ package mobileapps import ( "context" + "fmt" "net/http" "github.com/google/exposure-notifications-verification-server/pkg/controller" @@ -110,6 +111,7 @@ func (c *Controller) HandleUpdate() http.Handler { // renderEdit renders the edit page. func (c *Controller) renderEdit(ctx context.Context, w http.ResponseWriter, app *database.MobileApp) { m := templateMap(ctx) + m["title"] = fmt.Sprintf("%s - Edit mobile app - %s", app.Name, m["title"]) m["app"] = app c.h.RenderHTML(w, "mobileapps/edit", m) } diff --git a/pkg/database/realm.go b/pkg/database/realm.go index ca84faa08..bc408a804 100644 --- a/pkg/database/realm.go +++ b/pkg/database/realm.go @@ -730,20 +730,27 @@ func (r *Realm) FindAuthorizedApp(db *Database, id interface{}) (*AuthorizedApp, } // ListMobileApps gets all the mobile apps for the realm. -func (r *Realm) ListMobileApps(db *Database) ([]*MobileApp, error) { - var apps []*MobileApp - if err := db.db. +func (r *Realm) ListMobileApps(db *Database, p *pagination.PageParams) ([]*MobileApp, *pagination.Paginator, error) { + var mobileApps []*MobileApp + query := db.db. Unscoped(). - Model(r). - Order("mobile_apps.deleted_at DESC, LOWER(mobile_apps.name)"). - Related(&apps). - Error; err != nil { + Model(&MobileApp{}). + Where("realm_id = ?", r.ID). + Order("mobile_apps.deleted_at DESC, LOWER(mobile_apps.name)") + + if p == nil { + p = new(pagination.PageParams) + } + + paginator, err := Paginate(query, &mobileApps, p.Page, p.Limit) + if err != nil { if IsNotFound(err) { - return apps, nil + return mobileApps, nil, nil } - return nil, err + return nil, nil, err } - return apps, nil + + return mobileApps, paginator, nil } // FindMobileApp finds the mobile app by the given id associated with the realm.