diff --git a/cmd/workspace/alerts/overrides.go b/cmd/workspace/alerts/overrides.go index ed5fdaa8db..b7d3a3e3f4 100644 --- a/cmd/workspace/alerts/overrides.go +++ b/cmd/workspace/alerts/overrides.go @@ -7,7 +7,7 @@ import ( "github.com/spf13/cobra" ) -func listOverride(listCmd *cobra.Command, listReq *sql.ListAlertsRequest) { +func listOverride(listCmd *cobra.Command, _ *sql.ListAlertsRequest) { listCmd.Annotations["template"] = cmdio.Heredoc(` {{range .}}{{green "%s" .Id}} {{.DisplayName}} {{.State}} {{end}}`) diff --git a/cmd/workspace/apps/overrides.go b/cmd/workspace/apps/overrides.go index ec6a25b803..480f271a94 100644 --- a/cmd/workspace/apps/overrides.go +++ b/cmd/workspace/apps/overrides.go @@ -10,7 +10,7 @@ import ( "github.com/spf13/cobra" ) -func listOverride(listCmd *cobra.Command, listReq *apps.ListAppsRequest) { +func listOverride(listCmd *cobra.Command, _ *apps.ListAppsRequest) { listCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` {{header "Name"}} {{header "Url"}} {{header "ComputeStatus"}} {{header "DeploymentStatus"}}`) listCmd.Annotations["template"] = cmdio.Heredoc(` @@ -45,7 +45,7 @@ func listOverride(listCmd *cobra.Command, listReq *apps.ListAppsRequest) { tableview.RegisterConfig(listCmd, tableview.TableConfig{Columns: columns}) } -func listDeploymentsOverride(listDeploymentsCmd *cobra.Command, listDeploymentsReq *apps.ListAppDeploymentsRequest) { +func listDeploymentsOverride(listDeploymentsCmd *cobra.Command, _ *apps.ListAppDeploymentsRequest) { listDeploymentsCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` {{header "DeploymentId"}} {{header "State"}} {{header "CreatedAt"}}`) listDeploymentsCmd.Annotations["template"] = cmdio.Heredoc(` diff --git a/cmd/workspace/catalogs/overrides.go b/cmd/workspace/catalogs/overrides.go index 46d66a08b2..d86af1eeea 100644 --- a/cmd/workspace/catalogs/overrides.go +++ b/cmd/workspace/catalogs/overrides.go @@ -7,7 +7,7 @@ import ( "github.com/spf13/cobra" ) -func listOverride(listCmd *cobra.Command, listReq *catalog.ListCatalogsRequest) { +func listOverride(listCmd *cobra.Command, _ *catalog.ListCatalogsRequest) { listCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` {{header "Name"}} {{header "Type"}} {{header "Comment"}}`) listCmd.Annotations["template"] = cmdio.Heredoc(` diff --git a/cmd/workspace/cluster-policies/overrides.go b/cmd/workspace/cluster-policies/overrides.go index 9278b29c39..8bc320aa5f 100644 --- a/cmd/workspace/cluster-policies/overrides.go +++ b/cmd/workspace/cluster-policies/overrides.go @@ -2,6 +2,7 @@ package cluster_policies import ( "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/tableview" "github.com/databricks/databricks-sdk-go/service/compute" "github.com/spf13/cobra" ) @@ -10,6 +11,23 @@ func listOverride(listCmd *cobra.Command, _ *compute.ListClusterPoliciesRequest) listCmd.Annotations["template"] = cmdio.Heredoc(` {{range .}}{{.PolicyId | green}} {{.Name}} {{end}}`) + + columns := []tableview.ColumnDef{ + {Header: "Policy ID", Extract: func(v any) string { + return v.(compute.Policy).PolicyId + }}, + {Header: "Name", Extract: func(v any) string { + return v.(compute.Policy).Name + }}, + {Header: "Default", Extract: func(v any) string { + if v.(compute.Policy).IsDefault { + return "yes" + } + return "" + }}, + } + + tableview.RegisterConfig(listCmd, tableview.TableConfig{Columns: columns}) } func getOverride(getCmd *cobra.Command, _ *compute.GetClusterPolicyRequest) { diff --git a/cmd/workspace/external-locations/overrides.go b/cmd/workspace/external-locations/overrides.go index 9d9108f5be..607550da3a 100644 --- a/cmd/workspace/external-locations/overrides.go +++ b/cmd/workspace/external-locations/overrides.go @@ -7,7 +7,7 @@ import ( "github.com/spf13/cobra" ) -func listOverride(listCmd *cobra.Command, listReq *catalog.ListExternalLocationsRequest) { +func listOverride(listCmd *cobra.Command, _ *catalog.ListExternalLocationsRequest) { listCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` {{header "Name"}} {{header "Credential"}} {{header "URL"}}`) listCmd.Annotations["template"] = cmdio.Heredoc(` diff --git a/cmd/workspace/jobs/overrides.go b/cmd/workspace/jobs/overrides.go index d9786601ba..d20efd6195 100644 --- a/cmd/workspace/jobs/overrides.go +++ b/cmd/workspace/jobs/overrides.go @@ -44,7 +44,7 @@ func listOverride(listCmd *cobra.Command, listReq *jobs.ListJobsRequest) { }) } -func listRunsOverride(listRunsCmd *cobra.Command, listRunsReq *jobs.ListRunsRequest) { +func listRunsOverride(listRunsCmd *cobra.Command, _ *jobs.ListRunsRequest) { listRunsCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` {{header "Job ID"}} {{header "Run ID"}} {{header "Result State"}} URL`) listRunsCmd.Annotations["template"] = cmdio.Heredoc(` diff --git a/cmd/workspace/lakeview/overrides.go b/cmd/workspace/lakeview/overrides.go index 6ffb641aa9..55357f703d 100644 --- a/cmd/workspace/lakeview/overrides.go +++ b/cmd/workspace/lakeview/overrides.go @@ -1,10 +1,34 @@ package lakeview import ( + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/tableview" "github.com/databricks/databricks-sdk-go/service/dashboards" "github.com/spf13/cobra" ) +func listOverride(listCmd *cobra.Command, _ *dashboards.ListDashboardsRequest) { + listCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` + {{header "Dashboard ID"}} {{header "Name"}} {{header "State"}}`) + listCmd.Annotations["template"] = cmdio.Heredoc(` + {{range .}}{{green "%s" .DashboardId}} {{.DisplayName}} {{blue "%s" .LifecycleState}} + {{end}}`) + + columns := []tableview.ColumnDef{ + {Header: "Dashboard ID", Extract: func(v any) string { + return v.(dashboards.Dashboard).DashboardId + }}, + {Header: "Name", Extract: func(v any) string { + return v.(dashboards.Dashboard).DisplayName + }}, + {Header: "State", Extract: func(v any) string { + return string(v.(dashboards.Dashboard).LifecycleState) + }}, + } + + tableview.RegisterConfig(listCmd, tableview.TableConfig{Columns: columns}) +} + func publishOverride(cmd *cobra.Command, req *dashboards.PublishRequest) { originalRunE := cmd.RunE cmd.RunE = func(cmd *cobra.Command, args []string) error { @@ -15,5 +39,6 @@ func publishOverride(cmd *cobra.Command, req *dashboards.PublishRequest) { } func init() { + listOverrides = append(listOverrides, listOverride) publishOverrides = append(publishOverrides, publishOverride) } diff --git a/cmd/workspace/pipelines/overrides.go b/cmd/workspace/pipelines/overrides.go index 361c834bfa..083c77c591 100644 --- a/cmd/workspace/pipelines/overrides.go +++ b/cmd/workspace/pipelines/overrides.go @@ -62,8 +62,34 @@ func listPipelinesOverride(listCmd *cobra.Command, listReq *pipelines.ListPipeli } } +func listPipelineEventsOverride(listCmd *cobra.Command, _ *pipelines.ListPipelineEventsRequest) { + listCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` + {{header "Timestamp"}} {{header "Level"}} {{header "Event Type"}} {{header "Message"}}`) + listCmd.Annotations["template"] = cmdio.Heredoc(` + {{range .}}{{.Timestamp}} {{.Level}} {{.EventType}} {{.Message | sanitize}} + {{end}}`) + + columns := []tableview.ColumnDef{ + {Header: "Timestamp", Extract: func(v any) string { + return v.(pipelines.PipelineEvent).Timestamp + }}, + {Header: "Level", Extract: func(v any) string { + return string(v.(pipelines.PipelineEvent).Level) + }}, + {Header: "Event Type", Extract: func(v any) string { + return v.(pipelines.PipelineEvent).EventType + }}, + {Header: "Message", MaxWidth: 200, Extract: func(v any) string { + return sanitizeWhitespace(v.(pipelines.PipelineEvent).Message) + }}, + } + + tableview.RegisterConfig(listCmd, tableview.TableConfig{Columns: columns}) +} + func init() { listPipelinesOverrides = append(listPipelinesOverrides, listPipelinesOverride) + listPipelineEventsOverrides = append(listPipelineEventsOverrides, listPipelineEventsOverride) cmdOverrides = append(cmdOverrides, func(cli *cobra.Command) { // all auto-generated commands apart from nonManagementCommands go into 'management' group @@ -135,6 +161,14 @@ func disableSearchIfFilterSet(cmd *cobra.Command) { } } +var controlWhitespaceReplacer = strings.NewReplacer("\n", " ", "\r", " ", "\t", " ") + +// sanitizeWhitespace replaces control whitespace (newlines, tabs) with spaces +// to prevent corrupting tab-delimited or TUI table output. +func sanitizeWhitespace(s string) string { + return controlWhitespaceReplacer.Replace(s) +} + var uuidRegex = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`) // looksLikeUUID checks if a string matches the UUID format with lowercase hex digits diff --git a/cmd/workspace/repos/overrides.go b/cmd/workspace/repos/overrides.go index 85bdb1a6d6..65cc353dd7 100644 --- a/cmd/workspace/repos/overrides.go +++ b/cmd/workspace/repos/overrides.go @@ -17,7 +17,7 @@ import ( "github.com/spf13/cobra" ) -func listOverride(listCmd *cobra.Command, listReq *workspace.ListReposRequest) { +func listOverride(listCmd *cobra.Command, _ *workspace.ListReposRequest) { listCmd.Annotations["template"] = cmdio.Heredoc(` {{range .}}{{green "%d" .Id}} {{.Path}} {{.Branch|blue}} {{.Url|cyan}} {{end}}`) diff --git a/cmd/workspace/schemas/overrides.go b/cmd/workspace/schemas/overrides.go index 625c92f3d7..0e9b1b03b9 100644 --- a/cmd/workspace/schemas/overrides.go +++ b/cmd/workspace/schemas/overrides.go @@ -7,7 +7,7 @@ import ( "github.com/spf13/cobra" ) -func listOverride(listCmd *cobra.Command, listReq *catalog.ListSchemasRequest) { +func listOverride(listCmd *cobra.Command, _ *catalog.ListSchemasRequest) { listCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` {{header "Full Name"}} {{header "Owner"}} {{header "Comment"}}`) listCmd.Annotations["template"] = cmdio.Heredoc(` diff --git a/cmd/workspace/secrets/overrides.go b/cmd/workspace/secrets/overrides.go index b215f17a7f..5de7268905 100644 --- a/cmd/workspace/secrets/overrides.go +++ b/cmd/workspace/secrets/overrides.go @@ -1,7 +1,10 @@ package secrets import ( + "time" + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/tableview" "github.com/databricks/databricks-sdk-go/service/workspace" "github.com/spf13/cobra" ) @@ -16,6 +19,17 @@ func listScopesOverride(listScopesCmd *cobra.Command) { listScopesCmd.Annotations["template"] = cmdio.Heredoc(` {{range .}}{{.Name|green}} {{.BackendType}} {{end}}`) + + columns := []tableview.ColumnDef{ + {Header: "Scope", Extract: func(v any) string { + return v.(workspace.SecretScope).Name + }}, + {Header: "Backend Type", Extract: func(v any) string { + return string(v.(workspace.SecretScope).BackendType) + }}, + } + + tableview.RegisterConfig(listScopesCmd, tableview.TableConfig{Columns: columns}) } func listSecretsOverride(listSecretsCommand *cobra.Command, _ *workspace.ListSecretsRequest) { @@ -24,6 +38,21 @@ func listSecretsOverride(listSecretsCommand *cobra.Command, _ *workspace.ListSec listSecretsCommand.Annotations["template"] = cmdio.Heredoc(` {{range .}}{{.Key|green}} {{.LastUpdatedTimestamp}} {{end}}`) + + columns := []tableview.ColumnDef{ + {Header: "Key", Extract: func(v any) string { + return v.(workspace.SecretMetadata).Key + }}, + {Header: "Last Updated", Extract: func(v any) string { + ts := v.(workspace.SecretMetadata).LastUpdatedTimestamp + if ts == 0 { + return "" + } + return time.UnixMilli(ts).UTC().Format("2006-01-02 15:04:05") + }}, + } + + tableview.RegisterConfig(listSecretsCommand, tableview.TableConfig{Columns: columns}) } func init() { diff --git a/cmd/workspace/tables/overrides.go b/cmd/workspace/tables/overrides.go index 157d62daf9..8e0987d469 100644 --- a/cmd/workspace/tables/overrides.go +++ b/cmd/workspace/tables/overrides.go @@ -7,7 +7,7 @@ import ( "github.com/spf13/cobra" ) -func listOverride(listCmd *cobra.Command, listReq *catalog.ListTablesRequest) { +func listOverride(listCmd *cobra.Command, _ *catalog.ListTablesRequest) { listCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` {{header "Full Name"}} {{header "Table Type"}}`) listCmd.Annotations["template"] = cmdio.Heredoc(` diff --git a/cmd/workspace/volumes/overrides.go b/cmd/workspace/volumes/overrides.go index 0a4f645de3..66b946f2ea 100644 --- a/cmd/workspace/volumes/overrides.go +++ b/cmd/workspace/volumes/overrides.go @@ -7,7 +7,7 @@ import ( "github.com/spf13/cobra" ) -func listOverride(listCmd *cobra.Command, listReq *catalog.ListVolumesRequest) { +func listOverride(listCmd *cobra.Command, _ *catalog.ListVolumesRequest) { listCmd.Annotations["template"] = cmdio.Heredoc(` {{range .}}{{green "%s" .Name}} {{.VolumeType}} {{.FullName}} {{end}}`) diff --git a/cmd/workspace/warehouses/overrides.go b/cmd/workspace/warehouses/overrides.go index edc58ad681..14b2635a04 100644 --- a/cmd/workspace/warehouses/overrides.go +++ b/cmd/workspace/warehouses/overrides.go @@ -7,7 +7,7 @@ import ( "github.com/spf13/cobra" ) -func listOverride(listCmd *cobra.Command, listReq *sql.ListWarehousesRequest) { +func listOverride(listCmd *cobra.Command, _ *sql.ListWarehousesRequest) { listCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` {{header "ID"}} {{header "Name"}} {{header "Size"}} {{header "State"}}`) listCmd.Annotations["template"] = cmdio.Heredoc(` diff --git a/libs/cmdio/render.go b/libs/cmdio/render.go index f4289dbfd7..6b6d1b9121 100644 --- a/libs/cmdio/render.go +++ b/libs/cmdio/render.go @@ -313,6 +313,14 @@ func RenderIteratorJson[T any](ctx context.Context, i listing.Iterator[T]) error return renderWithTemplate(ctx, newIteratorRenderer(i), c.outputFormat, c.out, c.headerTemplate, c.template) } +var controlWhitespaceReplacer = strings.NewReplacer("\n", " ", "\r", " ", "\t", " ") + +// sanitizeControlWhitespace replaces newlines and tabs with spaces to prevent +// corrupting tab-delimited text output. +func sanitizeControlWhitespace(s string) string { + return controlWhitespaceReplacer.Replace(s) +} + var renderFuncMap = template.FuncMap{ // we render colored output if stdout is TTY, otherwise we render text. // in the future we'll check if we can explicitly check for stderr being @@ -330,8 +338,9 @@ var renderFuncMap = template.FuncMap{ "italic": func(format string, a ...any) string { return color.New(color.Italic).Sprintf(format, a...) }, - "replace": strings.ReplaceAll, - "join": strings.Join, + "replace": strings.ReplaceAll, + "sanitize": sanitizeControlWhitespace, + "join": strings.Join, "sub": func(a, b int) int { return a - b }, diff --git a/libs/tableview/config.go b/libs/tableview/config.go index c933f08e87..f6ba32a41a 100644 --- a/libs/tableview/config.go +++ b/libs/tableview/config.go @@ -4,8 +4,11 @@ import "context" // ColumnDef defines a column in the TUI table. type ColumnDef struct { - Header string // Display name in header row. - MaxWidth int // Max cell width; 0 = default (50). + Header string // Display name in header row. + // MaxWidth caps cell display width; 0 = default (50). Values exceeding + // this limit are destructively truncated with "..." in the rendered + // output. Horizontal scrolling does not recover the hidden portion. + MaxWidth int Extract func(v any) string // Extracts cell value from typed SDK struct. } diff --git a/libs/tableview/paginated.go b/libs/tableview/paginated.go index b72ac1ce01..b9d3be3f07 100644 --- a/libs/tableview/paginated.go +++ b/libs/tableview/paginated.go @@ -78,6 +78,11 @@ type paginatedModel struct { limitReached bool } +// Err returns the error recorded during data fetching, if any. +func (m paginatedModel) Err() error { + return m.err +} + // newFetchCmdFunc returns a closure that creates fetch commands, capturing ctx. func newFetchCmdFunc(ctx context.Context) func(paginatedModel) tea.Cmd { return func(m paginatedModel) tea.Cmd { @@ -156,19 +161,14 @@ func RunPaginated(ctx context.Context, w io.Writer, cfg *TableConfig, iter RowIt if err != nil { return err } - if pm, ok := finalModel.(FinalModel); ok { - if modelErr := pm.Err(); modelErr != nil { - return modelErr + if m, ok := finalModel.(FinalModel); ok { + if fetchErr := m.Err(); fetchErr != nil { + return fetchErr } } return nil } -// Err returns any error that occurred during data fetching. -func (m paginatedModel) Err() error { - return m.err -} - func (m paginatedModel) Init() tea.Cmd { return m.makeFetchCmd(m) } @@ -275,7 +275,8 @@ func (m paginatedModel) renderContent() string { } fmt.Fprintln(tw, strings.Join(seps, "\t")) - // Data rows + // Data rows. + // MaxWidth truncation is destructive; horizontal scroll won't recover hidden text. for _, row := range m.rows { vals := make([]string, len(m.headers)) for i := range m.headers {