diff --git a/app/widget_maker.go b/app/widget_maker.go index d1f66244..bfb08612 100644 --- a/app/widget_maker.go +++ b/app/widget_maker.go @@ -14,6 +14,7 @@ import ( "github.com/wtfutil/wtf/modules/datadog" "github.com/wtfutil/wtf/modules/digitalclock" "github.com/wtfutil/wtf/modules/docker" + "github.com/wtfutil/wtf/modules/azuredevops" "github.com/wtfutil/wtf/modules/feedreader" "github.com/wtfutil/wtf/modules/gcal" "github.com/wtfutil/wtf/modules/gerrit" @@ -112,6 +113,9 @@ func MakeWidget( case "docker": settings := docker.NewSettingsFromYAML(moduleName, moduleConfig, config) widget = docker.NewWidget(app, pages, settings) + case "azuredevops": + settings := azuredevops.NewSettingsFromYAML(moduleName, moduleConfig, config) + widget = azuredevops.NewWidget(app, pages, settings) case "feedreader": settings := feedreader.NewSettingsFromYAML(moduleName, moduleConfig, config) widget = feedreader.NewWidget(app, pages, settings) diff --git a/go.mod b/go.mod index de7800f4..c4819c24 100644 --- a/go.mod +++ b/go.mod @@ -48,6 +48,8 @@ require ( github.com/magiconair/properties v1.8.1 // indirect github.com/mattn/go-colorable v0.1.2 // indirect github.com/mattn/go-isatty v0.0.9 // indirect + github.com/microsoft/azure-devops-go-api v0.0.0-20190912142452-3207b4a469d3 // indirect + github.com/microsoft/azure-devops-go-api/azuredevops v0.0.0-20190912142452-3207b4a469d3 github.com/mmcdole/gofeed v1.0.0-beta2.0.20190420154928-0e68beaf6fdf github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect github.com/olebedev/config v0.0.0-20190528211619-364964f3a8e4 diff --git a/go.sum b/go.sum index 95095014..b758b017 100644 --- a/go.sum +++ b/go.sum @@ -154,6 +154,7 @@ github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OI github.com/google/pprof v0.0.0-20190723021845-34ac40c74b70/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= @@ -222,6 +223,10 @@ github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/microsoft/azure-devops-go-api v0.0.0-20190912142452-3207b4a469d3 h1:FhI3QJOcd1CaXz2+DeFHUNEsS47+gxDERzVhexlTGvg= +github.com/microsoft/azure-devops-go-api v0.0.0-20190912142452-3207b4a469d3/go.mod h1:HfrxIH2ObkhL6NepJKAuEwUYY8kNvspOWDp6HDRIRQs= +github.com/microsoft/azure-devops-go-api/azuredevops v0.0.0-20190912142452-3207b4a469d3 h1:Q42uNxLRytr5ynbRkqspq37TjcfnuXwKmrS9APJv/Bw= +github.com/microsoft/azure-devops-go-api/azuredevops v0.0.0-20190912142452-3207b4a469d3/go.mod h1:PoGiBqKSQK1vIfQ+yVaFcGjDySHvym6FM1cNYnwzbrY= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mmcdole/gofeed v1.0.0-beta2.0.20190420154928-0e68beaf6fdf h1:poo3e5STwUVGyyUX0e3fHQUwT1tV8IYEFUUpYd/7LuA= diff --git a/modules/azuredevops/client.go b/modules/azuredevops/client.go new file mode 100644 index 00000000..3fb526c7 --- /dev/null +++ b/modules/azuredevops/client.go @@ -0,0 +1,59 @@ +package azuredevops + +import ( + "fmt" + "strings" + + azrBuild "github.com/microsoft/azure-devops-go-api/azuredevops/build" + "github.com/pkg/errors" +) + +func (widget *Widget) getBuildStats() string { + projName := widget.settings.projectName + statusFilter := azrBuild.BuildStatusValues.All + top := widget.settings.maxRows + builds, err := widget.cli.GetBuilds(widget.ctx, azrBuild.GetBuildsArgs{Project: &projName, StatusFilter: &statusFilter, Top: &top}) + if err != nil { + return errors.Wrap(err, "could not get builds").Error() + } + + result := "" + for _, build := range builds.Value { + num := *build.BuildNumber + branch := *build.SourceBranch + reason := *build.Reason + triggers := *build.TriggerInfo + if reason == azrBuild.BuildReasonValues.PullRequest { + branch = triggers["pr.sourceBranch"] + } + branch = strings.TrimPrefix(branch, "refs/heads/") + status := *build.Status + statusDisplay := "[white:grey]unknown" + if status == azrBuild.BuildStatusValues.InProgress { + statusDisplay = "[white:blue]in progress" + } else if status == azrBuild.BuildStatusValues.Cancelling { + statusDisplay = "[white:orange]in cancelling" + } else if (status == azrBuild.BuildStatusValues.Postponed) || (status == azrBuild.BuildStatusValues.NotStarted) { + statusDisplay = "[white:blue]waiting" + } else if status == azrBuild.BuildStatusValues.Completed { + buildResult := *build.Result + if buildResult == azrBuild.BuildResultValues.Succeeded { + statusDisplay = "[white:green]succeeded" + } else if buildResult == azrBuild.BuildResultValues.Failed { + statusDisplay = "[white:red]failed" + } else if buildResult == azrBuild.BuildResultValues.Canceled { + statusDisplay = "[white:darkgrey]cancelled" + } else if buildResult == azrBuild.BuildResultValues.PartiallySucceeded { + statusDisplay = "[white:magenta]partially" + } + } + + result += fmt.Sprintf("%s[-:-:-] #%s %s (%s) \n", statusDisplay, num, branch, reason) + } + + if result == "" { + result = "no builds found" + } + + return result +} diff --git a/modules/azuredevops/example-conf.yml b/modules/azuredevops/example-conf.yml new file mode 100644 index 00000000..9eac7b85 --- /dev/null +++ b/modules/azuredevops/example-conf.yml @@ -0,0 +1,42 @@ +wtf: + colors: + # background: black + # foreground: blue + border: + focusable: darkslateblue + focused: orange + normal: gray + checked: yellow + highlight: + fore: black + back: gray + rows: + even: yellow + odd: white + grid: + # How _wide_ the columns are, in terminal characters. In this case we have + # four columns, each of which are 35 characters wide. + # columns: [50, ] + # How _high_ the rows are, in terminal lines. In this case we have four rows + # that support ten line of text and one of four. + # rows: [50] + refreshInterval: 1 + openFileUtil: "open" + mods: + azuredevops: + type: azuredevops + title: "💻" + enabled: true + position: + top: 0 + left: 0 + height: 3 + width: 3 + refreshInterval: 1 + labelColor: lightblue # title label color (optional / default: white) + apiToken: "mysecret api token" # api key (required) + orgUrl: "https://dev.azure.com/myawesomecompany/" # url to your azure devops project (required) + prjectName: "the awesome project" # name of your project (required) + maxRows: 3 #max rows to show (optional / default 3) + + \ No newline at end of file diff --git a/modules/azuredevops/settings.go b/modules/azuredevops/settings.go new file mode 100644 index 00000000..ceada4eb --- /dev/null +++ b/modules/azuredevops/settings.go @@ -0,0 +1,32 @@ +package azuredevops + +import ( + "github.com/olebedev/config" + "github.com/wtfutil/wtf/cfg" +) + +const defaultTitle = "azuredevops" + +// Settings defines the configuration options for this module +type Settings struct { + common *cfg.Common + labelColor string + apiToken string + orgURL string + projectName string + maxRows int +} + +// NewSettingsFromYAML creates and returns an instance of Settings with configuration options populated +func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings { + settings := Settings{ + common: cfg.NewCommonSettingsFromModule(name, defaultTitle, ymlConfig, globalConfig), + labelColor: ymlConfig.UString("labelColor", "white"), + apiToken: ymlConfig.UString("apiToken", "api token not specified"), + orgURL: ymlConfig.UString("orgURL", "org url not specified"), + projectName: ymlConfig.UString("projectName", "project name not specified"), + maxRows: ymlConfig.UInt("maxRows", 3), + } + + return &settings +} diff --git a/modules/azuredevops/widget.go b/modules/azuredevops/widget.go new file mode 100644 index 00000000..9b856e50 --- /dev/null +++ b/modules/azuredevops/widget.go @@ -0,0 +1,66 @@ +package azuredevops + +import ( + "context" + "fmt" + + azr "github.com/microsoft/azure-devops-go-api/azuredevops" + azrBuild "github.com/microsoft/azure-devops-go-api/azuredevops/build" + "github.com/pkg/errors" + "github.com/rivo/tview" + "github.com/wtfutil/wtf/view" +) + +type Widget struct { + view.TextWidget + cli *azrBuild.Client + settings *Settings + displayBuffer string + ctx context.Context +} + +func NewWidget(app *tview.Application, pages *tview.Pages, settings *Settings) *Widget { + widget := Widget{ + TextWidget: view.NewTextWidget(app, settings.common, false), + settings: settings, + } + + widget.View.SetScrollable(true) + connection := azr.NewPatConnection(settings.orgURL, settings.apiToken) + ctx := context.Background() + + cli, err := azrBuild.NewClient(ctx, connection) + if err != nil { + widget.displayBuffer = errors.Wrap(err, "could not create client 2").Error() + } else { + widget.cli = cli + widget.ctx = ctx + } + + widget.refreshDisplayBuffer() + + return &widget +} + +func (widget *Widget) Refresh() { + widget.refreshDisplayBuffer() + widget.Redraw(widget.display) +} + +func (widget *Widget) display() (string, string, bool) { + return widget.CommonSettings().Title, widget.displayBuffer, true +} + +func (widget *Widget) refreshDisplayBuffer() { + if widget.cli == nil { + return + } + + widget.displayBuffer = "" + + widget.displayBuffer += fmt.Sprintf("[%s::bul] build status - %s\n", + widget.settings.labelColor, + widget.settings.projectName) + + widget.displayBuffer += widget.getBuildStats() +}