diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index a6d4ee2..5c95d23 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -14,7 +14,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: '1.22' + go-version: '1.22.4' - name: make verification run: make verifiers diff --git a/.golangci.yml b/.golangci.yml index eb57b6a..801b4a9 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -32,6 +32,3 @@ issues: - should have comment or be unexported - time-naming - 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/cmd/autocomplete.go b/cmd/autocomplete.go index 9e77748..2596a96 100644 --- a/cmd/autocomplete.go +++ b/cmd/autocomplete.go @@ -1,4 +1,3 @@ - // Copyright (c) 2024 Parseable, Inc // // @@ -18,35 +17,35 @@ package cmd import ( - "fmt" - "os" + "fmt" + "os" - "github.com/spf13/cobra" + "github.com/spf13/cobra" ) // AutocompleteCmd represents the autocomplete command var AutocompleteCmd = &cobra.Command{ - Use: "autocomplete [bash|zsh|powershell]", - Short: "Generate autocomplete script", - Long: `Generate autocomplete script for bash, zsh, or powershell`, - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - var err error - switch args[0] { - case "bash": - err = cmd.Root().GenBashCompletion(os.Stdout) - case "zsh": - err = cmd.Root().GenZshCompletion(os.Stdout) - case "powershell": - err = cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) - default: - err = fmt.Errorf("unsupported shell type: %s. Only bash, zsh, and powershell are supported", args[0]) - } + Use: "autocomplete [bash|zsh|powershell]", + Short: "Generate autocomplete script", + Long: `Generate autocomplete script for bash, zsh, or powershell`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + var err error + switch args[0] { + case "bash": + err = cmd.Root().GenBashCompletion(os.Stdout) + case "zsh": + err = cmd.Root().GenZshCompletion(os.Stdout) + case "powershell": + err = cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) + default: + err = fmt.Errorf("unsupported shell type: %s. Only bash, zsh, and powershell are supported", args[0]) + } - if err != nil { - return fmt.Errorf("error generating autocomplete script: %w", err) - } + if err != nil { + return fmt.Errorf("error generating autocomplete script: %w", err) + } - return nil - }, + return nil + }, } diff --git a/cmd/filterList.go b/cmd/filterList.go new file mode 100644 index 0000000..7563e97 --- /dev/null +++ b/cmd/filterList.go @@ -0,0 +1,142 @@ +// Copyright (c) 2024 Parseable, Inc +// +// 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 ( + "fmt" + "os" + "pb/pkg/model" + "strings" + "time" + + "github.com/spf13/cobra" +) + +var FilterList = &cobra.Command{ + Use: "list", + Example: "pb query list ", + Short: "List of saved filter for a stream", + Long: "\nShow a list of saved filter for a stream ", + PreRunE: PreRunDefaultProfile, + Run: func(_ *cobra.Command, _ []string) { + client := DefaultClient() + + p := model.UIApp() + _, err := p.Run() + if err != nil { + os.Exit(1) + } + + a := model.FilterToApply() + d := model.FilterToDelete() + if a.Stream() != "" { + filterToPbQuery(a.Stream(), a.StartTime(), a.EndTime()) + } + if d.FilterID() != "" { + deleteFilter(&client, d.FilterID()) + } + }, +} + +// Delete a saved filter from the list of filter +func deleteFilter(client *HTTPClient, filterID string) { + deleteURL := `filters/filter/` + filterID + req, err := client.NewRequest("DELETE", deleteURL, nil) + if err != nil { + fmt.Println("Error deleting the filter") + } + + resp, err := client.client.Do(req) + if err != nil { + return + } + defer resp.Body.Close() + + if resp.StatusCode == 200 { + fmt.Printf("\n\nFilter Deleted") + } +} + +// Convert a filter to executable pb query +func filterToPbQuery(query string, start string, end string) { + var timeStamps string + if start == "" || end == "" { + timeStamps = `` + } else { + startFormatted := formatToRFC3339(start) + endFormatted := formatToRFC3339(end) + timeStamps = ` --from=` + startFormatted + ` --to=` + endFormatted + } + queryTemplate := `pb query run ` + query + timeStamps + fmt.Printf("\nCopy and paste the command") + fmt.Printf("\n\n%s\n\n", queryTemplate) +} + +// Parses all UTC time format from string to time interface +func parseTimeToFormat(input string) (time.Time, error) { + // List of possible formats + formats := []string{ + time.RFC3339, + "2006-01-02 15:04:05", + "2006-01-02", + "01/02/2006 15:04:05", + "02-Jan-2006 15:04:05 MST", + "2006-01-02T15:04:05Z", + "02-Jan-2006", + } + + var err error + var t time.Time + + for _, format := range formats { + t, err = time.Parse(format, input) + if err == nil { + return t, nil + } + } + + return t, fmt.Errorf("unable to parse time: %s", input) +} + +// Converts to RFC3339 +func convertTime(input string) (string, error) { + t, err := parseTimeToFormat(input) + if err != nil { + return "", err + } + + return t.Format(time.RFC3339), nil +} + +// Converts User inputted time to string type RFC3339 time +func formatToRFC3339(time string) string { + var formattedTime string + if len(strings.Fields(time)) > 1 { + newTime := strings.Fields(time)[0:2] + rfc39990time, err := convertTime(strings.Join(newTime, " ")) + if err != nil { + fmt.Println("error formatting time") + } + formattedTime = rfc39990time + } else { + rfc39990time, err := convertTime(time) + if err != nil { + fmt.Println("error formatting time") + } + formattedTime = rfc39990time + } + return formattedTime +} diff --git a/cmd/query.go b/cmd/query.go index dbc2948..245768c 100644 --- a/cmd/query.go +++ b/cmd/query.go @@ -21,11 +21,13 @@ import ( "io" "os" "pb/pkg/config" - "pb/pkg/model" "strings" "time" - tea "github.com/charmbracelet/bubbletea" + // "pb/pkg/model" + + //! This dependency is required by the interactive flag Do not remove + // tea "github.com/charmbracelet/bubbletea" "github.com/spf13/cobra" ) @@ -41,12 +43,12 @@ var ( // save filter flags saveFilterFlag = "save-as" saveFilterShort = "s" - //save filter with time flags + // save filter with time flags saveFilterTimeFlag = "with-time" saveFilterTimeShort = "w" - interactiveFlag = "interactive" - interactiveFlagShort = "i" + // interactiveFlag = "interactive" + // interactiveFlagShort = "i" ) var query = &cobra.Command{ @@ -65,10 +67,10 @@ var query = &cobra.Command{ fmt.Println("please enter your query") fmt.Printf("Example:\n pb query run \"select * from frontend\" --from=10m --to=now\n") return nil - } else { - query = args[0] } + query = args[0] + start, err := command.Flags().GetString(startFlag) if err != nil { return err @@ -85,15 +87,16 @@ var query = &cobra.Command{ end = defaultEnd } - interactive, err := command.Flags().GetBool(interactiveFlag) - if err != nil { - return err - } + // TODO: Interactive Flag disabled + // interactive, err := command.Flags().GetBool(interactiveFlag) + // if err != nil { + // return err + // } - startTime, endTime, err := parseTime(start, end) - if err != nil { - return err - } + // startTime, endTime, err := parseTime(start, end) + // if err != nil { + // return err + // } keepTime, err := command.Flags().GetBool(saveFilterTimeFlag) if err != nil { @@ -106,14 +109,15 @@ var query = &cobra.Command{ } filterNameTrimmed := strings.Trim(filterName, " ") - if interactive { - p := tea.NewProgram(model.NewQueryModel(DefaultProfile, query, startTime, endTime), tea.WithAltScreen()) - if _, err := p.Run(); err != nil { - fmt.Printf("there's been an error: %v", err) - os.Exit(1) - } - return nil - } + // TODO: Interactive Flag disabled + // if interactive { + // p := tea.NewProgram(model.NewQueryModel(DefaultProfile, query, startTime, endTime), tea.WithAltScreen()) + // if _, err := p.Run(); err != nil { + // fmt.Printf("there's been an error: %v", err) + // os.Exit(1) + // } + // return nil + // } // Checks if there is filter name which is not empty. Empty filter name wont be allowed if command.Flags().Changed(saveFilterFlag) { @@ -124,7 +128,6 @@ var query = &cobra.Command{ } else if filterName != "" { if keepTime { createFilterWithTime(query, filterNameTrimmed, start, end) - } else { // if there is no keep time filter pass empty values for startTime and endTime createFilter(query, filterNameTrimmed) @@ -143,7 +146,7 @@ var query = &cobra.Command{ var QueryCmd = func() *cobra.Command { query.Flags().BoolP(saveFilterTimeFlag, saveFilterTimeShort, false, "Save the time range associated in the query to the filter") // save time for a filter flag; default value = false (boolean type) - query.Flags().BoolP(interactiveFlag, interactiveFlagShort, false, "open the query result in interactive mode") + // query.Flags().BoolP(interactiveFlag, interactiveFlagShort, false, "open the query result in interactive mode") query.Flags().StringP(startFlag, startFlagShort, defaultStart, "Start time for query. Takes date as '2024-10-12T07:20:50.52Z' or string like '10m', '1hr'") query.Flags().StringP(endFlag, endFlagShort, defaultEnd, "End time for query. Takes date as '2024-10-12T07:20:50.52Z' or 'now'") query.Flags().StringP(saveFilterFlag, saveFilterShort, "", "Save a query filter") // save filter flag. Default value = FILTER_NAME (type string) @@ -180,36 +183,35 @@ func fetchData(client *HTTPClient, query string, startTime string, endTime strin } // Returns start and end time for query in RFC3339 format -func parseTime(start, end string) (time.Time, time.Time, error) { - if start == defaultStart && end == defaultEnd { - return time.Now().Add(-1 * time.Minute), time.Now(), nil - } - - startTime, err := time.Parse(time.RFC3339, start) - if err != nil { - // try parsing as duration - duration, err := time.ParseDuration(start) - if err != nil { - return time.Time{}, time.Time{}, err - } - startTime = time.Now().Add(-1 * duration) - } - - endTime, err := time.Parse(time.RFC3339, end) - if err != nil { - if end == "now" { - endTime = time.Now() - } else { - return time.Time{}, time.Time{}, err - } - } - - return startTime, endTime, nil -} +// func parseTime(start, end string) (time.Time, time.Time, error) { +// if start == defaultStart && end == defaultEnd { +// return time.Now().Add(-1 * time.Minute), time.Now(), nil +// } + +// startTime, err := time.Parse(time.RFC3339, start) +// if err != nil { +// // try parsing as duration +// duration, err := time.ParseDuration(start) +// if err != nil { +// return time.Time{}, time.Time{}, err +// } +// startTime = time.Now().Add(-1 * duration) +// } + +// endTime, err := time.Parse(time.RFC3339, end) +// if err != nil { +// if end == "now" { +// endTime = time.Now() +// } else { +// return time.Time{}, time.Time{}, err +// } +// } + +// return startTime, endTime, nil +// } // create a request body for saving filter without time_filter func createFilter(query string, filterName string) (err error) { - userConfig, err := config.ReadConfigFromFile() if err != nil { return err @@ -248,7 +250,6 @@ func createFilter(query string, filterName string) (err error) { saveFilterToServer(finalQuery) return err - } // create a request body for saving filter with time_filter diff --git a/go.mod b/go.mod index f0f3e42..42f5166 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,9 @@ go 1.22 require ( github.com/apache/arrow/go/v13 v13.0.0 - github.com/charmbracelet/bubbles v0.16.1 - github.com/charmbracelet/bubbletea v0.26.4 + github.com/charmbracelet/bubbles v0.18.0 + github.com/charmbracelet/bubbletea v0.26.6 + github.com/charmbracelet/lipgloss v0.12.1 github.com/dustin/go-humanize v1.0.1 golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 golang.org/x/term v0.21.0 @@ -13,7 +14,7 @@ require ( ) require ( - github.com/charmbracelet/x/ansi v0.1.2 // indirect + github.com/charmbracelet/x/ansi v0.1.4 // indirect github.com/charmbracelet/x/input v0.1.0 // indirect github.com/charmbracelet/x/term v0.1.1 // indirect github.com/charmbracelet/x/windows v0.1.0 // indirect @@ -37,17 +38,16 @@ require ( require ( github.com/atotto/clipboard v0.1.4 // indirect - github.com/charmbracelet/lipgloss v0.7.1 github.com/evertras/bubble-table v0.15.2 github.com/muesli/termenv v0.15.2 github.com/pelletier/go-toml/v2 v2.0.9 - github.com/sahilm/fuzzy v0.1.0 // indirect + github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect ) require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect diff --git a/go.sum b/go.sum index 859bae7..2094cee 100644 --- a/go.sum +++ b/go.sum @@ -4,14 +4,14 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= -github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= -github.com/charmbracelet/bubbletea v0.26.4 h1:2gDkkzLZaTjMl/dQBpNVtnvcCxsh/FCkimep7FC9c40= -github.com/charmbracelet/bubbletea v0.26.4/go.mod h1:P+r+RRA5qtI1DOHNFn0otoNwB4rn+zNAzSj/EXz6xU0= -github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= -github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= -github.com/charmbracelet/x/ansi v0.1.2 h1:6+LR39uG8DE6zAmbu023YlqjJHkYXDF1z36ZwzO4xZY= -github.com/charmbracelet/x/ansi v0.1.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= +github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= +github.com/charmbracelet/bubbletea v0.26.6 h1:zTCWSuST+3yZYZnVSvbXwKOPRSNZceVeqpzOLN2zq1s= +github.com/charmbracelet/bubbletea v0.26.6/go.mod h1:dz8CWPlfCCGLFbBlTY4N7bjLiyOGDJEnd2Muu7pOWhk= +github.com/charmbracelet/lipgloss v0.12.1 h1:/gmzszl+pedQpjCOH+wFkZr/N90Snz40J/NR7A0zQcs= +github.com/charmbracelet/lipgloss v0.12.1/go.mod h1:V2CiwIuhx9S1S1ZlADfOj9HmxeMAORuz5izHb0zGbB8= +github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM= +github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= github.com/charmbracelet/x/input v0.1.0 h1:TEsGSfZYQyOtp+STIjyBq6tpRaorH0qpwZUj8DavAhQ= github.com/charmbracelet/x/input v0.1.0/go.mod h1:ZZwaBxPF7IG8gWWzPUVqHEtWhc1+HXJPNuerJGRGZ28= github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI= @@ -46,8 +46,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= @@ -72,8 +72,8 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= -github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y= +github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= diff --git a/main.go b/main.go index 5985d38..e356167 100644 --- a/main.go +++ b/main.go @@ -113,6 +113,7 @@ func main() { stream.AddCommand(cmd.StatStreamCmd) query.AddCommand(cmd.QueryCmd) + query.AddCommand(cmd.FilterList) cli.AddCommand(profile) cli.AddCommand(query) @@ -141,7 +142,7 @@ func main() { } config.WriteConfigToFile(&conf) } else { - //updates the demo profile for existing users + // updates the demo profile for existing users _, exists := previousConfig.Profiles["demo"] if exists { conf := config.Config{ diff --git a/pkg/model/credential/credential.go b/pkg/model/credential/credential.go index d906d7b..7b190d5 100644 --- a/pkg/model/credential/credential.go +++ b/pkg/model/credential/credential.go @@ -71,7 +71,7 @@ func New() Model { var t textinput.Model for i := range m.inputs { t = textinput.New() - t.Cursor.Style = focusedStyle.Copy() + t.Cursor.Style = focusedStyle t.CharLimit = 32 switch i { diff --git a/pkg/model/savedFilters.go b/pkg/model/savedFilters.go new file mode 100644 index 0000000..9660881 --- /dev/null +++ b/pkg/model/savedFilters.go @@ -0,0 +1,267 @@ +// Copyright (c) 2024 Parseable, Inc +// +// 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 model + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "pb/pkg/config" + "strings" + "time" + + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +const ( + applyFilterButton = "a" + deleteFilterButton = "d" +) + +var docStyle = lipgloss.NewStyle().Margin(1, 2) + +// FilterDetails represents the structure of filter data +type FilterDetails struct { + FilterID string `json:"filter_id"` + FilterName string `json:"filter_name"` + StreamName string `json:"stream_name"` + QueryField map[string]interface{} `json:"query"` + TimeFilter map[string]interface{} `json:"time_filter"` +} + +// Item represents the structure of the filter item +type Item struct { + id, title, stream, desc, from, to string +} + +var ( + titleStyles = lipgloss.NewStyle().PaddingLeft(0).Bold(true).Foreground(lipgloss.Color("9")) + queryStyle = lipgloss.NewStyle().PaddingLeft(0).Foreground(lipgloss.Color("7")) + itemStyle = lipgloss.NewStyle().PaddingLeft(4).Foreground(lipgloss.Color("8")) + // selectedItemStyle = lipgloss.NewStyle().PaddingLeft(4).Foreground(lipgloss.Color("170")) + selectedItemStyle = lipgloss.NewStyle().PaddingLeft(1).Foreground(lipgloss.AdaptiveColor{Light: "16", Dark: "226"}) +) + +type itemDelegate struct{} + +func (d itemDelegate) Height() int { return 4 } +func (d itemDelegate) Spacing() int { return 1 } +func (d itemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil } +func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { + i, ok := listItem.(Item) + if !ok { + return + } + var str string + + if i.from != "" || i.to != "" { + str = fmt.Sprintf("From: %s\nTo: %s", i.from, i.to) + } else { + str = "" + } + + fn := itemStyle.Render + tr := titleStyles.Render + qr := queryStyle.Render + if index == m.Index() { + tr = func(s ...string) string { + return selectedItemStyle.Render("> " + strings.Join(s, " ")) + } + } + + fmt.Fprint(w, fn(tr(i.title)+"\n"+qr(i.desc)+"\n"+str)) +} + +func (d itemDelegate) ShortHelp() []key.Binding { + return []key.Binding{ + key.NewBinding( + key.WithKeys(applyFilterButton), + key.WithHelp(applyFilterButton, "apply"), + ), + key.NewBinding( + key.WithKeys(deleteFilterButton), + key.WithHelp(deleteFilterButton, "delete"), + ), + } +} + +// FullHelp returns the extended list of keybindings. +func (d itemDelegate) FullHelp() [][]key.Binding { + return [][]key.Binding{ + { + key.NewBinding( + key.WithKeys(applyFilterButton), + key.WithHelp(applyFilterButton, "apply"), + ), + key.NewBinding( + key.WithKeys(deleteFilterButton), + key.WithHelp(deleteFilterButton, "delete"), + ), + }, + } +} + +var ( + selectedFilterApply Item + selectedFilterDelete Item +) + +func (i Item) Title() string { return fmt.Sprintf("Filter:%s, Query:%s", i.title, i.desc) } + +func (i Item) Description() string { + if i.to == "" || i.from == "" { + return "" + } + return fmt.Sprintf("From:%s To:%s", i.from, i.to) +} + +func (i Item) FilterValue() string { return i.title } +func (i Item) FilterID() string { return i.id } +func (i Item) Stream() string { return i.desc } +func (i Item) StartTime() string { return i.from } +func (i Item) EndTime() string { return i.to } + +type modelFilter struct { + list list.Model +} + +func (m modelFilter) Init() tea.Cmd { + return nil +} + +func (m modelFilter) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + if msg.String() == "ctrl+c" { + return m, tea.Quit + } + if msg.String() == "a" || msg.Type == tea.KeyEnter { + selectedFilterApply = m.list.SelectedItem().(Item) + return m, tea.Quit + } + if msg.String() == "d" { + selectedFilterDelete = m.list.SelectedItem().(Item) + return m, tea.Quit + + } + case tea.WindowSizeMsg: + h, v := docStyle.GetFrameSize() + m.list.SetSize(msg.Width-h, msg.Height-v) + } + + var cmd tea.Cmd + m.list, cmd = m.list.Update(msg) + return m, cmd +} + +func (m modelFilter) View() string { + return docStyle.Render(m.list.View()) +} + +// UIApp lists interactive list for the user to display all the available filters (only saved SQL filters ) +func UIApp() *tea.Program { + userConfig, err := config.ReadConfigFromFile() + if err != nil { + fmt.Println("Error reading Default Profile") + } + var userProfile config.Profile + if profile, ok := userConfig.Profiles[userConfig.DefaultProfile]; ok { + userProfile = profile + } + + client := &http.Client{ + Timeout: time.Second * 60, + } + userFilters := fetchFilters(client, &userProfile) + + m := modelFilter{list: list.New(userFilters, itemDelegate{}, 0, 0)} + m.list.Title = fmt.Sprintf("Saved Filters for User: %s", userProfile.Username) + + return tea.NewProgram(m, tea.WithAltScreen()) +} + +// fetchFilters fetches filters from the server and sends them to the channel +func fetchFilters(client *http.Client, profile *config.Profile) []list.Item { + endpoint := fmt.Sprintf("%s/%s/%s", profile.URL, "api/v1/filters", profile.Username) + req, err := http.NewRequest("GET", endpoint, nil) + if err != nil { + fmt.Println("Error creating request:", err) + return nil + } + + req.SetBasicAuth(profile.Username, profile.Password) + req.Header.Add("Content-Type", "application/json") + resp, err := client.Do(req) + if err != nil { + fmt.Println("Error making request:", err) + return nil + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + fmt.Println("Error reading response body:", err) + return nil + } + + var filters []FilterDetails + err = json.Unmarshal(body, &filters) + if err != nil { + fmt.Println("Error unmarshalling response:", err) + return nil + } + var userFilters []list.Item + for _, filter := range filters { + var userFilter Item + queryBytes, _ := json.Marshal(filter.QueryField["filter_query"]) + + // Extract "from" and "to" from time_filter + var from, to string + if fromValue, exists := filter.TimeFilter["from"]; exists { + from = fmt.Sprintf("%v", fromValue) + } + if toValue, exists := filter.TimeFilter["to"]; exists { + to = fmt.Sprintf("%v", toValue) + } + // filtering only SQL type filters Filter_name is tile and Stream Name is desc + if string(queryBytes) != "null" { + userFilter = Item{ + id: filter.FilterID, + title: filter.FilterName, + stream: filter.StreamName, + desc: string(queryBytes), + from: from, + to: to, + } + userFilters = append(userFilters, userFilter) + } + } + return userFilters +} + +// FilterToApply returns the selected filter by user in the interactive list to apply +func FilterToApply() Item { + return selectedFilterApply +} + +// FilterToDelete returns the selected filter by user in the interactive list to delete +func FilterToDelete() Item { + return selectedFilterDelete +} diff --git a/pkg/model/status.go b/pkg/model/status.go index 129244f..1000d43 100644 --- a/pkg/model/status.go +++ b/pkg/model/status.go @@ -24,19 +24,19 @@ import ( var ( commonStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#FFFFFF", Dark: "#000000"}) - titleStyle = commonStyle.Copy(). + titleStyle = commonStyle. Background(lipgloss.AdaptiveColor{Light: "#134074", Dark: "#FFADAD"}). Padding(0, 1) - hostStyle = commonStyle.Copy(). + hostStyle = commonStyle. Background(lipgloss.AdaptiveColor{Light: "#13315C", Dark: "#FFD6A5"}). Padding(0, 1) - infoStyle = commonStyle.Copy(). + infoStyle = commonStyle. Background(lipgloss.AdaptiveColor{Light: "#212529", Dark: "#CAFFBF"}). AlignHorizontal(lipgloss.Right) - errorStyle = commonStyle.Copy(). + errorStyle = commonStyle. Background(lipgloss.AdaptiveColor{Light: "#5A2A27", Dark: "#D4A373"}). AlignHorizontal(lipgloss.Right) ) diff --git a/pkg/model/timerange.go b/pkg/model/timerange.go index b77337c..2a394fb 100644 --- a/pkg/model/timerange.go +++ b/pkg/model/timerange.go @@ -90,8 +90,8 @@ func NewTimeRangeModel() list.Model { list.SetShowHelp(false) list.SetShowFilter(false) list.SetShowTitle(true) - list.Styles.TitleBar = baseStyle.Copy() - list.Styles.Title = baseStyle.Copy().MarginBottom(1) + list.Styles.TitleBar = baseStyle + list.Styles.Title = baseStyle.MarginBottom(1) list.Styles.TitleBar.Align(lipgloss.Left) list.Title = "Select Time Range" list.SetShowStatusBar(false)