Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor TUIModel
Browse files Browse the repository at this point in the history
Get rid of the internals in Config and move everything to TUIModel.
grisu48 committed Dec 16, 2024
1 parent e4268e7 commit edf648a
Showing 4 changed files with 245 additions and 233 deletions.
23 changes: 0 additions & 23 deletions cmd/openqa-revtui/config.go
Original file line number Diff line number Diff line change
@@ -5,7 +5,6 @@ import (
"strings"

"github.com/BurntSushi/toml"
"github.com/os-autoinst/gopenqa"
)

/* Group is a single configurable monitoring unit. A group contains all parameters that will be queried from openQA */
@@ -29,17 +28,8 @@ type Config struct {
MaxJobs int // Maximum number of jobs per group to consider
GroupBy string // Display group mode: "none", "groups"
RequestJobLimit int // Maximum number of jobs in a single request

// Internal settings
jobGroups map[int]gopenqa.JobGroup // Fetched JobGroup instances from openQA
jobs []gopenqa.Job // Fetched jobs for this config
knownJobs []gopenqa.Job // Known jobs for this configuration
instance *gopenqa.Instance // openQA instance for this config
}

var cfs []Config // All available configurations
var cfi int // Current configuration

func (cf Config) Validate() error {
if len(cf.Groups) == 0 {
return fmt.Errorf("no review groups defined")
@@ -92,16 +82,3 @@ func CreateConfig() Config {
cf.RequestJobLimit = 100
return cf
}

/** Try to update the job with the given status, if present. Returns the found job and true if the job was present */
func (cf *Config) UpdateJobStatus(status gopenqa.JobStatus) (gopenqa.Job, bool) {
var job gopenqa.Job
for i, j := range cf.knownJobs {
if j.ID == status.ID {
cf.knownJobs[i].State = "done"
cf.knownJobs[i].Result = fmt.Sprintf("%s", status.Result)
return cf.knownJobs[i], true
}
}
return job, false
}
224 changes: 115 additions & 109 deletions cmd/openqa-revtui/openqa-revtui.go
Original file line number Diff line number Diff line change
@@ -13,28 +13,19 @@ import (

var tui *TUI

func getKnownJob(id int64) (gopenqa.Job, bool) {
for _, j := range cfs[cfi].knownJobs {
if j.ID == id {
return j, true
}
}
return gopenqa.Job{}, false
}

func loadDefaultConfig() (Config, error) {
var cf Config
configFile := homeDir() + "/.openqa-revtui.toml"
if fileExists(configFile) {
if err := cf.LoadToml(configFile); err != nil {
return cf, err
}
cfs = append(cfs, cf)
}
return cf, nil
}

func parseProgramArgs(cf *Config) error {
func parseProgramArgs(cf *Config) ([]Config, error) {
cfs := make([]Config, 0)
n := len(os.Args)
for i := 1; i < n; i++ {
arg := os.Args[i]
@@ -50,35 +41,35 @@ func parseProgramArgs(cf *Config) error {
os.Exit(0)
} else if arg == "-c" || arg == "--config" {
if i++; i >= n {
return fmt.Errorf("missing argument: %s", "config file")
return cfs, fmt.Errorf("missing argument: %s", "config file")
}
filename := os.Args[i]
var cf Config
if err := cf.LoadToml(filename); err != nil {
return fmt.Errorf("in %s: %s", filename, err)
return cfs, fmt.Errorf("in %s: %s", filename, err)
}
cfs = append(cfs, cf)
} else if arg == "-r" || arg == "--remote" {
if i++; i >= n {
return fmt.Errorf("missing argument: %s", "remote")
return cfs, fmt.Errorf("missing argument: %s", "remote")
}
cf.Instance = os.Args[i]
} else if arg == "-q" || arg == "--rabbit" || arg == "--rabbitmq" {
if i++; i >= n {
return fmt.Errorf("missing argument: %s", "RabbitMQ link")
return cfs, fmt.Errorf("missing argument: %s", "RabbitMQ link")
}
cf.RabbitMQ = os.Args[i]
} else if arg == "-i" || arg == "--hide" || arg == "--hide-status" {
if i++; i >= n {
return fmt.Errorf("missing argument: %s", "Status to hide")
return cfs, fmt.Errorf("missing argument: %s", "Status to hide")
}
cf.HideStatus = append(cf.HideStatus, strings.Split(os.Args[i], ",")...)
} else if arg == "-p" || arg == "--param" {
if i++; i >= n {
return fmt.Errorf("missing argument: %s", "parameter")
return cfs, fmt.Errorf("missing argument: %s", "parameter")
}
if name, value, err := splitNV(os.Args[i]); err != nil {
return fmt.Errorf("argument parameter is invalid: %s", err)
return cfs, fmt.Errorf("argument parameter is invalid: %s", err)
} else {
cf.DefaultParams[name] = value
}
@@ -87,27 +78,27 @@ func parseProgramArgs(cf *Config) error {
} else if arg == "-m" || arg == "--mute" || arg == "--silent" || arg == "--no-notify" {
cf.Notify = false
} else {
return fmt.Errorf("illegal argument: %s", arg)
return cfs, fmt.Errorf("illegal argument: %s", arg)
}
} else {
// Convenience logic. If it contains a = then assume it's a parameter, otherwise assume it's a config file
if strings.Contains(arg, "=") {
if name, value, err := splitNV(arg); err != nil {
return fmt.Errorf("argument parameter is invalid: %s", err)
return cfs, fmt.Errorf("argument parameter is invalid: %s", err)
} else {
cf.DefaultParams[name] = value
}
} else {
// Assume it's a config file
var cf Config
if err := cf.LoadToml(arg); err != nil {
return fmt.Errorf("in %s: %s", arg, err)
return cfs, fmt.Errorf("in %s: %s", arg, err)
}
cfs = append(cfs, cf)
}
}
}
return nil
return cfs, nil
}

func printUsage() {
@@ -127,7 +118,7 @@ func printUsage() {
}

// Register the given rabbitMQ instance for the tui
func registerRabbitMQ(cf *Config, remote, topic string) (gopenqa.RabbitMQ, error) {
func registerRabbitMQ(model *TUIModel, remote, topic string) (gopenqa.RabbitMQ, error) {
rmq, err := gopenqa.ConnectRabbitMQ(remote)
if err != nil {
return rmq, fmt.Errorf("RabbitMQ connection error: %s", err)
@@ -137,47 +128,45 @@ func registerRabbitMQ(cf *Config, remote, topic string) (gopenqa.RabbitMQ, error
return rmq, fmt.Errorf("RabbitMQ subscribe error: %s", err)
}
// Receive function
go func(cf *Config) {
go func(model *TUIModel) {
cf := model.Config
for {
if status, err := sub.ReceiveJobStatus(); err == nil {
now := time.Now()
// Update job, if present
if job, found := cf.UpdateJobStatus(status); found {
tui.SetTracker(fmt.Sprintf("[%s] Job %d-%s:%s %s", now.Format("15:04:05"), job.ID, status.Flavor, status.Build, status.Result))
tui.Update()
if cf.Notify && !hideJob(job) {
NotifySend(fmt.Sprintf("%s: %s %s", job.JobState(), job.Name, job.Test))
}
} else {
name := status.Flavor
if status.Build != "" {
name += ":" + status.Build
}
tui.SetTracker(fmt.Sprintf("RabbitMQ: [%s] Foreign job %d-%s %s", now.Format("15:04:05"), job.ID, name, status.Result))
tui.Update()
// Check if we know this job or if this is just another job.
job := model.Job(status.ID)
if job.ID == 0 {
continue
}

tui.SetTracker(fmt.Sprintf("[%s] Job %d-%s:%s %s", now.Format("15:04:05"), job.ID, status.Flavor, status.Build, status.Result))
job.State = "done"
job.Result = fmt.Sprintf("%s", status.Result)

if cf.Notify && !model.HideJob(*job) {
NotifySend(fmt.Sprintf("%s: %s %s", job.JobState(), job.Name, job.Test))
}
}
}
}(cf)
}(model)
return rmq, err
}

func main() {
var defaultConfig Config
var err error
cfs = make([]Config, 0)
var cfs []Config

if defaultConfig, err = loadDefaultConfig(); err != nil {
fmt.Fprintf(os.Stderr, "Error loading default config file: %s\n", err)
os.Exit(1)
}
if err := parseProgramArgs(&defaultConfig); err != nil {
if cfs, err = parseProgramArgs(&defaultConfig); err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(1)
}

// If no configuration file has been added, use the default configuration
// This is needed for allowing configuration to be set via program parameters
// Use default configuration only if no configuration files are loaded.
if len(cfs) < 1 {
if err := defaultConfig.Validate(); err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err)
@@ -186,44 +175,62 @@ func main() {
cfs = append(cfs, defaultConfig)
}

cf := &cfs[cfi]

// Run TUI and use the return code
// Run terminal user interface from all available configuration objects
tui = CreateTUI()
// Apply sorting of the first group
switch cf.GroupBy {
case "none", "":
tui.SetSorting(0)
case "groups", "jobgroups":
tui.SetSorting(1)
default:
fmt.Fprintf(os.Stderr, "Unsupported GroupBy: '%s'\n", cf.GroupBy)
os.Exit(1)
for _, cf := range cfs {
model := tui.CreateTUIModel(&cf)

// Apply sorting of the first group
switch cf.GroupBy {
case "none", "":
model.SetSorting(0)
case "groups", "jobgroups":
model.SetSorting(1)
default:
fmt.Fprintf(os.Stderr, "Unsupported GroupBy: '%s'\n", cf.GroupBy)
os.Exit(1)
}
}

// Some settings get applied from the last available configuration
cf := cfs[len(cfs)-1]
tui.SetHideStatus(cf.HideStatus)

// Enter main loop
err = tui_main()
tui.LeaveAltScreen() // Ensure we leave alt screen
tui.LeaveAltScreen()
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(1)
}
}

func RefreshJobs() error {
model := tui.Model()

// Determine if a job is already known or not
knownJobs := model.jobs
getKnownJob := func(id int64) (gopenqa.Job, bool) {
for _, j := range knownJobs {
if j.ID == id {
return j, true
}
}
return gopenqa.Job{}, false
}

// Get fresh jobs
status := tui.Status()
oldJobs := tui.Model.Jobs()
oldJobs := model.Jobs()
tui.SetStatus(fmt.Sprintf("Refreshing %d jobs ... ", len(oldJobs)))
tui.Update()

cf := &cfs[cfi]

// Refresh all jobs at once in one request
ids := make([]int64, 0)
for _, job := range oldJobs {
ids = append(ids, job.ID)
}
jobs, err := fetchJobsFollow(ids, cf)
jobs, err := fetchJobsFollow(ids, model)
if err != nil {
return err
}
@@ -238,27 +245,26 @@ func RefreshJobs() error {
if updated {
status = fmt.Sprintf("Last update: [%s] Job %d-%s %s", time.Now().Format("15:04:05"), job.ID, job.Name, job.JobState())
tui.SetStatus(status)
tui.Model.Apply(jobs)
model.Apply(jobs)
tui.Update()
if cf.Notify && !hideJob(job) {
if model.Config.Notify && !model.HideJob(job) {
NotifySend(fmt.Sprintf("%s: %s %s", job.JobState(), job.Name, job.Test))
}
}
tui.Update()
// Scan failed jobs for comments
state := job.JobState()
if state == "failed" || state == "incomplete" || state == "parallel_failed" {
reviewed, err := isReviewed(job, cf.instance, state == "parallel_failed")
reviewed, err := isReviewed(job, model, state == "parallel_failed")
if err != nil {
return err
}

tui.Model.SetReviewed(job.ID, reviewed)
model.SetReviewed(job.ID, reviewed)
tui.Update()
}
}
cf.knownJobs = jobs
tui.Model.Apply(jobs)
model.Apply(jobs)
tui.SetStatus(status)
tui.Update()
return nil
@@ -277,11 +283,12 @@ func browserJobs(jobs []gopenqa.Job) error {
// main routine for the TUI instance
func tui_main() error {
title := "openqa Review TUI Dashboard v" + internal.VERSION
var rabbitmq gopenqa.RabbitMQ
var rabbitmqs []gopenqa.RabbitMQ
var err error

rabbitmqs = make([]gopenqa.RabbitMQ, 0)
refreshing := false
tui.Keypress = func(key byte) {
tui.Keypress = func(key byte, update *bool) {
// Input handling
switch key {
case 'r':
@@ -293,23 +300,21 @@ func tui_main() error {
}
refreshing = false
}()
tui.Update()
}
case 'u':
tui.Update()
// Pass, update is anyways happening
case 'q':
tui.done <- true
*update = false
case 'h':
tui.SetHide(!tui.Hide())
tui.Model.MoveHome()
tui.Update()
tui.Model().MoveHome()
case 'm':
tui.SetShowTracker(!tui.showTracker)
tui.Update()
case 's':
// Shift through the sorting mechanism
tui.SetSorting((tui.Sorting() + 1) % 2)
tui.Update()
model := tui.Model()
model.SetSorting((model.Sorting() + 1) % 2)
case 'o', 'O':
// Note: 'o' has a failsafe to not open more than 10 links. 'O' overrides this failsafe
jobs := tui.GetVisibleJobs()
@@ -325,9 +330,6 @@ func tui_main() error {
tui.SetStatus(fmt.Sprintf("Opened %d links", len(jobs)))
}
}
tui.Update()
default:
tui.Update()
}
}
tui.EnterAltScreen()
@@ -339,18 +341,17 @@ func tui_main() error {

fmt.Println(title)
fmt.Println("")
if len(cfs) == 0 {
cf := &cfs[0]
if len(tui.Tabs) == 0 {
model := &tui.Tabs[0]
cf := model.Config
fmt.Printf("Initial querying instance %s ... \n", cf.Instance)
} else {
fmt.Printf("Initial querying for %d configurations ... \n", len(cfs))
fmt.Printf("Initial querying for %d configurations ... \n", len(tui.Tabs))
}

for i, _ := range cfs {
cf := &cfs[i]
instance := gopenqa.CreateInstance(cf.Instance)
cf.instance = &instance
cf.instance.SetUserAgent("openqa-mon/revtui")
for i, _ := range tui.Tabs {
model := &tui.Tabs[i]
cf := model.Config

// Refresh rates below 5 minutes are not allowed on public instances due to the load it puts on them

@@ -360,17 +361,17 @@ func tui_main() error {
}
}

fmt.Printf("Initial querying instance %s for config %d/%d ... \n", cf.Instance, i+1, len(cfs))
cf.jobGroups, err = FetchJobGroups(cf.instance)
fmt.Printf("Initial querying instance %s for config %d/%d ... \n", cf.Instance, i+1, len(tui.Tabs))
model.jobGroups, err = FetchJobGroups(model.Instance)
if err != nil {
return fmt.Errorf("error fetching job groups: %s", err)
}
if len(cf.jobGroups) == 0 {
if len(model.jobGroups) == 0 {
fmt.Fprintf(os.Stderr, "Warn: No job groups\n")
}
fmt.Print("\033[s") // Save cursor position
fmt.Printf("\tGet jobs for %d groups ...", len(cf.Groups))
cf.jobs, err = FetchJobs(cf, func(group int, groups int, job int, jobs int) {
jobs, err := FetchJobs(model, func(group int, groups int, job int, jobs int) {
fmt.Print("\033[u") // Restore cursor position
fmt.Print("\033[K") // Erase till end of line
fmt.Printf("\tGet jobs for %d groups ... %d/%d", len(cf.Groups), group, groups)
@@ -380,58 +381,63 @@ func tui_main() error {
fmt.Printf(" (%d/%d jobs)", job, jobs)
}
})
model.Apply(jobs)
fmt.Println()
if err != nil {
return fmt.Errorf("error fetching jobs: %s", err)
}
if len(cf.jobs) == 0 {
if len(jobs) == 0 {
// No reason to continue - there are no jobs to scan
return fmt.Errorf("no jobs found")
}
// Failed jobs will be also scanned for comments
for _, job := range cf.jobs {
for _, job := range model.jobs {
state := job.JobState()
if state == "failed" || state == "incomplete" || state == "parallel_failed" {
reviewed, err := isReviewed(job, cf.instance, state == "parallel_failed")
reviewed, err := isReviewed(job, model, state == "parallel_failed")
if err != nil {
return fmt.Errorf("error fetching job comment: %s", err)
}
tui.Model.SetReviewed(job.ID, reviewed)
model.SetReviewed(job.ID, reviewed)
}
}
cf.knownJobs = cf.jobs

// Register RabbitMQ
if cf.RabbitMQ != "" {
rabbitmq, err = registerRabbitMQ(cf, cf.RabbitMQ, cf.RabbitMQTopic)
rabbitmq, err := registerRabbitMQ(model, cf.RabbitMQ, cf.RabbitMQTopic)
if err != nil {
fmt.Fprintf(os.Stderr, "Error establishing link to RabbitMQ %s: %s\n", rabbitRemote(cf.RabbitMQ), err)
}
defer rabbitmq.Close()
rabbitmqs = append(rabbitmqs, rabbitmq)
}
}
cf := &cfs[0] // Select primary configuration
tui.Model.Apply(cf.knownJobs)
fmt.Println("Initial fetching completed. Entering main loop ... ")
tui.Start()
tui.Update()

// Periodic refresh
if cf.RefreshInterval > 0 {
go func() {
for {
time.Sleep(time.Duration(cf.RefreshInterval) * time.Second)
if err := RefreshJobs(); err != nil {
tui.SetStatus(fmt.Sprintf("Error while refreshing: %s", err))
for i := range tui.Tabs {
model := &tui.Tabs[i]
interval := model.Config.RefreshInterval
if interval > 0 {
go func(currentTab int) {
for {
time.Sleep(time.Duration(interval) * time.Second)
// Only refresh, if current tab is ours
if tui.currentTab == currentTab {
if err := RefreshJobs(); err != nil {
tui.SetStatus(fmt.Sprintf("Error while refreshing: %s", err))
}
}
}
}
}()
}(i)
}
}

tui.awaitTerminationSignal()
tui.LeaveAltScreen()
if cf.RabbitMQ != "" {
rabbitmq.Close()
for i := range rabbitmqs {
rabbitmqs[i].Close()
}
return nil
}
46 changes: 18 additions & 28 deletions cmd/openqa-revtui/openqa.go
Original file line number Diff line number Diff line change
@@ -9,16 +9,6 @@ import (
"github.com/os-autoinst/gopenqa"
)

func hideJob(job gopenqa.Job) bool {
status := job.JobState()
for _, s := range cfs[cfi].HideStatus {
if status == s {
return true
}
}
return false
}

func isJobTooOld(job gopenqa.Job, maxlifetime int64) bool {
if maxlifetime <= 0 {
return false
@@ -34,16 +24,16 @@ func isJobTooOld(job gopenqa.Job, maxlifetime int64) bool {
return deltaT > maxlifetime
}

func isReviewed(job gopenqa.Job, instance *gopenqa.Instance, checkParallel bool) (bool, error) {
reviewed, err := checkReviewed(job.ID, instance)
func isReviewed(job gopenqa.Job, model *TUIModel, checkParallel bool) (bool, error) {
reviewed, err := checkReviewed(job.ID, model.Instance)
if err != nil || reviewed {
return reviewed, err
}

// If not reviewed but "parallel_failed", check parallel jobs if they are reviewed
if checkParallel {
for _, childID := range job.Children.Parallel {
reviewed, err := checkReviewed(childID, instance)
reviewed, err := checkReviewed(childID, model.Instance)
if err != nil {
return reviewed, err
}
@@ -108,13 +98,13 @@ func FetchJob(id int64, instance *gopenqa.Instance) (gopenqa.Job, error) {
}

/* Fetch the given jobs and follow their clones */
func fetchJobsFollow(ids []int64, cf *Config) ([]gopenqa.Job, error) {
func fetchJobsFollow(ids []int64, model *TUIModel) ([]gopenqa.Job, error) {
// Obey the maximum number of job per requests.
// We split the job ids into multiple requests if necessary
jobs := make([]gopenqa.Job, 0)
for len(ids) > 0 {
n := min(cf.RequestJobLimit, len(ids))
chunk, err := cf.instance.GetJobsFollow(ids[:n])
n := min(model.Config.RequestJobLimit, len(ids))
chunk, err := model.Instance.GetJobsFollow(ids[:n])
ids = ids[n:]
if err != nil {
return jobs, err
@@ -126,17 +116,17 @@ func fetchJobsFollow(ids []int64, cf *Config) ([]gopenqa.Job, error) {
}

/* Fetch the given jobs from the instance at once */
func fetchJobs(ids []int64, cf *Config) ([]gopenqa.Job, error) {
func fetchJobs(ids []int64, model *TUIModel) ([]gopenqa.Job, error) {
// Obey the maximum number of job per requests.
// We split the job ids into multiple requests if necessary
jobs := make([]gopenqa.Job, 0)
for len(ids) > 0 {
n := len(ids)
if cf.RequestJobLimit > 0 {
n = min(cf.RequestJobLimit, len(ids))
if model.Config.RequestJobLimit > 0 {
n = min(model.Config.RequestJobLimit, len(ids))
}
n = max(1, n)
chunk, err := cf.instance.GetJobs(ids[:n])
chunk, err := model.Instance.GetJobs(ids[:n])
ids = ids[n:]
if err != nil {
return jobs, err
@@ -147,7 +137,7 @@ func fetchJobs(ids []int64, cf *Config) ([]gopenqa.Job, error) {
// Get cloned jobs, if present
for i, job := range jobs {
if job.IsCloned() {
if job, err := FetchJob(job.ID, cf.instance); err != nil {
if job, err := FetchJob(job.ID, model.Instance); err != nil {
return jobs, err
} else {
jobs[i] = job
@@ -159,18 +149,18 @@ func fetchJobs(ids []int64, cf *Config) ([]gopenqa.Job, error) {

type FetchJobsCallback func(int, int, int, int)

func FetchJobs(cf *Config, callback FetchJobsCallback) ([]gopenqa.Job, error) {
func FetchJobs(model *TUIModel, callback FetchJobsCallback) ([]gopenqa.Job, error) {
ret := make([]gopenqa.Job, 0)
for i, group := range cf.Groups {
for i, group := range model.Config.Groups {
params := group.Params
jobs, err := cf.instance.GetOverview("", params)
jobs, err := model.Instance.GetOverview("", params)
if err != nil {
return ret, err
}

// Limit jobs to at most MaxJobs
if len(jobs) > cf.MaxJobs {
jobs = jobs[:cf.MaxJobs]
if len(jobs) > model.Config.MaxJobs {
jobs = jobs[:model.Config.MaxJobs]
}

// Get detailed job instances. Fetch them at once
@@ -180,9 +170,9 @@ func FetchJobs(cf *Config, callback FetchJobsCallback) ([]gopenqa.Job, error) {
}
if callback != nil {
// Add one to the counter to indicate the progress to humans (0/16 looks weird)
callback(i+1, len(cf.Groups), 0, len(jobs))
callback(i+1, len(model.Config.Groups), 0, len(jobs))
}
jobs, err = fetchJobs(ids, cf)
jobs, err = fetchJobs(ids, model)
if err != nil {
return jobs, err
}
185 changes: 112 additions & 73 deletions cmd/openqa-revtui/tui.go
Original file line number Diff line number Diff line change
@@ -8,7 +8,6 @@ import (
"os/exec"
"os/signal"
"sort"
"sync"
"syscall"
"time"
"unsafe"
@@ -38,23 +37,23 @@ type winsize struct {
Ypixel uint16
}

type KeyPressCallback func(byte)
type KeyPressCallback func(byte, *bool)

/* Declares the terminal user interface */
type TUI struct {
Model TUIModel
done chan bool
Tabs []TUIModel
done chan bool

Keypress KeyPressCallback

currentTab int // Currently selected tab
status string // Additional status text
tracker string // Additional tracker text for RabbitMQ messages
header string // Additional header text
hideStatus []string // Statuses to hide
hide bool // Hide statuses in hideStatus
showTracker bool // Show tracker
showStatus bool // Show status line
sorting int // Sorting method - 0: none, 1 - by job group

screensize int // Lines per screen
}
@@ -66,25 +65,27 @@ func CreateTUI() *TUI {
tui.hide = true
tui.showTracker = false
tui.showStatus = true
tui.Model.jobs = make([]gopenqa.Job, 0)
tui.Model.jobGroups = make(map[int]gopenqa.JobGroup, 0)
tui.Model.reviewed = make(map[int64]bool, 0)
tui.Tabs = make([]TUIModel, 0)
return &tui
}

/* The model that will be displayed in the TUI*/
type TUIModel struct {
Instance *gopenqa.Instance // openQA instance for this config
Config *Config // Job group configuration for this model

jobs []gopenqa.Job // Jobs to be displayed
jobGroups map[int]gopenqa.JobGroup // Job Groups
mutex sync.Mutex // Access mutex to the model
offset int // Line offset for printing
printLines int // Lines that would need to be printed, needed for offset handling
reviewed map[int64]bool // Indicating if failed jobs are reviewed
sorting int // Sorting method - 0: none, 1 - by job group
}

func (tui *TUI) GetVisibleJobs() []gopenqa.Job {
jobs := make([]gopenqa.Job, 0)
for _, job := range tui.Model.jobs {
model := &tui.Tabs[tui.currentTab]
for _, job := range model.jobs {
if !tui.hideJob(job) {
jobs = append(jobs, job)
}
@@ -96,26 +97,60 @@ func (model *TUIModel) SetReviewed(job int64, reviewed bool) {
model.reviewed[job] = reviewed
}

func (model *TUIModel) HideJob(job gopenqa.Job) bool {
status := job.JobState()
for _, s := range model.Config.HideStatus {
if status == s {
return true
}
}
return false
}

func (tui *TUIModel) MoveHome() {
tui.mutex.Lock()
defer tui.mutex.Unlock()
tui.offset = 0
}

func (tui *TUIModel) Apply(jobs []gopenqa.Job) {
tui.mutex.Lock()
defer tui.mutex.Unlock()
tui.jobs = jobs
}

func (model *TUIModel) Jobs() []gopenqa.Job {
return model.jobs
}

func (model *TUIModel) Job(id int64) *gopenqa.Job {
for i := range model.jobs {
if model.jobs[i].ID == id {
return &model.jobs[i]
}
}
// Return dummy job
job := gopenqa.Job{ID: 0}
return &job
}

func (tui *TUIModel) SetJobGroups(grps map[int]gopenqa.JobGroup) {
tui.jobGroups = grps
}

func (tui *TUI) NextTab() {
if len(tui.Tabs) > 1 {
tui.currentTab--
if tui.currentTab < 0 {
tui.currentTab = len(tui.Tabs) - 1
}
tui.Update()
}
}

func (tui *TUI) PreviousTab() {
if len(tui.Tabs) > 1 {
tui.currentTab = (tui.currentTab + 1) % len(tui.Tabs)
tui.Update()
}
}

func (tui *TUI) SetHide(hide bool) {
tui.hide = hide
}
@@ -129,30 +164,24 @@ func (tui *TUI) SetHideStatus(st []string) {
}

// Apply sorting method. 0 = none, 1 = by job group
func (tui *TUI) SetSorting(sorting int) {
tui.Model.mutex.Lock()
defer tui.Model.mutex.Unlock()
func (tui *TUIModel) SetSorting(sorting int) {
tui.sorting = sorting
}

func (tui *TUI) Sorting() int {
func (tui *TUIModel) Sorting() int {
return tui.sorting
}

func (tui *TUI) SetStatus(status string) {
tui.Model.mutex.Lock()
defer tui.Model.mutex.Unlock()
tui.status = status
}

func (tui *TUI) SetTemporaryStatus(status string, duration int) {
tui.Model.mutex.Lock()
old := tui.status
tui.status = status
tui.Model.mutex.Unlock()
tui.Update()

// Reset status text after waiting for duration. But only, if the status text has not been altered in the meantime
// Reset status text after duration, if the status text has not been altered in the meantime
go func(old, status string, duration int) {
time.Sleep(time.Duration(duration) * time.Second)
if tui.status == status {
@@ -167,14 +196,10 @@ func (tui *TUI) Status() string {
}

func (tui *TUI) SetTracker(tracker string) {
tui.Model.mutex.Lock()
defer tui.Model.mutex.Unlock()
tui.tracker = tracker
}

func (tui *TUI) SetShowTracker(tracker bool) {
tui.Model.mutex.Lock()
defer tui.Model.mutex.Unlock()
tui.showTracker = tracker
}

@@ -184,11 +209,25 @@ func (tui *TUI) ShowTracker() bool {
}

func (tui *TUI) SetHeader(header string) {
tui.Model.mutex.Lock()
defer tui.Model.mutex.Unlock()
tui.header = header
}

func (tui *TUI) CreateTUIModel(cf *Config) *TUIModel {
instance := gopenqa.CreateInstance(cf.Instance)
instance.SetUserAgent("openqa-mon/revtui")
tui.Tabs = append(tui.Tabs, TUIModel{Instance: &instance, Config: cf})
model := &tui.Tabs[len(tui.Tabs)-1]
model.jobGroups = make(map[int]gopenqa.JobGroup)
model.jobs = make([]gopenqa.Job, 0)
model.reviewed = make(map[int64]bool)
return model
}

// Model returns the currently selected model
func (tui *TUI) Model() *TUIModel {
return &tui.Tabs[tui.currentTab]
}

func (tui *TUI) readInput() {
var b []byte = make([]byte, 1)
var p = make([]byte, 3) // History, needed for special keys
@@ -199,6 +238,8 @@ func (tui *TUI) readInput() {
} else if n == 0 { // EOL
break
}
model := tui.Model()

k := b[0]

// Shift history, do it manually for now
@@ -208,51 +249,43 @@ func (tui *TUI) readInput() {
if p[2] == 27 && p[1] == 91 {
switch k {
case 65: // arrow up
if tui.Model.offset > 0 {
tui.Model.offset--
tui.Update()
if model.offset > 0 {
model.offset--
}

case 66: // arrow down
max := max(0, (tui.Model.printLines - tui.screensize))
if tui.Model.offset < max {
tui.Model.offset++
tui.Update()
max := max(0, (model.printLines - tui.screensize))
if model.offset < max {
model.offset++
}
case 72: // home
tui.Model.offset = 0
model.offset = 0
case 70: // end
tui.Model.offset = max(0, (tui.Model.printLines - tui.screensize))
model.offset = max(0, (model.printLines - tui.screensize))
case 53: // page up
// Always leave one line overlap for better orientation
tui.Model.offset = max(0, tui.Model.offset-tui.screensize+1)
model.offset = max(0, model.offset-tui.screensize+1)
case 54: // page down
max := max(0, (tui.Model.printLines - tui.screensize))
max := max(0, (model.printLines - tui.screensize))
// Always leave one line overlap for better orientation
tui.Model.offset = min(max, tui.Model.offset+tui.screensize-1)
model.offset = min(max, model.offset+tui.screensize-1)
case 90: // Shift+Tab
if len(cfs) > 1 {
cfi--
if cfi < 0 {
cfi = len(cfs) - 1
}
RefreshJobs()
tui.Update()
}
tui.PreviousTab()
}
}
// Default keys
if k == 9 { // Tab
if len(cfs) > 1 {
cfi = (cfi + 1) % len(cfs)
RefreshJobs()
tui.Update()
}
tui.NextTab()
}

update := true

// Forward keypress to listener
if tui.Keypress != nil {
tui.Keypress(k)
tui.Keypress(k, &update)
}

if update {
tui.Update()
}
}
}
@@ -303,14 +336,15 @@ func (tui *TUI) hideJob(job gopenqa.Job) bool {
return false
}
state := job.JobState()
model := &tui.Tabs[tui.currentTab]
for _, s := range tui.hideStatus {
if state == s {
return true
}

// Special reviewed keyword
if s == "reviewed" && (state == "failed" || state == "parallel_failed" || state == "incomplete") {
if reviewed, found := tui.Model.reviewed[job.ID]; found && reviewed {
if reviewed, found := model.reviewed[job.ID]; found && reviewed {
return true
}
}
@@ -321,7 +355,8 @@ func (tui *TUI) hideJob(job gopenqa.Job) bool {
// print all jobs unsorted
func (tui *TUI) buildJobsScreen(width int) []string {
lines := make([]string, 0)
for _, job := range tui.Model.jobs {
model := &tui.Tabs[tui.currentTab]
for _, job := range model.jobs {
if !tui.hideJob(job) {
lines = append(lines, tui.formatJobLine(job, width))
}
@@ -358,10 +393,11 @@ func jobGroupHeader(group gopenqa.JobGroup, width int) string {

func (tui *TUI) buildJobsScreenByGroup(width int) []string {
lines := make([]string, 0)
model := &tui.Tabs[tui.currentTab]

// Determine active groups first
groups := make(map[int][]gopenqa.Job, 0)
for _, job := range tui.Model.jobs {
for _, job := range model.jobs {
// Create item if not existing, then append job
if _, ok := groups[job.GroupID]; !ok {
groups[job.GroupID] = make([]gopenqa.Job, 0)
@@ -378,7 +414,7 @@ func (tui *TUI) buildJobsScreenByGroup(width int) []string {
// Now print them sorted by group ID
first := true
for _, id := range grpIDs {
grp := tui.Model.jobGroups[id]
grp := model.jobGroups[id]
jobs := groups[id]
statC := make(map[string]int, 0)
hidden := 0
@@ -397,7 +433,7 @@ func (tui *TUI) buildJobsScreenByGroup(width int) []string {
}
// Increase status counter
status := job.JobState()
if status == "failed" && tui.Model.reviewed[job.ID] {
if status == "failed" && model.reviewed[job.ID] {
status = "reviewed"
}
if c, exists := statC[status]; exists {
@@ -477,11 +513,12 @@ func (tui *TUI) buildHeader(_ int) []string {
lines = append(lines, tui.header)
lines = append(lines, "q:Quit r:Refresh h:Hide/Show jobs o:Open links m:Toggle RabbitMQ tracker s:Switch sorting Arrows:Move up/down")
// Tabs if multiple configs are present
if len(cfs) > 1 {
if len(tui.Tabs) > 1 {
tabs := ""
for i := range cfs {
enabled := (cfi == i)
cf := &cfs[i]
for i := range tui.Tabs {
enabled := (tui.currentTab == i)
model := &tui.Tabs[i]
cf := model.Config
name := cf.Name
if name == "" {
name = fmt.Sprintf("Config %d", i+1)
@@ -529,25 +566,25 @@ func (tui *TUI) buildFooter(width int) []string {
// Build the full screen
func (tui *TUI) buildScreen(width int) []string {
lines := make([]string, 0)
model := tui.Model()

switch tui.sorting {
switch model.sorting {
case 1:
lines = append(lines, tui.buildJobsScreenByGroup(width)...)
default:
lines = append(lines, tui.buildJobsScreen(width)...)
}
lines = trimEmpty(lines)

tui.Model.printLines = len(lines)
// We only scroll through the screen, so those are the relevant lines
model.printLines = len(lines)

return lines
}

/* Redraw screen */
func (tui *TUI) Update() {
tui.Model.mutex.Lock()
defer tui.Model.mutex.Unlock()
tui.Model.jobs = cfs[cfi].jobs
tui.Model.jobGroups = cfs[cfi].jobGroups
model := tui.Model()
width, height := terminalSize()
if width <= 0 || height <= 0 {
return
@@ -587,7 +624,7 @@ func (tui *TUI) Update() {

// Print screen
screensize := 0
for elem := tui.Model.offset; remainingLines > 0; remainingLines-- {
for elem := model.offset; remainingLines > 0; remainingLines-- {
if elem >= len(screen) {
fmt.Println("") // Fill screen with empty lines for alignment
} else {
@@ -646,6 +683,8 @@ func getDateColorcode(t time.Time) string {
}

func (tui *TUI) formatJobLine(job gopenqa.Job, width int) string {
model := &tui.Tabs[tui.currentTab]

c1 := ANSI_WHITE // date color
tStr := "" // Timestamp string

@@ -667,7 +706,7 @@ func (tui *TUI) formatJobLine(job gopenqa.Job, width int) string {
}
// For failed jobs check if they are reviewed
if state == "failed" || state == "incomplete" || state == "parallel_failed" {
if reviewed, found := tui.Model.reviewed[job.ID]; found && reviewed {
if reviewed, found := model.reviewed[job.ID]; found && reviewed {
c2 = ANSI_MAGENTA
state = "reviewed"
}

0 comments on commit edf648a

Please sign in to comment.