1
0
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:
Muhammad Auliya 2020-02-11 11:35:47 +07:00 committed by GitHub
commit 6148904bec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 638 additions and 21 deletions

3
.gitignore vendored
View File

@ -1,2 +1,3 @@
grpcox grpcox
log log
*.out

View File

@ -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()

View File

@ -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
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 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
} }

View File

@ -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"]

View File

@ -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
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);
}

BIN
index/img/file.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -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
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")) {
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;
}

View File

@ -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(){