From fa721440a94792660c31207850756815e2ae22ff Mon Sep 17 00:00:00 2001 From: Gavin Mogan Date: Fri, 10 Oct 2025 11:10:51 -0700 Subject: [PATCH] feat: Allow overriding grpc command displayed in UI Scenario: Which to create copy and paste able commands to share with coworkers. Current implementation would copy the exact paths for -cacerts (eg /home/halkeye/staff.key) instead of variables (eg $HOME/staff.key) Add override flag so the command can be anything you want eg ``` grpcui -plaintext -override-grpccurl-cmd="grpcurl -key ~/.ssh/staff.key -cert ~/.ssh/staff.crt" localhost:8008 ``` --- cmd/grpcui/grpcui.go | 16 ++++++++++++---- internal/resources/webform/webform-template.html | 2 +- internal/resources/webform/webform.js | 8 +++----- standalone/opts.go | 10 ++++++++-- standalone/standalone.go | 10 +++++----- webform.go | 8 ++++---- 6 files changed, 33 insertions(+), 21 deletions(-) diff --git a/cmd/grpcui/grpcui.go b/cmd/grpcui/grpcui.go index 9e4317fa..8a05799c 100644 --- a/cmd/grpcui/grpcui.go +++ b/cmd/grpcui/grpcui.go @@ -155,6 +155,9 @@ var ( suite as a sequence of RPCs. This is similar to the "collections" feature in postman. The format of this file is the same as used when saving history from the gRPC UI "History" tab.`)) + overrideGRPCCurl = flags.String("override-grpccurl-cmd", "", prettify(` + Override the command outputted in the gRPCurl section of the request tab. + `)) reflection = optionalBoolFlag{val: true} port = flags.Int("port", 0, prettify(` @@ -361,14 +364,15 @@ func main() { flags.Usage = usage flags.Parse(os.Args[1:]) - var gRPCOptions []string + var grpcCurlCmd []string + grpcCurlCmd = append(grpcCurlCmd, "grpcurl") for _, flagName := range grpcCurlFlags { f := flags.Lookup(flagName) if f.Value.String() != f.DefValue { if getter, ok := f.Value.(flag.Getter); ok && getter.Get() == true { - gRPCOptions = append(gRPCOptions, fmt.Sprintf("-%s", f.Name)) + grpcCurlCmd = append(grpcCurlCmd, fmt.Sprintf("-%s", f.Name)) } else { - gRPCOptions = append(gRPCOptions, fmt.Sprintf("-%s=%s", f.Name, strconv.Quote(f.Value.String()))) + grpcCurlCmd = append(grpcCurlCmd, fmt.Sprintf("-%s=%s", f.Name, strconv.Quote(f.Value.String()))) } } } @@ -654,7 +658,11 @@ func main() { handlerOpts = append(handlerOpts, configureJSandCSS(extraJS, standalone.AddJSFile)...) handlerOpts = append(handlerOpts, configureJSandCSS(extraCSS, standalone.AddCSSFile)...) handlerOpts = append(handlerOpts, configureAssets(otherAssets)...) - handlerOpts = append(handlerOpts, standalone.WithGRPCOptions(gRPCOptions)) + if overrideGRPCCurl != nil { + handlerOpts = append(handlerOpts, standalone.WithGRPCCmd([]string{*overrideGRPCCurl})) + } else { + handlerOpts = append(handlerOpts, standalone.WithGRPCCmd(grpcCurlCmd)) + } handler := standalone.Handler(cc, target, methods, allFiles, handlerOpts...) if *maxTime > 0 { diff --git a/internal/resources/webform/webform-template.html b/internal/resources/webform/webform-template.html index 5a97dac3..a76ff6eb 100644 --- a/internal/resources/webform/webform-template.html +++ b/internal/resources/webform/webform-template.html @@ -144,7 +144,7 @@

Response Trailers

]; window.target = {{ .Target }}; - window.gRPCurlOptions = {{ .GRPCurlOptions }}; + window.gRPCurlCmd = {{ .GRPCurlCmd }}; initGRPCForm(services, svcDescs, mtdDescs, '{{ .InvokeURI }}', '{{ .MetadataURI }}', {{ .Debug }}, headers); })(); diff --git a/internal/resources/webform/webform.js b/internal/resources/webform/webform.js index ede68566..d68745dc 100644 --- a/internal/resources/webform/webform.js +++ b/internal/resources/webform/webform.js @@ -2113,9 +2113,7 @@ window.initGRPCForm = function(services, svcDescs, mtdDescs, invokeURI, metadata let gRPCurlTextArea = $("#grpc-curl-text"); function updateCurlCommand(requestDataJson) { - const grpcOpts = window.gRPCurlOptions ? ' ' + window.gRPCurlOptions : ''; - - let metadataStr = ""; + let grpcCmd = window.gRPCurlCmd; const service = $("#grpc-service").val(); const method = $("#grpc-method").val(); @@ -2129,11 +2127,11 @@ window.initGRPCForm = function(services, svcDescs, mtdDescs, invokeURI, metadata const name = $(cells[0]).val(); const val = $(cells[1]).val(); if (name !== "") { - metadataStr += ` -H "${name}: ${val}"`; + grpcCmd += ` -H "${name}: ${val}"`; } } - gRPCurlTextArea.text(`grpcurl${grpcOpts}${metadataStr} -d '${requestDataJson}' ${window.target} ${service}.${method}`); + gRPCurlTextArea.text(`${grpcCmd} -d '${requestDataJson}' ${window.target} ${service}.${method}`); } var jsonRawTextArea = $("#grpc-request-raw-text"); diff --git a/standalone/opts.go b/standalone/opts.go index 4756aeb5..f73cf971 100644 --- a/standalone/opts.go +++ b/standalone/opts.go @@ -219,9 +219,15 @@ func WithClientDebug(debug bool) HandlerOption { }) } +func WithGRPCCmd(cmdLine []string) HandlerOption { + return optFunc(func(opts *handlerOptions) { + opts.gRPCurlCmd = cmdLine + }) +} + func WithGRPCOptions(options []string) HandlerOption { return optFunc(func(opts *handlerOptions) { - opts.gRPCurlOptions = options + opts.gRPCurlCmd = append([]string{"grpcurl"}, options...) }) } @@ -245,7 +251,7 @@ type handlerOptions struct { emitDefaults bool invokeVerbosity int debug *bool - gRPCurlOptions []string + gRPCurlCmd []string } func (opts *handlerOptions) addlServedResources() []*resource { diff --git a/standalone/standalone.go b/standalone/standalone.go index 44d032e0..897f17e8 100644 --- a/standalone/standalone.go +++ b/standalone/standalone.go @@ -45,10 +45,10 @@ const csrfHeaderName = "x-grpcui-csrf-token" // be handling a sub-path (e.g. handling "/rpc-ui/") then use http.StripPrefix. func Handler(ch grpcdynamic.Channel, target string, methods []*desc.MethodDescriptor, files []*desc.FileDescriptor, opts ...HandlerOption) http.Handler { uiOpts := &handlerOptions{ - indexTmpl: defaultIndexTemplate, - css: grpcui.WebFormSampleCSS(), - cssPublic: true, - gRPCurlOptions: nil, + indexTmpl: defaultIndexTemplate, + css: grpcui.WebFormSampleCSS(), + cssPublic: true, + gRPCurlCmd: []string{"grpcurl"}, } for _, o := range opts { o.apply(uiOpts) @@ -79,7 +79,7 @@ func Handler(ch grpcdynamic.Channel, target string, methods []*desc.MethodDescri formOpts := grpcui.WebFormOptions{ DefaultMetadata: uiOpts.defaultMetadata, Debug: uiOpts.debug, - GRPCurlOptions: uiOpts.gRPCurlOptions, + GRPCurlCmd: uiOpts.gRPCurlCmd, } webFormHTML := grpcui.WebFormContentsWithOptions("invoke", "metadata", target, methods, formOpts) indexContents := getIndexContents(uiOpts.indexTmpl, target, webFormHTML, uiOpts.tmplResources) diff --git a/webform.go b/webform.go index 3252e444..db261cf9 100644 --- a/webform.go +++ b/webform.go @@ -72,7 +72,7 @@ type WebFormOptions struct { // debug is enabled). Debug *bool // Any options that will be rendered before grpcurl in the grpccurl/raw request box - GRPCurlOptions []string + GRPCurlCmd []string } // WebFormContentsWithOptions is the same as WebFormContents except that it @@ -93,7 +93,7 @@ func WebFormContentsWithOptions(invokeURI, metadataURI string, target string, de DefaultMetadata []metadataEntry Debug bool Target string - GRPCurlOptions template.JS + GRPCurlCmd template.JS }{ InvokeURI: invokeURI, MetadataURI: metadataURI, @@ -104,11 +104,11 @@ func WebFormContentsWithOptions(invokeURI, metadataURI string, target string, de Target: target, } - gRPCOptionsJSONStr, err := json.Marshal(strings.Join(opts.GRPCurlOptions, " ")) + gRPCOptionsJSONStr, err := json.Marshal(strings.Join(opts.GRPCurlCmd, " ")) if err != nil { panic(fmt.Errorf("Error marshaling to JSON: %w", err)) } - params.GRPCurlOptions = template.JS(gRPCOptionsJSONStr) + params.GRPCurlCmd = template.JS(gRPCOptionsJSONStr) if opts.Debug != nil { params.Debug = *opts.Debug }