Skip to content

Commit

Permalink
Improve time fields (#171)
Browse files Browse the repository at this point in the history
* Add cobra time arg type

* Add time parsing to form and structs

* Add placeholder and suggestions

* Add missing enum tests
  • Loading branch information
mdbenjam authored Dec 6, 2024
1 parent 7892634 commit 7932c13
Show file tree
Hide file tree
Showing 10 changed files with 291 additions and 37 deletions.
9 changes: 4 additions & 5 deletions cmd/deploylist.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"github.com/renderinc/cli/pkg/command"
"github.com/renderinc/cli/pkg/dashboard"
"github.com/renderinc/cli/pkg/deploy"
"github.com/renderinc/cli/pkg/pointers"
"github.com/renderinc/cli/pkg/resource"
"github.com/renderinc/cli/pkg/text"
"github.com/renderinc/cli/pkg/tui/views"
Expand Down Expand Up @@ -57,14 +56,14 @@ func interactiveDeployList(cmd *cobra.Command, input views.DeployListInput) tea.
}

func commandsForDeploy(dep *client.Deploy, serviceID, serviceType string) []views.PaletteCommand {
var startTime *string
var startTime *command.TimeOrRelative
if dep.CreatedAt != nil {
startTime = pointers.From(dep.CreatedAt.String())
startTime = &command.TimeOrRelative{T: dep.CreatedAt}
}

var endTime *string
var endTime *command.TimeOrRelative
if dep.FinishedAt != nil {
endTime = pointers.From(dep.FinishedAt.String())
endTime = &command.TimeOrRelative{T: dep.FinishedAt}
}

commands := []views.PaletteCommand{
Expand Down
13 changes: 7 additions & 6 deletions cmd/joblist.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
clientjob "github.com/renderinc/cli/pkg/client/jobs"
"github.com/renderinc/cli/pkg/command"
"github.com/renderinc/cli/pkg/job"
"github.com/renderinc/cli/pkg/pointers"
"github.com/renderinc/cli/pkg/resource"
"github.com/renderinc/cli/pkg/text"
"github.com/renderinc/cli/pkg/tui/views"
Expand Down Expand Up @@ -41,7 +40,9 @@ func interactiveJobList(cmd *cobra.Command, input views.JobListInput) tea.Cmd {
"Jobs",
&input,
views.NewServiceList(ctx, views.ServiceInput{
Types: []client.ServiceType{client.WebService, client.BackgroundWorker, client.PrivateService, client.CronJob},
Types: []client.ServiceType{
client.WebService, client.BackgroundWorker, client.PrivateService, client.CronJob,
},
}, func(ctx context.Context, r resource.Resource) tea.Cmd {
input.ServiceID = r.ID()
return InteractiveJobList(ctx, input, resource.BreadcrumbForResource(r))
Expand All @@ -58,14 +59,14 @@ func interactiveJobList(cmd *cobra.Command, input views.JobListInput) tea.Cmd {
}

func commandsForJob(j *clientjob.Job) []views.PaletteCommand {
var startTime *string
var startTime *command.TimeOrRelative
if j.StartedAt != nil {
startTime = pointers.From(j.StartedAt.String())
startTime = &command.TimeOrRelative{T: j.StartedAt}
}

var endTime *string
var endTime *command.TimeOrRelative
if j.FinishedAt != nil {
endTime = pointers.From(j.FinishedAt.String())
endTime = &command.TimeOrRelative{T: j.FinishedAt}
}

commands := []views.PaletteCommand{
Expand Down
9 changes: 6 additions & 3 deletions cmd/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func TailResourceLogs(ctx context.Context, resourceID string) tea.Cmd {
return InteractiveLogs(
ctx,
views.LogInput{
StartTime: pointers.From(time.Now().Format(time.RFC3339)),
StartTime: &command.TimeOrRelative{T: pointers.From(time.Now())},
ResourceIDs: []string{resourceID},
Tail: true,
}, "Logs")
Expand Down Expand Up @@ -128,6 +128,9 @@ func init() {
"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD", "CONNECT", "TRACE",
}, true)

startTimeFlag := command.NewTimeInput()
endTimeFlag := command.NewTimeInput()

LogsCmd.RunE = func(cmd *cobra.Command, args []string) error {
var input views.LogInput
err := command.ParseCommand(cmd, args, &input)
Expand Down Expand Up @@ -156,8 +159,8 @@ func init() {
rootCmd.AddCommand(LogsCmd)

LogsCmd.Flags().StringSliceP("resources", "r", []string{}, "A list of comma separated resource IDs to query. Required in non-interactive mode.")
LogsCmd.Flags().String("start", "", "The start time of the logs to query")
LogsCmd.Flags().String("end", "", "The end time of the logs to query")
LogsCmd.Flags().Var(startTimeFlag, "start", "The start time of the logs to query")
LogsCmd.Flags().Var(endTimeFlag, "end", "The end time of the logs to query")
LogsCmd.Flags().StringSlice("text", []string{}, "A list of comma separated strings to search for in the logs")
LogsCmd.Flags().Var(levelFlag, "level", "A list of comma separated log levels to query")
LogsCmd.Flags().Var(logTypeFlag, "type", "A list of comma separated log types to query")
Expand Down
35 changes: 34 additions & 1 deletion pkg/command/form.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"reflect"
"strconv"
"strings"
"time"

"github.com/charmbracelet/huh"
"github.com/charmbracelet/x/ansi"
Expand Down Expand Up @@ -63,7 +64,13 @@ func FormValuesFromStruct(v any) FormValues {
case reflect.Ptr:
if elemField.IsNil() {
formValues[cliTag] = NewStringFormValue("")
break
continue
}

if field.Type == reflect.TypeOf(&TimeOrRelative{}) {
val := elemField.Interface().(*TimeOrRelative)
formValues[cliTag] = NewStringFormValue(val.String())
continue
}

switch field.Type.Elem().Kind() {
Expand Down Expand Up @@ -159,6 +166,21 @@ func StructFromFormValues(formValues FormValues, v any) error {

switch field.Type.Kind() {
case reflect.Ptr:
if field.Type == reflect.TypeOf(&TimeOrRelative{}) {
val, ok := formValues[cliTag]
if !ok {
continue
}

timeOrRelative, err := ParseTime(time.Now(), pointers.From(val.String()))
if err != nil {
return err
}

elemField.Set(reflect.ValueOf(timeOrRelative))
continue
}

switch field.Type.Elem().Kind() {
case reflect.String:
val, ok := formValues[cliTag]
Expand Down Expand Up @@ -337,6 +359,17 @@ func HuhFormFields(cmd *cobra.Command, v any) ([]huh.Field, FormValues) {

huhFieldMap[flag.Name] = huh.NewSelect[string]().Key(flag.Name).Title(flag.Name).Description(wrappedDescription).Options(options...).Value((*string)(strValue))
}
} else if flag.Value.Type() == TimeType {
timeValue := NewStringFormValue(value.String())
formValues[flag.Name] = timeValue

huhFieldMap[flag.Name] = huh.NewInput().
Key(flag.Name).
Title(flag.Name).
Description(wrappedDescription).
Value((*string)(timeValue)).
Placeholder(fmt.Sprintf("Relative time or %s", time.RFC3339)).
SuggestionsFunc(func() []string { return TimeSuggestion(timeValue.String()) }, timeValue)
} else {
strValue := NewStringFormValue(value.String())
formValues[flag.Name] = strValue
Expand Down
53 changes: 53 additions & 0 deletions pkg/command/form_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package command_test

import (
"testing"
"time"

"github.com/charmbracelet/huh"
"github.com/renderinc/cli/pkg/command"
Expand Down Expand Up @@ -72,6 +73,38 @@ func TestStructFromFormValues(t *testing.T) {
require.NoError(t, command.StructFromFormValues(formValues, &v))
require.Equal(t, []string{"owner-id-1", "owner-id-2"}, v.OwnerIDs)
})

t.Run("converts time type", func(t *testing.T) {
type testStruct struct {
Time *command.TimeOrRelative `cli:"time"`
}
str := "1m"
formValues := command.FormValues{"time": command.NewStringFormValue(str)}
v := testStruct{}
require.NoError(t, command.StructFromFormValues(formValues, &v))
require.Equal(t, "1m", v.Time.String())
require.WithinDuration(t, *v.Time.T, time.Now().Add(-time.Minute), time.Second)
})

t.Run("converts enum type", func(t *testing.T) {
type testStruct struct {
Foo string `cli:"foo"`
}
formValues := command.FormValues{"foo": command.NewStringFormValue("value")}
v := testStruct{}
require.NoError(t, command.StructFromFormValues(formValues, &v))
require.Equal(t, "value", v.Foo)
})

t.Run("converts enum multi type", func(t *testing.T) {
type testStruct struct {
Foo []string `cli:"foo"`
}
formValues := command.FormValues{"foo": command.NewStringFormValue("value,other")}
v := testStruct{}
require.NoError(t, command.StructFromFormValues(formValues, &v))
require.Equal(t, []string{"value", "other"}, v.Foo)
})
}

func TestHuhForm(t *testing.T) {
Expand Down Expand Up @@ -116,4 +149,24 @@ func TestHuhForm(t *testing.T) {
require.Contains(t, form.View(), "multi choice 3")
require.Contains(t, form.View(), "single choice 2")
})

t.Run("creates form with time", func(t *testing.T) {
type testStruct struct {
Foo *command.TimeOrRelative `cli:"foo"`
}
v := testStruct{}
cmd := cobra.Command{}

// foo is multi select
fooInput := command.NewTimeInput()
cmd.Flags().Var(fooInput, "foo", "")

fields, _ := command.HuhFormFields(&cmd, &v)
form := huh.NewForm(huh.NewGroup(fields...))
form.Init()

require.Contains(t, form.View(), "foo")
// Find placeholder text
require.Contains(t, form.View(), "Relative time or")
})
}
25 changes: 25 additions & 0 deletions pkg/command/inputs.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,21 @@ func getArgValue(tag string, args []string) (*string, error) {
return &args[index], nil
}

func getTimeValue(flags *pflag.FlagSet, tag string) (*TimeOrRelative, error) {
if flag := flags.Lookup(tag); flag != nil {
if flag.Value.Type() == TimeType {
cobraTime, ok := flag.Value.(*CobraTime)
if !ok {
return nil, fmt.Errorf("unexpected time type")
}
val := cobraTime.t
return val, nil
}
}

return nil, nil
}

func getStringValue(flags *pflag.FlagSet, args []string, tag string) (*string, error) {
if isArg(tag) {
if val, err := getArgValue(tag, args); err != nil {
Expand Down Expand Up @@ -176,6 +191,16 @@ func ParseCommand(cmd *cobra.Command, args []string, v any) error {

switch field.Type.Kind() {
case reflect.Ptr:
if field.Type == reflect.TypeOf(&TimeOrRelative{}) {
val, err := getTimeValue(flags, cliTag)
if err != nil {
return err
}

elemField.Set(reflect.ValueOf(val))
continue
}

switch field.Type.Elem().Kind() {
case reflect.String:
val, err := getStringValue(flags, args, cliTag)
Expand Down
19 changes: 19 additions & 0 deletions pkg/command/inputs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package command_test

import (
"testing"
"time"

"github.com/renderinc/cli/pkg/command"
"github.com/renderinc/cli/pkg/pointers"
Expand Down Expand Up @@ -59,6 +60,24 @@ func TestParseCommand(t *testing.T) {
require.Equal(t, []string{"a", "c"}, v.Foo)
})

t.Run("parse time", func(t *testing.T) {
timeInput := command.NewTimeInput()

type testStruct struct {
Foo *command.TimeOrRelative `cli:"foo"`
}
var v testStruct
cmd := &cobra.Command{}
cmd.Flags().Var(timeInput, "foo", "")
require.NoError(t, cmd.ParseFlags([]string{"--foo", "5m"}))

err := command.ParseCommand(cmd, []string{}, &v)
require.NoError(t, err)

require.Equal(t, "5m", *v.Foo.Relative)
require.WithinDuration(t, *v.Foo.T, time.Now().Add(-5*time.Minute), time.Second)
})

t.Run("parse pointer", func(t *testing.T) {
type testStruct struct {
Foo *string `cli:"foo"`
Expand Down
Loading

0 comments on commit 7932c13

Please sign in to comment.