add initial bits for an elm frontend
This commit is contained in:
parent
2fcac629fe
commit
4ad5b2964a
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@ result
|
||||
gostart
|
||||
.direnv
|
||||
*.db
|
||||
elm-stuff
|
||||
|
27
elm.json
Normal file
27
elm.json
Normal file
@ -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": {}
|
||||
}
|
||||
}
|
@ -44,7 +44,8 @@
|
||||
sqlc
|
||||
sqlite
|
||||
rlwrap
|
||||
nodePackages.typescript
|
||||
elmPackages.elm
|
||||
elmPackages.elm-live
|
||||
];
|
||||
};
|
||||
});
|
||||
|
@ -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)
|
||||
|
41
main.go
41
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)
|
||||
|
179
page.go
179
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"`
|
||||
}
|
||||
|
34
src/Data.elm
Normal file
34
src/Data.elm
Normal file
@ -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
|
||||
}
|
161
src/Main.elm
Normal file
161
src/Main.elm
Normal file
@ -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" ]
|
||||
]
|
195
watches.go
Normal file
195
watches.go
Normal file
@ -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
|
||||
}
|
Loading…
Reference in New Issue
Block a user