Skip to content

Commit

Permalink
refactor(cmd): replace table printer and refactor utils
Browse files Browse the repository at this point in the history
Replace the previous table printer with go-pretty table printer to enable more capabilities, like sorting, setting column width, adding table header/footer and many more.
  • Loading branch information
gabor-boros committed Oct 11, 2021
1 parent 6391c0f commit 67721bf
Show file tree
Hide file tree
Showing 8 changed files with 526 additions and 223 deletions.
58 changes: 38 additions & 20 deletions README.md
Expand Up @@ -23,25 +23,21 @@
<!-- ABOUT THE PROJECT -->
## About The Project

```shell
Incomplete entries:
| Task | Summary | Billed| Unbilled |
| | | | |
| | | 26m16s| 0s |
| | | | |
| | Total time spent: | 26m16s| 0s |

Complete entries:
| Task | Summary | Billed | Unbilled |
| | | | |
| TA-4685 | Read developer reviews | 8m43s | 0s |
| TA-4305 | Check ticket updates and clean up after the ticket | 23m25s | 0s |
| TA-4815 | Review ticket | 43m22s | 0s |
| TA-4869 | Continue the discovery document | 3h24m2s | 0s |
| TA-4869 | Read the API developer documentations | 1h56m15s | 0s |
| TA-4909 | Participate in firefighting | 25m37s | 0s |
| | | | |
| | Total time spent: | 7h1m24s | 0s |
```plaintext
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Worklog entries (2021-10-04 02:00:00 +0200 CEST - 2021-10-05 02:00:00 +0200 CEST) │
├────┬─────────┬────────────────────────────────┬──────────────────────┬───────────────┬─────────────────────┬─────────────────────┬───────────┬──────────┤
│ │ TASK │ SUMMARY │ PROJECT │ CLIENT │ START │ END │ BILLED │ UNBILLED │
├────┼─────────┼────────────────────────────────┼──────────────────────┼───────────────┼─────────────────────┼─────────────────────┼───────────┼──────────┤
│ 1 │ MIN-001 │ Create an mkdocs based doc... │ Document every... │ Example Corp. │ 2021-10-04 08:00:00 │ 2021-10-04 08:20:37 │ 20m37s │ 0s │
│ 2 │ MIN-002 │ Add column text truncating │ Time syncing tool │ Example Corp. │ 2021-10-04 14:33:56 │ 2021-10-04 14:46:52 │ 12m56s │ 0s │
│ 3 │ MIN-007 │ Some very long summary tha... │ Time syncing tool │ Example Corp. │ 2021-10-04 15:45:32 │ 2021-10-04 15:53:21 │ 7m49s │ 0s │
│ 4 │ MIN-008 │ New table formatted output │ Time syncing tool │ Example Corp. │ 2021-10-04 19:11:51 │ 2021-10-04 19:56:01 │ 44m10s │ 0s │
│ 5 │ MIN-014 │ Debug time parsing issues │ Time syncing tool │ Example Corp. │ 2021-10-04 21:44:05 │ 2021-10-04 22:15:53 │ 31m48s │ 0s │
├────┼─────────┼────────────────────────────────┼──────────────────────┼───────────────┼─────────────────────┼─────────────────────┼───────────┼──────────┤
│ │ │ │ │ │ │ total time spent │ 10h18m11s │ 0s │
└────┴─────────┴────────────────────────────────┴──────────────────────┴───────────────┴─────────────────────┴─────────────────────┴───────────┴──────────┘
You have 5 complete and 0 incomplete items. Before proceeding, please double-check them.
Continue? [y/n]:
```
Expand Down Expand Up @@ -86,6 +82,8 @@ Flags:
-s, --source string set the source of the sync [clockify tempo]
--source-user string set the source user ID
--start string set the start date (defaults to 00:00:00)
--table-hide-column strings hide table column [summary project client start end]
--table-sort-by strings sort table by column [task summary project client start end billable unbillable] (default [start,project,task,summary])
-t, --target string set the target of the sync [tempo]
--target-user string set the source user ID
--tasks-as-tags treat tags matching the value of tasks-as-tags-regex as tasks
Expand Down Expand Up @@ -137,7 +135,7 @@ $ minutes --tasks-as-tags --tasks-as-tags-regex '[A-Z]{2,7}-\d{1,6}'
$ minutes --round-to-closest-minute
```

### Simple config file
### Sample config file

