...
This commit is contained in:
		
							
								
								
									
										173
									
								
								pkg/servers/ui/controllers/openrpc_controller.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								pkg/servers/ui/controllers/openrpc_controller.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,173 @@ | ||||
| package controllers | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"log" | ||||
|  | ||||
| 	orpcmodels "git.ourworld.tf/herocode/heroagent/pkg/openrpc/models" | ||||
| 	uimodels "git.ourworld.tf/herocode/heroagent/pkg/servers/ui/models" | ||||
| 	"github.com/gofiber/fiber/v2" | ||||
| ) | ||||
|  | ||||
| // OpenRPCController handles requests related to OpenRPC specifications | ||||
| type OpenRPCController struct { | ||||
| 	openrpcManager uimodels.OpenRPCUIManager | ||||
| } | ||||
|  | ||||
| // NewOpenRPCController creates a new instance of OpenRPCController | ||||
| func NewOpenRPCController(openrpcManager uimodels.OpenRPCUIManager) *OpenRPCController { | ||||
| 	return &OpenRPCController{ | ||||
| 		openrpcManager: openrpcManager, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // OpenRPCPageData represents the data needed for the OpenRPC UI pages | ||||
| type OpenRPCPageData struct { | ||||
| 	Title          string | ||||
| 	Specs          []string | ||||
| 	SelectedSpec   string | ||||
| 	Methods        []string | ||||
| 	SelectedMethod string | ||||
| 	Method         *orpcmodels.Method | ||||
| 	SocketPath     string | ||||
| 	ExampleParams  string | ||||
| 	Result         string | ||||
| 	Error          string | ||||
| } | ||||
|  | ||||
| // ShowOpenRPCUI renders the OpenRPC UI page | ||||
| func (c *OpenRPCController) ShowOpenRPCUI(ctx *fiber.Ctx) error { | ||||
| 	// Get query parameters | ||||
| 	selectedSpec := ctx.Query("spec", "") | ||||
| 	selectedMethod := ctx.Query("method", "") | ||||
| 	socketPath := ctx.Query("socketPath", "") | ||||
|  | ||||
| 	// Get all specs | ||||
| 	specs := c.openrpcManager.ListSpecs() | ||||
|  | ||||
| 	// Initialize page data using fiber.Map instead of struct | ||||
| 	pageData := fiber.Map{ | ||||
| 		"Title":        "OpenRPC UI", | ||||
| 		"SpecList":     specs, | ||||
| 		"SelectedSpec": selectedSpec, | ||||
| 		"SocketPath":   socketPath, | ||||
| 	} | ||||
|  | ||||
| 	// If a spec is selected, get its methods | ||||
| 	if selectedSpec != "" { | ||||
| 		methods := c.openrpcManager.ListMethods(selectedSpec) | ||||
| 		pageData["Methods"] = methods | ||||
| 		pageData["SelectedMethod"] = selectedMethod | ||||
|  | ||||
| 		// If a method is selected, get its details | ||||
| 		if selectedMethod != "" { | ||||
| 			method := c.openrpcManager.GetMethod(selectedSpec, selectedMethod) | ||||
| 			if method != nil { | ||||
| 				pageData["Method"] = method | ||||
|  | ||||
| 				// Generate example parameters if available | ||||
| 				if len(method.Examples) > 0 { | ||||
| 					exampleParams, err := json.MarshalIndent(method.Examples[0].Params, "", "  ") | ||||
| 					if err == nil { | ||||
| 						pageData["ExampleParams"] = string(exampleParams) | ||||
| 					} | ||||
| 				} else if len(method.Params) > 0 { | ||||
| 					// Generate example from parameter schema | ||||
| 					exampleParams := generateExampleParams(method.Params) | ||||
| 					jsonParams, err := json.MarshalIndent(exampleParams, "", "  ") | ||||
| 					if err == nil { | ||||
| 						pageData["ExampleParams"] = string(jsonParams) | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return ctx.Render("pages/rpcui", pageData) | ||||
| } | ||||
|  | ||||
| // ExecuteRPC handles RPC execution requests | ||||
| func (c *OpenRPCController) ExecuteRPC(ctx *fiber.Ctx) error { | ||||
| 	// Parse request | ||||
| 	var request struct { | ||||
| 		Spec       string          `json:"spec"` | ||||
| 		Method     string          `json:"method"` | ||||
| 		SocketPath string          `json:"socketPath"` | ||||
| 		Params     json.RawMessage `json:"params"` | ||||
| 	} | ||||
|  | ||||
| 	if err := ctx.BodyParser(&request); err != nil { | ||||
| 		return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{ | ||||
| 			"error": "Invalid request: " + err.Error(), | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	// Validate request | ||||
| 	if request.Spec == "" || request.Method == "" || request.SocketPath == "" { | ||||
| 		return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{ | ||||
| 			"error": "Missing required fields: spec, method, or socketPath", | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	// Parse params | ||||
| 	var params interface{} | ||||
| 	if len(request.Params) > 0 { | ||||
| 		if err := json.Unmarshal(request.Params, ¶ms); err != nil { | ||||
| 			return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{ | ||||
| 				"error": "Invalid parameters: " + err.Error(), | ||||
| 			}) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Execute RPC | ||||
| 	result, err := c.openrpcManager.ExecuteRPC(request.Spec, request.Method, request.SocketPath, params) | ||||
| 	if err != nil { | ||||
| 		log.Printf("Error executing RPC: %v", err) | ||||
| 		return ctx.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ | ||||
| 			"error": err.Error(), | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	// Return result | ||||
| 	return ctx.JSON(fiber.Map{ | ||||
| 		"result": result, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // generateExampleParams generates example parameters from parameter schemas | ||||
| func generateExampleParams(params []orpcmodels.Parameter) map[string]interface{} { | ||||
| 	example := make(map[string]interface{}) | ||||
|  | ||||
| 	for _, param := range params { | ||||
| 		example[param.Name] = generateExampleValue(param.Schema) | ||||
| 	} | ||||
|  | ||||
| 	return example | ||||
| } | ||||
|  | ||||
| // generateExampleValue generates an example value from a schema | ||||
| func generateExampleValue(schema orpcmodels.SchemaObject) interface{} { | ||||
| 	switch schema.Type { | ||||
| 	case "string": | ||||
| 		return "example" | ||||
| 	case "number": | ||||
| 		return 0 | ||||
| 	case "integer": | ||||
| 		return 0 | ||||
| 	case "boolean": | ||||
| 		return false | ||||
| 	case "array": | ||||
| 		if schema.Items != nil { | ||||
| 			return []interface{}{generateExampleValue(*schema.Items)} | ||||
| 		} | ||||
| 		return []interface{}{} | ||||
| 	case "object": | ||||
| 		obj := make(map[string]interface{}) | ||||
| 		for name, propSchema := range schema.Properties { | ||||
| 			obj[name] = generateExampleValue(propSchema) | ||||
| 		} | ||||
| 		return obj | ||||
| 	default: | ||||
| 		return nil | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										190
									
								
								pkg/servers/ui/models/openrpc_manager.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										190
									
								
								pkg/servers/ui/models/openrpc_manager.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,190 @@ | ||||
| package models | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"time" | ||||
|  | ||||
| 	"git.ourworld.tf/herocode/heroagent/pkg/openrpc" | ||||
| 	"git.ourworld.tf/herocode/heroagent/pkg/openrpc/models" | ||||
| ) | ||||
|  | ||||
| // OpenRPCUIManager is the interface for managing OpenRPC specifications in the UI | ||||
| type OpenRPCUIManager interface { | ||||
| 	// ListSpecs returns a list of all loaded specification names | ||||
| 	ListSpecs() []string | ||||
|  | ||||
| 	// GetSpec returns an OpenRPC specification by name | ||||
| 	GetSpec(name string) *models.OpenRPCSpec | ||||
|  | ||||
| 	// ListMethods returns a list of all method names in a specification | ||||
| 	ListMethods(specName string) []string | ||||
|  | ||||
| 	// GetMethod returns a method from a specification | ||||
| 	GetMethod(specName, methodName string) *models.Method | ||||
|  | ||||
| 	// ExecuteRPC executes an RPC call | ||||
| 	ExecuteRPC(specName, methodName, socketPath string, params interface{}) (interface{}, error) | ||||
| } | ||||
|  | ||||
| // OpenRPCManager implements the OpenRPCUIManager interface | ||||
| type OpenRPCManager struct { | ||||
| 	manager *openrpc.ORPCManager | ||||
| } | ||||
|  | ||||
| // NewOpenRPCManager creates a new OpenRPCUIManager | ||||
| func NewOpenRPCManager() OpenRPCUIManager { | ||||
| 	manager := openrpc.NewORPCManager() | ||||
|  | ||||
| 	// Try to load specs from the default directory | ||||
| 	specDirs := []string{ | ||||
| 		"./pkg/openrpc/services", | ||||
| 		"./pkg/openrpc/specs", | ||||
| 		"./specs/openrpc", | ||||
| 	} | ||||
|  | ||||
| 	for _, dir := range specDirs { | ||||
| 		err := manager.LoadSpecs(dir) | ||||
| 		if err == nil { | ||||
| 			// Successfully loaded specs from this directory | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return &OpenRPCManager{ | ||||
| 		manager: manager, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // ListSpecs returns a list of all loaded specification names | ||||
| func (m *OpenRPCManager) ListSpecs() []string { | ||||
| 	return m.manager.ListSpecs() | ||||
| } | ||||
|  | ||||
| // GetSpec returns an OpenRPC specification by name | ||||
| func (m *OpenRPCManager) GetSpec(name string) *models.OpenRPCSpec { | ||||
| 	return m.manager.GetSpec(name) | ||||
| } | ||||
|  | ||||
| // ListMethods returns a list of all method names in a specification | ||||
| func (m *OpenRPCManager) ListMethods(specName string) []string { | ||||
| 	return m.manager.ListMethods(specName) | ||||
| } | ||||
|  | ||||
| // GetMethod returns a method from a specification | ||||
| func (m *OpenRPCManager) GetMethod(specName, methodName string) *models.Method { | ||||
| 	return m.manager.GetMethod(specName, methodName) | ||||
| } | ||||
|  | ||||
| // ExecuteRPC executes an RPC call | ||||
| func (m *OpenRPCManager) ExecuteRPC(specName, methodName, socketPath string, params interface{}) (interface{}, error) { | ||||
| 	// Create JSON-RPC request | ||||
| 	request := map[string]interface{}{ | ||||
| 		"jsonrpc": "2.0", | ||||
| 		"method":  methodName, | ||||
| 		"params":  params, | ||||
| 		"id":      1, | ||||
| 	} | ||||
|  | ||||
| 	// Marshal request to JSON | ||||
| 	requestJSON, err := json.Marshal(request) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to marshal request: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	// Check if socket path is a Unix socket or HTTP endpoint | ||||
| 	if socketPath[:1] == "/" { | ||||
| 		// Unix socket | ||||
| 		return executeUnixSocketRPC(socketPath, requestJSON) | ||||
| 	} else { | ||||
| 		// HTTP endpoint | ||||
| 		return executeHTTPRPC(socketPath, requestJSON) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // executeUnixSocketRPC executes an RPC call over a Unix socket | ||||
| func executeUnixSocketRPC(socketPath string, requestJSON []byte) (interface{}, error) { | ||||
| 	// Connect to Unix socket | ||||
| 	conn, err := net.Dial("unix", socketPath) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to connect to socket %s: %w", socketPath, err) | ||||
| 	} | ||||
| 	defer conn.Close() | ||||
|  | ||||
| 	// Set timeout | ||||
| 	deadline := time.Now().Add(10 * time.Second) | ||||
| 	if err := conn.SetDeadline(deadline); err != nil { | ||||
| 		return nil, fmt.Errorf("failed to set deadline: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	// Send request | ||||
| 	if _, err := conn.Write(requestJSON); err != nil { | ||||
| 		return nil, fmt.Errorf("failed to send request: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	// Read response | ||||
| 	var buf bytes.Buffer | ||||
| 	if _, err := buf.ReadFrom(conn); err != nil { | ||||
| 		return nil, fmt.Errorf("failed to read response: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	// Parse response | ||||
| 	var response map[string]interface{} | ||||
| 	if err := json.Unmarshal(buf.Bytes(), &response); err != nil { | ||||
| 		return nil, fmt.Errorf("failed to parse response: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	// Check for error | ||||
| 	if errObj, ok := response["error"]; ok { | ||||
| 		return nil, fmt.Errorf("RPC error: %v", errObj) | ||||
| 	} | ||||
|  | ||||
| 	// Return result | ||||
| 	return response["result"], nil | ||||
| } | ||||
|  | ||||
| // executeHTTPRPC executes an RPC call over HTTP | ||||
| func executeHTTPRPC(endpoint string, requestJSON []byte) (interface{}, error) { | ||||
| 	// Create HTTP request | ||||
| 	req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(requestJSON)) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to create HTTP request: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	// Set headers | ||||
| 	req.Header.Set("Content-Type", "application/json") | ||||
|  | ||||
| 	// Create HTTP client with timeout | ||||
| 	client := &http.Client{ | ||||
| 		Timeout: 10 * time.Second, | ||||
| 	} | ||||
|  | ||||
| 	// Send request | ||||
| 	resp, err := client.Do(req) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to send HTTP request: %w", err) | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
|  | ||||
| 	// Check status code | ||||
| 	if resp.StatusCode != http.StatusOK { | ||||
| 		return nil, fmt.Errorf("HTTP error: %s", resp.Status) | ||||
| 	} | ||||
|  | ||||
| 	// Parse response | ||||
| 	var response map[string]interface{} | ||||
| 	if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { | ||||
| 		return nil, fmt.Errorf("failed to parse response: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	// Check for error | ||||
| 	if errObj, ok := response["error"]; ok { | ||||
| 		return nil, fmt.Errorf("RPC error: %v", errObj) | ||||
| 	} | ||||
|  | ||||
| 	// Return result | ||||
| 	return response["result"], nil | ||||
| } | ||||
| @@ -12,11 +12,13 @@ func SetupRoutes(app *fiber.App) { | ||||
| 	// For now, using the mock process manager | ||||
| 	processManagerService := models.NewMockProcessManager() | ||||
| 	jobManagerService := models.NewMockJobManager() | ||||
| 	openrpcManagerService := models.NewOpenRPCManager() | ||||
|  | ||||
| 	dashboardController := controllers.NewDashboardController() | ||||
| 	processController := controllers.NewProcessController(processManagerService) | ||||
| 	jobController := controllers.NewJobController(jobManagerService) | ||||
| 	authController := controllers.NewAuthController() | ||||
| 	openrpcController := controllers.NewOpenRPCController(openrpcManagerService) | ||||
|  | ||||
| 	// --- Public Routes --- | ||||
| 	// Login and Logout | ||||
| @@ -43,6 +45,10 @@ func SetupRoutes(app *fiber.App) { | ||||
| 	app.Get("/jobs", jobController.ShowJobsPage) | ||||
| 	app.Get("/jobs/:id", jobController.ShowJobDetails) | ||||
|  | ||||
| 	// OpenRPC UI routes | ||||
| 	app.Get("/rpcui", openrpcController.ShowOpenRPCUI) | ||||
| 	app.Post("/api/rpcui/execute", openrpcController.ExecuteRPC) | ||||
|  | ||||
| 	// Debug routes | ||||
| 	app.Get("/debug", func(c *fiber.Ctx) error { | ||||
| 		// Get all data from the jobs page to debug | ||||
|   | ||||
							
								
								
									
										144
									
								
								pkg/servers/ui/static/css/rpcui.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								pkg/servers/ui/static/css/rpcui.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,144 @@ | ||||
| /* OpenRPC UI Styles */ | ||||
|  | ||||
| .method-tree { | ||||
|   max-height: 600px; | ||||
|   overflow-y: auto; | ||||
|   border-right: 1px solid #dee2e6; | ||||
| } | ||||
|  | ||||
| .method-item { | ||||
|   cursor: pointer; | ||||
|   padding: 8px 15px; | ||||
|   border-radius: 4px; | ||||
|   transition: background-color 0.2s ease; | ||||
| } | ||||
|  | ||||
| .method-item:hover { | ||||
|   background-color: #f8f9fa; | ||||
| } | ||||
|  | ||||
| .method-item.active { | ||||
|   background-color: #e9ecef; | ||||
|   font-weight: bold; | ||||
|   border-left: 3px solid #0d6efd; | ||||
| } | ||||
|  | ||||
| .param-card { | ||||
|   margin-bottom: 15px; | ||||
|   border: 1px solid #dee2e6; | ||||
|   border-radius: 4px; | ||||
| } | ||||
|  | ||||
| .result-container { | ||||
|   max-height: 400px; | ||||
|   overflow-y: auto; | ||||
|   margin-top: 20px; | ||||
|   padding: 15px; | ||||
|   border: 1px solid #dee2e6; | ||||
|   border-radius: 4px; | ||||
|   background-color: #f8f9fa; | ||||
| } | ||||
|  | ||||
| .code-editor { | ||||
|   font-family: 'Courier New', Courier, monospace; | ||||
|   min-height: 200px; | ||||
|   max-height: 400px; | ||||
|   overflow-y: auto; | ||||
|   white-space: pre; | ||||
|   font-size: 14px; | ||||
|   line-height: 1.5; | ||||
|   padding: 10px; | ||||
|   border: 1px solid #ced4da; | ||||
|   border-radius: 4px; | ||||
|   background-color: #f8f9fa; | ||||
| } | ||||
|  | ||||
| .schema-table { | ||||
|   font-size: 0.9rem; | ||||
|   width: 100%; | ||||
|   margin-bottom: 1rem; | ||||
| } | ||||
|  | ||||
| .schema-table th { | ||||
|   font-weight: 600; | ||||
|   background-color: #f8f9fa; | ||||
| } | ||||
|  | ||||
| .schema-required { | ||||
|   color: #dc3545; | ||||
|   font-weight: bold; | ||||
| } | ||||
|  | ||||
| .schema-optional { | ||||
|   color: #6c757d; | ||||
| } | ||||
|  | ||||
| .method-description { | ||||
|   font-size: 0.9rem; | ||||
|   color: #6c757d; | ||||
|   margin-bottom: 15px; | ||||
| } | ||||
|  | ||||
| .section-header { | ||||
|   font-size: 1.1rem; | ||||
|   font-weight: 600; | ||||
|   margin-top: 20px; | ||||
|   margin-bottom: 10px; | ||||
|   padding-bottom: 5px; | ||||
|   border-bottom: 1px solid #dee2e6; | ||||
| } | ||||
|  | ||||
| .example-container { | ||||
|   margin-bottom: 15px; | ||||
| } | ||||
|  | ||||
| .example-header { | ||||
|   font-weight: 600; | ||||
|   margin-bottom: 5px; | ||||
| } | ||||
|  | ||||
| .example-content { | ||||
|   background-color: #f8f9fa; | ||||
|   padding: 10px; | ||||
|   border-radius: 4px; | ||||
|   overflow-x: auto; | ||||
| } | ||||
|  | ||||
| /* Socket path input styling */ | ||||
| .socket-path-container { | ||||
|   margin-bottom: 15px; | ||||
| } | ||||
|  | ||||
| .socket-path-label { | ||||
|   font-weight: 600; | ||||
|   margin-bottom: 5px; | ||||
| } | ||||
|  | ||||
| /* Execute button styling */ | ||||
| .execute-button { | ||||
|   margin-top: 10px; | ||||
| } | ||||
|  | ||||
| /* Response styling */ | ||||
| .response-success { | ||||
|   border-left: 4px solid #28a745; | ||||
| } | ||||
|  | ||||
| .response-error { | ||||
|   border-left: 4px solid #dc3545; | ||||
| } | ||||
|  | ||||
| /* Loading indicator */ | ||||
| .loading-spinner { | ||||
|   display: inline-block; | ||||
|   width: 1rem; | ||||
|   height: 1rem; | ||||
|   border: 0.2em solid currentColor; | ||||
|   border-right-color: transparent; | ||||
|   border-radius: 50%; | ||||
|   animation: spinner-border .75s linear infinite; | ||||
| } | ||||
|  | ||||
| @keyframes spinner-border { | ||||
|   to { transform: rotate(360deg); } | ||||
| } | ||||
							
								
								
									
										141
									
								
								pkg/servers/ui/static/js/rpcui.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								pkg/servers/ui/static/js/rpcui.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,141 @@ | ||||
| /** | ||||
|  * OpenRPC UI JavaScript | ||||
|  * Handles the interactive functionality of the OpenRPC UI | ||||
|  */ | ||||
|  | ||||
| document.addEventListener('DOMContentLoaded', function() { | ||||
|   // Initialize form elements | ||||
|   const specForm = document.getElementById('specForm'); | ||||
|   const rpcForm = document.getElementById('rpcForm'); | ||||
|   const paramsEditor = document.getElementById('paramsEditor'); | ||||
|   const resultContainer = document.getElementById('resultContainer'); | ||||
|   const resultOutput = document.getElementById('resultOutput'); | ||||
|   const errorContainer = document.getElementById('errorContainer'); | ||||
|   const errorOutput = document.getElementById('errorOutput'); | ||||
|    | ||||
|   // Format JSON in the parameters editor | ||||
|   if (paramsEditor && paramsEditor.value) { | ||||
|     try { | ||||
|       const params = JSON.parse(paramsEditor.value); | ||||
|       paramsEditor.value = JSON.stringify(params, null, 2); | ||||
|     } catch (e) { | ||||
|       // If not valid JSON, leave as is | ||||
|       console.warn('Could not format parameters as JSON:', e); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   // Handle RPC execution | ||||
|   if (rpcForm) { | ||||
|     rpcForm.addEventListener('submit', function(e) { | ||||
|       e.preventDefault(); | ||||
|        | ||||
|       // Hide previous results | ||||
|       if (resultContainer) resultContainer.classList.add('d-none'); | ||||
|       if (errorContainer) errorContainer.classList.add('d-none'); | ||||
|        | ||||
|       // Get form data | ||||
|       const spec = document.getElementById('spec').value; | ||||
|       const method = document.querySelector('input[name="selectedMethod"]').value; | ||||
|       const socketPath = document.getElementById('socketPath').value; | ||||
|       const paramsText = paramsEditor.value; | ||||
|        | ||||
|       // Show loading indicator | ||||
|       const submitButton = rpcForm.querySelector('button[type="submit"]'); | ||||
|       const originalButtonText = submitButton.innerHTML; | ||||
|       submitButton.disabled = true; | ||||
|       submitButton.innerHTML = '<span class="loading-spinner me-2"></span>Executing...'; | ||||
|        | ||||
|       // Validate | ||||
|       if (!spec || !method || !socketPath) { | ||||
|         showError('Missing required fields: spec, method, or socketPath'); | ||||
|         resetButton(); | ||||
|         return; | ||||
|       } | ||||
|        | ||||
|       // Parse params | ||||
|       let params; | ||||
|       try { | ||||
|         params = JSON.parse(paramsText); | ||||
|       } catch (e) { | ||||
|         showError('Invalid JSON parameters: ' + e.message); | ||||
|         resetButton(); | ||||
|         return; | ||||
|       } | ||||
|        | ||||
|       // Execute RPC | ||||
|       fetch('/api/rpcui/execute', { | ||||
|         method: 'POST', | ||||
|         headers: { | ||||
|           'Content-Type': 'application/json' | ||||
|         }, | ||||
|         body: JSON.stringify({ | ||||
|           spec: spec, | ||||
|           method: method, | ||||
|           socketPath: socketPath, | ||||
|           params: params | ||||
|         }) | ||||
|       }) | ||||
|       .then(response => response.json()) | ||||
|       .then(data => { | ||||
|         if (data.error) { | ||||
|           showError(data.error); | ||||
|         } else { | ||||
|           showResult(data.result); | ||||
|         } | ||||
|       }) | ||||
|       .catch(error => { | ||||
|         showError('Request failed: ' + error.message); | ||||
|       }) | ||||
|       .finally(() => { | ||||
|         resetButton(); | ||||
|       }); | ||||
|        | ||||
|       function resetButton() { | ||||
|         submitButton.disabled = false; | ||||
|         submitButton.innerHTML = originalButtonText; | ||||
|       } | ||||
|        | ||||
|       function showError(message) { | ||||
|         if (errorContainer && errorOutput) { | ||||
|           errorContainer.classList.remove('d-none'); | ||||
|           errorOutput.textContent = message; | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       function showResult(result) { | ||||
|         if (resultContainer && resultOutput) { | ||||
|           resultContainer.classList.remove('d-none'); | ||||
|           resultOutput.textContent = JSON.stringify(result, null, 2); | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   // Method tree navigation | ||||
|   const methodItems = document.querySelectorAll('.method-item'); | ||||
|   methodItems.forEach(item => { | ||||
|     item.addEventListener('click', function(e) { | ||||
|       // Already handled by href, but could add additional functionality here | ||||
|     }); | ||||
|   }); | ||||
|    | ||||
|   // Format JSON examples | ||||
|   const jsonExamples = document.querySelectorAll('pre code'); | ||||
|   jsonExamples.forEach(example => { | ||||
|     try { | ||||
|       const content = example.textContent; | ||||
|       const json = JSON.parse(content); | ||||
|       example.textContent = JSON.stringify(json, null, 2); | ||||
|     } catch (e) { | ||||
|       // If not valid JSON, leave as is | ||||
|       console.warn('Could not format example as JSON:', e); | ||||
|     } | ||||
|   }); | ||||
|    | ||||
|   // Add syntax highlighting if a library like highlight.js is available | ||||
|   if (typeof hljs !== 'undefined') { | ||||
|     document.querySelectorAll('pre code').forEach((block) => { | ||||
|       hljs.highlightBlock(block); | ||||
|     }); | ||||
|   } | ||||
| }); | ||||
| @@ -18,6 +18,12 @@ | ||||
|             Job Manager | ||||
|         </a> | ||||
|     </li> | ||||
|     <li class="nav-item"> | ||||
|         <a class="nav-link" href="/rpcui"> | ||||
|             <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-code"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline></svg> | ||||
|             OpenRPC UI | ||||
|         </a> | ||||
|     </li> | ||||
|     <!-- Add more menu items here as needed --> | ||||
| </ul> | ||||
| {{ end }} | ||||
							
								
								
									
										185
									
								
								pkg/servers/ui/views/pages/rpcui.jet
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										185
									
								
								pkg/servers/ui/views/pages/rpcui.jet
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,185 @@ | ||||
| {{ extends "../layouts/base" }} | ||||
|  | ||||
| {{ block title() }}OpenRPC UI - HeroApp UI{{ end }} | ||||
|  | ||||
| {{ block css() }} | ||||
| <link rel="stylesheet" href="/static/css/rpcui.css"> | ||||
| {{ end }} | ||||
|  | ||||
| {{ block body() }} | ||||
| <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"> | ||||
|   <h1 class="h2">OpenRPC UI</h1> | ||||
| </div> | ||||
|  | ||||
| <div class="row mb-4"> | ||||
|   <div class="col-md-12"> | ||||
|     <div class="card"> | ||||
|       <div class="card-header"> | ||||
|         <h5>Select OpenRPC Specification</h5> | ||||
|       </div> | ||||
|       <div class="card-body"> | ||||
|         <form id="specForm" action="/rpcui" method="get" class="row g-3"> | ||||
|           <div class="col-md-4"> | ||||
|             <label for="spec" class="form-label">Specification</label> | ||||
|             <select class="form-select" id="spec" name="spec" onchange="this.form.submit()"> | ||||
|               <option value="">Select a specification</option> | ||||
|               {{ if .SpecList }} | ||||
|                 {{ range .SpecList }} | ||||
|                 <option value="{{ . }}" {{ if eq . $.SelectedSpec }}selected{{ end }}>{{ . }}</option> | ||||
|                 {{ end }} | ||||
|               {{ else }} | ||||
|                 <option value="" disabled>No specifications available</option> | ||||
|               {{ end }} | ||||
|             </select> | ||||
|           </div> | ||||
|           <div class="col-md-4"> | ||||
|             <label for="socketPath" class="form-label">Socket Path</label> | ||||
|             <input type="text" class="form-control" id="socketPath" name="socketPath" value="{{ .SocketPath }}" placeholder="e.g., /tmp/rpc.sock"> | ||||
|           </div> | ||||
|           <div class="col-md-4 d-flex align-items-end"> | ||||
|             <button type="submit" class="btn btn-primary">Apply</button> | ||||
|           </div> | ||||
|         </form> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
| <div class="row"> | ||||
|   <div class="col-md-12"> | ||||
|     <div class="alert alert-info"> | ||||
|       <p>This is the OpenRPC UI page. It allows you to interact with OpenRPC specifications.</p> | ||||
|       <p>Currently available specs: {{ if .SpecList }}{{ len(.SpecList) }}{{ else }}0{{ end }}</p> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
| {{ if .SelectedSpec }} | ||||
| <div class="row"> | ||||
|   <!-- Method Tree --> | ||||
|   <div class="col-md-3"> | ||||
|     <div class="card"> | ||||
|       <div class="card-header"> | ||||
|         <h5>Methods</h5> | ||||
|       </div> | ||||
|       <div class="card-body p-0"> | ||||
|         <div class="method-tree list-group list-group-flush"> | ||||
|           {{ if .Methods }} | ||||
|             {{ range .Methods }} | ||||
|             <a href="/rpcui?spec={{ $.SelectedSpec }}&method={{ . }}&socketPath={{ $.SocketPath }}"  | ||||
|                class="list-group-item list-group-item-action method-item {{ if eq . $.SelectedMethod }}active{{ end }}"> | ||||
|               {{ . }} | ||||
|             </a> | ||||
|             {{ end }} | ||||
|           {{ else }} | ||||
|             <div class="list-group-item">No methods available</div> | ||||
|           {{ end }} | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|  | ||||
|   <!-- Method Details --> | ||||
|   <div class="col-md-9"> | ||||
|     {{ if .Method }} | ||||
|     <div class="card mb-4"> | ||||
|       <div class="card-header"> | ||||
|         <h5>{{ .Method.Name }}</h5> | ||||
|         {{ if .Method.Description }} | ||||
|         <p class="text-muted mb-0">{{ .Method.Description }}</p> | ||||
|         {{ end }} | ||||
|       </div> | ||||
|       <div class="card-body"> | ||||
|         <!-- Parameters --> | ||||
|         <h6>Parameters</h6> | ||||
|         <table class="table table-sm schema-table"> | ||||
|           <thead> | ||||
|             <tr> | ||||
|               <th>Name</th> | ||||
|               <th>Type</th> | ||||
|               <th>Required</th> | ||||
|               <th>Description</th> | ||||
|             </tr> | ||||
|           </thead> | ||||
|           <tbody> | ||||
|             {{ if .Method.Params }} | ||||
|               {{ range .Method.Params }} | ||||
|               <tr> | ||||
|                 <td>{{ .Name }}</td> | ||||
|                 <td><code>{{ .Schema.Type }}</code></td> | ||||
|                 <td> | ||||
|                   {{ if .Required }} | ||||
|                   <span class="schema-required">Yes</span> | ||||
|                   {{ else }} | ||||
|                   <span class="schema-optional">No</span> | ||||
|                   {{ end }} | ||||
|                 </td> | ||||
|                 <td>{{ .Description }}</td> | ||||
|               </tr> | ||||
|               {{ end }} | ||||
|             {{ else }} | ||||
|               <tr> | ||||
|                 <td colspan="4">No parameters</td> | ||||
|               </tr> | ||||
|             {{ end }} | ||||
|           </tbody> | ||||
|         </table> | ||||
|  | ||||
|         <!-- Result --> | ||||
|         <h6 class="mt-4">Result</h6> | ||||
|         <table class="table table-sm schema-table"> | ||||
|           <thead> | ||||
|             <tr> | ||||
|               <th>Name</th> | ||||
|               <th>Type</th> | ||||
|               <th>Description</th> | ||||
|             </tr> | ||||
|           </thead> | ||||
|           <tbody> | ||||
|             <tr> | ||||
|               <td>{{ .Method.Result.Name }}</td> | ||||
|               <td><code>{{ .Method.Result.Schema.Type }}</code></td> | ||||
|               <td>{{ .Method.Result.Description }}</td> | ||||
|             </tr> | ||||
|           </tbody> | ||||
|         </table> | ||||
|  | ||||
|         <!-- Try It --> | ||||
|         <h6 class="mt-4">Try It</h6> | ||||
|         <form id="rpcForm" class="mb-3"> | ||||
|           <input type="hidden" name="selectedMethod" value="{{ .SelectedMethod }}"> | ||||
|           <div class="mb-3"> | ||||
|             <label for="paramsEditor" class="form-label">Parameters:</label> | ||||
|             <textarea class="form-control code-editor" id="paramsEditor" rows="10">{{ .ExampleParams }}</textarea> | ||||
|           </div> | ||||
|           <button type="submit" class="btn btn-primary">Execute</button> | ||||
|         </form> | ||||
|  | ||||
|         <div id="resultContainer" class="result-container d-none"> | ||||
|           <h6>Result:</h6> | ||||
|           <pre id="resultOutput" class="bg-light p-2 rounded"></pre> | ||||
|         </div> | ||||
|  | ||||
|         <div id="errorContainer" class="result-container d-none"> | ||||
|           <h6>Error:</h6> | ||||
|           <pre id="errorOutput" class="bg-light p-2 rounded text-danger"></pre> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|     {{ else if .SelectedMethod }} | ||||
|     <div class="alert alert-warning"> | ||||
|       Method not found: {{ .SelectedMethod }} | ||||
|     </div> | ||||
|     {{ else }} | ||||
|     <div class="alert alert-info"> | ||||
|       Select a method from the list to view details. | ||||
|     </div> | ||||
|     {{ end }} | ||||
|   </div> | ||||
| </div> | ||||
| {{ end }} | ||||
| {{ end }} | ||||
|  | ||||
| {{ block scripts() }} | ||||
| <script src="/static/js/rpcui.js"></script> | ||||
| {{ end }} | ||||
		Reference in New Issue
	
	Block a user