github.com/grahambrereton-form3/tilt@v0.10.18/internal/hud/resourceview.go (about)

     1  package hud
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  	"time"
     7  
     8  	"github.com/gdamore/tcell"
     9  
    10  	"github.com/windmilleng/tilt/internal/hud/view"
    11  	"github.com/windmilleng/tilt/internal/rty"
    12  	"github.com/windmilleng/tilt/pkg/model"
    13  )
    14  
    15  // These widths are determined experimentally, to see what shows up in a typical UX.
    16  const DeployCellMinWidth = 8
    17  const BuildDurCellMinWidth = 7
    18  const BuildStatusCellMinWidth = 8
    19  const MaxInlineErrHeight = 6
    20  
    21  type ResourceView struct {
    22  	res         view.Resource
    23  	rv          view.ResourceViewState
    24  	triggerMode model.TriggerMode
    25  	selected    bool
    26  
    27  	clock func() time.Time
    28  }
    29  
    30  func NewResourceView(res view.Resource, rv view.ResourceViewState, triggerMode model.TriggerMode,
    31  	selected bool, clock func() time.Time) *ResourceView {
    32  	return &ResourceView{
    33  		res:         res,
    34  		rv:          rv,
    35  		triggerMode: triggerMode,
    36  		selected:    selected,
    37  		clock:       clock,
    38  	}
    39  }
    40  
    41  func (v *ResourceView) Build() rty.Component {
    42  	layout := rty.NewConcatLayout(rty.DirVert)
    43  	layout.Add(v.resourceTitle())
    44  	if v.res.IsCollapsed(v.rv) {
    45  		return layout
    46  	}
    47  
    48  	layout.Add(v.resourceExpandedPane())
    49  	return layout
    50  }
    51  
    52  func (v *ResourceView) resourceTitle() rty.Component {
    53  	l := rty.NewConcatLayout(rty.DirHor)
    54  	l.Add(v.titleTextName())
    55  	l.Add(rty.TextString(" "))
    56  	l.AddDynamic(rty.Fg(rty.NewFillerString('╌'), cLightText))
    57  	l.Add(rty.TextString(" "))
    58  
    59  	if tt := v.titleText(); tt != nil {
    60  		l.Add(tt)
    61  		l.Add(middotText())
    62  	}
    63  
    64  	l.Add(v.titleTextBuild())
    65  	l.Add(middotText())
    66  	l.Add(v.titleTextDeploy())
    67  	return rty.OneLine(l)
    68  }
    69  
    70  func statusColor(res view.Resource) tcell.Color {
    71  	if res.IsTiltfile {
    72  		if !res.CurrentBuild.Empty() {
    73  			return cPending
    74  		} else if res.CrashLog.Empty() {
    75  			return cGood
    76  		} else {
    77  			return cBad
    78  		}
    79  	}
    80  
    81  	if !res.CurrentBuild.Empty() && !res.CurrentBuild.Reason.IsCrashOnly() {
    82  		return cPending
    83  	} else if !res.PendingBuildSince.IsZero() && !res.PendingBuildReason.IsCrashOnly() {
    84  		if res.TriggerMode.AutoOnChange() {
    85  			return cPending
    86  		} else {
    87  			return cLightText
    88  		}
    89  	} else if isCrashing(res) {
    90  		return cBad
    91  	} else if res.LastBuild().Error != nil {
    92  		return cBad
    93  	} else if res.IsYAML() && !res.LastDeployTime.IsZero() {
    94  		return cGood
    95  	} else if !res.LastBuild().FinishTime.IsZero() && res.ResourceInfo.Status() == "" {
    96  		return cPending // pod status hasn't shown up yet
    97  	} else {
    98  		if res.ResourceInfo != nil {
    99  			if statusColor, ok := statusColors[res.ResourceInfo.Status()]; ok {
   100  				return statusColor
   101  			}
   102  		}
   103  		return cLightText
   104  	}
   105  }
   106  
   107  func (v *ResourceView) titleTextName() rty.Component {
   108  	sb := rty.NewStringBuilder()
   109  	selected := v.selected
   110  
   111  	p := " "
   112  	if selected {
   113  		p = "▼"
   114  	}
   115  	if selected && v.res.IsCollapsed(v.rv) {
   116  		p = "▶"
   117  	}
   118  
   119  	color := statusColor(v.res)
   120  	sb.Text(p)
   121  	sb.Fg(color).Textf(" ● ")
   122  
   123  	name := v.res.Name.String()
   124  	if color == cPending {
   125  		name = fmt.Sprintf("%s %s", v.res.Name, v.spinner())
   126  	}
   127  	if len(warnings(v.res)) > 0 {
   128  		name = fmt.Sprintf("%s %s", v.res.Name, "— Warning ⚠️")
   129  	}
   130  	sb.Fg(tcell.ColorDefault).Text(name)
   131  	return sb.Build()
   132  }
   133  
   134  func (v *ResourceView) titleText() rty.Component {
   135  	switch i := v.res.ResourceInfo.(type) {
   136  	case view.DCResourceInfo:
   137  		return titleTextDC(i)
   138  	case view.K8sResourceInfo:
   139  		return titleTextK8s(i)
   140  	default:
   141  		return nil
   142  	}
   143  }
   144  
   145  func titleTextK8s(k8sInfo view.K8sResourceInfo) rty.Component {
   146  	status := k8sInfo.PodStatus
   147  	if status == "" {
   148  		status = "Pending"
   149  	}
   150  	return rty.TextString(status)
   151  }
   152  
   153  func titleTextDC(dcInfo view.DCResourceInfo) rty.Component {
   154  	sb := rty.NewStringBuilder()
   155  	status := dcInfo.Status()
   156  	if status == "" {
   157  		status = "Pending"
   158  	}
   159  	sb.Text(status)
   160  	return sb.Build()
   161  }
   162  
   163  func (v *ResourceView) titleTextBuild() rty.Component {
   164  	return buildStatusCell(makeBuildStatus(v.res, v.triggerMode))
   165  }
   166  
   167  func (v *ResourceView) titleTextDeploy() rty.Component {
   168  	return deployTimeCell(v.res.LastDeployTime, tcell.ColorDefault)
   169  }
   170  
   171  func (v *ResourceView) resourceExpandedPane() rty.Component {
   172  	l := rty.NewConcatLayout(rty.DirHor)
   173  	l.Add(rty.TextString(strings.Repeat(" ", 4)))
   174  
   175  	rhs := rty.NewConcatLayout(rty.DirVert)
   176  	rhs.Add(v.resourceExpandedHistory())
   177  	rhs.Add(v.resourceExpanded())
   178  	rhs.Add(v.resourceExpandedEndpoints())
   179  	rhs.Add(v.resourceExpandedError())
   180  	l.AddDynamic(rhs)
   181  	return l
   182  }
   183  
   184  func (v *ResourceView) resourceExpanded() rty.Component {
   185  	switch v.res.ResourceInfo.(type) {
   186  	case view.DCResourceInfo:
   187  		return v.resourceExpandedDC()
   188  	case view.K8sResourceInfo:
   189  		return v.resourceExpandedK8s()
   190  	case view.YAMLResourceInfo:
   191  		return v.resourceExpandedYAML()
   192  	default:
   193  		return rty.EmptyLayout
   194  	}
   195  }
   196  
   197  func (v *ResourceView) resourceExpandedYAML() rty.Component {
   198  	yi := v.res.YAMLInfo()
   199  
   200  	if !v.res.IsYAML() || len(yi.K8sResources) == 0 {
   201  		return rty.EmptyLayout
   202  	}
   203  
   204  	l := rty.NewConcatLayout(rty.DirHor)
   205  	l.Add(rty.TextString(strings.Repeat(" ", 2)))
   206  	rhs := rty.NewConcatLayout(rty.DirVert)
   207  	rhs.Add(rty.NewStringBuilder().Fg(cLightText).Text("(Kubernetes objects that don't match a group)").Build())
   208  	rhs.Add(rty.TextString(strings.Join(yi.K8sResources, "\n")))
   209  	l.AddDynamic(rhs)
   210  	return l
   211  }
   212  
   213  func (v *ResourceView) resourceExpandedDC() rty.Component {
   214  	dcInfo := v.res.DCInfo()
   215  
   216  	l := rty.NewConcatLayout(rty.DirHor)
   217  	l.Add(v.resourceTextDCContainer(dcInfo))
   218  	l.Add(rty.TextString(" "))
   219  	l.AddDynamic(rty.NewFillerString(' '))
   220  
   221  	st := v.res.DockerComposeTarget().StartTime
   222  	if !st.IsZero() {
   223  		if len(v.res.Endpoints) > 0 {
   224  			v.appendEndpoints(l)
   225  			l.Add(middotText())
   226  		}
   227  		l.Add(resourceTextAge(st))
   228  	}
   229  
   230  	return rty.OneLine(l)
   231  }
   232  
   233  func (v *ResourceView) resourceTextDCContainer(dcInfo view.DCResourceInfo) rty.Component {
   234  	if dcInfo.ContainerID.String() == "" {
   235  		return rty.EmptyLayout
   236  	}
   237  
   238  	sb := rty.NewStringBuilder()
   239  	sb.Fg(cLightText).Text("Container ID: ")
   240  	sb.Fg(tcell.ColorDefault).Text(dcInfo.ContainerID.ShortStr())
   241  	return sb.Build()
   242  }
   243  
   244  func (v *ResourceView) endpointsNeedSecondLine() bool {
   245  	if len(v.res.Endpoints) > 1 {
   246  		return true
   247  	}
   248  	if v.res.IsK8s() && v.res.K8sInfo().PodRestarts > 0 && len(v.res.Endpoints) == 1 {
   249  		return true
   250  	}
   251  	return false
   252  }
   253  
   254  func (v *ResourceView) resourceExpandedK8s() rty.Component {
   255  	k8sInfo := v.res.K8sInfo()
   256  	if k8sInfo.PodName == "" {
   257  		return rty.EmptyLayout
   258  	}
   259  
   260  	l := rty.NewConcatLayout(rty.DirHor)
   261  	l.Add(resourceTextPodName(k8sInfo))
   262  	l.Add(rty.TextString(" "))
   263  	l.AddDynamic(rty.NewFillerString(' '))
   264  	l.Add(rty.TextString(" "))
   265  
   266  	if k8sInfo.PodRestarts > 0 {
   267  		l.Add(resourceTextPodRestarts(k8sInfo))
   268  		l.Add(middotText())
   269  	}
   270  
   271  	if len(v.res.Endpoints) > 0 && !v.endpointsNeedSecondLine() {
   272  		v.appendEndpoints(l)
   273  		l.Add(middotText())
   274  	}
   275  
   276  	l.Add(resourceTextAge(k8sInfo.PodCreationTime))
   277  	return rty.OneLine(l)
   278  }
   279  
   280  func resourceTextPodName(k8sInfo view.K8sResourceInfo) rty.Component {
   281  	sb := rty.NewStringBuilder()
   282  	sb.Fg(cLightText).Text("K8S POD: ")
   283  	sb.Fg(tcell.ColorDefault).Text(k8sInfo.PodName)
   284  	return sb.Build()
   285  }
   286  
   287  func resourceTextPodRestarts(k8sInfo view.K8sResourceInfo) rty.Component {
   288  	s := "restarts"
   289  	if k8sInfo.PodRestarts == 1 {
   290  		s = "restart"
   291  	}
   292  	return rty.NewStringBuilder().
   293  		Fg(cPending).
   294  		Textf("%d %s", k8sInfo.PodRestarts, s).
   295  		Build()
   296  }
   297  
   298  func resourceTextAge(t time.Time) rty.Component {
   299  	sb := rty.NewStringBuilder()
   300  	sb.Fg(cLightText).Text("AGE ")
   301  	sb.Fg(tcell.ColorDefault).Text(formatDeployAge(time.Since(t)))
   302  	return rty.NewMinLengthLayout(DeployCellMinWidth, rty.DirHor).
   303  		SetAlign(rty.AlignEnd).
   304  		Add(sb.Build())
   305  }
   306  
   307  func (v *ResourceView) appendEndpoints(l *rty.ConcatLayout) {
   308  	for i, endpoint := range v.res.Endpoints {
   309  		if i != 0 {
   310  			l.Add(middotText())
   311  		}
   312  		l.Add(rty.TextString(endpoint))
   313  	}
   314  }
   315  
   316  func (v *ResourceView) resourceExpandedEndpoints() rty.Component {
   317  	if !v.endpointsNeedSecondLine() {
   318  		return rty.NewConcatLayout(rty.DirVert)
   319  	}
   320  
   321  	l := rty.NewConcatLayout(rty.DirHor)
   322  	l.Add(resourceTextURLPrefix())
   323  	v.appendEndpoints(l)
   324  
   325  	return l
   326  }
   327  
   328  func resourceTextURLPrefix() rty.Component {
   329  	sb := rty.NewStringBuilder()
   330  	sb.Fg(cLightText).Text("URL: ")
   331  	return sb.Build()
   332  }
   333  
   334  func (v *ResourceView) resourceExpandedHistory() rty.Component {
   335  	if v.res.IsYAML() {
   336  		return rty.NewConcatLayout(rty.DirVert)
   337  	}
   338  
   339  	if v.res.CurrentBuild.Empty() && len(v.res.BuildHistory) == 0 {
   340  		return rty.NewConcatLayout(rty.DirVert)
   341  	}
   342  
   343  	l := rty.NewConcatLayout(rty.DirHor)
   344  	l.Add(rty.NewStringBuilder().Fg(cLightText).Text("HISTORY: ").Build())
   345  
   346  	rows := rty.NewConcatLayout(rty.DirVert)
   347  	rowCount := 0
   348  	if !v.res.CurrentBuild.Empty() {
   349  		rows.Add(NewEditStatusLine(buildStatus{
   350  			edits:    v.res.CurrentBuild.Edits,
   351  			reason:   v.res.CurrentBuild.Reason,
   352  			duration: v.res.CurrentBuild.Duration(),
   353  			status:   "Building",
   354  			muted:    true,
   355  		}))
   356  		rowCount++
   357  	}
   358  	for _, bStatus := range v.res.BuildHistory {
   359  		if rowCount >= 2 {
   360  			// at most 2 rows
   361  			break
   362  		}
   363  
   364  		status := "OK"
   365  		if bStatus.Error != nil {
   366  			status = "Error"
   367  		}
   368  
   369  		rows.Add(NewEditStatusLine(buildStatus{
   370  			edits:      bStatus.Edits,
   371  			reason:     bStatus.Reason,
   372  			duration:   bStatus.Duration(),
   373  			status:     status,
   374  			deployTime: bStatus.FinishTime,
   375  		}))
   376  		rowCount++
   377  	}
   378  	l.AddDynamic(rows)
   379  	return l
   380  }
   381  
   382  func (v *ResourceView) resourceExpandedError() rty.Component {
   383  	errPane, ok := v.resourceExpandedBuildError()
   384  	isWarnings := false
   385  	if !ok {
   386  		errPane, ok = v.resourceExpandedRuntimeError()
   387  	}
   388  	if !ok {
   389  		errPane, ok = v.resourceExpandedWarnings()
   390  		if ok {
   391  			isWarnings = true
   392  		}
   393  	}
   394  
   395  	if !ok {
   396  		return rty.NewConcatLayout(rty.DirVert)
   397  	}
   398  
   399  	l := rty.NewConcatLayout(rty.DirVert)
   400  	if isWarnings {
   401  		l.Add(rty.NewStringBuilder().Fg(cLightText).Text("WARNINGS:").Build())
   402  	} else {
   403  		l.Add(rty.NewStringBuilder().Fg(cLightText).Text("ERROR:").Build())
   404  	}
   405  
   406  	indentPane := rty.NewConcatLayout(rty.DirHor)
   407  	indentPane.Add(rty.TextString(strings.Repeat(" ", 3)))
   408  
   409  	errPane = rty.NewTailLayout(errPane)
   410  	errPane = rty.NewMaxLengthLayout(errPane, rty.DirVert, MaxInlineErrHeight)
   411  	indentPane.Add(errPane)
   412  	l.Add(indentPane)
   413  
   414  	return l
   415  }
   416  
   417  func (v *ResourceView) resourceExpandedRuntimeError() (rty.Component, bool) {
   418  	pane := rty.NewConcatLayout(rty.DirVert)
   419  	ok := false
   420  	if isCrashing(v.res) {
   421  		runtimeLog := v.res.CrashLog.Tail(abbreviatedLogLineCount).String()
   422  		if runtimeLog == "" {
   423  			runtimeLog = v.res.ResourceInfo.RuntimeLog().Tail(abbreviatedLogLineCount).String()
   424  		}
   425  		abbrevLog := abbreviateLog(runtimeLog)
   426  		for _, logLine := range abbrevLog {
   427  			pane.Add(rty.TextString(logLine))
   428  			ok = true
   429  		}
   430  	}
   431  	return pane, ok
   432  }
   433  
   434  func (v *ResourceView) resourceExpandedWarnings() (rty.Component, bool) {
   435  	pane := rty.NewConcatLayout(rty.DirVert)
   436  	ok := false
   437  	if len(warnings(v.res)) > 0 {
   438  		abbrevLog := abbreviateLog(strings.Join(warnings(v.res), "\n"))
   439  		for _, logLine := range abbrevLog {
   440  			pane.Add(rty.TextString(logLine))
   441  			ok = true
   442  		}
   443  	}
   444  	return pane, ok
   445  }
   446  
   447  func (v *ResourceView) resourceExpandedBuildError() (rty.Component, bool) {
   448  	pane := rty.NewConcatLayout(rty.DirVert)
   449  	ok := false
   450  
   451  	if v.res.LastBuild().Error != nil {
   452  		abbrevLog := abbreviateLog(v.res.LastBuild().Log.Tail(abbreviatedLogLineCount).String())
   453  		for _, logLine := range abbrevLog {
   454  			pane.Add(rty.TextString(logLine))
   455  			ok = true
   456  		}
   457  
   458  		// if the build log is non-empty, it will contain the error, so we don't need to show this separately
   459  		if len(abbrevLog) == 0 {
   460  			pane.Add(rty.TextString(fmt.Sprintf("Error: %s", v.res.LastBuild().Error)))
   461  			ok = true
   462  		}
   463  	}
   464  
   465  	return pane, ok
   466  }
   467  
   468  var spinnerChars = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
   469  
   470  func (v *ResourceView) spinner() string {
   471  	decisecond := v.clock().Nanosecond() / int(time.Second/10)
   472  	return spinnerChars[decisecond%len(spinnerChars)] // tick spinner every 10x/second
   473  }