1
0
mirror of https://github.com/gusaul/grpcox.git synced 2025-04-25 16:45:37 +00:00

Compare commits

...

30 Commits

Author SHA1 Message Date
Muhammad Auliya
6feae4127c
Merge pull request #42 from AlessandroLimTkp/fix-concurrent-metadata-issue
fix: stop resource headers from bleeding into invoke
2023-06-23 11:19:26 +07:00
AlessandroLimTkp
b998f098b8 stop reflect headers from affecting request headers 2023-06-13 17:46:15 +07:00
Muhammad Auliya
f240d0494e
Merge pull request #36 from beruangcoklat/master
handle error when remove and create directory
2022-10-18 17:38:06 +07:00
Muhammad Auliya
c5b61eb2d0
Merge pull request #39 from lilac/master
Update the url of code-prettify
2022-10-18 17:34:41 +07:00
Muhammad Auliya
c46a882ba7
Merge pull request #38 from petragabriela/f_petra_reflection-headers
feat: 🎸 allow using metadata as reflection headers
2022-10-18 17:33:00 +07:00
Junjun Deng
ca8f456f00 Update the url of code-prettify 2022-09-29 12:26:15 +08:00
Petra
63fe96c73d feat: 🎸 allow using metadata as reflection headers 2022-07-30 12:22:19 +07:00
beruangcoklat
6d348c6611 fix broken unit test 2022-05-26 08:00:15 +07:00
beruangcoklat
c2a6f5dd7f handling error in AddProtos 2022-05-26 08:00:05 +07:00
Muhammad Auliya
8534ee1dc1
Merge pull request #32 from Nitrillo/feature/TLS
Added TLS checkbox
2021-12-21 17:07:51 +07:00
Muhammad Auliya
54efdebe24
Merge pull request #35 from adzimzf/adzim/save_request
feat(request-list): Support CRUD for request list
2021-12-21 17:07:00 +07:00
Azdim Zul Fahmi
464de07064 feat(request-list): Support CRUD for request list
by using this request list, user doesn't need to memorise the endpoint & the request anymore, they can store the request and come again if they need it.
at the moment the data is store in IndexDB (browser), hence it doesn't need backend effort.
2021-12-12 15:20:17 +07:00
Cesar Guirao
e7578eaed5 Added TLS checkbox 2021-07-23 09:41:02 -07:00
Muhammad Auliya
c83858ebfc
Merge pull request #27 from davidgoitia/allow_protoset
allow protoset file
2021-02-16 17:49:53 +07:00
Muhammad Auliya
6eccacc39f
Merge pull request #29 from davidgoitia/env_bind_address
allow specifying bind address
2021-02-16 17:47:07 +07:00
Muhammad Auliya
b941d8727d
Merge pull request #20 from risoll/i_rizky_metadata
add ctx metadata
2021-02-16 17:46:34 +07:00
David Goitia
f5768c3035
allow specifying bind address 2021-02-05 20:26:15 +01:00
David Goitia
d1a4237cb2
allow protoset file 2021-02-05 17:21:41 +01:00
Muhammad Auliya
790d251031
Merge pull request #26 from gusaul/modify_invoke_piping
change invoke handler directly to bytes buffer
2020-11-03 09:39:10 +07:00
gusaul
ec4ba2090d change invoke handler directly to bytes buffer 2020-11-03 09:37:50 +07:00
Muhammad Auliya
70e5c28119
Merge pull request #25 from gusaul/modify_invoke_piping
update pipe commit output to handling huge output
2020-11-02 20:05:13 +07:00
gusaul
8aa67b6529 update pipe commit output to handling huge output 2020-11-02 20:02:07 +07:00
risoll
1080e8f0b0 add ctx metadata 2020-06-02 00:22:22 +07:00
Muhammad Auliya
30fef12ac9
Merge pull request #19 from gusaul/aul_add_connection_mutex
add mutex to append conn to map to avoid concurrent map access
2020-05-29 08:16:08 +07:00
gusaul
3908c00c11 add mutex to append conn to map to avoid concurrent map access 2020-05-29 08:14:33 +07:00
Muhammad Auliya
6148904bec
Merge pull request #17 from alvinmatias69/master
feat(descriptor): add support for local proto descriptor
2020-02-11 11:35:47 +07:00
Matias Alvin
293bb364c0 feat(descriptor): add support for local proto descriptor
Currently, grpcox depends on server reflection to get proto descriptor. It has a
significant drawback, since not every grpc server support
[server
reflection](https://github.com/grpc/grpc/blob/master/doc/server-reflection.md#known-implementations).
Using local proto files is more feasible, as every grpc server certainly have
one.

Even though using protofile should be simple enough, there's still a problem
regarding this. Some protofile use extra plugins for their proto. i.e.
gogoprotobuf is a project that does just that. The problems with plugins are
most of them require explicit import to the plugin inside of the protofile. It
will break grpcurl proto descriptor extraction. Thus, the plugin proto must be
uploaded alongside the protofile. Also, the protofile should be modified
automatically to change their import to local import.

Given that, I proposed a way for the user to upload multiple protofile to
grpcox. Then, use that to get the descriptor.

Changelog:
- Add `use local proto` checkbox in HTML client. On checked it will show upload
button and list of selected proto.
- `get-service` ajax will use POST when `use local proto` is checked. The
uploaded protofile will be the payload for the ajax request.
- Add a new route to handle POST "get-service". It will persist the uploaded
protofile to `/tmp/` directory and add protos field in the resource.
- Modify `openDescriptor` to use local proto if protos field in the resource is
available.
- Modify `openDescriptor` to return an error, as opening descriptor from local
proto may fail.
- Modify the main server so it can be shut down gracefully. This is necessary as
grpcox need to remove persisted proto right after the server is turned off.

This Pull Request will resolve #16
2020-01-31 10:27:46 +07:00
Muhammad Auliya
3633418ad0
Update README.md 2019-12-03 17:14:07 +07:00
Muhammad Auliya
6d9d03f52a
Merge pull request #12 from gusaul/separate_reflection_conn
separate reflection descriptor from established conn
2019-11-13 12:17:29 +07:00
gusaul
2676b0ccef separate reflection descriptor from established conn 2019-11-13 12:12:58 +07:00
18 changed files with 1319 additions and 57 deletions

5
.gitignore vendored
View File

@ -1,2 +1,5 @@
grpcox
log
log
*.out
vendor/
.idea

View File

@ -9,6 +9,15 @@ turn [gRPCurl](https://github.com/fullstorydev/grpcurl) into web based UI, extre
- Save established connection, and reuse it for next invoke/request (also can close/restart connection)
## Installation
### Docker
```shell
docker pull gusaul/grpcox:latest
```
then run
```shell
docker run -p 6969:6969 -v {ABSOLUTE_PATH_TO_LOG}/log:/log -d gusaul/grpcox
```
### Docker Compose
from terminal, move to grpcox directory, then run command
```shell
@ -22,6 +31,7 @@ if you have golang installed on your local machine, just run command
```shell
make start
```
from grpcox directory
configure app preferences by editing `config.env` file
@ -29,6 +39,7 @@ configure app preferences by editing `config.env` file
|-----------------|---------------------------------------------|--------|--------|
| MAX_LIFE_CONN | maximum idle time connection before closed | number | minute |
| TICK_CLOSE_CONN | ticker interval to sweep expired connection | number | second |
| BIND_ADDR | ip:port to bind service | string | |
set value `0 (zero)` to disable auto close idle connection.

View File

@ -121,7 +121,9 @@ func (c *ConnStore) addConnection(host string, res *Resource, ttl ...time.Durati
conn.expired = time.Now().Add(ttl[0])
}
c.Lock()
c.conn[host] = conn
c.Unlock()
}
func (c *ConnStore) getConnection(host string) (res *Resource, found bool) {

View File

@ -3,16 +3,14 @@ package core
import (
"context"
"os"
"reflect"
"strconv"
"time"
"github.com/fullstorydev/grpcurl"
"github.com/jhump/protoreflect/grpcreflect"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/keepalive"
"google.golang.org/grpc/metadata"
reflectpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
)
// GrpCox - main object
@ -34,6 +32,14 @@ type GrpCox struct {
isUnixSocket func() bool
}
// Proto define protofile uploaded from client
// will be used to be persisted to disk and indicator
// whether connections should reflect from server or local proto
type Proto struct {
Name string
Content []byte
}
// InitGrpCox constructor
func InitGrpCox() *GrpCox {
maxLife, tick := 10, 3
@ -71,21 +77,37 @@ func (g *GrpCox) GetResource(ctx context.Context, target string, plainText, isRe
var err error
r := new(Resource)
h := append(g.headers, g.reflectHeaders...)
md := grpcurl.MetadataFromHeaders(h)
refCtx := metadata.NewOutgoingContext(ctx, md)
r.md = grpcurl.MetadataFromHeaders(h)
r.clientConn, err = g.dial(ctx, target, plainText)
if err != nil {
return nil, err
}
r.refClient = grpcreflect.NewClient(refCtx, reflectpb.NewServerReflectionClient(r.clientConn))
r.descSource = grpcurl.DescriptorSourceFromServer(ctx, r.refClient)
// what is r.Headers used for?
r.headers = h
g.activeConn.addConnection(target, r, g.maxLifeConn)
return r, nil
}
// GetResourceWithProto - open resource to targeted grpc server using given protofile
func (g *GrpCox) GetResourceWithProto(ctx context.Context, target string, plainText, isRestartConn bool, protos []Proto) (*Resource, error) {
r, err := g.GetResource(ctx, target, plainText, isRestartConn)
if err != nil {
return nil, err
}
// if given protofile is equal to current, skip adding protos as it's already
// persisted in the harddisk anyway
if reflect.DeepEqual(r.protos, protos) {
return r, nil
}
// add protos property to resource and persist it to harddisk
err = r.AddProtos(protos)
return r, err
}
// GetActiveConns - get all saved active connection
func (g *GrpCox) GetActiveConns(ctx context.Context) []string {
active := g.activeConn.getAllConn()
@ -116,6 +138,11 @@ func (g *GrpCox) Extend(host string) {
g.activeConn.extend(host, g.maxLifeConn)
}
// SetReflectHeaders sets grpcox reflection headers
func (g *GrpCox) SetReflectHeaders(headers ...string) {
g.reflectHeaders = headers
}
func (g *GrpCox) dial(ctx context.Context, target string, plainText bool) (*grpc.ClientConn, error) {
dialTime := 10 * time.Second
ctx, cancel := context.WithTimeout(ctx, dialTime)

View File

@ -5,8 +5,13 @@ import (
"context"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
"github.com/fullstorydev/grpcurl"
@ -14,21 +19,88 @@ import (
"github.com/jhump/protoreflect/grpcreflect"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
reflectpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
)
// BasePath define path where proto file will persisted
const BasePath = "/tmp/grpcox/"
// Resource - hold 3 main function (List, Describe, and Invoke)
type Resource struct {
clientConn *grpc.ClientConn
descSource grpcurl.DescriptorSource
refClient *grpcreflect.Client
protos []Proto
protosets []Proto
headers []string
md metadata.MD
}
//openDescriptor - use it to reflect server descriptor
func (r *Resource) openDescriptor() error {
ctx := context.Background()
refCtx := metadata.NewOutgoingContext(ctx, r.md)
r.refClient = grpcreflect.NewClient(refCtx, reflectpb.NewServerReflectionClient(r.clientConn))
// if no protos available use server reflection
if r.protos == nil && r.protosets == nil {
r.descSource = grpcurl.DescriptorSourceFromServer(ctx, r.refClient)
return nil
}
protoPath := filepath.Join(BasePath, r.clientConn.Target())
var err error
if len(r.protosets) > 0 {
// make list of protos name to be used as descriptor
protos := make([]string, 0, len(r.protosets))
for _, proto := range r.protosets {
protos = append(protos, filepath.Join(protoPath, proto.Name))
}
r.descSource, err = grpcurl.DescriptorSourceFromProtoSets(protos...)
} else {
// make list of protos name to be used as descriptor
protos := make([]string, 0, len(r.protos))
for _, proto := range r.protos {
protos = append(protos, proto.Name)
}
r.descSource, err = grpcurl.DescriptorSourceFromProtoFiles([]string{protoPath}, protos...)
}
return err
}
//closeDescriptor - please ensure to always close after open in the same flow
func (r *Resource) closeDescriptor() {
done := make(chan int)
go func() {
if r.refClient != nil {
r.refClient.Reset()
}
done <- 1
}()
select {
case <-done:
return
case <-time.After(3 * time.Second):
log.Printf("Reflection %s failed to close\n", r.clientConn.Target())
return
}
}
// List - To list all services exposed by a server
// symbol can be "" to list all available services
// symbol also can be service name to list all available method
func (r *Resource) List(symbol string) ([]string, error) {
err := r.openDescriptor()
if err != nil {
return nil, err
}
defer r.closeDescriptor()
var result []string
if symbol == "" {
@ -65,6 +137,11 @@ func (r *Resource) List(symbol string) ([]string, error) {
// It also prints a description of that symbol, in the form of snippets of proto source.
// It won't necessarily be the original source that defined the element, but it will be equivalent.
func (r *Resource) Describe(symbol string) (string, string, error) {
err := r.openDescriptor()
if err != nil {
return "", "", err
}
defer r.closeDescriptor()
var result, template string
@ -118,28 +195,28 @@ func (r *Resource) Describe(symbol string) (string, string, error) {
}
// Invoke - invoking gRPC function
func (r *Resource) Invoke(ctx context.Context, symbol string, in io.Reader) (string, time.Duration, error) {
// because of grpcurl directly fmt.Printf on their invoke function
// so we stub the Stdout using os.Pipe
backUpStdout := os.Stdout
defer func() {
os.Stdout = backUpStdout
}()
f, w, err := os.Pipe()
func (r *Resource) Invoke(ctx context.Context, metadata []string, symbol string, in io.Reader) (string, time.Duration, error) {
err := r.openDescriptor()
if err != nil {
return "", 0, err
}
os.Stdout = w
defer r.closeDescriptor()
var resultBuffer bytes.Buffer
rf, formatter, err := grpcurl.RequestParserAndFormatterFor(grpcurl.Format("json"), r.descSource, false, true, in)
if err != nil {
return "", 0, err
}
h := grpcurl.NewDefaultEventHandler(os.Stdout, r.descSource, formatter, false)
h := grpcurl.NewDefaultEventHandler(&resultBuffer, r.descSource, formatter, false)
var headers []string
if len(metadata) != 0 {
headers = metadata
}
start := time.Now()
err = grpcurl.InvokeRPC(ctx, r.descSource, r.clientConn, symbol, r.headers, h, rf.Next)
err = grpcurl.InvokeRPC(ctx, r.descSource, r.clientConn, symbol, headers, h, rf.Next)
end := time.Now().Sub(start) / time.Millisecond
if err != nil {
return "", end, err
@ -149,40 +226,42 @@ func (r *Resource) Invoke(ctx context.Context, symbol string, in io.Reader) (str
return "", end, fmt.Errorf(h.Status.Message())
}
// copy the output in a separate goroutine so printing can't block indefinitely
outC := make(chan string)
go func() {
var buf bytes.Buffer
io.Copy(&buf, f)
outC <- buf.String()
}()
w.Close()
out := <-outC
return out, end, nil
return resultBuffer.String(), end, nil
}
// Close - to close all resources that was opened before
func (r *Resource) Close() {
done := make(chan int)
var wg sync.WaitGroup
wg.Add(1)
go func() {
if r.refClient != nil {
r.refClient.Reset()
r.refClient = nil
}
defer wg.Done()
if r.clientConn != nil {
r.clientConn.Close()
r.clientConn = nil
}
done <- 1
}()
wg.Add(1)
go func() {
defer wg.Done()
err := os.RemoveAll(BasePath)
if err != nil {
log.Printf("error removing proto dir from tmp: %s", err.Error())
}
}()
c := make(chan struct{})
go func() {
defer close(c)
wg.Wait()
}()
select {
case <-done:
case <-c:
return
case <-time.After(3 * time.Second):
log.Printf("Connection %s falied to close\n", r.clientConn.Target())
log.Printf("Connection %s failed to close\n", r.clientConn.Target())
return
}
}
@ -196,3 +275,71 @@ func (r *Resource) exit(code int) {
r.Close()
os.Exit(code)
}
// AddProtos to resource properties and harddisk
// added protos will be persisted in `basepath + connection target`
// i.e. connection target == 127.0.0.1:8888
// proto files will be persisted in /tmp/grpcox/127.0.0.1:8888
// if the directory is already there, remove it first
func (r *Resource) AddProtos(protos []Proto) error {
protoPath := filepath.Join(BasePath, r.clientConn.Target())
err := os.MkdirAll(protoPath, 0777)
if os.IsExist(err) {
err = os.RemoveAll(protoPath)
if err != nil {
return err
}
err = os.MkdirAll(protoPath, 0777)
if err != nil {
return err
}
} else if err != nil {
return err
}
var protoSlice, protosetSlice []Proto
for _, proto := range protos {
var err error
if strings.HasSuffix(proto.Name, ".protoset") {
protosetSlice = append(protosetSlice, proto)
err = ioutil.WriteFile(filepath.Join(protoPath, "/", proto.Name),
proto.Content,
0777)
} else {
protoSlice = append(protoSlice, proto)
err = ioutil.WriteFile(filepath.Join(protoPath, "/", proto.Name),
prepareImport(proto.Content),
0777)
}
if err != nil {
return err
}
}
r.protos = protoSlice
r.protosets = protosetSlice
return nil
}
// prepareImport transforming proto import into local path
// with exception to google proto import as it won't cause any problem
func prepareImport(proto []byte) []byte {
const pattern = `import ".+`
result := string(proto)
re := regexp.MustCompile(pattern)
matchs := re.FindAllString(result, -1)
for _, match := range matchs {
if strings.Contains(match, "\"google/") {
continue
}
name := strings.Split(match, "/")
if len(name) < 2 {
continue
}
importString := `import "` + name[len(name)-1]
result = strings.Replace(result, match, importString, -1)
}
return []byte(result)
}

70
core/resource_test.go Normal file
View File

@ -0,0 +1,70 @@
package core
import (
"reflect"
"testing"
)
func Test_prepareImport(t *testing.T) {
type args struct {
proto []byte
}
tests := []struct {
name string
args args
want []byte
}{
{
name: "sucess change import path to local",
args: args{
proto: []byte(`
package testing;
import "test.com/owner/repo/content.proto";`),
},
want: []byte(`
package testing;
import "content.proto";`),
},
{
name: "sucess keep google import",
args: args{
proto: []byte(`
package testing;
import "google/proto/buf";
import "test.com/owner/repo/content.proto";`),
},
want: []byte(`
package testing;
import "google/proto/buf";
import "content.proto";`),
},
{
name: "sucess keep local import",
args: args{
proto: []byte(`
package testing;
import "repo.proto";
import "test.com/owner/repo/content.proto";`),
},
want: []byte(`
package testing;
import "repo.proto";
import "content.proto";`),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := prepareImport(tt.args.proto); !reflect.DeepEqual(got, tt.want) {
t.Errorf("prepareImport() = %v, want %v",
string(got),
string(tt.want))
}
})
}
}