```toml
# Source config
Expand All @@ -161,6 +159,26 @@ tasks-as-tags = true
tasks-as-tags-regex = '[A-Z]{2,7}-\d{1,6}'
round-to-closest-minute = true
force-billed-duration = true

table-sort-by = [
"start",
"project",
"task",
"summary",
]

table-hide-column = [
"end"
]

[table-column-truncates]
summary = 40
project = 10
client = 10

# Column Config
[table-column-config.summary]
widthmax = 40
```

## Supported tools
Expand Down
181 changes: 162 additions & 19 deletions cmd/root.go
Expand Up @@ -2,20 +2,30 @@ package cmd

import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"regexp"
"strings"

"github.com/gabor-boros/minutes/internal/cmd/printer"
"github.com/gabor-boros/minutes/internal/cmd/utils"
"github.com/gabor-boros/minutes/internal/pkg/client/clockify"
"github.com/gabor-boros/minutes/internal/pkg/client/tempo"

"github.com/jedib0t/go-pretty/v6/table"

"github.com/gabor-boros/minutes/internal/pkg/client"
"github.com/gabor-boros/minutes/internal/pkg/worklog"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"os"
"regexp"
"sort"
"strings"
)

const (
program string = "minutes"
worklogTableFormat string = "| %s\t| %s\t| %s\t| %s\t|\n"
program string = "minutes"
defaultDateFormat string = "2006-01-02 15:04:05"
)

