mirror of
https://github.com/gusaul/grpcox.git
synced 2024-12-25 18:10:11 +00:00
Merge pull request #17 from alvinmatias69/master
feat(descriptor): add support for local proto descriptor
This commit is contained in:
commit
6148904bec
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
|||
grpcox
|
||||
log
|
||||
*.out
|
|
@ -3,6 +3,7 @@ package core
|
|||
import (
|
||||
"context"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
|
@ -31,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
|
||||
|
@ -80,6 +89,24 @@ func (g *GrpCox) GetResource(ctx context.Context, target string, plainText, isRe
|
|||
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()
|
||||
|
|
123
core/resource.go
123
core/resource.go
|
@ -5,8 +5,13 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fullstorydev/grpcurl"
|
||||
|
@ -18,22 +23,43 @@ import (
|
|||
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
|
||||
|
||||
headers []string
|
||||
md metadata.MD
|
||||
}
|
||||
|
||||
//openDescriptor - use it to reflect server descriptor
|
||||
func (r *Resource) openDescriptor() {
|
||||
func (r *Resource) openDescriptor() error {
|
||||
ctx := context.Background()
|
||||
refCtx := metadata.NewOutgoingContext(ctx, r.md)
|
||||
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
|
||||
|
@ -50,7 +76,7 @@ func (r *Resource) closeDescriptor() {
|
|||
case <-done:
|
||||
return
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -59,7 +85,10 @@ func (r *Resource) closeDescriptor() {
|
|||
// 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) {
|
||||
r.openDescriptor()
|
||||
err := r.openDescriptor()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer r.closeDescriptor()
|
||||
|
||||
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 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) {
|
||||
r.openDescriptor()
|
||||
err := r.openDescriptor()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
defer r.closeDescriptor()
|
||||
|
||||
var result, template string
|
||||
|
@ -153,7 +185,10 @@ 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) {
|
||||
r.openDescriptor()
|
||||
err := r.openDescriptor()
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
defer r.closeDescriptor()
|
||||
|
||||
// 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
|
||||
func (r *Resource) Close() {
|
||||
done := make(chan int)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -229,3 +281,54 @@ 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) {
|
||||
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
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gusaul/grpcox/core"
|
||||
"github.com/gusaul/grpcox/handler"
|
||||
)
|
||||
|
||||
|
@ -25,13 +28,48 @@ func main() {
|
|||
port := ":6969"
|
||||
muxRouter := mux.NewRouter()
|
||||
handler.Init(muxRouter)
|
||||
var wait time.Duration = time.Second * 15
|
||||
|
||||
srv := &http.Server{
|
||||
Handler: muxRouter,
|
||||
Addr: "0.0.0.0" + port,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: time.Second * 15,
|
||||
ReadTimeout: time.Second * 15,
|
||||
IdleTimeout: time.Second * 60,
|
||||
Handler: muxRouter,
|
||||
}
|
||||
|
||||
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"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -85,6 +86,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"]
|
||||
|
|
|
@ -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
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/mdb.min.css" rel="stylesheet">
|
||||
<link href="css/style.css" rel="stylesheet">
|
||||
<link href="css/proto.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -28,6 +29,24 @@
|
|||
<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>
|
||||
|
||||
<div class="other-elem" id="choose-service" style="display: none">
|
||||
<div class="input-group">
|
||||
<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://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/proto.js"></script>
|
||||
</body>
|
||||
|
||||
</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"
|
||||
}
|
||||
|
||||
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;
|
||||
} 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",
|
||||
|
@ -48,7 +51,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(){
|
||||
|
|
Loading…
Reference in New Issue
Block a user