From 4ad5b2964a622d925c67b0c868063471c2dde525 Mon Sep 17 00:00:00 2001 From: Aaron Bieber Date: Sun, 23 Apr 2023 19:12:26 -0600 Subject: [PATCH] add initial bits for an elm frontend --- .gitignore | 1 + elm.json | 27 +++++++ flake.nix | 3 +- handlers.go | 8 +-- main.go | 41 ++++++----- page.go | 179 ---------------------------------------------- src/Data.elm | 34 +++++++++ src/Main.elm | 161 ++++++++++++++++++++++++++++++++++++++++++ watches.go | 195 +++++++++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 446 insertions(+), 203 deletions(-) create mode 100644 elm.json create mode 100644 src/Data.elm create mode 100644 src/Main.elm create mode 100644 watches.go diff --git a/.gitignore b/.gitignore index 98cedab..10e3e9e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ result gostart .direnv *.db +elm-stuff diff --git a/elm.json b/elm.json new file mode 100644 index 0000000..785c5be --- /dev/null +++ b/elm.json @@ -0,0 +1,27 @@ +{ + "type": "application", + "source-directories": [ + "src" + ], + "elm-version": "0.19.1", + "dependencies": { + "direct": { + "elm/browser": "1.0.2", + "elm/core": "1.0.5", + "elm/html": "1.0.0", + "elm/http": "2.0.0", + "elm/json": "1.1.3" + }, + "indirect": { + "elm/bytes": "1.0.8", + "elm/file": "1.0.5", + "elm/time": "1.0.0", + "elm/url": "1.0.0", + "elm/virtual-dom": "1.0.3" + } + }, + "test-dependencies": { + "direct": {}, + "indirect": {} + } +} diff --git a/flake.nix b/flake.nix index 9778520..6eafbb5 100644 --- a/flake.nix +++ b/flake.nix @@ -44,7 +44,8 @@ sqlc sqlite rlwrap - nodePackages.typescript + elmPackages.elm + elmPackages.elm-live ]; }; }); diff --git a/handlers.go b/handlers.go index b94d1af..9302699 100644 --- a/handlers.go +++ b/handlers.go @@ -117,11 +117,9 @@ func watchitemGET(w http.ResponseWriter, r *http.Request) { http.Error(w, http.StatusText(http.StatusUnprocessableEntity), http.StatusUnprocessableEntity) return } - watches, err := app.queries.GetAllWatchItemsByOwner(app.ctx, ownerID) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } + + watches := app.watches.forID(ownerID) + wJson, err := json.Marshal(watches) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/main.go b/main.go index 5124d22..f44f484 100644 --- a/main.go +++ b/main.go @@ -50,6 +50,23 @@ func main() { log.Fatal("can't open database: ", err) } + dbExists := false + if *dbFile == ":memory:" { + err := tmpDBPopulate(db) + if err != nil { + log.Fatal(err) + } + dbExists = true + } else { + if _, err := os.Stat(*dbFile); os.IsNotExist(err) { + log.Println("Creating database..") + if _, err := db.ExecContext(app.ctx, schema); err != nil { + log.Fatal("can't create database schema: ", err) + } + } + dbExists = true + } + app.watches = &WatchResults{} app.queries = data.New(db) app.tsServer = &tsnet.Server{ @@ -60,24 +77,12 @@ func main() { log.Fatal("can't get ts local client: ", err) } - dbExists := false - if *dbFile == ":memory:" { - err := tmpDBPopulate(db) - if err != nil { - log.Fatal(err) - } - } else { - if _, err := os.Stat(*dbFile); os.IsNotExist(err) { - log.Println("Creating database..") - if _, err := db.ExecContext(app.ctx, schema); err != nil { - log.Fatal("can't create database schema: ", err) - } - } - } - go func() { - time.Sleep(6 * time.Second) - dbExists = true - }() + /* + go func() { + time.Sleep(6 * time.Second) + dbExists = true + }() + */ if *key != "" { keyData, err := os.ReadFile(*key) diff --git a/page.go b/page.go index b67981a..822b62c 100644 --- a/page.go +++ b/page.go @@ -1,100 +1,12 @@ package main import ( - "bytes" - "context" - "encoding/json" - "fmt" - "log" - "net/http" "sort" - "time" "suah.dev/gostart/data" "tailscale.com/tailcfg" ) -const gqEndPoint = "https://api.github.com/graphql" - -const graphQuery = ` -{ - search( - query: "is:open is:public archived:false repo:%s in:title %s", - type: ISSUE, - first: 20 - ) { - issueCount - edges { - node { - ... on Issue { - number - title - url - repository { - nameWithOwner - } - createdAt - } - ... on PullRequest { - number - title - repository { - nameWithOwner - } - createdAt - url - } - } - } - } - rateLimit { - remaining - resetAt - } - } -` - -type GQLQuery struct { - Query string `json:"query"` -} - -func getData(q GQLQuery, token string) (*WatchResult, error) { - var req *http.Request - var err error - var re = &WatchResult{} - - client := &http.Client{} - buf := new(bytes.Buffer) - if err := json.NewEncoder(buf).Encode(q); err != nil { - return nil, err - } - - req, err = http.NewRequest("POST", gqEndPoint, buf) - if err != nil { - return nil, err - } - - req.Header.Set("Authorization", fmt.Sprintf("bearer %s", token)) - - res, err := client.Do(req) - if err != nil { - return nil, err - } - - defer func() { - err := res.Body.Close() - if err != nil { - log.Fatal("can't close body: ", err) - } - }() - - if err = json.NewDecoder(res.Body).Decode(re); err != nil { - return nil, err - } - - return re, nil -} - type Page struct { Title string System data.Owner @@ -114,94 +26,3 @@ func (p *Page) Sort() { return p.PullRequests[i].Number > p.PullRequests[j].Number }) } - -type WatchResults []WatchResult - -func (w *WatchResults) forID(ownerID int64) *WatchResults { - newResults := WatchResults{} - for _, r := range *w { - if r.OwnerID == ownerID { - newResults = append(newResults, r) - } - } - sort.Slice(newResults, func(i, j int) bool { - return newResults[i].Name < newResults[j].Name - }) - return &newResults -} - -func (w WatchResults) GetLimits() *RateLimit { - rl := &RateLimit{} - sort.Slice(w, func(i, j int) bool { - return w[i].Data.RateLimit.Remaining < w[j].Data.RateLimit.Remaining - }) - if len(w) > 0 { - rl = &w[0].Data.RateLimit - } - return rl -} - -func UpdateWatches(ghToken string) (*WatchResults, error) { - ctx := context.Background() - w := WatchResults{} - - watches, err := app.queries.GetAllWatchItems(ctx) - if err != nil { - return nil, err - } - - for _, watch := range watches { - qd := GQLQuery{Query: fmt.Sprintf(graphQuery, watch.Repo, watch.Name)} - wr, err := getData(qd, ghToken) - if err != nil { - return nil, err - } - - wr.OwnerID = watch.OwnerID - wr.Name = watch.Name - wr.Repo = watch.Repo - sort.Slice(wr.Data.Search.Edges, func(i, j int) bool { - return wr.Data.Search.Edges[i].Node.CreatedAt.After(wr.Data.Search.Edges[j].Node.CreatedAt) - }) - w = append(w, *wr) - } - - sort.Slice(w, func(i, j int) bool { - return w[i].Name < w[j].Name - }) - - return &w, nil -} - -type WatchResult struct { - Data Data `json:"data,omitempty"` - OwnerID int64 - Name string - Repo string -} -type Repository struct { - NameWithOwner string `json:"nameWithOwner,omitempty"` -} -type Node struct { - Number int `json:"number,omitempty"` - Title string `json:"title,omitempty"` - Repository Repository `json:"repository,omitempty"` - CreatedAt time.Time `json:"createdAt,omitempty"` - URL string `json:"url,omitempty"` -} -type Edges struct { - Node Node `json:"node,omitempty"` -} -type Search struct { - IssueCount int `json:"issueCount,omitempty"` - Edges []Edges `json:"edges,omitempty"` -} - -type RateLimit struct { - Remaining int `json:"remaining,omitempty"` - ResetAt time.Time `json:"resetAt,omitempty"` -} -type Data struct { - Search Search `json:"search,omitempty"` - RateLimit RateLimit `json:"rateLimit,omitempty"` -} diff --git a/src/Data.elm b/src/Data.elm new file mode 100644 index 0000000..605ad28 --- /dev/null +++ b/src/Data.elm @@ -0,0 +1,34 @@ +module Data exposing (Edge, Link, Node, Watch, WatchData) + + +type alias Watch = + { owner_id : Int + , name : String + , repo : String + , result_count : Int + } + + +type alias Node = + { number : Int + } + + +type alias Edge = + { node : Node + } + + +type alias WatchData = + { search : List Edge + } + + +type alias Link = + { id : Int + , owner_id : Int + , created_at : String + , name : String + , url : String + , logo_url : String + } diff --git a/src/Main.elm b/src/Main.elm new file mode 100644 index 0000000..a38981a --- /dev/null +++ b/src/Main.elm @@ -0,0 +1,161 @@ +module Main exposing (..) + +import Browser +import Html exposing (..) +import Html.Attributes exposing (style) +import Html.Events exposing (..) +import Http +import Json.Decode exposing (Decoder, field, int, list, map3, map4, map5, maybe, string) + + +main = + Browser.element + { init = init + , update = update + , subscriptions = subscriptions + , view = view + } + + +type Model + = Failure + | Loading + | Success (List Watch) + + +type alias Watch = + { owner_id : Int + , name : String + , repo : String + , result_count : Int + } + + +type alias Node = + { number : Int + } + + +type alias Edge = + { node : Node + } + + +type alias WatchData = + { search : List Edge + } + + +type alias Link = + { id : Int + , owner_id : Int + , created_at : String + , name : String + , url : String + , logo_url : String + } + + +init : () -> ( Model, Cmd Msg ) +init _ = + ( Loading + , Cmd.batch + [ getWatches + ] + ) + + +type Msg + = MorePlease + | GetWatches (Result Http.Error (List Watch)) + + +update : Msg -> Model -> ( Model, Cmd Msg ) +update msg model = + case msg of + MorePlease -> + ( Loading, getWatches ) + + GetWatches result -> + case result of + Ok watches -> + ( Success watches, Cmd.none ) + + Err _ -> + ( Failure, Cmd.none ) + + +subscriptions : Model -> Sub Msg +subscriptions model = + Sub.none + + +view : Model -> Html Msg +view model = + div [] + [ h2 [] [ text "Watches" ] + , viewWatches model + ] + + +viewWatches : Model -> Html Msg +viewWatches model = + case model of + Failure -> + div [] + [ text "I can't load the watches" + , button [ onClick MorePlease ] [ text "Try agan!" ] + ] + + Loading -> + text "Loading..." + + Success watches -> + div [] + (List.map viewWatch watches) + + +viewLinks : Model -> Html Msg +viewLinks model = + case model of + Failure -> + div [] + [ text "I can't load the links" + , button [ onClick MorePlease ] [ text "Try agan!" ] + ] + + Loading -> + text "Loading..." + + Success links -> + text "success links..." + + +getWatches : Cmd Msg +getWatches = + Http.get + { url = "/watches" + , expect = Http.expectJson GetWatches watchListDecoder + } + + +watchListDecoder : Decoder (List Watch) +watchListDecoder = + list watchDecoder + + +watchDecoder : Decoder Watch +watchDecoder = + map4 Watch + (field "owner_id" int) + (field "name" string) + (field "repo" string) + (field "result_count" int) + + +viewWatch : Watch -> Html Msg +viewWatch watch = + li [] + [ text (String.fromInt watch.result_count ++ " " ++ watch.name) + , li [] [ text "butter" ] + ] diff --git a/watches.go b/watches.go new file mode 100644 index 0000000..f3282a5 --- /dev/null +++ b/watches.go @@ -0,0 +1,195 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "sort" + "time" +) + +const gqEndPoint = "https://api.github.com/graphql" + +const graphQuery = ` +{ + search( + query: "is:open is:public archived:false repo:%s in:title %s", + type: ISSUE, + first: 20 + ) { + issueCount + edges { + node { + ... on Issue { + number + title + url + repository { + nameWithOwner + } + createdAt + } + ... on PullRequest { + number + title + repository { + nameWithOwner + } + createdAt + url + } + } + } + } + rateLimit { + remaining + resetAt + } + } +` + +type GQLQuery struct { + Query string `json:"query"` +} + +type WatchResults []WatchResult + +func (w *WatchResults) forID(ownerID int64) *WatchResults { + newResults := WatchResults{} + for _, r := range *w { + if r.OwnerID == ownerID { + newResults = append(newResults, r) + } + } + sort.Slice(newResults, func(i, j int) bool { + return newResults[i].Name < newResults[j].Name + }) + return &newResults +} + +func (w WatchResults) GetLimits() *RateLimit { + rl := &RateLimit{} + sort.Slice(w, func(i, j int) bool { + return w[i].Data.RateLimit.Remaining < w[j].Data.RateLimit.Remaining + }) + if len(w) > 0 { + rl = &w[0].Data.RateLimit + } + return rl +} + +func UpdateWatches(ghToken string) (*WatchResults, error) { + ctx := context.Background() + w := WatchResults{} + + watches, err := app.queries.GetAllWatchItems(ctx) + if err != nil { + return nil, err + } + + for _, watch := range watches { + qd := GQLQuery{Query: fmt.Sprintf(graphQuery, watch.Repo, watch.Name)} + wr, err := getWatchData(qd, ghToken) + if err != nil { + return nil, err + } + + wr.OwnerID = watch.OwnerID + wr.Name = watch.Name + wr.Repo = watch.Repo + wr.ResultCount = wr.Data.Search.IssueCount + for _, dr := range wr.Data.Search.Edges { + wr.Results = append(wr.Results, dr.Node) + } + sort.Slice(wr.Data.Search.Edges, func(i, j int) bool { + return wr.Data.Search.Edges[i].Node.CreatedAt.After(wr.Data.Search.Edges[j].Node.CreatedAt) + }) + w = append(w, *wr) + } + + sort.Slice(w, func(i, j int) bool { + return w[i].Name < w[j].Name + }) + + return &w, nil +} + +type WatchResult struct { + Data Data `json:"data"` + OwnerID int64 `json:"owner_id"` + Name string `json:"name"` + Repo string `json:"repo"` + Results []Node `json:"results"` + ResultCount int `json:"result_count"` +} + +type Repository struct { + NameWithOwner string `json:"nameWithOwner,omitempty"` +} + +type Node struct { + Number int `json:"number,omitempty"` + Title string `json:"title,omitempty"` + Repository Repository `json:"repository,omitempty"` + CreatedAt time.Time `json:"createdAt,omitempty"` + URL string `json:"url,omitempty"` +} + +type Edges struct { + Node Node `json:"node,omitempty"` +} + +type Search struct { + IssueCount int `json:"issueCount,omitempty"` + Edges []Edges `json:"edges,omitempty"` +} + +type RateLimit struct { + Remaining int `json:"remaining,omitempty"` + ResetAt time.Time `json:"resetAt,omitempty"` +} + +type Data struct { + Search Search `json:"search,omitempty"` + RateLimit RateLimit `json:"rateLimit,omitempty"` +} + +func getWatchData(q GQLQuery, token string) (*WatchResult, error) { + var req *http.Request + var err error + var re = &WatchResult{} + + client := &http.Client{} + buf := new(bytes.Buffer) + if err := json.NewEncoder(buf).Encode(q); err != nil { + return nil, err + } + + req, err = http.NewRequest("POST", gqEndPoint, buf) + if err != nil { + return nil, err + } + + req.Header.Set("Authorization", fmt.Sprintf("bearer %s", token)) + + res, err := client.Do(req) + if err != nil { + return nil, err + } + + defer func() { + err := res.Body.Close() + if err != nil { + log.Fatal("can't close body: ", err) + } + }() + + if err = json.NewDecoder(res.Body).Decode(re); err != nil { + return nil, err + } + + return re, nil +}