From eb480cd8a05f44913ba4d5ebb11497c3236d7e9f Mon Sep 17 00:00:00 2001 From: Koustav Date: Fri, 2 Aug 2024 22:42:37 +0530 Subject: [PATCH 1/6] List with bubble tea --- cmd/filterList.go | 44 ++++++++++ go.mod | 12 +-- go.sum | 24 +++--- main.go | 1 + pkg/model/savedFilters.go | 169 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 232 insertions(+), 18 deletions(-) create mode 100644 cmd/filterList.go create mode 100644 pkg/model/savedFilters.go diff --git a/cmd/filterList.go b/cmd/filterList.go new file mode 100644 index 0000000..9a55dc0 --- /dev/null +++ b/cmd/filterList.go @@ -0,0 +1,44 @@ +// 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 ( + "os" + "pb/pkg/model" + + "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(command *cobra.Command, args []string) { + // model.FilterListUI() + p:= model.UiApp() + _,err := p.Run(); if err != nil { + os.Exit(1) + } + +}, + +} + + + + 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..e974f11 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) diff --git a/pkg/model/savedFilters.go b/pkg/model/savedFilters.go new file mode 100644 index 0000000..1fd6f81 --- /dev/null +++ b/pkg/model/savedFilters.go @@ -0,0 +1,169 @@ +// 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" + "time" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +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"` +} + +type item struct { + id,title, stream, desc, from, to string +} + + + + +func (i item) Title() string { return i.title } + +func (i item) Description() string { + if i.to == "" || i.from==""{ + return i.desc + }else{ + return fmt.Sprintf("%s From:%s To:%s",i.desc,i.from,i.to) + } + } + +func (i item) FilterValue() string { return i.title } + +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 + } + 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()) +} + +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, list.NewDefaultDelegate(), 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 unmarshaling 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 + +} From c6860e149fa1c5cdda93c9eca4790c456755eeff Mon Sep 17 00:00:00 2001 From: Koustav Date: Mon, 5 Aug 2024 15:05:14 +0530 Subject: [PATCH 2/6] This PR adds an interactive list for saved filters from which the user can apply or delete a saved filter This pr also disables the interactive flag option for pb query run --- cmd/filterList.go | 102 +++++++++++++++++++++++++++++++++++++- cmd/query.go | 46 +++++++++-------- pkg/model/savedFilters.go | 36 +++++++++++--- 3 files changed, 155 insertions(+), 29 deletions(-) diff --git a/cmd/filterList.go b/cmd/filterList.go index 9a55dc0..a8f3879 100644 --- a/cmd/filterList.go +++ b/cmd/filterList.go @@ -16,8 +16,11 @@ package cmd import ( + "fmt" "os" "pb/pkg/model" + "strings" + "time" "github.com/spf13/cobra" ) @@ -29,16 +32,113 @@ var FilterList = &cobra.Command{ Long: "\nShow a list of saved filter for a stream ", PreRunE: PreRunDefaultProfile, Run: func(command *cobra.Command, args []string) { - // model.FilterListUI() + 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..dca9318 100644 --- a/cmd/query.go +++ b/cmd/query.go @@ -21,11 +21,11 @@ import ( "io" "os" "pb/pkg/config" - "pb/pkg/model" + // "pb/pkg/model" "strings" "time" - - tea "github.com/charmbracelet/bubbletea" + //! This dependancy is required by the interactive flag Do not remove + // tea "github.com/charmbracelet/bubbletea" "github.com/spf13/cobra" ) @@ -45,8 +45,8 @@ var ( saveFilterTimeFlag = "with-time" saveFilterTimeShort = "w" - interactiveFlag = "interactive" - interactiveFlagShort = "i" + // interactiveFlag = "interactive" + // interactiveFlagShort = "i" ) var query = &cobra.Command{ @@ -85,15 +85,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 +107,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) { @@ -143,7 +145,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) diff --git a/pkg/model/savedFilters.go b/pkg/model/savedFilters.go index 1fd6f81..6934a6d 100644 --- a/pkg/model/savedFilters.go +++ b/pkg/model/savedFilters.go @@ -1,6 +1,5 @@ // 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 @@ -45,18 +44,24 @@ type item struct { +var selectedFilterApply item +var selectedFilterDelete item -func (i item) Title() string { return i.title } +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 i.desc + if i.to =="" || i.from==""{ + return "" }else{ - return fmt.Sprintf("%s From:%s To:%s",i.desc,i.from,i.to) + 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 @@ -72,6 +77,15 @@ func (m modelFilter) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 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) @@ -86,6 +100,7 @@ func (m modelFilter) View() string { return docStyle.Render(m.list.View()) } +// Interactive list for the user to display all the available filters (only saved SQL filters ) func UiApp() *tea.Program { userConfig, err := config.ReadConfigFromFile() @@ -102,7 +117,7 @@ func UiApp() *tea.Program { } userFilters := fetchFilters(client, &userProfile) - m := modelFilter{list: list.New(userFilters, list.NewDefaultDelegate(), 0, 0)} + m := modelFilter{list: list.New(userFilters, list.NewDefaultDelegate(), 0, 6)} m.list.Title = fmt.Sprintf("Saved Filters for User: %s", userProfile.Username) return tea.NewProgram(m, tea.WithAltScreen()) @@ -167,3 +182,12 @@ func fetchFilters(client *http.Client, profile *config.Profile) []list.Item { return userFilters } + +// returns the selected filter by user in the iteractive list +func FilterToApply() item{ + return selectedFilterApply +} +// returns the selected filter by user in the iteractive list +func FilterToDelete() item{ + return selectedFilterDelete +} \ No newline at end of file From c68f3a285d1674345ea28d01d82e771db1b1a797 Mon Sep 17 00:00:00 2001 From: Koustav Date: Mon, 5 Aug 2024 19:17:13 +0530 Subject: [PATCH 3/6] Feature: saved filter listing/apply/delete This pr adds 3 functionalities to pb. Now a user can see their saved Filters using the command pb query list which will open a interactive menu with a list of their saved filters then the user can apply the filter using 'a' or enter and can also delete the filter using the 'd' button. *note: this pr disables the -i or --interactive flag for pb query run command --- pkg/model/savedFilters.go | 81 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 3 deletions(-) diff --git a/pkg/model/savedFilters.go b/pkg/model/savedFilters.go index 6934a6d..77847c2 100644 --- a/pkg/model/savedFilters.go +++ b/pkg/model/savedFilters.go @@ -20,13 +20,20 @@ import ( "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 @@ -42,6 +49,75 @@ 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 selected filter"), + ), + key.NewBinding( + key.WithKeys(deleteFilterButton), + key.WithHelp(deleteFilterButton, "delete selected filter"), + ), + } +} + +// 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 selected filter"), + ), + key.NewBinding( + key.WithKeys(deleteFilterButton), + key.WithHelp(deleteFilterButton, "delete selected filter"), + ), + }, + } +} + + var selectedFilterApply item @@ -52,9 +128,8 @@ func (i item) Title() string { return fmt.Sprintf("Filter:%s, Query:%s",i.title, func (i item) Description() string { if i.to =="" || i.from==""{ return "" - }else{ + } return fmt.Sprintf("From:%s To:%s",i.from,i.to) - } } func (i item) FilterValue() string { return i.title } @@ -117,7 +192,7 @@ func UiApp() *tea.Program { } userFilters := fetchFilters(client, &userProfile) - m := modelFilter{list: list.New(userFilters, list.NewDefaultDelegate(), 0, 6)} + 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()) From c3d25e08335e79e251f281ec1d2d3fc64eb2fe78 Mon Sep 17 00:00:00 2001 From: Koustav Date: Mon, 5 Aug 2024 19:26:03 +0530 Subject: [PATCH 4/6] Go fmt'ed all files --- cmd/filterList.go | 124 +++++++++++++++++++------------------- cmd/query.go | 7 +-- pkg/model/savedFilters.go | 103 ++++++++++++++++--------------- 3 files changed, 115 insertions(+), 119 deletions(-) diff --git a/cmd/filterList.go b/cmd/filterList.go index a8f3879..3e2dc61 100644 --- a/cmd/filterList.go +++ b/cmd/filterList.go @@ -33,30 +33,29 @@ var FilterList = &cobra.Command{ PreRunE: PreRunDefaultProfile, Run: func(command *cobra.Command, args []string) { client := DefaultClient() - - - p:= model.UiApp() - _,err := p.Run(); if err != nil { - os.Exit(1) + + 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()) + a := model.FilterToApply() + d := model.FilterToDelete() + if a.Stream() != "" { + filterToPbQuery(a.Stream(), a.StartTime(), a.EndTime()) } - if d.FilterId() != ""{ - deleteFilter(&client,d.FilterId()) + 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{ +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") } @@ -72,70 +71,69 @@ func deleteFilter(client *HTTPClient, filterID string){ } // Convert a filter to executable pb query -func filterToPbQuery( query string, start string, end string){ +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) + 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) + // 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 - } + t, err := parseTimeToFormat(input) + if err != nil { + return "", err + } - return t.Format(time.RFC3339), nil + return t.Format(time.RFC3339), nil } // Converts User inputted time to string type RFC3339 time -func formatToRFC3339(time string) string{ +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{ + 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{ + } else { + rfc39990time, err := convertTime(time) + if err != nil { fmt.Println("error formatting time") } formattedTime = rfc39990time diff --git a/cmd/query.go b/cmd/query.go index dca9318..2485802 100644 --- a/cmd/query.go +++ b/cmd/query.go @@ -65,9 +65,8 @@ 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 { @@ -85,7 +84,7 @@ var query = &cobra.Command{ end = defaultEnd } - //TODO: Interactive Flag disabled + //TODO: Interactive Flag disabled // interactive, err := command.Flags().GetBool(interactiveFlag) // if err != nil { // return err @@ -107,7 +106,7 @@ var query = &cobra.Command{ } filterNameTrimmed := strings.Trim(filterName, " ") - //TODO: Interactive Flag disabled + //TODO: Interactive Flag disabled // if interactive { // p := tea.NewProgram(model.NewQueryModel(DefaultProfile, query, startTime, endTime), tea.WithAltScreen()) // if _, err := p.Run(); err != nil { diff --git a/pkg/model/savedFilters.go b/pkg/model/savedFilters.go index 77847c2..c6d96aa 100644 --- a/pkg/model/savedFilters.go +++ b/pkg/model/savedFilters.go @@ -30,8 +30,8 @@ import ( ) const ( - applyFilterButton ="a" - deleteFilterButton ="d" + applyFilterButton = "a" + deleteFilterButton = "d" ) var docStyle = lipgloss.NewStyle().Margin(1, 2) @@ -46,13 +46,13 @@ type FilterDetails struct { } type item struct { - id,title, stream, desc, from, to string + 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")) + 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"}) ) @@ -69,9 +69,9 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list } var str string - if i.from != "" || i.to != ""{ - str = fmt.Sprintf("From: %s\nTo: %s",i.from, i.to) - }else{ + if i.from != "" || i.to != "" { + str = fmt.Sprintf("From: %s\nTo: %s", i.from, i.to) + } else { str = "" } @@ -87,56 +87,52 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list 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 selected filter"), - ), - key.NewBinding( - key.WithKeys(deleteFilterButton), - key.WithHelp(deleteFilterButton, "delete selected filter"), - ), - } + return []key.Binding{ + key.NewBinding( + key.WithKeys(applyFilterButton), + key.WithHelp(applyFilterButton, "apply selected filter"), + ), + key.NewBinding( + key.WithKeys(deleteFilterButton), + key.WithHelp(deleteFilterButton, "delete selected filter"), + ), + } } // FullHelp returns the extended list of keybindings. func (d itemDelegate) FullHelp() [][]key.Binding { - return [][]key.Binding{ - { - key.NewBinding( + return [][]key.Binding{ + { + key.NewBinding( key.WithKeys(applyFilterButton), key.WithHelp(applyFilterButton, "apply selected filter"), - ), - key.NewBinding( + ), + key.NewBinding( key.WithKeys(deleteFilterButton), key.WithHelp(deleteFilterButton, "delete selected filter"), - ), - }, - } + ), + }, + } } - - - var selectedFilterApply item var selectedFilterDelete item -func (i item) Title() string { return fmt.Sprintf("Filter:%s, Query:%s",i.title,i.desc) } +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==""{ + if i.to == "" || i.from == "" { return "" } - return fmt.Sprintf("From:%s To:%s",i.from,i.to) - } + 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 } +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 @@ -152,14 +148,14 @@ func (m modelFilter) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.String() == "ctrl+c" { return m, tea.Quit } - if msg.String() == "a" ||msg.Type == tea.KeyEnter{ + 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() @@ -244,25 +240,28 @@ func fetchFilters(client *http.Client, profile *config.Profile) []list.Item { 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, + 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) } - userFilters = append(userFilters, userFilter)} } return userFilters } // returns the selected filter by user in the iteractive list -func FilterToApply() item{ +func FilterToApply() item { return selectedFilterApply } + // returns the selected filter by user in the iteractive list -func FilterToDelete() item{ +func FilterToDelete() item { return selectedFilterDelete -} \ No newline at end of file +} From ef39ef0ec1764898548db6cb736b82da0e862be9 Mon Sep 17 00:00:00 2001 From: Koustav Date: Tue, 6 Aug 2024 10:08:43 +0530 Subject: [PATCH 5/6] help message updated --- pkg/model/savedFilters.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/model/savedFilters.go b/pkg/model/savedFilters.go index c6d96aa..e40f65e 100644 --- a/pkg/model/savedFilters.go +++ b/pkg/model/savedFilters.go @@ -91,11 +91,11 @@ func (d itemDelegate) ShortHelp() []key.Binding { return []key.Binding{ key.NewBinding( key.WithKeys(applyFilterButton), - key.WithHelp(applyFilterButton, "apply selected filter"), + key.WithHelp(applyFilterButton, "apply"), ), key.NewBinding( key.WithKeys(deleteFilterButton), - key.WithHelp(deleteFilterButton, "delete selected filter"), + key.WithHelp(deleteFilterButton, "delete"), ), } } @@ -106,11 +106,11 @@ func (d itemDelegate) FullHelp() [][]key.Binding { { key.NewBinding( key.WithKeys(applyFilterButton), - key.WithHelp(applyFilterButton, "apply selected filter"), + key.WithHelp(applyFilterButton, "apply"), ), key.NewBinding( key.WithKeys(deleteFilterButton), - key.WithHelp(deleteFilterButton, "delete selected filter"), + key.WithHelp(deleteFilterButton, "delete"), ), }, } From 6d9751444a5cbc17119e7050c7c517eaebb9beb3 Mon Sep 17 00:00:00 2001 From: Koustav Date: Tue, 6 Aug 2024 10:12:10 +0530 Subject: [PATCH 6/6] Optimisation --- cmd/query.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/query.go b/cmd/query.go index 2485802..4db92e3 100644 --- a/cmd/query.go +++ b/cmd/query.go @@ -66,6 +66,7 @@ var query = &cobra.Command{ fmt.Printf("Example:\n pb query run \"select * from frontend\" --from=10m --to=now\n") return nil } + query = args[0] start, err := command.Flags().GetString(startFlag)