Switch to a new grid-view for the progress display. (#1201)
This commit is contained in:
parent
246aaba00e
commit
541b8b3f4e
3 changed files with 518 additions and 261 deletions
|
@ -165,6 +165,7 @@ func renderSummaryEvent(event engine.SummaryEventPayload, opts backend.DisplayOp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if c := changes[deploy.OpSame]; c > 0 {
|
if c := changes[deploy.OpSame]; c > 0 {
|
||||||
fprintfIgnoreError(out, " %v %v unchanged\n", c, plural("resource", c))
|
fprintfIgnoreError(out, " %v %v unchanged\n", c, plural("resource", c))
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,24 +30,12 @@ import (
|
||||||
"golang.org/x/crypto/ssh/terminal"
|
"golang.org/x/crypto/ssh/terminal"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Status helps us keep track for a resource as it is worked on by the engine.
|
type DiagInfo struct {
|
||||||
type Status struct {
|
ErrorCount, WarningCount, InfoCount, DebugCount int
|
||||||
// The simple short ID we have generated for the resource to present it to the user.
|
|
||||||
// Usually similar to the form: aws.Function("name")
|
|
||||||
ID string
|
|
||||||
|
|
||||||
// The change that the engine wants apply to that resource.
|
// The last event of each severity kind. We'll print out the most significant of these next
|
||||||
Step engine.StepEventMetadata
|
// to a resource while it is in progress.
|
||||||
|
LastError, LastWarning, LastInfoError, LastInfo, LastDebug *engine.Event
|
||||||
// The tick we were on when we created this status. Purely used for generating an
|
|
||||||
// ellipses to show progress for in-flight resources.
|
|
||||||
Tick int
|
|
||||||
|
|
||||||
// If the engine finished processing this resources.
|
|
||||||
Done bool
|
|
||||||
|
|
||||||
// If we failed this operation for any reason.
|
|
||||||
Failed bool
|
|
||||||
|
|
||||||
// All the diagnostic events we've heard about this resource. We'll print the last
|
// All the diagnostic events we've heard about this resource. We'll print the last
|
||||||
// diagnostic in the status region while a resource is in progress. At the end we'll
|
// diagnostic in the status region while a resource is in progress. At the end we'll
|
||||||
|
@ -58,12 +46,12 @@ type Status struct {
|
||||||
var (
|
var (
|
||||||
// simple regex to take our names like "aws:function:Function" and convert to
|
// simple regex to take our names like "aws:function:Function" and convert to
|
||||||
// "aws:Function"
|
// "aws:Function"
|
||||||
typeNameRegex = regexp.MustCompile("^(.*):(.*):(.*)$")
|
typeNameRegex = regexp.MustCompile("^(.*):(.*)/(.*):(.*)$")
|
||||||
)
|
)
|
||||||
|
|
||||||
func simplifyTypeName(typ tokens.Type) string {
|
func simplifyTypeName(typ tokens.Type) string {
|
||||||
typeString := string(typ)
|
typeString := string(typ)
|
||||||
return typeNameRegex.ReplaceAllString(typeString, "$1:$3")
|
return typeNameRegex.ReplaceAllString(typeString, "$1:$2:$4")
|
||||||
}
|
}
|
||||||
|
|
||||||
// getEventUrn returns the resource URN associated with an event, or the empty URN if this is not an
|
// getEventUrn returns the resource URN associated with an event, or the empty URN if this is not an
|
||||||
|
@ -110,55 +98,6 @@ func (display *ProgressDisplay) writeBlankLine() {
|
||||||
display.writeSimpleMessage(" ")
|
display.writeSimpleMessage(" ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the worst diagnostic we've seen, along with counts of all the diagnostic kinds. Used to
|
|
||||||
// produce a diagnostic string to go along with any resource if it has had any issues.
|
|
||||||
func getDiagnosticInformation(status Status) (
|
|
||||||
worstDiag *engine.Event, errorEvents, warningEvents, infoEvents, debugEvents int) {
|
|
||||||
|
|
||||||
errors := 0
|
|
||||||
warnings := 0
|
|
||||||
infos := 0
|
|
||||||
debugs := 0
|
|
||||||
|
|
||||||
var lastError, lastInfoError, lastWarning, lastInfo, lastDebug *engine.Event
|
|
||||||
|
|
||||||
for _, ev := range status.DiagEvents {
|
|
||||||
payload := ev.Payload.(engine.DiagEventPayload)
|
|
||||||
|
|
||||||
switch payload.Severity {
|
|
||||||
case diag.Error:
|
|
||||||
errors++
|
|
||||||
lastError = &ev
|
|
||||||
case diag.Warning:
|
|
||||||
warnings++
|
|
||||||
lastWarning = &ev
|
|
||||||
case diag.Infoerr:
|
|
||||||
infos++
|
|
||||||
lastInfoError = &ev
|
|
||||||
case diag.Info:
|
|
||||||
infos++
|
|
||||||
lastInfo = &ev
|
|
||||||
case diag.Debug:
|
|
||||||
debugs++
|
|
||||||
lastDebug = &ev
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if lastError != nil {
|
|
||||||
worstDiag = lastError
|
|
||||||
} else if lastWarning != nil {
|
|
||||||
worstDiag = lastWarning
|
|
||||||
} else if lastInfoError != nil {
|
|
||||||
worstDiag = lastInfoError
|
|
||||||
} else if lastInfo != nil {
|
|
||||||
worstDiag = lastInfo
|
|
||||||
} else {
|
|
||||||
worstDiag = lastDebug
|
|
||||||
}
|
|
||||||
|
|
||||||
return worstDiag, errors, warnings, infos, debugs
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProgressDisplay struct {
|
type ProgressDisplay struct {
|
||||||
opts backend.DisplayOptions
|
opts backend.DisplayOptions
|
||||||
progressOutput progress.Output
|
progressOutput progress.Output
|
||||||
|
@ -181,14 +120,15 @@ type ProgressDisplay struct {
|
||||||
// The length of the largest ID we've seen. We use this so we can align status messages per
|
// The length of the largest ID we've seen. We use this so we can align status messages per
|
||||||
// resource. i.e. status messages for shorter IDs will get passed with spaces so that
|
// resource. i.e. status messages for shorter IDs will get passed with spaces so that
|
||||||
// everything aligns.
|
// everything aligns.
|
||||||
maxIDLength int
|
maxColumnLengths []int
|
||||||
|
|
||||||
// What tick we're currently on. Used to determine the number of ellipses to concat to
|
// What tick we're currently on. Used to determine the number of ellipses to concat to
|
||||||
// a status message to help indicate that things are still working.
|
// a status message to help indicate that things are still working.
|
||||||
currentTick int
|
currentTick int
|
||||||
|
|
||||||
|
rows []Row
|
||||||
// A mapping from each resource URN we are told about to its current status.
|
// A mapping from each resource URN we are told about to its current status.
|
||||||
eventUrnToStatus map[resource.URN]Status
|
eventUrnToResourceRow map[resource.URN]ResourceRow
|
||||||
|
|
||||||
// Remember if we're a terminal or not. In a terminal we get a little bit fancier.
|
// Remember if we're a terminal or not. In a terminal we get a little bit fancier.
|
||||||
// For example, we'll go back and update previous status messages to make sure things
|
// For example, we'll go back and update previous status messages to make sure things
|
||||||
|
@ -200,7 +140,6 @@ type ProgressDisplay struct {
|
||||||
|
|
||||||
// Maps used so we can generate short IDs for resource urns.
|
// Maps used so we can generate short IDs for resource urns.
|
||||||
urnToID map[resource.URN]string
|
urnToID map[resource.URN]string
|
||||||
idToUrn map[string]resource.URN
|
|
||||||
|
|
||||||
// If all progress messages are done and we can print out the final display.
|
// If all progress messages are done and we can print out the final display.
|
||||||
Done bool
|
Done bool
|
||||||
|
@ -225,11 +164,10 @@ func DisplayProgressEvents(
|
||||||
progressOutput := streamformatter.NewJSONStreamFormatter().NewProgressOutput(pipeWriter, false)
|
progressOutput := streamformatter.NewJSONStreamFormatter().NewProgressOutput(pipeWriter, false)
|
||||||
|
|
||||||
display := &ProgressDisplay{
|
display := &ProgressDisplay{
|
||||||
opts: opts,
|
opts: opts,
|
||||||
progressOutput: progressOutput,
|
progressOutput: progressOutput,
|
||||||
eventUrnToStatus: make(map[resource.URN]Status),
|
eventUrnToResourceRow: make(map[resource.URN]ResourceRow),
|
||||||
urnToID: make(map[resource.URN]string),
|
urnToID: make(map[resource.URN]string),
|
||||||
idToUrn: make(map[string]resource.URN),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
display.initializeTermInfo()
|
display.initializeTermInfo()
|
||||||
|
@ -267,49 +205,24 @@ func (display *ProgressDisplay) initializeTermInfo() {
|
||||||
display.terminalWidth = terminalWidth
|
display.terminalWidth = terminalWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
func (display *ProgressDisplay) makeID(urn resource.URN) string {
|
|
||||||
makeSingleID := func(suffix int) string {
|
|
||||||
var id string
|
|
||||||
if urn == "" {
|
|
||||||
id = "global"
|
|
||||||
} else {
|
|
||||||
id = simplifyTypeName(urn.Type()) + "(\"" + string(urn.Name()) + "\")"
|
|
||||||
}
|
|
||||||
|
|
||||||
if suffix > 0 {
|
|
||||||
id += fmt.Sprintf("-%v", suffix)
|
|
||||||
}
|
|
||||||
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
if id, has := display.urnToID[urn]; !has {
|
|
||||||
for i := 0; ; i++ {
|
|
||||||
id = makeSingleID(i)
|
|
||||||
|
|
||||||
if _, has = display.idToUrn[id]; !has {
|
|
||||||
display.urnToID[urn] = id
|
|
||||||
display.idToUrn[id] = urn
|
|
||||||
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gets the padding necessary to prepend to a message in order to keep it aligned in the
|
// Gets the padding necessary to prepend to a message in order to keep it aligned in the
|
||||||
// terminal.
|
// terminal.
|
||||||
func (display *ProgressDisplay) getMessagePadding(status Status) string {
|
func (display *ProgressDisplay) getMessagePadding(uncolorizedColumns []string, columnIndex int) string {
|
||||||
extraWhitespace := 0
|
extraWhitespace := 1
|
||||||
|
|
||||||
// In the terminal we try to align the status messages for each resource.
|
// In the terminal we try to align the status messages for each resource.
|
||||||
// do not bother with this in the non-terminal case.
|
// do not bother with this in the non-terminal case.
|
||||||
if display.isTerminal {
|
if display.isTerminal {
|
||||||
id := status.ID
|
column := uncolorizedColumns[columnIndex]
|
||||||
extraWhitespace = display.maxIDLength - len(id)
|
maxIDLength := display.maxColumnLengths[columnIndex]
|
||||||
contract.Assertf(extraWhitespace >= 0, "Neg whitespace. %v %s", display.maxIDLength, id)
|
extraWhitespace = maxIDLength - len(column)
|
||||||
|
contract.Assertf(extraWhitespace >= 0, "Neg whitespace. %v %s", maxIDLength, column)
|
||||||
|
|
||||||
|
if columnIndex > 0 {
|
||||||
|
// docker puts an extra whitespace one the first column. We replicate that for all
|
||||||
|
// other columns
|
||||||
|
extraWhitespace++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.Repeat(" ", extraWhitespace)
|
return strings.Repeat(" ", extraWhitespace)
|
||||||
|
@ -319,99 +232,105 @@ func (display *ProgressDisplay) getMessagePadding(status Status) string {
|
||||||
// status, then some amount of optional padding, then some amount of msgWithColors, then the
|
// status, then some amount of optional padding, then some amount of msgWithColors, then the
|
||||||
// suffix. Importantly, if there isn't enough room to display all of that on the terminal, then
|
// suffix. Importantly, if there isn't enough room to display all of that on the terminal, then
|
||||||
// the msg will be truncated to try to make it fit.
|
// the msg will be truncated to try to make it fit.
|
||||||
func (display *ProgressDisplay) getPaddedMessage(status Status, msgWithColors string, suffix string) string {
|
func (display *ProgressDisplay) getPaddedMessage(
|
||||||
id := status.ID
|
colorizedColumns, uncolorizedColumns []string,
|
||||||
padding := display.getMessagePadding(status)
|
colorizedSuffix, uncolorizedSuffix string) string {
|
||||||
|
|
||||||
|
colorizedMessage := ""
|
||||||
|
|
||||||
|
// Figure out the last column that is non-empty. That's where we'll add the suffix to.
|
||||||
|
lastNonEmptyColumn := len(uncolorizedColumns)
|
||||||
|
for lastNonEmptyColumn > 0 && uncolorizedColumns[lastNonEmptyColumn-1] == "" {
|
||||||
|
lastNonEmptyColumn--
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 1; i < lastNonEmptyColumn; i++ {
|
||||||
|
padding := display.getMessagePadding(uncolorizedColumns, i-1)
|
||||||
|
colorizedColumn := padding + colorizedColumns[i]
|
||||||
|
colorizedMessage += colorizedColumn
|
||||||
|
}
|
||||||
|
|
||||||
// In the terminal, only include the first line of the message
|
// In the terminal, only include the first line of the message
|
||||||
if display.isTerminal {
|
if display.isTerminal {
|
||||||
newLineIndex := strings.Index(msgWithColors, "\n")
|
newLineIndex := strings.Index(colorizedMessage, "\n")
|
||||||
if newLineIndex >= 0 {
|
if newLineIndex >= 0 {
|
||||||
msgWithColors = msgWithColors[0:newLineIndex]
|
colorizedMessage = colorizedMessage[0:newLineIndex]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we don't go past the end of the terminal. Note: this is made complex due to
|
// Ensure we don't go past the end of the terminal. Note: this is made complex due to
|
||||||
// msgWithColors having the color code information embedded with it. So we need to get
|
// msgWithColors having the color code information embedded with it. So we need to get
|
||||||
// the right substring of it, assuming that embedded colors are just markup and do not
|
// the right substring of it, assuming that embedded colors are just markup and do not
|
||||||
// actually contribute to the length
|
// actually contribute to the length
|
||||||
maxMsgLength := display.terminalWidth - len(id) - len(":") - len(padding) - len(suffix) - 1
|
id := uncolorizedColumns[0]
|
||||||
|
maxMsgLength := display.terminalWidth - len(id) - len(": ") - len(uncolorizedSuffix) - 1
|
||||||
if maxMsgLength < 0 {
|
if maxMsgLength < 0 {
|
||||||
maxMsgLength = 0
|
maxMsgLength = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
msgWithColors = colors.TrimColorizedString(msgWithColors, maxMsgLength)
|
colorizedMessage = colors.TrimColorizedString(colorizedMessage, maxMsgLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
return padding + msgWithColors + suffix
|
return colorizedMessage + colorizedSuffix
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets the single line summary to show for a resource. This will include the current state of
|
func (display *ProgressDisplay) refreshSingleRow(row Row) {
|
||||||
// the resource (i.e. "Creating", "Replaced", "Failed", etc.) as well as relevant diagnostic
|
colorizedColumns := row.ColorizedColumns()
|
||||||
// information if there is any.
|
uncolorizedColumns := row.UncolorizedColumns()
|
||||||
func (display *ProgressDisplay) getUnpaddedStatusSummary(status Status) string {
|
colorizedSuffix := row.ColorizedSuffix()
|
||||||
if status.Step.Op == "" {
|
uncolorizedSuffix := colors.Never.Colorize(colorizedSuffix)
|
||||||
contract.Failf("Finishing a resource we never heard about: '%s'", status.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
worstDiag, errors, warnings, infos, debugs := getDiagnosticInformation(status)
|
msg := display.getPaddedMessage(
|
||||||
|
colorizedColumns, uncolorizedColumns, colorizedSuffix, uncolorizedSuffix)
|
||||||
failed := status.Failed || errors > 0
|
|
||||||
msg := display.getMetadataSummary(status.Step, status.Done, failed)
|
|
||||||
|
|
||||||
if errors > 0 {
|
|
||||||
msg += fmt.Sprintf(", %v error(s)", errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
if warnings > 0 {
|
|
||||||
msg += fmt.Sprintf(", %v warning(s)", warnings)
|
|
||||||
}
|
|
||||||
|
|
||||||
if infos > 0 {
|
|
||||||
msg += fmt.Sprintf(", %v info message(s)", infos)
|
|
||||||
}
|
|
||||||
|
|
||||||
if debugs > 0 {
|
|
||||||
msg += fmt.Sprintf(", %v debug message(s)", debugs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we're not totally done, also print out the worst diagnostic next to the status message.
|
|
||||||
// This is helpful for long running tasks to know what's going on. However, once done, we print
|
|
||||||
// the diagnostics at the bottom, so we don't need to show this.
|
|
||||||
if worstDiag != nil && !display.Done {
|
|
||||||
diagMsg := display.renderProgressDiagEvent(*worstDiag)
|
|
||||||
if diagMsg != "" {
|
|
||||||
msg += ". " + diagMsg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return msg
|
|
||||||
}
|
|
||||||
|
|
||||||
var ellipsesArray = []string{"", ".", "..", "..."}
|
|
||||||
|
|
||||||
func (display *ProgressDisplay) refreshSingleStatusMessage(status Status) {
|
|
||||||
unpaddedMsg := display.getUnpaddedStatusSummary(status)
|
|
||||||
suffix := ""
|
|
||||||
|
|
||||||
if !status.Done {
|
|
||||||
suffix = ellipsesArray[(status.Tick+display.currentTick)%len(ellipsesArray)]
|
|
||||||
}
|
|
||||||
|
|
||||||
msg := display.getPaddedMessage(status, unpaddedMsg, suffix)
|
|
||||||
|
|
||||||
display.colorizeAndWriteProgress(progress.Progress{
|
display.colorizeAndWriteProgress(progress.Progress{
|
||||||
ID: status.ID,
|
ID: display.opts.Color.Colorize(colorizedColumns[0]),
|
||||||
Action: msg,
|
Action: msg,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (display *ProgressDisplay) refreshAllStatusMessages(includeDone bool) {
|
// Ensure our stored dimension info is up to date. Returns 'true' if the stored dimension info is
|
||||||
for _, v := range display.eventUrnToStatus {
|
// updated.
|
||||||
if v.Done && !includeDone {
|
func (display *ProgressDisplay) updateDimensions() bool {
|
||||||
continue
|
updated := false
|
||||||
|
|
||||||
|
// don't do any refreshing if we're not in a terminal
|
||||||
|
if display.isTerminal {
|
||||||
|
currentTerminalWidth, _, err := terminal.GetSize(int(os.Stdout.Fd()))
|
||||||
|
contract.IgnoreError(err)
|
||||||
|
|
||||||
|
if currentTerminalWidth != display.terminalWidth {
|
||||||
|
// terminal width changed. Refresh everything
|
||||||
|
display.terminalWidth = currentTerminalWidth
|
||||||
|
updated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
display.refreshSingleStatusMessage(v)
|
for _, row := range display.rows {
|
||||||
|
columns := row.UncolorizedColumns()
|
||||||
|
|
||||||
|
if len(display.maxColumnLengths) == 0 {
|
||||||
|
display.maxColumnLengths = make([]int, len(columns))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, column := range columns {
|
||||||
|
if len(column) > display.maxColumnLengths[i] {
|
||||||
|
display.maxColumnLengths[i] = len(column)
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated
|
||||||
|
}
|
||||||
|
|
||||||
|
func (display *ProgressDisplay) refreshAllRowsIfInTerminal() {
|
||||||
|
if display.isTerminal {
|
||||||
|
// make sure our stored dimension info is up to date
|
||||||
|
display.updateDimensions()
|
||||||
|
|
||||||
|
for _, row := range display.rows {
|
||||||
|
display.refreshSingleRow(row)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -421,25 +340,37 @@ func (display *ProgressDisplay) refreshAllStatusMessages(includeDone bool) {
|
||||||
func (display *ProgressDisplay) processEndSteps() {
|
func (display *ProgressDisplay) processEndSteps() {
|
||||||
display.Done = true
|
display.Done = true
|
||||||
|
|
||||||
// Mark all in progress resources as done.
|
for _, v := range display.eventUrnToResourceRow {
|
||||||
for k, v := range display.eventUrnToStatus {
|
// transition everything to the done state. If we're not in an a terminal and this is a
|
||||||
if !v.Done {
|
// transition, then print out the transition. Don't bother doing this in a terminal as
|
||||||
v.Done = true
|
// we're going to refresh everything when we break out of the loop.
|
||||||
display.eventUrnToStatus[k] = v
|
if !v.Done() {
|
||||||
|
v.SetDone()
|
||||||
|
|
||||||
|
if !display.isTerminal {
|
||||||
|
display.refreshSingleRow(v)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Explicitly transition the status so that we clear out any cached data for it.
|
||||||
|
v.SetDone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure we refresh everything. That way we remove any diagnostics printed on the line.
|
// Now refresh everything. this ensures that we go back and remove things like the diagnostic
|
||||||
display.refreshAllStatusMessages(true /*includeDone*/)
|
// messages from a status message (since we're going to print them all) below. Note, this will
|
||||||
|
// only do something in a terminal. This i what we want, because if we're not in a terminal we
|
||||||
|
// don't really want to reprint any finished items we've already printed.
|
||||||
|
display.refreshAllRowsIfInTerminal()
|
||||||
|
|
||||||
// Print all diagnostics we've seen.
|
// Print all diagnostics we've seen.
|
||||||
|
|
||||||
wroteDiagnosticHeader := false
|
wroteDiagnosticHeader := false
|
||||||
|
|
||||||
for _, status := range display.eventUrnToStatus {
|
for _, row := range display.eventUrnToResourceRow {
|
||||||
if len(status.DiagEvents) > 0 {
|
events := row.DiagInfo().DiagEvents
|
||||||
|
if len(events) > 0 {
|
||||||
wroteResourceHeader := false
|
wroteResourceHeader := false
|
||||||
for _, v := range status.DiagEvents {
|
for _, v := range events {
|
||||||
msg := display.renderProgressDiagEvent(v)
|
msg := display.renderProgressDiagEvent(v)
|
||||||
|
|
||||||
lines := strings.Split(msg, "\n")
|
lines := strings.Split(msg, "\n")
|
||||||
|
@ -466,7 +397,11 @@ func (display *ProgressDisplay) processEndSteps() {
|
||||||
|
|
||||||
if !wroteResourceHeader {
|
if !wroteResourceHeader {
|
||||||
wroteResourceHeader = true
|
wroteResourceHeader = true
|
||||||
display.writeSimpleMessage(" " + status.ID + ":")
|
columns := row.ColorizedColumns()
|
||||||
|
display.writeSimpleMessage(" " +
|
||||||
|
columns[idColumn] + ": " +
|
||||||
|
columns[typeColumn] + ": " +
|
||||||
|
columns[nameColumn])
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
|
@ -489,17 +424,11 @@ func (display *ProgressDisplay) processEndSteps() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (display *ProgressDisplay) processTick() {
|
func (display *ProgressDisplay) processTick() {
|
||||||
// Got a tick. Update all the in-progress resources.
|
// Got a tick. Update all resources if we're in a terminal. If we're not, then this won't do
|
||||||
|
// anything.
|
||||||
display.currentTick++
|
display.currentTick++
|
||||||
|
|
||||||
currentTerminalWidth, _, _ := terminal.GetSize(int(os.Stdout.Fd()))
|
display.refreshAllRowsIfInTerminal()
|
||||||
if currentTerminalWidth != display.terminalWidth {
|
|
||||||
// terminal width changed. Update our output.
|
|
||||||
display.terminalWidth = currentTerminalWidth
|
|
||||||
display.refreshAllStatusMessages(true /*includeDone*/)
|
|
||||||
} else {
|
|
||||||
display.refreshAllStatusMessages(false /*includeDone*/)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (display *ProgressDisplay) processNormalEvent(event engine.Event) {
|
func (display *ProgressDisplay) processNormalEvent(event engine.Event) {
|
||||||
|
@ -543,57 +472,60 @@ func (display *ProgressDisplay) processNormalEvent(event engine.Event) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(display.rows) == 0 {
|
||||||
|
// about to make our first status message. make sure we present the header line first.
|
||||||
|
display.rows = append(display.rows, &headerRowData{})
|
||||||
|
}
|
||||||
|
|
||||||
// At this point, all events should relate to resources.
|
// At this point, all events should relate to resources.
|
||||||
|
|
||||||
refreshAllStatuses := false
|
row, has := display.eventUrnToResourceRow[eventUrn]
|
||||||
status, has := display.eventUrnToStatus[eventUrn]
|
|
||||||
if !has {
|
if !has {
|
||||||
|
id := fmt.Sprintf("%v", len(display.eventUrnToResourceRow)+1)
|
||||||
|
|
||||||
// first time we're hearing about this resource. Create an initial nearly-empty
|
// first time we're hearing about this resource. Create an initial nearly-empty
|
||||||
// status for it, assigning it a nice short ID.
|
// status for it, assigning it a nice short ID.
|
||||||
status = Status{Tick: display.currentTick}
|
row = &resourceRowData{
|
||||||
status.Step.Op = deploy.OpSame
|
display: display,
|
||||||
status.ID = display.makeID(eventUrn)
|
id: id,
|
||||||
|
tick: display.currentTick,
|
||||||
if display.isTerminal {
|
diagInfo: &DiagInfo{},
|
||||||
// in the terminal we want to align the status portions of messages. If we
|
step: engine.StepEventMetadata{Op: deploy.OpSame},
|
||||||
// heard about a resource with a longer id, go and update all in-flight and
|
|
||||||
// finished resources so that their statuses get aligned.
|
|
||||||
if len(status.ID) > display.maxIDLength {
|
|
||||||
display.maxIDLength = len(status.ID)
|
|
||||||
refreshAllStatuses = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
display.eventUrnToResourceRow[eventUrn] = row
|
||||||
|
display.rows = append(display.rows, row)
|
||||||
}
|
}
|
||||||
|
|
||||||
if event.Type == engine.ResourcePreEvent {
|
if event.Type == engine.ResourcePreEvent {
|
||||||
status.Step = event.Payload.(engine.ResourcePreEventPayload).Metadata
|
step := event.Payload.(engine.ResourcePreEventPayload).Metadata
|
||||||
if status.Step.Op == "" {
|
if step.Op == "" {
|
||||||
contract.Failf("Got empty op for %s", event.Type)
|
contract.Failf("Got empty op for %s", event.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
row.SetStep(step)
|
||||||
} else if event.Type == engine.ResourceOutputsEvent {
|
} else if event.Type == engine.ResourceOutputsEvent {
|
||||||
// transition the status to done.
|
// transition the status to done.
|
||||||
if !isRootURN(eventUrn) {
|
if !isRootURN(eventUrn) {
|
||||||
status.Done = true
|
row.SetDone()
|
||||||
}
|
}
|
||||||
} else if event.Type == engine.ResourceOperationFailed {
|
} else if event.Type == engine.ResourceOperationFailed {
|
||||||
status.Done = true
|
row.SetDone()
|
||||||
status.Failed = true
|
row.SetFailed()
|
||||||
} else if event.Type == engine.DiagEvent {
|
} else if event.Type == engine.DiagEvent {
|
||||||
// also record this diagnostic so we print it at the end.
|
// also record this diagnostic so we print it at the end.
|
||||||
status.DiagEvents = append(status.DiagEvents, event)
|
row.RecordDiagEvent(event)
|
||||||
} else {
|
} else {
|
||||||
contract.Failf("Unhandled event type '%s'", event.Type)
|
contract.Failf("Unhandled event type '%s'", event.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure that this updated status is recorded.
|
// See if this new status information causes us to have to refresh everything. Otherwise,
|
||||||
display.eventUrnToStatus[eventUrn] = status
|
// just refresh the info for that single status message.
|
||||||
|
if display.updateDimensions() {
|
||||||
// refresh the progress information for this resource. (or update all resources if
|
contract.Assertf(display.isTerminal, "we should only need to refresh if we're in a terminal")
|
||||||
// we need to realign everything)
|
display.refreshAllRowsIfInTerminal()
|
||||||
if refreshAllStatuses {
|
|
||||||
display.refreshAllStatusMessages(true /*includeDone*/)
|
|
||||||
} else {
|
} else {
|
||||||
display.refreshSingleStatusMessage(status)
|
display.refreshSingleRow(row)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -601,6 +533,7 @@ func (display *ProgressDisplay) processEvents(ticker *time.Ticker, events <-chan
|
||||||
// Main processing loop. The purpose of this func is to read in events from the engine
|
// Main processing loop. The purpose of this func is to read in events from the engine
|
||||||
// and translate them into Status objects and progress messages to be presented to the
|
// and translate them into Status objects and progress messages to be presented to the
|
||||||
// command line.
|
// command line.
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
|
@ -629,40 +562,6 @@ func (display *ProgressDisplay) renderProgressDiagEvent(event engine.Event) stri
|
||||||
return strings.TrimRightFunc(payload.Message, unicode.IsSpace)
|
return strings.TrimRightFunc(payload.Message, unicode.IsSpace)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (display *ProgressDisplay) getMetadataSummary(
|
|
||||||
step engine.StepEventMetadata, done bool, failed bool) string {
|
|
||||||
|
|
||||||
out := &bytes.Buffer{}
|
|
||||||
|
|
||||||
if done {
|
|
||||||
writeString(out, display.getStepDoneDescription(step, failed))
|
|
||||||
} else {
|
|
||||||
writeString(out, display.getStepInProgressDescription(step))
|
|
||||||
}
|
|
||||||
writeString(out, colors.Reset)
|
|
||||||
|
|
||||||
if step.Old != nil && step.New != nil && step.Old.Inputs != nil && step.New.Inputs != nil {
|
|
||||||
diff := step.Old.Inputs.Diff(step.New.Inputs)
|
|
||||||
|
|
||||||
if diff != nil {
|
|
||||||
writeString(out, " changes:")
|
|
||||||
|
|
||||||
updates := make(resource.PropertyMap)
|
|
||||||
for k := range diff.Updates {
|
|
||||||
updates[k] = resource.PropertyValue{}
|
|
||||||
}
|
|
||||||
|
|
||||||
writePropertyKeys(out, diff.Adds, deploy.OpCreate)
|
|
||||||
writePropertyKeys(out, diff.Deletes, deploy.OpDelete)
|
|
||||||
writePropertyKeys(out, updates, deploy.OpReplace)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fprintIgnoreError(out, colors.Reset)
|
|
||||||
|
|
||||||
return out.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (display *ProgressDisplay) getStepDoneDescription(step engine.StepEventMetadata, failed bool) string {
|
func (display *ProgressDisplay) getStepDoneDescription(step engine.StepEventMetadata, failed bool) string {
|
||||||
makeError := func(v string) string {
|
makeError := func(v string) string {
|
||||||
return colors.SpecError + "**" + v + "**" + colors.Reset
|
return colors.SpecError + "**" + v + "**" + colors.Reset
|
||||||
|
|
357
pkg/backend/local/rows.go
Normal file
357
pkg/backend/local/rows.go
Normal file
|
@ -0,0 +1,357 @@
|
||||||
|
// Copyright 2016-2018, Pulumi Corporation. All rights reserved.
|
||||||
|
|
||||||
|
package local
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/pulumi/pulumi/pkg/diag"
|
||||||
|
"github.com/pulumi/pulumi/pkg/diag/colors"
|
||||||
|
"github.com/pulumi/pulumi/pkg/engine"
|
||||||
|
"github.com/pulumi/pulumi/pkg/resource"
|
||||||
|
"github.com/pulumi/pulumi/pkg/resource/deploy"
|
||||||
|
"github.com/pulumi/pulumi/pkg/util/contract"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Row interface {
|
||||||
|
ColorizedColumns() []string
|
||||||
|
UncolorizedColumns() []string
|
||||||
|
|
||||||
|
ColorizedSuffix() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResourceRow interface {
|
||||||
|
Row
|
||||||
|
|
||||||
|
// The change that the engine wants apply to that resource.
|
||||||
|
SetStep(step engine.StepEventMetadata)
|
||||||
|
|
||||||
|
// The tick we were on when we created this row. Purely used for generating an
|
||||||
|
// ellipses to show progress for in-flight resources.
|
||||||
|
Tick() int
|
||||||
|
|
||||||
|
Done() bool
|
||||||
|
SetDone()
|
||||||
|
|
||||||
|
SetFailed()
|
||||||
|
|
||||||
|
DiagInfo() *DiagInfo
|
||||||
|
RecordDiagEvent(diagEvent engine.Event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implementation of a Row, used for the header of the grid.
|
||||||
|
type headerRowData struct {
|
||||||
|
columns []string
|
||||||
|
uncolorizedColumns []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (data *headerRowData) ColorizedColumns() []string {
|
||||||
|
if len(data.columns) == 0 {
|
||||||
|
blue := func(msg string) string {
|
||||||
|
return colors.Blue + msg + colors.Reset
|
||||||
|
}
|
||||||
|
|
||||||
|
header := func(msg string) string {
|
||||||
|
return blue(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
data.columns = []string{"#", header("Resource Type"), header("Name"), header("Status"), header("Extra Info")}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.columns
|
||||||
|
}
|
||||||
|
|
||||||
|
func (data *headerRowData) UncolorizedColumns() []string {
|
||||||
|
if len(data.uncolorizedColumns) == 0 {
|
||||||
|
data.uncolorizedColumns = uncolorise(data.ColorizedColumns())
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.uncolorizedColumns
|
||||||
|
}
|
||||||
|
|
||||||
|
func (data *headerRowData) ColorizedSuffix() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implementation of a row used for all the resource rows in the grid.
|
||||||
|
type resourceRowData struct {
|
||||||
|
display *ProgressDisplay
|
||||||
|
|
||||||
|
// The simple short ID we have generated for the resource to present it to the user.
|
||||||
|
// Usually similar to the form: aws.Function("name")
|
||||||
|
id string
|
||||||
|
|
||||||
|
// The change that the engine wants apply to that resource.
|
||||||
|
step engine.StepEventMetadata
|
||||||
|
|
||||||
|
// The tick we were on when we created this row. Purely used for generating an
|
||||||
|
// ellipses to show progress for in-flight resources.
|
||||||
|
tick int
|
||||||
|
|
||||||
|
// If the engine finished processing this resources.
|
||||||
|
done bool
|
||||||
|
|
||||||
|
// If we failed this operation for any reason.
|
||||||
|
failed bool
|
||||||
|
|
||||||
|
diagInfo *DiagInfo
|
||||||
|
|
||||||
|
columns []string
|
||||||
|
uncolorizedColumns []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (data *resourceRowData) SetStep(step engine.StepEventMetadata) {
|
||||||
|
data.step = step
|
||||||
|
data.ClearCachedData()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (data *resourceRowData) Tick() int {
|
||||||
|
return data.tick
|
||||||
|
}
|
||||||
|
|
||||||
|
func (data *resourceRowData) Done() bool {
|
||||||
|
return data.done
|
||||||
|
}
|
||||||
|
|
||||||
|
func (data *resourceRowData) SetDone() {
|
||||||
|
data.done = true
|
||||||
|
data.ClearCachedData()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (data *resourceRowData) Failed() bool {
|
||||||
|
return data.failed
|
||||||
|
}
|
||||||
|
|
||||||
|
func (data *resourceRowData) SetFailed() {
|
||||||
|
data.failed = true
|
||||||
|
data.ClearCachedData()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (data *resourceRowData) DiagInfo() *DiagInfo {
|
||||||
|
return data.diagInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (data *resourceRowData) RecordDiagEvent(event engine.Event) {
|
||||||
|
data.ClearCachedData()
|
||||||
|
|
||||||
|
diagInfo := data.diagInfo
|
||||||
|
payload := event.Payload.(engine.DiagEventPayload)
|
||||||
|
|
||||||
|
switch payload.Severity {
|
||||||
|
case diag.Error:
|
||||||
|
diagInfo.ErrorCount++
|
||||||
|
diagInfo.LastError = &event
|
||||||
|
case diag.Warning:
|
||||||
|
diagInfo.WarningCount++
|
||||||
|
diagInfo.LastWarning = &event
|
||||||
|
case diag.Infoerr:
|
||||||
|
diagInfo.InfoCount++
|
||||||
|
diagInfo.LastInfoError = &event
|
||||||
|
case diag.Info:
|
||||||
|
diagInfo.InfoCount++
|
||||||
|
diagInfo.LastInfo = &event
|
||||||
|
case diag.Debug:
|
||||||
|
diagInfo.DebugCount++
|
||||||
|
diagInfo.LastDebug = &event
|
||||||
|
}
|
||||||
|
|
||||||
|
diagInfo.DiagEvents = append(diagInfo.DiagEvents, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (data *resourceRowData) ClearCachedData() {
|
||||||
|
data.columns = []string{}
|
||||||
|
data.uncolorizedColumns = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type column int
|
||||||
|
|
||||||
|
const (
|
||||||
|
idColumn column = 0
|
||||||
|
typeColumn column = 1
|
||||||
|
nameColumn column = 2
|
||||||
|
statusColumn column = 3
|
||||||
|
infoColumn column = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
var ellipsesArray = []string{"", ".", "..", "..."}
|
||||||
|
|
||||||
|
func (data *resourceRowData) ColorizedSuffix() string {
|
||||||
|
if !data.done {
|
||||||
|
uncolorizedColumns := data.UncolorizedColumns()
|
||||||
|
ellipses := ellipsesArray[(data.tick+data.display.currentTick)%len(ellipsesArray)]
|
||||||
|
|
||||||
|
if uncolorizedColumns[infoColumn] == "" {
|
||||||
|
return data.step.Op.Color() + ellipses + colors.Reset
|
||||||
|
}
|
||||||
|
|
||||||
|
return ellipses
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (data *resourceRowData) ColorizedColumns() []string {
|
||||||
|
if len(data.columns) == 0 {
|
||||||
|
columns := data.getUnpaddedColumns()
|
||||||
|
data.columns = columns
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.columns
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets the single line summary to show for a resource. This will include the current state of
|
||||||
|
// the resource (i.e. "Creating", "Replaced", "Failed", etc.) as well as relevant diagnostic
|
||||||
|
// information if there is any.
|
||||||
|
func (data *resourceRowData) getUnpaddedColumns() []string {
|
||||||
|
step := data.step
|
||||||
|
if step.Op == "" {
|
||||||
|
contract.Failf("Finishing a resource we never heard about: '%s'", data.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
var name string
|
||||||
|
var typ string
|
||||||
|
if data.step.URN == "" {
|
||||||
|
name = "global"
|
||||||
|
typ = "global"
|
||||||
|
} else {
|
||||||
|
name = string(data.step.URN.Name())
|
||||||
|
typ = simplifyTypeName(data.step.URN.Type())
|
||||||
|
}
|
||||||
|
|
||||||
|
columns := make([]string, 5)
|
||||||
|
columns[idColumn] = data.id
|
||||||
|
columns[typeColumn] = typ
|
||||||
|
columns[nameColumn] = name
|
||||||
|
|
||||||
|
diagInfo := data.diagInfo
|
||||||
|
|
||||||
|
if data.done {
|
||||||
|
failed := data.failed || diagInfo.ErrorCount > 0
|
||||||
|
columns[statusColumn] = data.display.getStepDoneDescription(step, failed)
|
||||||
|
} else {
|
||||||
|
columns[statusColumn] = data.display.getStepInProgressDescription(step)
|
||||||
|
}
|
||||||
|
|
||||||
|
columns[infoColumn] = data.getInfo()
|
||||||
|
return columns
|
||||||
|
}
|
||||||
|
|
||||||
|
func (data *resourceRowData) getInfo() string {
|
||||||
|
step := data.step
|
||||||
|
changesBuf := &bytes.Buffer{}
|
||||||
|
|
||||||
|
if step.Old != nil && step.New != nil && step.Old.Inputs != nil && step.New.Inputs != nil {
|
||||||
|
diff := step.Old.Inputs.Diff(step.New.Inputs)
|
||||||
|
|
||||||
|
if diff != nil {
|
||||||
|
writeString(changesBuf, "changes:")
|
||||||
|
|
||||||
|
updates := make(resource.PropertyMap)
|
||||||
|
for k := range diff.Updates {
|
||||||
|
updates[k] = resource.PropertyValue{}
|
||||||
|
}
|
||||||
|
|
||||||
|
writePropertyKeys(changesBuf, diff.Adds, deploy.OpCreate)
|
||||||
|
writePropertyKeys(changesBuf, diff.Deletes, deploy.OpDelete)
|
||||||
|
writePropertyKeys(changesBuf, updates, deploy.OpReplace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintIgnoreError(changesBuf, colors.Reset)
|
||||||
|
changes := changesBuf.String()
|
||||||
|
|
||||||
|
diagMsg := ""
|
||||||
|
|
||||||
|
if colors.Never.Colorize(changes) != "" {
|
||||||
|
diagMsg += changes
|
||||||
|
}
|
||||||
|
|
||||||
|
appendDiagMessage := func(msg string) {
|
||||||
|
if diagMsg != "" {
|
||||||
|
diagMsg += ", "
|
||||||
|
}
|
||||||
|
|
||||||
|
diagMsg += msg
|
||||||
|
}
|
||||||
|
|
||||||
|
diagInfo := data.diagInfo
|
||||||
|
|
||||||
|
if diagInfo.ErrorCount == 1 {
|
||||||
|
appendDiagMessage("1 error")
|
||||||
|
} else if diagInfo.ErrorCount > 1 {
|
||||||
|
appendDiagMessage(fmt.Sprintf("%v errors", diagInfo.ErrorCount))
|
||||||
|
}
|
||||||
|
|
||||||
|
if diagInfo.WarningCount == 1 {
|
||||||
|
appendDiagMessage("1 warning")
|
||||||
|
} else if diagInfo.WarningCount > 1 {
|
||||||
|
appendDiagMessage(fmt.Sprintf("%v warnings", diagInfo.WarningCount))
|
||||||
|
}
|
||||||
|
|
||||||
|
if diagInfo.InfoCount == 1 {
|
||||||
|
appendDiagMessage("1 info message")
|
||||||
|
} else if diagInfo.InfoCount > 1 {
|
||||||
|
appendDiagMessage(fmt.Sprintf("%v info messages", diagInfo.InfoCount))
|
||||||
|
}
|
||||||
|
|
||||||
|
if diagInfo.DebugCount == 1 {
|
||||||
|
appendDiagMessage("1 debug message")
|
||||||
|
} else if diagInfo.ErrorCount > 1 {
|
||||||
|
appendDiagMessage(fmt.Sprintf("%v debug messages", diagInfo.DebugCount))
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're not totally done, also print out the worst diagnostic next to the status message.
|
||||||
|
// This is helpful for long running tasks to know what's going on. However, once done, we print
|
||||||
|
// the diagnostics at the bottom, so we don't need to show this.
|
||||||
|
worstDiag := getWorstDiagnostic(data.diagInfo)
|
||||||
|
if worstDiag != nil && !data.display.Done {
|
||||||
|
eventMsg := data.display.renderProgressDiagEvent(*worstDiag)
|
||||||
|
if eventMsg != "" {
|
||||||
|
diagMsg += ". " + eventMsg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return diagMsg
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the worst diagnostic we've seen. Used to produce a diagnostic string to go along with
|
||||||
|
// any resource if it has had any issues.
|
||||||
|
func getWorstDiagnostic(diagInfo *DiagInfo) *engine.Event {
|
||||||
|
if diagInfo.LastError != nil {
|
||||||
|
return diagInfo.LastError
|
||||||
|
}
|
||||||
|
|
||||||
|
if diagInfo.LastWarning != nil {
|
||||||
|
return diagInfo.LastWarning
|
||||||
|
}
|
||||||
|
|
||||||
|
if diagInfo.LastInfoError != nil {
|
||||||
|
return diagInfo.LastInfoError
|
||||||
|
}
|
||||||
|
|
||||||
|
if diagInfo.LastInfo != nil {
|
||||||
|
return diagInfo.LastInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
return diagInfo.LastDebug
|
||||||
|
}
|
||||||
|
|
||||||
|
func uncolorise(columns []string) []string {
|
||||||
|
uncolorizedColumns := make([]string, len(columns))
|
||||||
|
|
||||||
|
for i, v := range columns {
|
||||||
|
uncolorizedColumns[i] = colors.Never.Colorize(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return uncolorizedColumns
|
||||||
|
}
|
||||||
|
|
||||||
|
func (data *resourceRowData) UncolorizedColumns() []string {
|
||||||
|
if len(data.uncolorizedColumns) == 0 {
|
||||||
|
columns := data.ColorizedColumns()
|
||||||
|
data.uncolorizedColumns = uncolorise(columns)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.uncolorizedColumns
|
||||||
|
}
|
Loading…
Reference in a new issue