diff --git a/cmd/incus/operation.go b/cmd/incus/operation.go index 75eaddca2e0..b646546b30b 100644 --- a/cmd/incus/operation.go +++ b/cmd/incus/operation.go @@ -17,6 +17,11 @@ type cmdOperation struct { global *cmdGlobal } +type operationColumn struct { + Name string + Data func(api.Operation) string +} + func (c *cmdOperation) Command() *cobra.Command { cmd := &cobra.Command{} cmd.Use = usage("operation") @@ -96,6 +101,7 @@ type cmdOperationList struct { operation *cmdOperation flagFormat string + flagColumns string flagAllProjects bool } @@ -105,15 +111,106 @@ func (c *cmdOperationList) Command() *cobra.Command { cmd.Aliases = []string{"ls"} cmd.Short = i18n.G("List background operations") cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G( - `List background operations`)) + `List background operations + +Default column layout: itdscCl + +== Columns == +The -c option takes a comma separated list of arguments that control +which instance attributes to output when displaying in table or csv +format. + +Column arguments are either pre-defined shorthand chars (see below), +or (extended) config keys. + +Commas between consecutive shorthand chars are optional. + +Pre-defined column shorthand chars: + i - ID + t - Type + d - Description + s - State + c - Cancelable + C - Created + l - Location(clustered only)`)) cmd.Flags().StringVarP(&c.flagFormat, "format", "f", "table", i18n.G("Format (csv|json|table|yaml|compact)")+"``") cmd.Flags().BoolVar(&c.flagAllProjects, "all-projects", false, i18n.G("List operations from all projects")+"``") + cmd.Flags().StringVarP(&c.flagColumns, "columns", "c", defaultOperationColumns, i18n.G("Columns")+"``") cmd.RunE = c.Run return cmd } +const defaultOperationColumns = "itdscCl" + +func (c *cmdOperationList) parseColumns() ([]operationColumn, error) { + columnsShorthandMap := map[rune]operationColumn{ + 'i': {i18n.G("ID"), c.operationIDcolumnData}, + 't': {i18n.G("TYPE"), c.typeColumnData}, + 'd': {i18n.G("DESCRIPTION"), c.descriptionColumnData}, + 's': {i18n.G("STATE"), c.stateColumnData}, + 'c': {i18n.G("CANCELABLE"), c.cancelableColumnData}, + 'C': {i18n.G("CREATED"), c.createdColumnData}, + 'l': {i18n.G("LOCATION"), c.locationColumnData}, + } + + columnList := strings.Split(c.flagColumns, ",") + + columns := []operationColumn{} + + for _, columnEntry := range columnList { + if columnEntry == "" { + return nil, fmt.Errorf(i18n.G("Empty column entry (redundant, leading or trailing command) in '%s'"), c.flagColumns) + } + + for _, columnRune := range columnEntry { + column, ok := columnsShorthandMap[columnRune] + if !ok { + return nil, fmt.Errorf(i18n.G("Unknown column shorthand char '%c' in '%s'"), columnRune, columnEntry) + } + + columns = append(columns, column) + } + } + + return columns, nil +} + +func (c *cmdOperationList) operationIDcolumnData(op api.Operation) string { + return op.ID +} + +func (c *cmdOperationList) typeColumnData(op api.Operation) string { + return strings.ToUpper(op.Class) +} + +func (c *cmdOperationList) descriptionColumnData(op api.Operation) string { + return op.Description +} + +func (c *cmdOperationList) stateColumnData(op api.Operation) string { + return strings.ToUpper(op.Status) +} + +func (c *cmdOperationList) cancelableColumnData(op api.Operation) string { + strCancelable := i18n.G("NO") + + if op.MayCancel { + strCancelable = i18n.G("YES") + } + + return strCancelable +} + +func (c *cmdOperationList) createdColumnData(op api.Operation) string { + return op.CreatedAt.Local().Format(dateLayout) +} + +func (c *cmdOperationList) locationColumnData(op api.Operation) string { + return op.Location +} + func (c *cmdOperationList) Run(cmd *cobra.Command, args []string) error { // Quick checks. exit, err := c.global.CheckArgs(cmd, args, 0, 1) @@ -149,33 +246,43 @@ func (c *cmdOperationList) Run(cmd *cobra.Command, args []string) error { return err } + // Parse column flags. + columns, err := c.parseColumns() + if err != nil { + return err + } + // Render the table data := [][]string{} for _, op := range operations { - cancelable := i18n.G("NO") - if op.MayCancel { - cancelable = i18n.G("YES") + line := []string{} + for _, column := range columns { + if column.Name == i18n.G("LOCATION") { + if resource.server.IsClustered() { + line = append(line, column.Data(op)) + } + + } else { + line = append(line, column.Data(op)) + } } - entry := []string{op.ID, strings.ToUpper(op.Class), op.Description, strings.ToUpper(op.Status), cancelable, op.CreatedAt.Local().Format(dateLayout)} - if resource.server.IsClustered() { - entry = append(entry, op.Location) - } - - data = append(data, entry) + data = append(data, line) } sort.Sort(cli.SortColumnsNaturally(data)) - header := []string{ - i18n.G("ID"), - i18n.G("TYPE"), - i18n.G("DESCRIPTION"), - i18n.G("STATE"), - i18n.G("CANCELABLE"), - i18n.G("CREATED")} - if resource.server.IsClustered() { - header = append(header, i18n.G("LOCATION")) + header := []string{} + for _, column := range columns { + if column.Name == "LOCATION" { + if resource.server.IsClustered() { + header = append(header, column.Name) + } + + } else { + header = append(header, column.Name) + } + } return cli.RenderTable(c.flagFormat, header, data, operations)