mirror of
https://github.com/gusaul/grpcox.git
synced 2024-12-25 09:50:10 +00:00
Merge pull request #17 from alvinmatias69/master
feat(descriptor): add support for local proto descriptor
This commit is contained in:
commit
6148904bec
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
grpcox
|
grpcox
|
||||||
log
|
log
|
||||||
|
*.out
|
|
@ -3,6 +3,7 @@ package core
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -31,6 +32,14 @@ type GrpCox struct {
|
||||||
isUnixSocket func() bool
|
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
|
// InitGrpCox constructor
|
||||||
func InitGrpCox() *GrpCox {
|
func InitGrpCox() *GrpCox {
|
||||||
maxLife, tick := 10, 3
|
maxLife, tick := 10, 3
|
||||||
|
@ -80,6 +89,24 @@ func (g *GrpCox) GetResource(ctx context.Context, target string, plainText, isRe
|
||||||
return r, nil
|
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
|
// GetActiveConns - get all saved active connection
|
||||||
func (g *GrpCox) GetActiveConns(ctx context.Context) []string {
|
func (g *GrpCox) GetActiveConns(ctx context.Context) []string {
|
||||||
active := g.activeConn.getAllConn()
|
active := g.activeConn.getAllConn()
|
||||||
|
|
123
core/resource.go
123
core/resource.go
|
@ -5,8 +5,13 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fullstorydev/grpcurl"
|
"github.com/fullstorydev/grpcurl"
|
||||||
|
@ -18,22 +23,43 @@ import (
|
||||||
reflectpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
|
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)
|
// Resource - hold 3 main function (List, Describe, and Invoke)
|
||||||
type Resource struct {
|
type Resource struct {
|
||||||
clientConn *grpc.ClientConn
|
clientConn *grpc.ClientConn
|
||||||
descSource grpcurl.DescriptorSource
|
descSource grpcurl.DescriptorSource
|
||||||
refClient *grpcreflect.Client
|
refClient *grpcreflect.Client
|
||||||
|
protos []Proto
|
||||||
|
|
||||||
headers []string
|
headers []string
|
||||||
md metadata.MD
|
md metadata.MD
|
||||||
}
|
}
|
||||||
|
|
||||||
//openDescriptor - use it to reflect server descriptor
|
//openDescriptor - use it to reflect server descriptor
|
||||||
func (r *Resource) openDescriptor() {
|
func (r *Resource) openDescriptor() error {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
refCtx := metadata.NewOutgoingContext(ctx, r.md)
|
refCtx := metadata.NewOutgoingContext(ctx, r.md)
|
||||||
r.refClient = grpcreflect.NewClient(refCtx, reflectpb.NewServerReflectionClient(r.clientConn))
|
r.refClient = grpcreflect.NewClient(refCtx, reflectpb.NewServerReflectionClient(r.clientConn))
|
||||||
r.descSource = grpcurl.DescriptorSourceFromServer(ctx, r.refClient)
|
|
||||||
|
// if no protos available use server reflection
|
||||||
|
if r.protos == nil {
|
||||||
|
r.descSource = grpcurl.DescriptorSourceFromServer(ctx, r.refClient)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
protoPath := filepath.Join(BasePath, r.clientConn.Target())
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
r.descSource, err = grpcurl.DescriptorSourceFromProtoFiles([]string{protoPath}, protos...)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
//closeDescriptor - please ensure to always close after open in the same flow
|
//closeDescriptor - please ensure to always close after open in the same flow
|
||||||
|
@ -50,7 +76,7 @@ func (r *Resource) closeDescriptor() {
|
||||||
case <-done:
|
case <-done:
|
||||||
return
|
return
|
||||||
case <-time.After(3 * time.Second):
|
case <-time.After(3 * time.Second):
|
||||||
log.Printf("Reflection %s falied to close\n", r.clientConn.Target())
|
log.Printf("Reflection %s failed to close\n", r.clientConn.Target())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,7 +85,10 @@ func (r *Resource) closeDescriptor() {
|
||||||
// symbol can be "" to list all available services
|
// symbol can be "" to list all available services
|
||||||
// symbol also can be service name to list all available method
|
// symbol also can be service name to list all available method
|
||||||
func (r *Resource) List(symbol string) ([]string, error) {
|
func (r *Resource) List(symbol string) ([]string, error) {
|
||||||
r.openDescriptor()
|
err := r.openDescriptor()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
defer r.closeDescriptor()
|
defer r.closeDescriptor()
|
||||||
|
|
||||||
var result []string
|
var result []string
|
||||||
|
@ -97,7 +126,10 @@ 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 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.
|
// 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) {
|
func (r *Resource) Describe(symbol string) (string, string, error) {
|
||||||
r.openDescriptor()
|
err := r.openDescriptor()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
defer r.closeDescriptor()
|
defer r.closeDescriptor()
|
||||||
|
|
||||||
var result, template string
|
var result, template string
|
||||||
|
@ -153,7 +185,10 @@ func (r *Resource) Describe(symbol string) (string, string, error) {
|
||||||
|
|
||||||
// Invoke - invoking gRPC function
|
// Invoke - invoking gRPC function
|
||||||
func (r *Resource) Invoke(ctx context.Context, symbol string, in io.Reader) (string, time.Duration, error) {
|
func (r *Resource) Invoke(ctx context.Context, symbol string, in io.Reader) (string, time.Duration, error) {
|
||||||
r.openDescriptor()
|
err := r.openDescriptor()
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
defer r.closeDescriptor()
|
defer r.closeDescriptor()
|
||||||
|
|
||||||
// because of grpcurl directly fmt.Printf on their invoke function
|
// because of grpcurl directly fmt.Printf on their invoke function
|
||||||
|
@ -202,20 +237,37 @@ func (r *Resource) Invoke(ctx context.Context, symbol string, in io.Reader) (str
|
||||||
|
|
||||||
// Close - to close all resources that was opened before
|
// Close - to close all resources that was opened before
|
||||||
func (r *Resource) Close() {
|
func (r *Resource) Close() {
|
||||||
done := make(chan int)
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
if r.clientConn != nil {
|
if r.clientConn != nil {
|
||||||
r.clientConn.Close()
|
r.clientConn.Close()
|
||||||
r.clientConn = nil
|
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 {
|
select {
|
||||||
case <-done:
|
case <-c:
|
||||||
return
|
return
|
||||||
case <-time.After(3 * time.Second):
|
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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -229,3 +281,54 @@ func (r *Resource) exit(code int) {
|
||||||
r.Close()
|
r.Close()
|
||||||
os.Exit(code)
|
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) {
|
||||||
|
os.RemoveAll(protoPath)
|
||||||
|
err = os.MkdirAll(protoPath, 0777)
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, proto := range protos {
|
||||||
|
err := ioutil.WriteFile(filepath.Join(protoPath, "/", proto.Name),
|
||||||
|
prepareImport(proto.Content),
|
||||||
|
0777)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.protos = protos
|
||||||
|
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
70
core/resource_test.go
Normal 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))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
46
grpcox.go
46
grpcox.go
|
@ -1,13 +1,16 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/gusaul/grpcox/core"
|
||||||
"github.com/gusaul/grpcox/handler"
|
"github.com/gusaul/grpcox/handler"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -25,13 +28,48 @@ func main() {
|
||||||
port := ":6969"
|
port := ":6969"
|
||||||
muxRouter := mux.NewRouter()
|
muxRouter := mux.NewRouter()
|
||||||
handler.Init(muxRouter)
|
handler.Init(muxRouter)
|
||||||
|
var wait time.Duration = time.Second * 15
|
||||||
|
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
Handler: muxRouter,
|
|
||||||
Addr: "0.0.0.0" + port,
|
Addr: "0.0.0.0" + port,
|
||||||
WriteTimeout: 15 * time.Second,
|
WriteTimeout: time.Second * 15,
|
||||||
ReadTimeout: 15 * time.Second,
|
ReadTimeout: time.Second * 15,
|
||||||
|
IdleTimeout: time.Second * 60,
|
||||||
|
Handler: muxRouter,
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Service started on", port)
|
fmt.Println("Service started on", port)
|
||||||
log.Fatal(srv.ListenAndServe())
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -85,6 +86,65 @@ func (h *Handler) getLists(w http.ResponseWriter, r *http.Request) {
|
||||||
response(w, result)
|
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) {
|
func (h *Handler) describeFunction(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
host := vars["host"]
|
host := vars["host"]
|
||||||
|
|
|
@ -14,6 +14,7 @@ func Init(router *mux.Router) {
|
||||||
|
|
||||||
ajaxRoute := router.PathPrefix("/server/{host}").Subrouter()
|
ajaxRoute := router.PathPrefix("/server/{host}").Subrouter()
|
||||||
ajaxRoute.HandleFunc("/services", corsHandler(h.getLists)).Methods(http.MethodGet, http.MethodOptions)
|
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("/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}/describe", corsHandler(h.describeFunction)).Methods(http.MethodGet, http.MethodOptions)
|
||||||
ajaxRoute.HandleFunc("/function/{func_name}/invoke", corsHandler(h.invokeFunction)).Methods(http.MethodPost, http.MethodOptions)
|
ajaxRoute.HandleFunc("/function/{func_name}/invoke", corsHandler(h.invokeFunction)).Methods(http.MethodPost, http.MethodOptions)
|
||||||
|
|
115
index/css/proto.css
Normal file
115
index/css/proto.css
Normal 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);
|
||||||
|
}
|
BIN
index/img/file.png
Normal file
BIN
index/img/file.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
|
@ -10,6 +10,7 @@
|
||||||
<link href="css/bootstrap.min.css" rel="stylesheet">
|
<link href="css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link href="css/mdb.min.css" rel="stylesheet">
|
<link href="css/mdb.min.css" rel="stylesheet">
|
||||||
<link href="css/style.css" rel="stylesheet">
|
<link href="css/style.css" rel="stylesheet">
|
||||||
|
<link href="css/proto.css" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
@ -28,6 +29,24 @@
|
||||||
<label class="custom-control-label" for="restart-conn">Restart Connection</label>
|
<label class="custom-control-label" for="restart-conn">Restart Connection</label>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div class="other-elem" id="choose-service" style="display: none">
|
<div class="other-elem" id="choose-service" style="display: none">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-group-prepend">
|
<div class="input-group-prepend">
|
||||||
|
@ -118,6 +137,7 @@
|
||||||
<script src="https://cdn.rawgit.com/google/code-prettify/master/loader/run_prettify.js"></script>
|
<script src="https://cdn.rawgit.com/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 src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.1/ace.js"></script>
|
||||||
<script type="text/javascript" src="js/style.js"></script>
|
<script type="text/javascript" src="js/style.js"></script>
|
||||||
|
<script type="text/javascript" src="js/proto.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
165
index/js/proto.js
Normal file
165
index/js/proto.js
Normal 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")) {
|
||||||
|
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;
|
||||||
|
}
|
|
@ -9,15 +9,18 @@ $('#get-services').click(function(){
|
||||||
restart = "1"
|
restart = "1"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (target != t || restart == "1") {
|
// 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;
|
target = t;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$('.other-elem').hide();
|
// prepare ajax options beforehand
|
||||||
var button = $(this).html();
|
// makes it easier for local proto to modify some of its properties
|
||||||
$.ajax({
|
const ajaxProps = {
|
||||||
url: "server/"+target+"/services?restart="+restart,
|
url: "server/"+target+"/services?restart="+restart,
|
||||||
global: true,
|
global: true,
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
@ -48,7 +51,21 @@ $('#get-services').click(function(){
|
||||||
$(this).html(button);
|
$(this).html(button);
|
||||||
hide_loading();
|
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(){
|
$('#select-service').change(function(){
|
||||||
|
|
Loading…
Reference in New Issue
Block a user