Merge remote-tracking branch 'origin/feature/dynamo-query'
# Conflicts: # cmd/dynamo-browse/main.go # cmd/ssm-browse/main.go # docker-compose.yml # internal/dynamo-browse/ui/model.go # test/cmd/load-test-table/main.go
This commit is contained in:
commit
ffca588a2c
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
|
@ -16,7 +16,7 @@ jobs:
|
|||
postgres:
|
||||
image: amazon/dynamodb-local:latest
|
||||
ports:
|
||||
- 8000:8000
|
||||
- 18000:8000
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
@ -31,6 +31,6 @@ jobs:
|
|||
run: |
|
||||
set -xue
|
||||
go get ./...
|
||||
go test ./...
|
||||
go test -p 1 ./...
|
||||
env:
|
||||
GOPRIVATE: "github:com/lmika/*"
|
52
.github/workflows/release.yaml
vendored
Normal file
52
.github/workflows/release.yaml
vendored
Normal file
|
@ -0,0 +1,52 @@
|
|||
name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: amazon/dynamodb-local:latest
|
||||
ports:
|
||||
- 18000:8000
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.18
|
||||
- name: Configure
|
||||
run: |
|
||||
git config --global url."https://${{ secrets.GO_MODULES_TOKEN }}:x-oauth-basic@github.com/lmika".insteadOf "https://github.com/lmika"
|
||||
- name: Test
|
||||
run: |
|
||||
set -xue
|
||||
go get ./...
|
||||
go test -p 1 ./...
|
||||
env:
|
||||
GOPRIVATE: "github:com/lmika/*"
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.18
|
||||
- name: Configure
|
||||
run: |
|
||||
git config --global url."https://${{ secrets.GO_MODULES_TOKEN }}:x-oauth-basic@github.com/lmika".insteadOf "https://github.com/lmika"
|
||||
- name: Release
|
||||
uses: goreleaser/goreleaser-action@v1
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
version: latest
|
||||
args: release --skip-validate --rm-dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
54
.goreleaser.yml
Normal file
54
.goreleaser.yml
Normal file
|
@ -0,0 +1,54 @@
|
|||
builds:
|
||||
- id: dynamo-browse
|
||||
targets:
|
||||
- windows_amd64
|
||||
- linux_amd64
|
||||
- darwin_amd64
|
||||
- darwin_arm64
|
||||
main: ./cmd/dynamo-browse/.
|
||||
binary: dynamo-browse
|
||||
archives:
|
||||
- id: zip
|
||||
builds:
|
||||
- dynamo-browse
|
||||
wrap_in_directory: true
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
- goos: linux
|
||||
format: tar.gz
|
||||
- goos: macos
|
||||
format: tar.gz
|
||||
nfpms:
|
||||
- id: package_nfpms
|
||||
package_name: awstools
|
||||
builds:
|
||||
- dynamo-browse
|
||||
vendor: lmika
|
||||
homepage: https://awstools.lmika.dev/
|
||||
maintainer: Leon Mika <lmika@lmika.org>
|
||||
description: TUI tools for AWS administration
|
||||
license: MIT
|
||||
formats:
|
||||
- deb
|
||||
- rpm
|
||||
bindir: /usr/local/bin
|
||||
brews:
|
||||
- name: awstools
|
||||
tap:
|
||||
owner: lmika
|
||||
name: awstools
|
||||
folder: Formula
|
||||
homepage: https://awstools.lmika.dev
|
||||
description: TUI tools for AWS administration
|
||||
license: MIT
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-next"
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
|
@ -10,18 +10,21 @@ import (
|
|||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/lmika/awstools/internal/common/ui/commandctrl"
|
||||
"github.com/lmika/awstools/internal/common/ui/logging"
|
||||
"github.com/lmika/awstools/internal/common/ui/osstyle"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/controllers"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/providers/dynamo"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/services/tables"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui"
|
||||
"github.com/lmika/gopkgs/cli"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var flagTable = flag.String("t", "", "dynamodb table name")
|
||||
var flagLocal = flag.Bool("local", false, "local endpoint")
|
||||
var flagLocal = flag.String("local", "", "local endpoint")
|
||||
var flagDebug = flag.String("debug", "", "file to log debug messages")
|
||||
flag.Parse()
|
||||
|
||||
ctx := context.Background()
|
||||
|
@ -32,9 +35,19 @@ func main() {
|
|||
}
|
||||
|
||||
var dynamoClient *dynamodb.Client
|
||||
if *flagLocal {
|
||||
if *flagLocal != "" {
|
||||
host, port, err := net.SplitHostPort(*flagLocal)
|
||||
if err != nil {
|
||||
cli.Fatalf("invalid address '%v': %v", *flagLocal, err)
|
||||
}
|
||||
if host == "" {
|
||||
host = "localhost"
|
||||
}
|
||||
if port == "" {
|
||||
port = "8000"
|
||||
}
|
||||
dynamoClient = dynamodb.NewFromConfig(cfg,
|
||||
dynamodb.WithEndpointResolver(dynamodb.EndpointResolverFromURL("http://localhost:4566")))
|
||||
dynamodb.WithEndpointResolver(dynamodb.EndpointResolverFromURL(fmt.Sprintf("http://%v:%v", host, port))))
|
||||
} else {
|
||||
dynamoClient = dynamodb.NewFromConfig(cfg)
|
||||
}
|
||||
|
@ -57,6 +70,23 @@ func main() {
|
|||
closeFn := logging.EnableLogging()
|
||||
defer closeFn()
|
||||
|
||||
// Pre-determine if layout has dark background. This prevents calls for creating a list to hang.
|
||||
if lipgloss.HasDarkBackground() {
|
||||
if colorScheme := osstyle.CurrentColorScheme(); colorScheme == osstyle.ColorSchemeLightMode {
|
||||
log.Printf("terminal reads dark but really in light mode")
|
||||
lipgloss.SetHasDarkBackground(true)
|
||||
} else {
|
||||
log.Printf("in dark background")
|
||||
}
|
||||
} else {
|
||||
if colorScheme := osstyle.CurrentColorScheme(); colorScheme == osstyle.ColorSchemeDarkMode {
|
||||
log.Printf("terminal reads light but really in dark mode")
|
||||
lipgloss.SetHasDarkBackground(true)
|
||||
} else {
|
||||
log.Printf("cannot detect system darkmode")
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("launching")
|
||||
if err := p.Start(); err != nil {
|
||||
fmt.Printf("Alas, there's been an error: %v", err)
|
||||
|
|
|
@ -7,14 +7,15 @@ import (
|
|||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/lmika/awstools/internal/common/ui/commandctrl"
|
||||
"github.com/lmika/awstools/internal/common/ui/logging"
|
||||
"github.com/lmika/awstools/internal/slog-view/services/logreader"
|
||||
"github.com/lmika/awstools/internal/slog-view/controllers"
|
||||
"github.com/lmika/awstools/internal/slog-view/services/logreader"
|
||||
"github.com/lmika/awstools/internal/slog-view/ui"
|
||||
"github.com/lmika/gopkgs/cli"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var flagDebug = flag.String("debug", "", "file to log debug messages")
|
||||
flag.Parse()
|
||||
|
||||
if flag.NArg() == 0 {
|
||||
|
@ -24,7 +25,7 @@ func main() {
|
|||
// Pre-determine if layout has dark background. This prevents calls for creating a list to hang.
|
||||
lipgloss.HasDarkBackground()
|
||||
|
||||
closeFn := logging.EnableLogging()
|
||||
closeFn := logging.EnableLogging(*flagDebug)
|
||||
defer closeFn()
|
||||
|
||||
service := logreader.NewService()
|
||||
|
|
|
@ -20,12 +20,13 @@ import (
|
|||
|
||||
func main() {
|
||||
var flagLocal = flag.Bool("local", false, "local endpoint")
|
||||
var flagDebug = flag.String("debug", "", "file to log debug messages")
|
||||
flag.Parse()
|
||||
|
||||
// Pre-determine if layout has dark background. This prevents calls for creating a list to hang.
|
||||
lipgloss.HasDarkBackground()
|
||||
|
||||
closeFn := logging.EnableLogging()
|
||||
closeFn := logging.EnableLogging(*flagDebug)
|
||||
defer closeFn()
|
||||
|
||||
cfg, err := config.LoadDefaultConfig(context.Background())
|
||||
|
|
34
go.mod
34
go.mod
|
@ -5,16 +5,15 @@ go 1.18
|
|||
require (
|
||||
github.com/alecthomas/participle/v2 v2.0.0-alpha7
|
||||
github.com/asdine/storm v2.1.2+incompatible
|
||||
github.com/aws/aws-sdk-go-v2 v1.16.1
|
||||
github.com/aws/aws-sdk-go-v2 v1.16.5
|
||||
github.com/aws/aws-sdk-go-v2/config v1.13.1
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.8.0
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.8.0
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.15.0
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.9.4
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.15.7
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.16.0
|
||||
github.com/brianvoe/gofakeit/v6 v6.15.0
|
||||
github.com/calyptia/go-bubble-table v0.1.0
|
||||
github.com/charmbracelet/bubbles v0.10.3
|
||||
github.com/charmbracelet/bubbletea v0.20.0
|
||||
github.com/charmbracelet/bubbles v0.11.0
|
||||
github.com/charmbracelet/bubbletea v0.21.0
|
||||
github.com/charmbracelet/lipgloss v0.5.0
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/lmika/events v0.0.0-20200906102219-a2269cd4394e
|
||||
|
@ -26,34 +25,37 @@ require (
|
|||
|
||||
require (
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.4.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.13.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.13.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.24.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.9.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.14.0 // indirect
|
||||
github.com/aws/smithy-go v1.11.2 // indirect
|
||||
github.com/aws/smithy-go v1.11.3 // indirect
|
||||
github.com/containerd/console v1.0.3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc // indirect
|
||||
github.com/lmika/go-bubble-table v0.2.2-0.20220616114432-6bbb2995e538 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/lunixbochs/vtclean v1.0.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
|
||||
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect
|
||||
github.com/muesli/cancelreader v0.2.0 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect
|
||||
github.com/muesli/termenv v0.12.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/sahilm/fuzzy v0.1.0 // indirect
|
||||
go.etcd.io/bbolt v1.3.6 // indirect
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
|
||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
|
||||
)
|
||||
|
|
45
go.sum
45
go.sum
|
@ -12,12 +12,18 @@ github.com/aws/aws-sdk-go-v2 v1.15.0 h1:f9kWLNfyCzCB43eupDAk3/XgJ2EpgktiySD6leqs
|
|||
github.com/aws/aws-sdk-go-v2 v1.15.0/go.mod h1:lJYcuZZEHWNIb6ugJjbQY1fykdoobWbOS7kJYb4APoI=
|
||||
github.com/aws/aws-sdk-go-v2 v1.16.1 h1:udzee98w8H6ikRgtFdVN9JzzYEbi/quFfSvduZETJIU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.16.1/go.mod h1:ytwTPBG6fXTZLxxeeCCWj2/EMYp/xDUgX+OET6TLNNU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.16.5 h1:Ah9h1TZD9E2S1LzHpViBO3Jz9FPL5+rmflmb8hXirtI=
|
||||
github.com/aws/aws-sdk-go-v2 v1.16.5/go.mod h1:Wh7MEsmEApyL5hrWzpDkba4gwAPc5/piwLVLFnCxp48=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.13.1 h1:yLv8bfNoT4r+UvUKQKqRtdnvuWGMK5a82l4ru9Jvnuo=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.13.1/go.mod h1:Ba5Z4yL/UGbjQUzsiaN378YobhFo0MLfueXGiOsYtEs=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.8.0 h1:8Ow0WcyDesGNL0No11jcgb1JAtE+WtubqXjgxau+S0o=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.8.0/go.mod h1:gnMo58Vwx3Mu7hj1wpcG8DI0s57c9o42UQ6wgTQT5to=
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.8.0 h1:XxTy21xVUkoCZOSGwf+AW22v8aK3eEbYMaGGQ3MbKKk=
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.8.0/go.mod h1:6WkjzWenkrj3IgLPIPBBz4Qh99jNDF8L4Wj03vfMhAA=
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.9.4 h1:EoyeSOfbSuKh+bQIDoZaVJjON6PF+dsSn5w1RhIpMD0=
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.9.4/go.mod h1:bfCL7OwZS6owS06pahfGxhcgpLWj2W1sQASoYRuenag=
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.4.10 h1:IBIZfpnWCTTQhH/bMvDcCMw10BtLBPYO30Ev8MLXMTY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.4.10/go.mod h1:RL7aJOwlWj2N6wkE4nKR1S5M4iGph+xSu7JovwNYpyU=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0 h1:NITDuUZO34mqtOwFWZiXo7yAHj7kf+XPE+EiKuCBNUI=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0/go.mod h1:I6/fHT/fH460v09eg2gVrd8B/IqskhNdpcLH0WNO3QI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.4 h1:CRiQJ4E2RhfDdqbie1ZYDo8QtIo75Mk7oTdJSfwJTMQ=
|
||||
|
@ -26,22 +32,34 @@ github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.6 h1:xiGjGVQsem2cxoIX61
|
|||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.6/go.mod h1:SSPEdf9spsFgJyhjrXvawfpyzrXHBCUe+2eQ1CjC1Ak=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.8 h1:CDaO90VZVBAL1sK87S5oSPIrp7yZqORv1hPIi2UsTMk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.8/go.mod h1:LnTQMTqbKsbtt+UI5+wPsB7jedW+2ZgozoPG8k6cMxg=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.12 h1:Zt7DDk5V7SyQULUUwIKzsROtVzp/kVvcz15uQx/Tkow=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.12/go.mod h1:Afj/U8svX6sJ77Q+FPWMzabJ9QjbwP32YlopgKALUpg=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.2.0 h1:3ADoioDMOtF4uiK59vCpplpCwugEU+v4ZFD29jDL3RQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.2.0/go.mod h1:BsCSJHx5DnDXIrOcqB8KN1/B+hXLG/bi4Y6Vjcx/x9E=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.0 h1:bt3zw79tm209glISdMRCIVRCwvSDXxgAxh5KWe2qHkY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.0/go.mod h1:viTrxhAuejD+LszDahzAE2x40YjYWhMqzHxv2ZiWaME=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.2 h1:XXR3cdOcKRCTZf6ctcqpMf+go1BdzTm6+T9Ul5zxcMI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.2/go.mod h1:1x4ZP3Z8odssdhuLI+/1Tqw6Pt/VAaP4Tr8EUxHvPXE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.6 h1:eeXdGVtXEe+2Jc49+/vAzna3FAQnUD4AagAw8tzbmfc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.6/go.mod h1:FwpAKI+FBPIELJIdmQzlLtRe8LQSOreMcM2wBsPMvvc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5 h1:ixotxbfTCFpqbuwFv/RcZwyzhkxPSYDYEMcj4niB5Uk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5/go.mod h1:R3sWUqPcfXSiF/LSFJhjyJmpg9uV6yP2yv3YZZjldVI=
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.15.0 h1:qnx+WyIH9/AD+wAxi05WCMNanO236ceqHg6hChCWs3M=
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.15.0/go.mod h1:+Kc1UmbE37ijaAsb3KogW6FR8z0myjX6VtdcCkQEK0k=
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.15.7 h1:Ls6kDGWNr3wxE8JypXgTTonHpQ1eRVCGNqaFHY2UASw=
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.15.7/go.mod h1:+v2jeT4/39fCXUQ0ZfHQHMMiJljnmiuj16F03uAd9DY=
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.13.0 h1:s71pGCiLqqGRoUWtdJ2j4PazwEpZVwQc16na/4FfXdk=
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.13.0/go.mod h1:YGzTq/joAih4HRZZtMBWGP4bI8xVucOBQ9RvuanpclA=
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.13.7 h1:o2HKntJx3vr3y11NK58RA6tYKZKQo5PWWt/bs0rWR0U=
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.13.7/go.mod h1:FAVtDKEl/8WxRDQ33e2fz16RO1t4zeEwWIU5kR29xXs=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.0 h1:uhb7moM7VjqIEpWzTpCvceLDSwrWpaleXm39OnVjuLE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.0/go.mod h1:pA2St3Pu2Ldy6fBPY45Azoh1WBG4oS7eIKOd4XN7Meg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.2 h1:T/ywkX1ed+TsZVQccu/8rRJGxKZF/t0Ivgrb4MHTSeo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.2/go.mod h1:RnloUnyZ4KN9JStGY1LuQ7Wzqh7V0f8FinmRdHYtuaA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.0 h1:6Bc0KHhAyxGe15JUHrK+Udw7KhE5LN+5HKZjQGo4yDI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.0/go.mod h1:0nXuX9UrkN4r0PX9TSKfcueGRfsdEYIKG4rjTeJ61X8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.6 h1:JGrc3+kkyr848/wpG2+kWuzHK3H4Fyxj2jnXj8ijQ/Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.6/go.mod h1:zwvTysbXES8GDwFcwCPB8NkC+bCdio1abH+E+BRe/xg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0 h1:4QAOB3KrvI1ApJK14sliGr3Ie2pjyvNypn/lfzDHfUw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0/go.mod h1:K/qPe6AP2TGYv4l6n7c88zh9jWBDf6nHhvg1fx/EWfU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.16.0 h1:dzWS4r8E9bA0TesHM40FSAtedwpTVCuTsLI8EziSqyk=
|
||||
|
@ -58,16 +76,25 @@ github.com/aws/smithy-go v1.11.1 h1:IQ+lPZVkSM3FRtyaDox41R8YS6iwPMYIreejOgPW49g=
|
|||
github.com/aws/smithy-go v1.11.1/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM=
|
||||
github.com/aws/smithy-go v1.11.2 h1:eG/N+CcUMAvsdffgMvjMKwfyDzIkjM6pfxMJ8Mzc6mE=
|
||||
github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM=
|
||||
github.com/aws/smithy-go v1.11.3 h1:DQixirEFM9IaKxX1olZ3ke3nvxRS2xMDteKIDWxozW8=
|
||||
github.com/aws/smithy-go v1.11.3/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
|
||||
github.com/brianvoe/gofakeit/v6 v6.15.0 h1:lJPGJZ2/07TRGDazyTzD5b18N3y4tmmJpdhCUw18FlI=
|
||||
github.com/brianvoe/gofakeit/v6 v6.15.0/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8=
|
||||
github.com/calyptia/go-bubble-table v0.1.0 h1:mXpaaBlrHGH4K8v5PvM8YqBFT9jlysS1YOycU2u3gEQ=
|
||||
github.com/calyptia/go-bubble-table v0.1.0/go.mod h1:2nnweuFos+eEIIbgweXvZuX+ROOatsMwB3NHnX/vTC4=
|
||||
github.com/calyptia/go-bubble-table v0.2.1 h1:NWcVRyGCLuP7QIA29uUFSY+IjmWcmUWHjy5J/CPb0Rk=
|
||||
github.com/calyptia/go-bubble-table v0.2.1/go.mod h1:gJvzUOUzfQeA9JmgLumyJYWJMtuRQ7WxxTwc9tjEiGw=
|
||||
github.com/charmbracelet/bubbles v0.10.3 h1:fKarbRaObLn/DCsZO4Y3vKCwRUzynQD9L+gGev1E/ho=
|
||||
github.com/charmbracelet/bubbles v0.10.3/go.mod h1:jOA+DUF1rjZm7gZHcNyIVW+YrBPALKfpGVdJu8UiJsA=
|
||||
github.com/charmbracelet/bubbles v0.11.0 h1:fBLyY0PvJnd56Vlu5L84JJH6f4axhgIJ9P3NET78f0Q=
|
||||
github.com/charmbracelet/bubbles v0.11.0/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc=
|
||||
github.com/charmbracelet/bubbletea v0.19.3/go.mod h1:VuXF2pToRxDUHcBUcPmCRUHRvFATM4Ckb/ql1rBl3KA=
|
||||
github.com/charmbracelet/bubbletea v0.20.0 h1:/b8LEPgCbNr7WWZ2LuE/BV1/r4t5PyYJtDb+J3vpwxc=
|
||||
github.com/charmbracelet/bubbletea v0.20.0/go.mod h1:zpkze1Rioo4rJELjRyGlm9T2YNou1Fm4LIJQSa5QMEM=
|
||||
github.com/charmbracelet/bubbletea v0.21.0 h1:f3y+kanzgev5PA916qxmDybSHU3N804uOnKnhRPXTcI=
|
||||
github.com/charmbracelet/bubbletea v0.21.0/go.mod h1:GgmJMec61d08zXsOhqRC/AiOx4K4pmz+VIcRIm1FKr4=
|
||||
github.com/charmbracelet/harmonica v0.1.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
||||
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
||||
github.com/charmbracelet/lipgloss v0.4.0/go.mod h1:vmdkHvce7UzX6xkyf4cca8WlwdQ5RQr8fzta+xl7BOM=
|
||||
github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8=
|
||||
github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
|
||||
|
@ -80,6 +107,7 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
|||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
|
@ -93,6 +121,10 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lmika/events v0.0.0-20200906102219-a2269cd4394e h1:0QkUe2ejnT/i+xbgGylMU1b+XnZponQKiPVNi+C/xgA=
|
||||
github.com/lmika/events v0.0.0-20200906102219-a2269cd4394e/go.mod h1:qtkBmNC9OfD0STtOR9sF55pQchjIfNlC3gzm4n8CrqM=
|
||||
github.com/lmika/go-bubble-table v0.2.2-0.20220608033210-61eeb29a6239 h1:GGw5pZtEFnHtD7kKdWsiwgcIwZTnok60sShrHVYz4ok=
|
||||
github.com/lmika/go-bubble-table v0.2.2-0.20220608033210-61eeb29a6239/go.mod h1:0RT1upgKZ6qZ6B1SqseE3wWsPjSQRv/G/HjpYK8jNsg=
|
||||
github.com/lmika/go-bubble-table v0.2.2-0.20220616114432-6bbb2995e538 h1:dtMPRNoDqDnnP3HgOvYhswcJVSqdISkYlCtGOjTqg6Q=
|
||||
github.com/lmika/go-bubble-table v0.2.2-0.20220616114432-6bbb2995e538/go.mod h1:0RT1upgKZ6qZ6B1SqseE3wWsPjSQRv/G/HjpYK8jNsg=
|
||||
github.com/lmika/gopkgs v0.0.0-20211210041137-0dc91e939890 h1:mwl/exYV/WkBMeShqK7q+B2w2r+b0vP1TSA7clBn9kI=
|
||||
github.com/lmika/gopkgs v0.0.0-20211210041137-0dc91e939890/go.mod h1:FH6OJSvYcJ9xY8CGs9yGgR89kMCK1UimuUQ6kE5YuJQ=
|
||||
github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe h1:1UXS/6OFkbi6JrihPykmYO1VtsABB02QQ+YmYYzTY18=
|
||||
|
@ -112,6 +144,10 @@ github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4
|
|||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
|
||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
|
||||
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 h1:kMlmsLSbjkikxQJ1IPwaM+7LJ9ltFu/fi8CRzvSnQmA=
|
||||
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
|
||||
github.com/muesli/cancelreader v0.2.0 h1:SOpr+CfyVNce341kKqvbhhzQhBPyJRXQaCtn03Pae1Q=
|
||||
github.com/muesli/cancelreader v0.2.0/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
|
@ -119,6 +155,8 @@ github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtl
|
|||
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
|
||||
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI=
|
||||
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
|
||||
github.com/muesli/termenv v0.12.0 h1:KuQRUE3PgxRFWhq4gHvZtPSLCGDqM5q/cYr1pZ39ytc=
|
||||
github.com/muesli/termenv v0.12.0/go.mod h1:WCCv32tusQ/EEZ5S8oUIIrC/nIuBcxCVqlN4Xfkv+7A=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
|
@ -140,11 +178,18 @@ golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM=
|
||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
|
|
@ -2,6 +2,8 @@ package commandctrl
|
|||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/pkg/errors"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/lmika/awstools/internal/common/ui/events"
|
||||
|
@ -35,15 +37,18 @@ func (c *CommandController) Prompt() tea.Cmd {
|
|||
}
|
||||
|
||||
func (c *CommandController) Execute(commandInput string) tea.Cmd {
|
||||
log.Println("Received input: ", commandInput)
|
||||
input := strings.TrimSpace(commandInput)
|
||||
if input == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
tokens := shellwords.Split(input)
|
||||
log.Println("Tokens: ", tokens)
|
||||
command := c.lookupCommand(tokens[0])
|
||||
if command == nil {
|
||||
return events.SetStatus("no such command: " + tokens[0])
|
||||
log.Println("No such command: ", tokens)
|
||||
return events.SetError(errors.New("no such command: " + tokens[0]))
|
||||
}
|
||||
|
||||
return command(tokens[1:])
|
||||
|
@ -51,6 +56,7 @@ func (c *CommandController) Execute(commandInput string) tea.Cmd {
|
|||
|
||||
func (c *CommandController) lookupCommand(name string) Command {
|
||||
for ctx := c.commandList; ctx != nil; ctx = ctx.parent {
|
||||
log.Printf("Looking in command list: %v", c.commandList)
|
||||
if cmd, ok := ctx.Commands[name]; ok {
|
||||
return cmd
|
||||
}
|
||||
|
|
|
@ -10,6 +10,12 @@ func Error(err error) tea.Msg {
|
|||
return ErrorMsg(err)
|
||||
}
|
||||
|
||||
func SetError(err error) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
return Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func SetStatus(msg string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
return StatusMsg(msg)
|
||||
|
@ -25,6 +31,20 @@ func PromptForInput(prompt string, onDone func(value string) tea.Cmd) tea.Cmd {
|
|||
}
|
||||
}
|
||||
|
||||
func Confirm(prompt string, onYes func() tea.Cmd) tea.Cmd {
|
||||
return PromptForInput(prompt, func(value string) tea.Cmd {
|
||||
if value == "y" {
|
||||
return onYes()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
type MessageWithStatus interface {
|
||||
StatusMessage() string
|
||||
}
|
||||
|
||||
type MessageWithMode interface {
|
||||
MessageWithStatus
|
||||
ModeMessage() string
|
||||
}
|
||||
|
|
|
@ -10,6 +10,9 @@ type ErrorMsg error
|
|||
// Message indicates that a message should be shown to the user
|
||||
type StatusMsg string
|
||||
|
||||
// ModeMessage indicates that the mode should be changed to the following
|
||||
type ModeMessage string
|
||||
|
||||
// PromptForInput indicates that the context is requesting a line of input
|
||||
type PromptForInputMsg struct {
|
||||
Prompt string
|
||||
|
|
|
@ -6,8 +6,18 @@ import (
|
|||
"os"
|
||||
)
|
||||
|
||||
func EnableLogging() (closeFn func()) {
|
||||
f, err := tea.LogToFile("debug.log", "debug")
|
||||
func EnableLogging(logFile string) (closeFn func()) {
|
||||
if logFile == "" {
|
||||
tempFile, err := os.CreateTemp("", "debug.log")
|
||||
if err != nil {
|
||||
fmt.Println("fatal:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
tempFile.Close()
|
||||
logFile = tempFile.Name()
|
||||
}
|
||||
|
||||
f, err := tea.LogToFile(logFile, "debug")
|
||||
if err != nil {
|
||||
fmt.Println("fatal:", err)
|
||||
os.Exit(1)
|
||||
|
|
18
internal/common/ui/osstyle/osstyle.go
Normal file
18
internal/common/ui/osstyle/osstyle.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
package osstyle
|
||||
|
||||
type ColorScheme int
|
||||
|
||||
const (
|
||||
ColorSchemeUnknown ColorScheme = iota
|
||||
ColorSchemeLightMode
|
||||
ColorSchemeDarkMode
|
||||
)
|
||||
|
||||
var getOSColorScheme func() ColorScheme = nil
|
||||
|
||||
func CurrentColorScheme() ColorScheme {
|
||||
if getOSColorScheme == nil {
|
||||
return ColorSchemeUnknown
|
||||
}
|
||||
return getOSColorScheme()
|
||||
}
|
27
internal/common/ui/osstyle/osstyle_darwin.go
Normal file
27
internal/common/ui/osstyle/osstyle_darwin.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package osstyle
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// Usage: https://stefan.sofa-rockers.org/2018/10/23/macos-dark-mode-terminal-vim/
|
||||
func darwinGetOSColorScheme() ColorScheme {
|
||||
d, err := exec.Command("defaults", "read", "-g", "AppleInterfaceStyle").Output()
|
||||
if err != nil {
|
||||
log.Printf("cannot get current OS color scheme: %v", err)
|
||||
return ColorSchemeUnknown
|
||||
}
|
||||
|
||||
switch string(d) {
|
||||
case "Dark\n":
|
||||
return ColorSchemeDarkMode
|
||||
case "Light\n":
|
||||
return ColorSchemeLightMode
|
||||
}
|
||||
return ColorSchemeUnknown
|
||||
}
|
||||
|
||||
func init() {
|
||||
getOSColorScheme = darwinGetOSColorScheme
|
||||
}
|
96
internal/dynamo-browse/controllers/attrpath.go
Normal file
96
internal/dynamo-browse/controllers/attrpath.go
Normal file
|
@ -0,0 +1,96 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/models"
|
||||
"github.com/pkg/errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type attrPath []string
|
||||
|
||||
func newAttrPath(expr string) attrPath {
|
||||
return strings.Split(expr, ".")
|
||||
}
|
||||
|
||||
func (ap attrPath) follow(item models.Item) (types.AttributeValue, error) {
|
||||
var step types.AttributeValue
|
||||
for i, seg := range ap {
|
||||
if i == 0 {
|
||||
step = item[seg]
|
||||
continue
|
||||
}
|
||||
|
||||
switch s := step.(type) {
|
||||
case *types.AttributeValueMemberM:
|
||||
step = s.Value[seg]
|
||||
default:
|
||||
return nil, errors.Errorf("seg %v expected to be a map", i)
|
||||
}
|
||||
}
|
||||
return step, nil
|
||||
}
|
||||
|
||||
func (ap attrPath) deleteAt(item models.Item) error {
|
||||
if len(ap) == 1 {
|
||||
delete(item, ap[0])
|
||||
return nil
|
||||
}
|
||||
|
||||
var step types.AttributeValue
|
||||
for i, seg := range ap[:len(ap)-1] {
|
||||
if i == 0 {
|
||||
step = item[seg]
|
||||
continue
|
||||
}
|
||||
|
||||
switch s := step.(type) {
|
||||
case *types.AttributeValueMemberM:
|
||||
step = s.Value[seg]
|
||||
default:
|
||||
return errors.Errorf("seg %v expected to be a map", i)
|
||||
}
|
||||
}
|
||||
|
||||
lastSeg := ap[len(ap)-1]
|
||||
switch s := step.(type) {
|
||||
case *types.AttributeValueMemberM:
|
||||
delete(s.Value, lastSeg)
|
||||
default:
|
||||
return errors.Errorf("last seg expected to be a map, but was %T", lastSeg)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap attrPath) setAt(item models.Item, newValue types.AttributeValue) error {
|
||||
if len(ap) == 1 {
|
||||
item[ap[0]] = newValue
|
||||
return nil
|
||||
}
|
||||
|
||||
var step types.AttributeValue
|
||||
for i, seg := range ap[:len(ap)-1] {
|
||||
if i == 0 {
|
||||
step = item[seg]
|
||||
continue
|
||||
}
|
||||
|
||||
switch s := step.(type) {
|
||||
case *types.AttributeValueMemberM:
|
||||
step = s.Value[seg]
|
||||
default:
|
||||
return errors.Errorf("seg %v expected to be a map", i)
|
||||
}
|
||||
}
|
||||
|
||||
lastSeg := ap[len(ap)-1]
|
||||
switch s := step.(type) {
|
||||
case *types.AttributeValueMemberM:
|
||||
s.Value[lastSeg] = newValue
|
||||
default:
|
||||
return errors.Errorf("last seg expected to be a map, but was %T", lastSeg)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
25
internal/dynamo-browse/controllers/commands.go
Normal file
25
internal/dynamo-browse/controllers/commands.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/awstools/internal/common/ui/events"
|
||||
)
|
||||
|
||||
type promptSequence struct {
|
||||
prompts []string
|
||||
receivedValues []string
|
||||
onAllDone func(values []string) tea.Msg
|
||||
}
|
||||
|
||||
func (ps *promptSequence) next() tea.Msg {
|
||||
if len(ps.receivedValues) < len(ps.prompts) {
|
||||
return events.PromptForInputMsg{
|
||||
Prompt: ps.prompts[len(ps.receivedValues)],
|
||||
OnDone: func(value string) tea.Cmd {
|
||||
ps.receivedValues = append(ps.receivedValues, value)
|
||||
return ps.next
|
||||
},
|
||||
}
|
||||
}
|
||||
return ps.onAllDone(ps.receivedValues)
|
||||
}
|
|
@ -2,17 +2,42 @@ package controllers
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/models"
|
||||
)
|
||||
|
||||
type NewResultSet struct {
|
||||
ResultSet *models.ResultSet
|
||||
ResultSet *models.ResultSet
|
||||
currentFilter string
|
||||
filteredCount int
|
||||
statusMessage string
|
||||
}
|
||||
|
||||
func (rs NewResultSet) ModeMessage() string {
|
||||
var modeLine string
|
||||
|
||||
if rs.ResultSet.Query != nil {
|
||||
modeLine = rs.ResultSet.Query.String()
|
||||
} else {
|
||||
modeLine = "All results"
|
||||
}
|
||||
|
||||
if rs.currentFilter != "" {
|
||||
modeLine = fmt.Sprintf("%v - Filter: '%v'", modeLine, rs.currentFilter)
|
||||
}
|
||||
return modeLine
|
||||
}
|
||||
|
||||
func (rs NewResultSet) StatusMessage() string {
|
||||
return fmt.Sprintf("%d items returned", len(rs.ResultSet.Items()))
|
||||
if rs.statusMessage != "" {
|
||||
return rs.statusMessage
|
||||
}
|
||||
|
||||
if rs.currentFilter != "" {
|
||||
return fmt.Sprintf("%d of %d items returned", rs.filteredCount, len(rs.ResultSet.Items()))
|
||||
} else {
|
||||
return fmt.Sprintf("%d items returned", len(rs.ResultSet.Items()))
|
||||
}
|
||||
}
|
||||
|
||||
type SetReadWrite struct {
|
||||
|
|
14
internal/dynamo-browse/controllers/iface.go
Normal file
14
internal/dynamo-browse/controllers/iface.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/models"
|
||||
)
|
||||
|
||||
type TableReadService interface {
|
||||
ListTables(background context.Context) ([]string, error)
|
||||
Describe(ctx context.Context, table string) (*models.TableInfo, error)
|
||||
Scan(ctx context.Context, tableInfo *models.TableInfo) (*models.ResultSet, error)
|
||||
Filter(resultSet *models.ResultSet, filter string) *models.ResultSet
|
||||
ScanOrQuery(ctx context.Context, tableInfo *models.TableInfo, query models.Queryable) (*models.ResultSet, error)
|
||||
}
|
|
@ -1,28 +1,69 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/models"
|
||||
)
|
||||
|
||||
type State struct {
|
||||
ResultSet *models.ResultSet
|
||||
SelectedItem models.Item
|
||||
|
||||
// InReadWriteMode indicates whether modifications can be made to the table
|
||||
InReadWriteMode bool
|
||||
mutex *sync.Mutex
|
||||
resultSet *models.ResultSet
|
||||
filter string
|
||||
}
|
||||
|
||||
type stateContextKeyType struct{}
|
||||
|
||||
var stateContextKey = stateContextKeyType{}
|
||||
|
||||
func CurrentState(ctx context.Context) State {
|
||||
state, _ := ctx.Value(stateContextKey).(State)
|
||||
return state
|
||||
func NewState() *State {
|
||||
return &State{
|
||||
mutex: new(sync.Mutex),
|
||||
}
|
||||
}
|
||||
|
||||
func ContextWithState(ctx context.Context, state State) context.Context {
|
||||
return context.WithValue(ctx, stateContextKey, state)
|
||||
func (s *State) ResultSet() *models.ResultSet {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
return s.resultSet
|
||||
}
|
||||
|
||||
func (s *State) Filter() string {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
return s.filter
|
||||
}
|
||||
|
||||
func (s *State) withResultSet(rs func(*models.ResultSet)) {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
rs(s.resultSet)
|
||||
}
|
||||
|
||||
func (s *State) withResultSetReturningError(rs func(*models.ResultSet) error) (err error) {
|
||||
s.withResultSet(func(set *models.ResultSet) {
|
||||
err = rs(set)
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *State) setResultSetAndFilter(resultSet *models.ResultSet, filter string) {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
s.resultSet = resultSet
|
||||
s.filter = filter
|
||||
}
|
||||
|
||||
func (s *State) buildNewResultSetMessage(statusMessage string) NewResultSet {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
var filteredCount int = 0
|
||||
if s.filter != "" {
|
||||
for i := range s.resultSet.Items() {
|
||||
if !s.resultSet.Hidden(i) {
|
||||
filteredCount += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NewResultSet{s.resultSet, s.filter, filteredCount, statusMessage}
|
||||
}
|
||||
|
|
|
@ -2,26 +2,30 @@ package controllers
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/awstools/internal/common/ui/events"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/models"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/services/tables"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/models/queryexpr"
|
||||
"github.com/pkg/errors"
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type TableReadController struct {
|
||||
tableService *tables.Service
|
||||
tableService TableReadService
|
||||
tableName string
|
||||
|
||||
// state
|
||||
mutex *sync.Mutex
|
||||
resultSet *models.ResultSet
|
||||
filter string
|
||||
mutex *sync.Mutex
|
||||
state *State
|
||||
//resultSet *models.ResultSet
|
||||
//filter string
|
||||
}
|
||||
|
||||
func NewTableReadController(tableService *tables.Service, tableName string) *TableReadController {
|
||||
func NewTableReadController(state *State, tableService TableReadService, tableName string) *TableReadController {
|
||||
return &TableReadController{
|
||||
state: state,
|
||||
tableService: tableService,
|
||||
tableName: tableName,
|
||||
mutex: new(sync.Mutex),
|
||||
|
@ -67,55 +71,106 @@ func (c *TableReadController) ScanTable(name string) tea.Cmd {
|
|||
return events.Error(err)
|
||||
}
|
||||
|
||||
return c.setResultSetAndFilter(resultSet, c.filter)
|
||||
return c.setResultSetAndFilter(resultSet, c.state.Filter())
|
||||
}
|
||||
}
|
||||
|
||||
func (c *TableReadController) PromptForQuery() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
return events.PromptForInputMsg{
|
||||
Prompt: "query: ",
|
||||
OnDone: func(value string) tea.Cmd {
|
||||
if value == "" {
|
||||
return func() tea.Msg {
|
||||
resultSet := c.state.ResultSet()
|
||||
return c.doScan(context.Background(), resultSet, nil)
|
||||
}
|
||||
}
|
||||
|
||||
expr, err := queryexpr.Parse(value)
|
||||
if err != nil {
|
||||
return events.SetError(err)
|
||||
}
|
||||
|
||||
return func() tea.Msg {
|
||||
resultSet := c.state.ResultSet()
|
||||
newResultSet, err := c.tableService.ScanOrQuery(context.Background(), resultSet.TableInfo, expr)
|
||||
if err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
|
||||
return c.setResultSetAndFilter(newResultSet, "")
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *TableReadController) Rescan() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
return c.doScan(context.Background(), c.resultSet)
|
||||
resultSet := c.state.ResultSet()
|
||||
return c.doScan(context.Background(), resultSet, resultSet.Query)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *TableReadController) doScan(ctx context.Context, resultSet *models.ResultSet) tea.Msg {
|
||||
newResultSet, err := c.tableService.Scan(ctx, resultSet.TableInfo)
|
||||
func (c *TableReadController) ExportCSV(filename string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
resultSet := c.state.ResultSet()
|
||||
if resultSet == nil {
|
||||
return events.Error(errors.New("no result set"))
|
||||
}
|
||||
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return events.Error(errors.Wrapf(err, "cannot export to '%v'", filename))
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
cw := csv.NewWriter(f)
|
||||
defer cw.Flush()
|
||||
|
||||
columns := resultSet.Columns()
|
||||
if err := cw.Write(columns); err != nil {
|
||||
return events.Error(errors.Wrapf(err, "cannot export to '%v'", filename))
|
||||
}
|
||||
|
||||
row := make([]string, len(columns))
|
||||
for _, item := range resultSet.Items() {
|
||||
for i, col := range columns {
|
||||
row[i], _ = item.AttributeValueAsString(col)
|
||||
}
|
||||
if err := cw.Write(row); err != nil {
|
||||
return events.Error(errors.Wrapf(err, "cannot export to '%v'", filename))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *TableReadController) doScan(ctx context.Context, resultSet *models.ResultSet, query models.Queryable) tea.Msg {
|
||||
newResultSet, err := c.tableService.ScanOrQuery(ctx, resultSet.TableInfo, query)
|
||||
if err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
|
||||
newResultSet = c.tableService.Filter(newResultSet, c.filter)
|
||||
newResultSet = c.tableService.Filter(newResultSet, c.state.Filter())
|
||||
|
||||
return c.setResultSetAndFilter(newResultSet, c.filter)
|
||||
}
|
||||
|
||||
func (c *TableReadController) ResultSet() *models.ResultSet {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
return c.resultSet
|
||||
return c.setResultSetAndFilter(newResultSet, c.state.Filter())
|
||||
}
|
||||
|
||||
func (c *TableReadController) setResultSetAndFilter(resultSet *models.ResultSet, filter string) tea.Msg {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
c.resultSet = resultSet
|
||||
c.filter = filter
|
||||
return NewResultSet{resultSet}
|
||||
c.state.setResultSetAndFilter(resultSet, filter)
|
||||
return c.state.buildNewResultSetMessage("")
|
||||
}
|
||||
|
||||
func (c *TableReadController) Unmark() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
resultSet := c.ResultSet()
|
||||
|
||||
for i := range resultSet.Items() {
|
||||
resultSet.SetMark(i, false)
|
||||
}
|
||||
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
c.resultSet = resultSet
|
||||
c.state.withResultSet(func(resultSet *models.ResultSet) {
|
||||
for i := range resultSet.Items() {
|
||||
resultSet.SetMark(i, false)
|
||||
}
|
||||
})
|
||||
return ResultSetUpdated{}
|
||||
}
|
||||
}
|
||||
|
@ -126,7 +181,7 @@ func (c *TableReadController) Filter() tea.Cmd {
|
|||
Prompt: "filter: ",
|
||||
OnDone: func(value string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
resultSet := c.ResultSet()
|
||||
resultSet := c.state.ResultSet()
|
||||
newResultSet := c.tableService.Filter(resultSet, value)
|
||||
|
||||
return c.setResultSetAndFilter(newResultSet, value)
|
||||
|
|
257
internal/dynamo-browse/controllers/tableread_test.go
Normal file
257
internal/dynamo-browse/controllers/tableread_test.go
Normal file
|
@ -0,0 +1,257 @@
|
|||
package controllers_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/awstools/internal/common/ui/events"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/controllers"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/providers/dynamo"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/services/tables"
|
||||
"github.com/lmika/awstools/test/testdynamo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTableReadController_InitTable(t *testing.T) {
|
||||
client, cleanupFn := testdynamo.SetupTestTable(t, testData)
|
||||
defer cleanupFn()
|
||||
|
||||
provider := dynamo.NewProvider(client)
|
||||
service := tables.NewService(provider)
|
||||
|
||||
t.Run("should prompt for table if no table name provided", func(t *testing.T) {
|
||||
readController := controllers.NewTableReadController(controllers.NewState(), service, "")
|
||||
|
||||
cmd := readController.Init()
|
||||
event := cmd()
|
||||
|
||||
assert.IsType(t, controllers.PromptForTableMsg{}, event)
|
||||
})
|
||||
|
||||
t.Run("should scan table if table name provided", func(t *testing.T) {
|
||||
readController := controllers.NewTableReadController(controllers.NewState(), service, "")
|
||||
|
||||
cmd := readController.Init()
|
||||
event := cmd()
|
||||
|
||||
assert.IsType(t, controllers.PromptForTableMsg{}, event)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTableReadController_ListTables(t *testing.T) {
|
||||
client, cleanupFn := testdynamo.SetupTestTable(t, testData)
|
||||
defer cleanupFn()
|
||||
|
||||
provider := dynamo.NewProvider(client)
|
||||
service := tables.NewService(provider)
|
||||
readController := controllers.NewTableReadController(controllers.NewState(), service, "")
|
||||
|
||||
t.Run("returns a list of tables", func(t *testing.T) {
|
||||
cmd := readController.ListTables()
|
||||
event := cmd().(controllers.PromptForTableMsg)
|
||||
|
||||
assert.Equal(t, []string{"alpha-table", "bravo-table"}, event.Tables)
|
||||
|
||||
selectedCmd := event.OnSelected("alpha-table")
|
||||
selectedEvent := selectedCmd()
|
||||
|
||||
resultSet := selectedEvent.(controllers.NewResultSet)
|
||||
assert.Equal(t, "alpha-table", resultSet.ResultSet.TableInfo.Name)
|
||||
assert.Equal(t, "pk", resultSet.ResultSet.TableInfo.Keys.PartitionKey)
|
||||
assert.Equal(t, "sk", resultSet.ResultSet.TableInfo.Keys.SortKey)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTableReadController_ExportCSV(t *testing.T) {
|
||||
client, cleanupFn := testdynamo.SetupTestTable(t, testData)
|
||||
defer cleanupFn()
|
||||
|
||||
provider := dynamo.NewProvider(client)
|
||||
service := tables.NewService(provider)
|
||||
readController := controllers.NewTableReadController(controllers.NewState(), service, "bravo-table")
|
||||
|
||||
t.Run("should export result set to CSV file", func(t *testing.T) {
|
||||
tempFile := tempFile(t)
|
||||
|
||||
invokeCommand(t, readController.Init())
|
||||
invokeCommand(t, readController.ExportCSV(tempFile))
|
||||
|
||||
bts, err := os.ReadFile(tempFile)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, string(bts), strings.Join([]string{
|
||||
"pk,sk,alpha,beta,gamma\n",
|
||||
"abc,222,This is another some value,1231,\n",
|
||||
"bbb,131,,2468,foobar\n",
|
||||
"foo,bar,This is some value,,\n",
|
||||
}, ""))
|
||||
})
|
||||
|
||||
t.Run("should return error if result set is not set", func(t *testing.T) {
|
||||
tempFile := tempFile(t)
|
||||
readController := controllers.NewTableReadController(controllers.NewState(), service, "non-existant-table")
|
||||
|
||||
invokeCommandExpectingError(t, readController.Init())
|
||||
invokeCommandExpectingError(t, readController.ExportCSV(tempFile))
|
||||
})
|
||||
|
||||
// Hidden items?
|
||||
}
|
||||
|
||||
func TestTableReadController_Query(t *testing.T) {
|
||||
client, cleanupFn := testdynamo.SetupTestTable(t, testData)
|
||||
defer cleanupFn()
|
||||
|
||||
provider := dynamo.NewProvider(client)
|
||||
service := tables.NewService(provider)
|
||||
readController := controllers.NewTableReadController(controllers.NewState(), service, "bravo-table")
|
||||
|
||||
t.Run("should run scan with filter based on user query", func(t *testing.T) {
|
||||
tempFile := tempFile(t)
|
||||
|
||||
invokeCommand(t, readController.Init())
|
||||
invokeCommandWithPrompts(t, readController.PromptForQuery(), `pk ^= "abc"`)
|
||||
invokeCommand(t, readController.ExportCSV(tempFile))
|
||||
|
||||
bts, err := os.ReadFile(tempFile)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, string(bts), strings.Join([]string{
|
||||
"pk,sk,alpha,beta\n",
|
||||
"abc,222,This is another some value,1231\n",
|
||||
}, ""))
|
||||
})
|
||||
|
||||
t.Run("should return error if result set is not set", func(t *testing.T) {
|
||||
tempFile := tempFile(t)
|
||||
readController := controllers.NewTableReadController(controllers.NewState(), service, "non-existant-table")
|
||||
|
||||
invokeCommandExpectingError(t, readController.Init())
|
||||
invokeCommandExpectingError(t, readController.ExportCSV(tempFile))
|
||||
})
|
||||
}
|
||||
|
||||
func tempFile(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
tempFile, err := os.CreateTemp("", "export.csv")
|
||||
assert.NoError(t, err)
|
||||
tempFile.Close()
|
||||
|
||||
t.Cleanup(func() {
|
||||
os.Remove(tempFile.Name())
|
||||
})
|
||||
|
||||
return tempFile.Name()
|
||||
}
|
||||
|
||||
func invokeCommand(t *testing.T, cmd tea.Cmd) tea.Msg {
|
||||
msg := cmd()
|
||||
|
||||
err, isErr := msg.(events.ErrorMsg)
|
||||
if isErr {
|
||||
assert.Fail(t, fmt.Sprintf("expected no error but got one: %v", err))
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
func invokeCommandWithPrompt(t *testing.T, cmd tea.Cmd, promptValue string) {
|
||||
msg := cmd()
|
||||
|
||||
pi, isPi := msg.(events.PromptForInputMsg)
|
||||
if !isPi {
|
||||
assert.Fail(t, fmt.Sprintf("expected prompt for input but didn't get one"))
|
||||
}
|
||||
|
||||
invokeCommand(t, pi.OnDone(promptValue))
|
||||
}
|
||||
|
||||
func invokeCommandWithPrompts(t *testing.T, cmd tea.Cmd, promptValues ...string) {
|
||||
msg := cmd()
|
||||
|
||||
for _, promptValue := range promptValues {
|
||||
pi, isPi := msg.(events.PromptForInputMsg)
|
||||
if !isPi {
|
||||
assert.Fail(t, fmt.Sprintf("expected prompt for input but didn't get one"))
|
||||
}
|
||||
|
||||
msg = invokeCommand(t, pi.OnDone(promptValue))
|
||||
}
|
||||
}
|
||||
|
||||
func invokeCommandWithPromptsExpectingError(t *testing.T, cmd tea.Cmd, promptValues ...string) {
|
||||
msg := cmd()
|
||||
|
||||
for _, promptValue := range promptValues {
|
||||
pi, isPi := msg.(events.PromptForInputMsg)
|
||||
if !isPi {
|
||||
assert.Fail(t, fmt.Sprintf("expected prompt for input but didn't get one"))
|
||||
}
|
||||
|
||||
msg = invokeCommand(t, pi.OnDone(promptValue))
|
||||
}
|
||||
|
||||
_, isErr := msg.(events.ErrorMsg)
|
||||
assert.True(t, isErr)
|
||||
}
|
||||
|
||||
func invokeCommandExpectingError(t *testing.T, cmd tea.Cmd) {
|
||||
msg := cmd()
|
||||
|
||||
_, isErr := msg.(events.ErrorMsg)
|
||||
assert.True(t, isErr)
|
||||
}
|
||||
|
||||
var testData = []testdynamo.TestData{
|
||||
{
|
||||
TableName: "alpha-table",
|
||||
Data: []map[string]interface{}{
|
||||
{
|
||||
"pk": "abc",
|
||||
"sk": "111",
|
||||
"alpha": "This is some value",
|
||||
"age": 23,
|
||||
"address": map[string]any{
|
||||
"no": 123,
|
||||
"street": "Fake st.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"pk": "abc",
|
||||
"sk": "222",
|
||||
"alpha": "This is another some value",
|
||||
"beta": 1231,
|
||||
},
|
||||
{
|
||||
"pk": "bbb",
|
||||
"sk": "131",
|
||||
"beta": 2468,
|
||||
"gamma": "foobar",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
TableName: "bravo-table",
|
||||
Data: []map[string]interface{}{
|
||||
{
|
||||
"pk": "foo",
|
||||
"sk": "bar",
|
||||
"alpha": "This is some value",
|
||||
},
|
||||
{
|
||||
"pk": "abc",
|
||||
"sk": "222",
|
||||
"alpha": "This is another some value",
|
||||
"beta": 1231,
|
||||
},
|
||||
{
|
||||
"pk": "bbb",
|
||||
"sk": "131",
|
||||
"beta": 2468,
|
||||
"gamma": "foobar",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
|
@ -3,18 +3,23 @@ package controllers
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/awstools/internal/common/ui/events"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/models"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/services/tables"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type TableWriteController struct {
|
||||
state *State
|
||||
tableService *tables.Service
|
||||
tableReadControllers *TableReadController
|
||||
}
|
||||
|
||||
func NewTableWriteController(tableService *tables.Service, tableReadControllers *TableReadController) *TableWriteController {
|
||||
func NewTableWriteController(state *State, tableService *tables.Service, tableReadControllers *TableReadController) *TableWriteController {
|
||||
return &TableWriteController{
|
||||
state: state,
|
||||
tableService: tableService,
|
||||
tableReadControllers: tableReadControllers,
|
||||
}
|
||||
|
@ -22,16 +27,232 @@ func NewTableWriteController(tableService *tables.Service, tableReadControllers
|
|||
|
||||
func (twc *TableWriteController) ToggleMark(idx int) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
resultSet := twc.tableReadControllers.ResultSet()
|
||||
resultSet.SetMark(idx, !resultSet.Marked(idx))
|
||||
twc.state.withResultSet(func(resultSet *models.ResultSet) {
|
||||
resultSet.SetMark(idx, !resultSet.Marked(idx))
|
||||
})
|
||||
|
||||
return ResultSetUpdated{}
|
||||
}
|
||||
}
|
||||
|
||||
func (twc *TableWriteController) NewItem() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
// Work out which keys we need to prompt for
|
||||
rs := twc.state.ResultSet()
|
||||
|
||||
keyPrompts := &promptSequence{
|
||||
prompts: []string{rs.TableInfo.Keys.PartitionKey + ": "},
|
||||
}
|
||||
if rs.TableInfo.Keys.SortKey != "" {
|
||||
keyPrompts.prompts = append(keyPrompts.prompts, rs.TableInfo.Keys.SortKey+": ")
|
||||
}
|
||||
keyPrompts.onAllDone = func(values []string) tea.Msg {
|
||||
twc.state.withResultSet(func(set *models.ResultSet) {
|
||||
newItem := models.Item{}
|
||||
|
||||
// TODO: deal with keys of different type
|
||||
newItem[rs.TableInfo.Keys.PartitionKey] = &types.AttributeValueMemberS{Value: values[0]}
|
||||
if len(values) == 2 {
|
||||
newItem[rs.TableInfo.Keys.SortKey] = &types.AttributeValueMemberS{Value: values[1]}
|
||||
}
|
||||
|
||||
set.AddNewItem(newItem, models.ItemAttribute{
|
||||
New: true,
|
||||
Dirty: true,
|
||||
})
|
||||
})
|
||||
return twc.state.buildNewResultSetMessage("New item added")
|
||||
}
|
||||
|
||||
return keyPrompts.next()
|
||||
}
|
||||
}
|
||||
|
||||
func (twc *TableWriteController) SetStringValue(idx int, key string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
// Verify that the expression is valid
|
||||
apPath := newAttrPath(key)
|
||||
|
||||
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
|
||||
_, err := apPath.follow(set.Items()[idx])
|
||||
return err
|
||||
}); err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
|
||||
return events.PromptForInputMsg{
|
||||
Prompt: "string value: ",
|
||||
OnDone: func(value string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
|
||||
err := apPath.setAt(set.Items()[idx], &types.AttributeValueMemberS{Value: value})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
set.SetDirty(idx, true)
|
||||
set.RefreshColumns()
|
||||
return nil
|
||||
}); err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
return ResultSetUpdated{}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (twc *TableWriteController) SetNumberValue(idx int, key string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
// Verify that the expression is valid
|
||||
apPath := newAttrPath(key)
|
||||
|
||||
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
|
||||
_, err := apPath.follow(set.Items()[idx])
|
||||
return err
|
||||
}); err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
|
||||
return events.PromptForInputMsg{
|
||||
Prompt: "number value: ",
|
||||
OnDone: func(value string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
|
||||
err := apPath.setAt(set.Items()[idx], &types.AttributeValueMemberN{Value: value})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
set.SetDirty(idx, true)
|
||||
set.RefreshColumns()
|
||||
return nil
|
||||
}); err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
return ResultSetUpdated{}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (twc *TableWriteController) DeleteAttribute(idx int, key string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
// Verify that the expression is valid
|
||||
apPath := newAttrPath(key)
|
||||
|
||||
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
|
||||
_, err := apPath.follow(set.Items()[idx])
|
||||
return err
|
||||
}); err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
|
||||
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
|
||||
err := apPath.deleteAt(set.Items()[idx])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
set.SetDirty(idx, true)
|
||||
set.RefreshColumns()
|
||||
return nil
|
||||
}); err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
|
||||
return ResultSetUpdated{}
|
||||
}
|
||||
}
|
||||
|
||||
func (twc *TableWriteController) PutItem(idx int) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
resultSet := twc.state.ResultSet()
|
||||
if !resultSet.IsDirty(idx) {
|
||||
return events.Error(errors.New("item is not dirty"))
|
||||
}
|
||||
|
||||
return events.PromptForInputMsg{
|
||||
Prompt: "put item? ",
|
||||
OnDone: func(value string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
if value != "y" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := twc.tableService.PutItemAt(context.Background(), resultSet, idx); err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
return ResultSetUpdated{}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (twc *TableWriteController) TouchItem(idx int) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
resultSet := twc.state.ResultSet()
|
||||
if resultSet.IsDirty(idx) {
|
||||
return events.Error(errors.New("cannot touch dirty items"))
|
||||
}
|
||||
|
||||
return events.PromptForInputMsg{
|
||||
Prompt: "touch item? ",
|
||||
OnDone: func(value string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
if value != "y" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := twc.tableService.PutItemAt(context.Background(), resultSet, idx); err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
return ResultSetUpdated{}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (twc *TableWriteController) NoisyTouchItem(idx int) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
resultSet := twc.state.ResultSet()
|
||||
if resultSet.IsDirty(idx) {
|
||||
return events.Error(errors.New("cannot noisy touch dirty items"))
|
||||
}
|
||||
|
||||
return events.PromptForInputMsg{
|
||||
Prompt: "noisy touch item? ",
|
||||
OnDone: func(value string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
ctx := context.Background()
|
||||
|
||||
if value != "y" {
|
||||
return nil
|
||||
}
|
||||
|
||||
item := resultSet.Items()[0]
|
||||
if err := twc.tableService.Delete(ctx, resultSet.TableInfo, []models.Item{item}); err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
|
||||
if err := twc.tableService.Put(ctx, resultSet.TableInfo, item); err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
|
||||
return twc.tableReadControllers.doScan(ctx, resultSet, resultSet.Query)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (twc *TableWriteController) DeleteMarked() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
resultSet := twc.tableReadControllers.ResultSet()
|
||||
resultSet := twc.state.ResultSet()
|
||||
markedItems := resultSet.MarkedItems()
|
||||
|
||||
if len(markedItems) == 0 {
|
||||
|
@ -51,7 +272,7 @@ func (twc *TableWriteController) DeleteMarked() tea.Cmd {
|
|||
return events.Error(err)
|
||||
}
|
||||
|
||||
return twc.tableReadControllers.doScan(ctx, resultSet)
|
||||
return twc.tableReadControllers.doScan(ctx, resultSet, resultSet.Query)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,199 +1,365 @@
|
|||
package controllers_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/controllers"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/providers/dynamo"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/services/tables"
|
||||
"github.com/lmika/awstools/test/testdynamo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTableWriteController_ToggleReadWrite(t *testing.T) {
|
||||
t.Skip("needs to be updated")
|
||||
func TestTableWriteController_NewItem(t *testing.T) {
|
||||
t.Run("should add an item with pk and sk set at the end of the result set", func(t *testing.T) {
|
||||
client, cleanupFn := testdynamo.SetupTestTable(t, testData)
|
||||
defer cleanupFn()
|
||||
|
||||
/*
|
||||
twc, _, closeFn := setupController(t)
|
||||
t.Cleanup(closeFn)
|
||||
provider := dynamo.NewProvider(client)
|
||||
service := tables.NewService(provider)
|
||||
|
||||
t.Run("should enabling read write if disabled", func(t *testing.T) {
|
||||
ctx, uiCtx := testuictx.New(context.Background())
|
||||
ctx = controllers.ContextWithState(ctx, controllers.State{
|
||||
InReadWriteMode: false,
|
||||
})
|
||||
state := controllers.NewState()
|
||||
readController := controllers.NewTableReadController(state, service, "alpha-table")
|
||||
writeController := controllers.NewTableWriteController(state, service, readController)
|
||||
|
||||
err := twc.ToggleReadWrite().Execute(ctx)
|
||||
assert.NoError(t, err)
|
||||
invokeCommand(t, readController.Init())
|
||||
assert.Len(t, state.ResultSet().Items(), 3)
|
||||
|
||||
assert.Contains(t, uiCtx.Messages, controllers.SetReadWrite{NewValue: true})
|
||||
})
|
||||
// Prompt for keys
|
||||
invokeCommandWithPrompts(t, writeController.NewItem(), "pk-value", "sk-value")
|
||||
|
||||
t.Run("should disable read write if enabled", func(t *testing.T) {
|
||||
ctx, uiCtx := testuictx.New(context.Background())
|
||||
ctx = controllers.ContextWithState(ctx, controllers.State{
|
||||
InReadWriteMode: true,
|
||||
})
|
||||
newResultSet := state.ResultSet()
|
||||
assert.Len(t, newResultSet.Items(), 4)
|
||||
assert.Len(t, newResultSet.Items()[3], 2)
|
||||
|
||||
err := twc.ToggleReadWrite().Execute(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Contains(t, uiCtx.Messages, controllers.SetReadWrite{NewValue: false})
|
||||
})
|
||||
*/
|
||||
pk, _ := newResultSet.Items()[3].AttributeValueAsString("pk")
|
||||
sk, _ := newResultSet.Items()[3].AttributeValueAsString("sk")
|
||||
assert.Equal(t, "pk-value", pk)
|
||||
assert.Equal(t, "sk-value", sk)
|
||||
assert.True(t, newResultSet.IsNew(3))
|
||||
assert.True(t, newResultSet.IsDirty(3))
|
||||
})
|
||||
}
|
||||
|
||||
func TestTableWriteController_Delete(t *testing.T) {
|
||||
/*
|
||||
t.Run("should delete selected item if in read/write mode is inactive", func(t *testing.T) {
|
||||
twc, ctrls, closeFn := setupController(t)
|
||||
t.Cleanup(closeFn)
|
||||
func TestTableWriteController_SetStringValue(t *testing.T) {
|
||||
client, cleanupFn := testdynamo.SetupTestTable(t, testData)
|
||||
defer cleanupFn()
|
||||
|
||||
ti, err := ctrls.tableService.Describe(context.Background(), ctrls.tableName)
|
||||
assert.NoError(t, err)
|
||||
|
||||
resultSet, err := ctrls.tableService.Scan(context.Background(), ti)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, resultSet.Items, 3)
|
||||
|
||||
ctx, uiCtx := testuictx.New(context.Background())
|
||||
ctx = controllers.ContextWithState(ctx, controllers.State{
|
||||
ResultSet: resultSet,
|
||||
SelectedItem: resultSet.Items[1],
|
||||
InReadWriteMode: true,
|
||||
})
|
||||
|
||||
op := twc.Delete()
|
||||
|
||||
// Should prompt first
|
||||
err = op.Execute(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_ = uiCtx
|
||||
|
||||
*/
|
||||
/*
|
||||
promptRequest, ok := uiCtx.Messages[0].(events.PromptForInput)
|
||||
assert.True(t, ok)
|
||||
|
||||
// After prompt, continue to delete
|
||||
err = promptRequest.OnDone.Execute(uimodels.WithPromptValue(ctx, "y"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
afterResultSet, err := ctrls.tableService.Scan(context.Background(), ti)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, afterResultSet.Items, 2)
|
||||
assert.Contains(t, afterResultSet.Items, resultSet.Items[0])
|
||||
assert.NotContains(t, afterResultSet.Items, resultSet.Items[1])
|
||||
assert.Contains(t, afterResultSet.Items, resultSet.Items[2])
|
||||
*/
|
||||
/*
|
||||
})
|
||||
|
||||
t.Run("should not delete selected item if prompt is not y", func(t *testing.T) {
|
||||
twc, ctrls, closeFn := setupController(t)
|
||||
t.Cleanup(closeFn)
|
||||
|
||||
ti, err := ctrls.tableService.Describe(context.Background(), ctrls.tableName)
|
||||
assert.NoError(t, err)
|
||||
|
||||
resultSet, err := ctrls.tableService.Scan(context.Background(), ti)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, resultSet.Items, 3)
|
||||
|
||||
ctx, uiCtx := testuictx.New(context.Background())
|
||||
ctx = controllers.ContextWithState(ctx, controllers.State{
|
||||
ResultSet: resultSet,
|
||||
SelectedItem: resultSet.Items[1],
|
||||
InReadWriteMode: true,
|
||||
})
|
||||
|
||||
op := twc.Delete()
|
||||
|
||||
// Should prompt first
|
||||
err = op.Execute(ctx)
|
||||
assert.NoError(t, err)
|
||||
_ = uiCtx
|
||||
*/
|
||||
/*
|
||||
promptRequest, ok := uiCtx.Messages[0].(events.PromptForInput)
|
||||
assert.True(t, ok)
|
||||
|
||||
// After prompt, continue to delete
|
||||
err = promptRequest.OnDone.Execute(uimodels.WithPromptValue(ctx, "n"))
|
||||
assert.Error(t, err)
|
||||
|
||||
afterResultSet, err := ctrls.tableService.Scan(context.Background(), ti)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, afterResultSet.Items, 3)
|
||||
assert.Contains(t, afterResultSet.Items, resultSet.Items[0])
|
||||
assert.Contains(t, afterResultSet.Items, resultSet.Items[1])
|
||||
assert.Contains(t, afterResultSet.Items, resultSet.Items[2])
|
||||
*/
|
||||
/*
|
||||
})
|
||||
|
||||
t.Run("should not delete if read/write mode is inactive", func(t *testing.T) {
|
||||
tableWriteController, ctrls, closeFn := setupController(t)
|
||||
t.Cleanup(closeFn)
|
||||
|
||||
ti, err := ctrls.tableService.Describe(context.Background(), ctrls.tableName)
|
||||
assert.NoError(t, err)
|
||||
|
||||
resultSet, err := ctrls.tableService.Scan(context.Background(), ti)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, resultSet.Items, 3)
|
||||
|
||||
ctx, _ := testuictx.New(context.Background())
|
||||
ctx = controllers.ContextWithState(ctx, controllers.State{
|
||||
ResultSet: resultSet,
|
||||
SelectedItem: resultSet.Items[1],
|
||||
InReadWriteMode: false,
|
||||
})
|
||||
|
||||
op := tableWriteController.Delete()
|
||||
|
||||
err = op.Execute(ctx)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
*/
|
||||
}
|
||||
|
||||
type controller struct {
|
||||
tableName string
|
||||
tableService *tables.Service
|
||||
}
|
||||
|
||||
func setupController(t *testing.T) (*controllers.TableWriteController, controller, func()) {
|
||||
tableName := "table-write-controller-table"
|
||||
|
||||
client, cleanupFn := testdynamo.SetupTestTable(t, tableName, testData)
|
||||
provider := dynamo.NewProvider(client)
|
||||
tableService := tables.NewService(provider)
|
||||
tableReadController := controllers.NewTableReadController(tableService, tableName)
|
||||
tableWriteController := controllers.NewTableWriteController(tableService, tableReadController)
|
||||
return tableWriteController, controller{
|
||||
tableName: tableName,
|
||||
tableService: tableService,
|
||||
}, cleanupFn
|
||||
service := tables.NewService(provider)
|
||||
|
||||
t.Run("should change the value of a string field if already present", func(t *testing.T) {
|
||||
state := controllers.NewState()
|
||||
readController := controllers.NewTableReadController(state, service, "alpha-table")
|
||||
writeController := controllers.NewTableWriteController(state, service, readController)
|
||||
|
||||
invokeCommand(t, readController.Init())
|
||||
before, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha")
|
||||
assert.Equal(t, "This is some value", before)
|
||||
assert.False(t, state.ResultSet().IsDirty(0))
|
||||
|
||||
invokeCommandWithPrompt(t, writeController.SetStringValue(0, "alpha"), "a new value")
|
||||
|
||||
after, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha")
|
||||
assert.Equal(t, "a new value", after)
|
||||
assert.True(t, state.ResultSet().IsDirty(0))
|
||||
})
|
||||
|
||||
t.Run("should change the value of a string field within a map if already present", func(t *testing.T) {
|
||||
state := controllers.NewState()
|
||||
readController := controllers.NewTableReadController(state, service, "alpha-table")
|
||||
writeController := controllers.NewTableWriteController(state, service, readController)
|
||||
|
||||
invokeCommand(t, readController.Init())
|
||||
|
||||
beforeAddress := state.ResultSet().Items()[0]["address"].(*types.AttributeValueMemberM)
|
||||
beforeStreet := beforeAddress.Value["street"].(*types.AttributeValueMemberS).Value
|
||||
|
||||
assert.Equal(t, "Fake st.", beforeStreet)
|
||||
assert.False(t, state.ResultSet().IsDirty(0))
|
||||
|
||||
invokeCommandWithPrompt(t, writeController.SetStringValue(0, "address.street"), "Fiction rd.")
|
||||
|
||||
afterAddress := state.ResultSet().Items()[0]["address"].(*types.AttributeValueMemberM)
|
||||
afterStreet := afterAddress.Value["street"].(*types.AttributeValueMemberS).Value
|
||||
|
||||
assert.Equal(t, "Fiction rd.", afterStreet)
|
||||
assert.True(t, state.ResultSet().IsDirty(0))
|
||||
})
|
||||
}
|
||||
|
||||
var testData = testdynamo.TestData{
|
||||
{
|
||||
"pk": "abc",
|
||||
"sk": "222",
|
||||
"alpha": "This is another some value",
|
||||
"beta": 1231,
|
||||
},
|
||||
{
|
||||
"pk": "abc",
|
||||
"sk": "111",
|
||||
"alpha": "This is some value",
|
||||
},
|
||||
{
|
||||
"pk": "bbb",
|
||||
"sk": "131",
|
||||
"beta": 2468,
|
||||
"gamma": "foobar",
|
||||
},
|
||||
func TestTableWriteController_SetNumberValue(t *testing.T) {
|
||||
client, cleanupFn := testdynamo.SetupTestTable(t, testData)
|
||||
defer cleanupFn()
|
||||
|
||||
provider := dynamo.NewProvider(client)
|
||||
service := tables.NewService(provider)
|
||||
|
||||
t.Run("should change the value of a number field if already present", func(t *testing.T) {
|
||||
state := controllers.NewState()
|
||||
readController := controllers.NewTableReadController(state, service, "alpha-table")
|
||||
writeController := controllers.NewTableWriteController(state, service, readController)
|
||||
|
||||
invokeCommand(t, readController.Init())
|
||||
before, _ := state.ResultSet().Items()[0].AttributeValueAsString("age")
|
||||
assert.Equal(t, "23", before)
|
||||
assert.False(t, state.ResultSet().IsDirty(0))
|
||||
|
||||
invokeCommandWithPrompt(t, writeController.SetNumberValue(0, "age"), "46")
|
||||
|
||||
after, _ := state.ResultSet().Items()[0].AttributeValueAsString("age")
|
||||
assert.Equal(t, "46", after)
|
||||
assert.True(t, state.ResultSet().IsDirty(0))
|
||||
})
|
||||
|
||||
t.Run("should change the value of a number field within a map if already present", func(t *testing.T) {
|
||||
state := controllers.NewState()
|
||||
readController := controllers.NewTableReadController(state, service, "alpha-table")
|
||||
writeController := controllers.NewTableWriteController(state, service, readController)
|
||||
|
||||
invokeCommand(t, readController.Init())
|
||||
|
||||
beforeAddress := state.ResultSet().Items()[0]["address"].(*types.AttributeValueMemberM)
|
||||
beforeStreet := beforeAddress.Value["no"].(*types.AttributeValueMemberN).Value
|
||||
|
||||
assert.Equal(t, "123", beforeStreet)
|
||||
assert.False(t, state.ResultSet().IsDirty(0))
|
||||
|
||||
invokeCommandWithPrompt(t, writeController.SetNumberValue(0, "address.no"), "456")
|
||||
|
||||
afterAddress := state.ResultSet().Items()[0]["address"].(*types.AttributeValueMemberM)
|
||||
afterStreet := afterAddress.Value["no"].(*types.AttributeValueMemberN).Value
|
||||
|
||||
assert.Equal(t, "456", afterStreet)
|
||||
assert.True(t, state.ResultSet().IsDirty(0))
|
||||
})
|
||||
}
|
||||
|
||||
func TestTableWriteController_DeleteAttribute(t *testing.T) {
|
||||
client, cleanupFn := testdynamo.SetupTestTable(t, testData)
|
||||
defer cleanupFn()
|
||||
|
||||
provider := dynamo.NewProvider(client)
|
||||
service := tables.NewService(provider)
|
||||
|
||||
t.Run("should delete top level attribute", func(t *testing.T) {
|
||||
state := controllers.NewState()
|
||||
readController := controllers.NewTableReadController(state, service, "alpha-table")
|
||||
writeController := controllers.NewTableWriteController(state, service, readController)
|
||||
|
||||
invokeCommand(t, readController.Init())
|
||||
before, _ := state.ResultSet().Items()[0].AttributeValueAsString("age")
|
||||
assert.Equal(t, "23", before)
|
||||
assert.False(t, state.ResultSet().IsDirty(0))
|
||||
|
||||
invokeCommand(t, writeController.DeleteAttribute(0, "age"))
|
||||
|
||||
_, hasAge := state.ResultSet().Items()[0]["age"]
|
||||
assert.False(t, hasAge)
|
||||
})
|
||||
|
||||
t.Run("should delete attribute of map", func(t *testing.T) {
|
||||
state := controllers.NewState()
|
||||
readController := controllers.NewTableReadController(state, service, "alpha-table")
|
||||
writeController := controllers.NewTableWriteController(state, service, readController)
|
||||
|
||||
invokeCommand(t, readController.Init())
|
||||
|
||||
beforeAddress := state.ResultSet().Items()[0]["address"].(*types.AttributeValueMemberM)
|
||||
beforeStreet := beforeAddress.Value["no"].(*types.AttributeValueMemberN).Value
|
||||
|
||||
assert.Equal(t, "123", beforeStreet)
|
||||
assert.False(t, state.ResultSet().IsDirty(0))
|
||||
|
||||
invokeCommand(t, writeController.DeleteAttribute(0, "address.no"))
|
||||
|
||||
afterAddress := state.ResultSet().Items()[0]["address"].(*types.AttributeValueMemberM)
|
||||
_, hasStreet := afterAddress.Value["no"]
|
||||
|
||||
assert.False(t, hasStreet)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTableWriteController_PutItem(t *testing.T) {
|
||||
t.Run("should put the selected item if dirty", func(t *testing.T) {
|
||||
client, cleanupFn := testdynamo.SetupTestTable(t, testData)
|
||||
defer cleanupFn()
|
||||
|
||||
provider := dynamo.NewProvider(client)
|
||||
service := tables.NewService(provider)
|
||||
|
||||
state := controllers.NewState()
|
||||
readController := controllers.NewTableReadController(state, service, "alpha-table")
|
||||
writeController := controllers.NewTableWriteController(state, service, readController)
|
||||
|
||||
// Read the table
|
||||
invokeCommand(t, readController.Init())
|
||||
before, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha")
|
||||
assert.Equal(t, "This is some value", before)
|
||||
assert.False(t, state.ResultSet().IsDirty(0))
|
||||
|
||||
// Modify the item and put it
|
||||
invokeCommandWithPrompt(t, writeController.SetStringValue(0, "alpha"), "a new value")
|
||||
invokeCommandWithPrompt(t, writeController.PutItem(0), "y")
|
||||
|
||||
// Rescan the table
|
||||
invokeCommand(t, readController.Rescan())
|
||||
after, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha")
|
||||
assert.Equal(t, "a new value", after)
|
||||
assert.False(t, state.ResultSet().IsDirty(0))
|
||||
})
|
||||
|
||||
t.Run("should not put the selected item if user does not confirm", func(t *testing.T) {
|
||||
client, cleanupFn := testdynamo.SetupTestTable(t, testData)
|
||||
defer cleanupFn()
|
||||
|
||||
provider := dynamo.NewProvider(client)
|
||||
service := tables.NewService(provider)
|
||||
|
||||
state := controllers.NewState()
|
||||
readController := controllers.NewTableReadController(state, service, "alpha-table")
|
||||
writeController := controllers.NewTableWriteController(state, service, readController)
|
||||
|
||||
// Read the table
|
||||
invokeCommand(t, readController.Init())
|
||||
before, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha")
|
||||
assert.Equal(t, "This is some value", before)
|
||||
assert.False(t, state.ResultSet().IsDirty(0))
|
||||
|
||||
// Modify the item but do not put it
|
||||
invokeCommandWithPrompt(t, writeController.SetStringValue(0, "alpha"), "a new value")
|
||||
invokeCommandWithPrompt(t, writeController.PutItem(0), "n")
|
||||
|
||||
current, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha")
|
||||
assert.Equal(t, "a new value", current)
|
||||
assert.True(t, state.ResultSet().IsDirty(0))
|
||||
|
||||
// Rescan the table to confirm item is not modified
|
||||
invokeCommand(t, readController.Rescan())
|
||||
after, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha")
|
||||
assert.Equal(t, "This is some value", after)
|
||||
assert.False(t, state.ResultSet().IsDirty(0))
|
||||
})
|
||||
|
||||
t.Run("should not put the selected item if not dirty", func(t *testing.T) {
|
||||
client, cleanupFn := testdynamo.SetupTestTable(t, testData)
|
||||
defer cleanupFn()
|
||||
|
||||
provider := dynamo.NewProvider(client)
|
||||
service := tables.NewService(provider)
|
||||
|
||||
state := controllers.NewState()
|
||||
readController := controllers.NewTableReadController(state, service, "alpha-table")
|
||||
writeController := controllers.NewTableWriteController(state, service, readController)
|
||||
|
||||
// Read the table
|
||||
invokeCommand(t, readController.Init())
|
||||
before, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha")
|
||||
assert.Equal(t, "This is some value", before)
|
||||
assert.False(t, state.ResultSet().IsDirty(0))
|
||||
|
||||
invokeCommandExpectingError(t, writeController.PutItem(0))
|
||||
})
|
||||
}
|
||||
|
||||
func TestTableWriteController_TouchItem(t *testing.T) {
|
||||
t.Run("should put the selected item if unmodified", func(t *testing.T) {
|
||||
client, cleanupFn := testdynamo.SetupTestTable(t, testData)
|
||||
defer cleanupFn()
|
||||
|
||||
provider := dynamo.NewProvider(client)
|
||||
service := tables.NewService(provider)
|
||||
|
||||
state := controllers.NewState()
|
||||
readController := controllers.NewTableReadController(state, service, "alpha-table")
|
||||
writeController := controllers.NewTableWriteController(state, service, readController)
|
||||
|
||||
// Read the table
|
||||
invokeCommand(t, readController.Init())
|
||||
before, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha")
|
||||
assert.Equal(t, "This is some value", before)
|
||||
assert.False(t, state.ResultSet().IsDirty(0))
|
||||
|
||||
// Modify the item and put it
|
||||
invokeCommandWithPrompt(t, writeController.TouchItem(0), "y")
|
||||
|
||||
// Rescan the table
|
||||
invokeCommand(t, readController.Rescan())
|
||||
after, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha")
|
||||
assert.Equal(t, "This is some value", after)
|
||||
assert.False(t, state.ResultSet().IsDirty(0))
|
||||
})
|
||||
|
||||
t.Run("should not put the selected item if modified", func(t *testing.T) {
|
||||
client, cleanupFn := testdynamo.SetupTestTable(t, testData)
|
||||
defer cleanupFn()
|
||||
|
||||
provider := dynamo.NewProvider(client)
|
||||
service := tables.NewService(provider)
|
||||
|
||||
state := controllers.NewState()
|
||||
readController := controllers.NewTableReadController(state, service, "alpha-table")
|
||||
writeController := controllers.NewTableWriteController(state, service, readController)
|
||||
|
||||
// Read the table
|
||||
invokeCommand(t, readController.Init())
|
||||
before, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha")
|
||||
assert.Equal(t, "This is some value", before)
|
||||
assert.False(t, state.ResultSet().IsDirty(0))
|
||||
|
||||
// Modify the item and put it
|
||||
invokeCommandWithPrompt(t, writeController.SetStringValue(0, "alpha"), "a new value")
|
||||
invokeCommandExpectingError(t, writeController.TouchItem(0))
|
||||
})
|
||||
}
|
||||
|
||||
func TestTableWriteController_NoisyTouchItem(t *testing.T) {
|
||||
t.Run("should delete and put the selected item if unmodified", func(t *testing.T) {
|
||||
client, cleanupFn := testdynamo.SetupTestTable(t, testData)
|
||||
defer cleanupFn()
|
||||
|
||||
provider := dynamo.NewProvider(client)
|
||||
service := tables.NewService(provider)
|
||||
|
||||
state := controllers.NewState()
|
||||
readController := controllers.NewTableReadController(state, service, "alpha-table")
|
||||
writeController := controllers.NewTableWriteController(state, service, readController)
|
||||
|
||||
// Read the table
|
||||
invokeCommand(t, readController.Init())
|
||||
before, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha")
|
||||
assert.Equal(t, "This is some value", before)
|
||||
assert.False(t, state.ResultSet().IsDirty(0))
|
||||
|
||||
// Modify the item and put it
|
||||
invokeCommandWithPrompt(t, writeController.NoisyTouchItem(0), "y")
|
||||
|
||||
// Rescan the table
|
||||
invokeCommand(t, readController.Rescan())
|
||||
after, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha")
|
||||
assert.Equal(t, "This is some value", after)
|
||||
assert.False(t, state.ResultSet().IsDirty(0))
|
||||
})
|
||||
|
||||
t.Run("should not put the selected item if modified", func(t *testing.T) {
|
||||
client, cleanupFn := testdynamo.SetupTestTable(t, testData)
|
||||
defer cleanupFn()
|
||||
|
||||
provider := dynamo.NewProvider(client)
|
||||
service := tables.NewService(provider)
|
||||
|
||||
state := controllers.NewState()
|
||||
readController := controllers.NewTableReadController(state, service, "alpha-table")
|
||||
writeController := controllers.NewTableWriteController(state, service, readController)
|
||||
|
||||
// Read the table
|
||||
invokeCommand(t, readController.Init())
|
||||
before, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha")
|
||||
assert.Equal(t, "This is some value", before)
|
||||
assert.False(t, state.ResultSet().IsDirty(0))
|
||||
|
||||
// Modify the item and put it
|
||||
invokeCommandWithPrompt(t, writeController.SetStringValue(0, "alpha"), "a new value")
|
||||
invokeCommandExpectingError(t, writeController.NoisyTouchItem(0))
|
||||
})
|
||||
}
|
||||
|
|
60
internal/dynamo-browse/models/itemrender/coll.go
Normal file
60
internal/dynamo-browse/models/itemrender/coll.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
package itemrender
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"sort"
|
||||
)
|
||||
|
||||
type ListRenderer types.AttributeValueMemberL
|
||||
|
||||
func (sr *ListRenderer) TypeName() string {
|
||||
return "L"
|
||||
}
|
||||
|
||||
func (sr *ListRenderer) StringValue() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (sr *ListRenderer) MetaInfo() string {
|
||||
if len(sr.Value) == 1 {
|
||||
return fmt.Sprintf("(1 item)")
|
||||
}
|
||||
return fmt.Sprintf("(%d items)", len(sr.Value))
|
||||
}
|
||||
|
||||
func (sr *ListRenderer) SubItems() []SubItem {
|
||||
subitems := make([]SubItem, len(sr.Value))
|
||||
for i, r := range sr.Value {
|
||||
subitems[i] = SubItem{Key: fmt.Sprint(i), Value: ToRenderer(r)}
|
||||
}
|
||||
return subitems
|
||||
}
|
||||
|
||||
type MapRenderer types.AttributeValueMemberM
|
||||
|
||||
func (sr *MapRenderer) TypeName() string {
|
||||
return "M"
|
||||
}
|
||||
|
||||
func (sr *MapRenderer) StringValue() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (sr *MapRenderer) MetaInfo() string {
|
||||
if len(sr.Value) == 1 {
|
||||
return fmt.Sprintf("(1 item)")
|
||||
}
|
||||
return fmt.Sprintf("(%d items)", len(sr.Value))
|
||||
}
|
||||
|
||||
func (sr *MapRenderer) SubItems() []SubItem {
|
||||
subitems := make([]SubItem, 0)
|
||||
for k, r := range sr.Value {
|
||||
subitems = append(subitems, SubItem{Key: k, Value: ToRenderer(r)})
|
||||
}
|
||||
sort.Slice(subitems, func(i, j int) bool {
|
||||
return subitems[i].Key < subitems[j].Key
|
||||
})
|
||||
return subitems
|
||||
}
|
50
internal/dynamo-browse/models/itemrender/itemdisp.go
Normal file
50
internal/dynamo-browse/models/itemrender/itemdisp.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
package itemrender
|
||||
|
||||
import "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
|
||||
type Renderer interface {
|
||||
TypeName() string
|
||||
StringValue() string
|
||||
MetaInfo() string
|
||||
SubItems() []SubItem
|
||||
}
|
||||
|
||||
func ToRenderer(v types.AttributeValue) Renderer {
|
||||
switch colVal := v.(type) {
|
||||
case nil:
|
||||
return nil
|
||||
case *types.AttributeValueMemberS:
|
||||
x := StringRenderer(*colVal)
|
||||
return &x
|
||||
case *types.AttributeValueMemberN:
|
||||
x := NumberRenderer(*colVal)
|
||||
return &x
|
||||
case *types.AttributeValueMemberBOOL:
|
||||
x := BoolRenderer(*colVal)
|
||||
return &x
|
||||
case *types.AttributeValueMemberNULL:
|
||||
x := NullRenderer(*colVal)
|
||||
return &x
|
||||
case *types.AttributeValueMemberB:
|
||||
x := BinaryRenderer(*colVal)
|
||||
return &x
|
||||
case *types.AttributeValueMemberL:
|
||||
x := ListRenderer(*colVal)
|
||||
return &x
|
||||
case *types.AttributeValueMemberM:
|
||||
x := MapRenderer(*colVal)
|
||||
return &x
|
||||
case *types.AttributeValueMemberBS:
|
||||
return newBinarySetRenderer(colVal)
|
||||
case *types.AttributeValueMemberNS:
|
||||
return newNumberSetRenderer(colVal)
|
||||
case *types.AttributeValueMemberSS:
|
||||
return newStringSetRenderer(colVal)
|
||||
}
|
||||
return OtherRenderer{}
|
||||
}
|
||||
|
||||
type SubItem struct {
|
||||
Key string
|
||||
Value Renderer
|
||||
}
|
19
internal/dynamo-browse/models/itemrender/nils.go
Normal file
19
internal/dynamo-browse/models/itemrender/nils.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package itemrender
|
||||
|
||||
type OtherRenderer struct{}
|
||||
|
||||
func (u OtherRenderer) TypeName() string {
|
||||
return "??"
|
||||
}
|
||||
|
||||
func (sr OtherRenderer) StringValue() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (u OtherRenderer) MetaInfo() string {
|
||||
return "(unrecognised)"
|
||||
}
|
||||
|
||||
func (u OtherRenderer) SubItems() []SubItem {
|
||||
return nil
|
||||
}
|
98
internal/dynamo-browse/models/itemrender/scalars.go
Normal file
98
internal/dynamo-browse/models/itemrender/scalars.go
Normal file
|
@ -0,0 +1,98 @@
|
|||
package itemrender
|
||||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
)
|
||||
|
||||
type StringRenderer types.AttributeValueMemberS
|
||||
|
||||
func (sr *StringRenderer) TypeName() string {
|
||||
return "S"
|
||||
}
|
||||
|
||||
func (sr *StringRenderer) StringValue() string {
|
||||
return sr.Value
|
||||
}
|
||||
|
||||
func (sr *StringRenderer) MetaInfo() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (sr *StringRenderer) SubItems() []SubItem {
|
||||
return nil
|
||||
}
|
||||
|
||||
type NumberRenderer types.AttributeValueMemberN
|
||||
|
||||
func (sr *NumberRenderer) TypeName() string {
|
||||
return "N"
|
||||
}
|
||||
|
||||
func (sr *NumberRenderer) StringValue() string {
|
||||
return sr.Value
|
||||
}
|
||||
|
||||
func (sr *NumberRenderer) MetaInfo() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (sr *NumberRenderer) SubItems() []SubItem {
|
||||
return nil
|
||||
}
|
||||
|
||||
type BoolRenderer types.AttributeValueMemberBOOL
|
||||
|
||||
func (sr *BoolRenderer) TypeName() string {
|
||||
return "BOOL"
|
||||
}
|
||||
|
||||
func (sr *BoolRenderer) StringValue() string {
|
||||
if sr.Value {
|
||||
return "True"
|
||||
}
|
||||
return "False"
|
||||
}
|
||||
|
||||
func (sr *BoolRenderer) MetaInfo() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (sr *BoolRenderer) SubItems() []SubItem {
|
||||
return nil
|
||||
}
|
||||
|
||||
type BinaryRenderer types.AttributeValueMemberB
|
||||
|
||||
func (sr *BinaryRenderer) TypeName() string {
|
||||
return "B"
|
||||
}
|
||||
|
||||
func (sr *BinaryRenderer) StringValue() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (sr *BinaryRenderer) MetaInfo() string {
|
||||
return cardinality(len(sr.Value), "byte", "bytes")
|
||||
}
|
||||
|
||||
func (sr *BinaryRenderer) SubItems() []SubItem {
|
||||
return nil
|
||||
}
|
||||
|
||||
type NullRenderer types.AttributeValueMemberNULL
|
||||
|
||||
func (sr *NullRenderer) TypeName() string {
|
||||
return "NULL"
|
||||
}
|
||||
|
||||
func (sr *NullRenderer) MetaInfo() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (sr *NullRenderer) StringValue() string {
|
||||
return "null"
|
||||
}
|
||||
|
||||
func (sr *NullRenderer) SubItems() []SubItem {
|
||||
return nil
|
||||
}
|
55
internal/dynamo-browse/models/itemrender/sets.go
Normal file
55
internal/dynamo-browse/models/itemrender/sets.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package itemrender
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
)
|
||||
|
||||
type GenericRenderer struct {
|
||||
typeName string
|
||||
subitemValue []Renderer
|
||||
}
|
||||
|
||||
func (sr *GenericRenderer) TypeName() string {
|
||||
return sr.typeName
|
||||
}
|
||||
|
||||
func (sr *GenericRenderer) StringValue() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (sr *GenericRenderer) MetaInfo() string {
|
||||
return cardinality(len(sr.subitemValue), "item", "items")
|
||||
}
|
||||
|
||||
func (sr *GenericRenderer) SubItems() []SubItem {
|
||||
subitems := make([]SubItem, len(sr.subitemValue))
|
||||
for i, r := range sr.subitemValue {
|
||||
subitems[i] = SubItem{Key: fmt.Sprint(i), Value: r}
|
||||
}
|
||||
return subitems
|
||||
}
|
||||
|
||||
func newBinarySetRenderer(v *types.AttributeValueMemberBS) *GenericRenderer {
|
||||
vs := make([]Renderer, len(v.Value))
|
||||
for i, b := range v.Value {
|
||||
vs[i] = &BinaryRenderer{Value: b}
|
||||
}
|
||||
return &GenericRenderer{typeName: "BS", subitemValue: vs}
|
||||
}
|
||||
|
||||
func newNumberSetRenderer(v *types.AttributeValueMemberNS) *GenericRenderer {
|
||||
vs := make([]Renderer, len(v.Value))
|
||||
for i, n := range v.Value {
|
||||
vs[i] = &NumberRenderer{Value: n}
|
||||
}
|
||||
return &GenericRenderer{typeName: "NS", subitemValue: vs}
|
||||
}
|
||||
|
||||
func newStringSetRenderer(v *types.AttributeValueMemberSS) *GenericRenderer {
|
||||
vs := make([]Renderer, len(v.Value))
|
||||
for i, s := range v.Value {
|
||||
vs[i] = &StringRenderer{Value: s}
|
||||
}
|
||||
return &GenericRenderer{typeName: "SS", subitemValue: vs}
|
||||
}
|
10
internal/dynamo-browse/models/itemrender/utils.go
Normal file
10
internal/dynamo-browse/models/itemrender/utils.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package itemrender
|
||||
|
||||
import "fmt"
|
||||
|
||||
func cardinality(c int, single, multi string) string {
|
||||
if c == 1 {
|
||||
return fmt.Sprintf("(%d %v)", c, single)
|
||||
}
|
||||
return fmt.Sprintf("(%d %v)", c, multi)
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
package models
|
||||
|
||||
import "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/models/itemrender"
|
||||
)
|
||||
|
||||
type Item map[string]types.AttributeValue
|
||||
|
||||
|
@ -25,6 +28,10 @@ func (i Item) KeyValue(info *TableInfo) map[string]types.AttributeValue {
|
|||
return itemKey
|
||||
}
|
||||
|
||||
func (i Item) AttributeValueAsString(k string) (string, bool) {
|
||||
return attributeToString(i[k])
|
||||
func (i Item) AttributeValueAsString(key string) (string, bool) {
|
||||
return attributeToString(i[key])
|
||||
}
|
||||
|
||||
func (i Item) Renderer(key string) itemrender.Renderer {
|
||||
return itemrender.ToRenderer(i[key])
|
||||
}
|
||||
|
|
|
@ -1,15 +1,27 @@
|
|||
package models
|
||||
|
||||
import "sort"
|
||||
|
||||
type ResultSet struct {
|
||||
TableInfo *TableInfo
|
||||
Columns []string
|
||||
TableInfo *TableInfo
|
||||
Query Queryable
|
||||
//Columns []string
|
||||
items []Item
|
||||
attributes []ItemAttribute
|
||||
|
||||
columns []string
|
||||
}
|
||||
|
||||
type Queryable interface {
|
||||
String() string
|
||||
Plan(tableInfo *TableInfo) (*QueryExecutionPlan, error)
|
||||
}
|
||||
|
||||
type ItemAttribute struct {
|
||||
Marked bool
|
||||
Hidden bool
|
||||
Dirty bool
|
||||
New bool
|
||||
}
|
||||
|
||||
func (rs *ResultSet) Items() []Item {
|
||||
|
@ -21,6 +33,11 @@ func (rs *ResultSet) SetItems(items []Item) {
|
|||
rs.attributes = make([]ItemAttribute, len(items))
|
||||
}
|
||||
|
||||
func (rs *ResultSet) AddNewItem(item Item, attrs ItemAttribute) {
|
||||
rs.items = append(rs.items, item)
|
||||
rs.attributes = append(rs.attributes, attrs)
|
||||
}
|
||||
|
||||
func (rs *ResultSet) SetMark(idx int, marked bool) {
|
||||
rs.attributes[idx].Marked = marked
|
||||
}
|
||||
|
@ -29,6 +46,14 @@ func (rs *ResultSet) SetHidden(idx int, hidden bool) {
|
|||
rs.attributes[idx].Hidden = hidden
|
||||
}
|
||||
|
||||
func (rs *ResultSet) SetDirty(idx int, dirty bool) {
|
||||
rs.attributes[idx].Dirty = dirty
|
||||
}
|
||||
|
||||
func (rs *ResultSet) SetNew(idx int, isNew bool) {
|
||||
rs.attributes[idx].New = isNew
|
||||
}
|
||||
|
||||
func (rs *ResultSet) Marked(idx int) bool {
|
||||
return rs.attributes[idx].Marked
|
||||
}
|
||||
|
@ -37,6 +62,14 @@ func (rs *ResultSet) Hidden(idx int) bool {
|
|||
return rs.attributes[idx].Hidden
|
||||
}
|
||||
|
||||
func (rs *ResultSet) IsDirty(idx int) bool {
|
||||
return rs.attributes[idx].Dirty
|
||||
}
|
||||
|
||||
func (rs *ResultSet) IsNew(idx int) bool {
|
||||
return rs.attributes[idx].New
|
||||
}
|
||||
|
||||
func (rs *ResultSet) MarkedItems() []Item {
|
||||
items := make([]Item, 0)
|
||||
for i, itemAttr := range rs.attributes {
|
||||
|
@ -46,3 +79,46 @@ func (rs *ResultSet) MarkedItems() []Item {
|
|||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func (rs *ResultSet) Columns() []string {
|
||||
if rs.columns == nil {
|
||||
rs.RefreshColumns()
|
||||
}
|
||||
return rs.columns
|
||||
}
|
||||
|
||||
func (rs *ResultSet) RefreshColumns() {
|
||||
seenColumns := make(map[string]int)
|
||||
seenColumns[rs.TableInfo.Keys.PartitionKey] = 0
|
||||
if rs.TableInfo.Keys.SortKey != "" {
|
||||
seenColumns[rs.TableInfo.Keys.SortKey] = 1
|
||||
}
|
||||
|
||||
for _, definedAttribute := range rs.TableInfo.DefinedAttributes {
|
||||
if _, seen := seenColumns[definedAttribute]; !seen {
|
||||
seenColumns[definedAttribute] = len(seenColumns)
|
||||
}
|
||||
}
|
||||
|
||||
otherColsRank := len(seenColumns)
|
||||
for _, result := range rs.items {
|
||||
for k := range result {
|
||||
if _, isSeen := seenColumns[k]; !isSeen {
|
||||
seenColumns[k] = otherColsRank
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
columns := make([]string, 0, len(seenColumns))
|
||||
for k := range seenColumns {
|
||||
columns = append(columns, k)
|
||||
}
|
||||
sort.Slice(columns, func(i, j int) bool {
|
||||
if seenColumns[columns[i]] == seenColumns[columns[j]] {
|
||||
return columns[i] < columns[j]
|
||||
}
|
||||
return seenColumns[columns[i]] < seenColumns[columns[j]]
|
||||
})
|
||||
|
||||
rs.columns = columns
|
||||
}
|
||||
|
|
8
internal/dynamo-browse/models/query.go
Normal file
8
internal/dynamo-browse/models/query.go
Normal file
|
@ -0,0 +1,8 @@
|
|||
package models
|
||||
|
||||
import "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
|
||||
|
||||
type QueryExecutionPlan struct {
|
||||
CanQuery bool
|
||||
Expression expression.Expression
|
||||
}
|
32
internal/dynamo-browse/models/queryexpr/ast.go
Normal file
32
internal/dynamo-browse/models/queryexpr/ast.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
package queryexpr
|
||||
|
||||
import (
|
||||
"github.com/alecthomas/participle/v2"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type astExpr struct {
|
||||
Equality *astBinOp `parser:"@@"`
|
||||
}
|
||||
|
||||
type astBinOp struct {
|
||||
Name string `parser:"@Ident"`
|
||||
Op string `parser:"@('^' '=' | '=')"`
|
||||
Value *astLiteralValue `parser:"@@"`
|
||||
}
|
||||
|
||||
type astLiteralValue struct {
|
||||
StringVal string `parser:"@String"`
|
||||
}
|
||||
|
||||
var parser = participle.MustBuild(&astExpr{})
|
||||
|
||||
func Parse(expr string) (*QueryExpr, error) {
|
||||
var ast astExpr
|
||||
|
||||
if err := parser.ParseString("expr", expr, &ast); err != nil {
|
||||
return nil, errors.Wrapf(err, "cannot parse expression: '%v'", expr)
|
||||
}
|
||||
|
||||
return &QueryExpr{ast: &ast}, nil
|
||||
}
|
52
internal/dynamo-browse/models/queryexpr/calcquery.go
Normal file
52
internal/dynamo-browse/models/queryexpr/calcquery.go
Normal file
|
@ -0,0 +1,52 @@
|
|||
package queryexpr
|
||||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/models"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (a *astExpr) calcQuery(tableInfo *models.TableInfo) (*models.QueryExecutionPlan, error) {
|
||||
return a.Equality.calcQuery(tableInfo)
|
||||
}
|
||||
|
||||
func (a *astBinOp) calcQuery(info *models.TableInfo) (*models.QueryExecutionPlan, error) {
|
||||
// TODO: check if can be a query
|
||||
cb, err := a.calcQueryForScan(info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
builder := expression.NewBuilder()
|
||||
builder = builder.WithFilter(cb)
|
||||
|
||||
expr, err := builder.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.QueryExecutionPlan{
|
||||
CanQuery: false,
|
||||
Expression: expr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *astBinOp) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) {
|
||||
v, err := a.Value.goValue()
|
||||
if err != nil {
|
||||
return expression.ConditionBuilder{}, err
|
||||
}
|
||||
|
||||
switch a.Op {
|
||||
case "=":
|
||||
return expression.Name(a.Name).Equal(expression.Value(v)), nil
|
||||
case "^=":
|
||||
strValue, isStrValue := v.(string)
|
||||
if !isStrValue {
|
||||
return expression.ConditionBuilder{}, errors.New("operand '^=' must be string")
|
||||
}
|
||||
return expression.Name(a.Name).BeginsWith(strValue), nil
|
||||
}
|
||||
|
||||
return expression.ConditionBuilder{}, errors.Errorf("unrecognised operator: %v", a.Op)
|
||||
}
|
15
internal/dynamo-browse/models/queryexpr/expr.go
Normal file
15
internal/dynamo-browse/models/queryexpr/expr.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package queryexpr
|
||||
|
||||
import "github.com/lmika/awstools/internal/dynamo-browse/models"
|
||||
|
||||
type QueryExpr struct {
|
||||
ast *astExpr
|
||||
}
|
||||
|
||||
func (md *QueryExpr) Plan(tableInfo *models.TableInfo) (*models.QueryExecutionPlan, error) {
|
||||
return md.ast.calcQuery(tableInfo)
|
||||
}
|
||||
|
||||
func (md *QueryExpr) String() string {
|
||||
return md.ast.String()
|
||||
}
|
47
internal/dynamo-browse/models/queryexpr/expr_test.go
Normal file
47
internal/dynamo-browse/models/queryexpr/expr_test.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package queryexpr_test
|
||||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/models/queryexpr"
|
||||
"testing"
|
||||
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestModExpr_Query(t *testing.T) {
|
||||
tableInfo := &models.TableInfo{
|
||||
Name: "test",
|
||||
Keys: models.KeyAttribute{
|
||||
PartitionKey: "pk",
|
||||
SortKey: "sk",
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("perform query when request pk is fixed", func(t *testing.T) {
|
||||
modExpr, err := queryexpr.Parse(`pk="prefix"`)
|
||||
assert.NoError(t, err)
|
||||
|
||||
plan, err := modExpr.Plan(tableInfo)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.False(t, plan.CanQuery)
|
||||
assert.Equal(t, "#0 = :0", aws.ToString(plan.Expression.Filter()))
|
||||
assert.Equal(t, "pk", plan.Expression.Names()["#0"])
|
||||
assert.Equal(t, "prefix", plan.Expression.Values()[":0"].(*types.AttributeValueMemberS).Value)
|
||||
})
|
||||
|
||||
t.Run("perform scan when request pk prefix", func(t *testing.T) {
|
||||
modExpr, err := queryexpr.Parse(`pk^="prefix"`) // TODO: fix this so that '^ =' is invalid
|
||||
assert.NoError(t, err)
|
||||
|
||||
plan, err := modExpr.Plan(tableInfo)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.False(t, plan.CanQuery)
|
||||
assert.Equal(t, "begins_with (#0, :0)", aws.ToString(plan.Expression.Filter()))
|
||||
assert.Equal(t, "pk", plan.Expression.Names()["#0"])
|
||||
assert.Equal(t, "prefix", plan.Expression.Values()[":0"].(*types.AttributeValueMemberS).Value)
|
||||
})
|
||||
}
|
13
internal/dynamo-browse/models/queryexpr/tostr.go
Normal file
13
internal/dynamo-browse/models/queryexpr/tostr.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package queryexpr
|
||||
|
||||
func (a *astExpr) String() string {
|
||||
return a.Equality.String()
|
||||
}
|
||||
|
||||
func (a *astBinOp) String() string {
|
||||
return a.Name + a.Op + a.Value.String()
|
||||
}
|
||||
|
||||
func (a *astLiteralValue) String() string {
|
||||
return a.StringVal
|
||||
}
|
24
internal/dynamo-browse/models/queryexpr/values.go
Normal file
24
internal/dynamo-browse/models/queryexpr/values.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
package queryexpr
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (a *astLiteralValue) dynamoValue() (types.AttributeValue, error) {
|
||||
s, err := strconv.Unquote(a.StringVal)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot unquote string")
|
||||
}
|
||||
return &types.AttributeValueMemberS{Value: s}, nil
|
||||
}
|
||||
|
||||
func (a *astLiteralValue) goValue() (any, error) {
|
||||
s, err := strconv.Unquote(a.StringVal)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot unquote string")
|
||||
}
|
||||
return s, nil
|
||||
}
|
|
@ -2,8 +2,8 @@ package dynamo
|
|||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/models"
|
||||
|
@ -64,17 +64,34 @@ func NewProvider(client *dynamodb.Client) *Provider {
|
|||
return &Provider{client: client}
|
||||
}
|
||||
|
||||
func (p *Provider) ScanItems(ctx context.Context, tableName string) ([]models.Item, error) {
|
||||
res, err := p.client.Scan(ctx, &dynamodb.ScanInput{
|
||||
func (p *Provider) ScanItems(ctx context.Context, tableName string, filterExpr *expression.Expression, maxItems int) ([]models.Item, error) {
|
||||
input := &dynamodb.ScanInput{
|
||||
TableName: aws.String(tableName),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "cannot execute scan on table %v", tableName)
|
||||
Limit: aws.Int32(int32(maxItems)),
|
||||
}
|
||||
if filterExpr != nil {
|
||||
input.FilterExpression = filterExpr.Filter()
|
||||
input.ExpressionAttributeNames = filterExpr.Names()
|
||||
input.ExpressionAttributeValues = filterExpr.Values()
|
||||
}
|
||||
|
||||
items := make([]models.Item, len(res.Items))
|
||||
for i, itm := range res.Items {
|
||||
items[i] = itm
|
||||
paginator := dynamodb.NewScanPaginator(p.client, input)
|
||||
|
||||
items := make([]models.Item, 0)
|
||||
|
||||
outer:
|
||||
for paginator.HasMorePages() {
|
||||
res, err := paginator.NextPage(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "cannot execute scan on table %v", tableName)
|
||||
}
|
||||
|
||||
for _, itm := range res.Items {
|
||||
items = append(items, itm)
|
||||
if len(items) >= maxItems {
|
||||
break outer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return items, nil
|
||||
|
|
|
@ -11,38 +11,38 @@ import (
|
|||
)
|
||||
|
||||
func TestProvider_ScanItems(t *testing.T) {
|
||||
tableName := "provider-scanimages-test-table"
|
||||
tableName := "test-table"
|
||||
|
||||
client, cleanupFn := testdynamo.SetupTestTable(t, tableName, testData)
|
||||
client, cleanupFn := testdynamo.SetupTestTable(t, testData)
|
||||
defer cleanupFn()
|
||||
provider := dynamo.NewProvider(client)
|
||||
|
||||
t.Run("should return scanned items from the table", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
items, err := provider.ScanItems(ctx, tableName)
|
||||
items, err := provider.ScanItems(ctx, tableName, nil, 100)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, items, 3)
|
||||
|
||||
assert.Contains(t, items, testdynamo.TestRecordAsItem(t, testData[0]))
|
||||
assert.Contains(t, items, testdynamo.TestRecordAsItem(t, testData[1]))
|
||||
assert.Contains(t, items, testdynamo.TestRecordAsItem(t, testData[2]))
|
||||
assert.Contains(t, items, testdynamo.TestRecordAsItem(t, testData[0].Data[0]))
|
||||
assert.Contains(t, items, testdynamo.TestRecordAsItem(t, testData[0].Data[1]))
|
||||
assert.Contains(t, items, testdynamo.TestRecordAsItem(t, testData[0].Data[2]))
|
||||
})
|
||||
|
||||
t.Run("should return error if table name does not exist", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
items, err := provider.ScanItems(ctx, "does-not-exist")
|
||||
items, err := provider.ScanItems(ctx, "does-not-exist", nil, 100)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, items)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProvider_DeleteItem(t *testing.T) {
|
||||
tableName := "provider-deleteitem-test-table"
|
||||
tableName := "test-table"
|
||||
|
||||
t.Run("should delete item if exists in table", func(t *testing.T) {
|
||||
client, cleanupFn := testdynamo.SetupTestTable(t, tableName, testData)
|
||||
client, cleanupFn := testdynamo.SetupTestTable(t, testData)
|
||||
defer cleanupFn()
|
||||
provider := dynamo.NewProvider(client)
|
||||
|
||||
|
@ -53,18 +53,18 @@ func TestProvider_DeleteItem(t *testing.T) {
|
|||
"sk": &types.AttributeValueMemberS{Value: "222"},
|
||||
})
|
||||
|
||||
items, err := provider.ScanItems(ctx, tableName)
|
||||
items, err := provider.ScanItems(ctx, tableName, nil, 100)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, items, 2)
|
||||
|
||||
assert.Contains(t, items, testdynamo.TestRecordAsItem(t, testData[0]))
|
||||
assert.Contains(t, items, testdynamo.TestRecordAsItem(t, testData[2]))
|
||||
assert.NotContains(t, items, testdynamo.TestRecordAsItem(t, testData[1]))
|
||||
assert.Contains(t, items, testdynamo.TestRecordAsItem(t, testData[0].Data[0]))
|
||||
assert.Contains(t, items, testdynamo.TestRecordAsItem(t, testData[0].Data[2]))
|
||||
assert.NotContains(t, items, testdynamo.TestRecordAsItem(t, testData[0].Data[1]))
|
||||
|
||||
})
|
||||
|
||||
t.Run("should do nothing if key does not exist", func(t *testing.T) {
|
||||
client, cleanupFn := testdynamo.SetupTestTable(t, tableName, testData)
|
||||
client, cleanupFn := testdynamo.SetupTestTable(t, testData)
|
||||
defer cleanupFn()
|
||||
provider := dynamo.NewProvider(client)
|
||||
|
||||
|
@ -75,44 +75,49 @@ func TestProvider_DeleteItem(t *testing.T) {
|
|||
"sk": &types.AttributeValueMemberS{Value: "999"},
|
||||
})
|
||||
|
||||
items, err := provider.ScanItems(ctx, tableName)
|
||||
items, err := provider.ScanItems(ctx, tableName, nil, 100)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, items, 3)
|
||||
|
||||
assert.Contains(t, items, testdynamo.TestRecordAsItem(t, testData[0]))
|
||||
assert.Contains(t, items, testdynamo.TestRecordAsItem(t, testData[1]))
|
||||
assert.Contains(t, items, testdynamo.TestRecordAsItem(t, testData[2]))
|
||||
assert.Contains(t, items, testdynamo.TestRecordAsItem(t, testData[0].Data[0]))
|
||||
assert.Contains(t, items, testdynamo.TestRecordAsItem(t, testData[0].Data[1]))
|
||||
assert.Contains(t, items, testdynamo.TestRecordAsItem(t, testData[0].Data[2]))
|
||||
})
|
||||
|
||||
t.Run("should return error if table name does not exist", func(t *testing.T) {
|
||||
client, cleanupFn := testdynamo.SetupTestTable(t, tableName, testData)
|
||||
client, cleanupFn := testdynamo.SetupTestTable(t, testData)
|
||||
defer cleanupFn()
|
||||
provider := dynamo.NewProvider(client)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
items, err := provider.ScanItems(ctx, "does-not-exist")
|
||||
items, err := provider.ScanItems(ctx, "does-not-exist", nil, 100)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, items)
|
||||
})
|
||||
}
|
||||
|
||||
var testData = testdynamo.TestData{
|
||||
var testData = []testdynamo.TestData{
|
||||
{
|
||||
"pk": "abc",
|
||||
"sk": "111",
|
||||
"alpha": "This is some value",
|
||||
},
|
||||
{
|
||||
"pk": "abc",
|
||||
"sk": "222",
|
||||
"alpha": "This is another some value",
|
||||
"beta": 1231,
|
||||
},
|
||||
{
|
||||
"pk": "bbb",
|
||||
"sk": "131",
|
||||
"beta": 2468,
|
||||
"gamma": "foobar",
|
||||
TableName: "test-table",
|
||||
Data: []map[string]interface{}{
|
||||
{
|
||||
"pk": "abc",
|
||||
"sk": "111",
|
||||
"alpha": "This is some value",
|
||||
},
|
||||
{
|
||||
"pk": "abc",
|
||||
"sk": "222",
|
||||
"alpha": "This is another some value",
|
||||
"beta": 1231,
|
||||
},
|
||||
{
|
||||
"pk": "bbb",
|
||||
"sk": "131",
|
||||
"beta": 2468,
|
||||
"gamma": "foobar",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package tables
|
|||
|
||||
import (
|
||||
"context"
|
||||
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/models"
|
||||
|
@ -10,7 +11,7 @@ import (
|
|||
type TableProvider interface {
|
||||
ListTables(ctx context.Context) ([]string, error)
|
||||
DescribeTable(ctx context.Context, tableName string) (*models.TableInfo, error)
|
||||
ScanItems(ctx context.Context, tableName string) ([]models.Item, error)
|
||||
ScanItems(ctx context.Context, tableName string, filterExpr *expression.Expression, maxItems int) ([]models.Item, error)
|
||||
DeleteItem(ctx context.Context, tableName string, key map[string]types.AttributeValue) error
|
||||
PutItem(ctx context.Context, name string, item models.Item) error
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ package tables
|
|||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
|
||||
"strings"
|
||||
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/models"
|
||||
|
@ -28,51 +28,73 @@ func (s *Service) Describe(ctx context.Context, table string) (*models.TableInfo
|
|||
}
|
||||
|
||||
func (s *Service) Scan(ctx context.Context, tableInfo *models.TableInfo) (*models.ResultSet, error) {
|
||||
results, err := s.provider.ScanItems(ctx, tableInfo.Name)
|
||||
return s.doScan(ctx, tableInfo, nil)
|
||||
}
|
||||
|
||||
func (s *Service) doScan(ctx context.Context, tableInfo *models.TableInfo, expr models.Queryable) (*models.ResultSet, error) {
|
||||
var filterExpr *expression.Expression
|
||||
|
||||
if expr != nil {
|
||||
plan, err := expr.Plan(tableInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TEMP
|
||||
if plan.CanQuery {
|
||||
return nil, errors.Errorf("queries not yet supported")
|
||||
}
|
||||
|
||||
filterExpr = &plan.Expression
|
||||
}
|
||||
|
||||
results, err := s.provider.ScanItems(ctx, tableInfo.Name, filterExpr, 1000)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "unable to scan table %v", tableInfo.Name)
|
||||
}
|
||||
|
||||
// Get the columns
|
||||
seenColumns := make(map[string]int)
|
||||
seenColumns[tableInfo.Keys.PartitionKey] = 0
|
||||
if tableInfo.Keys.SortKey != "" {
|
||||
seenColumns[tableInfo.Keys.SortKey] = 1
|
||||
}
|
||||
|
||||
for _, definedAttribute := range tableInfo.DefinedAttributes {
|
||||
if _, seen := seenColumns[definedAttribute]; !seen {
|
||||
seenColumns[definedAttribute] = len(seenColumns)
|
||||
}
|
||||
}
|
||||
|
||||
otherColsRank := len(seenColumns)
|
||||
for _, result := range results {
|
||||
for k := range result {
|
||||
if _, isSeen := seenColumns[k]; !isSeen {
|
||||
seenColumns[k] = otherColsRank
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
columns := make([]string, 0, len(seenColumns))
|
||||
for k := range seenColumns {
|
||||
columns = append(columns, k)
|
||||
}
|
||||
sort.Slice(columns, func(i, j int) bool {
|
||||
if seenColumns[columns[i]] == seenColumns[columns[j]] {
|
||||
return columns[i] < columns[j]
|
||||
}
|
||||
return seenColumns[columns[i]] < seenColumns[columns[j]]
|
||||
})
|
||||
//seenColumns := make(map[string]int)
|
||||
//seenColumns[tableInfo.Keys.PartitionKey] = 0
|
||||
//if tableInfo.Keys.SortKey != "" {
|
||||
// seenColumns[tableInfo.Keys.SortKey] = 1
|
||||
//}
|
||||
//
|
||||
//for _, definedAttribute := range tableInfo.DefinedAttributes {
|
||||
// if _, seen := seenColumns[definedAttribute]; !seen {
|
||||
// seenColumns[definedAttribute] = len(seenColumns)
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//otherColsRank := len(seenColumns)
|
||||
//for _, result := range results {
|
||||
// for k := range result {
|
||||
// if _, isSeen := seenColumns[k]; !isSeen {
|
||||
// seenColumns[k] = otherColsRank
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//columns := make([]string, 0, len(seenColumns))
|
||||
//for k := range seenColumns {
|
||||
// columns = append(columns, k)
|
||||
//}
|
||||
//sort.Slice(columns, func(i, j int) bool {
|
||||
// if seenColumns[columns[i]] == seenColumns[columns[j]] {
|
||||
// return columns[i] < columns[j]
|
||||
// }
|
||||
// return seenColumns[columns[i]] < seenColumns[columns[j]]
|
||||
//})
|
||||
|
||||
models.Sort(results, tableInfo)
|
||||
|
||||
resultSet := &models.ResultSet{
|
||||
TableInfo: tableInfo,
|
||||
Columns: columns,
|
||||
Query: expr,
|
||||
//Columns: columns,
|
||||
}
|
||||
resultSet.SetItems(results)
|
||||
resultSet.RefreshColumns()
|
||||
|
||||
return resultSet, nil
|
||||
}
|
||||
|
@ -81,6 +103,17 @@ func (s *Service) Put(ctx context.Context, tableInfo *models.TableInfo, item mod
|
|||
return s.provider.PutItem(ctx, tableInfo.Name, item)
|
||||
}
|
||||
|
||||
func (s *Service) PutItemAt(ctx context.Context, resultSet *models.ResultSet, index int) error {
|
||||
item := resultSet.Items()[index]
|
||||
if err := s.provider.PutItem(ctx, resultSet.TableInfo.Name, item); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resultSet.SetDirty(index, false)
|
||||
resultSet.SetNew(index, false)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) Delete(ctx context.Context, tableInfo *models.TableInfo, items []models.Item) error {
|
||||
for _, item := range items {
|
||||
if err := s.provider.DeleteItem(ctx, tableInfo.Name, item.KeyValue(tableInfo)); err != nil {
|
||||
|
@ -90,6 +123,10 @@ func (s *Service) Delete(ctx context.Context, tableInfo *models.TableInfo, items
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) ScanOrQuery(ctx context.Context, tableInfo *models.TableInfo, expr models.Queryable) (*models.ResultSet, error) {
|
||||
return s.doScan(ctx, tableInfo, expr)
|
||||
}
|
||||
|
||||
// TODO: move into a new service
|
||||
func (s *Service) Filter(resultSet *models.ResultSet, filter string) *models.ResultSet {
|
||||
for i, item := range resultSet.Items() {
|
||||
|
|
|
@ -11,9 +11,9 @@ import (
|
|||
)
|
||||
|
||||
func TestService_Describe(t *testing.T) {
|
||||
tableName := "service-describe-table"
|
||||
tableName := "service-test-data"
|
||||
|
||||
client, cleanupFn := testdynamo.SetupTestTable(t, tableName, testData)
|
||||
client, cleanupFn := testdynamo.SetupTestTable(t, testData)
|
||||
defer cleanupFn()
|
||||
provider := dynamo.NewProvider(client)
|
||||
|
||||
|
@ -33,9 +33,9 @@ func TestService_Describe(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestService_Scan(t *testing.T) {
|
||||
tableName := "service-scan-test-table"
|
||||
tableName := "service-test-data"
|
||||
|
||||
client, cleanupFn := testdynamo.SetupTestTable(t, tableName, testData)
|
||||
client, cleanupFn := testdynamo.SetupTestTable(t, testData)
|
||||
defer cleanupFn()
|
||||
provider := dynamo.NewProvider(client)
|
||||
|
||||
|
@ -51,29 +51,34 @@ func TestService_Scan(t *testing.T) {
|
|||
|
||||
// Hash first, then range, then columns in alphabetic order
|
||||
assert.Equal(t, rs.TableInfo, ti)
|
||||
assert.Equal(t, rs.Columns, []string{"pk", "sk", "alpha", "beta", "gamma"})
|
||||
assert.Equal(t, rs.Columns(), []string{"pk", "sk", "alpha", "beta", "gamma"})
|
||||
//assert.Equal(t, rs.Items[0], testdynamo.TestRecordAsItem(t, testData[1]))
|
||||
//assert.Equal(t, rs.Items[1], testdynamo.TestRecordAsItem(t, testData[0]))
|
||||
//assert.Equal(t, rs.Items[2], testdynamo.TestRecordAsItem(t, testData[2]))
|
||||
})
|
||||
}
|
||||
|
||||
var testData = testdynamo.TestData{
|
||||
var testData = []testdynamo.TestData{
|
||||
{
|
||||
"pk": "abc",
|
||||
"sk": "222",
|
||||
"alpha": "This is another some value",
|
||||
"beta": 1231,
|
||||
},
|
||||
{
|
||||
"pk": "abc",
|
||||
"sk": "111",
|
||||
"alpha": "This is some value",
|
||||
},
|
||||
{
|
||||
"pk": "bbb",
|
||||
"sk": "131",
|
||||
"beta": 2468,
|
||||
"gamma": "foobar",
|
||||
TableName: "service-test-data",
|
||||
Data: []map[string]interface{}{
|
||||
{
|
||||
"pk": "abc",
|
||||
"sk": "222",
|
||||
"alpha": "This is another some value",
|
||||
"beta": 1231,
|
||||
},
|
||||
{
|
||||
"pk": "abc",
|
||||
"sk": "111",
|
||||
"alpha": "This is some value",
|
||||
},
|
||||
{
|
||||
"pk": "bbb",
|
||||
"sk": "131",
|
||||
"beta": 2468,
|
||||
"gamma": "foobar",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -3,13 +3,17 @@ package ui
|
|||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/awstools/internal/common/ui/commandctrl"
|
||||
"github.com/lmika/awstools/internal/common/ui/events"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/controllers"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/dynamoitemedit"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/dialogprompt"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/dynamoitemview"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/dynamotableview"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/layout"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/statusandprompt"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/styles"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/tableselect"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
|
@ -25,13 +29,16 @@ type Model struct {
|
|||
}
|
||||
|
||||
func NewModel(rc *controllers.TableReadController, wc *controllers.TableWriteController, cc *commandctrl.CommandController) Model {
|
||||
dtv := dynamotableview.New()
|
||||
div := dynamoitemview.New()
|
||||
uiStyles := styles.DefaultStyles
|
||||
|
||||
dtv := dynamotableview.New(uiStyles)
|
||||
div := dynamoitemview.New(uiStyles)
|
||||
mainView := layout.NewVBox(layout.LastChildFixedAt(17), dtv, div)
|
||||
|
||||
itemEdit := dynamoitemedit.NewModel(mainView)
|
||||
statusAndPrompt := statusandprompt.New(itemEdit, "")
|
||||
tableSelect := tableselect.New(statusAndPrompt)
|
||||
statusAndPrompt := statusandprompt.New(itemEdit, "", uiStyles.StatusAndPrompt)
|
||||
dialogPrompt := dialogprompt.New(statusAndPrompt)
|
||||
tableSelect := tableselect.New(dialogPrompt, uiStyles)
|
||||
|
||||
cc.AddCommands(&commandctrl.CommandContext{
|
||||
Commands: map[string]commandctrl.Command{
|
||||
|
@ -43,8 +50,45 @@ func NewModel(rc *controllers.TableReadController, wc *controllers.TableWriteCon
|
|||
return rc.ScanTable(args[0])
|
||||
}
|
||||
},
|
||||
"export": func(args []string) tea.Cmd {
|
||||
if len(args) == 0 {
|
||||
return events.SetError(errors.New("expected filename"))
|
||||
}
|
||||
return rc.ExportCSV(args[0])
|
||||
},
|
||||
"unmark": commandctrl.NoArgCommand(rc.Unmark()),
|
||||
"delete": commandctrl.NoArgCommand(wc.DeleteMarked()),
|
||||
|
||||
// TEMP
|
||||
"new-item": commandctrl.NoArgCommand(wc.NewItem()),
|
||||
"set-s": func(args []string) tea.Cmd {
|
||||
if len(args) == 0 {
|
||||
return events.SetError(errors.New("expected field"))
|
||||
}
|
||||
return wc.SetStringValue(dtv.SelectedItemIndex(), args[0])
|
||||
},
|
||||
"set-n": func(args []string) tea.Cmd {
|
||||
if len(args) == 0 {
|
||||
return events.SetError(errors.New("expected field"))
|
||||
}
|
||||
return wc.SetNumberValue(dtv.SelectedItemIndex(), args[0])
|
||||
},
|
||||
"del-attr": func(args []string) tea.Cmd {
|
||||
if len(args) == 0 {
|
||||
return events.SetError(errors.New("expected field"))
|
||||
}
|
||||
return wc.DeleteAttribute(dtv.SelectedItemIndex(), args[0])
|
||||
},
|
||||
|
||||
"put": func(args []string) tea.Cmd {
|
||||
return wc.PutItem(dtv.SelectedItemIndex())
|
||||
},
|
||||
"touch": func(args []string) tea.Cmd {
|
||||
return wc.TouchItem(dtv.SelectedItemIndex())
|
||||
},
|
||||
"noisy-touch": func(args []string) tea.Cmd {
|
||||
return wc.NoisyTouchItem(dtv.SelectedItemIndex())
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -77,8 +121,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
if idx := m.tableView.SelectedItemIndex(); idx >= 0 {
|
||||
return m, m.tableWriteController.ToggleMark(idx)
|
||||
}
|
||||
case "s":
|
||||
case "R":
|
||||
return m, m.tableReadController.Rescan()
|
||||
case "?":
|
||||
return m, m.tableReadController.PromptForQuery()
|
||||
case "/":
|
||||
return m, m.tableReadController.Filter()
|
||||
case "e":
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
package dialogprompt
|
||||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/layout"
|
||||
)
|
||||
|
||||
var style = lipgloss.NewStyle().
|
||||
BorderStyle(lipgloss.NormalBorder()).
|
||||
BorderForeground(lipgloss.Color("63"))
|
||||
|
||||
type dialogModel struct {
|
||||
w, h int
|
||||
borderStyle lipgloss.Style
|
||||
}
|
||||
|
||||
func (d *dialogModel) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dialogModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *dialogModel) View() string {
|
||||
return style.Width(d.w).Height(d.h).Render("Hello this is a test of some content")
|
||||
}
|
||||
|
||||
func (d *dialogModel) Resize(w, h int) layout.ResizingModel {
|
||||
d.w, d.h = w-2, h-2
|
||||
return d
|
||||
}
|
38
internal/dynamo-browse/ui/teamodels/dialogprompt/model.go
Normal file
38
internal/dynamo-browse/ui/teamodels/dialogprompt/model.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
package dialogprompt
|
||||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/layout"
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
compositor *layout.Compositor
|
||||
}
|
||||
|
||||
func New(model layout.ResizingModel) *Model {
|
||||
m := &Model{
|
||||
compositor: layout.NewCompositor(model),
|
||||
}
|
||||
// TEMP
|
||||
//m.compositor.SetOverlay(&dialogModel{}, 5, 5, 30, 12)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Model) Init() tea.Cmd {
|
||||
return m.compositor.Init()
|
||||
}
|
||||
|
||||
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
newModel, cmd := m.compositor.Update(msg)
|
||||
m.compositor = newModel.(*layout.Compositor)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m *Model) View() string {
|
||||
return m.compositor.View()
|
||||
}
|
||||
|
||||
func (m *Model) Resize(w, h int) layout.ResizingModel {
|
||||
m.compositor = m.compositor.Resize(w, h).(*layout.Compositor)
|
||||
return m
|
||||
}
|
|
@ -2,10 +2,12 @@ package dynamoitemview
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/models/itemrender"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/styles"
|
||||
"io"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
@ -16,9 +18,14 @@ import (
|
|||
|
||||
var (
|
||||
activeHeaderStyle = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#ffffff")).
|
||||
Background(lipgloss.Color("#4479ff"))
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#ffffff")).
|
||||
Background(lipgloss.Color("#4479ff"))
|
||||
|
||||
fieldTypeStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.AdaptiveColor{Light: "#2B800C", Dark: "#73C653"})
|
||||
metaInfoStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
|
@ -32,9 +39,9 @@ type Model struct {
|
|||
selectedItem models.Item
|
||||
}
|
||||
|
||||
func New() *Model {
|
||||
func New(uiStyles styles.Styles) *Model {
|
||||
return &Model{
|
||||
frameTitle: frame.NewFrameTitle("Item", false, activeHeaderStyle),
|
||||
frameTitle: frame.NewFrameTitle("Item", false, uiStyles.Frames),
|
||||
viewport: viewport.New(100, 100),
|
||||
}
|
||||
}
|
||||
|
@ -82,16 +89,19 @@ func (m *Model) updateViewportToSelectedMessage() {
|
|||
|
||||
viewportContent := &strings.Builder{}
|
||||
tabWriter := tabwriter.NewWriter(viewportContent, 0, 1, 1, ' ', 0)
|
||||
for _, colName := range m.currentResultSet.Columns {
|
||||
switch colVal := m.selectedItem[colName].(type) {
|
||||
case nil:
|
||||
break
|
||||
case *types.AttributeValueMemberS:
|
||||
fmt.Fprintf(tabWriter, "%v\tS\t%s\n", colName, colVal.Value)
|
||||
case *types.AttributeValueMemberN:
|
||||
fmt.Fprintf(tabWriter, "%v\tN\t%s\n", colName, colVal.Value)
|
||||
default:
|
||||
fmt.Fprintf(tabWriter, "%v\t?\t%s\n", colName, "(other)")
|
||||
|
||||
seenColumns := make(map[string]struct{})
|
||||
for _, colName := range m.currentResultSet.Columns() {
|
||||
seenColumns[colName] = struct{}{}
|
||||
if r := m.selectedItem.Renderer(colName); r != nil {
|
||||
m.renderItem(tabWriter, "", colName, r)
|
||||
}
|
||||
}
|
||||
for k, _ := range m.selectedItem {
|
||||
if _, seen := seenColumns[k]; !seen {
|
||||
if r := m.selectedItem.Renderer(k); r != nil {
|
||||
m.renderItem(tabWriter, "", k, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -100,3 +110,13 @@ func (m *Model) updateViewportToSelectedMessage() {
|
|||
m.viewport.Height = m.h - m.frameTitle.HeaderHeight()
|
||||
m.viewport.SetContent(viewportContent.String())
|
||||
}
|
||||
|
||||
func (m *Model) renderItem(w io.Writer, prefix string, name string, r itemrender.Renderer) {
|
||||
fmt.Fprintf(w, "%s%v\t%s\t%s%s\n",
|
||||
prefix, name, fieldTypeStyle.Render(r.TypeName()), r.StringValue(), metaInfoStyle.Render(r.MetaInfo()))
|
||||
if subitems := r.SubItems(); len(subitems) > 0 {
|
||||
for _, si := range subitems {
|
||||
m.renderItem(w, prefix+" ", si.Key, si.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package dynamotableview
|
||||
|
||||
import (
|
||||
table "github.com/calyptia/go-bubble-table"
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/controllers"
|
||||
|
@ -9,6 +9,8 @@ import (
|
|||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/dynamoitemview"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/frame"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/layout"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/styles"
|
||||
table "github.com/lmika/go-bubble-table"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -18,26 +20,61 @@ var (
|
|||
Background(lipgloss.Color("#4479ff"))
|
||||
)
|
||||
|
||||
type KeyBinding struct {
|
||||
MoveUp key.Binding
|
||||
MoveDown key.Binding
|
||||
PageUp key.Binding
|
||||
PageDown key.Binding
|
||||
Home key.Binding
|
||||
End key.Binding
|
||||
ColLeft key.Binding
|
||||
ColRight key.Binding
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
frameTitle frame.FrameTitle
|
||||
table table.Model
|
||||
w, h int
|
||||
keyBinding KeyBinding
|
||||
|
||||
// model state
|
||||
colOffset int
|
||||
rows []table.Row
|
||||
resultSet *models.ResultSet
|
||||
}
|
||||
|
||||
func New() *Model {
|
||||
tbl := table.New([]string{"pk", "sk"}, 100, 100)
|
||||
type columnModel struct {
|
||||
m *Model
|
||||
}
|
||||
|
||||
func (cm columnModel) Len() int {
|
||||
return len(cm.m.resultSet.Columns()[cm.m.colOffset:])
|
||||
}
|
||||
|
||||
func (cm columnModel) Header(index int) string {
|
||||
return cm.m.resultSet.Columns()[cm.m.colOffset+index]
|
||||
}
|
||||
|
||||
func New(uiStyles styles.Styles) *Model {
|
||||
tbl := table.New(table.SimpleColumns([]string{"pk", "sk"}), 100, 100)
|
||||
rows := make([]table.Row, 0)
|
||||
tbl.SetRows(rows)
|
||||
|
||||
frameTitle := frame.NewFrameTitle("No table", true, activeHeaderStyle)
|
||||
frameTitle := frame.NewFrameTitle("No table", true, uiStyles.Frames)
|
||||
|
||||
return &Model{
|
||||
frameTitle: frameTitle,
|
||||
table: tbl,
|
||||
keyBinding: KeyBinding{
|
||||
MoveUp: key.NewBinding(key.WithKeys("i", "up")),
|
||||
MoveDown: key.NewBinding(key.WithKeys("k", "down")),
|
||||
PageUp: key.NewBinding(key.WithKeys("I", "pgup")),
|
||||
PageDown: key.NewBinding(key.WithKeys("K", "pgdown")),
|
||||
Home: key.NewBinding(key.WithKeys("I", "home")),
|
||||
End: key.NewBinding(key.WithKeys("K", "end")),
|
||||
ColLeft: key.NewBinding(key.WithKeys("j", "left")),
|
||||
ColRight: key.NewBinding(key.WithKeys("l", "right")),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -52,26 +89,49 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
m.updateTable()
|
||||
return m, m.postSelectedItemChanged
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
switch {
|
||||
// Table nav
|
||||
case "i", "up":
|
||||
case key.Matches(msg, m.keyBinding.MoveUp):
|
||||
m.table.GoUp()
|
||||
return m, m.postSelectedItemChanged
|
||||
case "k", "down":
|
||||
case key.Matches(msg, m.keyBinding.MoveDown):
|
||||
m.table.GoDown()
|
||||
return m, m.postSelectedItemChanged
|
||||
case "I", "pgup":
|
||||
case key.Matches(msg, m.keyBinding.PageUp):
|
||||
m.table.GoPageUp()
|
||||
return m, m.postSelectedItemChanged
|
||||
case "K", "pgdn":
|
||||
case key.Matches(msg, m.keyBinding.PageDown):
|
||||
m.table.GoPageDown()
|
||||
return m, m.postSelectedItemChanged
|
||||
case key.Matches(msg, m.keyBinding.Home):
|
||||
m.table.GoTop()
|
||||
return m, m.postSelectedItemChanged
|
||||
case key.Matches(msg, m.keyBinding.End):
|
||||
m.table.GoBottom()
|
||||
return m, m.postSelectedItemChanged
|
||||
case key.Matches(msg, m.keyBinding.ColLeft):
|
||||
m.setLeftmostDisplayedColumn(m.colOffset - 1)
|
||||
return m, nil
|
||||
case key.Matches(msg, m.keyBinding.ColRight):
|
||||
m.setLeftmostDisplayedColumn(m.colOffset + 1)
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Model) setLeftmostDisplayedColumn(newCol int) {
|
||||
if newCol < 0 {
|
||||
m.colOffset = 0
|
||||
} else if newCol >= len(m.resultSet.Columns()) {
|
||||
m.colOffset = len(m.resultSet.Columns()) - 1
|
||||
} else {
|
||||
m.colOffset = newCol
|
||||
}
|
||||
m.table.UpdateView()
|
||||
}
|
||||
|
||||
func (m *Model) View() string {
|
||||
return lipgloss.JoinVertical(lipgloss.Top, m.frameTitle.View(), m.table.View())
|
||||
}
|
||||
|
@ -85,23 +145,32 @@ func (m *Model) Resize(w, h int) layout.ResizingModel {
|
|||
}
|
||||
|
||||
func (m *Model) updateTable() {
|
||||
m.colOffset = 0
|
||||
|
||||
m.frameTitle.SetTitle("Table: " + m.resultSet.TableInfo.Name)
|
||||
m.rebuildTable()
|
||||
}
|
||||
|
||||
func (m *Model) rebuildTable() {
|
||||
resultSet := m.resultSet
|
||||
|
||||
m.frameTitle.SetTitle("Table: " + resultSet.TableInfo.Name)
|
||||
|
||||
newTbl := table.New(resultSet.Columns, m.w, m.h-m.frameTitle.HeaderHeight())
|
||||
newTbl := table.New(columnModel{m}, m.w, m.h-m.frameTitle.HeaderHeight())
|
||||
newRows := make([]table.Row, 0)
|
||||
for i, r := range resultSet.Items() {
|
||||
if resultSet.Hidden(i) {
|
||||
continue
|
||||
}
|
||||
|
||||
newRows = append(newRows, itemTableRow{resultSet: resultSet, itemIndex: i, item: r})
|
||||
newRows = append(newRows, itemTableRow{
|
||||
model: m,
|
||||
resultSet: resultSet,
|
||||
itemIndex: i,
|
||||
item: r,
|
||||
})
|
||||
}
|
||||
|
||||
m.rows = newRows
|
||||
newTbl.SetRows(newRows)
|
||||
|
||||
m.table = newTbl
|
||||
}
|
||||
|
||||
|
@ -135,5 +204,6 @@ func (m *Model) postSelectedItemChanged() tea.Msg {
|
|||
}
|
||||
|
||||
func (m *Model) Refresh() {
|
||||
|
||||
m.table.SetRows(m.rows)
|
||||
}
|
||||
|
|
|
@ -6,17 +6,24 @@ import (
|
|||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
table "github.com/calyptia/go-bubble-table"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/models"
|
||||
table "github.com/lmika/go-bubble-table"
|
||||
)
|
||||
|
||||
var (
|
||||
markedRowStyle = lipgloss.NewStyle().
|
||||
Background(lipgloss.Color("#e1e1e1"))
|
||||
Background(lipgloss.AdaptiveColor{Light: "#e1e1e1", Dark: "#414141"})
|
||||
dirtyRowStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#e13131"))
|
||||
newRowStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#31e131"))
|
||||
|
||||
metaInfoStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
)
|
||||
|
||||
type itemTableRow struct {
|
||||
model *Model
|
||||
resultSet *models.ResultSet
|
||||
itemIndex int
|
||||
item models.Item
|
||||
|
@ -24,33 +31,39 @@ type itemTableRow struct {
|
|||
|
||||
func (mtr itemTableRow) Render(w io.Writer, model table.Model, index int) {
|
||||
isMarked := mtr.resultSet.Marked(mtr.itemIndex)
|
||||
isDirty := mtr.resultSet.IsDirty(mtr.itemIndex)
|
||||
isNew := mtr.resultSet.IsNew(mtr.itemIndex)
|
||||
|
||||
var style lipgloss.Style
|
||||
|
||||
if index == model.Cursor() {
|
||||
style = model.Styles.SelectedRow
|
||||
}
|
||||
if isMarked {
|
||||
style = style.Copy().Inherit(markedRowStyle)
|
||||
}
|
||||
if isNew {
|
||||
style = style.Copy().Inherit(newRowStyle)
|
||||
} else if isDirty {
|
||||
style = style.Copy().Inherit(dirtyRowStyle)
|
||||
}
|
||||
metaInfoStyle := style.Copy().Inherit(metaInfoStyle)
|
||||
|
||||
sb := strings.Builder{}
|
||||
for i, colName := range mtr.resultSet.Columns {
|
||||
for i, colName := range mtr.resultSet.Columns()[mtr.model.colOffset:] {
|
||||
if i > 0 {
|
||||
sb.WriteString("\t")
|
||||
sb.WriteString(style.Render("\t"))
|
||||
}
|
||||
|
||||
switch colVal := mtr.item[colName].(type) {
|
||||
case nil:
|
||||
sb.WriteString("(nil)")
|
||||
case *types.AttributeValueMemberS:
|
||||
sb.WriteString(colVal.Value)
|
||||
case *types.AttributeValueMemberN:
|
||||
sb.WriteString(colVal.Value)
|
||||
default:
|
||||
sb.WriteString("(other)")
|
||||
if r := mtr.item.Renderer(colName); r != nil {
|
||||
sb.WriteString(style.Render(r.StringValue()))
|
||||
if mi := r.MetaInfo(); mi != "" {
|
||||
sb.WriteString(metaInfoStyle.Render(mi))
|
||||
}
|
||||
} else {
|
||||
sb.WriteString(metaInfoStyle.Render("~"))
|
||||
}
|
||||
}
|
||||
if index == model.Cursor() {
|
||||
style := model.Styles.SelectedRow
|
||||
if isMarked {
|
||||
style = style.Copy().Inherit(markedRowStyle)
|
||||
}
|
||||
fmt.Fprintln(w, style.Render(sb.String()))
|
||||
} else if isMarked {
|
||||
fmt.Fprintln(w, markedRowStyle.Render(sb.String()))
|
||||
} else {
|
||||
fmt.Fprintln(w, sb.String())
|
||||
}
|
||||
|
||||
fmt.Fprintln(w, sb.String())
|
||||
}
|
||||
|
|
|
@ -15,14 +15,19 @@ var (
|
|||
|
||||
// Frame is a frame that appears in the
|
||||
type FrameTitle struct {
|
||||
header string
|
||||
active bool
|
||||
activeStyle lipgloss.Style
|
||||
width int
|
||||
header string
|
||||
active bool
|
||||
style Style
|
||||
width int
|
||||
}
|
||||
|
||||
func NewFrameTitle(header string, active bool, activeStyle lipgloss.Style) FrameTitle {
|
||||
return FrameTitle{header, active, activeStyle, 0}
|
||||
type Style struct {
|
||||
ActiveTitle lipgloss.Style
|
||||
InactiveTitle lipgloss.Style
|
||||
}
|
||||
|
||||
func NewFrameTitle(header string, active bool, style Style) FrameTitle {
|
||||
return FrameTitle{header, active, style, 0}
|
||||
}
|
||||
|
||||
func (f *FrameTitle) SetTitle(title string) {
|
||||
|
@ -42,9 +47,9 @@ func (f FrameTitle) HeaderHeight() int {
|
|||
}
|
||||
|
||||
func (f FrameTitle) headerView() string {
|
||||
style := inactiveHeaderStyle
|
||||
style := f.style.InactiveTitle
|
||||
if f.active {
|
||||
style = f.activeStyle
|
||||
style = f.style.ActiveTitle
|
||||
}
|
||||
|
||||
titleText := f.header
|
||||
|
|
37
internal/dynamo-browse/ui/teamodels/itemdisplay/model.go
Normal file
37
internal/dynamo-browse/ui/teamodels/itemdisplay/model.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package itemdisplay
|
||||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/layout"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/utils"
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
baseMode tea.Model
|
||||
}
|
||||
|
||||
func New(baseMode tea.Model) *Model {
|
||||
return &Model{
|
||||
baseMode: baseMode,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cc utils.CmdCollector
|
||||
|
||||
m.baseMode = cc.Collect(m.baseMode.Update(msg))
|
||||
return m, cc.Cmd()
|
||||
}
|
||||
|
||||
func (m *Model) View() string {
|
||||
return m.baseMode.View()
|
||||
}
|
||||
|
||||
func (m *Model) Resize(w, h int) layout.ResizingModel {
|
||||
m.baseMode = layout.Resize(m.baseMode, w, h)
|
||||
return m
|
||||
}
|
85
internal/dynamo-browse/ui/teamodels/layout/composit.go
Normal file
85
internal/dynamo-browse/ui/teamodels/layout/composit.go
Normal file
|
@ -0,0 +1,85 @@
|
|||
package layout
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Compositor struct {
|
||||
background ResizingModel
|
||||
|
||||
foreground ResizingModel
|
||||
foreX, foreY int
|
||||
foreW, foreH int
|
||||
}
|
||||
|
||||
func NewCompositor(background ResizingModel) *Compositor {
|
||||
return &Compositor{
|
||||
background: background,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Compositor) Init() tea.Cmd {
|
||||
return c.background.Init()
|
||||
}
|
||||
|
||||
func (c *Compositor) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
// TODO: allow the compositor the
|
||||
newM, cmd := c.background.Update(msg)
|
||||
c.background = newM.(ResizingModel)
|
||||
return c, cmd
|
||||
}
|
||||
|
||||
func (c *Compositor) SetOverlay(m ResizingModel, x, y, w, h int) {
|
||||
c.foreground = m
|
||||
c.foreX, c.foreY = x, y
|
||||
c.foreW, c.foreH = w, h
|
||||
}
|
||||
|
||||
func (c *Compositor) View() string {
|
||||
if c.foreground == nil {
|
||||
return c.background.View()
|
||||
}
|
||||
|
||||
// Need to compose
|
||||
backgroundView := c.background.View()
|
||||
foregroundViewLines := strings.Split(c.foreground.View(), "\n")
|
||||
|
||||
backgroundScanner := bufio.NewScanner(strings.NewReader(backgroundView))
|
||||
compositeOutput := new(strings.Builder)
|
||||
|
||||
r := 0
|
||||
for backgroundScanner.Scan() {
|
||||
if r > 0 {
|
||||
compositeOutput.WriteRune('\n')
|
||||
}
|
||||
|
||||
line := backgroundScanner.Text()
|
||||
if r >= c.foreY && r < c.foreY+c.foreH {
|
||||
compositeOutput.WriteString(line[:c.foreX])
|
||||
|
||||
foregroundScanPos := r - c.foreY
|
||||
if foregroundScanPos < len(foregroundViewLines) {
|
||||
displayLine := foregroundViewLines[foregroundScanPos]
|
||||
compositeOutput.WriteString(lipgloss.PlaceHorizontal(c.foreW, lipgloss.Left, displayLine, lipgloss.WithWhitespaceChars(" ")))
|
||||
}
|
||||
|
||||
compositeOutput.WriteString(line[c.foreX+c.foreW:])
|
||||
} else {
|
||||
compositeOutput.WriteString(line)
|
||||
}
|
||||
r++
|
||||
}
|
||||
|
||||
return compositeOutput.String()
|
||||
}
|
||||
|
||||
func (c *Compositor) Resize(w, h int) ResizingModel {
|
||||
c.background = c.background.Resize(w, h)
|
||||
if c.foreground != nil {
|
||||
c.foreground = c.foreground.Resize(c.foreW, c.foreH)
|
||||
}
|
||||
return c
|
||||
}
|
|
@ -12,15 +12,21 @@ import (
|
|||
// event is received, focus will be torn away and the user will be given a prompt the enter text.
|
||||
type StatusAndPrompt struct {
|
||||
model layout.ResizingModel
|
||||
style Style
|
||||
modeLine string
|
||||
statusMessage string
|
||||
pendingInput *events.PromptForInputMsg
|
||||
textInput textinput.Model
|
||||
width int
|
||||
}
|
||||
|
||||
func New(model layout.ResizingModel, initialMsg string) *StatusAndPrompt {
|
||||
type Style struct {
|
||||
ModeLine lipgloss.Style
|
||||
}
|
||||
|
||||
func New(model layout.ResizingModel, initialMsg string, style Style) *StatusAndPrompt {
|
||||
textInput := textinput.New()
|
||||
return &StatusAndPrompt{model: model, statusMessage: initialMsg, textInput: textInput}
|
||||
return &StatusAndPrompt{model: model, style: style, statusMessage: initialMsg, modeLine: "", textInput: textInput}
|
||||
}
|
||||
|
||||
func (s *StatusAndPrompt) Init() tea.Cmd {
|
||||
|
@ -33,7 +39,12 @@ func (s *StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
s.statusMessage = "Error: " + msg.Error()
|
||||
case events.StatusMsg:
|
||||
s.statusMessage = string(msg)
|
||||
case events.ModeMessage:
|
||||
s.modeLine = string(msg)
|
||||
case events.MessageWithStatus:
|
||||
if hasModeMessage, ok := msg.(events.MessageWithMode); ok {
|
||||
s.modeLine = hasModeMessage.ModeMessage()
|
||||
}
|
||||
s.statusMessage = msg.StatusMessage()
|
||||
case events.PromptForInputMsg:
|
||||
if s.pendingInput != nil {
|
||||
|
@ -61,6 +72,8 @@ func (s *StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
s.textInput = newTextInput
|
||||
return s, cmd
|
||||
}
|
||||
} else {
|
||||
s.statusMessage = ""
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,8 +98,14 @@ func (s *StatusAndPrompt) Resize(w, h int) layout.ResizingModel {
|
|||
}
|
||||
|
||||
func (s *StatusAndPrompt) viewStatus() string {
|
||||
modeLine := s.style.ModeLine.Render(lipgloss.PlaceHorizontal(s.width, lipgloss.Left, s.modeLine, lipgloss.WithWhitespaceChars(" ")))
|
||||
|
||||
var statusLine string
|
||||
if s.pendingInput != nil {
|
||||
return s.textInput.View()
|
||||
statusLine = s.textInput.View()
|
||||
} else {
|
||||
statusLine = s.statusMessage
|
||||
}
|
||||
return s.statusMessage
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Top, modeLine, statusLine)
|
||||
}
|
||||
|
|
29
internal/dynamo-browse/ui/teamodels/styles/styles.go
Normal file
29
internal/dynamo-browse/ui/teamodels/styles/styles.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package styles
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/frame"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/statusandprompt"
|
||||
)
|
||||
|
||||
type Styles struct {
|
||||
Frames frame.Style
|
||||
StatusAndPrompt statusandprompt.Style
|
||||
}
|
||||
|
||||
var DefaultStyles = Styles{
|
||||
Frames: frame.Style{
|
||||
ActiveTitle: lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#ffffff")).
|
||||
Background(lipgloss.Color("#4479ff")),
|
||||
InactiveTitle: lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#000000")).
|
||||
Background(lipgloss.Color("#d1d1d1")),
|
||||
},
|
||||
StatusAndPrompt: statusandprompt.Style{
|
||||
ModeLine: lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#000000")).
|
||||
Background(lipgloss.Color("#d1d1d1")),
|
||||
},
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package tableselect
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
@ -32,6 +33,22 @@ func newListController(tableNames []string, w, h int) listController {
|
|||
Padding(0, 0, 0, 1)
|
||||
|
||||
list := list.New(items, delegate, w, h)
|
||||
list.KeyMap.CursorUp = key.NewBinding(
|
||||
key.WithKeys("up", "i"),
|
||||
key.WithHelp("↑/i", "up"),
|
||||
)
|
||||
list.KeyMap.CursorDown = key.NewBinding(
|
||||
key.WithKeys("down", "k"),
|
||||
key.WithHelp("↓/k", "down"),
|
||||
)
|
||||
list.KeyMap.PrevPage = key.NewBinding(
|
||||
key.WithKeys("left", "j", "pgup", "b", "u"),
|
||||
key.WithHelp("←/j/pgup", "prev page"),
|
||||
)
|
||||
list.KeyMap.NextPage = key.NewBinding(
|
||||
key.WithKeys("right", "l", "pgdown", "f", "d"),
|
||||
key.WithHelp("→/l/pgdn", "next page"),
|
||||
)
|
||||
list.SetShowTitle(false)
|
||||
|
||||
return listController{list: list}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/lmika/awstools/internal/dynamo-browse/controllers"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/frame"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/layout"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/styles"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/utils"
|
||||
)
|
||||
|
||||
|
@ -26,8 +27,8 @@ type Model struct {
|
|||
w, h int
|
||||
}
|
||||
|
||||
func New(submodel tea.Model) *Model {
|
||||
frameTitle := frame.NewFrameTitle("Select table", false, activeHeaderStyle)
|
||||
func New(submodel tea.Model, uiStyles styles.Styles) *Model {
|
||||
frameTitle := frame.NewFrameTitle("Select table", false, uiStyles.Frames)
|
||||
return &Model{frameTitle: frameTitle, submodel: submodel}
|
||||
}
|
||||
|
||||
|
|
29
internal/slog-view/styles/styles.go
Normal file
29
internal/slog-view/styles/styles.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package styles
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/frame"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/statusandprompt"
|
||||
)
|
||||
|
||||
type Styles struct {
|
||||
Frames frame.Style
|
||||
StatusAndPrompt statusandprompt.Style
|
||||
}
|
||||
|
||||
var DefaultStyles = Styles{
|
||||
Frames: frame.Style{
|
||||
ActiveTitle: lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#000000")).
|
||||
Background(lipgloss.Color("#d1d1d1")),
|
||||
InactiveTitle: lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#000000")).
|
||||
Background(lipgloss.Color("#d1d1d1")),
|
||||
},
|
||||
StatusAndPrompt: statusandprompt.Style{
|
||||
ModeLine: lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#000000")).
|
||||
Background(lipgloss.Color("#d1d1d1")),
|
||||
},
|
||||
}
|
|
@ -2,22 +2,23 @@ package fullviewlinedetails
|
|||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/frame"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/layout"
|
||||
"github.com/lmika/awstools/internal/slog-view/models"
|
||||
"github.com/lmika/awstools/internal/slog-view/ui/linedetails"
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
submodel tea.Model
|
||||
submodel tea.Model
|
||||
lineDetails *linedetails.Model
|
||||
|
||||
visible bool
|
||||
}
|
||||
|
||||
func NewModel(submodel tea.Model) *Model {
|
||||
func NewModel(submodel tea.Model, style frame.Style) *Model {
|
||||
return &Model{
|
||||
submodel: submodel,
|
||||
lineDetails: linedetails.New(),
|
||||
submodel: submodel,
|
||||
lineDetails: linedetails.New(style),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,6 +50,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
func (m *Model) ViewItem(item *models.LogLine) {
|
||||
m.visible = true
|
||||
m.lineDetails.SetSelectedItem(item)
|
||||
m.lineDetails.SetFocused(true)
|
||||
}
|
||||
|
||||
func (m *Model) View() string {
|
||||
|
|
|
@ -10,13 +10,6 @@ import (
|
|||
"github.com/lmika/awstools/internal/slog-view/models"
|
||||
)
|
||||
|
||||
var (
|
||||
activeHeaderStyle = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#ffffff")).
|
||||
Background(lipgloss.Color("#9c9c9c"))
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
frameTitle frame.FrameTitle
|
||||
viewport viewport.Model
|
||||
|
@ -27,11 +20,11 @@ type Model struct {
|
|||
selectedItem *models.LogLine
|
||||
}
|
||||
|
||||
func New() *Model {
|
||||
func New(style frame.Style) *Model {
|
||||
viewport := viewport.New(0, 0)
|
||||
viewport.SetContent("")
|
||||
return &Model{
|
||||
frameTitle: frame.NewFrameTitle("Item", false, activeHeaderStyle),
|
||||
frameTitle: frame.NewFrameTitle("Item", false, style),
|
||||
viewport: viewport,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,15 @@
|
|||
package loglines
|
||||
|
||||
import (
|
||||
table "github.com/calyptia/go-bubble-table"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/frame"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/layout"
|
||||
"github.com/lmika/awstools/internal/slog-view/models"
|
||||
table "github.com/lmika/go-bubble-table"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var (
|
||||
activeHeaderStyle = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#ffffff")).
|
||||
Background(lipgloss.Color("#9c9c9c"))
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
frameTitle frame.FrameTitle
|
||||
table table.Model
|
||||
|
@ -26,9 +19,9 @@ type Model struct {
|
|||
w, h int
|
||||
}
|
||||
|
||||
func New() *Model {
|
||||
frameTitle := frame.NewFrameTitle("File: ", true, activeHeaderStyle)
|
||||
table := table.New([]string{"level", "error", "message"}, 0, 0)
|
||||
func New(style frame.Style) *Model {
|
||||
frameTitle := frame.NewFrameTitle("File: ", true, style)
|
||||
table := table.New(table.SimpleColumns{"level", "error", "message"}, 0, 0)
|
||||
|
||||
return &Model{
|
||||
frameTitle: frameTitle,
|
||||
|
@ -40,7 +33,7 @@ func (m *Model) SetLogFile(newLogFile *models.LogFile) {
|
|||
m.logFile = newLogFile
|
||||
m.frameTitle.SetTitle("File: " + filepath.Base(newLogFile.Filename))
|
||||
|
||||
cols := []string{"level", "error", "message"}
|
||||
cols := table.SimpleColumns{"level", "error", "message"}
|
||||
|
||||
newTbl := table.New(cols, m.w, m.h-m.frameTitle.HeaderHeight())
|
||||
newRows := make([]table.Row, len(newLogFile.Lines))
|
||||
|
|
|
@ -2,7 +2,7 @@ package loglines
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
table "github.com/calyptia/go-bubble-table"
|
||||
table "github.com/lmika/go-bubble-table"
|
||||
"github.com/lmika/awstools/internal/slog-view/models"
|
||||
"io"
|
||||
"strings"
|
||||
|
|
|
@ -6,38 +6,40 @@ import (
|
|||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/layout"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/statusandprompt"
|
||||
"github.com/lmika/awstools/internal/slog-view/controllers"
|
||||
"github.com/lmika/awstools/internal/slog-view/styles"
|
||||
"github.com/lmika/awstools/internal/slog-view/ui/fullviewlinedetails"
|
||||
"github.com/lmika/awstools/internal/slog-view/ui/linedetails"
|
||||
"github.com/lmika/awstools/internal/slog-view/ui/loglines"
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
controller *controllers.LogFileController
|
||||
cmdController *commandctrl.CommandController
|
||||
controller *controllers.LogFileController
|
||||
cmdController *commandctrl.CommandController
|
||||
|
||||
root tea.Model
|
||||
logLines *loglines.Model
|
||||
lineDetails *linedetails.Model
|
||||
statusAndPrompt *statusandprompt.StatusAndPrompt
|
||||
root tea.Model
|
||||
logLines *loglines.Model
|
||||
lineDetails *linedetails.Model
|
||||
statusAndPrompt *statusandprompt.StatusAndPrompt
|
||||
fullViewLineDetails *fullviewlinedetails.Model
|
||||
}
|
||||
|
||||
func NewModel(controller *controllers.LogFileController, cmdController *commandctrl.CommandController) Model {
|
||||
logLines := loglines.New()
|
||||
lineDetails := linedetails.New()
|
||||
defaultStyles := styles.DefaultStyles
|
||||
logLines := loglines.New(defaultStyles.Frames)
|
||||
lineDetails := linedetails.New(defaultStyles.Frames)
|
||||
box := layout.NewVBox(layout.LastChildFixedAt(17), logLines, lineDetails)
|
||||
fullViewLineDetails := fullviewlinedetails.NewModel(box)
|
||||
statusAndPrompt := statusandprompt.New(fullViewLineDetails, "")
|
||||
fullViewLineDetails := fullviewlinedetails.NewModel(box, defaultStyles.Frames)
|
||||
statusAndPrompt := statusandprompt.New(fullViewLineDetails, "", defaultStyles.StatusAndPrompt)
|
||||
|
||||
root := layout.FullScreen(statusAndPrompt)
|
||||
|
||||
return Model{
|
||||
controller: controller,
|
||||
cmdController: cmdController,
|
||||
root: root,
|
||||
statusAndPrompt: statusAndPrompt,
|
||||
logLines: logLines,
|
||||
lineDetails: lineDetails,
|
||||
controller: controller,
|
||||
cmdController: cmdController,
|
||||
root: root,
|
||||
statusAndPrompt: statusAndPrompt,
|
||||
logLines: logLines,
|
||||
lineDetails: lineDetails,
|
||||
fullViewLineDetails: fullViewLineDetails,
|
||||
}
|
||||
}
|
||||
|
|
29
internal/sqs-browse/styles/styles.go
Normal file
29
internal/sqs-browse/styles/styles.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package styles
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/frame"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/statusandprompt"
|
||||
)
|
||||
|
||||
type Styles struct {
|
||||
Frames frame.Style
|
||||
StatusAndPrompt statusandprompt.Style
|
||||
}
|
||||
|
||||
var DefaultStyles = Styles{
|
||||
Frames: frame.Style{
|
||||
ActiveTitle: lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#ffffff")).
|
||||
Background(lipgloss.Color("#4479ff")),
|
||||
InactiveTitle: lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#000000")).
|
||||
Background(lipgloss.Color("#d1d1d1")),
|
||||
},
|
||||
StatusAndPrompt: statusandprompt.Style{
|
||||
ModeLine: lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#000000")).
|
||||
Background(lipgloss.Color("#d1d1d1")),
|
||||
},
|
||||
}
|
|
@ -7,7 +7,6 @@ import (
|
|||
"log"
|
||||
"strings"
|
||||
|
||||
table "github.com/calyptia/go-bubble-table"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
@ -16,6 +15,7 @@ import (
|
|||
"github.com/lmika/awstools/internal/common/ui/events"
|
||||
"github.com/lmika/awstools/internal/sqs-browse/controllers"
|
||||
"github.com/lmika/awstools/internal/sqs-browse/models"
|
||||
table "github.com/lmika/go-bubble-table"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -45,7 +45,7 @@ type uiModel struct {
|
|||
}
|
||||
|
||||
func NewModel(dispatcher *dispatcher.Dispatcher, msgSendingHandlers *controllers.MessageSendingController) tea.Model {
|
||||
tbl := table.New([]string{"seq", "message"}, 100, 20)
|
||||
tbl := table.New(table.SimpleColumns{"seq", "message"}, 100, 20)
|
||||
rows := make([]table.Row, 0)
|
||||
tbl.SetRows(rows)
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"io"
|
||||
"strings"
|
||||
|
||||
table "github.com/calyptia/go-bubble-table"
|
||||
table "github.com/lmika/go-bubble-table"
|
||||
"github.com/lmika/awstools/internal/sqs-browse/models"
|
||||
)
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/awstools/internal/common/ui/events"
|
||||
"github.com/lmika/awstools/internal/ssm-browse/models"
|
||||
"github.com/lmika/awstools/internal/ssm-browse/services/ssmparameters"
|
||||
"sync"
|
||||
)
|
||||
|
@ -12,15 +13,15 @@ type SSMController struct {
|
|||
service *ssmparameters.Service
|
||||
|
||||
// state
|
||||
mutex *sync.Mutex
|
||||
mutex *sync.Mutex
|
||||
prefix string
|
||||
}
|
||||
|
||||
func New(service *ssmparameters.Service) *SSMController {
|
||||
return &SSMController{
|
||||
service: service,
|
||||
prefix: "/",
|
||||
mutex: new(sync.Mutex),
|
||||
prefix: "/",
|
||||
mutex: new(sync.Mutex),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,7 +33,7 @@ func (c *SSMController) Fetch() tea.Cmd {
|
|||
}
|
||||
|
||||
return NewParameterListMsg{
|
||||
Prefix: c.prefix,
|
||||
Prefix: c.prefix,
|
||||
Parameters: res,
|
||||
}
|
||||
}
|
||||
|
@ -50,8 +51,50 @@ func (c *SSMController) ChangePrefix(newPrefix string) tea.Cmd {
|
|||
c.prefix = newPrefix
|
||||
|
||||
return NewParameterListMsg{
|
||||
Prefix: c.prefix,
|
||||
Prefix: c.prefix,
|
||||
Parameters: res,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *SSMController) Clone(param models.SSMParameter) tea.Cmd {
|
||||
return events.PromptForInput("New key: ", func(value string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
ctx := context.Background()
|
||||
if err := c.service.Clone(ctx, param, value); err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
|
||||
res, err := c.service.List(context.Background(), c.prefix)
|
||||
if err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
|
||||
return NewParameterListMsg{
|
||||
Prefix: c.prefix,
|
||||
Parameters: res,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (c *SSMController) DeleteParameter(param models.SSMParameter) tea.Cmd {
|
||||
return events.Confirm("delete parameter? ", func() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
ctx := context.Background()
|
||||
if err := c.service.Delete(ctx, param); err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
|
||||
res, err := c.service.List(context.Background(), c.prefix)
|
||||
if err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
|
||||
return NewParameterListMsg{
|
||||
Prefix: c.prefix,
|
||||
Parameters: res,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
package models
|
||||
|
||||
import "github.com/aws/aws-sdk-go-v2/service/ssm/types"
|
||||
|
||||
type SSMParameters struct {
|
||||
Items []SSMParameter
|
||||
}
|
||||
|
||||
type SSMParameter struct {
|
||||
Name string
|
||||
Type types.ParameterType
|
||||
Value string
|
||||
}
|
||||
|
|
|
@ -4,11 +4,14 @@ import (
|
|||
"context"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ssm"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ssm/types"
|
||||
"github.com/lmika/awstools/internal/ssm-browse/models"
|
||||
"github.com/pkg/errors"
|
||||
"log"
|
||||
)
|
||||
|
||||
const defaultKMSKeyIDForSecureStrings = "alias/aws/ssm"
|
||||
|
||||
type Provider struct {
|
||||
client *ssm.Client
|
||||
}
|
||||
|
@ -23,13 +26,14 @@ func (p *Provider) List(ctx context.Context, prefix string, maxCount int) (*mode
|
|||
log.Printf("new prefix: %v", prefix)
|
||||
|
||||
pager := ssm.NewGetParametersByPathPaginator(p.client, &ssm.GetParametersByPathInput{
|
||||
Path: aws.String(prefix),
|
||||
Recursive: true,
|
||||
Path: aws.String(prefix),
|
||||
Recursive: true,
|
||||
WithDecryption: true,
|
||||
})
|
||||
|
||||
items := make([]models.SSMParameter, 0)
|
||||
outer: for pager.HasMorePages() {
|
||||
outer:
|
||||
for pager.HasMorePages() {
|
||||
out, err := pager.NextPage(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot get parameters from path")
|
||||
|
@ -38,6 +42,7 @@ func (p *Provider) List(ctx context.Context, prefix string, maxCount int) (*mode
|
|||
for _, p := range out.Parameters {
|
||||
items = append(items, models.SSMParameter{
|
||||
Name: aws.ToString(p.Name),
|
||||
Type: p.Type,
|
||||
Value: aws.ToString(p.Value),
|
||||
})
|
||||
if len(items) >= maxCount {
|
||||
|
@ -48,3 +53,32 @@ func (p *Provider) List(ctx context.Context, prefix string, maxCount int) (*mode
|
|||
|
||||
return &models.SSMParameters{Items: items}, nil
|
||||
}
|
||||
|
||||
func (p *Provider) Put(ctx context.Context, param models.SSMParameter, override bool) error {
|
||||
in := &ssm.PutParameterInput{
|
||||
Name: aws.String(param.Name),
|
||||
Type: param.Type,
|
||||
Value: aws.String(param.Value),
|
||||
Overwrite: override,
|
||||
}
|
||||
if param.Type == types.ParameterTypeSecureString {
|
||||
in.KeyId = aws.String(defaultKMSKeyIDForSecureStrings)
|
||||
}
|
||||
|
||||
_, err := p.client.PutParameter(ctx, in)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to put new SSM parameter")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) Delete(ctx context.Context, param models.SSMParameter) error {
|
||||
_, err := p.client.DeleteParameter(ctx, &ssm.DeleteParameterInput{
|
||||
Name: aws.String(param.Name),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to delete SSM parameter")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -7,4 +7,6 @@ import (
|
|||
|
||||
type SSMProvider interface {
|
||||
List(ctx context.Context, prefix string, maxCount int) (*models.SSMParameters, error)
|
||||
Put(ctx context.Context, param models.SSMParameter, override bool) error
|
||||
Delete(ctx context.Context, param models.SSMParameter) error
|
||||
}
|
||||
|
|
|
@ -17,4 +17,17 @@ func NewService(provider SSMProvider) *Service {
|
|||
|
||||
func (s *Service) List(ctx context.Context, prefix string) (*models.SSMParameters, error) {
|
||||
return s.provider.List(ctx, prefix, 100)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Clone(ctx context.Context, param models.SSMParameter, newName string) error {
|
||||
newParam := models.SSMParameter{
|
||||
Name: newName,
|
||||
Type: param.Type,
|
||||
Value: param.Value,
|
||||
}
|
||||
return s.provider.Put(ctx, newParam, false)
|
||||
}
|
||||
|
||||
func (s *Service) Delete(ctx context.Context, param models.SSMParameter) error {
|
||||
return s.provider.Delete(ctx, param)
|
||||
}
|
||||
|
|
29
internal/ssm-browse/styles/styles.go
Normal file
29
internal/ssm-browse/styles/styles.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package styles
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/frame"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/statusandprompt"
|
||||
)
|
||||
|
||||
type Styles struct {
|
||||
Frames frame.Style
|
||||
StatusAndPrompt statusandprompt.Style
|
||||
}
|
||||
|
||||
var DefaultStyles = Styles{
|
||||
Frames: frame.Style{
|
||||
ActiveTitle: lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#ffffff")).
|
||||
Background(lipgloss.Color("#c144ff")),
|
||||
InactiveTitle: lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#000000")).
|
||||
Background(lipgloss.Color("#d1d1d1")),
|
||||
},
|
||||
StatusAndPrompt: statusandprompt.Style{
|
||||
ModeLine: lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#000000")).
|
||||
Background(lipgloss.Color("#d1d1d1")),
|
||||
},
|
||||
}
|
|
@ -3,11 +3,14 @@ package ui
|
|||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/awstools/internal/common/ui/commandctrl"
|
||||
"github.com/lmika/awstools/internal/common/ui/events"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/layout"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/statusandprompt"
|
||||
"github.com/lmika/awstools/internal/ssm-browse/controllers"
|
||||
"github.com/lmika/awstools/internal/ssm-browse/styles"
|
||||
"github.com/lmika/awstools/internal/ssm-browse/ui/ssmdetails"
|
||||
"github.com/lmika/awstools/internal/ssm-browse/ui/ssmlist"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
|
@ -21,11 +24,28 @@ type Model struct {
|
|||
}
|
||||
|
||||
func NewModel(controller *controllers.SSMController, cmdController *commandctrl.CommandController) Model {
|
||||
ssmList := ssmlist.New()
|
||||
ssmdDetails := ssmdetails.New()
|
||||
defaultStyles := styles.DefaultStyles
|
||||
ssmList := ssmlist.New(defaultStyles.Frames)
|
||||
ssmdDetails := ssmdetails.New(defaultStyles.Frames)
|
||||
statusAndPrompt := statusandprompt.New(
|
||||
layout.NewVBox(layout.LastChildFixedAt(17), ssmList, ssmdDetails),
|
||||
"")
|
||||
layout.NewVBox(layout.LastChildFixedAt(17), ssmList, ssmdDetails), "", defaultStyles.StatusAndPrompt)
|
||||
|
||||
cmdController.AddCommands(&commandctrl.CommandContext{
|
||||
Commands: map[string]commandctrl.Command{
|
||||
"clone": func(args []string) tea.Cmd {
|
||||
if currentParam := ssmList.CurrentParameter(); currentParam != nil {
|
||||
return controller.Clone(*currentParam)
|
||||
}
|
||||
return events.SetError(errors.New("no parameter selected"))
|
||||
},
|
||||
"delete": func(args []string) tea.Cmd {
|
||||
if currentParam := ssmList.CurrentParameter(); currentParam != nil {
|
||||
return controller.DeleteParameter(*currentParam)
|
||||
}
|
||||
return events.SetError(errors.New("no parameter selected"))
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
root := layout.FullScreen(statusAndPrompt)
|
||||
|
||||
|
|
|
@ -11,13 +11,6 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
activeHeaderStyle = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#ffffff")).
|
||||
Background(lipgloss.Color("#c144ff"))
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
frameTitle frame.FrameTitle
|
||||
viewport viewport.Model
|
||||
|
@ -28,11 +21,11 @@ type Model struct {
|
|||
selectedItem *models.SSMParameter
|
||||
}
|
||||
|
||||
func New() *Model {
|
||||
func New(style frame.Style) *Model {
|
||||
viewport := viewport.New(0, 0)
|
||||
viewport.SetContent("")
|
||||
return &Model{
|
||||
frameTitle: frame.NewFrameTitle("Item", false, activeHeaderStyle),
|
||||
frameTitle: frame.NewFrameTitle("Item", false, style),
|
||||
viewport: viewport,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,12 @@
|
|||
package ssmlist
|
||||
|
||||
import (
|
||||
table "github.com/calyptia/go-bubble-table"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/frame"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/layout"
|
||||
"github.com/lmika/awstools/internal/ssm-browse/models"
|
||||
)
|
||||
|
||||
var (
|
||||
activeHeaderStyle = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#ffffff")).
|
||||
Background(lipgloss.Color("#c144ff"))
|
||||
table "github.com/lmika/go-bubble-table"
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
|
@ -25,9 +18,9 @@ type Model struct {
|
|||
w, h int
|
||||
}
|
||||
|
||||
func New() *Model {
|
||||
frameTitle := frame.NewFrameTitle("SSM: /", true, activeHeaderStyle)
|
||||
table := table.New([]string{"name", "type", "value"}, 0, 0)
|
||||
func New(style frame.Style) *Model {
|
||||
frameTitle := frame.NewFrameTitle("SSM: /", true, style)
|
||||
table := table.New(table.SimpleColumns{"name", "type", "value"}, 0, 0)
|
||||
|
||||
return &Model{
|
||||
frameTitle: frameTitle,
|
||||
|
@ -41,7 +34,7 @@ func (m *Model) SetPrefix(newPrefix string) {
|
|||
|
||||
func (m *Model) SetParameters(parameters *models.SSMParameters) {
|
||||
m.parameters = parameters
|
||||
cols := []string{"name", "type", "value"}
|
||||
cols := table.SimpleColumns{"name", "type", "value"}
|
||||
|
||||
newTbl := table.New(cols, m.w, m.h-m.frameTitle.HeaderHeight())
|
||||
newRows := make([]table.Row, len(parameters.Items))
|
||||
|
@ -85,6 +78,14 @@ func (m *Model) emitNewSelectedParameter() tea.Cmd {
|
|||
}
|
||||
}
|
||||
|
||||
func (m *Model) CurrentParameter() *models.SSMParameter {
|
||||
if row, ok := m.table.SelectedRow().(itemTableRow); ok {
|
||||
return &(row.item)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Model) View() string {
|
||||
return lipgloss.JoinVertical(lipgloss.Top, m.frameTitle.View(), m.table.View())
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ package ssmlist
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
table "github.com/calyptia/go-bubble-table"
|
||||
table "github.com/lmika/go-bubble-table"
|
||||
"github.com/lmika/awstools/internal/ssm-browse/models"
|
||||
"io"
|
||||
"strings"
|
||||
|
@ -14,7 +14,7 @@ type itemTableRow struct {
|
|||
|
||||
func (mtr itemTableRow) Render(w io.Writer, model table.Model, index int) {
|
||||
firstLine := strings.SplitN(mtr.item.Value, "\n", 2)[0]
|
||||
line := fmt.Sprintf("%s\t%s\t%s", mtr.item.Name, "String", firstLine)
|
||||
line := fmt.Sprintf("%s\t%s\t%s", mtr.item.Name, mtr.item.Type, firstLine)
|
||||
|
||||
if index == model.Cursor() {
|
||||
fmt.Fprintln(w, model.Styles.SelectedRow.Render(line))
|
||||
|
|
|
@ -2,24 +2,24 @@ package main
|
|||
|
||||
import (
|
||||
"context"
|
||||
"github.com/brianvoe/gofakeit/v6"
|
||||
"github.com/google/uuid"
|
||||
"log"
|
||||
|
||||
"fmt"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/brianvoe/gofakeit/v6"
|
||||
"github.com/google/uuid"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/models"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/providers/dynamo"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/services/tables"
|
||||
"github.com/lmika/gopkgs/cli"
|
||||
"log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
tableName := "awstools-test"
|
||||
totalItems := 300
|
||||
tableName := "business-addresses"
|
||||
totalItems := 5000
|
||||
|
||||
cfg, err := config.LoadDefaultConfig(ctx)
|
||||
if err != nil {
|
||||
|
@ -66,13 +66,19 @@ func main() {
|
|||
for i := 0; i < totalItems; i++ {
|
||||
key := uuid.New().String()
|
||||
if err := tableService.Put(ctx, tableInfo, models.Item{
|
||||
"pk": &types.AttributeValueMemberS{Value: key},
|
||||
"sk": &types.AttributeValueMemberS{Value: key},
|
||||
"name": &types.AttributeValueMemberS{Value: gofakeit.Name()},
|
||||
"address": &types.AttributeValueMemberS{Value: gofakeit.Address().Address},
|
||||
"city": &types.AttributeValueMemberS{Value: gofakeit.Address().City},
|
||||
"phone": &types.AttributeValueMemberS{Value: gofakeit.Phone()},
|
||||
"web": &types.AttributeValueMemberS{Value: gofakeit.URL()},
|
||||
"pk": &types.AttributeValueMemberS{Value: key},
|
||||
"sk": &types.AttributeValueMemberS{Value: key},
|
||||
"name": &types.AttributeValueMemberS{Value: gofakeit.Name()},
|
||||
"address": &types.AttributeValueMemberS{Value: gofakeit.Address().Address},
|
||||
"city": &types.AttributeValueMemberS{Value: gofakeit.Address().City},
|
||||
"phone": &types.AttributeValueMemberN{Value: gofakeit.Phone()},
|
||||
"web": &types.AttributeValueMemberS{Value: gofakeit.URL()},
|
||||
"officeOpened": &types.AttributeValueMemberBOOL{Value: gofakeit.Bool()},
|
||||
"ratings": &types.AttributeValueMemberL{Value: []types.AttributeValue{
|
||||
&types.AttributeValueMemberN{Value: fmt.Sprint(gofakeit.IntRange(0, 5))},
|
||||
&types.AttributeValueMemberN{Value: fmt.Sprint(gofakeit.IntRange(0, 5))},
|
||||
&types.AttributeValueMemberN{Value: fmt.Sprint(gofakeit.IntRange(0, 5))},
|
||||
}},
|
||||
}); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
|
|
@ -13,9 +13,12 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type TestData []map[string]interface{}
|
||||
type TestData struct {
|
||||
TableName string
|
||||
Data []map[string]interface{}
|
||||
}
|
||||
|
||||
func SetupTestTable(t *testing.T, tableName string, testData TestData) (*dynamodb.Client, func()) {
|
||||
func SetupTestTable(t *testing.T, testData []TestData) (*dynamodb.Client, func()) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
|
@ -25,39 +28,45 @@ func SetupTestTable(t *testing.T, tableName string, testData TestData) (*dynamod
|
|||
assert.NoError(t, err)
|
||||
|
||||
dynamoClient := dynamodb.NewFromConfig(cfg,
|
||||
dynamodb.WithEndpointResolver(dynamodb.EndpointResolverFromURL("http://localhost:8000")))
|
||||
dynamodb.WithEndpointResolver(dynamodb.EndpointResolverFromURL("http://localhost:18000")))
|
||||
|
||||
_, err = dynamoClient.CreateTable(ctx, &dynamodb.CreateTableInput{
|
||||
TableName: aws.String(tableName),
|
||||
KeySchema: []types.KeySchemaElement{
|
||||
{AttributeName: aws.String("pk"), KeyType: types.KeyTypeHash},
|
||||
{AttributeName: aws.String("sk"), KeyType: types.KeyTypeRange},
|
||||
},
|
||||
AttributeDefinitions: []types.AttributeDefinition{
|
||||
{AttributeName: aws.String("pk"), AttributeType: types.ScalarAttributeTypeS},
|
||||
{AttributeName: aws.String("sk"), AttributeType: types.ScalarAttributeTypeS},
|
||||
},
|
||||
ProvisionedThroughput: &types.ProvisionedThroughput{
|
||||
ReadCapacityUnits: aws.Int64(100),
|
||||
WriteCapacityUnits: aws.Int64(100),
|
||||
},
|
||||
for _, table := range testData {
|
||||
_, err = dynamoClient.CreateTable(ctx, &dynamodb.CreateTableInput{
|
||||
TableName: aws.String(table.TableName),
|
||||
KeySchema: []types.KeySchemaElement{
|
||||
{AttributeName: aws.String("pk"), KeyType: types.KeyTypeHash},
|
||||
{AttributeName: aws.String("sk"), KeyType: types.KeyTypeRange},
|
||||
},
|
||||
AttributeDefinitions: []types.AttributeDefinition{
|
||||
{AttributeName: aws.String("pk"), AttributeType: types.ScalarAttributeTypeS},
|
||||
{AttributeName: aws.String("sk"), AttributeType: types.ScalarAttributeTypeS},
|
||||
},
|
||||
ProvisionedThroughput: &types.ProvisionedThroughput{
|
||||
ReadCapacityUnits: aws.Int64(100),
|
||||
WriteCapacityUnits: aws.Int64(100),
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
for _, item := range table.Data {
|
||||
m, err := attributevalue.MarshalMap(item)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = dynamoClient.PutItem(ctx, &dynamodb.PutItemInput{
|
||||
TableName: aws.String(table.TableName),
|
||||
Item: m,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
for _, table := range testData {
|
||||
dynamoClient.DeleteTable(ctx, &dynamodb.DeleteTableInput{
|
||||
TableName: aws.String(table.TableName),
|
||||
})
|
||||
}
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
for _, item := range testData {
|
||||
m, err := attributevalue.MarshalMap(item)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = dynamoClient.PutItem(ctx, &dynamodb.PutItemInput{
|
||||
TableName: aws.String(tableName),
|
||||
Item: m,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
return dynamoClient, func() {
|
||||
dynamoClient.DeleteTable(ctx, &dynamodb.DeleteTableInput{
|
||||
TableName: aws.String(tableName),
|
||||
})
|
||||
}
|
||||
return dynamoClient, func() {}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue