diff --git a/assets/_data/script/main.go b/assets/_data/script/main.go deleted file mode 100644 index b0508ee..0000000 --- a/assets/_data/script/main.go +++ /dev/null @@ -1,76 +0,0 @@ -// +build js - -package main - -import ( - "net/url" - - "github.com/gopherjs/gopherjs/js" - "github.com/shurcooL/go/gopherjs_http/jsutil" - "honnef.co/go/js/dom" - "honnef.co/go/js/xhr" -) - -var document = dom.GetWindow().Document().(dom.HTMLDocument) - -func main() { - js.Global.Set("UpdateRepository", jsutil.Wrap(UpdateRepository)) -} - -// UpdateRepository updates specified repository. -// repoRoot is the import path corresponding to the root of the repository. -func UpdateRepository(event dom.Event, repoRoot string) { - event.PreventDefault() - if event.(*dom.MouseEvent).Button != 0 { - return - } - - repoUpdate := document.GetElementByID(repoRoot) - updateButton := repoUpdate.GetElementsByClassName("update-button")[0].(*dom.HTMLAnchorElement) - - updateButton.SetTextContent("Updating...") - updateButton.AddEventListener("click", false, func(event dom.Event) { event.PreventDefault() }) - updateButton.SetTabIndex(-1) - updateButton.Class().Add("disabled") - - go func() { - req := xhr.NewRequest("POST", "/-/update") - req.SetRequestHeader("Content-Type", "application/x-www-form-urlencoded") - err := req.Send(url.Values{"repo_root": {repoRoot}}.Encode()) - if err != nil { - println(err.Error()) - return - } - - // Hide the "Updating..." label. - updateButton.Style().SetProperty("display", "none", "") - - // Show "No Updates Available" if there are no remaining updates. - if !anyUpdatesRemaining() { - document.GetElementByID("no_updates").(dom.HTMLElement).Style().SetProperty("display", "", "") - } - - // Move this Go package to "Installed Updates" list. - installedUpdates := document.GetElementByID("installed_updates").(dom.HTMLElement) - installedUpdates.Style().SetProperty("display", "", "") - installedUpdates.ParentNode().InsertBefore(repoUpdate, installedUpdates.NextSibling()) // Insert after. - }() -} - -// anyUpdatesRemaining reports if there's at least one available or in-flight update. -func anyUpdatesRemaining() bool { - updates := document.GetElementsByClassName("go-package-update") - for _, update := range updates { - els := update.GetElementsByClassName("update-button") - if len(els) == 0 { - // There may not be any matches if this package was already updated and has no update button. - continue - } - updateButton := els[0].(*dom.HTMLAnchorElement) - updateButtonVisible := updateButton.Style().GetPropertyValue("display") != "none" - if updateButtonVisible { - return true - } - } - return false -} diff --git a/assets/_data/style.css b/assets/_data/style.css index 4ce1a7c..329fa20 100644 --- a/assets/_data/style.css +++ b/assets/_data/style.css @@ -26,12 +26,6 @@ h2 { margin-bottom: 40px; } -.disabled { - pointer-events: none; - cursor: default; - color: gray; - text-decoration: none; -} span.smaller { font-size: 11px; color: gray; diff --git a/assets/assets.go b/assets/assets.go index 5a32edf..ab4a6cf 100644 --- a/assets/assets.go +++ b/assets/assets.go @@ -13,7 +13,8 @@ import ( // Assets contains assets for Go Package Store. var Assets = union.New(map[string]http.FileSystem{ - "/assets": gopherjs_http.NewFS(http.Dir(importPathToDir("github.com/shurcooL/Go-Package-Store/assets/_data"))), + "/assets": gopherjs_http.NewFS(http.Dir(importPathToDir("github.com/shurcooL/Go-Package-Store/assets/_data"))), + "/frontend.js": gopherjs_http.Package("github.com/shurcooL/Go-Package-Store/frontend"), }) func importPathToDir(importPath string) string { diff --git a/cmd/Go-Package-Store/dev.go b/cmd/Go-Package-Store/dev.go index 05c638c..1b1bdc0 100644 --- a/cmd/Go-Package-Store/dev.go +++ b/cmd/Go-Package-Store/dev.go @@ -4,43 +4,213 @@ package main import ( "errors" + "io" "net/http" "time" "github.com/shurcooL/Go-Package-Store" + gpscomponent "github.com/shurcooL/Go-Package-Store/component" "github.com/shurcooL/Go-Package-Store/workspace" + "github.com/shurcooL/htmlg" "github.com/shurcooL/httperror" ) +import _ "net/http/pprof" + const production = false func init() { - http.HandleFunc("/mock.html", mockHandler) + http.Handle("/mock.html", errorHandler(mockHandler)) + http.Handle("/component.html", errorHandler(componentHandler)) } -func mockHandler(w http.ResponseWriter, req *http.Request) { +func mockHandler(w http.ResponseWriter, req *http.Request) error { if req.Method != "GET" { - httperror.HandleMethod(w, httperror.Method{Allowed: []string{"GET"}}) - return + return httperror.Method{Allowed: []string{"GET"}} } // Reset the pipeline and populate it with mock repo presentations, // complete with artificial delays (to simulate processing time). c.pipeline = workspace.NewPipeline(wd) go func() { - for _, repoPresentation := range mockRepoPresentations { - repoPresentation := repoPresentation - time.Sleep(time.Second) - c.pipeline.AddPresented(&repoPresentation) + for _, rp := range mockWorkspaceRPs { + time.Sleep(5 * time.Second) + rp := rp + c.pipeline.AddPresented(&rp) } - time.Sleep(time.Second) + time.Sleep(5 * time.Second) c.pipeline.Done() }() - mainHandler(w, req) + return indexHandler(w, req) +} + +func componentHandler(w http.ResponseWriter, req *http.Request) error { + if req.Method != "GET" { + return httperror.Method{Allowed: []string{"GET"}} + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + + _, err := io.WriteString(w, ` +
+Error: {{.}}
- {{end}} - */ - var ns []*html.Node - ns = append(ns, PresentationChanges{ - Changes: p.Changes, - LocalRevision: p.LocalRevision, - RemoteRevision: p.RemoteRevision, - }.Render()...) - if p.Error != "" { - n := &html.Node{ - Type: html.ElementNode, Data: atom.P.String(), - Attr: []html.Attribute{{Key: atom.Class.String(), Val: "presentation-error"}}, - } - n.AppendChild(htmlg.Strong("Error:")) - n.AppendChild(htmlg.Text(" ")) - n.AppendChild(htmlg.Text(p.Error)) - ns = append(ns, n) - } - return ns -} - // TODO: Turn this into a maybeLink, etc. func (p RepoPresentation) importPathPattern() *html.Node { /* @@ -149,37 +131,78 @@ func (p RepoPresentation) importPathPattern() *html.Node { return importPathPattern } -func (p RepoPresentation) updateButton() *html.Node { +func (p RepoPresentation) updateState() *html.Node { /* -Error: {{.}}
+ {{end}} + */ + var ns []*html.Node + ns = append(ns, PresentationChanges{ + Changes: p.Changes, + LocalRevision: p.LocalRevision, + RemoteRevision: p.RemoteRevision, + }.Render()...) + if p.Error != "" { + n := &html.Node{ + Type: html.ElementNode, Data: atom.P.String(), + Attr: []html.Attribute{{Key: atom.Class.String(), Val: "presentation-error"}}, } + n.AppendChild(htmlg.Strong("Error:")) + n.AppendChild(htmlg.Text(" ")) + n.AppendChild(htmlg.Text(p.Error)) + ns = append(ns, n) } + return ns } type PresentationChanges struct { diff --git a/frontend/main.go b/frontend/main.go new file mode 100644 index 0000000..fe01aee --- /dev/null +++ b/frontend/main.go @@ -0,0 +1,251 @@ +// Command frontend runs on frontend of Go Package Store. +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "net/url" + "sync" + + "github.com/gopherjs/gopherjs/js" + gpscomponent "github.com/shurcooL/Go-Package-Store/component" + "github.com/shurcooL/go/gopherjs_http/jsutil" + "github.com/shurcooL/htmlg" + "honnef.co/go/js/dom" +) + +var document = dom.GetWindow().Document().(dom.HTMLDocument) + +func main() { + js.Global.Set("UpdateRepository", jsutil.Wrap(UpdateRepository)) + js.Global.Set("UpdateAll", jsutil.Wrap(UpdateAll)) + + switch readyState := document.ReadyState(); readyState { + case "loading": + document.AddEventListener("DOMContentLoaded", false, func(dom.Event) { + go run() + }) + case "interactive", "complete": + run() + default: + panic(fmt.Errorf("internal error: unexpected document.ReadyState value: %v", readyState)) + } +} + +func run() { + err := stream() + if err != nil { + log.Println(err) + } +} + +func stream() error { + // TODO: Initial render might not be needed if the server prerenders initial state. + err := renderBody() + if err != nil { + return err + } + + resp, err := http.Get("/api/updates") + if err != nil { + return err + } + defer resp.Body.Close() + dec := json.NewDecoder(resp.Body) + for { + var rp gpscomponent.RepoPresentation + err := dec.Decode(&rp) + if err == io.EOF { + break + } else if err != nil { + return err + } + + rpsMu.Lock() + rps = append(rps, &rp) + moveUp(rps, &rp) + rpsMu.Unlock() + + err = renderBody() + if err != nil { + return err + } + } + checkingUpdates = false + + err = renderBody() + return err +} + +var ( + rpsMu sync.Mutex // TODO: Move towards a channel-based unified state manipulator. + rps []*gpscomponent.RepoPresentation + + checkingUpdates = true +) + +func renderBody() error { + rpsMu.Lock() + defer rpsMu.Unlock() + + var buf bytes.Buffer + + err := htmlg.RenderComponents(&buf, gpscomponent.Header{}) + if err != nil { + return err + } + + _, err = io.WriteString(&buf, `