From 1c3149740aedbfef9ae986cbe31b5d2146216482 Mon Sep 17 00:00:00 2001 From: Nitish Tiwari Date: Thu, 17 Aug 2023 23:10:16 +0530 Subject: [PATCH 1/2] Add initial buildscripts, makefile --- .gitignore | 3 + .golangci.yml | 37 ++ Makefile | 57 +++ buildscripts/cross-compile.sh | 55 +++ buildscripts/gen-ldflags.go | 118 +++++ cmd/client.go | 18 +- cmd/pre.go | 6 +- cmd/profile.go | 469 +++++++++--------- cmd/stream.go | 711 ++++++++++++++-------------- cmd/style.go | 1 - cmd/user.go | 569 +++++++++++----------- cmd/version.go | 2 + main.go | 12 +- pkg/config/config.go | 17 +- pkg/model/button/button.go | 27 +- pkg/model/credential/credential.go | 15 +- pkg/model/datetime/datetime.go | 12 + pkg/model/defaultprofile/profile.go | 18 +- pkg/model/query.go | 49 +- pkg/model/role/role.go | 8 +- pkg/model/selection/selection.go | 14 +- pkg/model/status.go | 1 - pkg/model/timeinput.go | 8 +- pkg/model/timerange.go | 3 +- 24 files changed, 1274 insertions(+), 956 deletions(-) create mode 100644 .golangci.yml create mode 100644 Makefile create mode 100644 buildscripts/cross-compile.sh create mode 100644 buildscripts/gen-ldflags.go diff --git a/.gitignore b/.gitignore index 8ec8f2b..8575996 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ config.toml # build pb + +# OS Files +.DS_Store diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..7f7b24b --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,37 @@ +linters: + disable-all: true + enable: + - typecheck + - goimports + - misspell + - govet + - revive + - ineffassign + - gomodguard + - gofmt + - unused + - gofumpt + +linters-settings: + golint: + min-confidence: 0 + + misspell: + locale: US + + gofumpt: + lang-version: "1.17" + + # Choose whether or not to use the extra rules that are disabled + # by default + extra-rules: false + +issues: + exclude-use-default: false + exclude: + - instead of using struct literal + - should have a package comment + - error strings should not be capitalized or end with punctuation or a newline + +service: + golangci-lint-version: 1.43.0 # use the fixed version to not introduce new linters unexpectedly diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..481b82f --- /dev/null +++ b/Makefile @@ -0,0 +1,57 @@ +PWD := $(shell pwd) +GOPATH := $(shell go env GOPATH) +LDFLAGS := $(shell go run buildscripts/gen-ldflags.go) + +GOARCH := $(shell go env GOARCH) +GOOS := $(shell go env GOOS) + +VERSION ?= $(shell git describe --tags) +TAG ?= "parseablehq/pb:$(VERSION)" + +all: build + +checks: + @echo "Checking dependencies" + @(env bash $(PWD)/buildscripts/checkdeps.sh) + +getdeps: + @mkdir -p ${GOPATH}/bin + @echo "Installing golangci-lint" && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH)/bin + @echo "Installing stringer" && go install -v golang.org/x/tools/cmd/stringer@latest + @echo "Installing staticheck" && go install honnef.co/go/tools/cmd/staticcheck@latest + +crosscompile: + @(env bash $(PWD)/buildscripts/cross-compile.sh) + +verifiers: getdeps vet lint + +docker: build + @docker build -t $(TAG) . -f Dockerfile.dev + +vet: + @echo "Running $@" + @GO111MODULE=on go vet $(PWD)/... + +lint: + @echo "Running $@ check" + @GO111MODULE=on ${GOPATH}/bin/golangci-lint run --timeout=5m --config ./.golangci.yml + @GO111MODULE=on ${GOPATH}/bin/staticcheck -tests=false -checks="all,-ST1000,-ST1003,-ST1016,-ST1020,-ST1021,-ST1022,-ST1023,-ST1005" ./... + +# Builds pb locally. +build: checks + @echo "Building pb binary to './pb'" + @GO111MODULE=on CGO_ENABLED=0 go build -trimpath -tags kqueue --ldflags "$(LDFLAGS)" -o $(PWD)/pb + +# Builds pb and installs it to $GOPATH/bin. +install: build + @echo "Installing pb binary to '$(GOPATH)/bin/pb'" + @mkdir -p $(GOPATH)/bin && cp -f $(PWD)/pb $(GOPATH)/bin/pb + @echo "Installation successful. To learn more, try \"pb --help\"." + +clean: + @echo "Cleaning up all the generated files" + @find . -name '*.test' | xargs rm -fv + @find . -name '*~' | xargs rm -fv + @rm -rvf pb + @rm -rvf build + @rm -rvf release diff --git a/buildscripts/cross-compile.sh b/buildscripts/cross-compile.sh new file mode 100644 index 0000000..e7b7e16 --- /dev/null +++ b/buildscripts/cross-compile.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# +# Copyright (c) 2023 Cloudnatively Services Pvt Ltd +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +set -e + +# Enable tracing if set. +[ -n "$BASH_XTRACEFD" ] && set -x + +function _init() { + ## All binaries are static make sure to disable CGO. + export CGO_ENABLED=0 + + ## List of architectures and OS to test coss compilation. + SUPPORTED_OSARCH="linux/amd64 linux/arm64 darwin/arm64 darwin/amd64 windows/amd64" +} + +function _build() { + local osarch=$1 + IFS=/ read -r -a arr <<<"$osarch" + os="${arr[0]}" + arch="${arr[1]}" + package=$(go list -f '{{.ImportPath}}') + printf -- "--> %15s:%s\n" "${osarch}" "${package}" + + # Go build to build the binary. + export GOOS=$os + export GOARCH=$arch + export GO111MODULE=on + export CGO_ENABLED=0 + go build -tags kqueue -o /dev/null +} + +function main() { + echo "Testing builds for OS/Arch: ${SUPPORTED_OSARCH}" + for each_osarch in ${SUPPORTED_OSARCH}; do + _build "${each_osarch}" + done +} + +_init && main "$@" diff --git a/buildscripts/gen-ldflags.go b/buildscripts/gen-ldflags.go new file mode 100644 index 0000000..9a412be --- /dev/null +++ b/buildscripts/gen-ldflags.go @@ -0,0 +1,118 @@ +//go:build ignore +// +build ignore + +// Copyright (c) 2023 Cloudnatively Services Pvt Ltd +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package main + +import ( + "fmt" + "os" + "os/exec" + "strings" + "time" +) + +func genLDFlags(version string) string { + releaseTag, date := releaseTag(version) + copyrightYear := fmt.Sprintf("%d", date.Year()) + + var ldflagsStr string + ldflagsStr = "-s -w -X github.com/parseablehq/pb/cmd.Version=" + version + " " + ldflagsStr = ldflagsStr + "-X github.com/parseablehq/pb/cmd.CopyrightYear=" + copyrightYear + " " + ldflagsStr = ldflagsStr + "-X github.com/parseablehq/pb/cmd.ReleaseTag=" + releaseTag + " " + ldflagsStr = ldflagsStr + "-X github.com/parseablehq/pb/cmd.CommitID=" + commitID() + " " + ldflagsStr = ldflagsStr + "-X github.com/parseablehq/pb/cmd.ShortCommitID=" + commitID()[:12] + return ldflagsStr +} + +// releaseTag prints release tag to the console for easy git tagging. +func releaseTag(version string) (string, time.Time) { + relPrefix := "DEVELOPMENT" + if prefix := os.Getenv("PB_RELEASE"); prefix != "" { + relPrefix = prefix + } + + relSuffix := "" + if hotfix := os.Getenv("PB_HOTFIX"); hotfix != "" { + relSuffix = hotfix + } + + relTag := strings.ReplaceAll(version, " ", "-") + relTag = strings.ReplaceAll(relTag, ":", "-") + t, err := time.Parse("2006-01-02T15-04-05Z", relTag) + if err != nil { + panic(err) + } + + relTag = strings.ReplaceAll(relTag, ",", "") + relTag = relPrefix + "." + relTag + if relSuffix != "" { + relTag += "." + relSuffix + } + + return relTag, t +} + +// commitID returns the abbreviated commit-id hash of the last commit. +func commitID() string { + // git log --format="%h" -n1 + var ( + commit []byte + e error + ) + cmdName := "git" + cmdArgs := []string{"log", "--format=%H", "-n1"} + if commit, e = exec.Command(cmdName, cmdArgs...).Output(); e != nil { + fmt.Fprintln(os.Stderr, "Error generating git commit-id: ", e) + os.Exit(1) + } + + return strings.TrimSpace(string(commit)) +} + +func commitTime() time.Time { + // git log --format=%cD -n1 + var ( + commitUnix []byte + err error + ) + cmdName := "git" + cmdArgs := []string{"log", "--format=%cI", "-n1"} + if commitUnix, err = exec.Command(cmdName, cmdArgs...).Output(); err != nil { + fmt.Fprintln(os.Stderr, "Error generating git commit-time: ", err) + os.Exit(1) + } + + t, err := time.Parse(time.RFC3339, strings.TrimSpace(string(commitUnix))) + if err != nil { + fmt.Fprintln(os.Stderr, "Error generating git commit-time: ", err) + os.Exit(1) + } + + return t.UTC() +} + +func main() { + var version string + if len(os.Args) > 1 { + version = os.Args[1] + } else { + version = commitTime().Format(time.RFC3339) + } + + fmt.Println(genLDFlags(version)) +} diff --git a/cmd/client.go b/cmd/client.go index 10ff572..9219299 100644 --- a/cmd/client.go +++ b/cmd/client.go @@ -1,6 +1,5 @@ // Copyright (c) 2023 Cloudnatively Services Pvt Ltd // -// This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by @@ -21,17 +20,18 @@ import ( "io" "net/http" "net/url" - "pb/pkg/config" "time" + + "pb/pkg/config" ) -type HttpClient struct { +type HTTPClient struct { client http.Client profile *config.Profile } -func DefaultClient() HttpClient { - return HttpClient{ +func DefaultClient() HTTPClient { + return HTTPClient{ client: http.Client{ Timeout: 60 * time.Second, }, @@ -39,13 +39,13 @@ func DefaultClient() HttpClient { } } -func (client *HttpClient) baseApiUrl(path string) (x string) { - x, _ = url.JoinPath(client.profile.Url, "api/v1/", path) +func (client *HTTPClient) baseAPIURL(path string) (x string) { + x, _ = url.JoinPath(client.profile.URL, "api/v1/", path) return } -func (client *HttpClient) NewRequest(method string, path string, body io.Reader) (req *http.Request, err error) { - req, err = http.NewRequest(method, client.baseApiUrl(path), body) +func (client *HTTPClient) NewRequest(method string, path string, body io.Reader) (req *http.Request, err error) { + req, err = http.NewRequest(method, client.baseAPIURL(path), body) if err != nil { return } diff --git a/cmd/pre.go b/cmd/pre.go index 19ce3d8..53bcf68 100644 --- a/cmd/pre.go +++ b/cmd/pre.go @@ -1,6 +1,5 @@ // Copyright (c) 2023 Cloudnatively Services Pvt Ltd // -// This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by @@ -20,6 +19,7 @@ package cmd import ( "errors" "os" + "pb/pkg/config" "github.com/spf13/cobra" @@ -38,10 +38,10 @@ func PreRunDefaultProfile(cmd *cobra.Command, args []string) error { return err } } - if conf.Profiles == nil || conf.Default_profile == "" { + if conf.Profiles == nil || conf.DefaultProfile == "" { return errors.New("no profile is configured to run this command. please create one using profile command") } - DefaultProfile = conf.Profiles[conf.Default_profile] + DefaultProfile = conf.Profiles[conf.DefaultProfile] return nil } diff --git a/cmd/profile.go b/cmd/profile.go index d94a3f1..bbda3ea 100644 --- a/cmd/profile.go +++ b/cmd/profile.go @@ -1,236 +1,233 @@ -// Copyright (c) 2023 Cloudnatively Services Pvt Ltd -// -// This file is part of MinIO Object Storage stack -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package cmd - -import ( - "errors" - "fmt" - "net/url" - "os" - "pb/pkg/config" - "pb/pkg/model/credential" - "pb/pkg/model/defaultprofile" - - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - "github.com/muesli/termenv" - "github.com/spf13/cobra" -) - -type ProfileListItem struct { - title, url, user string -} - -func (item *ProfileListItem) Render(highlight bool) string { - if highlight { - render := fmt.Sprintf( - "%s\n%s\n%s", - selectedStyle.Render(item.title), - selectedStyleAlt.Render(fmt.Sprintf("url: %s", item.url)), - selectedStyleAlt.Render(fmt.Sprintf("user: %s", item.user)), - ) - return selectedItemOuter.Render(render) - } else { - render := fmt.Sprintf( - "%s\n%s\n%s", - standardStyle.Render(item.title), - standardStyleAlt.Render(fmt.Sprintf("url: %s", item.url)), - standardStyleAlt.Render(fmt.Sprintf("user: %s", item.user)), - ) - return itemOuter.Render(render) - } -} - -var AddProfileCmd = &cobra.Command{ - Use: "add profile-name url ", - Example: " pb profile add local_parseable http://0.0.0.0:8000 admin admin", - Short: "Add a new profile", - Long: "Add a new profile to the config file", - Args: func(cmd *cobra.Command, args []string) error { - if err := cobra.MinimumNArgs(2)(cmd, args); err != nil { - return err - } - if err := cobra.MaximumNArgs(4)(cmd, args); err != nil { - return err - } - return nil - }, - RunE: func(cmd *cobra.Command, args []string) error { - name := args[0] - url, err := url.Parse(args[1]) - if err != nil { - return err - } - - var username string - var password string - - if len(args) < 4 { - _m, err := tea.NewProgram(credential.New()).Run() - if err != nil { - fmt.Printf("Alas, there's been an error: %v", err) - os.Exit(1) - } - m := _m.(credential.Model) - - username, password = m.Values() - } else { - username = args[2] - password = args[3] - } - - profile := config.Profile{ - Url: url.String(), - Username: username, - Password: password, - } - - file_config, err := config.ReadConfigFromFile() - - if err != nil { - // create new file - new_config := config.Config{ - Profiles: map[string]config.Profile{ - name: profile, - }, - Default_profile: name, - } - err = config.WriteConfigToFile(&new_config) - return err - } else { - if file_config.Profiles == nil { - file_config.Profiles = make(map[string]config.Profile) - } - file_config.Profiles[name] = profile - if file_config.Default_profile == "" { - file_config.Default_profile = name - } - config.WriteConfigToFile(file_config) - } - - return nil - }, -} - -var RemoveProfileCmd = &cobra.Command{ - Use: "remove profile-name", - Aliases: []string{"rm"}, - Example: " pb profile remove local_parseable", - Args: cobra.ExactArgs(1), - Short: "Delete a profile", - RunE: func(cmd *cobra.Command, args []string) error { - name := args[0] - file_config, err := config.ReadConfigFromFile() - if err != nil { - return nil - } - - _, exists := file_config.Profiles[name] - if exists { - delete(file_config.Profiles, name) - if len(file_config.Profiles) == 0 { - file_config.Default_profile = "" - } - config.WriteConfigToFile(file_config) - fmt.Printf("Deleted profile %s\n", styleBold.Render(name)) - } else { - fmt.Printf("No profile found with the name: %s", styleBold.Render(name)) - } - - return nil - }, -} - -var DefaultProfileCmd = &cobra.Command{ - Use: "default profile-name", - Args: cobra.MaximumNArgs(1), - Short: "Set default profile to use with all commands", - Example: " pb profile default local_parseable", - RunE: func(cmd *cobra.Command, args []string) error { - var name string - - file_config, err := config.ReadConfigFromFile() - if err != nil { - return nil - } - - if len(args) > 0 { - name = args[0] - } else { - model := defaultprofile.New(file_config.Profiles) - _m, err := tea.NewProgram(model).Run() - if err != nil { - fmt.Printf("Alas, there's been an error: %v", err) - os.Exit(1) - } - m := _m.(defaultprofile.Model) - termenv.DefaultOutput().ClearLines(lipgloss.Height(model.View()) - 1) - if m.Success { - name = m.Choice - } else { - return nil - } - } - - _, exists := file_config.Profiles[name] - if exists { - file_config.Default_profile = name - } else { - name = lipgloss.NewStyle().Bold(true).Render(name) - err := fmt.Sprintf("profile %s does not exist", styleBold.Render(name)) - return errors.New(err) - } - - config.WriteConfigToFile(file_config) - fmt.Printf("%s is now set as default profile\n", styleBold.Render(name)) - return nil - }, -} - -var ListProfileCmd = &cobra.Command{ - Use: "list profiles", - Short: "List all added profiles", - Example: " pb profile list", - RunE: func(cmd *cobra.Command, args []string) error { - file_config, err := config.ReadConfigFromFile() - if err != nil { - return nil - } - - if len(file_config.Profiles) != 0 { - println() - } - - row := 0 - for key, value := range file_config.Profiles { - item := ProfileListItem{key, value.Url, value.Username} - fmt.Println(item.Render(file_config.Default_profile == key)) - row += 1 - fmt.Println() - } - return nil - }, -} - -func Max(a int, b int) int { - if a >= b { - return a - } else { - return b - } -} +// Copyright (c) 2023 Cloudnatively Services Pvt Ltd +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "errors" + "fmt" + "net/url" + "os" + + "pb/pkg/config" + "pb/pkg/model/credential" + "pb/pkg/model/defaultprofile" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/muesli/termenv" + "github.com/spf13/cobra" +) + +// ProfileListItem is a struct to hold the profile list items +type ProfileListItem struct { + title, url, user string +} + +func (item *ProfileListItem) Render(highlight bool) string { + if highlight { + render := fmt.Sprintf( + "%s\n%s\n%s", + selectedStyle.Render(item.title), + selectedStyleAlt.Render(fmt.Sprintf("url: %s", item.url)), + selectedStyleAlt.Render(fmt.Sprintf("user: %s", item.user)), + ) + return selectedItemOuter.Render(render) + } + render := fmt.Sprintf( + "%s\n%s\n%s", + standardStyle.Render(item.title), + standardStyleAlt.Render(fmt.Sprintf("url: %s", item.url)), + standardStyleAlt.Render(fmt.Sprintf("user: %s", item.user)), + ) + return itemOuter.Render(render) +} + +var AddProfileCmd = &cobra.Command{ + Use: "add profile-name url ", + Example: " pb profile add local_parseable http://0.0.0.0:8000 admin admin", + Short: "Add a new profile", + Long: "Add a new profile to the config file", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.MinimumNArgs(2)(cmd, args); err != nil { + return err + } + if err := cobra.MaximumNArgs(4)(cmd, args); err != nil { + return err + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + name := args[0] + url, err := url.Parse(args[1]) + if err != nil { + return err + } + + var username string + var password string + + if len(args) < 4 { + _m, err := tea.NewProgram(credential.New()).Run() + if err != nil { + fmt.Printf("Alas, there's been an error: %v", err) + os.Exit(1) + } + m := _m.(credential.Model) + + username, password = m.Values() + } else { + username = args[2] + password = args[3] + } + + profile := config.Profile{ + URL: url.String(), + Username: username, + Password: password, + } + + file_config, err := config.ReadConfigFromFile() + if err != nil { + // create new file + new_config := config.Config{ + Profiles: map[string]config.Profile{ + name: profile, + }, + DefaultProfile: name, + } + err = config.WriteConfigToFile(&new_config) + return err + } + if file_config.Profiles == nil { + file_config.Profiles = make(map[string]config.Profile) + } + file_config.Profiles[name] = profile + if file_config.DefaultProfile == "" { + file_config.DefaultProfile = name + } + config.WriteConfigToFile(file_config) + + return nil + }, +} + +var RemoveProfileCmd = &cobra.Command{ + Use: "remove profile-name", + Aliases: []string{"rm"}, + Example: " pb profile remove local_parseable", + Args: cobra.ExactArgs(1), + Short: "Delete a profile", + RunE: func(cmd *cobra.Command, args []string) error { + name := args[0] + file_config, err := config.ReadConfigFromFile() + if err != nil { + return nil + } + + _, exists := file_config.Profiles[name] + if exists { + delete(file_config.Profiles, name) + if len(file_config.Profiles) == 0 { + file_config.DefaultProfile = "" + } + config.WriteConfigToFile(file_config) + fmt.Printf("Deleted profile %s\n", styleBold.Render(name)) + } else { + fmt.Printf("No profile found with the name: %s", styleBold.Render(name)) + } + + return nil + }, +} + +var DefaultProfileCmd = &cobra.Command{ + Use: "default profile-name", + Args: cobra.MaximumNArgs(1), + Short: "Set default profile to use with all commands", + Example: " pb profile default local_parseable", + RunE: func(cmd *cobra.Command, args []string) error { + var name string + + file_config, err := config.ReadConfigFromFile() + if err != nil { + return nil + } + + if len(args) > 0 { + name = args[0] + } else { + model := defaultprofile.New(file_config.Profiles) + _m, err := tea.NewProgram(model).Run() + if err != nil { + fmt.Printf("Alas, there's been an error: %v", err) + os.Exit(1) + } + m := _m.(defaultprofile.Model) + termenv.DefaultOutput().ClearLines(lipgloss.Height(model.View()) - 1) + if m.Success { + name = m.Choice + } else { + return nil + } + } + + _, exists := file_config.Profiles[name] + if exists { + file_config.DefaultProfile = name + } else { + name = lipgloss.NewStyle().Bold(true).Render(name) + err := fmt.Sprintf("profile %s does not exist", styleBold.Render(name)) + return errors.New(err) + } + + config.WriteConfigToFile(file_config) + fmt.Printf("%s is now set as default profile\n", styleBold.Render(name)) + return nil + }, +} + +var ListProfileCmd = &cobra.Command{ + Use: "list profiles", + Short: "List all added profiles", + Example: " pb profile list", + RunE: func(cmd *cobra.Command, args []string) error { + file_config, err := config.ReadConfigFromFile() + if err != nil { + return nil + } + + if len(file_config.Profiles) != 0 { + println() + } + + row := 0 + for key, value := range file_config.Profiles { + item := ProfileListItem{key, value.URL, value.Username} + fmt.Println(item.Render(file_config.DefaultProfile == key)) + row++ + fmt.Println() + } + return nil + }, +} + +func Max(a int, b int) int { + if a >= b { + return a + } else { + return b + } +} diff --git a/cmd/stream.go b/cmd/stream.go index 1a12e18..ec824d3 100644 --- a/cmd/stream.go +++ b/cmd/stream.go @@ -1,354 +1,357 @@ -// Copyright (c) 2023 Cloudnatively Services Pvt Ltd -// -// This file is part of MinIO Object Storage stack -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package cmd - -import ( - "encoding/json" - "errors" - "fmt" - "io" - "strconv" - "strings" - "time" - - "github.com/dustin/go-humanize" - "github.com/spf13/cobra" -) - -type StreamStatsData struct { - Ingestion struct { - Count int `json:"count"` - Format string `json:"format"` - Size string `json:"size"` - } `json:"ingestion"` - Storage struct { - Format string `json:"format"` - Size string `json:"size"` - } `json:"storage"` - Stream string `json:"stream"` - Time time.Time `json:"time"` -} - -type StreamRetentionData []struct { - Description string `json:"description"` - Action string `json:"action"` - Duration string `json:"duration"` -} - -type StreamAlertData struct { - Alerts []struct { - Message string `json:"message"` - Name string `json:"name"` - Rule struct { - Config struct { - Column string `json:"column"` - Operator string `json:"operator"` - Repeats int `json:"repeats"` - Value int `json:"value"` - } `json:"config"` - Type string `json:"type"` - } `json:"rule"` - Targets []struct { - Endpoint string `json:"endpoint"` - Password string `json:"password,omitempty"` - Repeat struct { - Interval string `json:"interval"` - Times int `json:"times"` - } `json:"repeat"` - SkipTLSCheck bool `json:"skip_tls_check,omitempty"` - Type string `json:"type"` - Username string `json:"username,omitempty"` - Headers struct { - Authorization string `json:"Authorization"` - } `json:"headers,omitempty"` - } `json:"targets"` - } `json:"alerts"` - Version string `json:"version"` -} - -var AddStreamCmd = &cobra.Command{ - Use: "add stream-name", - Example: " pb stream add backend_logs", - Short: "Create a new stream", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - name := args[0] - client := DefaultClient() - req, err := client.NewRequest("PUT", "logstream/"+name, nil) - if err != nil { - return err - } - - resp, err := client.client.Do(req) - if err != nil { - return err - } - - if resp.StatusCode == 200 { - fmt.Printf("Created stream %s\n", styleBold.Render(name)) - } else { - bytes, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - body := string(bytes) - defer resp.Body.Close() - fmt.Printf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, body) - } - - return nil - }, -} - -var StatStreamCmd = &cobra.Command{ - Use: "info stream-name", - Example: " pb stream info backend_logs", - Short: "Get statistics for a stream", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - name := args[0] - client := DefaultClient() - - stats, err := fetchStats(&client, name) - if err != nil { - return err - } - - ingestion_count := stats.Ingestion.Count - ingestion_size, _ := strconv.Atoi(strings.TrimRight(stats.Ingestion.Size, " Bytes")) - storage_size, _ := strconv.Atoi(strings.TrimRight(stats.Storage.Size, " Bytes")) - - retention, err := fetchRetention(&client, name) - if err != nil { - return err - } - - is_rentention_set := len(retention) > 0 - - fmt.Println(styleBold.Render("Info:")) - fmt.Printf(" Event Count: %d\n", ingestion_count) - fmt.Printf(" Ingestion Size: %s\n", humanize.Bytes(uint64(ingestion_size))) - fmt.Printf(" Storage Size: %s\n", humanize.Bytes(uint64(storage_size))) - fmt.Printf( - " Compression Ratio: %.2f%s\n", - 100-(float64(storage_size)/float64(ingestion_size))*100, "%") - fmt.Println() - - if is_rentention_set { - fmt.Println(styleBold.Render("Retention:")) - for _, item := range retention { - fmt.Printf(" Action: %s\n", styleBold.Render(item.Action)) - fmt.Printf(" Duration: %s\n", styleBold.Render(item.Duration)) - fmt.Println() - } - } else { - fmt.Println(styleBold.Render("No retention period set on stream\n")) - } - - alerts_data, err := fetchAlerts(&client, name) - if err != nil { - return err - } - alerts := alerts_data.Alerts - - is_alerts_set := len(alerts) > 0 - - if is_alerts_set { - fmt.Println(styleBold.Render("Alerts:")) - for _, alert := range alerts { - fmt.Printf(" Alert: %s\n", styleBold.Render(alert.Name)) - rule_fmt := fmt.Sprintf( - "%s %s %s repeated %d times", - alert.Rule.Config.Column, - alert.Rule.Config.Operator, - fmt.Sprint(alert.Rule.Config.Value), - alert.Rule.Config.Repeats, - ) - fmt.Printf(" Rule: %s\n", rule_fmt) - fmt.Printf(" Targets: ") - for _, target := range alert.Targets { - fmt.Printf("%s, ", target.Type) - } - fmt.Print("\n\n") - } - } else { - fmt.Println(styleBold.Render("No alerts set on stream\n")) - } - - return nil - }, -} - -var RemoveStreamCmd = &cobra.Command{ - Use: "remove stream-name", - Aliases: []string{"rm"}, - Example: " pb stream remove backend_logs", - Short: "Delete a stream", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - name := args[0] - client := DefaultClient() - req, err := client.NewRequest("DELETE", "logstream/"+name, nil) - if err != nil { - return err - } - - resp, err := client.client.Do(req) - if err != nil { - return err - } - - if resp.StatusCode == 200 { - fmt.Printf("Removed stream %s", styleBold.Render(name)) - } else { - bytes, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - body := string(bytes) - defer resp.Body.Close() - - fmt.Printf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, body) - } - - return nil - }, -} - -var ListStreamCmd = &cobra.Command{ - Use: "list", - Short: "List all streams", - Example: " pb stream list", - RunE: func(cmd *cobra.Command, args []string) error { - client := DefaultClient() - req, err := client.NewRequest("GET", "logstream", nil) - if err != nil { - return err - } - - resp, err := client.client.Do(req) - if err != nil { - return err - } - - if resp.StatusCode == 200 { - items := []map[string]string{} - err = json.NewDecoder(resp.Body).Decode(&items) - if err != nil { - return err - } - defer resp.Body.Close() - for _, item := range items { - fmt.Println(item["name"]) - } - } else { - bytes, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - body := string(bytes) - fmt.Printf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, body) - } - - return nil - }, -} - -func fetchStats(client *HttpClient, name string) (data StreamStatsData, err error) { - req, err := client.NewRequest("GET", fmt.Sprintf("logstream/%s/stats", name), nil) - if err != nil { - return - } - - resp, err := client.client.Do(req) - if err != nil { - return - } - - bytes, err := io.ReadAll(resp.Body) - if err != nil { - return - } - defer resp.Body.Close() - - if resp.StatusCode == 200 { - err = json.Unmarshal(bytes, &data) - return - } else { - body := string(bytes) - body = fmt.Sprintf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, body) - err = errors.New(body) - } - return -} - -func fetchRetention(client *HttpClient, name string) (data StreamRetentionData, err error) { - req, err := client.NewRequest("GET", fmt.Sprintf("logstream/%s/retention", name), nil) - if err != nil { - return - } - - resp, err := client.client.Do(req) - if err != nil { - return - } - - bytes, err := io.ReadAll(resp.Body) - if err != nil { - return - } - defer resp.Body.Close() - - if resp.StatusCode == 200 { - err = json.Unmarshal(bytes, &data) - return - } else { - body := string(bytes) - body = fmt.Sprintf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, body) - err = errors.New(body) - } - return -} - -func fetchAlerts(client *HttpClient, name string) (data StreamAlertData, err error) { - req, err := client.NewRequest("GET", fmt.Sprintf("logstream/%s/alert", name), nil) - if err != nil { - return - } - - resp, err := client.client.Do(req) - if err != nil { - return - } - - bytes, err := io.ReadAll(resp.Body) - if err != nil { - return - } - defer resp.Body.Close() - - if resp.StatusCode == 200 { - err = json.Unmarshal(bytes, &data) - return - } else { - body := string(bytes) - body = fmt.Sprintf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, body) - err = errors.New(body) - } - return -} +// Copyright (c) 2023 Cloudnatively Services Pvt Ltd +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "strconv" + "strings" + "time" + + "github.com/dustin/go-humanize" + "github.com/spf13/cobra" +) + +// StreamStatsData is the data structure for stream stats +type StreamStatsData struct { + Ingestion struct { + Count int `json:"count"` + Format string `json:"format"` + Size string `json:"size"` + } `json:"ingestion"` + Storage struct { + Format string `json:"format"` + Size string `json:"size"` + } `json:"storage"` + Stream string `json:"stream"` + Time time.Time `json:"time"` +} + +// StreamRetentionData is the data structure for stream retention +type StreamRetentionData []struct { + Description string `json:"description"` + Action string `json:"action"` + Duration string `json:"duration"` +} + +// StreamAlertData is the data structure for stream alerts +type StreamAlertData struct { + Alerts []struct { + Message string `json:"message"` + Name string `json:"name"` + Rule struct { + Config struct { + Column string `json:"column"` + Operator string `json:"operator"` + Repeats int `json:"repeats"` + Value int `json:"value"` + } `json:"config"` + Type string `json:"type"` + } `json:"rule"` + Targets []struct { + Endpoint string `json:"endpoint"` + Password string `json:"password,omitempty"` + Repeat struct { + Interval string `json:"interval"` + Times int `json:"times"` + } `json:"repeat"` + SkipTLSCheck bool `json:"skip_tls_check,omitempty"` + Type string `json:"type"` + Username string `json:"username,omitempty"` + Headers struct { + Authorization string `json:"Authorization"` + } `json:"headers,omitempty"` + } `json:"targets"` + } `json:"alerts"` + Version string `json:"version"` +} + +// AddStreamCmd is the parent command for stream +var AddStreamCmd = &cobra.Command{ + Use: "add stream-name", + Example: " pb stream add backend_logs", + Short: "Create a new stream", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + name := args[0] + client := DefaultClient() + req, err := client.NewRequest("PUT", "logstream/"+name, nil) + if err != nil { + return err + } + + resp, err := client.client.Do(req) + if err != nil { + return err + } + + if resp.StatusCode == 200 { + fmt.Printf("Created stream %s\n", styleBold.Render(name)) + } else { + bytes, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + body := string(bytes) + defer resp.Body.Close() + fmt.Printf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, body) + } + + return nil + }, +} + +// StatStreamCmd is the stat command for stream +var StatStreamCmd = &cobra.Command{ + Use: "info stream-name", + Example: " pb stream info backend_logs", + Short: "Get statistics for a stream", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + name := args[0] + client := DefaultClient() + + stats, err := fetchStats(&client, name) + if err != nil { + return err + } + + ingestion_count := stats.Ingestion.Count + ingestion_size, _ := strconv.Atoi(strings.TrimRight(stats.Ingestion.Size, " Bytes")) + storage_size, _ := strconv.Atoi(strings.TrimRight(stats.Storage.Size, " Bytes")) + + retention, err := fetchRetention(&client, name) + if err != nil { + return err + } + + isRententionSet := len(retention) > 0 + + fmt.Println(styleBold.Render("Info:")) + fmt.Printf(" Event Count: %d\n", ingestion_count) + fmt.Printf(" Ingestion Size: %s\n", humanize.Bytes(uint64(ingestion_size))) + fmt.Printf(" Storage Size: %s\n", humanize.Bytes(uint64(storage_size))) + fmt.Printf( + " Compression Ratio: %.2f%s\n", + 100-(float64(storage_size)/float64(ingestion_size))*100, "%") + fmt.Println() + + if isRententionSet { + fmt.Println(styleBold.Render("Retention:")) + for _, item := range retention { + fmt.Printf(" Action: %s\n", styleBold.Render(item.Action)) + fmt.Printf(" Duration: %s\n", styleBold.Render(item.Duration)) + fmt.Println() + } + } else { + fmt.Println(styleBold.Render("No retention period set on stream\n")) + } + + alerts_data, err := fetchAlerts(&client, name) + if err != nil { + return err + } + alerts := alerts_data.Alerts + + isAlertsSet := len(alerts) > 0 + + if isAlertsSet { + fmt.Println(styleBold.Render("Alerts:")) + for _, alert := range alerts { + fmt.Printf(" Alert: %s\n", styleBold.Render(alert.Name)) + rule_fmt := fmt.Sprintf( + "%s %s %s repeated %d times", + alert.Rule.Config.Column, + alert.Rule.Config.Operator, + fmt.Sprint(alert.Rule.Config.Value), + alert.Rule.Config.Repeats, + ) + fmt.Printf(" Rule: %s\n", rule_fmt) + fmt.Printf(" Targets: ") + for _, target := range alert.Targets { + fmt.Printf("%s, ", target.Type) + } + fmt.Print("\n\n") + } + } else { + fmt.Println(styleBold.Render("No alerts set on stream\n")) + } + + return nil + }, +} + +var RemoveStreamCmd = &cobra.Command{ + Use: "remove stream-name", + Aliases: []string{"rm"}, + Example: " pb stream remove backend_logs", + Short: "Delete a stream", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + name := args[0] + client := DefaultClient() + req, err := client.NewRequest("DELETE", "logstream/"+name, nil) + if err != nil { + return err + } + + resp, err := client.client.Do(req) + if err != nil { + return err + } + + if resp.StatusCode == 200 { + fmt.Printf("Removed stream %s", styleBold.Render(name)) + } else { + bytes, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + body := string(bytes) + defer resp.Body.Close() + + fmt.Printf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, body) + } + + return nil + }, +} + +var ListStreamCmd = &cobra.Command{ + Use: "list", + Short: "List all streams", + Example: " pb stream list", + RunE: func(cmd *cobra.Command, args []string) error { + client := DefaultClient() + req, err := client.NewRequest("GET", "logstream", nil) + if err != nil { + return err + } + + resp, err := client.client.Do(req) + if err != nil { + return err + } + + if resp.StatusCode == 200 { + items := []map[string]string{} + err = json.NewDecoder(resp.Body).Decode(&items) + if err != nil { + return err + } + defer resp.Body.Close() + for _, item := range items { + fmt.Println(item["name"]) + } + } else { + bytes, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + body := string(bytes) + fmt.Printf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, body) + } + + return nil + }, +} + +func fetchStats(client *HTTPClient, name string) (data StreamStatsData, err error) { + req, err := client.NewRequest("GET", fmt.Sprintf("logstream/%s/stats", name), nil) + if err != nil { + return + } + + resp, err := client.client.Do(req) + if err != nil { + return + } + + bytes, err := io.ReadAll(resp.Body) + if err != nil { + return + } + defer resp.Body.Close() + + if resp.StatusCode == 200 { + err = json.Unmarshal(bytes, &data) + return + } else { + body := string(bytes) + body = fmt.Sprintf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, body) + err = errors.New(body) + } + return +} + +func fetchRetention(client *HTTPClient, name string) (data StreamRetentionData, err error) { + req, err := client.NewRequest("GET", fmt.Sprintf("logstream/%s/retention", name), nil) + if err != nil { + return + } + + resp, err := client.client.Do(req) + if err != nil { + return + } + + bytes, err := io.ReadAll(resp.Body) + if err != nil { + return + } + defer resp.Body.Close() + + if resp.StatusCode == 200 { + err = json.Unmarshal(bytes, &data) + return + } else { + body := string(bytes) + body = fmt.Sprintf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, body) + err = errors.New(body) + } + return +} + +func fetchAlerts(client *HTTPClient, name string) (data StreamAlertData, err error) { + req, err := client.NewRequest("GET", fmt.Sprintf("logstream/%s/alert", name), nil) + if err != nil { + return + } + + resp, err := client.client.Do(req) + if err != nil { + return + } + + bytes, err := io.ReadAll(resp.Body) + if err != nil { + return + } + defer resp.Body.Close() + + if resp.StatusCode == 200 { + err = json.Unmarshal(bytes, &data) + return + } else { + body := string(bytes) + body = fmt.Sprintf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, body) + err = errors.New(body) + } + return +} diff --git a/cmd/style.go b/cmd/style.go index f02430a..dea7e61 100644 --- a/cmd/style.go +++ b/cmd/style.go @@ -1,6 +1,5 @@ // Copyright (c) 2023 Cloudnatively Services Pvt Ltd // -// This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by diff --git a/cmd/user.go b/cmd/user.go index f360b55..287eb2e 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -1,285 +1,284 @@ -// Copyright (c) 2023 Cloudnatively Services Pvt Ltd -// -// This file is part of MinIO Object Storage stack -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package cmd - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "os" - "pb/pkg/model/role" - "strings" - "sync" - - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - "github.com/spf13/cobra" - "golang.org/x/exp/slices" -) - -type RoleResource struct { - Stream string `json:"stream,omitempty"` - Tag string `json:"tag,omitempty"` -} - -type UserRoleData struct { - Privilege string `json:"privilege"` - Resource *RoleResource `json:"resource,omitempty"` -} - -func (user *UserRoleData) Render() string { - var s strings.Builder - s.WriteString(standardStyle.Render("Privilege: ")) - s.WriteString(standardStyleAlt.Render(user.Privilege)) - s.WriteString("\n") - if user.Resource != nil { - if user.Resource.Stream != "" { - s.WriteString(standardStyle.Render("Stream: ")) - s.WriteString(standardStyleAlt.Render(user.Resource.Stream)) - s.WriteString("\n") - } - if user.Resource.Tag != "" { - s.WriteString(standardStyle.Render("Tag: ")) - s.WriteString(standardStyleAlt.Render(user.Resource.Tag)) - s.WriteString("\n") - } - } - - return s.String() -} - -type FetchUserRoleRes struct { - data []UserRoleData - err error -} - -var AddUserCmd = &cobra.Command{ - Use: "add user-name", - Example: " pb user add bob", - Short: "Add a new user", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - name := args[0] - - var users []string - client := DefaultClient() - if err := fetchUsers(&client, &users); err != nil { - return err - } - - if slices.Contains(users, name) { - fmt.Println("user already exists") - return nil - } - - _m, err := tea.NewProgram(role.New()).Run() - if err != nil { - fmt.Printf("Alas, there's been an error: %v", err) - os.Exit(1) - } - m := _m.(role.Model) - - privilege := m.Selection.Value() - stream := m.Stream.Value() - tag := m.Tag.Value() - - if !m.Success { - fmt.Println("aborted by user") - return nil - } - - var putBody io.Reader - - // set role - if privilege != "none" { - roleData := UserRoleData{ - Privilege: privilege, - } - switch privilege { - case "writer": - roleData.Resource = &RoleResource{ - Stream: stream, - } - case "reader": - roleData.Resource = &RoleResource{ - Stream: stream, - } - if tag != "" { - roleData.Resource.Tag = tag - } - } - roleDataJson, _ := json.Marshal([]UserRoleData{roleData}) - putBody = bytes.NewBuffer(roleDataJson) - } - req, err := client.NewRequest("PUT", "user/"+name, putBody) - if err != nil { - return err - } - - resp, err := client.client.Do(req) - if err != nil { - return err - } - - bytes, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - body := string(bytes) - defer resp.Body.Close() - - if resp.StatusCode == 200 { - fmt.Printf("Added user %s \nPassword is: %s\n", name, body) - } else { - fmt.Printf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, body) - } - - return nil - }, -} - -var RemoveUserCmd = &cobra.Command{ - Use: "remove user-name", - Aliases: []string{"rm"}, - Example: " pb user remove bob", - Short: "Delete a user", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - name := args[0] - client := DefaultClient() - req, err := client.NewRequest("DELETE", "user/"+name, nil) - if err != nil { - return err - } - - resp, err := client.client.Do(req) - if err != nil { - return err - } - - if resp.StatusCode == 200 { - fmt.Printf("Removed user %s\n", styleBold.Render(name)) - } else { - bytes, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - body := string(bytes) - defer resp.Body.Close() - - fmt.Printf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, body) - } - - return nil - }, -} - -var ListUserCmd = &cobra.Command{ - Use: "list", - Short: "List all users", - Example: " pb user list", - RunE: func(cmd *cobra.Command, args []string) error { - var users []string - client := DefaultClient() - err := fetchUsers(&client, &users) - if err != nil { - return err - } - - role_responses := make([]FetchUserRoleRes, len(users)) - wsg := sync.WaitGroup{} - wsg.Add(len(users)) - - for idx, user := range users { - idx := idx - user := user - client := &client - go func() { - role_responses[idx] = fetchUserRoles(client, user) - wsg.Done() - }() - } - - wsg.Wait() - fmt.Println() - for idx, user := range users { - roles := role_responses[idx] - fmt.Print("• ") - fmt.Println(standardStyleBold.Bold(true).Render(user)) - if roles.err == nil { - for _, role := range roles.data { - fmt.Println(lipgloss.NewStyle().PaddingLeft(3).Render(role.Render())) - } - } else { - fmt.Println(roles.err) - } - } - - return nil - }, -} - -func fetchUsers(client *HttpClient, data *[]string) error { - req, err := client.NewRequest("GET", "user", nil) - if err != nil { - return err - } - - resp, err := client.client.Do(req) - if err != nil { - return err - } - - bytes, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - defer resp.Body.Close() - - if resp.StatusCode == 200 { - err = json.Unmarshal(bytes, data) - if err != nil { - return err - } - } else { - body := string(bytes) - return fmt.Errorf("request failed\nstatus code: %s\nresponse: %s", resp.Status, body) - } - - return nil -} - -func fetchUserRoles(client *HttpClient, user string) (res FetchUserRoleRes) { - req, err := client.NewRequest("GET", fmt.Sprintf("user/%s/role", user), nil) - if err != nil { - return - } - resp, err := client.client.Do(req) - if err != nil { - return - } - body, err := io.ReadAll(resp.Body) - if err != nil { - return - } - defer resp.Body.Close() - - res.err = json.Unmarshal(body, &res.data) - return -} +// Copyright (c) 2023 Cloudnatively Services Pvt Ltd +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "os" + "strings" + "sync" + + "pb/pkg/model/role" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" + "golang.org/x/exp/slices" +) + +type RoleResource struct { + Stream string `json:"stream,omitempty"` + Tag string `json:"tag,omitempty"` +} + +type UserRoleData struct { + Privilege string `json:"privilege"` + Resource *RoleResource `json:"resource,omitempty"` +} + +func (user *UserRoleData) Render() string { + var s strings.Builder + s.WriteString(standardStyle.Render("Privilege: ")) + s.WriteString(standardStyleAlt.Render(user.Privilege)) + s.WriteString("\n") + if user.Resource != nil { + if user.Resource.Stream != "" { + s.WriteString(standardStyle.Render("Stream: ")) + s.WriteString(standardStyleAlt.Render(user.Resource.Stream)) + s.WriteString("\n") + } + if user.Resource.Tag != "" { + s.WriteString(standardStyle.Render("Tag: ")) + s.WriteString(standardStyleAlt.Render(user.Resource.Tag)) + s.WriteString("\n") + } + } + + return s.String() +} + +type FetchUserRoleRes struct { + data []UserRoleData + err error +} + +var AddUserCmd = &cobra.Command{ + Use: "add user-name", + Example: " pb user add bob", + Short: "Add a new user", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + name := args[0] + + var users []string + client := DefaultClient() + if err := fetchUsers(&client, &users); err != nil { + return err + } + + if slices.Contains(users, name) { + fmt.Println("user already exists") + return nil + } + + _m, err := tea.NewProgram(role.New()).Run() + if err != nil { + fmt.Printf("Alas, there's been an error: %v", err) + os.Exit(1) + } + m := _m.(role.Model) + + privilege := m.Selection.Value() + stream := m.Stream.Value() + tag := m.Tag.Value() + + if !m.Success { + fmt.Println("aborted by user") + return nil + } + + var putBody io.Reader + + // set role + if privilege != "none" { + roleData := UserRoleData{ + Privilege: privilege, + } + switch privilege { + case "writer": + roleData.Resource = &RoleResource{ + Stream: stream, + } + case "reader": + roleData.Resource = &RoleResource{ + Stream: stream, + } + if tag != "" { + roleData.Resource.Tag = tag + } + } + roleDataJson, _ := json.Marshal([]UserRoleData{roleData}) + putBody = bytes.NewBuffer(roleDataJson) + } + req, err := client.NewRequest("PUT", "user/"+name, putBody) + if err != nil { + return err + } + + resp, err := client.client.Do(req) + if err != nil { + return err + } + + bytes, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + body := string(bytes) + defer resp.Body.Close() + + if resp.StatusCode == 200 { + fmt.Printf("Added user %s \nPassword is: %s\n", name, body) + } else { + fmt.Printf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, body) + } + + return nil + }, +} + +var RemoveUserCmd = &cobra.Command{ + Use: "remove user-name", + Aliases: []string{"rm"}, + Example: " pb user remove bob", + Short: "Delete a user", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + name := args[0] + client := DefaultClient() + req, err := client.NewRequest("DELETE", "user/"+name, nil) + if err != nil { + return err + } + + resp, err := client.client.Do(req) + if err != nil { + return err + } + + if resp.StatusCode == 200 { + fmt.Printf("Removed user %s\n", styleBold.Render(name)) + } else { + bytes, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + body := string(bytes) + defer resp.Body.Close() + + fmt.Printf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, body) + } + + return nil + }, +} + +var ListUserCmd = &cobra.Command{ + Use: "list", + Short: "List all users", + Example: " pb user list", + RunE: func(cmd *cobra.Command, args []string) error { + var users []string + client := DefaultClient() + err := fetchUsers(&client, &users) + if err != nil { + return err + } + + role_responses := make([]FetchUserRoleRes, len(users)) + wsg := sync.WaitGroup{} + wsg.Add(len(users)) + + for idx, user := range users { + idx := idx + user := user + client := &client + go func() { + role_responses[idx] = fetchUserRoles(client, user) + wsg.Done() + }() + } + + wsg.Wait() + fmt.Println() + for idx, user := range users { + roles := role_responses[idx] + fmt.Print("• ") + fmt.Println(standardStyleBold.Bold(true).Render(user)) + if roles.err == nil { + for _, role := range roles.data { + fmt.Println(lipgloss.NewStyle().PaddingLeft(3).Render(role.Render())) + } + } else { + fmt.Println(roles.err) + } + } + + return nil + }, +} + +func fetchUsers(client *HTTPClient, data *[]string) error { + req, err := client.NewRequest("GET", "user", nil) + if err != nil { + return err + } + + resp, err := client.client.Do(req) + if err != nil { + return err + } + + bytes, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode == 200 { + err = json.Unmarshal(bytes, data) + if err != nil { + return err + } + } else { + body := string(bytes) + return fmt.Errorf("request failed\nstatus code: %s\nresponse: %s", resp.Status, body) + } + + return nil +} + +func fetchUserRoles(client *HTTPClient, user string) (res FetchUserRoleRes) { + req, err := client.NewRequest("GET", fmt.Sprintf("user/%s/role", user), nil) + if err != nil { + return + } + resp, err := client.client.Do(req) + if err != nil { + return + } + body, err := io.ReadAll(resp.Body) + if err != nil { + return + } + defer resp.Body.Close() + + res.err = json.Unmarshal(body, &res.data) + return +} diff --git a/cmd/version.go b/cmd/version.go index f4d407a..8c866f1 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -6,6 +6,7 @@ import ( "github.com/spf13/cobra" ) +// VersionCmd is the command for printing version information var VersionCmd = &cobra.Command{ Use: "version", Short: "Print version", @@ -13,6 +14,7 @@ var VersionCmd = &cobra.Command{ Example: " pb version", } +// PrintVersion prints version information func PrintVersion(version string, commit string) { fmt.Printf("\n%s \n\n", standardStyleAlt.Render("pb version")) fmt.Printf("%s %s\n", standardStyleBold.Render("version: "), version) diff --git a/main.go b/main.go index cc64bcd..f645479 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,5 @@ // Copyright (c) 2023 Cloudnatively Services Pvt Ltd // -// This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by @@ -20,10 +19,11 @@ package main import ( "fmt" "os" + "strconv" + "pb/cmd" "pb/pkg/config" "pb/pkg/model" - "strconv" tea "github.com/charmbracelet/bubbletea" "github.com/spf13/cobra" @@ -45,7 +45,7 @@ var ( func DefaultInitialProfile() config.Profile { return config.Profile{ - Url: "https://demo.parseable.io", + URL: "https://demo.parseable.io", Username: "admin", Password: "admin", } @@ -136,7 +136,7 @@ func main() { cmd.PrintVersion(PBVersion, PBCommit) } cli.AddCommand(cmd.VersionCmd) - //set as flag + // set as flag cli.Flags().BoolP(versionFlag, versionFlagShort, false, "Print version") cli.CompletionOptions.HiddenDefaultCmd = true @@ -144,8 +144,8 @@ func main() { // create a default profile if file does not exist if _, err := config.ReadConfigFromFile(); os.IsNotExist(err) { conf := config.Config{ - Profiles: map[string]config.Profile{"demo": DefaultInitialProfile()}, - Default_profile: "demo", + Profiles: map[string]config.Profile{"demo": DefaultInitialProfile()}, + DefaultProfile: "demo", } config.WriteConfigToFile(&conf) } diff --git a/pkg/config/config.go b/pkg/config/config.go index 2174491..2af72f0 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1,6 +1,5 @@ // Copyright (c) 2023 Cloudnatively Services Pvt Ltd // -// This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by @@ -26,8 +25,8 @@ import ( ) var ( - ConfigFilename = "config.toml" - ConfigAppName = "parseable" + configFilename = "config.toml" + configAppName = "parseable" ) func ConfigPath() (string, error) { @@ -35,20 +34,23 @@ func ConfigPath() (string, error) { if err != nil { return "", err } - return path.Join(dir, ConfigAppName, ConfigFilename), nil + return path.Join(dir, configAppName, configFilename), nil } +// Config is the struct that holds the configuration type Config struct { - Profiles map[string]Profile - Default_profile string + Profiles map[string]Profile + DefaultProfile string } +// Profile is the struct that holds the profile configuration type Profile struct { - Url string + URL string Username string Password string } +// WriteConfigToFile writes the configuration to the config file func WriteConfigToFile(config *Config) error { tomlData, _ := toml.Marshal(config) filePath, err := ConfigPath() @@ -76,6 +78,7 @@ func WriteConfigToFile(config *Config) error { return err } +// ReadConfigFromFile reads the configuration from the config file func ReadConfigFromFile() (config *Config, err error) { filePath, err := ConfigPath() if err != nil { diff --git a/pkg/model/button/button.go b/pkg/model/button/button.go index 14ac65b..59adab3 100644 --- a/pkg/model/button/button.go +++ b/pkg/model/button/button.go @@ -1,3 +1,18 @@ +// Copyright (c) 2023 Cloudnatively Services Pvt Ltd +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + package button import ( @@ -7,8 +22,10 @@ import ( "github.com/charmbracelet/lipgloss" ) +// Pressed is a flag that is enabled when the button is pressed. type Pressed bool +// Model is the model for a button. type Model struct { text string FocusStyle lipgloss.Style @@ -17,6 +34,7 @@ type Model struct { Invalid bool } +// New returns a new button model. func New(text string) Model { return Model{ text: text, @@ -25,23 +43,28 @@ func New(text string) Model { } } +// Focus sets the focus flag to true. func (m *Model) Focus() tea.Cmd { m.focus = true return nil } +// Blur sets the focus flag to false. func (m *Model) Blur() { m.focus = false } +// Focused returns true if the button is focused. func (m *Model) Focused() bool { return m.focus } +// Init initializes the button. func (m Model) Init() tea.Cmd { return nil } +// Update updates the button. func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { if !m.focus { return m, nil @@ -53,9 +76,8 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { case tea.KeyEnter: if m.Invalid { return m, nil - } else { - return m, func() tea.Msg { return Pressed(true) } } + return m, func() tea.Msg { return Pressed(true) } default: return m, nil } @@ -64,6 +86,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { return m, nil } +// View renders the button. func (m Model) View() string { var b strings.Builder var text string diff --git a/pkg/model/credential/credential.go b/pkg/model/credential/credential.go index 17f0cc2..8dcbc45 100644 --- a/pkg/model/credential/credential.go +++ b/pkg/model/credential/credential.go @@ -1,6 +1,5 @@ // Copyright (c) 2023 Cloudnatively Services Pvt Ltd // -// This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by @@ -18,23 +17,25 @@ package credential import ( - "pb/pkg/model/button" "strings" + "pb/pkg/model/button" + "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) var ( - FocusPrimary = lipgloss.AdaptiveColor{Light: "16", Dark: "226"} - FocusSecondry = lipgloss.AdaptiveColor{Light: "18", Dark: "220"} + // FocusPrimary is the color used for the focused + FocusPrimary = lipgloss.AdaptiveColor{Light: "16", Dark: "226"} + FocusSecondary = lipgloss.AdaptiveColor{Light: "18", Dark: "220"} - StandardPrimary = lipgloss.AdaptiveColor{Light: "235", Dark: "255"} - StandardSecondry = lipgloss.AdaptiveColor{Light: "238", Dark: "254"} + StandardPrimary = lipgloss.AdaptiveColor{Light: "235", Dark: "255"} + StandardSecondary = lipgloss.AdaptiveColor{Light: "238", Dark: "254"} focusedStyle = lipgloss.NewStyle().Foreground(FocusPrimary) - blurredStyle = lipgloss.NewStyle().Foreground(StandardSecondry) + blurredStyle = lipgloss.NewStyle().Foreground(StandardSecondary) noStyle = lipgloss.NewStyle() ) diff --git a/pkg/model/datetime/datetime.go b/pkg/model/datetime/datetime.go index e962f7c..2860ad3 100644 --- a/pkg/model/datetime/datetime.go +++ b/pkg/model/datetime/datetime.go @@ -9,28 +9,34 @@ import ( tea "github.com/charmbracelet/bubbletea" ) +// Model is the model for the datetime component type Model struct { time time.Time input textinput.Model } +// Value returns the current value of the datetime component func (m *Model) Value() string { return m.time.Format(time.RFC3339) } +// ValueUtc returns the current value of the datetime component in UTC func (m *Model) ValueUtc() string { return m.time.UTC().Format(time.RFC3339) } +// SetTime sets the value of the datetime component func (m *Model) SetTime(t time.Time) { m.time = t m.input.SetValue(m.time.Format(time.DateTime)) } +// Time returns the current time of the datetime component func (m *Model) Time() time.Time { return m.time } +// New creates a new datetime component func New(prompt string) Model { input := textinput.New() input.Width = 20 @@ -42,23 +48,28 @@ func New(prompt string) Model { } } +// Focus focuses the datetime component func (m *Model) Focus() tea.Cmd { m.input.Focus() return nil } +// Blur blurs the datetime component func (m *Model) Blur() { m.input.Blur() } +// Focused returns true if the datetime component is focused func (m *Model) Focused() bool { return m.input.Focused() } +// Init initializes the datetime component func (m Model) Init() tea.Cmd { return nil } +// Update updates the datetime component func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { var cmd tea.Cmd if !m.Focused() { @@ -96,6 +107,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { return m, nil } +// View returns the view of the datetime component func (m Model) View() string { return m.input.View() } diff --git a/pkg/model/defaultprofile/profile.go b/pkg/model/defaultprofile/profile.go index 379a9bb..f879f25 100644 --- a/pkg/model/defaultprofile/profile.go +++ b/pkg/model/defaultprofile/profile.go @@ -1,7 +1,5 @@ // Copyright (c) 2023 Cloudnatively Services Pvt Ltd // -// This file is part of MinIO Object Storage stack -// // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or @@ -20,6 +18,7 @@ package defaultprofile import ( "fmt" "io" + "pb/pkg/config" "github.com/charmbracelet/bubbles/list" @@ -28,18 +27,21 @@ import ( ) var ( - FocusPrimary = lipgloss.AdaptiveColor{Light: "16", Dark: "226"} + // FocusPrimary is the primary focus color + FocusPrimary = lipgloss.AdaptiveColor{Light: "16", Dark: "226"} + // FocusSecondry is the secondry focus color FocusSecondry = lipgloss.AdaptiveColor{Light: "18", Dark: "220"} - - StandardPrimary = lipgloss.AdaptiveColor{Light: "235", Dark: "255"} - StandardSecondry = lipgloss.AdaptiveColor{Light: "238", Dark: "254"} + // StandardPrimary is the primary standard color + StandardPrimary = lipgloss.AdaptiveColor{Light: "235", Dark: "255"} + // StandardSecondary is the secondary standard color + StandardSecondary = lipgloss.AdaptiveColor{Light: "238", Dark: "254"} focusTitleStyle = lipgloss.NewStyle().Foreground(FocusPrimary) focusDescStyle = lipgloss.NewStyle().Foreground(FocusSecondry) focusedOuterStyle = lipgloss.NewStyle().BorderStyle(lipgloss.NormalBorder()).BorderLeft(true).BorderForeground(FocusPrimary) standardTitleStyle = lipgloss.NewStyle().Foreground(StandardPrimary) - standardDescStyle = lipgloss.NewStyle().Foreground(StandardSecondry) + standardDescStyle = lipgloss.NewStyle().Foreground(StandardSecondary) ) type item struct { @@ -92,7 +94,7 @@ func New(profiles map[string]config.Profile) Model { for name, profile := range profiles { i := item{ title: name, - url: profile.Url, + url: profile.URL, user: profile.Username, } items = append(items, i) diff --git a/pkg/model/query.go b/pkg/model/query.go index 36f8649..986ad0d 100644 --- a/pkg/model/query.go +++ b/pkg/model/query.go @@ -1,6 +1,5 @@ // Copyright (c) 2023 Cloudnatively Services Pvt Ltd // -// This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by @@ -49,8 +48,8 @@ var ( FocusPrimary = lipgloss.AdaptiveColor{Light: "16", Dark: "226"} FocusSecondry = lipgloss.AdaptiveColor{Light: "18", Dark: "220"} - StandardPrimary = lipgloss.AdaptiveColor{Light: "235", Dark: "255"} - StandardSecondry = lipgloss.AdaptiveColor{Light: "238", Dark: "254"} + StandardPrimary = lipgloss.AdaptiveColor{Light: "235", Dark: "255"} + StandardSecondary = lipgloss.AdaptiveColor{Light: "238", Dark: "254"} borderedStyle = lipgloss.NewStyle(). Border(lipgloss.RoundedBorder(), true). @@ -94,8 +93,10 @@ var ( QueryNavigationMap = []string{"query", "time", "table"} ) -type Mode int -type FetchResult int +type ( + Mode int + FetchResult int +) type FetchData struct { status FetchResult @@ -118,7 +119,7 @@ type QueryModel struct { height int table table.Model query textarea.Model - timerange TimeInputModel + timeRange TimeInputModel profile config.Profile help help.Model status StatusBar @@ -164,7 +165,7 @@ func NewQueryModel(profile config.Profile, stream string, duration uint) QueryMo WithPageSize(30). WithBaseStyle(tableStyle). WithMissingDataIndicatorStyled(table.StyledCell{ - Style: lipgloss.NewStyle().Foreground(StandardSecondry), + Style: lipgloss.NewStyle().Foreground(StandardSecondary), Data: "╌", }).WithMaxTotalWidth(100) @@ -186,17 +187,17 @@ func NewQueryModel(profile config.Profile, stream string, duration uint) QueryMo height: h, table: table, query: query, - timerange: inputs, + timeRange: inputs, overlay: OverlayNone, profile: profile, help: help, - status: NewStatusBar(profile.Url, stream, w), + status: NewStatusBar(profile.URL, stream, w), } } func (m QueryModel) Init() tea.Cmd { // Just return `nil`, which means "no I/O right now, please." - return NewFetchTask(m.profile, m.query.Value(), m.timerange.StartValueUtc(), m.timerange.EndValueUtc()) + return NewFetchTask(m.profile, m.query.Value(), m.timeRange.StartValueUtc(), m.timeRange.EndValueUtc()) } func (m QueryModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { @@ -224,7 +225,7 @@ func (m QueryModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Is it a key press? case tea.KeyMsg: - // special behaviour on main page + // special behavior on main page if m.overlay == OverlayNone { if msg.Type == tea.KeyEnter && m.currentFocus() == "time" { m.overlay = OverlayInputs @@ -232,7 +233,7 @@ func (m QueryModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } if msg.Type == tea.KeyTab { - m.focused += 1 + m.focused++ if m.focused > len(QueryNavigationMap)-1 { m.focused = 0 } @@ -241,7 +242,7 @@ func (m QueryModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } - // special behaviour on time input page + // special behavior on time input page if m.overlay == OverlayInputs { if msg.Type == tea.KeyEnter { m.overlay = OverlayNone @@ -253,7 +254,7 @@ func (m QueryModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // common keybind if msg.Type == tea.KeyCtrlR { m.overlay = OverlayNone - return m, NewFetchTask(m.profile, m.query.Value(), m.timerange.StartValueUtc(), m.timerange.EndValueUtc()) + return m, NewFetchTask(m.profile, m.query.Value(), m.timeRange.StartValueUtc(), m.timeRange.EndValueUtc()) } switch msg.Type { @@ -271,7 +272,7 @@ func (m QueryModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } cmds = append(cmds, cmd) case OverlayInputs: - m.timerange, cmd = m.timerange.Update(msg) + m.timeRange, cmd = m.timeRange.Update(msg) cmds = append(cmds, cmd) } } @@ -280,7 +281,7 @@ func (m QueryModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m QueryModel) View() string { - var outer = lipgloss.NewStyle().Inherit(baseStyle). + outer := lipgloss.NewStyle().Inherit(baseStyle). UnsetMaxHeight().Width(m.width).Height(m.height) m.table = m.table.WithMaxTotalWidth(m.width - 2) @@ -294,8 +295,8 @@ func (m QueryModel) View() string { time := lipgloss.JoinVertical( lipgloss.Left, - fmt.Sprintf("%s %s ", baseBoldUnderlinedStyle.Render(" start "), m.timerange.start.Value()), - fmt.Sprintf("%s %s ", baseBoldUnderlinedStyle.Render(" end "), m.timerange.end.Value()), + fmt.Sprintf("%s %s ", baseBoldUnderlinedStyle.Render(" start "), m.timeRange.start.Value()), + fmt.Sprintf("%s %s ", baseBoldUnderlinedStyle.Render(" end "), m.timeRange.end.Value()), ) queryOuter, timeOuter := &borderedStyle, &borderedStyle @@ -322,14 +323,14 @@ func (m QueryModel) View() string { helpKeys = TextAreaHelpKeys{}.FullHelp() case "time": helpKeys = [][]key.Binding{ - {key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "select timerange"))}, + {key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "select timeRange"))}, } case "table": helpKeys = tableHelpBinds.FullHelp() } case OverlayInputs: - mainView = m.timerange.View() - helpKeys = m.timerange.FullHelp() + mainView = m.timeRange.View() + helpKeys = m.timeRange.FullHelp() } helpKeys = append(helpKeys, additionalKeyBinds) helpView = m.help.FullHelpView(helpKeys) @@ -389,7 +390,7 @@ func fetchData(client *http.Client, profile *config.Profile, query string, start final_query := fmt.Sprintf(query_template, query, start_time, end_time) - endpoint := fmt.Sprintf("%s/%s", profile.Url, "api/v1/query?fields=true") + endpoint := fmt.Sprintf("%s/%s", profile.URL, "api/v1/query?fields=true") req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer([]byte(final_query))) if err != nil { return @@ -421,7 +422,7 @@ func (m *QueryModel) UpdateTable(data FetchData) { if contains_timestamp { columns[0] = table.NewColumn(datetimeKey, datetimeKey, datetimeWidth) - columnIndex += 1 + columnIndex++ } if contains_tags { @@ -439,7 +440,7 @@ func (m *QueryModel) UpdateTable(data FetchData) { default: width := inferWidthForColumns(title, &data.data, 100, 100) + 1 columns[columnIndex] = table.NewColumn(title, title, width).WithFiltered(true) - columnIndex += 1 + columnIndex++ } } diff --git a/pkg/model/role/role.go b/pkg/model/role/role.go index 3ed6b2c..6eb07e8 100644 --- a/pkg/model/role/role.go +++ b/pkg/model/role/role.go @@ -1,6 +1,5 @@ // Copyright (c) 2023 Cloudnatively Services Pvt Ltd // -// This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by @@ -19,9 +18,10 @@ package role import ( "fmt" + "strings" + "pb/pkg/model/button" "pb/pkg/model/selection" - "strings" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" @@ -151,13 +151,13 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyCtrlC: return m, tea.Quit case tea.KeyDown, tea.KeyTab, tea.KeyEnter: - m.focusIndex += 1 + m.focusIndex++ if m.focusIndex >= len(*m.navMap) { m.focusIndex = 0 } m.FocusSelected() case tea.KeyUp, tea.KeyShiftTab: - m.focusIndex -= 1 + m.focusIndex-- if m.focusIndex < 0 { m.focusIndex = len(*m.navMap) - 1 } diff --git a/pkg/model/selection/selection.go b/pkg/model/selection/selection.go index 37e15f2..cad26df 100644 --- a/pkg/model/selection/selection.go +++ b/pkg/model/selection/selection.go @@ -1,6 +1,5 @@ // Copyright (c) 2023 Cloudnatively Services Pvt Ltd // -// This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by @@ -22,6 +21,7 @@ import ( "github.com/charmbracelet/lipgloss" ) +// Model is the model for the selection component type Model struct { items []string focusIndex int @@ -30,23 +30,28 @@ type Model struct { BlurredStyle lipgloss.Style } +// Focus focuses the selection component func (m *Model) Focus() tea.Cmd { m.focus = true return nil } +// Blur blurs the selection component func (m *Model) Blur() { m.focus = false } +// Focused returns true if the selection component is focused func (m *Model) Focused() bool { return m.focus } +// Value returns the value of the selection component func (m *Model) Value() string { return m.items[m.focusIndex] } +// New creates a new selection component func New(items []string) Model { m := Model{ focusIndex: 0, @@ -57,10 +62,12 @@ func New(items []string) Model { return m } +// Init initializes the selection component func (m Model) Init() tea.Cmd { return nil } +// Update updates the selection component func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { if !m.focus { return m, nil @@ -71,11 +78,11 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { switch msg.Type { case tea.KeyLeft: if m.focusIndex > 0 { - m.focusIndex -= 1 + m.focusIndex-- } case tea.KeyRight: if m.focusIndex < len(m.items)-1 { - m.focusIndex += 1 + m.focusIndex++ } } } @@ -83,6 +90,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { return m, nil } +// View renders the selection component func (m Model) View() string { render := make([]string, len(m.items)) diff --git a/pkg/model/status.go b/pkg/model/status.go index 93e5793..6b335d5 100644 --- a/pkg/model/status.go +++ b/pkg/model/status.go @@ -1,6 +1,5 @@ // Copyright (c) 2023 Cloudnatively Services Pvt Ltd // -// This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by diff --git a/pkg/model/timeinput.go b/pkg/model/timeinput.go index b687eb9..5c79e88 100644 --- a/pkg/model/timeinput.go +++ b/pkg/model/timeinput.go @@ -1,6 +1,5 @@ // Copyright (c) 2023 Cloudnatively Services Pvt Ltd // -// This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by @@ -19,9 +18,10 @@ package model import ( "fmt" - "pb/pkg/model/datetime" "time" + "pb/pkg/model/datetime" + "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" @@ -100,12 +100,12 @@ func (m *TimeInputModel) Navigate(key tea.KeyMsg) { if m.focus == 0 { m.focus = len(rangeNavigationMap) } - m.focus -= 1 + m.focus-- case "tab": if m.focus == len(rangeNavigationMap)-1 { m.focus = -1 } - m.focus += 1 + m.focus++ default: return } diff --git a/pkg/model/timerange.go b/pkg/model/timerange.go index 83a25a9..8753039 100644 --- a/pkg/model/timerange.go +++ b/pkg/model/timerange.go @@ -1,6 +1,5 @@ // Copyright (c) 2023 Cloudnatively Services Pvt Ltd // -// This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by @@ -51,7 +50,7 @@ var ( timeDurationItem{duration: OneWeek, repr: "1 Week"}, } - listItemRender = lipgloss.NewStyle().Foreground(StandardSecondry) + listItemRender = lipgloss.NewStyle().Foreground(StandardSecondary) listSelectedItemRender = lipgloss.NewStyle().Foreground(FocusPrimary) ) From 4e6790c8b8548b2eb8d15c43dd66d0c981bcf481 Mon Sep 17 00:00:00 2001 From: Satyam Singh Date: Fri, 18 Aug 2023 14:10:19 +0530 Subject: [PATCH 2/2] Fix lints --- .golangci.yml | 2 + cmd/client.go | 3 +- cmd/pre.go | 16 +++--- cmd/profile.go | 57 +++++++++---------- cmd/stream.go | 25 ++++---- cmd/user.go | 13 ++--- main.go | 15 +++-- pkg/config/config.go | 7 ++- pkg/model/credential/credential.go | 8 +-- pkg/model/defaultprofile/profile.go | 2 +- pkg/model/query.go | 88 ++++++++++++++--------------- pkg/model/role/role.go | 6 +- pkg/model/status.go | 14 ++--- pkg/model/timeinput.go | 20 +++---- pkg/model/timerange.go | 2 + 15 files changed, 135 insertions(+), 143 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 7f7b24b..8f2c364 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -31,6 +31,8 @@ issues: exclude: - instead of using struct literal - should have a package comment + - should have comment or be unexported + - time-naming - error strings should not be capitalized or end with punctuation or a newline service: diff --git a/cmd/client.go b/cmd/client.go index 9219299..1696c1e 100644 --- a/cmd/client.go +++ b/cmd/client.go @@ -20,9 +20,8 @@ import ( "io" "net/http" "net/url" - "time" - "pb/pkg/config" + "time" ) type HTTPClient struct { diff --git a/cmd/pre.go b/cmd/pre.go index 53bcf68..6b969b0 100644 --- a/cmd/pre.go +++ b/cmd/pre.go @@ -19,7 +19,6 @@ package cmd import ( "errors" "os" - "pb/pkg/config" "github.com/spf13/cobra" @@ -27,17 +26,16 @@ import ( var DefaultProfile config.Profile -// Check if a profile exists. +// PreRunDefaultProfile if a profile exists. // This is required by mostly all commands except profile -func PreRunDefaultProfile(cmd *cobra.Command, args []string) error { +func PreRunDefaultProfile(_ *cobra.Command, _ []string) error { conf, err := config.ReadConfigFromFile() - if err != nil { - if os.IsNotExist(err) { - return errors.New("no config found to run this command. add a profile using pb profile command") - } else { - return err - } + if os.IsNotExist(err) { + return errors.New("no config found to run this command. add a profile using pb profile command") + } else if err != nil { + return err } + if conf.Profiles == nil || conf.DefaultProfile == "" { return errors.New("no profile is configured to run this command. please create one using profile command") } diff --git a/cmd/profile.go b/cmd/profile.go index bbda3ea..f259b03 100644 --- a/cmd/profile.go +++ b/cmd/profile.go @@ -20,7 +20,6 @@ import ( "fmt" "net/url" "os" - "pb/pkg/config" "pb/pkg/model/credential" "pb/pkg/model/defaultprofile" @@ -64,10 +63,7 @@ var AddProfileCmd = &cobra.Command{ if err := cobra.MinimumNArgs(2)(cmd, args); err != nil { return err } - if err := cobra.MaximumNArgs(4)(cmd, args); err != nil { - return err - } - return nil + return cobra.MaximumNArgs(4)(cmd, args) }, RunE: func(cmd *cobra.Command, args []string) error { name := args[0] @@ -99,26 +95,26 @@ var AddProfileCmd = &cobra.Command{ Password: password, } - file_config, err := config.ReadConfigFromFile() + fileConfig, err := config.ReadConfigFromFile() if err != nil { // create new file - new_config := config.Config{ + newConfig := config.Config{ Profiles: map[string]config.Profile{ name: profile, }, DefaultProfile: name, } - err = config.WriteConfigToFile(&new_config) + err = config.WriteConfigToFile(&newConfig) return err } - if file_config.Profiles == nil { - file_config.Profiles = make(map[string]config.Profile) + if fileConfig.Profiles == nil { + fileConfig.Profiles = make(map[string]config.Profile) } - file_config.Profiles[name] = profile - if file_config.DefaultProfile == "" { - file_config.DefaultProfile = name + fileConfig.Profiles[name] = profile + if fileConfig.DefaultProfile == "" { + fileConfig.DefaultProfile = name } - config.WriteConfigToFile(file_config) + config.WriteConfigToFile(fileConfig) return nil }, @@ -132,18 +128,18 @@ var RemoveProfileCmd = &cobra.Command{ Short: "Delete a profile", RunE: func(cmd *cobra.Command, args []string) error { name := args[0] - file_config, err := config.ReadConfigFromFile() + fileConfig, err := config.ReadConfigFromFile() if err != nil { return nil } - _, exists := file_config.Profiles[name] + _, exists := fileConfig.Profiles[name] if exists { - delete(file_config.Profiles, name) - if len(file_config.Profiles) == 0 { - file_config.DefaultProfile = "" + delete(fileConfig.Profiles, name) + if len(fileConfig.Profiles) == 0 { + fileConfig.DefaultProfile = "" } - config.WriteConfigToFile(file_config) + config.WriteConfigToFile(fileConfig) fmt.Printf("Deleted profile %s\n", styleBold.Render(name)) } else { fmt.Printf("No profile found with the name: %s", styleBold.Render(name)) @@ -161,7 +157,7 @@ var DefaultProfileCmd = &cobra.Command{ RunE: func(cmd *cobra.Command, args []string) error { var name string - file_config, err := config.ReadConfigFromFile() + fileConfig, err := config.ReadConfigFromFile() if err != nil { return nil } @@ -169,7 +165,7 @@ var DefaultProfileCmd = &cobra.Command{ if len(args) > 0 { name = args[0] } else { - model := defaultprofile.New(file_config.Profiles) + model := defaultprofile.New(fileConfig.Profiles) _m, err := tea.NewProgram(model).Run() if err != nil { fmt.Printf("Alas, there's been an error: %v", err) @@ -184,16 +180,16 @@ var DefaultProfileCmd = &cobra.Command{ } } - _, exists := file_config.Profiles[name] + _, exists := fileConfig.Profiles[name] if exists { - file_config.DefaultProfile = name + fileConfig.DefaultProfile = name } else { name = lipgloss.NewStyle().Bold(true).Render(name) err := fmt.Sprintf("profile %s does not exist", styleBold.Render(name)) return errors.New(err) } - config.WriteConfigToFile(file_config) + config.WriteConfigToFile(fileConfig) fmt.Printf("%s is now set as default profile\n", styleBold.Render(name)) return nil }, @@ -204,19 +200,19 @@ var ListProfileCmd = &cobra.Command{ Short: "List all added profiles", Example: " pb profile list", RunE: func(cmd *cobra.Command, args []string) error { - file_config, err := config.ReadConfigFromFile() + fileConfig, err := config.ReadConfigFromFile() if err != nil { return nil } - if len(file_config.Profiles) != 0 { + if len(fileConfig.Profiles) != 0 { println() } row := 0 - for key, value := range file_config.Profiles { + for key, value := range fileConfig.Profiles { item := ProfileListItem{key, value.URL, value.Username} - fmt.Println(item.Render(file_config.DefaultProfile == key)) + fmt.Println(item.Render(fileConfig.DefaultProfile == key)) row++ fmt.Println() } @@ -227,7 +223,6 @@ var ListProfileCmd = &cobra.Command{ func Max(a int, b int) int { if a >= b { return a - } else { - return b } + return b } diff --git a/cmd/stream.go b/cmd/stream.go index ec824d3..5227677 100644 --- a/cmd/stream.go +++ b/cmd/stream.go @@ -132,9 +132,9 @@ var StatStreamCmd = &cobra.Command{ return err } - ingestion_count := stats.Ingestion.Count - ingestion_size, _ := strconv.Atoi(strings.TrimRight(stats.Ingestion.Size, " Bytes")) - storage_size, _ := strconv.Atoi(strings.TrimRight(stats.Storage.Size, " Bytes")) + ingestionCount := stats.Ingestion.Count + ingestionSize, _ := strconv.Atoi(strings.TrimRight(stats.Ingestion.Size, " Bytes")) + storageSize, _ := strconv.Atoi(strings.TrimRight(stats.Storage.Size, " Bytes")) retention, err := fetchRetention(&client, name) if err != nil { @@ -144,12 +144,12 @@ var StatStreamCmd = &cobra.Command{ isRententionSet := len(retention) > 0 fmt.Println(styleBold.Render("Info:")) - fmt.Printf(" Event Count: %d\n", ingestion_count) - fmt.Printf(" Ingestion Size: %s\n", humanize.Bytes(uint64(ingestion_size))) - fmt.Printf(" Storage Size: %s\n", humanize.Bytes(uint64(storage_size))) + fmt.Printf(" Event Count: %d\n", ingestionCount) + fmt.Printf(" Ingestion Size: %s\n", humanize.Bytes(uint64(ingestionSize))) + fmt.Printf(" Storage Size: %s\n", humanize.Bytes(uint64(storageSize))) fmt.Printf( " Compression Ratio: %.2f%s\n", - 100-(float64(storage_size)/float64(ingestion_size))*100, "%") + 100-(float64(storageSize)/float64(ingestionSize))*100, "%") fmt.Println() if isRententionSet { @@ -163,11 +163,11 @@ var StatStreamCmd = &cobra.Command{ fmt.Println(styleBold.Render("No retention period set on stream\n")) } - alerts_data, err := fetchAlerts(&client, name) + alertsData, err := fetchAlerts(&client, name) if err != nil { return err } - alerts := alerts_data.Alerts + alerts := alertsData.Alerts isAlertsSet := len(alerts) > 0 @@ -175,14 +175,14 @@ var StatStreamCmd = &cobra.Command{ fmt.Println(styleBold.Render("Alerts:")) for _, alert := range alerts { fmt.Printf(" Alert: %s\n", styleBold.Render(alert.Name)) - rule_fmt := fmt.Sprintf( + ruleFmt := fmt.Sprintf( "%s %s %s repeated %d times", alert.Rule.Config.Column, alert.Rule.Config.Operator, fmt.Sprint(alert.Rule.Config.Value), alert.Rule.Config.Repeats, ) - fmt.Printf(" Rule: %s\n", rule_fmt) + fmt.Printf(" Rule: %s\n", ruleFmt) fmt.Printf(" Targets: ") for _, target := range alert.Targets { fmt.Printf("%s, ", target.Type) @@ -291,7 +291,6 @@ func fetchStats(client *HTTPClient, name string) (data StreamStatsData, err erro if resp.StatusCode == 200 { err = json.Unmarshal(bytes, &data) - return } else { body := string(bytes) body = fmt.Sprintf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, body) @@ -319,7 +318,6 @@ func fetchRetention(client *HTTPClient, name string) (data StreamRetentionData, if resp.StatusCode == 200 { err = json.Unmarshal(bytes, &data) - return } else { body := string(bytes) body = fmt.Sprintf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, body) @@ -347,7 +345,6 @@ func fetchAlerts(client *HTTPClient, name string) (data StreamAlertData, err err if resp.StatusCode == 200 { err = json.Unmarshal(bytes, &data) - return } else { body := string(bytes) body = fmt.Sprintf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, body) diff --git a/cmd/user.go b/cmd/user.go index 287eb2e..ec1afe7 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -21,11 +21,10 @@ import ( "fmt" "io" "os" + "pb/pkg/model/role" "strings" "sync" - "pb/pkg/model/role" - tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/spf13/cobra" @@ -123,8 +122,8 @@ var AddUserCmd = &cobra.Command{ roleData.Resource.Tag = tag } } - roleDataJson, _ := json.Marshal([]UserRoleData{roleData}) - putBody = bytes.NewBuffer(roleDataJson) + roleDataJSON, _ := json.Marshal([]UserRoleData{roleData}) + putBody = bytes.NewBuffer(roleDataJSON) } req, err := client.NewRequest("PUT", "user/"+name, putBody) if err != nil { @@ -201,7 +200,7 @@ var ListUserCmd = &cobra.Command{ return err } - role_responses := make([]FetchUserRoleRes, len(users)) + roleResponses := make([]FetchUserRoleRes, len(users)) wsg := sync.WaitGroup{} wsg.Add(len(users)) @@ -210,7 +209,7 @@ var ListUserCmd = &cobra.Command{ user := user client := &client go func() { - role_responses[idx] = fetchUserRoles(client, user) + roleResponses[idx] = fetchUserRoles(client, user) wsg.Done() }() } @@ -218,7 +217,7 @@ var ListUserCmd = &cobra.Command{ wsg.Wait() fmt.Println() for idx, user := range users { - roles := role_responses[idx] + roles := roleResponses[idx] fmt.Print("• ") fmt.Println(standardStyleBold.Bold(true).Render(user)) if roles.err == nil { diff --git a/main.go b/main.go index f645479..913b41a 100644 --- a/main.go +++ b/main.go @@ -19,11 +19,10 @@ package main import ( "fmt" "os" - "strconv" - "pb/cmd" "pb/pkg/config" "pb/pkg/model" + "strconv" tea "github.com/charmbracelet/bubbletea" "github.com/spf13/cobra" @@ -31,8 +30,8 @@ import ( var ( // populated at build time - PBVersion string - PBCommit string + version string + commit string ) var ( @@ -43,7 +42,7 @@ var ( defaultDuration = "10" ) -func DefaultInitialProfile() config.Profile { +func defaultInitialProfile() config.Profile { return config.Profile{ URL: "https://demo.parseable.io", Username: "admin", @@ -58,7 +57,7 @@ var cli = &cobra.Command{ Long: "\npb is a command line tool for Parseable", Run: func(command *cobra.Command, args []string) { if p, _ := command.Flags().GetBool(versionFlag); p { - cmd.PrintVersion(PBVersion, PBCommit) + cmd.PrintVersion(version, commit) } }, } @@ -133,7 +132,7 @@ func main() { // Set as command cmd.VersionCmd.Run = func(_ *cobra.Command, args []string) { - cmd.PrintVersion(PBVersion, PBCommit) + cmd.PrintVersion(version, commit) } cli.AddCommand(cmd.VersionCmd) // set as flag @@ -144,7 +143,7 @@ func main() { // create a default profile if file does not exist if _, err := config.ReadConfigFromFile(); os.IsNotExist(err) { conf := config.Config{ - Profiles: map[string]config.Profile{"demo": DefaultInitialProfile()}, + Profiles: map[string]config.Profile{"demo": defaultInitialProfile()}, DefaultProfile: "demo", } config.WriteConfigToFile(&conf) diff --git a/pkg/config/config.go b/pkg/config/config.go index 2af72f0..b90ec0c 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -29,7 +29,8 @@ var ( configAppName = "parseable" ) -func ConfigPath() (string, error) { +// Path returns user directory that can be used for the config file +func Path() (string, error) { dir, err := os.UserConfigDir() if err != nil { return "", err @@ -53,7 +54,7 @@ type Profile struct { // WriteConfigToFile writes the configuration to the config file func WriteConfigToFile(config *Config) error { tomlData, _ := toml.Marshal(config) - filePath, err := ConfigPath() + filePath, err := Path() if err != nil { return err } @@ -80,7 +81,7 @@ func WriteConfigToFile(config *Config) error { // ReadConfigFromFile reads the configuration from the config file func ReadConfigFromFile() (config *Config, err error) { - filePath, err := ConfigPath() + filePath, err := Path() if err != nil { return } diff --git a/pkg/model/credential/credential.go b/pkg/model/credential/credential.go index 8dcbc45..a601a2e 100644 --- a/pkg/model/credential/credential.go +++ b/pkg/model/credential/credential.go @@ -17,17 +17,16 @@ package credential import ( - "strings" - "pb/pkg/model/button" + "strings" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) +// Default Style for this widget var ( - // FocusPrimary is the color used for the focused FocusPrimary = lipgloss.AdaptiveColor{Light: "16", Dark: "226"} FocusSecondary = lipgloss.AdaptiveColor{Light: "18", Dark: "220"} @@ -48,9 +47,8 @@ type Model struct { func (m *Model) Values() (string, string) { if validInputs(&m.inputs) { return m.inputs[0].Value(), m.inputs[1].Value() - } else { - return "", "" } + return "", "" } func validInputs(inputs *[]textinput.Model) bool { diff --git a/pkg/model/defaultprofile/profile.go b/pkg/model/defaultprofile/profile.go index f879f25..0106806 100644 --- a/pkg/model/defaultprofile/profile.go +++ b/pkg/model/defaultprofile/profile.go @@ -18,7 +18,6 @@ package defaultprofile import ( "fmt" "io" - "pb/pkg/config" "github.com/charmbracelet/bubbles/list" @@ -83,6 +82,7 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list fmt.Fprint(w, render) } +// Model for profile selection command type Model struct { list list.Model Choice string diff --git a/pkg/model/query.go b/pkg/model/query.go index 986ad0d..82d417a 100644 --- a/pkg/model/query.go +++ b/pkg/model/query.go @@ -23,9 +23,8 @@ import ( "math" "net/http" "os" - "time" - "pb/pkg/config" + "time" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" @@ -44,6 +43,7 @@ const ( metadataKey = "p_metadata" ) +// Style for this widget var ( FocusPrimary = lipgloss.AdaptiveColor{Light: "16", Dark: "226"} FocusSecondry = lipgloss.AdaptiveColor{Light: "18", Dark: "220"} @@ -65,7 +65,9 @@ var ( baseBoldUnderlinedStyle = lipgloss.NewStyle().BorderForeground(StandardPrimary).Bold(true) headerStyle = lipgloss.NewStyle().Inherit(baseStyle).Foreground(FocusSecondry).Bold(true) tableStyle = lipgloss.NewStyle().Inherit(baseStyle).Align(lipgloss.Left) +) +var ( customBorder = table.Border{ Top: "─", Left: "│", @@ -105,13 +107,13 @@ type FetchData struct { } const ( - FetchOk FetchResult = iota - FetchErr + fetchOk FetchResult = iota + fetchErr ) const ( - OverlayNone uint = iota - OverlayInputs + overlayNone uint = iota + overlayInputs ) type QueryModel struct { @@ -188,7 +190,7 @@ func NewQueryModel(profile config.Profile, stream string, duration uint) QueryMo table: table, query: query, timeRange: inputs, - overlay: OverlayNone, + overlay: overlayNone, profile: profile, help: help, status: NewStatusBar(profile.URL, stream, w), @@ -216,7 +218,7 @@ func (m QueryModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil case FetchData: - if msg.status == FetchOk { + if msg.status == fetchOk { m.UpdateTable(msg) } else { m.status.Error = "failed to query" @@ -226,9 +228,9 @@ func (m QueryModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Is it a key press? case tea.KeyMsg: // special behavior on main page - if m.overlay == OverlayNone { + if m.overlay == overlayNone { if msg.Type == tea.KeyEnter && m.currentFocus() == "time" { - m.overlay = OverlayInputs + m.overlay = overlayInputs return m, nil } @@ -243,9 +245,9 @@ func (m QueryModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // special behavior on time input page - if m.overlay == OverlayInputs { + if m.overlay == overlayInputs { if msg.Type == tea.KeyEnter { - m.overlay = OverlayNone + m.overlay = overlayNone m.focusSelected() return m, nil } @@ -253,7 +255,7 @@ func (m QueryModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // common keybind if msg.Type == tea.KeyCtrlR { - m.overlay = OverlayNone + m.overlay = overlayNone return m, NewFetchTask(m.profile, m.query.Value(), m.timeRange.StartValueUtc(), m.timeRange.EndValueUtc()) } @@ -263,7 +265,7 @@ func (m QueryModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Quit default: switch m.overlay { - case OverlayNone: + case overlayNone: switch m.currentFocus() { case "query": m.query, cmd = m.query.Update(msg) @@ -271,7 +273,7 @@ func (m QueryModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.table, cmd = m.table.Update(msg) } cmds = append(cmds, cmd) - case OverlayInputs: + case overlayInputs: m.timeRange, cmd = m.timeRange.Update(msg) cmds = append(cmds, cmd) } @@ -313,7 +315,7 @@ func (m QueryModel) View() string { } switch m.overlay { - case OverlayNone: + case overlayNone: mainView = lipgloss.JoinVertical(lipgloss.Left, lipgloss.JoinHorizontal(lipgloss.Top, queryOuter.Render(m.query.View()), timeOuter.Render(time)), tableOuter.Render(m.table.View()), @@ -328,7 +330,7 @@ func (m QueryModel) View() string { case "table": helpKeys = tableHelpBinds.FullHelp() } - case OverlayInputs: + case overlayInputs: mainView = m.timeRange.View() helpKeys = m.timeRange.FullHelp() } @@ -351,10 +353,10 @@ type QueryData struct { Records []map[string]interface{} `json:"records"` } -func NewFetchTask(profile config.Profile, query string, start_time string, end_time string) func() tea.Msg { +func NewFetchTask(profile config.Profile, query string, startTime string, endTime string) func() tea.Msg { return func() tea.Msg { res := FetchData{ - status: FetchErr, + status: fetchErr, schema: []string{}, data: []map[string]interface{}{}, } @@ -363,35 +365,33 @@ func NewFetchTask(profile config.Profile, query string, start_time string, end_t Timeout: time.Second * 50, } - data, status := fetchData(client, &profile, query, start_time, end_time) - if status == FetchErr { - return res - } else { + data, status := fetchData(client, &profile, query, startTime, endTime) + + if status == fetchOk { res.data = data.Records res.schema = data.Fields + res.status = fetchOk } - res.status = FetchOk - return res } } -func fetchData(client *http.Client, profile *config.Profile, query string, start_time string, end_time string) (data QueryData, res FetchResult) { +func fetchData(client *http.Client, profile *config.Profile, query string, startTime string, endTime string) (data QueryData, res FetchResult) { data = QueryData{} - res = FetchErr + res = fetchErr - query_template := `{ + queryTemplate := `{ "query": "%s", "startTime": "%s", "endTime": "%s" } ` - final_query := fmt.Sprintf(query_template, query, start_time, end_time) + finalQuery := fmt.Sprintf(queryTemplate, query, startTime, endTime) endpoint := fmt.Sprintf("%s/%s", profile.URL, "api/v1/query?fields=true") - req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer([]byte(final_query))) + req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer([]byte(finalQuery))) if err != nil { return } @@ -408,28 +408,28 @@ func fetchData(client *http.Client, profile *config.Profile, query string, start return } - res = FetchOk + res = fetchOk return } func (m *QueryModel) UpdateTable(data FetchData) { // pin p_timestamp to left if available - contains_timestamp := slices.Contains(data.schema, datetimeKey) - contains_tags := slices.Contains(data.schema, tagKey) - contains_metadata := slices.Contains(data.schema, metadataKey) + containsTimestamp := slices.Contains(data.schema, datetimeKey) + containsTags := slices.Contains(data.schema, tagKey) + containsMetadata := slices.Contains(data.schema, metadataKey) columns := make([]table.Column, len(data.schema)) columnIndex := 0 - if contains_timestamp { + if containsTimestamp { columns[0] = table.NewColumn(datetimeKey, datetimeKey, datetimeWidth) columnIndex++ } - if contains_tags { + if containsTags { columns[len(columns)-2] = table.NewColumn(tagKey, tagKey, inferWidthForColumns(tagKey, &data.data, 100, 80)).WithFiltered(true) } - if contains_metadata { + if containsMetadata { columns[len(columns)-1] = table.NewColumn(metadataKey, metadataKey, inferWidthForColumns(metadataKey, &data.data, 100, 80)).WithFiltered(true) } @@ -446,22 +446,22 @@ func (m *QueryModel) UpdateTable(data FetchData) { rows := make([]table.Row, len(data.data)) for i := 0; i < len(data.data); i++ { - row_json := data.data[i] - rows[i] = table.NewRow(row_json) + rowJSON := data.data[i] + rows[i] = table.NewRow(rowJSON) } m.table = m.table.WithColumns(columns) m.table = m.table.WithRows(rows) } -func inferWidthForColumns(column string, data *[]map[string]interface{}, max_records int, max_width int) (width int) { +func inferWidthForColumns(column string, data *[]map[string]interface{}, maxRecords int, maxWidth int) (width int) { width = 2 records := 0 - if len(*data) < max_records { + if len(*data) < maxRecords { records = len(*data) } else { - records = max_records + records = maxRecords } for i := 0; i < records; i++ { @@ -477,10 +477,10 @@ func inferWidthForColumns(column string, data *[]map[string]interface{}, max_rec } if w > width { - if w < max_width { + if w < maxWidth { width = w } else { - width = max_width + width = maxWidth return } } diff --git a/pkg/model/role/role.go b/pkg/model/role/role.go index 6eb07e8..332e4ff 100644 --- a/pkg/model/role/role.go +++ b/pkg/model/role/role.go @@ -18,10 +18,9 @@ package role import ( "fmt" - "strings" - "pb/pkg/model/button" "pb/pkg/model/selection" + "strings" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" @@ -34,7 +33,10 @@ var ( navigationMapStream = []string{"role", "stream", "button"} navigationMap = []string{"role", "button"} navigationMapNone = []string{"role"} +) +// Style for role selection widget +var ( FocusPrimary = lipgloss.AdaptiveColor{Light: "16", Dark: "226"} FocusSecondry = lipgloss.AdaptiveColor{Light: "18", Dark: "220"} diff --git a/pkg/model/status.go b/pkg/model/status.go index 6b335d5..ec37e40 100644 --- a/pkg/model/status.go +++ b/pkg/model/status.go @@ -69,28 +69,28 @@ func (m StatusBar) Init() tea.Cmd { return nil } -func (m StatusBar) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m StatusBar) Update(_ tea.Msg) (tea.Model, tea.Cmd) { return m, nil } func (m StatusBar) View() string { var right string - var right_style lipgloss.Style + var rightStyle lipgloss.Style if m.Error != "" { right = m.Error - right_style = errorStyle + rightStyle = errorStyle } else { right = m.Info - right_style = infoStyle + rightStyle = infoStyle } left := lipgloss.JoinHorizontal(lipgloss.Bottom, titleStyle.Render(m.title), hostStyle.Render(m.host), streamStyle.Render(m.stream)) - left_width := lipgloss.Width(left) - right_width := m.width - left_width + leftWidth := lipgloss.Width(left) + rightWidth := m.width - leftWidth - right = right_style.Width(right_width).Render(right) + right = rightStyle.Width(rightWidth).Render(right) return lipgloss.JoinHorizontal(lipgloss.Bottom, left, right) } diff --git a/pkg/model/timeinput.go b/pkg/model/timeinput.go index 5c79e88..e68629b 100644 --- a/pkg/model/timeinput.go +++ b/pkg/model/timeinput.go @@ -18,9 +18,8 @@ package model import ( "fmt" - "time" - "pb/pkg/model/datetime" + "time" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" @@ -32,23 +31,23 @@ var rangeNavigationMap = []string{ "list", "start", "end", } -type EndTimeKeyBind struct { +type endTimeKeyBind struct { ResetTime key.Binding Ok key.Binding } -func (k EndTimeKeyBind) ShortHelp() []key.Binding { +func (k endTimeKeyBind) ShortHelp() []key.Binding { return []key.Binding{k.ResetTime, k.Ok} } -func (k EndTimeKeyBind) FullHelp() [][]key.Binding { +func (k endTimeKeyBind) FullHelp() [][]key.Binding { return [][]key.Binding{ {k.ResetTime}, {k.Ok}, } } -var EndHelpBinds = EndTimeKeyBind{ +var endHelpBinds = endTimeKeyBind{ ResetTime: key.NewBinding( key.WithKeys("ctrl+{"), key.WithHelp("ctrl+{", "change end time to current time"), @@ -115,6 +114,7 @@ func (m *TimeInputModel) currentFocus() string { return rangeNavigationMap[m.focus] } +// NewTimeInputModel creates a new model func NewTimeInputModel(duration uint) TimeInputModel { endTime := time.Now() startTime := endTime.Add(TenMinute) @@ -124,12 +124,12 @@ func NewTimeInputModel(duration uint) TimeInputModel { } list := NewTimeRangeModel() - input_style := lipgloss.NewStyle().Inherit(baseStyle).Bold(true).Width(6).Align(lipgloss.Center) + inputStyle := lipgloss.NewStyle().Inherit(baseStyle).Bold(true).Width(6).Align(lipgloss.Center) - start := datetime.New(input_style.Render("start")) + start := datetime.New(inputStyle.Render("start")) start.SetTime(startTime) start.Focus() - end := datetime.New(input_style.Render("end")) + end := datetime.New(inputStyle.Render("end")) end.SetTime(endTime) return TimeInputModel{ @@ -141,7 +141,7 @@ func NewTimeInputModel(duration uint) TimeInputModel { } func (m TimeInputModel) FullHelp() [][]key.Binding { - return EndHelpBinds.FullHelp() + return endHelpBinds.FullHelp() } func (m TimeInputModel) Init() tea.Cmd { diff --git a/pkg/model/timerange.go b/pkg/model/timerange.go index 8753039..2fb7e70 100644 --- a/pkg/model/timerange.go +++ b/pkg/model/timerange.go @@ -27,6 +27,7 @@ import ( "github.com/charmbracelet/lipgloss" ) +// Items for time range const ( TenMinute = -10 * time.Minute TwentyMinute = -20 * time.Minute @@ -82,6 +83,7 @@ func (d timeDurationItemDelegate) Render(w io.Writer, m list.Model, index int, l fmt.Fprint(w, fn(i.repr)) } +// NewTimeRangeModel creates new range model func NewTimeRangeModel() list.Model { list := list.New(timeDurations, timeDurationItemDelegate{}, 20, 10) list.SetShowPagination(false)