From 35fe78d3751ec09923932cece90f4ac03629816f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andy=20M=C3=A9ry?= Date: Sun, 3 Dec 2023 16:26:17 +0100 Subject: [PATCH] feat(ui): add created at (#36) --- go.mod | 1 + go.sum | 4 ++ internal/resource/resource.go | 5 ++ internal/resource/scaleway/cockpit.go | 1 + internal/resource/scaleway/container.go | 1 + .../resource/scaleway/container_namespace.go | 1 + internal/resource/scaleway/function.go | 1 + .../resource/scaleway/function_namespace.go | 1 + internal/resource/scaleway/iam_application.go | 1 + internal/resource/scaleway/instance.go | 1 + internal/resource/scaleway/job_definition.go | 1 + internal/resource/scaleway/job_run.go | 14 ++-- internal/resource/scaleway/kapsule_cluster.go | 1 + internal/resource/scaleway/project.go | 1 + internal/resource/scaleway/rdb_instance.go | 1 + .../resource/scaleway/registry_namespace.go | 1 + internal/ui/table/builder.go | 69 +++++++++++++++++-- internal/ui/table/builder_test.go | 22 ++++++ internal/ui/table/table.go | 2 +- 19 files changed, 118 insertions(+), 11 deletions(-) create mode 100644 internal/ui/table/builder_test.go diff --git a/go.mod b/go.mod index 443fe80..dd643d4 100644 --- a/go.mod +++ b/go.mod @@ -53,6 +53,7 @@ require ( github.com/rivo/uniseg v0.4.4 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/sahilm/fuzzy v0.1.0 // indirect + github.com/xeonx/timeago v1.0.0-rc5 // indirect go.etcd.io/bbolt v1.3.8 // indirect golang.org/x/sys v0.14.0 // indirect golang.org/x/term v0.13.0 // indirect diff --git a/go.sum b/go.sum index 2986ab3..c15d75d 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/RoaringBitmap/roaring v1.6.0 h1:dc7kRiroETgJcHhWX6BerXkZz2b3JgLGg9nTURJL/og= github.com/RoaringBitmap/roaring v1.6.0/go.mod h1:plvDsJQpxOC5bw8LRteu/MLWHsHez/3y6cubLI4/1yE= +github.com/SerhiiCho/timeago v0.0.0-20231128175802-7b007dc561f3 h1:uMjbcIh1uecu/6PWJFwSF/1GVubBUeOtAsaoI2yEq8M= +github.com/SerhiiCho/timeago v0.0.0-20231128175802-7b007dc561f3/go.mod h1:EPnGJQJwLfmanxiCk75E/Xy5Cl/ZO7BlVqrank2a/nM= github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink= github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= github.com/alecthomas/chroma/v2 v2.11.1 h1:m9uUtgcdAwgfFNxuqj7AIG75jD2YmL61BBIJWtdzJPs= @@ -143,6 +145,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/xeonx/timeago v1.0.0-rc5 h1:pwcQGpaH3eLfPtXeyPA4DmHWjoQt0Ea7/++FwpxqLxg= +github.com/xeonx/timeago v1.0.0-rc5/go.mod h1:qDLrYEFynLO7y5Ho7w3GwgtYgpy5UfhcXIIQvMKVDkA= go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= diff --git a/internal/resource/resource.go b/internal/resource/resource.go index 84565b8..fdc415c 100644 --- a/internal/resource/resource.go +++ b/internal/resource/resource.go @@ -2,6 +2,7 @@ package resource import ( "context" + "time" "github.com/scaleway/scaleway-sdk-go/scw" ) @@ -24,6 +25,10 @@ type Metadata struct { // May be empty. Description *string `json:"description"` + // CreatedAt is the date the resource was created. + // Maybe nil if not available. + CreatedAt *time.Time `json:"created_at,omitempty"` + // Tags is the list of tags associated with the resource. Tags []string `json:"tags"` diff --git a/internal/resource/scaleway/cockpit.go b/internal/resource/scaleway/cockpit.go index fd056c7..07060ef 100644 --- a/internal/resource/scaleway/cockpit.go +++ b/internal/resource/scaleway/cockpit.go @@ -18,6 +18,7 @@ func (c Cockpit) Metadata() resource.Metadata { ProjectID: c.ProjectID, Status: statusPtr(c.Status), Description: nil, + CreatedAt: c.CreatedAt, Tags: nil, Type: resource.TypeCockpit, Locality: resource.Global, diff --git a/internal/resource/scaleway/container.go b/internal/resource/scaleway/container.go index 78a3253..5a0ecb8 100644 --- a/internal/resource/scaleway/container.go +++ b/internal/resource/scaleway/container.go @@ -21,6 +21,7 @@ func (c Container) Metadata() resource.Metadata { ProjectID: c.Namespace.ProjectID, Status: statusPtr(c.Container.Status), Description: c.Container.Description, + CreatedAt: nil, Tags: nil, Type: resource.TypeContainer, Locality: resource.Region(c.Container.Region), diff --git a/internal/resource/scaleway/container_namespace.go b/internal/resource/scaleway/container_namespace.go index 9b9907f..4190c74 100644 --- a/internal/resource/scaleway/container_namespace.go +++ b/internal/resource/scaleway/container_namespace.go @@ -17,6 +17,7 @@ func (ns ContainerNamespace) Metadata() resource.Metadata { ProjectID: ns.ProjectID, Status: statusPtr(ns.Status), Description: ns.Description, + CreatedAt: nil, Tags: nil, Type: resource.TypeContainerNamespace, Locality: resource.Region(ns.Region), diff --git a/internal/resource/scaleway/function.go b/internal/resource/scaleway/function.go index 39f9e9f..0a975c8 100644 --- a/internal/resource/scaleway/function.go +++ b/internal/resource/scaleway/function.go @@ -21,6 +21,7 @@ func (f Function) Metadata() resource.Metadata { ProjectID: f.Namespace.ProjectID, Status: statusPtr(f.Function.Status), Description: f.Function.Description, + CreatedAt: nil, Tags: nil, Type: resource.TypeFunction, Locality: resource.Region(f.Function.Region), diff --git a/internal/resource/scaleway/function_namespace.go b/internal/resource/scaleway/function_namespace.go index 51342c4..60bdb73 100644 --- a/internal/resource/scaleway/function_namespace.go +++ b/internal/resource/scaleway/function_namespace.go @@ -17,6 +17,7 @@ func (ns FunctionNamespace) Metadata() resource.Metadata { ProjectID: ns.ProjectID, Status: statusPtr(ns.Status), Description: ns.Description, + CreatedAt: nil, Tags: nil, Type: resource.TypeFunctionNamespace, Locality: resource.Region(ns.Region), diff --git a/internal/resource/scaleway/iam_application.go b/internal/resource/scaleway/iam_application.go index ba223e2..cba6f3c 100644 --- a/internal/resource/scaleway/iam_application.go +++ b/internal/resource/scaleway/iam_application.go @@ -17,6 +17,7 @@ func (app IAMApplication) Metadata() resource.Metadata { ProjectID: "", Status: nil, Description: &app.Description, + CreatedAt: app.CreatedAt, Tags: nil, Type: resource.TypeIAMApplication, Locality: resource.Global, diff --git a/internal/resource/scaleway/instance.go b/internal/resource/scaleway/instance.go index 35084fe..368cb0c 100644 --- a/internal/resource/scaleway/instance.go +++ b/internal/resource/scaleway/instance.go @@ -17,6 +17,7 @@ func (i Instance) Metadata() resource.Metadata { ProjectID: i.Project, Status: statusPtr(i.State), Description: nil, + CreatedAt: i.CreationDate, Tags: i.Tags, Type: resource.TypeInstance, Locality: resource.Zone(i.Zone), diff --git a/internal/resource/scaleway/job_definition.go b/internal/resource/scaleway/job_definition.go index 3595a22..d8681e4 100644 --- a/internal/resource/scaleway/job_definition.go +++ b/internal/resource/scaleway/job_definition.go @@ -16,6 +16,7 @@ func (def JobDefinition) Metadata() resource.Metadata { Name: def.Name, ProjectID: def.ProjectID, Description: &def.Description, + CreatedAt: def.CreatedAt, Type: resource.TypeJobDefinition, Locality: resource.Region(def.Region), } diff --git a/internal/resource/scaleway/job_run.go b/internal/resource/scaleway/job_run.go index 2daca47..870301f 100644 --- a/internal/resource/scaleway/job_run.go +++ b/internal/resource/scaleway/job_run.go @@ -16,12 +16,14 @@ type JobRun struct { func (run JobRun) Metadata() resource.Metadata { return resource.Metadata{ - ID: run.ID, - Name: run.JobDefinition.Name, - ProjectID: run.JobDefinition.ProjectID, - Status: statusPtr(run.State), - Type: resource.TypeJobRun, - Locality: resource.Region(run.Region), + ID: run.ID, + Name: run.JobDefinition.Name, + ProjectID: run.JobDefinition.ProjectID, + Status: statusPtr(run.State), + Description: nil, + CreatedAt: run.CreatedAt, + Type: resource.TypeJobRun, + Locality: resource.Region(run.Region), } } diff --git a/internal/resource/scaleway/kapsule_cluster.go b/internal/resource/scaleway/kapsule_cluster.go index 7eb3694..c8defcc 100644 --- a/internal/resource/scaleway/kapsule_cluster.go +++ b/internal/resource/scaleway/kapsule_cluster.go @@ -17,6 +17,7 @@ func (c KapsuleCluster) Metadata() resource.Metadata { ProjectID: c.ProjectID, Status: statusPtr(c.Status), Description: &c.Description, + CreatedAt: c.CreatedAt, Tags: c.Tags, Type: resource.TypeKapsuleCluster, Locality: resource.Region(c.Region), diff --git a/internal/resource/scaleway/project.go b/internal/resource/scaleway/project.go index dcc0ce8..3a926ea 100644 --- a/internal/resource/scaleway/project.go +++ b/internal/resource/scaleway/project.go @@ -18,6 +18,7 @@ func (p Project) Metadata() resource.Metadata { ProjectID: p.ID, Status: nil, Description: &p.Description, + CreatedAt: p.CreatedAt, Tags: nil, Type: resource.TypeProject, Locality: resource.Global, diff --git a/internal/resource/scaleway/rdb_instance.go b/internal/resource/scaleway/rdb_instance.go index 66feb2a..0286a10 100644 --- a/internal/resource/scaleway/rdb_instance.go +++ b/internal/resource/scaleway/rdb_instance.go @@ -17,6 +17,7 @@ func (i RdbInstance) Metadata() resource.Metadata { ProjectID: i.ProjectID, Status: statusPtr(i.Status), Description: nil, + CreatedAt: i.CreatedAt, Tags: i.Tags, Type: resource.TypeRdbInstance, Locality: resource.Region(i.Region), diff --git a/internal/resource/scaleway/registry_namespace.go b/internal/resource/scaleway/registry_namespace.go index a6fe7e1..58581c3 100644 --- a/internal/resource/scaleway/registry_namespace.go +++ b/internal/resource/scaleway/registry_namespace.go @@ -17,6 +17,7 @@ func (ns RegistryNamespace) Metadata() resource.Metadata { ProjectID: ns.ProjectID, Status: statusPtr(ns.Status), Description: &ns.Description, + CreatedAt: ns.CreatedAt, Tags: nil, Type: resource.TypeRegistryNamespace, Locality: resource.Region(ns.Region), diff --git a/internal/ui/table/builder.go b/internal/ui/table/builder.go index 2529d11..180ffbd 100644 --- a/internal/ui/table/builder.go +++ b/internal/ui/table/builder.go @@ -1,10 +1,14 @@ package table import ( + "sync" + "time" + "github.com/charmbracelet/lipgloss" "github.com/cyclimse/scwtui/internal/resource" table "github.com/cyclimse/scwtui/internal/ui/table/custom" "github.com/mattn/go-runewidth" + "github.com/xeonx/timeago" ) const widthOfAnUUID = 36 @@ -16,6 +20,7 @@ var ( "Name", "Type", "Project", + "Created", "Locality", } @@ -24,6 +29,7 @@ var ( "ID", "Type", "Project ID", + "Created At", "Locality", } @@ -32,13 +38,32 @@ var ( "Locality": runewidth.StringWidth("Locality"), "ID": widthOfAnUUID, "Project ID": widthOfAnUUID, + "Created At": runewidth.StringWidth(time.RFC3339), + "Type": runewidth.StringWidth(resource.TypeContainerNamespace.String()), + "Created": runewidth.StringWidth("about an hour ago"), } ) func NewBuilder(styles table.Styles) *Build { - return &Build{ - styles: styles, + b := &Build{ + styles: styles, + timeagoCache: make(map[time.Time]string), + mutex: &sync.Mutex{}, } + + timer := time.NewTicker(time.Minute) + + go func() { + // purge the cache every minute to allow timeago to update. + // eg. "a few seconds ago" -> "a minute ago" + for range timer.C { + b.mutex.Lock() + b.timeagoCache = make(map[time.Time]string) + b.mutex.Unlock() + } + }() + + return b } type BuildParams struct { @@ -76,6 +101,7 @@ func (b *Build) buildRows(params BuildParams) []table.Row { metadata.Name, metadata.Type.String(), params.ProjectIDsToNames[metadata.ProjectID], + b.formattedTimeAgo(metadata.CreatedAt), metadata.Locality.String(), }) } @@ -89,11 +115,18 @@ func (b *Build) buildRowsAlt(params BuildParams) []table.Row { for _, r := range resources { metadata := r.Metadata() + + createdAt := "" + if metadata.CreatedAt != nil { + createdAt = metadata.CreatedAt.Format(time.RFC3339) + } + rows = append(rows, table.Row{ lipgloss.PlaceHorizontal(6, lipgloss.Center, string(metadata.Status.Emoji(metadata.Type))), metadata.ID, metadata.Type.String(), metadata.ProjectID, + createdAt, metadata.Locality.String(), }) } @@ -119,7 +152,11 @@ func (b *Build) buildCols(params BuildParams) []table.Column { } } - widthPerColumn := (widthWithPadding - fixedColumnsWidth) / (len(titles) - fixedColumnsCount) + widthPerColumn := 0 + if len(titles) > fixedColumnsCount { + widthPerColumn = (widthWithPadding - fixedColumnsWidth) / (len(titles) - fixedColumnsCount) + } + if widthPerColumn < 0 { widthPerColumn = 0 } @@ -139,9 +176,33 @@ func (b *Build) buildCols(params BuildParams) []table.Column { }) } + if widthPerColumn == 0 { + cols[len(cols)-1].Width += widthWithPadding - fixedColumnsWidth + } + return cols } +func (b Build) formattedTimeAgo(t *time.Time) string { + if t == nil { + return "" + } + + b.mutex.Lock() + defer b.mutex.Unlock() + + if formatted, ok := b.timeagoCache[*t]; ok { + return formatted + } + + formatted := timeago.English.Format(*t) + b.timeagoCache[*t] = formatted + + return formatted +} + type Build struct { - styles table.Styles + styles table.Styles + timeagoCache map[time.Time]string + mutex *sync.Mutex } diff --git a/internal/ui/table/builder_test.go b/internal/ui/table/builder_test.go new file mode 100644 index 0000000..858939b --- /dev/null +++ b/internal/ui/table/builder_test.go @@ -0,0 +1,22 @@ +package table + +import ( + "testing" + + "github.com/cyclimse/scwtui/internal/resource" + "github.com/mattn/go-runewidth" + "github.com/stretchr/testify/assert" +) + +func TestColumnsWithFixedWidth(t *testing.T) { + longestResourceType := 0 + + for i := 0; i < int(resource.NumberOfResourceTypes); i++ { + typeName := resource.Type(i).String() + if runewidth.StringWidth(typeName) > longestResourceType { + longestResourceType = runewidth.StringWidth(typeName) + } + } + + assert.Equal(t, longestResourceType, columnsWithFixedWidth["Type"]) +} diff --git a/internal/ui/table/table.go b/internal/ui/table/table.go index e89dd61..de1a535 100644 --- a/internal/ui/table/table.go +++ b/internal/ui/table/table.go @@ -43,7 +43,7 @@ func Table(state ui.ApplicationState) Model { func (m Model) Init() tea.Cmd { return nil } const ( - additionalHorizontalPadding = 8 + additionalHorizontalPadding = 10 tableHeaderHeight = 4 )