diff --git a/cmd/openqa-revtui/config.go b/cmd/openqa-revtui/config.go index 08ac112..e971724 100644 --- a/cmd/openqa-revtui/config.go +++ b/cmd/openqa-revtui/config.go @@ -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 -} diff --git a/cmd/openqa-revtui/openqa-revtui.go b/cmd/openqa-revtui/openqa-revtui.go index 64966e9..b8dbc8b 100644 --- a/cmd/openqa-revtui/openqa-revtui.go +++ b/cmd/openqa-revtui/openqa-revtui.go @@ -13,15 +13,6 @@ 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" @@ -29,12 +20,12 @@ func loadDefaultConfig() (Config, error) { 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,13 +78,13 @@ 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 } @@ -101,13 +92,13 @@ func parseProgramArgs(cf *Config) error { // 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,23 +175,30 @@ 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) @@ -210,20 +206,31 @@ func main() { } 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,9 +245,9 @@ 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)) } } @@ -248,17 +255,16 @@ func RefreshJobs() error { // 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 } diff --git a/cmd/openqa-revtui/openqa.go b/cmd/openqa-revtui/openqa.go index b5df904..21a9f8d 100644 --- a/cmd/openqa-revtui/openqa.go +++ b/cmd/openqa-revtui/openqa.go @@ -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,8 +24,8 @@ 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 } @@ -43,7 +33,7 @@ func isReviewed(job gopenqa.Job, instance *gopenqa.Instance, checkParallel bool) // 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 } diff --git a/cmd/openqa-revtui/tui.go b/cmd/openqa-revtui/tui.go index 7186f28..7d8c384 100644 --- a/cmd/openqa-revtui/tui.go +++ b/cmd/openqa-revtui/tui.go @@ -8,7 +8,6 @@ import ( "os/exec" "os/signal" "sort" - "sync" "syscall" "time" "unsafe" @@ -38,15 +37,16 @@ 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 @@ -54,7 +54,6 @@ type TUI struct { 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,15 +97,21 @@ 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 } @@ -112,10 +119,38 @@ 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,6 +336,7 @@ 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 @@ -310,7 +344,7 @@ func (tui *TUI) hideJob(job gopenqa.Job) bool { // 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,8 +566,9 @@ 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: @@ -538,16 +576,15 @@ func (tui *TUI) buildScreen(width int) []string { } 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" }