...
This commit is contained in:
parent
2ee8a95a90
commit
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
|
||||
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 }}
|
Loading…
Reference in New Issue
Block a user