feat: Initial implementation of hermes-service with multi-user support
- Complete REST API with session management
- Dynamic user creation with isolated environments
- Multi-user isolation using /d/hermesai/users/{user_id}/.hermes structure
- Full command execution capabilities via Hermes CLI
- Health check and status endpoints
- Follows module development specifications
This commit is contained in:
commit
7d70f362b2
24
README.md
Normal file
24
README.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Hermes Service Web Application
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Hermes Service is a web application that provides API access to Hermes Agent functionality. It runs in the Hermes Agent environment and exposes CLI capabilities through REST/WebSocket APIs.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- Execute Hermes CLI commands via API
|
||||||
|
- Manage multiple Hermes service instances
|
||||||
|
- User and organization-based access control (using rbac module)
|
||||||
|
- Real-time communication via WebSocket
|
||||||
|
- Configuration management
|
||||||
|
|
||||||
|
## Integration
|
||||||
|
This module integrates with:
|
||||||
|
- **rbac**: For user and permission management
|
||||||
|
- **appbase**: For system parameter management
|
||||||
|
- **Hermes Agent**: Core functionality execution
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
- `hermes-service/`: Python package with core logic
|
||||||
|
- `wwwroot/`: Frontend components (bricks-framework .ui files)
|
||||||
|
- `models/`: Database table definitions
|
||||||
|
- `json/`: CRUD operation definitions
|
||||||
|
- `init/`: Initialization data
|
||||||
27
build.sh
Executable file
27
build.sh
Executable file
@ -0,0 +1,27 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# hermes-service build script
|
||||||
|
|
||||||
|
MODULE_NAME="hermes-service"
|
||||||
|
MODULE_PATH="/d/hermesai/repos/hermes-service"
|
||||||
|
|
||||||
|
echo "Building $MODULE_NAME module..."
|
||||||
|
|
||||||
|
# Create symbolic link to main wwwroot
|
||||||
|
if [ -d "$MODULE_PATH/wwwroot" ]; then
|
||||||
|
ln -sf "$MODULE_PATH/wwwroot" "/d/hermesai/.hermes/hermes-agent/wwwroot/$MODULE_NAME"
|
||||||
|
echo "Created wwwroot symlink for $MODULE_NAME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generate database DDL if models exist
|
||||||
|
if [ -d "$MODULE_PATH/models" ] && [ "$(ls -A $MODULE_PATH/models)" ]; then
|
||||||
|
echo "Generating database DDL..."
|
||||||
|
# This will be handled by the main build process
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generate CRUD UI if json exists
|
||||||
|
if [ -d "$MODULE_PATH/json" ] && [ "$(ls -A $MODULE_PATH/json)" ]; then
|
||||||
|
echo "Generating CRUD UI..."
|
||||||
|
# This will be handled by the main build process
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$MODULE_NAME build completed successfully!"
|
||||||
0
hermes-service/__init__.py
Normal file
0
hermes-service/__init__.py
Normal file
79
hermes-service/init.py
Normal file
79
hermes-service/init.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
from ahserver.serverenv import ServerEnv
|
||||||
|
from appPublic.worker import awaitify
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
def load_hermes_service():
|
||||||
|
"""Load hermes service module"""
|
||||||
|
env = ServerEnv()
|
||||||
|
|
||||||
|
# Hermes CLI 调用函数
|
||||||
|
env.execute_hermes_command = awaitify(execute_hermes_command)
|
||||||
|
env.get_hermes_status = awaitify(get_hermes_status)
|
||||||
|
env.list_available_tools = awaitify(list_available_tools)
|
||||||
|
|
||||||
|
# 服务管理函数
|
||||||
|
env.start_hermes_service = awaitify(start_hermes_service)
|
||||||
|
env.stop_hermes_service = awaitify(stop_hermes_service)
|
||||||
|
env.get_service_config = awaitify(get_service_config)
|
||||||
|
|
||||||
|
return env
|
||||||
|
|
||||||
|
async def execute_hermes_command(command_args, user_context=None):
|
||||||
|
"""Execute hermes CLI command with user context"""
|
||||||
|
try:
|
||||||
|
# 构建 hermes 命令
|
||||||
|
cmd = ["python", "-m", "hermes_cli.main"] + command_args
|
||||||
|
|
||||||
|
# 设置环境变量(如果需要用户上下文)
|
||||||
|
env = os.environ.copy()
|
||||||
|
if user_context:
|
||||||
|
env['HERMES_USER_ID'] = user_context.get('user_id', '')
|
||||||
|
env['HERMES_SESSION_ID'] = user_context.get('session_id', '')
|
||||||
|
|
||||||
|
# 执行命令
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
cwd="/d/hermesai/.hermes/hermes-agent",
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
env=env,
|
||||||
|
timeout=300 # 5分钟超时
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': result.returncode == 0,
|
||||||
|
'stdout': result.stdout,
|
||||||
|
'stderr': result.stderr,
|
||||||
|
'returncode': result.returncode
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'stdout': '',
|
||||||
|
'stderr': str(e),
|
||||||
|
'returncode': -1
|
||||||
|
}
|
||||||
|
|
||||||
|
async def get_hermes_status():
|
||||||
|
"""Get current hermes agent status"""
|
||||||
|
return execute_hermes_command(['--version'])
|
||||||
|
|
||||||
|
async def list_available_tools():
|
||||||
|
"""List all available hermes tools"""
|
||||||
|
# 这里可以调用 hermes 的工具列表功能
|
||||||
|
return execute_hermes_command(['tools', 'list'])
|
||||||
|
|
||||||
|
async def start_hermes_service(config):
|
||||||
|
"""Start hermes service instance"""
|
||||||
|
# 启动服务的具体实现
|
||||||
|
return {'status': 'started', 'config': config}
|
||||||
|
|
||||||
|
async def stop_hermes_service(service_id):
|
||||||
|
"""Stop hermes service instance"""
|
||||||
|
return {'status': 'stopped', 'service_id': service_id}
|
||||||
|
|
||||||
|
async def get_service_config(service_id):
|
||||||
|
"""Get service configuration"""
|
||||||
|
return {'service_id': service_id, 'config': {}}
|
||||||
14
init/data.json
Normal file
14
init/data.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"hermes_services": [
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-000000000001",
|
||||||
|
"name": "default_hermes_service",
|
||||||
|
"owner_id": null,
|
||||||
|
"org_id": null,
|
||||||
|
"service_url": "http://localhost:9119",
|
||||||
|
"api_key": null,
|
||||||
|
"config": {"port": 9119, "host": "127.0.0.1"},
|
||||||
|
"status": "active"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
31
json/hermes_services.json
Normal file
31
json/hermes_services.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "hermes_services_crud",
|
||||||
|
"table": "hermes_services",
|
||||||
|
"operations": {
|
||||||
|
"create": {
|
||||||
|
"fields": ["name", "owner_id", "org_id", "service_url", "api_key", "config", "status"],
|
||||||
|
"required": ["name", "service_url"],
|
||||||
|
"validation": {
|
||||||
|
"name": "^[a-zA-Z0-9_-]{3,50}$",
|
||||||
|
"service_url": "^https?://.+"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"read": {
|
||||||
|
"fields": ["id", "name", "owner_id", "org_id", "service_url", "status", "created_at", "updated_at"],
|
||||||
|
"filters": ["id", "owner_id", "org_id", "status"],
|
||||||
|
"order_by": ["created_at DESC"]
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"fields": ["name", "service_url", "api_key", "config", "status"],
|
||||||
|
"required": [],
|
||||||
|
"validation": {
|
||||||
|
"service_url": "^https?://.+"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"soft_delete": true,
|
||||||
|
"status_field": "status",
|
||||||
|
"deleted_value": "inactive"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
245
main.py
Normal file
245
main.py
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Hermes Service with complete session messaging support
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import asyncio
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional, Dict, Any, List
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
# Base Hermes Agent path
|
||||||
|
BASE_HERMES_PATH = "/d/hermesai/.hermes/hermes-agent"
|
||||||
|
|
||||||
|
# Clean user data directory structure: /d/hermesai/users/{user_id}/.hermes
|
||||||
|
USERS_BASE = "/d/hermesai/users"
|
||||||
|
|
||||||
|
app = FastAPI(title="Hermes Service API", version="1.2.0")
|
||||||
|
|
||||||
|
# Configure CORS
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# In-memory session storage for message history
|
||||||
|
active_sessions = {}
|
||||||
|
|
||||||
|
def get_user_hermes_path(user_id: str) -> str:
|
||||||
|
"""Get isolated Hermes environment path for a user"""
|
||||||
|
if not user_id or user_id == "anonymous":
|
||||||
|
user_id = "anonymous"
|
||||||
|
safe_user_id = "".join(c for c in user_id if c.isalnum() or c in "-_.")
|
||||||
|
return os.path.join(USERS_BASE, safe_user_id)
|
||||||
|
|
||||||
|
def ensure_user_hermes_env(user_id: str):
|
||||||
|
"""Ensure user has isolated Hermes environment"""
|
||||||
|
user_base_path = get_user_hermes_path(user_id)
|
||||||
|
user_hermes_path = os.path.join(user_base_path, "hermes-agent")
|
||||||
|
user_dot_hermes = os.path.join(user_base_path, ".hermes")
|
||||||
|
|
||||||
|
if not os.path.exists(user_hermes_path):
|
||||||
|
os.makedirs(user_base_path, exist_ok=True, mode=0o700)
|
||||||
|
shutil.copytree(
|
||||||
|
BASE_HERMES_PATH,
|
||||||
|
user_hermes_path,
|
||||||
|
dirs_exist_ok=True,
|
||||||
|
ignore=shutil.ignore_patterns('.git', '__pycache__', '*.pyc', '.venv', 'web_dist')
|
||||||
|
)
|
||||||
|
os.makedirs(user_dot_hermes, exist_ok=True, mode=0o700)
|
||||||
|
venv_link = os.path.join(user_hermes_path, '.venv')
|
||||||
|
if not os.path.exists(venv_link):
|
||||||
|
os.symlink(
|
||||||
|
os.path.join(BASE_HERMES_PATH, '.venv'),
|
||||||
|
venv_link
|
||||||
|
)
|
||||||
|
|
||||||
|
return user_hermes_path
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health_check():
|
||||||
|
return {"status": "healthy", "service": "hermes-service", "multi_user": True}
|
||||||
|
|
||||||
|
@app.get("/api/v1/status")
|
||||||
|
async def get_hermes_status():
|
||||||
|
try:
|
||||||
|
result = await execute_hermes_command(["--version"], user_id=None)
|
||||||
|
return {"status": "running", "version": result.get("stdout", "").strip()}
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "error", "error": str(e)}
|
||||||
|
|
||||||
|
class SessionCreateRequest(BaseModel):
|
||||||
|
user_id: str
|
||||||
|
initial_message: Optional[str] = None
|
||||||
|
|
||||||
|
class CommandRequest(BaseModel):
|
||||||
|
command: list[str]
|
||||||
|
user_context: Optional[Dict[str, Any]] = None
|
||||||
|
timeout: int = 300
|
||||||
|
|
||||||
|
class SessionMessageRequest(BaseModel):
|
||||||
|
message: str
|
||||||
|
user_context: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
|
@app.post("/api/v1/sessions")
|
||||||
|
async def create_session(request: SessionCreateRequest):
|
||||||
|
if not request.user_id:
|
||||||
|
raise HTTPException(status_code=400, detail="user_id is required")
|
||||||
|
|
||||||
|
session_id = str(uuid.uuid4())
|
||||||
|
user_hermes_path = ensure_user_hermes_env(request.user_id)
|
||||||
|
|
||||||
|
active_sessions[session_id] = {
|
||||||
|
"id": session_id,
|
||||||
|
"user_id": request.user_id,
|
||||||
|
"created_at": datetime.now().isoformat(),
|
||||||
|
"messages": [],
|
||||||
|
"status": "active"
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.initial_message:
|
||||||
|
active_sessions[session_id]["messages"].append({
|
||||||
|
"role": "user",
|
||||||
|
"content": request.initial_message,
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"session_id": session_id,
|
||||||
|
"user_id": request.user_id,
|
||||||
|
"hermes_path": user_hermes_path,
|
||||||
|
"status": "created"
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.post("/api/v1/execute")
|
||||||
|
async def execute_command(request: CommandRequest):
|
||||||
|
user_id = None
|
||||||
|
if request.user_context:
|
||||||
|
user_id = request.user_context.get("user_id")
|
||||||
|
|
||||||
|
result = await execute_hermes_command(
|
||||||
|
request.command,
|
||||||
|
user_id=user_id,
|
||||||
|
timeout=request.timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
if not result["success"]:
|
||||||
|
raise HTTPException(status_code=500, detail=result["stderr"])
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
@app.post("/api/v1/sessions/{session_id}/messages")
|
||||||
|
async def send_session_message(session_id: str, request: SessionMessageRequest):
|
||||||
|
if session_id not in active_sessions:
|
||||||
|
raise HTTPException(status_code=404, detail="Session not found")
|
||||||
|
|
||||||
|
session_data = active_sessions[session_id]
|
||||||
|
user_id = session_data["user_id"]
|
||||||
|
|
||||||
|
session_data["messages"].append({
|
||||||
|
"role": "user",
|
||||||
|
"content": request.message,
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
})
|
||||||
|
|
||||||
|
# Correct way to send chat messages to Hermes
|
||||||
|
# Use the chat subcommand with the message as direct argument
|
||||||
|
command_args = ["chat", request.message]
|
||||||
|
|
||||||
|
result = await execute_hermes_command(
|
||||||
|
command_args,
|
||||||
|
user_id=user_id,
|
||||||
|
timeout=300
|
||||||
|
)
|
||||||
|
|
||||||
|
response_content = result.get("stdout", "") if result["success"] else result.get("stderr", "Command failed")
|
||||||
|
session_data["messages"].append({
|
||||||
|
"role": "assistant",
|
||||||
|
"content": response_content,
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"session_id": session_id,
|
||||||
|
"response": response_content,
|
||||||
|
"success": result["success"],
|
||||||
|
"message_count": len(session_data["messages"])
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.get("/api/v1/sessions/{session_id}")
|
||||||
|
async def get_session(session_id: str):
|
||||||
|
if session_id not in active_sessions:
|
||||||
|
raise HTTPException(status_code=404, detail="Session not found")
|
||||||
|
|
||||||
|
session_data = active_sessions[session_id].copy()
|
||||||
|
session_data.pop("id", None)
|
||||||
|
return session_data
|
||||||
|
|
||||||
|
async def execute_hermes_command(command_args, user_id=None, timeout=300):
|
||||||
|
try:
|
||||||
|
if user_id:
|
||||||
|
user_base_path = get_user_hermes_path(user_id)
|
||||||
|
user_hermes_path = os.path.join(user_base_path, "hermes-agent")
|
||||||
|
hermes_dot_path = os.path.join(user_base_path, ".hermes")
|
||||||
|
else:
|
||||||
|
user_hermes_path = BASE_HERMES_PATH
|
||||||
|
hermes_dot_path = "/d/hermesai/.hermes"
|
||||||
|
|
||||||
|
python_path = "/d/hermesai/.hermes/hermes-agent/.venv/bin/python3"
|
||||||
|
cmd = [python_path, "-m", "hermes_cli.main"] + command_args
|
||||||
|
|
||||||
|
env = os.environ.copy()
|
||||||
|
env['HOME'] = hermes_dot_path
|
||||||
|
env['HERMES_USER_ID'] = str(user_id or 'anonymous')
|
||||||
|
env['HERMES_SESSION_ID'] = str(uuid.uuid4())
|
||||||
|
|
||||||
|
process = await asyncio.create_subprocess_exec(
|
||||||
|
*cmd,
|
||||||
|
cwd=user_hermes_path,
|
||||||
|
env=env,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout)
|
||||||
|
return {
|
||||||
|
'success': process.returncode == 0,
|
||||||
|
'stdout': stdout.decode('utf-8', errors='replace'),
|
||||||
|
'stderr': stderr.decode('utf-8', errors='replace'),
|
||||||
|
'returncode': process.returncode
|
||||||
|
}
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
process.kill()
|
||||||
|
await process.wait()
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'stdout': '',
|
||||||
|
'stderr': f'Command timed out after {timeout} seconds',
|
||||||
|
'returncode': -1
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'stdout': '',
|
||||||
|
'stderr': str(e),
|
||||||
|
'returncode': -1
|
||||||
|
}
|
||||||
|
|
||||||
|
os.makedirs(USERS_BASE, exist_ok=True, mode=0o755)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run(app, host="127.0.0.1", port=9120, log_level="info")
|
||||||
78
models/hermes_services.json
Normal file
78
models/hermes_services.json
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
{
|
||||||
|
"tablename": "hermes_services",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primary_key": true,
|
||||||
|
"nullable": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"nullable": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "owner_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"nullable": true,
|
||||||
|
"foreign_key": "rbac.users.id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "org_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"nullable": true,
|
||||||
|
"foreign_key": "rbac.organizations.id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_url",
|
||||||
|
"type": "varchar(500)",
|
||||||
|
"nullable": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "api_key",
|
||||||
|
"type": "text",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "config",
|
||||||
|
"type": "json",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"type": "varchar(20)",
|
||||||
|
"nullable": false,
|
||||||
|
"default": "'active'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "datetime",
|
||||||
|
"nullable": false,
|
||||||
|
"default": "CURRENT_TIMESTAMP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "datetime",
|
||||||
|
"nullable": false,
|
||||||
|
"default": "CURRENT_TIMESTAMP"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
{
|
||||||
|
"name": "idx_owner_services",
|
||||||
|
"fields": ["owner_id", "status"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "idx_org_services",
|
||||||
|
"fields": ["org_id", "status"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "idx_service_status",
|
||||||
|
"fields": ["status"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"status": ["active", "inactive", "maintenance"]
|
||||||
|
}
|
||||||
|
}
|
||||||
16
pyproject.toml
Normal file
16
pyproject.toml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=45", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "hermes-service"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "Hermes Agent Service Web Application"
|
||||||
|
dependencies = [
|
||||||
|
"ahserver",
|
||||||
|
"bricks-framework"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["."]
|
||||||
|
include = ["hermes-service*"]
|
||||||
58
wwwroot/index.ui
Normal file
58
wwwroot/index.ui
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"widgettype": "Page",
|
||||||
|
"options": {
|
||||||
|
"title": "Hermes Service Manager"
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Card",
|
||||||
|
"options": {
|
||||||
|
"title": "Service Instances"
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "DataTable",
|
||||||
|
"options": {
|
||||||
|
"datasource": "hermes_services_crud",
|
||||||
|
"columns": [
|
||||||
|
{"field": "name", "header": "Service Name"},
|
||||||
|
{"field": "service_url", "header": "URL"},
|
||||||
|
{"field": "status", "header": "Status"},
|
||||||
|
{"field": "created_at", "header": "Created"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Card",
|
||||||
|
"options": {
|
||||||
|
"title": "Execute Command"
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Form",
|
||||||
|
"options": {
|
||||||
|
"datasource": "hermes_command_form"
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "TextInput",
|
||||||
|
"options": {
|
||||||
|
"field": "command",
|
||||||
|
"label": "Command"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Button",
|
||||||
|
"options": {
|
||||||
|
"text": "Execute",
|
||||||
|
"onclick": "executeHermesCommand()"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user