Compare commits
2 Commits
2ee8a95a90
...
0b62ac9ecd
Author | SHA1 | Date | |
---|---|---|---|
0b62ac9ecd | |||
c9b14730ad |
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
|
// For now, using the mock process manager
|
||||||
processManagerService := models.NewMockProcessManager()
|
processManagerService := models.NewMockProcessManager()
|
||||||
jobManagerService := models.NewMockJobManager()
|
jobManagerService := models.NewMockJobManager()
|
||||||
|
openrpcManagerService := models.NewOpenRPCManager()
|
||||||
|
|
||||||
dashboardController := controllers.NewDashboardController()
|
dashboardController := controllers.NewDashboardController()
|
||||||
processController := controllers.NewProcessController(processManagerService)
|
processController := controllers.NewProcessController(processManagerService)
|
||||||
jobController := controllers.NewJobController(jobManagerService)
|
jobController := controllers.NewJobController(jobManagerService)
|
||||||
authController := controllers.NewAuthController()
|
authController := controllers.NewAuthController()
|
||||||
|
openrpcController := controllers.NewOpenRPCController(openrpcManagerService)
|
||||||
|
|
||||||
// --- Public Routes ---
|
// --- Public Routes ---
|
||||||
// Login and Logout
|
// Login and Logout
|
||||||
@ -43,6 +45,10 @@ func SetupRoutes(app *fiber.App) {
|
|||||||
app.Get("/jobs", jobController.ShowJobsPage)
|
app.Get("/jobs", jobController.ShowJobsPage)
|
||||||
app.Get("/jobs/:id", jobController.ShowJobDetails)
|
app.Get("/jobs/:id", jobController.ShowJobDetails)
|
||||||
|
|
||||||
|
// OpenRPC UI routes
|
||||||
|
app.Get("/rpcui", openrpcController.ShowOpenRPCUI)
|
||||||
|
app.Post("/api/rpcui/execute", openrpcController.ExecuteRPC)
|
||||||
|
|
||||||
// Debug routes
|
// Debug routes
|
||||||
app.Get("/debug", func(c *fiber.Ctx) error {
|
app.Get("/debug", func(c *fiber.Ctx) error {
|
||||||
// Get all data from the jobs page to debug
|
// 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
|
Job Manager
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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 -->
|
<!-- Add more menu items here as needed -->
|
||||||
</ul>
|
</ul>
|
||||||
{{ end }}
|
{{ end }}
|
157
pkg/servers/ui/views/pages/rpcui.jet
Normal file
157
pkg/servers/ui/views/pages/rpcui.jet
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
{{extends "../layouts/base"}}
|
||||||
|
|
||||||
|
{{block title()}}
|
||||||
|
OpenRPC 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 spec := SpecList }}
|
||||||
|
<option value="{{ spec }}" >
|
||||||
|
{{ 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 m := Methods }}
|
||||||
|
<a href="/rpcui?spec={{ SelectedSpec }}&method={{ m }}&socketPath={{ SocketPath }}"
|
||||||
|
class="list-group-item list-group-item-action method-item {{ if eq(m, SelectedMethod) }}active{{ end }}">
|
||||||
|
{{ m }}
|
||||||
|
</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 p := Method.Params }}
|
||||||
|
<tr>
|
||||||
|
<td>{{ p.Name }}</td>
|
||||||
|
<td><code>{{ p.Schema.Type }}</code></td>
|
||||||
|
<td>{{ if p.Required }}<span class="schema-required">Yes</span>{{ else }}<span class="schema-optional">No</span>{{ end }}</td>
|
||||||
|
<td>{{ p.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}}
|
Loading…
Reference in New Issue
Block a user