mirror of https://gitlab.com/gitlab-org/cli.git
feat: add support for child pipelines in ci view
This commit is contained in:
parent
35385c5c0d
commit
945febf505
|
@ -277,7 +277,7 @@ var PipelineJobWithSha = func(client *gitlab.Client, pid interface{}, sha, name
|
|||
if client == nil {
|
||||
client = apiClient.Lab()
|
||||
}
|
||||
jobs, err := PipelineJobsWithSha(client, pid, sha)
|
||||
jobs, _, err := PipelineJobsWithSha(client, pid, sha)
|
||||
if len(jobs) == 0 || err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -321,19 +321,22 @@ func (s JobSort) Less(i, j int) bool {
|
|||
return (*s.Jobs[i].CreatedAt).Before(*s.Jobs[j].CreatedAt)
|
||||
}
|
||||
|
||||
// PipelineJobsWithSha returns a list of jobs in a pipeline for a given commit sha.
|
||||
type BridgeSort struct {
|
||||
Bridges []*gitlab.Bridge
|
||||
}
|
||||
|
||||
func (s BridgeSort) Len() int { return len(s.Bridges) }
|
||||
func (s BridgeSort) Swap(i, j int) { s.Bridges[i], s.Bridges[j] = s.Bridges[j], s.Bridges[i] }
|
||||
func (s BridgeSort) Less(i, j int) bool {
|
||||
return (*s.Bridges[i].CreatedAt).Before(*s.Bridges[j].CreatedAt)
|
||||
}
|
||||
|
||||
// PipelineJobsWithID returns a list of jobs in a pipeline for a id.
|
||||
// The jobs are returned in the order in which they were created
|
||||
var PipelineJobsWithSha = func(client *gitlab.Client, pid interface{}, sha string) ([]*gitlab.Job, error) {
|
||||
var PipelineJobsWithID = func(client *gitlab.Client, pid interface{}, ppid int) ([]*gitlab.Job, []*gitlab.Bridge, error) {
|
||||
if client == nil {
|
||||
client = apiClient.Lab()
|
||||
}
|
||||
pipelines, err := GetPipelines(client, &gitlab.ListProjectPipelinesOptions{
|
||||
SHA: gitlab.String(sha),
|
||||
}, pid)
|
||||
if len(pipelines) == 0 || err != nil {
|
||||
return nil, err
|
||||
}
|
||||
target := pipelines[0].ID
|
||||
opts := &gitlab.ListJobsOptions{
|
||||
ListOptions: gitlab.ListOptions{
|
||||
PerPage: 500,
|
||||
|
@ -341,9 +344,9 @@ var PipelineJobsWithSha = func(client *gitlab.Client, pid interface{}, sha strin
|
|||
}
|
||||
jobsList := make([]*gitlab.Job, 0)
|
||||
for {
|
||||
jobs, resp, err := client.Jobs.ListPipelineJobs(pid, target, opts)
|
||||
jobs, resp, err := client.Jobs.ListPipelineJobs(pid, ppid, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
opts.Page = resp.NextPage
|
||||
jobsList = append(jobsList, jobs...)
|
||||
|
@ -351,19 +354,51 @@ var PipelineJobsWithSha = func(client *gitlab.Client, pid interface{}, sha strin
|
|||
break
|
||||
}
|
||||
}
|
||||
opts.Page = 0
|
||||
bridgesList := make([]*gitlab.Bridge, 0)
|
||||
for {
|
||||
bridges, resp, err := client.Jobs.ListPipelineBridges(pid, ppid, opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
opts.Page = resp.NextPage
|
||||
bridgesList = append(bridgesList, bridges...)
|
||||
if resp.CurrentPage == resp.TotalPages {
|
||||
break
|
||||
}
|
||||
}
|
||||
// ListPipelineJobs returns jobs sorted by ID in descending order instead of returning
|
||||
// them in the order they were created, so we restore the order using the createdAt
|
||||
sort.Sort(JobSort{Jobs: jobsList})
|
||||
return jobsList, nil
|
||||
sort.Sort(BridgeSort{Bridges: bridgesList})
|
||||
return jobsList, bridgesList, nil
|
||||
}
|
||||
|
||||
// PipelineJobsWithSha returns a list of jobs in a pipeline for a given commit sha.
|
||||
// The jobs are returned in the order in which they were created
|
||||
var PipelineJobsWithSha = func(client *gitlab.Client, pid interface{}, sha string) ([]*gitlab.Job, []*gitlab.Bridge, error) {
|
||||
if client == nil {
|
||||
client = apiClient.Lab()
|
||||
}
|
||||
pipelines, err := GetPipelines(client, &gitlab.ListProjectPipelinesOptions{
|
||||
SHA: gitlab.String(sha),
|
||||
}, pid)
|
||||
if len(pipelines) == 0 || err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return PipelineJobsWithID(client, pid, pipelines[0].ID)
|
||||
}
|
||||
|
||||
var ProjectNamespaceLint = func(client *gitlab.Client, projectID int, content string) (*gitlab.ProjectLintResult, error) {
|
||||
if client == nil {
|
||||
client = apiClient.Lab()
|
||||
}
|
||||
c, _, err := client.Validate.ProjectNamespaceLint(projectID, &gitlab.ProjectNamespaceLintOptions{
|
||||
Content: &content,
|
||||
})
|
||||
c, _, err := client.Validate.ProjectNamespaceLint(
|
||||
projectID,
|
||||
&gitlab.ProjectNamespaceLintOptions{
|
||||
Content: &content,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -39,6 +39,62 @@ type ViewOpts struct {
|
|||
Output io.Writer
|
||||
}
|
||||
|
||||
type ViewJobKind int64
|
||||
|
||||
const (
|
||||
Job ViewJobKind = iota
|
||||
Bridge
|
||||
)
|
||||
|
||||
type ViewJob struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
StartedAt *time.Time `json:"started_at"`
|
||||
FinishedAt *time.Time `json:"finished_at"`
|
||||
ErasedAt *time.Time `json:"erased_at"`
|
||||
Duration float64 `json:"duration"`
|
||||
Stage string `json:"stage"`
|
||||
Status string `json:"status"`
|
||||
AllowFailure bool `json:"allow_failure"`
|
||||
|
||||
Kind ViewJobKind
|
||||
|
||||
OriginalJob *gitlab.Job
|
||||
OriginalBridge *gitlab.Bridge
|
||||
}
|
||||
|
||||
func ViewJobFromBridge(bridge *gitlab.Bridge) *ViewJob {
|
||||
vj := &ViewJob{}
|
||||
vj.ID = bridge.ID
|
||||
vj.Name = bridge.Name
|
||||
vj.Status = bridge.Status
|
||||
vj.Stage = bridge.Stage
|
||||
vj.StartedAt = bridge.StartedAt
|
||||
vj.FinishedAt = bridge.FinishedAt
|
||||
vj.ErasedAt = bridge.ErasedAt
|
||||
vj.Duration = bridge.Duration
|
||||
vj.AllowFailure = bridge.AllowFailure
|
||||
vj.OriginalBridge = bridge
|
||||
vj.Kind = Bridge
|
||||
return vj
|
||||
}
|
||||
|
||||
func ViewJobFromJob(job *gitlab.Job) *ViewJob {
|
||||
vj := &ViewJob{}
|
||||
vj.ID = job.ID
|
||||
vj.Name = job.Name
|
||||
vj.Status = job.Status
|
||||
vj.Stage = job.Stage
|
||||
vj.StartedAt = job.StartedAt
|
||||
vj.FinishedAt = job.FinishedAt
|
||||
vj.ErasedAt = job.ErasedAt
|
||||
vj.Duration = job.Duration
|
||||
vj.AllowFailure = job.AllowFailure
|
||||
vj.OriginalJob = job
|
||||
vj.Kind = Job
|
||||
return vj
|
||||
}
|
||||
|
||||
func NewCmdView(f *cmdutils.Factory) *cobra.Command {
|
||||
opts := ViewOpts{}
|
||||
pipelineCIView := &cobra.Command{
|
||||
|
@ -48,7 +104,8 @@ func NewCmdView(f *cmdutils.Factory) *cobra.Command {
|
|||
|
||||
Use arrow keys to navigate jobs and logs.
|
||||
|
||||
'Enter' to toggle a job's logs or trace.
|
||||
'Enter' to toggle a job's logs or trace or display a child pipeline (trigger jobs are marked with a »).
|
||||
'Esc' or 'q' to close logs,trace or go back to the parent pipeline.
|
||||
'Ctrl+R', 'Ctrl+P' to run/retry/play a job -- Use Tab / Arrow keys to navigate modal and Enter to confirm.
|
||||
'Ctrl+C' to cancel job -- (Quits CI/CD view if selected job isn't running or pending).
|
||||
'Ctrl+Q' to Quit CI/CD View.
|
||||
|
@ -97,12 +154,14 @@ func NewCmdView(f *cmdutils.Factory) *cobra.Command {
|
|||
if opts.Commit.LastPipeline == nil {
|
||||
return fmt.Errorf("Can't find pipeline for commit : %s", opts.CommitSHA)
|
||||
}
|
||||
pipelines = make([]gitlab.PipelineInfo, 0, 10)
|
||||
|
||||
return drawView(opts)
|
||||
},
|
||||
}
|
||||
|
||||
pipelineCIView.Flags().StringVarP(&opts.RefName, "branch", "b", "", "Check pipeline status for a branch/tag. (Default is the current branch)")
|
||||
pipelineCIView.Flags().
|
||||
StringVarP(&opts.RefName, "branch", "b", "", "Check pipeline status for a branch/tag. (Default is the current branch)")
|
||||
return pipelineCIView
|
||||
}
|
||||
|
||||
|
@ -113,7 +172,8 @@ func drawView(opts ViewOpts) error {
|
|||
SetTitle(fmt.Sprintf(" Pipeline #%d triggered %s by %s ", opts.Commit.LastPipeline.ID, utils.TimeToPrettyTimeAgo(*opts.Commit.LastPipeline.CreatedAt), opts.Commit.AuthorName))
|
||||
|
||||
boxes = make(map[string]*tview.TextView)
|
||||
jobsCh := make(chan []*gitlab.Job)
|
||||
jobsCh := make(chan []*ViewJob)
|
||||
forceUpdateCh := make(chan bool)
|
||||
inputCh := make(chan struct{})
|
||||
|
||||
screen, err := tcell.NewScreen()
|
||||
|
@ -124,8 +184,8 @@ func drawView(opts ViewOpts) error {
|
|||
defer recoverPanic(app)
|
||||
|
||||
var navi navigator
|
||||
app.SetInputCapture(inputCapture(app, root, navi, inputCh, opts))
|
||||
go updateJobs(app, jobsCh, opts)
|
||||
app.SetInputCapture(inputCapture(app, root, navi, inputCh, forceUpdateCh, opts))
|
||||
go updateJobs(app, jobsCh, forceUpdateCh, opts)
|
||||
go func() {
|
||||
defer recoverPanic(app)
|
||||
for {
|
||||
|
@ -140,7 +200,14 @@ func drawView(opts ViewOpts) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func inputCapture(app *tview.Application, root *tview.Pages, navi navigator, inputCh chan struct{}, opts ViewOpts) func(event *tcell.EventKey) *tcell.EventKey {
|
||||
func inputCapture(
|
||||
app *tview.Application,
|
||||
root *tview.Pages,
|
||||
navi navigator,
|
||||
inputCh chan struct{},
|
||||
forceUpdateCh chan bool,
|
||||
opts ViewOpts,
|
||||
) func(event *tcell.EventKey) *tcell.EventKey {
|
||||
return func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Rune() == 'q' || event.Key() == tcell.KeyEscape {
|
||||
switch {
|
||||
|
@ -157,6 +224,11 @@ func inputCapture(app *tview.Application, root *tview.Pages, navi navigator, inp
|
|||
inputCh <- struct{}{}
|
||||
}
|
||||
app.ForceDraw()
|
||||
case len(pipelines) > 0:
|
||||
pipelines = pipelines[:len(pipelines)-1]
|
||||
curJob = nil
|
||||
forceUpdateCh <- true
|
||||
app.ForceDraw()
|
||||
default:
|
||||
app.Stop()
|
||||
return nil
|
||||
|
@ -174,7 +246,7 @@ func inputCapture(app *tview.Application, root *tview.Pages, navi navigator, inp
|
|||
app.Stop()
|
||||
return nil
|
||||
case tcell.KeyCtrlC:
|
||||
if curJob.Status == "pending" || curJob.Status == "running" {
|
||||
if curJob.Kind == Job && (curJob.Status == "pending" || curJob.Status == "running") {
|
||||
modalVisible = true
|
||||
modal := tview.NewModal().
|
||||
SetText(fmt.Sprintf("Are you sure you want to Cancel %s", curJob.Name)).
|
||||
|
@ -194,7 +266,7 @@ func inputCapture(app *tview.Application, root *tview.Pages, navi navigator, inp
|
|||
log.Fatal(err)
|
||||
}
|
||||
if job != nil {
|
||||
curJob = job
|
||||
curJob = ViewJobFromJob(job)
|
||||
app.ForceDraw()
|
||||
}
|
||||
})
|
||||
|
@ -204,7 +276,7 @@ func inputCapture(app *tview.Application, root *tview.Pages, navi navigator, inp
|
|||
return nil
|
||||
}
|
||||
case tcell.KeyCtrlP, tcell.KeyCtrlR:
|
||||
if modalVisible {
|
||||
if modalVisible || curJob.Kind != Job {
|
||||
break
|
||||
}
|
||||
modalVisible = true
|
||||
|
@ -221,13 +293,18 @@ func inputCapture(app *tview.Application, root *tview.Pages, navi navigator, inp
|
|||
root.RemovePage("logs-" + curJob.Name)
|
||||
app.ForceDraw()
|
||||
|
||||
job, err := api.PlayOrRetryJobs(opts.ApiClient, opts.ProjectID, curJob.ID, curJob.Status)
|
||||
job, err := api.PlayOrRetryJobs(
|
||||
opts.ApiClient,
|
||||
opts.ProjectID,
|
||||
curJob.ID,
|
||||
curJob.Status,
|
||||
)
|
||||
if err != nil {
|
||||
app.Stop()
|
||||
log.Fatal(err)
|
||||
}
|
||||
if job != nil {
|
||||
curJob = job
|
||||
curJob = ViewJobFromJob(job)
|
||||
app.ForceDraw()
|
||||
}
|
||||
})
|
||||
|
@ -237,19 +314,33 @@ func inputCapture(app *tview.Application, root *tview.Pages, navi navigator, inp
|
|||
return nil
|
||||
case tcell.KeyEnter:
|
||||
if !modalVisible {
|
||||
logsVisible = !logsVisible
|
||||
if !logsVisible {
|
||||
root.HidePage("logs-" + curJob.Name)
|
||||
if curJob.Kind == Job {
|
||||
logsVisible = !logsVisible
|
||||
if !logsVisible {
|
||||
root.HidePage("logs-" + curJob.Name)
|
||||
}
|
||||
inputCh <- struct{}{}
|
||||
app.ForceDraw()
|
||||
} else {
|
||||
pipelines = append(pipelines, *curJob.OriginalBridge.DownstreamPipeline)
|
||||
curJob = nil
|
||||
forceUpdateCh <- true
|
||||
app.ForceDraw()
|
||||
}
|
||||
inputCh <- struct{}{}
|
||||
app.ForceDraw()
|
||||
return nil
|
||||
}
|
||||
case tcell.KeyCtrlSpace:
|
||||
app.Suspend(func() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
err := ciutils.RunTraceSha(ctx, opts.ApiClient, opts.Output, opts.ProjectID, opts.CommitSHA, curJob.Name)
|
||||
err := ciutils.RunTraceSha(
|
||||
ctx,
|
||||
opts.ApiClient,
|
||||
opts.Output,
|
||||
opts.ProjectID,
|
||||
opts.CommitSHA,
|
||||
curJob.Name,
|
||||
)
|
||||
if err != nil {
|
||||
app.Stop()
|
||||
log.Fatal(err)
|
||||
|
@ -285,11 +376,19 @@ func inputCapture(app *tview.Application, root *tview.Pages, navi navigator, inp
|
|||
|
||||
var (
|
||||
logsVisible, modalVisible bool
|
||||
curJob *gitlab.Job
|
||||
jobs []*gitlab.Job
|
||||
curJob *ViewJob
|
||||
jobs []*ViewJob
|
||||
pipelines []gitlab.PipelineInfo
|
||||
boxes map[string]*tview.TextView
|
||||
)
|
||||
|
||||
func curPipeline(opts ViewOpts) gitlab.PipelineInfo {
|
||||
if len(pipelines) == 0 {
|
||||
return *opts.Commit.LastPipeline
|
||||
}
|
||||
return pipelines[len(pipelines)-1]
|
||||
}
|
||||
|
||||
// navigator manages the internal state for processing tcell.EventKeys
|
||||
type navigator struct {
|
||||
depth, idx int
|
||||
|
@ -297,7 +396,7 @@ type navigator struct {
|
|||
|
||||
// Navigate uses the ci stages as boundaries and returns the currently focused
|
||||
// job index after processing a *tcell.EventKey
|
||||
func (n *navigator) Navigate(jobs []*gitlab.Job, event *tcell.EventKey) *gitlab.Job {
|
||||
func (n *navigator) Navigate(jobs []*ViewJob, event *tcell.EventKey) *ViewJob {
|
||||
stage := jobs[n.idx].Stage
|
||||
prev, next := adjacentStages(jobs, stage)
|
||||
switch event.Key() {
|
||||
|
@ -347,7 +446,7 @@ func (n *navigator) Navigate(jobs []*gitlab.Job, event *tcell.EventKey) *gitlab.
|
|||
return jobs[n.idx]
|
||||
}
|
||||
|
||||
func stageBounds(jobs []*gitlab.Job, s string) (l, u int) {
|
||||
func stageBounds(jobs []*ViewJob, s string) (l, u int) {
|
||||
if len(jobs) <= 1 {
|
||||
return 0, 0
|
||||
}
|
||||
|
@ -367,7 +466,7 @@ func stageBounds(jobs []*gitlab.Job, s string) (l, u int) {
|
|||
return
|
||||
}
|
||||
|
||||
func adjacentStages(jobs []*gitlab.Job, s string) (p, n string) {
|
||||
func adjacentStages(jobs []*ViewJob, s string) (p, n string) {
|
||||
if len(jobs) == 0 {
|
||||
return "", ""
|
||||
}
|
||||
|
@ -389,7 +488,13 @@ func adjacentStages(jobs []*gitlab.Job, s string) (p, n string) {
|
|||
return
|
||||
}
|
||||
|
||||
func jobsView(app *tview.Application, jobsCh chan []*gitlab.Job, inputCh chan struct{}, root *tview.Pages, opts ViewOpts) {
|
||||
func jobsView(
|
||||
app *tview.Application,
|
||||
jobsCh chan []*ViewJob,
|
||||
inputCh chan struct{},
|
||||
root *tview.Pages,
|
||||
opts ViewOpts,
|
||||
) {
|
||||
select {
|
||||
case jobs = <-jobsCh:
|
||||
case <-inputCh:
|
||||
|
@ -412,7 +517,14 @@ func jobsView(app *tview.Application, jobsCh chan []*gitlab.Job, inputCh chan st
|
|||
tv.SetBorderPadding(0, 0, 1, 1).SetBorder(true)
|
||||
|
||||
go func() {
|
||||
err := ciutils.RunTraceSha(context.Background(), opts.ApiClient, vtclean.NewWriter(tview.ANSIWriter(tv), true), opts.ProjectID, opts.CommitSHA, curJob.Name)
|
||||
err := ciutils.RunTraceSha(
|
||||
context.Background(),
|
||||
opts.ApiClient,
|
||||
vtclean.NewWriter(tview.ANSIWriter(tv), true),
|
||||
opts.ProjectID,
|
||||
opts.CommitSHA,
|
||||
curJob.Name,
|
||||
)
|
||||
if err != nil {
|
||||
app.Stop()
|
||||
log.Fatal(err)
|
||||
|
@ -440,12 +552,14 @@ func jobsView(app *tview.Application, jobsCh chan []*gitlab.Job, inputCh chan st
|
|||
stageIdx int
|
||||
maxTitle = 20
|
||||
)
|
||||
boxKeys := make(map[string]bool)
|
||||
for _, j := range jobs {
|
||||
boxX := px + (maxX / stages * stageIdx)
|
||||
if j.Stage != lastStage {
|
||||
stageIdx++
|
||||
lastStage = j.Stage
|
||||
key := "stage-" + j.Stage
|
||||
boxKeys[key] = true
|
||||
|
||||
x, y, w, h := boxX, maxY/6-4, maxTitle+2, 3
|
||||
b := box(root, key, x, y, w, h)
|
||||
|
@ -453,7 +567,6 @@ func jobsView(app *tview.Application, jobsCh chan []*gitlab.Job, inputCh chan st
|
|||
caser := cases.Title(language.English)
|
||||
b.SetText(caser.String(j.Stage))
|
||||
b.SetTextAlign(tview.AlignCenter)
|
||||
|
||||
}
|
||||
}
|
||||
lastStage = jobs[0].Stage
|
||||
|
@ -468,6 +581,7 @@ func jobsView(app *tview.Application, jobsCh chan []*gitlab.Job, inputCh chan st
|
|||
boxX := px + (maxX / stages * stageIdx)
|
||||
|
||||
key := "jobs-" + j.Name
|
||||
boxKeys[key] = true
|
||||
x, y, w, h := boxX, maxY/6+(rowIdx*5), maxTitle+2, 4
|
||||
b := box(root, key, x, y, w, h)
|
||||
b.SetTitle(j.Name)
|
||||
|
@ -514,19 +628,29 @@ func jobsView(app *tview.Application, jobsCh chan []*gitlab.Job, inputCh chan st
|
|||
if tview.TaggedStringWidth(title) > maxTitle {
|
||||
b.SetTitleAlign(tview.AlignLeft)
|
||||
}
|
||||
triggerText := ""
|
||||
if j.Kind == Bridge {
|
||||
triggerText = "»"
|
||||
}
|
||||
if j.StartedAt != nil {
|
||||
end := time.Now()
|
||||
if j.FinishedAt != nil {
|
||||
end = *j.FinishedAt
|
||||
}
|
||||
b.SetText("\n" + utils.FmtDuration(end.Sub(*j.StartedAt)))
|
||||
b.SetText(triggerText + "\n" + utils.FmtDuration(end.Sub(*j.StartedAt)))
|
||||
b.SetTextAlign(tview.AlignRight)
|
||||
} else {
|
||||
b.SetText("")
|
||||
b.SetText(triggerText)
|
||||
}
|
||||
b.SetTextAlign(tview.AlignRight)
|
||||
rowIdx++
|
||||
|
||||
}
|
||||
for k := range boxes {
|
||||
if !boxKeys[k] {
|
||||
root.RemovePage(k)
|
||||
}
|
||||
}
|
||||
root.SendToFront("jobs-" + curJob.Name)
|
||||
}
|
||||
|
||||
|
@ -550,20 +674,44 @@ func recoverPanic(app *tview.Application) {
|
|||
}
|
||||
}
|
||||
|
||||
func updateJobs(app *tview.Application, jobsCh chan []*gitlab.Job, opts ViewOpts) {
|
||||
func updateJobs(
|
||||
app *tview.Application,
|
||||
jobsCh chan []*ViewJob,
|
||||
forceUpdateCh chan bool,
|
||||
opts ViewOpts,
|
||||
) {
|
||||
defer recoverPanic(app)
|
||||
for {
|
||||
if modalVisible {
|
||||
time.Sleep(time.Second * 1)
|
||||
continue
|
||||
}
|
||||
jobs, err := api.PipelineJobsWithSha(opts.ApiClient, opts.ProjectID, opts.CommitSHA)
|
||||
if len(jobs) == 0 || err != nil {
|
||||
var jobs []*gitlab.Job
|
||||
var bridges []*gitlab.Bridge
|
||||
var err error
|
||||
pipeline := curPipeline(opts)
|
||||
jobs, bridges, err = api.PipelineJobsWithID(
|
||||
opts.ApiClient,
|
||||
pipeline.ProjectID,
|
||||
pipeline.ID,
|
||||
)
|
||||
if (len(jobs) == 0 && len(bridges) == 0) || err != nil {
|
||||
app.Stop()
|
||||
log.Fatal(errors.Wrap(err, "failed to find ci jobs"))
|
||||
}
|
||||
jobsCh <- latestJobs(jobs)
|
||||
time.Sleep(time.Second * 5)
|
||||
viewJobs := make([]*ViewJob, 0, len(jobs)+len(bridges))
|
||||
for _, j := range jobs {
|
||||
viewJobs = append(viewJobs, ViewJobFromJob(j))
|
||||
}
|
||||
for _, b := range bridges {
|
||||
viewJobs = append(viewJobs, ViewJobFromBridge(b))
|
||||
}
|
||||
jobsCh <- latestJobs(viewJobs)
|
||||
select {
|
||||
case <-forceUpdateCh:
|
||||
case <-time.After(time.Second * 5):
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -578,7 +726,7 @@ func linkJobsView(app *tview.Application) func(screen tcell.Screen) {
|
|||
}
|
||||
}
|
||||
|
||||
func linkJobs(screen tcell.Screen, jobs []*gitlab.Job, boxes map[string]*tview.TextView) error {
|
||||
func linkJobs(screen tcell.Screen, jobs []*ViewJob, boxes map[string]*tview.TextView) error {
|
||||
if logsVisible || modalVisible {
|
||||
return nil
|
||||
}
|
||||
|
@ -615,7 +763,13 @@ func linkJobs(screen tcell.Screen, jobs []*gitlab.Job, boxes map[string]*tview.T
|
|||
return nil
|
||||
}
|
||||
|
||||
func link(screen tcell.Screen, v1 *tview.Box, v2 *tview.Box, padding int, firstStage, lastStage bool) {
|
||||
func link(
|
||||
screen tcell.Screen,
|
||||
v1 *tview.Box,
|
||||
v2 *tview.Box,
|
||||
padding int,
|
||||
firstStage, lastStage bool,
|
||||
) {
|
||||
x1, y1, w, h := v1.GetRect()
|
||||
x2, y2, _, _ := v2.GetRect()
|
||||
|
||||
|
@ -677,9 +831,9 @@ func vline(screen tcell.Screen, x, y, l int) {
|
|||
|
||||
// latestJobs returns a list of unique jobs favoring the last stage+name
|
||||
// version of a job in the provided list
|
||||
func latestJobs(jobs []*gitlab.Job) []*gitlab.Job {
|
||||
func latestJobs(jobs []*ViewJob) []*ViewJob {
|
||||
var (
|
||||
lastJob = make(map[string]*gitlab.Job, len(jobs))
|
||||
lastJob = make(map[string]*ViewJob, len(jobs))
|
||||
dupIdx = -1
|
||||
)
|
||||
for i, j := range jobs {
|
||||
|
@ -694,7 +848,7 @@ func latestJobs(jobs []*gitlab.Job) []*gitlab.Job {
|
|||
dupIdx = len(jobs)
|
||||
}
|
||||
// first duplicate marks where retries begin
|
||||
outJobs := make([]*gitlab.Job, dupIdx)
|
||||
outJobs := make([]*ViewJob, dupIdx)
|
||||
for i := range outJobs {
|
||||
j := jobs[i]
|
||||
outJobs[i] = lastJob[j.Stage+j.Name]
|
||||
|
|
|
@ -8,7 +8,6 @@ import (
|
|||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/xanzy/go-gitlab"
|
||||
)
|
||||
|
||||
func assertScreen(t *testing.T, screen tcell.Screen, expected []string) {
|
||||
|
@ -245,42 +244,51 @@ func Test_LinkJobs(t *testing.T) {
|
|||
" └─┘ ",
|
||||
" ",
|
||||
}
|
||||
jobs := []*gitlab.Job{
|
||||
jobs := []*ViewJob{
|
||||
{
|
||||
Name: "stage1-job1",
|
||||
Stage: "stage1",
|
||||
Kind: Job,
|
||||
},
|
||||
{
|
||||
Name: "stage1-job2",
|
||||
Stage: "stage1",
|
||||
Kind: Job,
|
||||
},
|
||||
{
|
||||
Name: "stage1-job3",
|
||||
Stage: "stage1",
|
||||
Kind: Job,
|
||||
},
|
||||
{
|
||||
Name: "stage1-job4",
|
||||
Stage: "stage1",
|
||||
Kind: Job,
|
||||
},
|
||||
{
|
||||
Name: "stage2-job1",
|
||||
Stage: "stage2",
|
||||
Kind: Bridge,
|
||||
},
|
||||
{
|
||||
Name: "stage2-job2",
|
||||
Stage: "stage2",
|
||||
Kind: Job,
|
||||
},
|
||||
{
|
||||
Name: "stage2-job3",
|
||||
Stage: "stage2",
|
||||
Kind: Job,
|
||||
},
|
||||
{
|
||||
Name: "stage3-job1",
|
||||
Stage: "stage3",
|
||||
Kind: Job,
|
||||
},
|
||||
{
|
||||
Name: "stage3-job2",
|
||||
Stage: "stage3",
|
||||
Kind: Job,
|
||||
},
|
||||
}
|
||||
boxes := map[string]*tview.TextView{
|
||||
|
@ -321,15 +329,16 @@ func Test_LinkJobs(t *testing.T) {
|
|||
func Test_LinkJobsNegative(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
jobs []*gitlab.Job
|
||||
jobs []*ViewJob
|
||||
boxes map[string]*tview.TextView
|
||||
}{
|
||||
{
|
||||
"determinePadding -- first job missing",
|
||||
[]*gitlab.Job{
|
||||
[]*ViewJob{
|
||||
{
|
||||
Name: "stage1-job1",
|
||||
Stage: "stage1",
|
||||
Kind: Job,
|
||||
},
|
||||
},
|
||||
map[string]*tview.TextView{
|
||||
|
@ -339,18 +348,21 @@ func Test_LinkJobsNegative(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"determinePadding -- second job missing",
|
||||
[]*gitlab.Job{
|
||||
[]*ViewJob{
|
||||
{
|
||||
Name: "stage1-job1",
|
||||
Stage: "stage1",
|
||||
Kind: Job,
|
||||
},
|
||||
{
|
||||
Name: "stage2-job1",
|
||||
Stage: "stage2",
|
||||
Kind: Job,
|
||||
},
|
||||
{
|
||||
Name: "stage2-job2",
|
||||
Stage: "stage2",
|
||||
Kind: Job,
|
||||
},
|
||||
},
|
||||
map[string]*tview.TextView{
|
||||
|
@ -360,18 +372,21 @@ func Test_LinkJobsNegative(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"Link -- third job missing",
|
||||
[]*gitlab.Job{
|
||||
[]*ViewJob{
|
||||
{
|
||||
Name: "stage1-job1",
|
||||
Stage: "stage1",
|
||||
Kind: Job,
|
||||
},
|
||||
{
|
||||
Name: "stage2-job1",
|
||||
Stage: "stage2",
|
||||
Kind: Job,
|
||||
},
|
||||
{
|
||||
Name: "stage2-job2",
|
||||
Stage: "stage2",
|
||||
Kind: Job,
|
||||
},
|
||||
},
|
||||
map[string]*tview.TextView{
|
||||
|
@ -381,18 +396,21 @@ func Test_LinkJobsNegative(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"Link -- third job missing",
|
||||
[]*gitlab.Job{
|
||||
[]*ViewJob{
|
||||
{
|
||||
Name: "stage1-job1",
|
||||
Stage: "stage1",
|
||||
Kind: Job,
|
||||
},
|
||||
{
|
||||
Name: "stage2-job1",
|
||||
Stage: "stage2",
|
||||
Kind: Job,
|
||||
},
|
||||
{
|
||||
Name: "stage2-job2",
|
||||
Stage: "stage2",
|
||||
Kind: Job,
|
||||
},
|
||||
},
|
||||
map[string]*tview.TextView{
|
||||
|
@ -427,7 +445,7 @@ func Test_jobsView(t *testing.T) {
|
|||
" ╚════════════════════╝ ║ ║ └────────────────────┘ ║ ║ └────────────────────┘ ",
|
||||
" ║ ║ ║ ║ ",
|
||||
" ┌───✔ stage1-job2────┐ ║ ║ ┌───● stage2-job2────┐ ║ ║ ┌───■ stage3-job2────┐ ",
|
||||
" │ │ ║ ║ │ │ ║ ║ │ │ ",
|
||||
" │ │ ║ ║ │ │ ║ ║ │ »│ ",
|
||||
" │ │═╝ ╠═│ │═╝ ╚═│ │ ",
|
||||
" └────────────────────┘ ║ ║ └────────────────────┘ ║ └────────────────────┘ ",
|
||||
" ║ ║ ║ ",
|
||||
|
@ -446,7 +464,7 @@ func Test_jobsView(t *testing.T) {
|
|||
}
|
||||
now := time.Now()
|
||||
past := now.Add(time.Second * -61)
|
||||
jobs := []*gitlab.Job{
|
||||
jobs := []*ViewJob{
|
||||
{
|
||||
Name: "stage1-job1-really-long",
|
||||
Stage: "stage1",
|
||||
|
@ -493,11 +511,12 @@ func Test_jobsView(t *testing.T) {
|
|||
Name: "stage3-job2",
|
||||
Stage: "stage3",
|
||||
Status: "manual",
|
||||
Kind: Bridge,
|
||||
},
|
||||
}
|
||||
|
||||
boxes = make(map[string]*tview.TextView)
|
||||
jobsCh := make(chan []*gitlab.Job)
|
||||
jobsCh := make(chan []*ViewJob)
|
||||
inputCh := make(chan struct{})
|
||||
root := tview.NewPages()
|
||||
root.SetBorderPadding(1, 1, 2, 2)
|
||||
|
@ -527,12 +546,12 @@ func Test_jobsView(t *testing.T) {
|
|||
func Test_latestJobs(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
jobs []*gitlab.Job
|
||||
expected []*gitlab.Job
|
||||
jobs []*ViewJob
|
||||
expected []*ViewJob
|
||||
}{
|
||||
{
|
||||
desc: "no newer jobs",
|
||||
jobs: []*gitlab.Job{
|
||||
jobs: []*ViewJob{
|
||||
{
|
||||
ID: 1,
|
||||
Name: "stage1-job1",
|
||||
|
@ -549,7 +568,7 @@ func Test_latestJobs(t *testing.T) {
|
|||
Stage: "stage1",
|
||||
},
|
||||
},
|
||||
expected: []*gitlab.Job{
|
||||
expected: []*ViewJob{
|
||||
{
|
||||
ID: 1,
|
||||
Name: "stage1-job1",
|
||||
|
@ -569,7 +588,7 @@ func Test_latestJobs(t *testing.T) {
|
|||
},
|
||||
{
|
||||
desc: "1 newer",
|
||||
jobs: []*gitlab.Job{
|
||||
jobs: []*ViewJob{
|
||||
{
|
||||
ID: 1,
|
||||
Name: "stage1-job1",
|
||||
|
@ -591,7 +610,7 @@ func Test_latestJobs(t *testing.T) {
|
|||
Stage: "stage1",
|
||||
},
|
||||
},
|
||||
expected: []*gitlab.Job{
|
||||
expected: []*ViewJob{
|
||||
{
|
||||
ID: 4,
|
||||
Name: "stage1-job1",
|
||||
|
@ -611,7 +630,7 @@ func Test_latestJobs(t *testing.T) {
|
|||
},
|
||||
{
|
||||
desc: "2 newer",
|
||||
jobs: []*gitlab.Job{
|
||||
jobs: []*ViewJob{
|
||||
{
|
||||
ID: 1,
|
||||
Name: "stage1-job1",
|
||||
|
@ -638,7 +657,7 @@ func Test_latestJobs(t *testing.T) {
|
|||
Stage: "stage1",
|
||||
},
|
||||
},
|
||||
expected: []*gitlab.Job{
|
||||
expected: []*ViewJob{
|
||||
{
|
||||
ID: 5,
|
||||
Name: "stage1-job1",
|
||||
|
@ -672,19 +691,19 @@ func Test_adjacentStages(t *testing.T) {
|
|||
tests := []struct {
|
||||
desc string
|
||||
stage string
|
||||
jobs []*gitlab.Job
|
||||
jobs []*ViewJob
|
||||
expectedPrev, expectedNext string
|
||||
}{
|
||||
{
|
||||
"no jobs",
|
||||
"1",
|
||||
[]*gitlab.Job{},
|
||||
[]*ViewJob{},
|
||||
"", "",
|
||||
},
|
||||
{
|
||||
"first stage",
|
||||
"1",
|
||||
[]*gitlab.Job{
|
||||
[]*ViewJob{
|
||||
{
|
||||
Stage: "1",
|
||||
},
|
||||
|
@ -703,7 +722,7 @@ func Test_adjacentStages(t *testing.T) {
|
|||
{
|
||||
"mid stage",
|
||||
"2",
|
||||
[]*gitlab.Job{
|
||||
[]*ViewJob{
|
||||
{
|
||||
Stage: "1",
|
||||
},
|
||||
|
@ -728,7 +747,7 @@ func Test_adjacentStages(t *testing.T) {
|
|||
{
|
||||
"last stage",
|
||||
"3",
|
||||
[]*gitlab.Job{
|
||||
[]*ViewJob{
|
||||
{
|
||||
Stage: "1",
|
||||
},
|
||||
|
@ -767,19 +786,19 @@ func Test_stageBounds(t *testing.T) {
|
|||
tests := []struct {
|
||||
desc string
|
||||
stage string
|
||||
jobs []*gitlab.Job
|
||||
jobs []*ViewJob
|
||||
expectedLower, expectedUpper int
|
||||
}{
|
||||
{
|
||||
"no jobs",
|
||||
"1",
|
||||
[]*gitlab.Job{},
|
||||
[]*ViewJob{},
|
||||
0, 0,
|
||||
},
|
||||
{
|
||||
"first stage",
|
||||
"1",
|
||||
[]*gitlab.Job{
|
||||
[]*ViewJob{
|
||||
{
|
||||
Stage: "1",
|
||||
},
|
||||
|
@ -798,7 +817,7 @@ func Test_stageBounds(t *testing.T) {
|
|||
{
|
||||
"mid stage",
|
||||
"2",
|
||||
[]*gitlab.Job{
|
||||
[]*ViewJob{
|
||||
{
|
||||
Stage: "1",
|
||||
},
|
||||
|
@ -823,7 +842,7 @@ func Test_stageBounds(t *testing.T) {
|
|||
{
|
||||
"last stage",
|
||||
"3",
|
||||
[]*gitlab.Job{
|
||||
[]*ViewJob{
|
||||
{
|
||||
Stage: "1",
|
||||
},
|
||||
|
@ -859,7 +878,7 @@ func Test_stageBounds(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_handleNavigation(t *testing.T) {
|
||||
jobs := []*gitlab.Job{
|
||||
jobs := []*ViewJob{
|
||||
{
|
||||
Name: "stage1-job1-really-long",
|
||||
Stage: "stage1",
|
||||
|
|
|
@ -19,7 +19,8 @@ Supports viewing, running, tracing, and canceling jobs.
|
|||
|
||||
Use arrow keys to navigate jobs and logs.
|
||||
|
||||
'Enter' to toggle a job's logs or trace.
|
||||
'Enter' to toggle a job's logs or trace or display a child pipeline (trigger jobs are marked with a »).
|
||||
'Esc' or 'q' to close logs,trace or go back to the parent pipeline.
|
||||
'Ctrl+R', 'Ctrl+P' to run/retry/play a job -- Use Tab / Arrow keys to navigate modal and Enter to confirm.
|
||||
'Ctrl+C' to cancel job -- (Quits CI/CD view if selected job isn't running or pending).
|
||||
'Ctrl+Q' to Quit CI/CD View.
|
||||
|
|
|
@ -19,7 +19,8 @@ Supports viewing, running, tracing, and canceling jobs.
|
|||
|
||||
Use arrow keys to navigate jobs and logs.
|
||||
|
||||
'Enter' to toggle a job's logs or trace.
|
||||
'Enter' to toggle a job's logs or trace or display a child pipeline (trigger jobs are marked with a »).
|
||||
'Esc' or 'q' to close logs,trace or go back to the parent pipeline.
|
||||
'Ctrl+R', 'Ctrl+P' to run/retry/play a job -- Use Tab / Arrow keys to navigate modal and Enter to confirm.
|
||||
'Ctrl+C' to cancel job -- (Quits CI/CD view if selected job isn't running or pending).
|
||||
'Ctrl+Q' to Quit CI/CD View.
|
||||
|
|
Loading…
Reference in New Issue