Skip to content

Commit

Permalink
feat: initial web version
Browse files Browse the repository at this point in the history
  • Loading branch information
moul committed Oct 5, 2018
1 parent b70b5ae commit 9a86443
Show file tree
Hide file tree
Showing 10 changed files with 316 additions and 30 deletions.
54 changes: 54 additions & 0 deletions chi_util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package main

import (
"net/http"
"strings"

"github.com/go-chi/chi"
"github.com/go-chi/render"
)

// taken from
// - https://github.com/go-chi/chi/blob/cca4135d8dddff765463feaf1118047a9e506b4a/_examples/fileserver/main.go
// - https://github.com/go-chi/chi/blob/cca4135d8dddff765463feaf1118047a9e506b4a/_examples/rest/main.go

func (e *ErrResponse) Render(w http.ResponseWriter, r *http.Request) error {
render.Status(r, e.HTTPStatusCode)
return nil
}

type ErrResponse struct {
Err error `json:"-"` // low-level runtime error
HTTPStatusCode int `json:"-"` // http response status code

StatusText string `json:"status"` // user-level status message
AppCode int64 `json:"code,omitempty"` // application-specific error code
ErrorText string `json:"error,omitempty"` // application-level error message, for debugging
}

func ErrRender(err error) render.Renderer {
return &ErrResponse{
Err: err,
HTTPStatusCode: 422,
StatusText: "Error rendering response.",
ErrorText: err.Error(),
}
}

func FileServer(r chi.Router, path string, root http.FileSystem) {
if strings.ContainsAny(path, "{}*") {
panic("FileServer does not permit URL parameters.")
}

fs := http.StripPrefix(path, http.FileServer(root))

if path != "/" && path[len(path)-1] != '/' {
r.Get(path, http.RedirectHandler(path+"/", 301).ServeHTTP)
path += "/"
}
path += "*"

r.Get(path, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fs.ServeHTTP(w, r)
}))
}
3 changes: 0 additions & 3 deletions cmd_db.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import (
)

type dbOptions struct {
Path string `mapstructure:"dbpath"`
Verbose bool
}