var (
Expand All @@ -29,6 +39,9 @@ var (
sources = []string{"clockify", "tempo"}
targets = []string{"tempo"}

ErrNoSourceImplementation = errors.New("no source implementation found")
ErrNoTargetImplementation = errors.New("no target implementation found")

rootCmd = &cobra.Command{
Use: program,
Short: "Sync worklogs between multiple time trackers, invoicing, and bookkeeping software.",
Expand Down Expand Up @@ -94,14 +107,17 @@ func initCommonFlags() {

rootCmd.Flags().StringP("start", "", "", "set the start date (defaults to 00:00:00)")
rootCmd.Flags().StringP("end", "", "", "set the end date (defaults to now)")
rootCmd.Flags().StringP("date-format", "", "2006-01-02 15:04:05", "set start and end date format (in Go style)")
rootCmd.Flags().StringP("date-format", "", defaultDateFormat, "set start and end date format (in Go style)")

rootCmd.Flags().StringP("source-user", "", "", "set the source user ID")
rootCmd.Flags().StringP("source", "s", "", fmt.Sprintf("set the source of the sync %v", sources))

rootCmd.Flags().StringP("target-user", "", "", "set the source user ID")
rootCmd.Flags().StringP("target", "t", "", fmt.Sprintf("set the target of the sync %v", targets))

rootCmd.Flags().StringSliceP("table-sort-by", "", []string{printer.ColumnStart, printer.ColumnProject, printer.ColumnTask, printer.ColumnSummary}, fmt.Sprintf("sort table by column %v", printer.Columns))
rootCmd.Flags().StringSliceP("table-hide-column", "", []string{}, fmt.Sprintf("hide table column %v", printer.HideableColumns))

rootCmd.Flags().BoolP("tasks-as-tags", "", false, "treat tags matching the value of tasks-as-tags-regex as tasks")
rootCmd.Flags().StringP("tasks-as-tags-regex", "", "", "regex of the task pattern")

Expand Down Expand Up @@ -133,11 +149,11 @@ func validateFlags() {
cobra.CheckErr("sync source cannot match the target")
}

if !isSliceContains(source, sources) {
if !utils.IsSliceContains(source, sources) {
cobra.CheckErr(fmt.Sprintf("\"%s\" is not part of the supported sources %v\n", source, sources))
}

if !isSliceContains(target, targets) {
if !utils.IsSliceContains(target, targets) {
cobra.CheckErr(fmt.Sprintf("\"%s\" is not part of the supported targets %v\n", target, targets))
}

Expand All @@ -151,6 +167,119 @@ func validateFlags() {
_, err := regexp.Compile(tasksAsTagsRegex)
cobra.CheckErr(err)
}

for _, sortBy := range viper.GetStringSlice("table-sort-by") {
column := sortBy

if strings.HasPrefix(column, "-") {
column = sortBy[1:]
}

if !utils.IsSliceContains(column, printer.Columns) {
cobra.CheckErr(fmt.Sprintf("\"%s\" is not part of the sortable columns %v\n", column, printer.Columns))
}
}

for _, column := range viper.GetStringSlice("table-hide-column") {
if !utils.IsSliceContains(column, printer.HideableColumns) {
cobra.CheckErr(fmt.Sprintf("\"%s\" is not part of the hideable columns %v\n", column, printer.HideableColumns))
}
}
}

func getClientOpts(urlFlag string, usernameFlag string, passwordFlag string, tokenFlag string, tokenHeader string) (*client.BaseClientOpts, error) {
opts := &client.BaseClientOpts{
HTTPClientOptions: client.HTTPClientOptions{
HTTPClient: http.DefaultClient,
TokenHeader: tokenHeader,
},
TasksAsTags: viper.GetBool("tasks-as-tags"),
TasksAsTagsRegex: viper.GetString("tasks-as-tags-regex"),
}

baseURL, err := url.Parse(viper.GetString(urlFlag))
if err != nil {
return opts, err
}

if usernameFlag != "" {
opts.Username = viper.GetString(usernameFlag)
}

if passwordFlag != "" {
opts.Password = viper.GetString(passwordFlag)
}

if tokenFlag != "" {
opts.Token = viper.GetString(tokenFlag)
}

opts.BaseURL = baseURL.String()

return opts, nil
}

func getFetcher() (client.Fetcher, error) {
switch viper.GetString("source") {
case "clockify":
opts, err := getClientOpts(
"clockify-url",
"",
"",
"clockify-api-key",
"X-Api-Key",
)

if err != nil {
return nil, err
}

return clockify.NewClient(&clockify.ClientOpts{
BaseClientOpts: *opts,
Workspace: viper.GetString("clockify-workspace"),
}), nil
case "tempo":
opts, err := getClientOpts(
"tempo-url",
"tempo-username",
"tempo-password",
"",
"",
)

if err != nil {
return nil, err
}

return tempo.NewClient(&tempo.ClientOpts{
BaseClientOpts: *opts,
}), nil
default:
return nil, ErrNoSourceImplementation
}
}

func getUploader() (client.Uploader, error) {
switch viper.GetString("target") {
case "tempo":
opts, err := getClientOpts(
"tempo-url",
"tempo-username",
"tempo-password",
"",
"",
)

if err != nil {
return nil, err
}

return tempo.NewClient(&tempo.ClientOpts{
BaseClientOpts: *opts,
}), nil
default:
return nil, ErrNoTargetImplementation
}
}

func runRootCmd(_ *cobra.Command, _ []string) {
Expand All @@ -163,10 +292,12 @@ func runRootCmd(_ *cobra.Command, _ []string) {

validateFlags()

start, err := getTime(viper.GetString("start"))
dateFormat := viper.GetString("date-format")

start, err := utils.GetTime(viper.GetString("start"), dateFormat)
cobra.CheckErr(err)

end, err := getTime(viper.GetString("end"))
end, err := utils.GetTime(viper.GetString("end"), dateFormat)
cobra.CheckErr(err)

fetcher, err := getFetcher()
Expand All @@ -186,18 +317,30 @@ func runRootCmd(_ *cobra.Command, _ []string) {
completeEntries := wl.CompleteEntries()
incompleteEntries := wl.IncompleteEntries()

sort.Slice(completeEntries, func(i int, j int) bool {
return completeEntries[i].Task.Name < completeEntries[j].Task.Name
})
columnTruncates := map[string]int{}
err = viper.UnmarshalKey("table-column-truncates", &columnTruncates)
cobra.CheckErr(err)

sort.Slice(incompleteEntries, func(i int, j int) bool {
return incompleteEntries[i].Task.Name < incompleteEntries[j].Task.Name
tablePrinter := printer.NewTablePrinter(&printer.TablePrinterOpts{
BasePrinterOpts: printer.BasePrinterOpts{
Output: os.Stdout,
AutoIndex: true,
Title: fmt.Sprintf("Worklog entries (%s - %s)", start.Local().String(), end.Local().String()),
SortBy: viper.GetStringSlice("table-sort-by"),
HiddenColumns: viper.GetStringSlice("table-hide-column"),
},
Style: table.StyleLight,
ColumnConfig: printer.ParseColumnConfigs(
"table-column-config.%s",
viper.GetStringSlice("table-hide-column"),
),
ColumnTruncates: columnTruncates,
})

printEntries("Incomplete entries", incompleteEntries)
printEntries("Complete entries", completeEntries)
err = tablePrinter.Print(completeEntries, incompleteEntries)
cobra.CheckErr(err)

if strings.ToLower(prompt("Continue? [y/n]")) != "y" {
if strings.ToLower(utils.Prompt("Continue? [y/n]: ")) != "y" {
fmt.Println("User interruption. Aborting.")
os.Exit(0)
}
Expand Down

0 comments on commit 67721bf

Please sign in to comment.