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 }