func (opts dbOptions) String() string {
Expand All @@ -22,7 +20,6 @@ func (opts dbOptions) String() string {
}

func dbSetupFlags(flags *pflag.FlagSet, opts *dbOptions) {

viper.BindPFlags(flags)
}

Expand Down
48 changes: 23 additions & 25 deletions cmd_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,13 @@ type runOptions struct {
ReposToFetch []string

// db
DBOpts dbOptions
DBOpts dbOptions
GraphOpts graphOptions

// run
ShowClosed bool `mapstructure:"show-closed"`
ShowOrphans bool
AdditionalPulls []string
EpicLabel string
Destination string
DebugGraph bool
NoCompress bool
DarkTheme bool

Targets []string
//Preview bool
}

Expand All @@ -43,12 +37,6 @@ func (opts runOptions) String() string {

func runSetupFlags(flags *pflag.FlagSet, opts *runOptions) {
flags.BoolVarP(&opts.NoPull, "no-pull", "f", false, "do not pull new issues before runing")
flags.BoolVarP(&opts.ShowClosed, "show-closed", "", false, "show closed issues")
flags.BoolVarP(&opts.DebugGraph, "debug-graph", "", false, "debug graph")
flags.BoolVarP(&opts.ShowOrphans, "show-orphans", "", false, "show issues not linked to an epic")
flags.BoolVarP(&opts.NoCompress, "no-compress", "", false, "do not compress graph (no overlap)")
flags.BoolVarP(&opts.DarkTheme, "dark-theme", "", false, "dark theme")
flags.StringVarP(&opts.EpicLabel, "epic-label", "", "epic", "label used for epics (empty means issues with dependencies but without dependants)")
flags.StringVarP(&opts.Destination, "destination", "", "-", "destination ('-' for stdout)")
flags.StringSliceVarP(&opts.AdditionalPulls, "additional-pull", "", []string{}, "additional pull that won't necessarily be displayed on the graph")
//flags.BoolVarP(&opts.Preview, "preview", "p", false, "preview result")
Expand All @@ -69,9 +57,12 @@ func newRunCommand() *cobra.Command {
if err := viper.Unmarshal(&opts.DBOpts); err != nil {
return err
}
if err := viper.Unmarshal(&opts.GraphOpts); err != nil {
return err
}
opts.PullOpts.DBOpts = opts.DBOpts
opts.PullOpts.Targets = append(args, opts.AdditionalPulls...)
opts.Targets = args
opts.GraphOpts.Targets = args
return run(opts)
},
}
Expand All @@ -81,21 +72,17 @@ func newRunCommand() *cobra.Command {
return cmd
}

func run(opts *runOptions) error {
logger().Debug("run", zap.Stringer("opts", *opts))
if !opts.NoPull {
if err := pull(&opts.PullOpts); err != nil {
return err
}
func graphviz(opts *graphOptions) (string, error) {
if opts.Targets == nil || len(opts.Targets) < 1 || opts.Targets[0] == "" {
return "", fmt.Errorf("you need to specify at least one target")
}

issues, err := loadIssues(db, nil)
if err != nil {
return errors.Wrap(err, "failed to load issues")
return "", errors.Wrap(err, "failed to load issues")
}

if err := issues.prepare(); err != nil {
return errors.Wrap(err, "failed to prepare issues")
return "", errors.Wrap(err, "failed to prepare issues")
}

if !opts.ShowClosed {
Expand All @@ -106,7 +93,18 @@ func run(opts *runOptions) error {
logger().Warn("--show-orphans is deprecated and will be removed")
}

out, err := graphviz(issues, opts)
return graphvizRender(issues, opts)
}

func run(opts *runOptions) error {
logger().Debug("run", zap.Stringer("opts", *opts))
if !opts.NoPull {
if err := pull(&opts.PullOpts); err != nil {
return err
}
}

out, err := graphviz(&opts.GraphOpts)
if err != nil {
return err
}
Expand Down
153 changes: 153 additions & 0 deletions cmd_web.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package main

import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"time"

"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/go-chi/docgen"
"github.com/go-chi/render"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)

type webOptions struct {
// web specific
Bind string
ShowRoutes bool

// db
DBOpts dbOptions
}

func (opts webOptions) String() string {
out, _ := json.Marshal(opts)
return string(out)
}

func webSetupFlags(flags *pflag.FlagSet, opts *webOptions) {
flags.StringVarP(&opts.Bind, "bind", "b", ":2020", "web server bind address")
flags.BoolVarP(&opts.ShowRoutes, "show-routes", "", false, "display available routes and quit")
viper.BindPFlags(flags)
}

func newWebCommand() *cobra.Command {
opts := &webOptions{}
cmd := &cobra.Command{
Use: "web",
RunE: func(cmd *cobra.Command, args []string) error {
if err := viper.Unmarshal(opts); err != nil {
return err
}
if err := viper.Unmarshal(&opts.DBOpts); err != nil {
return err
}
return web(opts)
},
}
webSetupFlags(cmd.Flags(), opts)
dbSetupFlags(cmd.Flags(), &opts.DBOpts)
return cmd
}

func (i *Issue) Render(w http.ResponseWriter, r *http.Request) error {
return nil
}

func webListIssues(w http.ResponseWriter, r *http.Request) {
issues, err := loadIssues(db, nil)
if err != nil {
render.Render(w, r, ErrRender(err))
return
}

list := []render.Renderer{}
for _, issue := range issues {
list = append(list, issue)
}

if err := render.RenderList(w, r, list); err != nil {
render.Render(w, r, ErrRender(err))
return
}
}

func webGraphviz(r *http.Request) (string, error) {
opts := &graphOptions{
Targets: strings.Split(r.URL.Query().Get("targets"), ","),
ShowClosed: r.URL.Query().Get("show-closed") == "1",
}
return graphviz(opts)
}

func webDotIssues(w http.ResponseWriter, r *http.Request) {
out, err := webGraphviz(r)
if err != nil {
render.Render(w, r, ErrRender(err))
return
}

w.Write([]byte(out))
}

func webImageIssues(w http.ResponseWriter, r *http.Request) {
out, err := webGraphviz(r)
if err != nil {
render.Render(w, r, ErrRender(err))
return
}

cmd := exec.Command("dot", "-Tsvg")
cmd.Stdin = bytes.NewBuffer([]byte(out))
cmd.Stdout = w

if err := cmd.Run(); err != nil {
render.Render(w, r, ErrRender(err))
return
}
}

func web(opts *webOptions) error {
r := chi.NewRouter()

//r.Use(middleware.RequestID)
//r.Use(middleware.RealIP)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
//r.Use(middleware.URLFormat)
r.Use(middleware.Timeout(5 * time.Second))

r.Route("/api", func(r chi.Router) {
r.Route("/", func(r chi.Router) {
r.Use(render.SetContentType(render.ContentTypeJSON))
r.Get("/issues.json", webListIssues)
})
r.Get("/graph/dot", webDotIssues)
r.Get("/graph/image", webImageIssues)
})

workDir, _ := os.Getwd()
filesDir := filepath.Join(workDir, "web")
FileServer(r, "/", http.Dir(filesDir))

if opts.ShowRoutes {
fmt.Println(docgen.MarkdownRoutesDoc(r, docgen.MarkdownOpts{
ProjectPath: "moul.io/depviz",
Intro: "Welcome to depviz generated docs.",
}))
return nil
}

log.Printf("Listening on %s", opts.Bind)
return http.ListenAndServe(opts.Bind, r)
}
15 changes: 15 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,19 +1,34 @@
module moul.io/depviz

require (
cloud.google.com/go v0.29.0 // indirect
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/awalterschulze/gographviz v0.0.0-20180927133620-e69668a01397
github.com/denisenkom/go-mssqldb v0.0.0-20180901172138-1eb28afdf9b6 // indirect
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 // indirect
github.com/go-chi/chi v3.3.3+incompatible
github.com/go-chi/docgen v1.0.2 // indirect
github.com/go-chi/render v1.0.1
github.com/go-sql-driver/mysql v1.4.0 // indirect
github.com/google/go-cmp v0.2.0 // indirect
github.com/google/go-github v17.0.0+incompatible
github.com/google/go-querystring v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jinzhu/gorm v1.9.1
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a // indirect
github.com/jinzhu/now v0.0.0-20180511015916-ed742868f2ae // indirect
github.com/lib/pq v1.0.0 // indirect
github.com/mattn/go-sqlite3 v1.9.0
github.com/pkg/errors v0.8.0
github.com/spf13/cobra v0.0.3
github.com/spf13/pflag v1.0.3
github.com/spf13/viper v1.2.1
github.com/xanzy/go-gitlab v0.11.1
go.uber.org/zap v1.9.1
golang.org/x/crypto v0.0.0-20181001203147-e3636079e1a4 // indirect
golang.org/x/net v0.0.0-20181003013248-f5e5bdd77824 // indirect
golang.org/x/oauth2 v0.0.0-20181003184128-c57b0facaced
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f // indirect
google.golang.org/appengine v1.2.0 // indirect
moul.io/zapgorm v0.0.0-20181003053625-c808c1c4adc6
)
Loading

0 comments on commit 9a86443

Please sign in to comment.