diff --git a/internal/domain/grpc.go b/internal/domain/grpc.go index 6956a842..45a55c84 100644 --- a/internal/domain/grpc.go +++ b/internal/domain/grpc.go @@ -16,6 +16,9 @@ type GRPCRequestSpec struct { Settings Settings `yaml:"settings"` Body string `yaml:"body"` Services []GRPCService `yaml:"services"` + + PreRequest PreRequest `yaml:"preRequest"` + PostRequest PostRequest `yaml:"postRequest"` } type GRPCService struct { @@ -137,6 +140,14 @@ func CompareGRPCRequestSpecs(a, b *GRPCRequestSpec) bool { return false } + if !ComparePreRequest(a.PreRequest, b.PreRequest) { + return false + } + + if !ComparePostRequest(a.PostRequest, b.PostRequest) { + return false + } + return true } diff --git a/internal/domain/requests.go b/internal/domain/requests.go index 26b56c2a..5c00fd0d 100644 --- a/internal/domain/requests.go +++ b/internal/domain/requests.go @@ -28,11 +28,13 @@ const ( RequestBodyTypeBinary = "binary" RequestBodyTypeUrlEncoded = "urlEncoded" - PrePostTypeNone = "none" - PrePostTypePython = "python" - PrePostTypeShell = "ssh" - PrePostTypeSSHTunnel = "sshTunnel" - PrePostTypeK8sTunnel = "k8sTunnel" + PrePostTypeNone = "none" + PrePostTypeTriggerRequest = "triggerRequest" + PrePostTypeSetEnv = "setEnv" + PrePostTypePython = "python" + PrePostTypeShell = "ssh" + PrePostTypeSSHTunnel = "sshTunnel" + PrePostTypeK8sTunnel = "k8sTunnel" ) type Request struct { @@ -57,6 +59,48 @@ type RequestSpec struct { HTTP *HTTPRequestSpec `yaml:"http,omitempty"` } +func (r RequestSpec) GetGRPC() *GRPCRequestSpec { + if r.GRPC != nil { + return r.GRPC + } + return nil +} + +func (r RequestSpec) GetHTTP() *HTTPRequestSpec { + if r.HTTP != nil { + return r.HTTP + } + return nil +} + +func (r *GRPCRequestSpec) GetPreRequest() PreRequest { + if r != nil { + return r.PreRequest + } + return PreRequest{} +} + +func (r *GRPCRequestSpec) GetPostRequest() PostRequest { + if r != nil { + return r.PostRequest + } + return PostRequest{} +} + +func (r *HTTPRequestSpec) GetPreRequest() PreRequest { + if r != nil { + return r.Request.PreRequest + } + return PreRequest{} +} + +func (r *HTTPRequestSpec) GetPostRequest() PostRequest { + if r != nil { + return r.Request.PostRequest + } + return PostRequest{} +} + type LastUsedEnvironment struct { ID string `yaml:"id"` Name string `yaml:"name"` @@ -133,16 +177,13 @@ type PreRequest struct { SShTunnel *SShTunnel `yaml:"sshTunnel,omitempty"` KubernetesTunnel *KubernetesTunnel `yaml:"kubernetesTunnel,omitempty"` + TriggerRequest *TriggerRequest `yaml:"triggerRequest,omitempty"` } -const ( - PostRequestTypeNone = "none" - PostRequestTypeSetEnv = "setEnv" - PostRequestTypePythonScript = "pythonScript" - PostRequestTypeK8sTunnel = "k8sTunnel" - PostRequestTypeSSHTunnel = "sshTunnel" - PostRequestTypeShellScript = "shellScript" -) +type TriggerRequest struct { + CollectionID string `yaml:"collectionID"` + RequestID string `yaml:"requestID"` +} type PostRequest struct { Type string `yaml:"type"` @@ -151,9 +192,11 @@ type PostRequest struct { } const ( - PostRequestSetFromResponseHeader = "responseHeader" - PostRequestSetFromResponseBody = "responseBody" - PostRequestSetFromResponseCookie = "responseCookie" + PostRequestSetFromResponseHeader = "responseHeader" + PostRequestSetFromResponseBody = "responseBody" + PostRequestSetFromResponseCookie = "responseCookie" + PostRequestSetFromResponseMetaData = "responseMetaData" + PostRequestSetFromResponseTrailers = "responseTrailers" ) type PostRequestSet struct { @@ -295,6 +338,10 @@ func ComparePreRequest(a, b PreRequest) bool { return false } + if !CompareTriggerRequest(a.TriggerRequest, b.TriggerRequest) { + return false + } + return true } @@ -324,6 +371,22 @@ func CompareSShTunnel(a, b *SShTunnel) bool { return true } +func CompareTriggerRequest(a, b *TriggerRequest) bool { + if a == nil && b == nil { + return true + } + + if a == nil || b == nil { + return false + } + + if a.CollectionID != b.CollectionID || a.RequestID != b.RequestID { + return false + } + + return true +} + func CompareKubernetesTunnel(a, b *KubernetesTunnel) bool { if a == nil && b == nil { return true diff --git a/internal/egress/requests.go b/internal/egress/requests.go new file mode 100644 index 00000000..918ea5df --- /dev/null +++ b/internal/egress/requests.go @@ -0,0 +1,282 @@ +package egress + +import ( + "fmt" + + "github.com/chapar-rest/chapar/internal/domain" + "github.com/chapar-rest/chapar/internal/grpc" + "github.com/chapar-rest/chapar/internal/jsonpath" + "github.com/chapar-rest/chapar/internal/rest" + "github.com/chapar-rest/chapar/internal/state" +) + +type Service struct { + requests *state.Requests + environments *state.Environments + + rest *rest.Service + grpc *grpc.Service +} + +func New(requests *state.Requests, environments *state.Environments, rest *rest.Service, grpc *grpc.Service) *Service { + return &Service{ + requests: requests, + environments: environments, + rest: rest, + grpc: grpc, + } +} + +func (s *Service) Send(id, activeEnvironmentID string) (any, error) { + req := s.requests.GetRequest(id) + if req == nil { + return nil, fmt.Errorf("request with id %s not found", id) + } + + if err := s.preRequest(req, activeEnvironmentID); err != nil { + return nil, err + } + + var res any + var err error + if req.MetaData.Type == domain.RequestTypeHTTP { + res, err = s.rest.SendRequest(req.MetaData.ID, activeEnvironmentID) + } else { + res, err = s.grpc.Invoke(req.MetaData.ID, activeEnvironmentID) + } + + var activeEnvironment *domain.Environment + // Get environment if provided + if activeEnvironmentID != "" { + activeEnvironment = s.environments.GetEnvironment(activeEnvironmentID) + if activeEnvironment == nil { + return nil, fmt.Errorf("environment with id %s not found", activeEnvironmentID) + } + } + + if err := s.postRequest(req, res, activeEnvironment); err != nil { + return nil, err + } + + return res, err +} + +func (s *Service) preRequest(req *domain.Request, activeEnvironmentID string) error { + var preReq domain.PreRequest + if req.MetaData.Type == domain.RequestTypeHTTP { + preReq = req.Spec.GetHTTP().GetPreRequest() + } else { + preReq = req.Spec.GetGRPC().GetPreRequest() + } + + if preReq == (domain.PreRequest{}) || preReq.TriggerRequest == nil || preReq.TriggerRequest.RequestID == "none" { + return nil + } + + if preReq.Type != domain.PrePostTypeTriggerRequest { + // TODO: implement other types + return nil + } + + _, err := s.Send(preReq.TriggerRequest.RequestID, activeEnvironmentID) + return err +} + +func (s *Service) postRequest(req *domain.Request, res any, env *domain.Environment) error { + if req.MetaData.Type == domain.RequestTypeHTTP { + postReq := req.Spec.GetHTTP().GetPostRequest() + if response, ok := res.(*rest.Response); ok { + return s.handleHTTPPostRequest(postReq, response, env) + } else { + return fmt.Errorf("response is not of type *rest.Response") + } + } + + postReq := req.Spec.GetGRPC().GetPostRequest() + if response, ok := res.(*grpc.Response); ok { + return s.handleGRPCPostRequest(postReq, response, env) + } + + return fmt.Errorf("response is not of type *grpc.Response") +} + +func (s *Service) handleHTTPPostRequest(r domain.PostRequest, response *rest.Response, env *domain.Environment) error { + if r == (domain.PostRequest{}) || response == nil || env == nil { + return nil + } + + if r.Type != domain.PrePostTypeSetEnv { + // TODO: implement other types + return nil + } + + // only handle post request if the status code is the same as the one provided + if response.StatusCode != r.PostRequestSet.StatusCode { + return nil + } + + switch r.PostRequestSet.From { + case domain.PostRequestSetFromResponseBody: + return s.handlePostRequestFromBody(r, response, env) + case domain.PostRequestSetFromResponseHeader: + return s.handlePostRequestFromHeader(r, response, env) + case domain.PostRequestSetFromResponseCookie: + return s.handlePostRequestFromCookie(r, response, env) + } + + return nil +} + +func (s *Service) handlePostRequestFromBody(r domain.PostRequest, response *rest.Response, env *domain.Environment) error { + // handle post request + if r.PostRequestSet.From != domain.PostRequestSetFromResponseBody { + return nil + } + + if response.JSON == "" || !response.IsJSON { + return nil + + } + + data, err := jsonpath.Get(response.JSON, r.PostRequestSet.FromKey) + if err != nil { + return err + } + + if data == nil { + return nil + } + + if result, ok := data.(string); ok { + if env != nil { + env.SetKey(r.PostRequestSet.Target, result) + return s.environments.UpdateEnvironment(env, state.SourceRestService, false) + } + } + + return nil +} + +func (s *Service) handlePostRequestFromHeader(r domain.PostRequest, response *rest.Response, env *domain.Environment) error { + if r.PostRequestSet.From != domain.PostRequestSetFromResponseHeader { + return nil + } + + if result, ok := response.Headers[r.PostRequestSet.FromKey]; ok { + if env != nil { + env.SetKey(r.PostRequestSet.Target, result) + + if err := s.environments.UpdateEnvironment(env, state.SourceRestService, false); err != nil { + return err + } + } + } + return nil +} + +func (s *Service) handlePostRequestFromCookie(r domain.PostRequest, response *rest.Response, env *domain.Environment) error { + if r.PostRequestSet.From != domain.PostRequestSetFromResponseCookie { + return nil + } + + for _, c := range response.Cookies { + if c.Name == r.PostRequestSet.FromKey { + if env != nil { + env.SetKey(r.PostRequestSet.Target, c.Value) + return s.environments.UpdateEnvironment(env, state.SourceRestService, false) + } + } + } + return nil +} + +func (s *Service) handleGRPCPostRequest(r domain.PostRequest, res *grpc.Response, env *domain.Environment) error { + if r == (domain.PostRequest{}) || res == nil || env == nil { + return nil + } + + if r.Type != domain.PrePostTypeSetEnv { + return nil + } + + // only handle post request if the status code is the same as the one provided + if res.StatueCode != r.PostRequestSet.StatusCode { + return nil + } + + switch r.PostRequestSet.From { + case domain.PostRequestSetFromResponseBody: + return s.handleGRPCPostRequestFromBody(r, res, env) + case domain.PostRequestSetFromResponseMetaData: + return s.handlePostRequestFromMetaData(r, res, env) + case domain.PostRequestSetFromResponseTrailers: + return s.handlePostRequestFromTrailers(r, res, env) + } + + return nil +} + +func (s *Service) handleGRPCPostRequestFromBody(r domain.PostRequest, res *grpc.Response, env *domain.Environment) error { + if r.PostRequestSet.From != domain.PostRequestSetFromResponseBody { + return nil + } + + if res.Body == "" { + return nil + } + + data, err := jsonpath.Get(res.Body, r.PostRequestSet.FromKey) + if err != nil { + return err + } + + if data == nil { + return nil + } + + if result, ok := data.(string); ok { + if env != nil { + env.SetKey(r.PostRequestSet.Target, result) + + if err := s.environments.UpdateEnvironment(env, state.SourceGRPCService, false); err != nil { + return err + } + } + } + + return nil +} + +func (s *Service) handlePostRequestFromMetaData(r domain.PostRequest, res *grpc.Response, env *domain.Environment) error { + if r.PostRequestSet.From != domain.PostRequestSetFromResponseMetaData { + return nil + } + + for _, item := range res.Metadata { + if item.Key == r.PostRequestSet.FromKey { + if env != nil { + env.SetKey(r.PostRequestSet.Target, item.Value) + return s.environments.UpdateEnvironment(env, state.SourceGRPCService, false) + } + } + } + + return nil +} + +func (s *Service) handlePostRequestFromTrailers(r domain.PostRequest, res *grpc.Response, env *domain.Environment) error { + if r.PostRequestSet.From != domain.PostRequestSetFromResponseTrailers { + return nil + } + + for _, item := range res.Trailers { + if item.Key == r.PostRequestSet.FromKey { + if env != nil { + env.SetKey(r.PostRequestSet.Target, item.Value) + return s.environments.UpdateEnvironment(env, state.SourceGRPCService, false) + } + } + } + + return nil +} diff --git a/internal/grpc/grpc.go b/internal/grpc/grpc.go index 5997cd4f..05278afe 100644 --- a/internal/grpc/grpc.go +++ b/internal/grpc/grpc.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "io" - "net/http" "os" "path/filepath" "sort" @@ -48,7 +47,6 @@ type Response struct { Body string Metadata []domain.KeyValue Trailers []domain.KeyValue - Cookies []*http.Cookie TimePassed time.Duration Size int Error error @@ -228,6 +226,9 @@ func (s *Service) Invoke(id, activeEnvironmentID string) (*Response, error) { } spec := req.Clone().Spec.GRPC + if spec == nil { + return nil, nil + } var activeEnvironment = s.getActiveEnvironment(activeEnvironmentID) @@ -311,13 +312,13 @@ func (s *Service) Invoke(id, activeEnvironmentID string) (*Response, error) { StatueCode: int(status.Code(respErr)), Status: status.Code(respErr).String(), Size: len(respStr), + Body: respStr, } if respErr != nil { return out, respErr } - out.Body = respStr return out, nil } diff --git a/internal/rest/jsonpath.go b/internal/jsonpath/jsonpath.go similarity index 79% rename from internal/rest/jsonpath.go rename to internal/jsonpath/jsonpath.go index c0edd139..1829be90 100644 --- a/internal/rest/jsonpath.go +++ b/internal/jsonpath/jsonpath.go @@ -1,4 +1,4 @@ -package rest +package jsonpath import ( "encoding/json" @@ -6,7 +6,7 @@ import ( "github.com/PaesslerAG/jsonpath" ) -func GetJSONPATH(input string, path string) (interface{}, error) { +func Get(input string, path string) (interface{}, error) { v := interface{}(nil) if err := json.Unmarshal([]byte(input), &v); err != nil { return nil, err diff --git a/internal/rest/rest.go b/internal/rest/rest.go index 60ffbc44..70e76aeb 100644 --- a/internal/rest/rest.go +++ b/internal/rest/rest.go @@ -65,113 +65,9 @@ func (s *Service) SendRequest(requestID, activeEnvironmentID string) (*Response, return nil, err } - // handle post request - if err := s.handlePostRequest(r.Spec.HTTP.Request.PostRequest, response, activeEnvironment); err != nil { - return nil, err - } - return response, nil } -func (s *Service) handlePostRequest(r domain.PostRequest, response *Response, env *domain.Environment) error { - if r == (domain.PostRequest{}) { - return nil - } - - if response == nil { - return nil - } - - if r.Type == domain.PostRequestTypeSetEnv { - // only handle post request if the status code is the same as the one provided - if response.StatusCode != r.PostRequestSet.StatusCode { - return nil - } - - switch r.PostRequestSet.From { - case domain.PostRequestSetFromResponseBody: - if err := s.handlePostRequestFromBody(r, response, env); err != nil { - return err - } - case domain.PostRequestSetFromResponseHeader: - if err := s.handlePostRequestFromHeader(r, response, env); err != nil { - return err - } - case domain.PostRequestSetFromResponseCookie: - if err := s.handlePostRequestFromCookie(r, response, env); err != nil { - return err - } - } - } - - return nil -} - -func (s *Service) handlePostRequestFromBody(r domain.PostRequest, response *Response, env *domain.Environment) error { - // handle post request - if r.PostRequestSet.From != domain.PostRequestSetFromResponseBody { - return nil - } - - if response.JSON != "" && response.IsJSON { - data, err := GetJSONPATH(response.JSON, r.PostRequestSet.FromKey) - if err != nil { - return err - } - - if data == nil { - return nil - } - - if result, ok := data.(string); ok { - if env != nil { - env.SetKey(r.PostRequestSet.Target, result) - - if err := s.environments.UpdateEnvironment(env, state.SourceRestService, false); err != nil { - return err - } - } - } - } - return nil -} - -func (s *Service) handlePostRequestFromHeader(r domain.PostRequest, response *Response, env *domain.Environment) error { - if r.PostRequestSet.From != domain.PostRequestSetFromResponseHeader { - return nil - } - - if result, ok := response.Headers[r.PostRequestSet.FromKey]; ok { - if env != nil { - env.SetKey(r.PostRequestSet.Target, result) - - if err := s.environments.UpdateEnvironment(env, state.SourceRestService, false); err != nil { - return err - } - } - } - return nil -} - -func (s *Service) handlePostRequestFromCookie(r domain.PostRequest, response *Response, env *domain.Environment) error { - if r.PostRequestSet.From != domain.PostRequestSetFromResponseCookie { - return nil - } - - for _, c := range response.Cookies { - if c.Name == r.PostRequestSet.FromKey { - if env != nil { - env.SetKey(r.PostRequestSet.Target, c.Value) - - if err := s.environments.UpdateEnvironment(env, state.SourceRestService, false); err != nil { - return err - } - } - } - } - return nil -} - func (s *Service) sendRequest(req *domain.HTTPRequestSpec, e *domain.Environment) (*Response, error) { // prepare request // - apply environment diff --git a/internal/state/requests.go b/internal/state/requests.go index 1e3bd5ba..e327247f 100644 --- a/internal/state/requests.go +++ b/internal/state/requests.go @@ -166,6 +166,16 @@ func (m *Requests) GetRequests() []*domain.Request { return m.requests.Values() } +func (m *Requests) GetStandAloneRequests() []*domain.Request { + var standAloneRequests []*domain.Request + for _, req := range m.requests.Values() { + if req.CollectionID == "" { + standAloneRequests = append(standAloneRequests, req) + } + } + return standAloneRequests +} + func (m *Requests) GetCollections() []*domain.Collection { return m.collections.Values() } diff --git a/internal/state/state.go b/internal/state/state.go index b0acec23..46d2591d 100644 --- a/internal/state/state.go +++ b/internal/state/state.go @@ -15,7 +15,8 @@ const ( SourceView Source = "view" SourceFile Source = "file" SourceController Source = "controller" - SourceRestService Source = "reset-service" + SourceRestService Source = "rest-service" + SourceGRPCService Source = "grpc-service" ) var ErrNotFound = errors.New("ErrNotFound") diff --git a/ui/app/ui.go b/ui/app/ui.go index 390795cb..000199ec 100644 --- a/ui/app/ui.go +++ b/ui/app/ui.go @@ -6,9 +6,6 @@ import ( "image" "os" - "github.com/chapar-rest/chapar/internal/grpc" - "github.com/chapar-rest/chapar/ui/pages/protofiles" - "gioui.org/app" "gioui.org/layout" "gioui.org/op" @@ -18,6 +15,8 @@ import ( "gioui.org/widget/material" "github.com/chapar-rest/chapar/internal/domain" + "github.com/chapar-rest/chapar/internal/egress" + "github.com/chapar-rest/chapar/internal/grpc" "github.com/chapar-rest/chapar/internal/repository" "github.com/chapar-rest/chapar/internal/rest" "github.com/chapar-rest/chapar/internal/state" @@ -26,6 +25,7 @@ import ( "github.com/chapar-rest/chapar/ui/fonts" "github.com/chapar-rest/chapar/ui/pages/console" "github.com/chapar-rest/chapar/ui/pages/environments" + "github.com/chapar-rest/chapar/ui/pages/protofiles" "github.com/chapar-rest/chapar/ui/pages/requests" "github.com/chapar-rest/chapar/ui/pages/workspaces" ) @@ -98,6 +98,8 @@ func New(w *app.Window) (*UI, error) { grpcService := grpc.NewService(u.requestsState, u.environmentsState, u.protoFilesState) restService := rest.New(u.requestsState, u.environmentsState) + egressService := egress.New(u.requestsState, u.environmentsState, restService, grpcService) + theme := material.NewTheme() theme.Shaper = text.NewShaper(text.WithCollection(fontCollection)) // lest assume is dark theme, we will switch it later @@ -145,7 +147,7 @@ func New(w *app.Window) (*UI, error) { } u.requestsView = requests.NewView(w, u.Theme, explorerController) - u.requestsController = requests.NewController(u.requestsView, repo, u.requestsState, u.environmentsState, explorerController, restService, grpcService) + u.requestsController = requests.NewController(u.requestsView, repo, u.requestsState, u.environmentsState, explorerController, egressService, grpcService) u.header.OnSelectedWorkspaceChanged = func(ws *domain.Workspace) error { if err := repo.SetActiveWorkspace(ws); err != nil { diff --git a/ui/pages/requests/component/pre_post_request.go b/ui/pages/requests/component/pre_post_request.go index 3cf3bad4..c2da17de 100644 --- a/ui/pages/requests/component/pre_post_request.go +++ b/ui/pages/requests/component/pre_post_request.go @@ -1,6 +1,7 @@ package component import ( + "sort" "strconv" "gioui.org/layout" @@ -16,11 +17,14 @@ type PrePostRequest struct { dropDown *widgets.DropDown script *widgets.CodeEditor - dropDownItems []Option + actionDropDownItems []Option onScriptChanged func(script string) onDropDownChanged func(selected string) + triggerRequestForm *TriggerRequestForm + onTriggerRequestChange func(collectionID, requestID string) + setEnvForm *SetEnvForm onSetEnvFormChanged func(statusCode int, item, from, fromKey string) } @@ -33,12 +37,18 @@ type SetEnvForm struct { preview string } +type TriggerRequestForm struct { + collectionsDropDown *widgets.DropDown + requestDropDown *widgets.DropDown +} + const ( - TypeScript = "script" - TypeSetEnv = "set_env" - TypeShellScript = "shell_script" - TypeK8sTunnel = "kubectl_tunnel" - TypeSSHTunnel = "ssh_tunnel" + TypeScript = "script" + TypeSetEnv = "set_env" + TypeShellScript = "shell_script" + TypeK8sTunnel = "kubectl_tunnel" + TypeSSHTunnel = "ssh_tunnel" + TypeTriggerRequest = "trigger_request" ) type Option struct { @@ -48,18 +58,13 @@ type Option struct { Hint string } -func NewPrePostRequest(options []Option, theme *chapartheme.Theme) *PrePostRequest { +func NewPrePostRequest(actions []Option, setFormFromDropDown *widgets.DropDown, theme *chapartheme.Theme) *PrePostRequest { p := &PrePostRequest{ - dropDown: widgets.NewDropDown(theme), - script: widgets.NewCodeEditor("", widgets.CodeLanguagePython, theme), - dropDownItems: options, + dropDown: widgets.NewDropDown(theme), + script: widgets.NewCodeEditor("", widgets.CodeLanguagePython, theme), + actionDropDownItems: actions, setEnvForm: &SetEnvForm{ - fromDropDown: widgets.NewDropDown( - theme, - widgets.NewDropDownOption("From Response").WithValue(domain.PostRequestSetFromResponseBody), - widgets.NewDropDownOption("From Header").WithValue(domain.PostRequestSetFromResponseHeader), - widgets.NewDropDownOption("From Cookie").WithValue(domain.PostRequestSetFromResponseCookie), - ), + fromDropDown: setFormFromDropDown, statusCodeEditor: &widgets.LabeledInput{ Label: "Status Code", SpaceBetween: 5, @@ -83,11 +88,21 @@ func NewPrePostRequest(options []Option, theme *chapartheme.Theme) *PrePostReque Hint: "e.g. name", }, }, + triggerRequestForm: &TriggerRequestForm{ + collectionsDropDown: widgets.NewDropDown(theme), + requestDropDown: widgets.NewDropDown(theme), + }, } - p.setEnvForm.fromDropDown.MaxWidth = unit.Dp(150) - opts := make([]*widgets.DropDownOption, 0, len(options)) - for _, o := range options { + if setFormFromDropDown != nil { + p.setEnvForm.fromDropDown.MaxWidth = unit.Dp(150) + } + + p.triggerRequestForm.requestDropDown.MaxWidth = unit.Dp(150) + p.triggerRequestForm.collectionsDropDown.MaxWidth = unit.Dp(150) + + opts := make([]*widgets.DropDownOption, 0, len(actions)) + for _, o := range actions { opts = append(opts, widgets.NewDropDownOption(o.Title).WithValue(o.Value)) } @@ -96,6 +111,45 @@ func NewPrePostRequest(options []Option, theme *chapartheme.Theme) *PrePostReque return p } +func (p *PrePostRequest) SetCollections(collections []*domain.Collection, selectedID string) { + sort.Slice(collections, func(i, j int) bool { + return collections[i].MetaData.Name < collections[j].MetaData.Name + }) + + opts := make([]*widgets.DropDownOption, 0, len(collections)+1) + opts = append(opts, widgets.NewDropDownOption("None").WithValue("none")) + for _, c := range collections { + opts = append(opts, widgets.NewDropDownOption(c.MetaData.Name).WithValue(c.MetaData.ID)) + } + p.triggerRequestForm.collectionsDropDown.SetOptions(opts...) + p.triggerRequestForm.collectionsDropDown.SetSelectedByValue(selectedID) +} + +func (p *PrePostRequest) SetRequests(requests []*domain.Request, selectedID string) { + sort.Slice(requests, func(i, j int) bool { + return requests[i].MetaData.Name < requests[j].MetaData.Name + }) + + opts := make([]*widgets.DropDownOption, 0, len(requests)+1) + opts = append(opts, widgets.NewDropDownOption("None").WithValue("none")) + for _, r := range requests { + opts = append(opts, widgets.NewDropDownOption(r.MetaData.Name).WithValue(r.MetaData.ID)) + } + p.triggerRequestForm.requestDropDown.SetOptions(opts...) + p.triggerRequestForm.requestDropDown.SetSelectedByValue(selectedID) +} + +func (p *PrePostRequest) SetOnTriggerRequestChanged(f func(collectionID, requestID string)) { + p.onTriggerRequestChange = f + p.triggerRequestForm.collectionsDropDown.SetOnChanged(func(selected string) { + p.onTriggerRequestChange(selected, p.triggerRequestForm.requestDropDown.GetSelected().GetValue()) + }) + + p.triggerRequestForm.requestDropDown.SetOnChanged(func(selected string) { + p.onTriggerRequestChange(p.triggerRequestForm.collectionsDropDown.GetSelected().GetValue(), selected) + }) +} + func (p *PrePostRequest) SetOnScriptChanged(f func(script string)) { p.onScriptChanged = f p.script.SetOnChanged(p.onScriptChanged) @@ -119,6 +173,10 @@ func (p *PrePostRequest) SetPreview(preview string) { } func (p *PrePostRequest) SetPostRequestSetValues(set domain.PostRequestSet) { + if p.setEnvForm.fromDropDown == nil { + return + } + p.setEnvForm.statusCodeEditor.SetText(strconv.Itoa(set.StatusCode)) p.setEnvForm.targetEditor.SetText(set.Target) p.setEnvForm.fromEditor.SetText(set.FromKey) @@ -126,6 +184,10 @@ func (p *PrePostRequest) SetPostRequestSetValues(set domain.PostRequestSet) { } func (p *PrePostRequest) SetOnPostRequestSetChanged(f func(statusCode int, item, from, fromKey string)) { + if p.setEnvForm.fromDropDown == nil { + return + } + p.onSetEnvFormChanged = f p.setEnvForm.fromDropDown.SetOnChanged(func(selected string) { statusCode, _ := strconv.Atoi(p.setEnvForm.statusCodeEditor.Text()) @@ -159,7 +221,7 @@ func (p *PrePostRequest) Layout(gtx layout.Context, theme *chapartheme.Theme) la }), layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { selectedIndex := p.dropDown.SelectedIndex() - selectedItem := p.dropDownItems[selectedIndex] + selectedItem := p.actionDropDownItems[selectedIndex] switch selectedItem.Type { case TypeScript: @@ -167,7 +229,12 @@ func (p *PrePostRequest) Layout(gtx layout.Context, theme *chapartheme.Theme) la return p.script.Layout(gtx, theme, selectedItem.Hint) }) case TypeSetEnv: + if p.setEnvForm.fromDropDown == nil { + return layout.Dimensions{} + } return p.SetEnvForm(gtx, theme) + case TypeTriggerRequest: + return p.TriggerRequestForm(gtx, theme) } return layout.Dimensions{} }), @@ -247,3 +314,44 @@ func (p *PrePostRequest) SetEnvForm(gtx layout.Context, theme *chapartheme.Theme }), ) } + +func (p *PrePostRequest) TriggerRequestForm(gtx layout.Context, theme *chapartheme.Theme) layout.Dimensions { + topButtonInset := layout.Inset{Top: unit.Dp(8), Bottom: unit.Dp(4)} + + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return layout.Flex{ + Axis: layout.Horizontal, + Alignment: layout.Middle, + }.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + gtx.Constraints.Min.X = gtx.Dp(85) + return material.Label(theme.Material(), theme.TextSize, "Collection").Layout(gtx) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + p.triggerRequestForm.collectionsDropDown.MinWidth = unit.Dp(162) + return topButtonInset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return p.triggerRequestForm.collectionsDropDown.Layout(gtx, theme) + }) + }), + ) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return layout.Flex{ + Axis: layout.Horizontal, + Alignment: layout.Middle, + }.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + gtx.Constraints.Min.X = gtx.Dp(85) + return material.Label(theme.Material(), theme.TextSize, "Request").Layout(gtx) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + p.triggerRequestForm.requestDropDown.MinWidth = unit.Dp(162) + return topButtonInset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return p.triggerRequestForm.requestDropDown.Layout(gtx, theme) + }) + }), + ) + }), + ) +} diff --git a/ui/pages/requests/container.go b/ui/pages/requests/container.go index 214119e7..c89a192a 100644 --- a/ui/pages/requests/container.go +++ b/ui/pages/requests/container.go @@ -35,10 +35,18 @@ type GrpcContainer interface { SetResponseLoading(loading bool) SetOnInvoke(f func(id string)) SetResponse(response domain.GRPCResponseDetail) + GetResponse() *domain.GRPCResponseDetail SetOnLoadRequestExample(f func(id string)) SetRequestBody(body string) ShowRequestPrompt(title, content, modalType string, onSubmit func(selectedOption string, remember bool), options ...widgets.Option) HideRequestPrompt() + SetPostRequestSetValues(set domain.PostRequestSet) + SetOnPostRequestSetChanged(f func(id string, statusCode int, item, from, fromKey string)) + SetPreRequestCollections(collections []*domain.Collection, selectedID string) + SetPreRequestRequests(requests []*domain.Request, selectedID string) + SetOnSetOnTriggerRequestChanged(f func(id, collectionID, requestID string)) + SetPostRequestSetPreview(preview string) + SetOnRequestTabChange(f func(id, tab string)) } type RestContainer interface { @@ -57,4 +65,8 @@ type RestContainer interface { SetBinaryBodyFilePath(filePath string) SetOnFormDataFileSelect(f func(requestId, fieldId string)) AddFileToFormData(fieldId, filePath string) + SetPreRequestCollections(collections []*domain.Collection, selectedID string) + SetPreRequestRequests(requests []*domain.Request, selectedID string) + SetOnSetOnTriggerRequestChanged(f func(id, collectionID, requestID string)) + SetOnRequestTabChange(f func(id, tab string)) } diff --git a/ui/pages/requests/controller.go b/ui/pages/requests/controller.go index ecdd9cad..0ca2753d 100644 --- a/ui/pages/requests/controller.go +++ b/ui/pages/requests/controller.go @@ -12,8 +12,10 @@ import ( "gioui.org/layout" "github.com/chapar-rest/chapar/internal/domain" + "github.com/chapar-rest/chapar/internal/egress" "github.com/chapar-rest/chapar/internal/grpc" "github.com/chapar-rest/chapar/internal/importer" + "github.com/chapar-rest/chapar/internal/jsonpath" "github.com/chapar-rest/chapar/internal/repository" "github.com/chapar-rest/chapar/internal/rest" "github.com/chapar-rest/chapar/internal/state" @@ -31,11 +33,11 @@ type Controller struct { explorer *explorer.Explorer - restService *rest.Service - grpcService *grpc.Service + grpcService *grpc.Service + egressService *egress.Service } -func NewController(view *View, repo repository.Repository, model *state.Requests, envState *state.Environments, explorer *explorer.Explorer, restService *rest.Service, grpcService *grpc.Service) *Controller { +func NewController(view *View, repo repository.Repository, model *state.Requests, envState *state.Environments, explorer *explorer.Explorer, egressService *egress.Service, grpcService *grpc.Service) *Controller { c := &Controller{ view: view, model: model, @@ -44,8 +46,8 @@ func NewController(view *View, repo repository.Repository, model *state.Requests explorer: explorer, - restService: restService, - grpcService: grpcService, + egressService: egressService, + grpcService: grpcService, } view.SetOnNewRequest(c.onNewRequest) @@ -66,6 +68,8 @@ func NewController(view *View, repo repository.Repository, model *state.Requests view.SetOnServerInfoReload(c.onServerInfoReload) view.SetOnGrpcInvoke(c.onGrpcInvoke) view.SetOnGrpcLoadRequestExample(c.onLoadRequestExample) + view.SetOnSetOnTriggerRequestChanged(c.onSetOnTriggerRequestChanged) + view.SetOnRequestTabChange(c.onRequestTabChange) return c } @@ -137,7 +141,7 @@ func (c *Controller) onGrpcInvoke(id string) { c.view.SetSendingRequestLoading(id) defer c.view.SetSendingRequestLoaded(id) - resp, err := c.grpcService.Invoke(id, c.getActiveEnvID()) + res, err := c.egressService.Send(id, c.getActiveEnvID()) if err != nil { c.view.SetGRPCResponse(id, domain.GRPCResponseDetail{ Error: err, @@ -145,6 +149,11 @@ func (c *Controller) onGrpcInvoke(id string) { return } + resp, ok := res.(*grpc.Response) + if !ok { + panic("invalid response type") + } + c.view.SetGRPCResponse(id, domain.GRPCResponseDetail{ Response: resp.Body, Metadata: resp.Metadata, @@ -198,35 +207,103 @@ func (c *Controller) onPostRequestSetChanged(id string, statusCode int, item, fr clone := req.Clone() clone.MetaData.ID = id - clone.Spec.HTTP.Request.PostRequest.PostRequestSet = domain.PostRequestSet{ + // Initialize PostRequestSet + postRequestSet := domain.PostRequestSet{ Target: item, StatusCode: statusCode, From: from, FromKey: fromKey, } - c.onRequestDataChanged(id, clone) - responseData := c.view.GetHTTPResponse(id) - if responseData == nil { - return + // Assign the PostRequestSet based on request type + switch req.MetaData.Type { + case domain.RequestTypeHTTP: + clone.Spec.HTTP.Request.PostRequest.PostRequestSet = postRequestSet + case domain.RequestTypeGRPC: + clone.Spec.GRPC.PostRequest.PostRequestSet = postRequestSet + default: + return // Unknown request type, exit early } - if responseData.Response == "" { - return + // Update the request data + c.onRequestDataChanged(id, clone) + + var ( + responseFrom string + response string + headers []domain.KeyValue + cookies []domain.KeyValue + metaData []domain.KeyValue + trailers []domain.KeyValue + ) + + switch req.MetaData.Type { + case domain.RequestTypeHTTP: + responseData := c.view.GetHTTPResponse(id) + if responseData == nil || responseData.Response == "" { + return + } + response = responseData.Response + headers = responseData.Headers + cookies = responseData.Cookies + responseFrom = clone.Spec.HTTP.Request.PostRequest.PostRequestSet.From + case domain.RequestTypeGRPC: + responseData := c.view.GetGRPCResponse(id) + if responseData == nil || responseData.Response == "" { + return + } + response = responseData.Response + headers = responseData.Metadata + metaData = responseData.Metadata + trailers = responseData.Trailers + responseFrom = clone.Spec.GRPC.PostRequest.PostRequestSet.From } - switch clone.Spec.HTTP.Request.PostRequest.PostRequestSet.From { + switch responseFrom { case domain.PostRequestSetFromResponseBody: - c.setPreviewFromResponse(id, responseData, fromKey) + c.setPreviewFromResponse(id, response, fromKey) case domain.PostRequestSetFromResponseHeader: - c.setPreviewFromHeader(id, responseData, fromKey) + c.setPreviewFromKeyValue(id, headers, fromKey) case domain.PostRequestSetFromResponseCookie: - c.setPreviewFromCookie(id, responseData, fromKey) + c.setPreviewFromKeyValue(id, cookies, fromKey) + case domain.PostRequestSetFromResponseMetaData: + c.setPreviewFromKeyValue(id, metaData, fromKey) + case domain.PostRequestSetFromResponseTrailers: + c.setPreviewFromKeyValue(id, trailers, fromKey) } } -func (c *Controller) setPreviewFromResponse(id string, responseData *domain.HTTPResponseDetail, fromKey string) { - resp, err := rest.GetJSONPATH(responseData.Response, fromKey) +func (c *Controller) onSetOnTriggerRequestChanged(id, collectionID, requestID string) { + req := c.model.GetRequest(id) + if req == nil { + return + } + + // break the reference + clone := req.Clone() + clone.MetaData.ID = id + + triggerRequest := &domain.TriggerRequest{ + CollectionID: collectionID, + RequestID: requestID, + } + + // Assign the PostRequestSet based on request type + switch req.MetaData.Type { + case domain.RequestTypeHTTP: + clone.Spec.HTTP.Request.PreRequest.TriggerRequest = triggerRequest + case domain.RequestTypeGRPC: + clone.Spec.GRPC.PreRequest.TriggerRequest = triggerRequest + default: + return // Unknown request type, exit early + } + + // Update the request data + c.onRequestDataChanged(id, clone) +} + +func (c *Controller) setPreviewFromResponse(id string, response, fromKey string) { + resp, err := jsonpath.Get(response, fromKey) if err != nil { // TODO show error without interrupting the user fmt.Println("failed to get data from response, %w", err) @@ -245,8 +322,8 @@ func (c *Controller) setPreviewFromResponse(id string, responseData *domain.HTTP } } -func (c *Controller) setPreviewFromHeader(id string, responseData *domain.HTTPResponseDetail, fromKey string) { - for _, header := range responseData.Headers { +func (c *Controller) setPreviewFromKeyValue(id string, kv []domain.KeyValue, fromKey string) { + for _, header := range kv { if header.Key == fromKey { c.view.SetPostRequestSetPreview(id, header.Value) return @@ -254,15 +331,6 @@ func (c *Controller) setPreviewFromHeader(id string, responseData *domain.HTTPRe } } -func (c *Controller) setPreviewFromCookie(id string, responseData *domain.HTTPResponseDetail, fromKey string) { - for _, cookie := range responseData.Cookies { - if cookie.Key == fromKey { - c.view.SetPostRequestSetPreview(id, cookie.Value) - return - } - } -} - func (c *Controller) onTitleChanged(id string, title, containerType string) { switch containerType { case TypeRequest: @@ -291,15 +359,13 @@ func (c *Controller) onCopyResponse(gtx layout.Context, dataType, data string) { }) c.view.showNotification(fmt.Sprintf("%s copied to clipboard", dataType), 2*time.Second) - - // notify.Send(fmt.Sprintf("%s copied to clipboard", dataType), 2*time.Second) } func (c *Controller) onSubmitRequest(id string) { c.view.SetSendingRequestLoading(id) defer c.view.SetSendingRequestLoaded(id) - res, err := c.restService.SendRequest(id, c.getActiveEnvID()) + egRes, err := c.egressService.Send(id, c.getActiveEnvID()) if err != nil { c.view.SetHTTPResponse(id, domain.HTTPResponseDetail{ Error: err, @@ -307,6 +373,11 @@ func (c *Controller) onSubmitRequest(id string) { return } + res, ok := egRes.(*rest.Response) + if !ok { + panic("invalid response type") + } + resp := string(res.Body) if res.IsJSON { resp = res.JSON @@ -380,6 +451,7 @@ func (c *Controller) onRequestDataChanged(id string, data any) { return } + c.checkForPreRequestParams(id, req, inComingRequest) c.checkForHTTPRequestParams(req, inComingRequest) // break the reference @@ -401,6 +473,44 @@ func (c *Controller) onRequestDataChanged(id string, data any) { c.view.SetTreeViewNodePrefix(id, req) } +func (c *Controller) checkForPreRequestParams(id string, req *domain.Request, inComingRequest *domain.Request) { + var ( + reqType string + preReq *domain.PreRequest + incomingPreReq *domain.PreRequest + ) + + if req.MetaData.Type == domain.RequestTypeHTTP { + reqType = domain.RequestTypeHTTP + preReq = &req.Spec.HTTP.Request.PreRequest + incomingPreReq = &inComingRequest.Spec.HTTP.Request.PreRequest + } else if req.MetaData.Type == domain.RequestTypeGRPC { + reqType = domain.RequestTypeGRPC + preReq = &req.Spec.GRPC.PreRequest + incomingPreReq = &inComingRequest.Spec.GRPC.PreRequest + } + + if reqType != "" && (preReq.Type != incomingPreReq.Type || incomingPreReq.Type == domain.PrePostTypeTriggerRequest) { + var ( + collectionID = domain.PrePostTypeNone + requestID string + ) + + if incomingPreReq.TriggerRequest != nil { + collectionID = incomingPreReq.TriggerRequest.CollectionID + requestID = incomingPreReq.TriggerRequest.RequestID + } + + c.view.SetPreRequestCollections(id, c.model.GetCollections(), collectionID) + if collectionID != domain.PrePostTypeNone { + requests := c.model.GetCollection(collectionID).Spec.Requests + c.view.SetPreRequestRequests(id, requests, requestID) + } else { + c.view.SetPreRequestRequests(id, c.model.GetStandAloneRequests(), requestID) + } + } +} + func (c *Controller) checkForHTTPRequestParams(req *domain.Request, inComingRequest *domain.Request) { if req.MetaData.Type != domain.RequestTypeHTTP { return @@ -857,3 +967,44 @@ func (c *Controller) deleteCollection(id string) { c.view.RemoveTreeViewNode(id) c.view.CloseTab(id) } + +func (c *Controller) onRequestTabChange(id, tab string) { + if tab != "Pre Request" { + return + } + + req := c.model.GetRequest(id) + if req == nil { + return + } + + var ( + collectionID = domain.PrePostTypeNone + requestID string + + preRequest *domain.PreRequest + ) + + if req.MetaData.Type == domain.RequestTypeHTTP { + preRequest = &req.Spec.HTTP.Request.PreRequest + } else if req.MetaData.Type == domain.RequestTypeGRPC { + preRequest = &req.Spec.GRPC.PreRequest + } + + if preRequest == nil { + return + } + + if preRequest.TriggerRequest != nil { + collectionID = preRequest.TriggerRequest.CollectionID + requestID = preRequest.TriggerRequest.RequestID + } + + c.view.SetPreRequestCollections(id, c.model.GetCollections(), collectionID) + if collectionID != domain.PrePostTypeNone { + requests := c.model.GetCollection(collectionID).Spec.Requests + c.view.SetPreRequestRequests(id, requests, requestID) + } else { + c.view.SetPreRequestRequests(id, c.model.GetRequests(), requestID) + } +} diff --git a/ui/pages/requests/grpc/grpc.go b/ui/pages/requests/grpc/grpc.go index f3b07838..fb3bc345 100644 --- a/ui/pages/requests/grpc/grpc.go +++ b/ui/pages/requests/grpc/grpc.go @@ -100,6 +100,16 @@ func (r *Grpc) setupHooks() { r.onDataChanged(r.Req.MetaData.ID, r.Req) }) + r.Request.PreRequest.SetOnDropDownChanged(func(selected string) { + r.Req.Spec.GRPC.PreRequest.Type = selected + r.onDataChanged(r.Req.MetaData.ID, r.Req) + }) + + r.Request.PostRequest.SetOnDropDownChanged(func(selected string) { + r.Req.Spec.GRPC.PostRequest.Type = selected + r.onDataChanged(r.Req.MetaData.ID, r.Req) + }) + r.Request.ServerInfo.FileSelector.SetOnChanged(func(filePath string) { protoFiles := r.Req.Spec.GRPC.ServerInfo.ProtoFiles if r.Req.Spec.GRPC.ServerInfo.ProtoFiles == nil || filePath == "" { @@ -118,6 +128,12 @@ func (r *Grpc) setupHooks() { }) } +func (r *Grpc) SetOnRequestTabChange(f func(id, tab string)) { + r.Request.OnTabChange = func(title string) { + f(r.Req.MetaData.ID, title) + } +} + func convertSettingsToItems(values map[string]any) domain.Settings { out := domain.Settings{ Insecure: false, @@ -152,6 +168,30 @@ func convertSettingsToItems(values map[string]any) domain.Settings { return out } +func (r *Grpc) SetPostRequestSetValues(set domain.PostRequestSet) { + r.Request.PostRequest.SetPostRequestSetValues(set) +} + +func (r *Grpc) SetOnPostRequestSetChanged(f func(id string, statusCode int, item, from, fromKey string)) { + r.Request.PostRequest.SetOnPostRequestSetChanged(func(statusCode int, item, from, fromKey string) { + f(r.Req.MetaData.ID, statusCode, item, from, fromKey) + }) +} + +func (r *Grpc) SetPreRequestCollections(collections []*domain.Collection, selectedID string) { + r.Request.PreRequest.SetCollections(collections, selectedID) +} + +func (r *Grpc) SetPreRequestRequests(requests []*domain.Request, selectedID string) { + r.Request.PreRequest.SetRequests(requests, selectedID) +} + +func (r *Grpc) SetOnSetOnTriggerRequestChanged(f func(id, collectionID, requestID string)) { + r.Request.PreRequest.SetOnTriggerRequestChanged(func(collectionID, requestID string) { + f(r.Req.MetaData.ID, collectionID, requestID) + }) +} + func (r *Grpc) SetOnProtoFileSelect(f func(id string)) { r.Request.ServerInfo.FileSelector.SetOnSelectFile(func() { f(r.Req.MetaData.ID) @@ -222,6 +262,18 @@ func (r *Grpc) SetResponse(detail domain.GRPCResponseDetail) { r.Response.SetStatusParams(detail.StatusCode, detail.Status, detail.Duration, detail.Size) } +func (r *Grpc) GetResponse() *domain.GRPCResponseDetail { + return &domain.GRPCResponseDetail{ + Response: r.Response.response, + Metadata: r.Response.Metadata.GetData(), + Trailers: r.Response.Trailers.GetData(), + } +} + +func (r *Grpc) SetPostRequestSetPreview(preview string) { + r.Request.PostRequest.SetPreview(preview) +} + func (r *Grpc) SetOnSave(f func(id string)) { r.onSave = f r.Breadcrumb.SetOnSave(f) diff --git a/ui/pages/requests/grpc/request.go b/ui/pages/requests/grpc/request.go index 5918081e..be5efee7 100644 --- a/ui/pages/requests/grpc/request.go +++ b/ui/pages/requests/grpc/request.go @@ -22,6 +22,12 @@ type Request struct { Metadata *widgets.KeyValue Auth *component.Auth Settings *widgets.Settings + + PreRequest *component.PrePostRequest + PostRequest *component.PrePostRequest + + currentTab string + OnTabChange func(title string) } func NewRequest(req *domain.Request, theme *chapartheme.Theme, explorer *explorer.Explorer) *Request { @@ -31,6 +37,13 @@ func NewRequest(req *domain.Request, theme *chapartheme.Theme, explorer *explore certExt := []string{"pem", "crt"} + postRequestDropDown := widgets.NewDropDown( + theme, + widgets.NewDropDownOption("From Response").WithValue(domain.PostRequestSetFromResponseBody), + widgets.NewDropDownOption("From Metadata").WithValue(domain.PostRequestSetFromResponseMetaData), + widgets.NewDropDownOption("From Trailers").WithValue(domain.PostRequestSetFromResponseTrailers), + ) + r := &Request{ Prompt: widgets.NewPrompt("Failed", "foo bar", widgets.ModalTypeErr), Tabs: widgets.NewTabs([]*widgets.Tab{ @@ -39,6 +52,8 @@ func NewRequest(req *domain.Request, theme *chapartheme.Theme, explorer *explore {Title: "Auth"}, {Title: "Meta Data"}, {Title: "Settings"}, + {Title: "Pre Request"}, + {Title: "Post Request"}, }, nil), ServerInfo: NewServerInfo(explorer, req.Spec.GRPC.ServerInfo), Body: widgets.NewCodeEditor(req.Spec.GRPC.Body, widgets.CodeLanguageJSON, theme), @@ -54,6 +69,32 @@ func NewRequest(req *domain.Request, theme *chapartheme.Theme, explorer *explore widgets.NewTextItem("Overwrite server name for certificate verification", "nameOverride", "The value used to validate the common name in the server certificate.", req.Spec.GRPC.Settings.NameOverride).SetVisibleWhen(visibilityFunc), widgets.NewNumberItem("Timeout", "timeoutMilliseconds", "Timeout for the request in milliseconds", req.Spec.GRPC.Settings.TimeoutMilliseconds), }), + PreRequest: component.NewPrePostRequest([]component.Option{ + {Title: "None", Value: domain.PrePostTypeNone}, + {Title: "Trigger request", Value: domain.PrePostTypeTriggerRequest, Type: component.TypeTriggerRequest, Hint: "Trigger another request"}, + // {Title: "Python", Value: domain.PostRequestTypePythonScript, Type: component.TypeScript, Hint: "Write your pre request python script here"}, + // {Title: "Shell Script", Value: domain.PostRequestTypeSSHTunnel, Type: component.TypeScript, Hint: "Write your pre request shell script here"}, + // {Title: "Kubectl tunnel", Value: domain.PostRequestTypeK8sTunnel, Type: component.TypeScript, Hint: "Run kubectl port-forward command"}, + // {Title: "SSH tunnel", Value: domain.PostRequestTypeSSHTunnel, Type: component.TypeScript, Hint: "Run ssh command"}, + }, nil, theme), + PostRequest: component.NewPrePostRequest([]component.Option{ + {Title: "None", Value: domain.PrePostTypeNone}, + {Title: "Set Environment Variable", Value: domain.PrePostTypeSetEnv, Type: component.TypeSetEnv, Hint: "Set environment variable"}, + // {Title: "Python", Value: domain.PostRequestTypePythonScript, Type: component.TypeScript, Hint: "Write your post request python script here"}, + // {Title: "Shell Script", Value: domain.PostRequestTypeShellScript, Type: component.TypeScript, Hint: "Write your post request shell script here"}, + }, postRequestDropDown, theme), + } + + if req.Spec.GRPC.PreRequest != (domain.PreRequest{}) { + r.PreRequest.SetSelectedDropDown(req.Spec.GRPC.PreRequest.Type) + } + + if req.Spec.GRPC.PostRequest != (domain.PostRequest{}) { + r.PostRequest.SetSelectedDropDown(req.Spec.GRPC.PostRequest.Type) + + if req.Spec.GRPC.PostRequest.PostRequestSet != (domain.PostRequestSet{}) { + r.PostRequest.SetPostRequestSetValues(req.Spec.GRPC.PostRequest.PostRequestSet) + } } return r @@ -73,6 +114,12 @@ func (r *Request) Layout(gtx layout.Context, theme *chapartheme.Theme) layout.Di return r.Prompt.Layout(gtx, theme) }), layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { + if r.Tabs.SelectedTab().Title != r.currentTab { + r.currentTab = r.Tabs.SelectedTab().Title + if r.OnTabChange != nil { + r.OnTabChange(r.currentTab) + } + } switch r.Tabs.SelectedTab().Title { case "Server Info": return layout.Inset{Top: unit.Dp(5), Right: unit.Dp(10)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { @@ -90,6 +137,10 @@ func (r *Request) Layout(gtx layout.Context, theme *chapartheme.Theme) layout.Di return r.Auth.Layout(gtx, theme) case "Settings": return r.Settings.Layout(gtx, theme) + case "Pre Request": + return r.PreRequest.Layout(gtx, theme) + case "Post Request": + return r.PostRequest.Layout(gtx, theme) default: return layout.Dimensions{} } diff --git a/ui/pages/requests/restful/request.go b/ui/pages/requests/restful/request.go index fdcde7aa..9025161d 100644 --- a/ui/pages/requests/restful/request.go +++ b/ui/pages/requests/restful/request.go @@ -21,31 +21,43 @@ type Request struct { Params *Params Headers *Headers Auth *component.Auth + + currentTab string + OnTabChange func(title string) } func NewRequest(req *domain.Request, explorer *explorer.Explorer, theme *chapartheme.Theme) *Request { + + postRequestDropDown := widgets.NewDropDown( + theme, + widgets.NewDropDownOption("From Response").WithValue(domain.PostRequestSetFromResponseBody), + widgets.NewDropDownOption("From Header").WithValue(domain.PostRequestSetFromResponseHeader), + widgets.NewDropDownOption("From Cookie").WithValue(domain.PostRequestSetFromResponseCookie), + ) + r := &Request{ Tabs: widgets.NewTabs([]*widgets.Tab{ {Title: "Params"}, {Title: "Body"}, {Title: "Auth"}, {Title: "Headers"}, - // {Title: "Pre Request"}, + {Title: "Pre Request"}, {Title: "Post Request"}, }, nil), - // PreRequest: component.NewPrePostRequest([]component.Option{ - // {Title: "None", Value: domain.PostRequestTypeNone}, - // {Title: "Python", Value: domain.PostRequestTypePythonScript, Type: component.TypeScript, Hint: "Write your pre request python script here"}, - // {Title: "Shell Script", Value: domain.PostRequestTypeSSHTunnel, Type: component.TypeScript, Hint: "Write your pre request shell script here"}, - // {Title: "Kubectl tunnel", Value: domain.PostRequestTypeK8sTunnel, Type: component.TypeScript, Hint: "Run kubectl port-forward command"}, - // {Title: "SSH tunnel", Value: domain.PostRequestTypeSSHTunnel, Type: component.TypeScript, Hint: "Run ssh command"}, - // }, theme), + PreRequest: component.NewPrePostRequest([]component.Option{ + {Title: "None", Value: domain.PrePostTypeNone}, + {Title: "Trigger request", Value: domain.PrePostTypeTriggerRequest, Type: component.TypeTriggerRequest, Hint: "Trigger another request"}, + // {Title: "Python", Value: domain.PostRequestTypePythonScript, Type: component.TypeScript, Hint: "Write your pre request python script here"}, + // {Title: "Shell Script", Value: domain.PostRequestTypeSSHTunnel, Type: component.TypeScript, Hint: "Write your pre request shell script here"}, + // {Title: "Kubectl tunnel", Value: domain.PostRequestTypeK8sTunnel, Type: component.TypeScript, Hint: "Run kubectl port-forward command"}, + // {Title: "SSH tunnel", Value: domain.PostRequestTypeSSHTunnel, Type: component.TypeScript, Hint: "Run ssh command"}, + }, nil, theme), PostRequest: component.NewPrePostRequest([]component.Option{ - {Title: "None", Value: domain.PostRequestTypeNone}, - {Title: "Set Environment Variable", Value: domain.PostRequestTypeSetEnv, Type: component.TypeSetEnv, Hint: "Set environment variable"}, + {Title: "None", Value: domain.PrePostTypeNone}, + {Title: "Set Environment Variable", Value: domain.PrePostTypeSetEnv, Type: component.TypeSetEnv, Hint: "Set environment variable"}, // {Title: "Python", Value: domain.PostRequestTypePythonScript, Type: component.TypeScript, Hint: "Write your post request python script here"}, // {Title: "Shell Script", Value: domain.PostRequestTypeShellScript, Type: component.TypeScript, Hint: "Write your post request shell script here"}, - }, theme), + }, postRequestDropDown, theme), Body: NewBody(req.Spec.HTTP.Request.Body, theme, explorer), Params: NewParams(nil, nil), @@ -58,10 +70,10 @@ func NewRequest(req *domain.Request, explorer *explorer.Explorer, theme *chapart r.Params.SetPathParams(req.Spec.HTTP.Request.PathParams) r.Headers.SetHeaders(req.Spec.HTTP.Request.Headers) - // if req.Spec.HTTP.Request.PreRequest != (domain.PreRequest{}) { - // r.PreRequest.SetSelectedDropDown(req.Spec.HTTP.Request.PreRequest.Type) - // r.PreRequest.SetCode(req.Spec.HTTP.Request.PreRequest.Script) - // } + if req.Spec.HTTP.Request.PreRequest != (domain.PreRequest{}) { + r.PreRequest.SetSelectedDropDown(req.Spec.HTTP.Request.PreRequest.Type) + // r.PreRequest.SetCode(req.Spec.HTTP.Request.PreRequest.Script) + } if req.Spec.HTTP.Request.PostRequest != (domain.PostRequest{}) { r.PostRequest.SetSelectedDropDown(req.Spec.HTTP.Request.PostRequest.Type) @@ -87,9 +99,16 @@ func (r *Request) Layout(gtx layout.Context, theme *chapartheme.Theme) layout.Di return r.Tabs.Layout(gtx, theme) }), layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if r.Tabs.SelectedTab().Title != r.currentTab { + r.currentTab = r.Tabs.SelectedTab().Title + if r.OnTabChange != nil { + r.OnTabChange(r.currentTab) + } + } + switch r.Tabs.SelectedTab().Title { - // case "Pre Request": - // return r.PreRequest.Layout(gtx, theme) + case "Pre Request": + return r.PreRequest.Layout(gtx, theme) case "Post Request": return r.PostRequest.Layout(gtx, theme) case "Params": diff --git a/ui/pages/requests/restful/restful.go b/ui/pages/requests/restful/restful.go index d00319e2..92e5ee9c 100644 --- a/ui/pages/requests/restful/restful.go +++ b/ui/pages/requests/restful/restful.go @@ -49,6 +49,20 @@ func New(req *domain.Request, theme *chapartheme.Theme, explorer *explorer.Explo return r } +func (r *Restful) SetPreRequestCollections(collections []*domain.Collection, selectedID string) { + r.Request.PreRequest.SetCollections(collections, selectedID) +} + +func (r *Restful) SetPreRequestRequests(requests []*domain.Request, selectedID string) { + r.Request.PreRequest.SetRequests(requests, selectedID) +} + +func (r *Restful) SetOnSetOnTriggerRequestChanged(f func(id, collectionID, requestID string)) { + r.Request.PreRequest.SetOnTriggerRequestChanged(func(collectionID, requestID string) { + f(r.Req.MetaData.ID, collectionID, requestID) + }) +} + func (r *Restful) SetPostRequestSetValues(set domain.PostRequestSet) { r.Request.PostRequest.SetPostRequestSetValues(set) } @@ -194,10 +208,10 @@ func (r *Restful) setupHooks() { r.onDataChanged(r.Req.MetaData.ID, r.Req) }) - // r.Request.PreRequest.SetOnDropDownChanged(func(selected string) { - // r.Req.Spec.HTTP.Request.PreRequest.Type = selected - // r.onDataChanged(r.Req.MetaData.ID, r.Req) - // }) + r.Request.PreRequest.SetOnDropDownChanged(func(selected string) { + r.Req.Spec.HTTP.Request.PreRequest.Type = selected + r.onDataChanged(r.Req.MetaData.ID, r.Req) + }) // r.Request.PreRequest.SetOnScriptChanged(func(code string) { // r.Req.Spec.HTTP.Request.PreRequest.Script = code @@ -220,6 +234,12 @@ func (r *Restful) setupHooks() { }) } +func (r *Restful) SetOnRequestTabChange(f func(id, tab string)) { + r.Request.OnTabChange = func(title string) { + f(r.Req.MetaData.ID, title) + } +} + func (r *Restful) SetQueryParams(params []domain.KeyValue) { r.Request.Params.SetQueryParams(params) } diff --git a/ui/pages/requests/view.go b/ui/pages/requests/view.go index 0f07d4da..4f13b865 100644 --- a/ui/pages/requests/view.go +++ b/ui/pages/requests/view.go @@ -60,26 +60,28 @@ type View struct { tabHeader *widgets.Tabs // callbacks - onTitleChanged func(id, title, containerType string) - onNewRequest func(requestType string) - onImport func() - onNewCollection func() - onTabClose func(id string) - onTreeViewNodeDoubleClicked func(id string) - onTreeViewNodeClicked func(id string) - onTreeViewMenuClicked func(id string, action string) - onTabSelected func(id string) - onSave func(id string) - onSubmit func(id, containerType string) - onDataChanged func(id string, data any, containerType string) - onCopyResponse func(gtx layout.Context, dataType, data string) - onOnPostRequestSetChanged func(id string, statusCode int, item, from, fromKey string) - onBinaryFileSelect func(id string) - onProtoFileSelect func(id string) - onFromDataFileSelect func(requestID, fieldID string) - onServerInfoReload func(id string) - onGrpcInvoke func(id string) - onGrpcLoadRequestExample func(id string) + onTitleChanged func(id, title, containerType string) + onNewRequest func(requestType string) + onImport func() + onNewCollection func() + onTabClose func(id string) + onTreeViewNodeDoubleClicked func(id string) + onTreeViewNodeClicked func(id string) + onTreeViewMenuClicked func(id string, action string) + onTabSelected func(id string) + onSave func(id string) + onSubmit func(id, containerType string) + onDataChanged func(id string, data any, containerType string) + onCopyResponse func(gtx layout.Context, dataType, data string) + onOnPostRequestSetChanged func(id string, statusCode int, item, from, fromKey string) + onOnSetOnTriggerRequestChanged func(id, collectionID, requestID string) + onBinaryFileSelect func(id string) + onProtoFileSelect func(id string) + onFromDataFileSelect func(requestID, fieldID string) + onServerInfoReload func(id string) + onGrpcInvoke func(id string) + onGrpcLoadRequestExample func(id string) + onRequestTabChanged func(id string, tab string) // state containers *safemap.Map[Container] @@ -133,6 +135,40 @@ func NewView(w *app.Window, theme *chapartheme.Theme, explorer *explorer.Explore return v } +func (v *View) SetOnRequestTabChange(f func(id string, tab string)) { + v.onRequestTabChanged = f +} + +func (v *View) SetPreRequestCollections(id string, collections []*domain.Collection, selectedID string) { + if ct, ok := v.containers.Get(id); ok { + if ct, ok := ct.(RestContainer); ok { + ct.SetPreRequestCollections(collections, selectedID) + return + } + + if ct, ok := ct.(GrpcContainer); ok { + ct.SetPreRequestCollections(collections, selectedID) + } + } +} + +func (v *View) SetPreRequestRequests(id string, requests []*domain.Request, selectedID string) { + if ct, ok := v.containers.Get(id); ok { + if ct, ok := ct.(RestContainer); ok { + ct.SetPreRequestRequests(requests, selectedID) + return + } + + if ct, ok := ct.(GrpcContainer); ok { + ct.SetPreRequestRequests(requests, selectedID) + } + } +} + +func (v *View) SetOnSetOnTriggerRequestChanged(f func(id, collectionID, requestID string)) { + v.onOnSetOnTriggerRequestChanged = f +} + func (v *View) showNotification(text string, duration time.Duration) { v.notify.Show(text, duration) } @@ -152,6 +188,11 @@ func (v *View) SetPostRequestSetValues(id string, set domain.PostRequestSet) { if ct, ok := v.containers.Get(id); ok { if ct, ok := ct.(RestContainer); ok { ct.SetPostRequestSetValues(set) + return + } + + if ct, ok := ct.(GrpcContainer); ok { + ct.SetPostRequestSetValues(set) } } } @@ -160,6 +201,11 @@ func (v *View) SetPostRequestSetPreview(id, preview string) { if ct, ok := v.containers.Get(id); ok { if ct, ok := ct.(RestContainer); ok { ct.SetPostRequestSetPreview(preview) + return + } + + if ct, ok := ct.(GrpcContainer); ok { + ct.SetPostRequestSetPreview(preview) } } } @@ -484,6 +530,18 @@ func (v *View) createGrpcContainer(req *domain.Request) Container { } }) + ct.SetOnSetOnTriggerRequestChanged(func(id, collectionID, requestID string) { + if v.onOnSetOnTriggerRequestChanged != nil { + v.onOnSetOnTriggerRequestChanged(id, collectionID, requestID) + } + }) + + ct.SetOnPostRequestSetChanged(func(id string, statusCode int, item, from, fromKey string) { + if v.onOnPostRequestSetChanged != nil { + v.onOnPostRequestSetChanged(id, statusCode, item, from, fromKey) + } + }) + ct.SetOnLoadRequestExample(func(id string) { if v.onGrpcLoadRequestExample != nil { v.onGrpcLoadRequestExample(id) @@ -496,6 +554,12 @@ func (v *View) createGrpcContainer(req *domain.Request) Container { } }) + ct.SetOnRequestTabChange(func(id, tab string) { + if v.onRequestTabChanged != nil { + v.onRequestTabChanged(id, tab) + } + }) + return ct } @@ -538,6 +602,12 @@ func (v *View) createRestfulContainer(req *domain.Request) Container { } }) + ct.SetOnSetOnTriggerRequestChanged(func(id, collectionID, requestID string) { + if v.onOnSetOnTriggerRequestChanged != nil { + v.onOnSetOnTriggerRequestChanged(id, collectionID, requestID) + } + }) + ct.SetOnBinaryFileSelect(func(id string) { if v.onBinaryFileSelect != nil { v.onBinaryFileSelect(id) @@ -550,6 +620,12 @@ func (v *View) createRestfulContainer(req *domain.Request) Container { } }) + ct.SetOnRequestTabChange(func(id, tab string) { + if v.onRequestTabChanged != nil { + v.onRequestTabChanged(id, tab) + } + }) + return ct } @@ -646,6 +722,16 @@ func (v *View) GetHTTPResponse(id string) *domain.HTTPResponseDetail { return nil } +func (v *View) GetGRPCResponse(id string) *domain.GRPCResponseDetail { + if ct, ok := v.containers.Get(id); ok { + if ct, ok := ct.(GrpcContainer); ok { + return ct.GetResponse() + } + } + + return nil +} + func (v *View) ShowPrompt(id, title, content, modalType string, onSubmit func(selectedOption string, remember bool), options ...widgets.Option) { ct, ok := v.containers.Get(id) if !ok {