View File

@ -1,13 +1,16 @@
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"time"
"github.com/gorilla/mux"
"github.com/gusaul/grpcox/core"
"github.com/gusaul/grpcox/handler"
)
@ -22,16 +25,54 @@ func main() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
// start app
port := ":6969"
addr := "0.0.0.0:6969"
if value, ok := os.LookupEnv("BIND_ADDR"); ok {
addr = value
}
muxRouter := mux.NewRouter()
handler.Init(muxRouter)
var wait time.Duration = time.Second * 15
srv := &http.Server{
Addr: addr,
WriteTimeout: time.Second * 15,
ReadTimeout: time.Second * 15,
IdleTimeout: time.Second * 60,
Handler: muxRouter,
Addr: "0.0.0.0" + port,
WriteTimeout: 15 * time.Second,
ReadTimeout: 15 * time.Second,
}
fmt.Println("Service started on", port)
log.Fatal(srv.ListenAndServe())
fmt.Println("Service started on", addr)
go func() {
log.Fatal(srv.ListenAndServe())
}()
c := make(chan os.Signal, 1)
// We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C)
// SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught.
signal.Notify(c, os.Interrupt)
// Block until we receive our signal.
<-c
// Create a deadline to wait for.
ctx, cancel := context.WithTimeout(context.Background(), wait)
defer cancel()
err = removeProtos()
if err != nil {
log.Printf("error while removing protos: %s", err.Error())
}
srv.Shutdown(ctx)
log.Println("shutting down")
os.Exit(0)
}
// removeProtos will remove all uploaded proto file
// this function will be called as the server shutdown gracefully
func removeProtos() error {
log.Println("removing proto dir from /tmp")
err := os.RemoveAll(core.BasePath)
return err
}

View File

@ -4,6 +4,7 @@ import (
"bytes"
"context"
"fmt"
"io/ioutil"
"net/http"
"strconv"
"strings"
@ -69,6 +70,26 @@ func (h *Handler) getLists(w http.ResponseWriter, r *http.Request) {
useTLS, _ := strconv.ParseBool(r.Header.Get("use_tls"))
restart, _ := strconv.ParseBool(r.FormValue("restart"))
// treat metadata as reflection headers
metadataHeader := r.Header.Get("Metadata")
metadataArr := strings.Split(metadataHeader, ",")
// construct array of strings with "key: value" form to be used in the reflection headers
var metadata []string
var metadataStr string
for i, m := range metadataArr {
i += 1
if isEven := i%2 == 0; isEven {
metadataStr = metadataStr + m
metadata = append(metadata, metadataStr)
metadataStr = ""
continue
}
metadataStr = fmt.Sprintf("%s:", m)
}
h.g.SetReflectHeaders(metadata...)
res, err := h.g.GetResource(context.Background(), host, !useTLS, restart)
if err != nil {
writeError(w, err)
@ -85,6 +106,65 @@ func (h *Handler) getLists(w http.ResponseWriter, r *http.Request) {
response(w, result)
}
// getListsWithProto handling client request for service list with proto
func (h *Handler) getListsWithProto(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
host := vars["host"]
if host == "" {
writeError(w, fmt.Errorf("Invalid Host"))
return
}
service := vars["serv_name"]
useTLS, _ := strconv.ParseBool(r.Header.Get("use_tls"))
restart, _ := strconv.ParseBool(r.FormValue("restart"))
// limit upload file to 5mb
err := r.ParseMultipartForm(5 << 20)
if err != nil {
writeError(w, err)
return
}
// convert uploaded files to list of Proto struct
files := r.MultipartForm.File["protos"]
protos := make([]core.Proto, 0, len(files))
for _, file := range files {
fileData, err := file.Open()
if err != nil {
writeError(w, err)
return
}
defer fileData.Close()
content, err := ioutil.ReadAll(fileData)
if err != nil {
writeError(w, err)
}
protos = append(protos, core.Proto{
Name: file.Filename,
Content: content,
})
}
res, err := h.g.GetResourceWithProto(context.Background(), host, !useTLS, restart, protos)
if err != nil {
writeError(w, err)
return
}
result, err := res.List(service)
if err != nil {
writeError(w, err)
return
}
h.g.Extend(host)
response(w, result)
}
func (h *Handler) describeFunction(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
host := vars["host"]
@ -161,8 +241,26 @@ func (h *Handler) invokeFunction(w http.ResponseWriter, r *http.Request) {
return
}
// context metadata
metadataHeader := r.Header.Get("Metadata")
metadataArr := strings.Split(metadataHeader, ",")
// construct array of string with "key: value" form to satisfy grpcurl MetadataFromHeaders
var metadata []string
var metadataStr string
for i, m := range metadataArr {
i += 1
if isEven := i%2 == 0; isEven {
metadataStr = metadataStr + m
metadata = append(metadata, metadataStr)
metadataStr = ""
continue
}
metadataStr = fmt.Sprintf("%s:", m)
}
// get param
result, timer, err := res.Invoke(context.Background(), funcName, r.Body)
result, timer, err := res.Invoke(context.Background(), metadata, funcName, r.Body)
if err != nil {
writeError(w, err)
return

View File

@ -14,6 +14,7 @@ func Init(router *mux.Router) {
ajaxRoute := router.PathPrefix("/server/{host}").Subrouter()
ajaxRoute.HandleFunc("/services", corsHandler(h.getLists)).Methods(http.MethodGet, http.MethodOptions)
ajaxRoute.HandleFunc("/services", corsHandler(h.getListsWithProto)).Methods(http.MethodPost)
ajaxRoute.HandleFunc("/service/{serv_name}/functions", corsHandler(h.getLists)).Methods(http.MethodGet, http.MethodOptions)
ajaxRoute.HandleFunc("/function/{func_name}/describe", corsHandler(h.describeFunction)).Methods(http.MethodGet, http.MethodOptions)
ajaxRoute.HandleFunc("/function/{func_name}/invoke", corsHandler(h.invokeFunction)).Methods(http.MethodPost, http.MethodOptions)

115
index/css/proto.css Normal file
View File

@ -0,0 +1,115 @@
#proto-input {
margin-top: 10px;
margin-bottom: 15px;
}
[type="file"] {
border: 0;
clip: rect(0, 0, 0, 0);
height: 1px;
overflow: hidden;
padding: 0;
position: absolute !important;
white-space: nowrap;
width: 1px;
}
[type="file"] + label {
background-color: #59698d;
border-radius: 10px;
color: #fff;
cursor: pointer;
display: inline-block;
font-family: 'Poppins', sans-serif;
font-size: 1rem;
font-weight: 600;
padding: 5px;
transition: background-color 0.3s;
}
[type="file"]:focus + label,
[type="file"] + label:hover {
background-color: #324674;
}
[type="file"]:focus + label {
outline: 1px dotted #000;
outline: -webkit-focus-ring-color auto 5px;
}
.proto-top-collection {
display: inline-flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
}
.proto-toggle {
color: rgba(0, 0, 0, 0.5);
font-size: 0.8rem;
}
.proto-toggle:hover {
cursor: pointer;
}
.proto-collection {
background-color: #e0dfe6;
border-radius: 5px;
min-height: 120px;
width: 100%;
display: inline-flex;
align-items: center;
align-content: center;
flex-direction: row;
padding: 20px;
padding-right: 0px;
flex-wrap: wrap;
}
.proto-item {
height: 80px;
margin-right: 20px;
margin-left: 10px;
margin-bottom: 30px;
display: inline-flex;
flex-direction: row;
align-items: flex-start;
}
.proto-icon {
height: 80px;
opacity: 0.5;
}
.proto-desc {
display: inline-flex;
flex-direction: column;
align-items: flex-start;
height: 100%;
}
.proto-caption {
font-weight: 600;
color: rgba(0,0,0,0.6);
font-size: 1rem;
}
.proto-size {
font-size: 1rem;
color: rgba(0,0,0,0.6);
}
.proto-remove {
background-color: rgba(0,0,0,0.5);
padding: 3px !important;
margin-left: 0px !important;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
}
.proto-remove:hover {
background-color: rgba(255,0,0,0.4);
}

View File

@ -200,4 +200,53 @@ circle {
transform: scaleY(1.0);
-webkit-transform: scaleY(1.0);
}
}
.save-button-dropdown {
padding-left: 10px;
padding-right: 15px;
}
.save-button-dropdown .dropdown-menu.show {
transform: translate3d(639px, 46px, 0px);
}
.one-long-line {
margin-bottom: 0;
margin-left: 5px;
max-width: 100%;
white-space:nowrap;
overflow:hidden;
text-overflow:ellipsis;
}
.one-long-line:hover {
cursor: pointer;
overflow:visible;
}
.column-row-left {
display: flex;
flex-direction: column;
margin-top: 8px;
}
.column-row-left .md-form{
margin-bottom: 0;
}
.search {
width: 100%;
}
.request-list {
display: flex;
padding: 5px;
}
.request-list .fa {
color: red;
}
.request-list.selected {
background: #dadfe3;;
}

BIN
index/img/file.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -10,24 +10,115 @@
<link href="css/bootstrap.min.css" rel="stylesheet">
<link href="css/mdb.min.css" rel="stylesheet">
<link href="css/style.css" rel="stylesheet">
<link href="css/proto.css" rel="stylesheet">
</head>
<body>
<div class="container pt-50">
<div class="row animated fadeIn">
<div class="col">
<div class="container-fluid pt-50">
<div class="row animated fadeIn justify-content-md-center">
<div class="col-3">
<div class="row">
<div class="col" style="padding-left: 70px;">
<div class="row column-row-left">
<div class="md-form input-group">
<input type="text" class="form-control search" id="search-request" onkeyup="search(this)">
<label for="search-request" class="">Search Request</label>
</div>
<ul class="list-group list-group-flush list" id="request-list"></ul>
</div>
</div>
</div>
</div>
<div class="col-7">
<div class="md-form input-group">
<input type="text" class="form-control" id="server-target">
<label for="server-target">gRPC Server Target</label>
<div class="input-group-append">
<button id="get-services" class="btn btn-mdb-color waves-effect m-0" type="button"><i class="fa fa-plug"></i></button>
<div class="dropdown">
<button type="button" class="btn btn-mdb-color waves-effect m-0 dropdown-toggle save-button-dropdown" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="true">
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="btnGroupDrop1">
<a class="dropdown-item" id="show-modal-save-request">Save</a>
<a class="dropdown-item" id="show-modal-save-as-request">Save As</a>
</div>
</div>
</div>
</div>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="use-tls">
<label class="custom-control-label" for="use-tls">Use TLS</label>
</div>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="restart-conn">
<label class="custom-control-label" for="restart-conn">Restart Connection</label>
</div>
<div class="input-group">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="local-proto">
<label class="custom-control-label" for="local-proto">Use local proto</label>
</div>
</div>
<div class="input-group" id="proto-input" style="display: none">
<div class="proto-top-collection">
<input class="proto-uploader" type="file" id="proto-file" multiple>
<label for="proto-file"><i class="fa fa-plus-circle"></i> proto files</label>
<span id="proto-collection-toggle" class="proto-toggle">Hide Proto Collection</span>
</div>
<div class="proto-collection"></div>
</div>
<!-- Context metadata -->
<div class="input-group">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="ctx-metadata-switch">
<label class="custom-control-label" for="ctx-metadata-switch">Use request metadata</label>
</div>
</div>
<div class="input-group" id="ctx-metadata-input" style="display: none">
<br>
<div id="ctx-metadata-table" class="table-editable">
<table class="table table-bordered">
<thead>
<tr>
<th class="text-start" style="width: 10%"></th>
<th class="text-start" style="width: 20%">Key</th>
<th class="text-start" style="width: 70%">Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<span class="table-remove">
<button type="button" class="btn btn-danger btn-rounded btn-sm my-0">
<i class="fa fa-times"></i>
</button>
</span>
</td>
<td class="ctx-metadata-input-field pt-3-half" contenteditable="true"></td>
<td class="ctx-metadata-input-field pt-3-half" contenteditable="true"></td>
</tr>
</tbody>
</table>
<div class="input-group-append">
<span class="table-add">
<button type="button" class="btn btn-success btn-rounded btn-sm my-0">
<i class="fa fa-plus"></i>
</button>
</span>
</div>
</div>
</div>
<!-- Context metadata -->
<div class="other-elem" id="choose-service" style="display: none">
<div class="input-group">
<div class="input-group-prepend">
@ -111,13 +202,42 @@
<div class="rect5"></div>
</div>
<!-- Modal Save request-->
<div class="modal fade" id="saveRequest" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Input the name for the request</h5>
</div>
<div class="modal-body">
<form>
<div class="form-group row">
<label for="input-request-name" class="col-sm-2 col-form-label">Name</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="input-request-name">
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button id="save-request" type="button" class="btn btn-primary">Save</button>
</div>
</div>
</div>
</div>
<script type="text/javascript" src="js/jquery-3.3.1.min.js"></script>
<script type="text/javascript" src="js/popper.min.js"></script>
<script type="text/javascript" src="js/bootstrap.min.js"></script>
<script type="text/javascript" src="js/mdb.min.js"></script>
<script src="https://cdn.rawgit.com/google/code-prettify/master/loader/run_prettify.js"></script>
<script src="https://cdn.jsdelivr.net/gh/google/code-prettify@master/loader/run_prettify.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.1/ace.js"></script>
<script type="text/javascript" src="js/db.js"></script>
<script type="text/javascript" src="js/style.js"></script>
<script type="text/javascript" src="js/proto.js"></script>
<script type="text/javascript" src="js/ctx.metadata.js"></script>
<script type="text/javascript" src="js/request.list.js"></script>
</body>
</html>
</html>

64
index/js/ctx.metadata.js Normal file
View File

@ -0,0 +1,64 @@
// table input for context metadata
const ctxMetadataTable = $('#ctx-metadata-table');
// new row for each context being added
const newTr = `
<tr>
<td>
<span class="table-remove">
<button type="button" class="btn btn-danger btn-rounded btn-sm my-0">
<i class="fa fa-times"></i>
</button>
</span>
</td>
<td class="ctx-metadata-input-field pt-3-half" contenteditable="true"></td>
<td class="ctx-metadata-input-field pt-3-half" contenteditable="true"></td>
</tr>`;
// helper variable to contains all of the context metadata input
let ctxArr = [];
// helper variable to contain the usage of metadata
let ctxUse = false;
// ctx metadata event listener
(function () {
// add event listener on ctx metadata checkbox
const ctxMetadataSwitch = document.getElementById("ctx-metadata-switch");
ctxMetadataSwitch.addEventListener("change", function(event) {
const { checked } = event.target;
ctxUse = checked;
toggleDisplayCtxMetadataTable(checked);
});
// remove for each row in ctx metadata table
ctxMetadataTable.on('click', '.table-remove', function () {
$(this).parents('tr').detach();
});
// add new row
ctxMetadataTable.on('click', '.table-add', () => {
$('tbody').append(newTr);
});
// only allow any paste action with plain text
ctxMetadataTable.on('paste', '.ctx-metadata-input-field', function (e) {
// cancel paste
e.preventDefault();
// get text representation of clipboard
const text = (e.originalEvent || e).clipboardData.getData('text/plain');
// insert text manually
document.execCommand("insertHTML", false, text);
});
}());
// toggle ctx metadata display
// will show the key-value pairs table input
function toggleDisplayCtxMetadataTable(show) {
const style = show ? "display: block" : "display: none";
const protoInput = document.getElementById("ctx-metadata-input");
protoInput.removeAttribute("style");
protoInput.style.cssText = style;
}

143
index/js/db.js Normal file
View File

@ -0,0 +1,143 @@
const objectStoreRequests = "requests";
let dbConn = null;
function gdb(){
if (!window.indexedDB) {
alert(`Your browser doesn't support IndexedDB`)
return;
}
return new Promise(function (success, error){
if (dbConn !== null){
success(dbConn)
return
}
const request = indexedDB.open('grpcox',1);
request.onerror = (event) => {
error(request.error)
};
request.onsuccess = (event) => {
// add implementation here
console.log("success open DB")
dbConn = event.target.result;
success(dbConn);
};
// create the Contacts object store and indexes
request.onupgradeneeded = (event) => {
let dbConn = event.target.result;
let store = dbConn.createObjectStore(objectStoreRequests,{
keyPath:"name"
});
};
})
}
function getRequest(name) {
return new Promise(async function (success, error){
let db = await gdb()
const txn = db.transaction(objectStoreRequests, 'readwrite');
const store = txn.objectStore(objectStoreRequests);
let idbRequest = store.get(name);
idbRequest.onerror = function (param) {
error(idbRequest.error.name)
}
idbRequest.onsuccess = function (event) {
success(event.target.result)
}
})
}
function getAllRequestKey() {
return new Promise(async function (success, error){
let db = await gdb();
const txn = db.transaction(objectStoreRequests, 'readwrite');
// get the Contacts object store
const store = txn.objectStore(objectStoreRequests);
let idbRequest = store.getAllKeys();
idbRequest.onerror = function (param) {
error(param.target)
}
idbRequest.onsuccess = function (event) {
success(event.target.result)
}
})
}
function insertRequest(request) {
return new Promise(async function(success, error){
let db = await gdb()
// create a new transaction
const txn = db.transaction(objectStoreRequests, 'readwrite');
const store = txn.objectStore(objectStoreRequests);
let query = store.add(request);
// handle success case
query.onsuccess = function (event) {
success('success')
};
// handle the error case
query.onerror = function (event) {
if (query.error.name === "ConstraintError") {
error('Duplicate request name')
return
}
error(query.error.name)
}
})
}
function updateRequest(request) {
return new Promise(async function(success, error){
let db = await gdb()
// create a new transaction
const txn = db.transaction(objectStoreRequests, 'readwrite');
const store = txn.objectStore(objectStoreRequests);
let query = store.put(request);
// handle success case
query.onsuccess = function (event) {
success('success')
};
// handle the error case
query.onerror = function (event) {
if (query.error.name === "ConstraintError") {
error('Duplicate request name')
return
}
error(query.error.name)
}
})
}
function deleteRequest(name){
return new Promise(async function (success, error){
let db = await gdb()
// create a new transaction
const txn = db.transaction(objectStoreRequests, 'readwrite');
const store = txn.objectStore(objectStoreRequests);
let query = store.delete(name);
// handle success case
query.onsuccess = function (event) {
success('success')
};
// handle the error case
query.onerror = function (event) {
error(query.error.name)
}
})
}

165
index/js/proto.js Normal file
View File

@ -0,0 +1,165 @@
let protoMap;
let showCollection;
// IIFE to setup event listener
(function () {
// add event listener on proto checkbox
// if checked the uploader and proto collection view will be displayed
// also the grpc reflection will use given proto instead of server reflection
const protoSwitch = document.getElementById("local-proto");
protoSwitch.addEventListener("change", function(event) {
const { checked } = event.target;
toggleDisplayProtoInput(checked);
});
// add event listener on upload proto file
// uploaded file extension will be checked, only those with *.proto will be processed
// add successful file to protoMap to be displayed
const protoUploader = document.getElementById("proto-file");
protoUploader.addEventListener("change", handleProtoUpload, false);
// add event listener on toggle proto collection display
// it will show / hide the proto collection
const protoCollectionToggle = document.getElementById("proto-collection-toggle");
protoCollectionToggle.addEventListener("click", toggleDisplayProtoCollection);
// init map to handle proto files
// every proto files will be unique to their name
protoMap = new Map();
// set proto collection display status to true
// by default the collection will be shown
showCollection = true;
}());
// toggle proto files display
// displaying upload button
function toggleDisplayProtoInput(show) {
const style = show ? "display: block" : "display: none";
const protoInput = document.getElementById("proto-input");
protoInput.removeAttribute("style");
protoInput.style.cssText = style;
}
// toggle proto files collection
// displaying uploaded protos collection
function toggleDisplayProtoCollection() {
const protoCollection = document.getElementsByClassName("proto-collection")[0];
protoCollection.removeAttribute("style");
const protoToggle = document.getElementById("proto-collection-toggle");
let collectionStyle = "";
let toggleText = protoToggle.innerHTML;
if (showCollection) {
collectionStyle = "display: none";
toggleText = toggleText.replace("Hide", "Show");
} else {
collectionStyle = "display: block";
toggleText = toggleText.replace("Show", "Hide");
}
protoCollection.style.cssText = collectionStyle;
protoToggle.innerHTML = toggleText;
showCollection = !showCollection;
}
// handling file upload event
// add uploaded files to protoMap to avoid duplication, on file with same name the older
// file will be replaced with the latest
// if the file isn't available before, add DOM element for UI representation
// file without *.proto won't be processed
function handleProtoUpload() {
const files = this.files;
for (const file of files) {
if (!file.name.endsWith(".proto") && !file.name.endsWith(".protoset")) {
continue;
}
if (!protoMap.has(file.name)) {
addProtoItem(file.name, file.size);
}
protoMap.set(file.name, file);
}
}
// adding proto item to proto collection view
// give visual representation to user and access to remove unwanted uploaded files
function addProtoItem(name, size) {
const protoItem = createProtoItem(name, size);
const collection = document.getElementsByClassName("proto-collection")[0];
collection.appendChild(protoItem);
}
// create dom element for proto item
// every item will be given id corresponding to it's name for easier access
function createProtoItem(name, size) {
const item = document.createElement("div");
item.classList.add("proto-item");
item.id = name;
const icon = document.createElement("img");
icon.src = "img/file.png";
icon.alt = "file icon";
icon.classList.add("proto-icon");
item.appendChild(icon);
const desc = createProtoItemDesc(name, size);
item.appendChild(desc);
return item;
}
// create dom element for proto item description
function createProtoItemDesc(name, size) {
const desc = document.createElement("div");
desc.classList.add("proto-desc");
const caption = document.createElement("span");
caption.classList.add("proto-caption");
const captionText = document.createTextNode(name.length > 15 ?
name.substring(0, 12) + "..." :
name);
caption.appendChild(captionText);
desc.appendChild(caption);
const sizeDOM = document.createElement("span");
sizeDOM.classList.add("proto-size");
const sizeText = document.createTextNode(size > 1000 ?
`${size/1000}kb` :
`${size}b`);
sizeDOM.appendChild(sizeText);
desc.appendChild(sizeDOM);
const remove = document.createElement("button");
remove.classList.add("btn", "btn-sm", "proto-remove");
remove.addEventListener("click", function() {removeProtoItem(name);});
const removeIcon = document.createElement("i");
removeIcon.classList.add("fa", "fa-trash");
remove.appendChild(removeIcon);
const removeText = document.createTextNode(" remove");
remove.appendChild(removeText);
desc.appendChild(remove);
return desc;
}
// remove proto item based on it's ID
function removeProtoItem(name) {
const item = document.getElementById(name);
item.parentNode.removeChild(item);
protoMap.delete(name);
}
// fetch all proto from protoMap
// compose it into formData to make it easier to send via ajax
function getProtos() {
const formData = new FormData();
for (const proto of protoMap.values()) {
formData.append("protos", proto, proto.name);
}
return formData;
}

162
index/js/request.list.js Normal file
View File

@ -0,0 +1,162 @@
$('#save-request').click(function(){
let requestName = document.getElementById("input-request-name").value;
if (requestName === "") {
alert("request name is require")
} else {
let data = getReqResData();
data.name = requestName
insertRequest(data).then(success => {
window.location.reload()
}).catch(error => {
alert(error);
})
}
});
$('#show-modal-save-request').click(function () {
const reqData = getReqResData();
const activeRequestName = getActiveRequestListName();
if ( activeRequestName === ""){
console.log(activeRequestName);
// generate name
// name format will be method
$('#input-request-name').val(`${reqData.selected_function}`)
$('#saveRequest').modal('toggle');
} else {
reqData.name = activeRequestName
updateRequest(reqData).catch(error => {
alert(error);
})
}
});
$('#show-modal-save-as-request').click(function () {
const reqData = getReqResData();
// generate name
// name format will be method
$('#input-request-name').val(`copy ${reqData.selected_function}`)
$('#saveRequest').modal('toggle');
});
function getReqResData() {
const serverTarget = document.getElementById("server-target").value;
const selectService = document.getElementById("select-service").value;
const selectFunction = document.getElementById("select-function").value;
const responseHTML = document.getElementById("json-response").innerHTML;
const schemaProtoHTML = document.getElementById("schema-proto").innerHTML;
editor = ace.edit("editor");
return {
server_target:serverTarget,
selected_service:selectService,
selected_function:selectFunction,
raw_request:editor.getValue(),
response_html:responseHTML,
schema_proto_html:schemaProtoHTML,
}
}
function setReqResData(data) {
$('#server-target').val(data.server_target);
target = data.server_target;
$("#select-service").html(new Option(data.selected_service, data.selected_service,true,true));
$('#choose-service').show();
$("#select-function").html(new Option(data.selected_function.substr(data.selected_service.length), data.selected_function,true,true));
$('#choose-function').show();
generate_editor(data.raw_request);
$('#body-request').show();
$('#schema-proto').html(data.schema_proto_html);
$('#json-response').html(data.response_html);
$('#response').show();
}
function resetReqResData() {
target="";
$('#choose-service').hide();
$('#choose-function').hide();
$('#body-request').hide();
$('#response').hide();
}
async function renderRequestList() {
const ul = document.getElementById("request-list")
ul.innerHTML = ""
const nameList = await getAllRequestKey();
nameList.forEach(function (item){
let node = document.createElement("li")
node.classList.add("list-group-item","request-list")
node.setAttribute("request-name",item)
node.addEventListener("click", function(el){
updateRequestView(el.target.children[1])
});
node.innerHTML = `
<a title="Delete this request" class="delete-request" onclick="removeRequest(this)"><i class="fa fa-times"></i></a>
<p class="one-long-line request" onclick="updateRequestView(this)">${item}</p>
`
ul.appendChild(node);
})
}
function removeRequestSelectedClass(){
const elems = document.querySelectorAll(".request-list");
[].forEach.call(elems, function(el) {
el.classList.remove("selected");
});
}
function getActiveRequestListName(){
const elems = document.querySelectorAll(".request-list");
for (let i = 0; i < elems.length; i++) {
const e = elems[i]
if (e.classList.contains("selected")) {
return e.innerText;
}
}
return ""
}
function setServerTargetActive() {
const elems = document.querySelectorAll('[for="server-target"]');
[].forEach.call(elems, function(el) {
el.classList.add("active");
});
}
function updateRequestView(elm) {
if (elm) {
getRequest(elm.innerText).then(data => {
resetReqResData()
setReqResData(data)
removeRequestSelectedClass()
elm.parentElement.classList.add('selected')
setServerTargetActive();
}).catch(error => {
alert(error)
})
}
}
function removeRequest(elm) {
const requestName = elm.parentElement.lastElementChild.innerText;
deleteRequest(requestName).then(()=>{
window.location.reload()
}).catch((error)=>{
alert(error)
})
}
function search(elm) {
const li = document.querySelectorAll(".request-list")
li.forEach(function (el) {
if (el.getAttribute("request-name").toLowerCase().includes(elm.value.toLowerCase())){
el.style.display = ""
}else{
el.style.display = "none"
}
})
}
$(document).ready(function(){
renderRequestList()
});

View File

@ -1,6 +1,11 @@
var target, use_tls, editor;
$('#get-services').click(function(){
// reset all selected list
resetReqResData()
removeRequestSelectedClass()
var t = get_valid_target();
use_tls = "false";
@ -8,16 +13,28 @@ $('#get-services').click(function(){
if($('#restart-conn').is(":checked")) {
restart = "1"
}
if($('#use-tls').is(":checked")) {
use_tls = "true"
}
if (target != t || restart == "1") {
// use metadata if there is any
ctxArr = [];
$(".ctx-metadata-input-field").each(function(index, val){
ctxArr.push($(val).text())
});
// determine whether the proto connection will use local proto or not
const use_proto = $('#local-proto').is(":checked");
if (target != t || restart == "1" || use_proto) {
target = t;
} else {
return false;
}
$('.other-elem').hide();
var button = $(this).html();
$.ajax({
// prepare ajax options beforehand
// makes it easier for local proto to modify some of its properties
const ajaxProps = {
url: "server/"+target+"/services?restart="+restart,
global: true,
method: "GET",
@ -40,6 +57,9 @@ $('#get-services').click(function(){
beforeSend: function(xhr){
$('#choose-service').hide();
xhr.setRequestHeader('use_tls', use_tls);
if(ctxUse) {
xhr.setRequestHeader('Metadata', ctxArr);
}
$(this).html("Loading...");
show_loading();
},
@ -48,7 +68,21 @@ $('#get-services').click(function(){
$(this).html(button);
hide_loading();
}
});
};
// modify ajax options if use local proto
if (use_proto) {
ajaxProps.method = "POST";
ajaxProps.enctype = "multipart/form-data";
ajaxProps.processData = false;
ajaxProps.contentType = false;
ajaxProps.cache = false;
ajaxProps.data = getProtos();
}
$('.other-elem').hide();
var button = $(this).html();
$.ajax(ajaxProps);
});
$('#select-service').change(function(){
@ -118,6 +152,13 @@ $('#select-function').change(function(){
});
$('#invoke-func').click(function(){
// use metadata if there is any
ctxArr = [];
$(".ctx-metadata-input-field").each(function(index, val){
ctxArr.push($(val).text())
});
var func = $('#select-function').val();
if (func == "") {
return false;
@ -143,6 +184,9 @@ $('#invoke-func').click(function(){
beforeSend: function(xhr){
$('#response').hide();
xhr.setRequestHeader('use_tls', use_tls);
if(ctxUse) {
xhr.setRequestHeader('Metadata', ctxArr);
}
$(this).html("Loading...");
show_loading();
},