feat: Add compact table views with advanced filtering and batch operations
- Enhanced Flutter web app management in PowerShell scripts - Added Flutter web server to start-services.ps1 as 4th service - Updated stop-services.ps1 to stop Flutter web server - Improved service orchestration and startup sequence - Implemented server caching for improved resilience - Added ServerCacheService for browser localStorage caching - Server lists persist across service restarts - Automatic fallback to cached data when API unavailable - Action picker categories always visible regardless of server status - Redesigned Action Mappings view with compact table layout - Replaced card-based ListView with DataTable for higher density - Added real-time search across name, input, output, description - Implemented multi-filter support (status: enabled/disabled) - Added column sorting (name, input, output, status, executions) - Batch operations: select all/multiple, batch delete - Reduced row height from ~120px to 56px for better overview - Redesigned Servers Management view with compact table layout - Replaced card-based ListView with DataTable - Added search by alias, host, user - Multi-filter support (type: all/G-Core/GeViScope, status: all/enabled/disabled) - Column sorting (alias, host, user, type, status) - Batch operations: select all/multiple, batch delete - Color-coded type and status badges - Improved action picker dialog for GSC/G-Core actions - GSC and G-Core categories always visible - Server validation with clear error messages - Fixed duplicate checkbox issue in table headers - Debug logging for troubleshooting server parameter issues 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
# Start Geutebruck API Services
|
# Start Geutebruck API Services
|
||||||
# This script starts GeViServer, SDK Bridge, and Python API
|
# This script starts GeViServer, SDK Bridge, Python API, and Flutter Web App
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
@@ -15,6 +15,7 @@ $sdkBridgeExe = "$sdkBridgePath\GeViScopeBridge.exe"
|
|||||||
$apiPath = "C:\DEV\COPILOT\geutebruck-api\src\api"
|
$apiPath = "C:\DEV\COPILOT\geutebruck-api\src\api"
|
||||||
$venvPython = "C:\DEV\COPILOT\geutebruck-api\.venv\Scripts\python.exe"
|
$venvPython = "C:\DEV\COPILOT\geutebruck-api\.venv\Scripts\python.exe"
|
||||||
$uvicorn = "C:\DEV\COPILOT\geutebruck-api\.venv\Scripts\uvicorn.exe"
|
$uvicorn = "C:\DEV\COPILOT\geutebruck-api\.venv\Scripts\uvicorn.exe"
|
||||||
|
$flutterWebPath = "C:\DEV\COPILOT\geutebruck_app\build\web"
|
||||||
|
|
||||||
# Function to wait for port to be listening
|
# Function to wait for port to be listening
|
||||||
function Wait-ForPort {
|
function Wait-ForPort {
|
||||||
@@ -40,12 +41,13 @@ function Wait-ForPort {
|
|||||||
$geviServerRunning = Get-Process -Name "GeViServer" -ErrorAction SilentlyContinue
|
$geviServerRunning = Get-Process -Name "GeViServer" -ErrorAction SilentlyContinue
|
||||||
$sdkBridgeRunning = Get-Process -Name "GeViScopeBridge" -ErrorAction SilentlyContinue
|
$sdkBridgeRunning = Get-Process -Name "GeViScopeBridge" -ErrorAction SilentlyContinue
|
||||||
$uvicornRunning = Get-Process -Name "uvicorn" -ErrorAction SilentlyContinue
|
$uvicornRunning = Get-Process -Name "uvicorn" -ErrorAction SilentlyContinue
|
||||||
|
$flutterRunning = Get-NetTCPConnection -LocalPort 8081 -State Listen -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
# Start GeViServer
|
# Start GeViServer
|
||||||
if ($geviServerRunning) {
|
if ($geviServerRunning) {
|
||||||
Write-Host "[SKIP] GeViServer is already running (PID: $($geviServerRunning.Id))" -ForegroundColor Yellow
|
Write-Host "[SKIP] GeViServer is already running (PID: $($geviServerRunning.Id))" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "[1/3] Starting GeViServer..." -ForegroundColor Green
|
Write-Host "[1/4] Starting GeViServer..." -ForegroundColor Green
|
||||||
Start-Process -FilePath $geviServerExe -ArgumentList "console" -WorkingDirectory "C:\GEVISOFT" -WindowStyle Hidden
|
Start-Process -FilePath $geviServerExe -ArgumentList "console" -WorkingDirectory "C:\GEVISOFT" -WindowStyle Hidden
|
||||||
|
|
||||||
# Wait for GeViServer to start listening on port 7700
|
# Wait for GeViServer to start listening on port 7700
|
||||||
@@ -65,7 +67,7 @@ if ($geviServerRunning) {
|
|||||||
if ($sdkBridgeRunning) {
|
if ($sdkBridgeRunning) {
|
||||||
Write-Host "[SKIP] SDK Bridge is already running (PID: $($sdkBridgeRunning.Id))" -ForegroundColor Yellow
|
Write-Host "[SKIP] SDK Bridge is already running (PID: $($sdkBridgeRunning.Id))" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "[2/3] Starting SDK Bridge..." -ForegroundColor Green
|
Write-Host "[2/4] Starting SDK Bridge..." -ForegroundColor Green
|
||||||
Start-Process -FilePath $sdkBridgeExe -WorkingDirectory $sdkBridgePath -WindowStyle Hidden
|
Start-Process -FilePath $sdkBridgeExe -WorkingDirectory $sdkBridgePath -WindowStyle Hidden
|
||||||
|
|
||||||
# Wait for SDK Bridge to start listening on port 50051
|
# Wait for SDK Bridge to start listening on port 50051
|
||||||
@@ -85,7 +87,7 @@ if ($sdkBridgeRunning) {
|
|||||||
if ($uvicornRunning) {
|
if ($uvicornRunning) {
|
||||||
Write-Host "[SKIP] Python API is already running (PID: $($uvicornRunning.Id))" -ForegroundColor Yellow
|
Write-Host "[SKIP] Python API is already running (PID: $($uvicornRunning.Id))" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "[3/3] Starting Python API..." -ForegroundColor Green
|
Write-Host "[3/4] Starting Python API..." -ForegroundColor Green
|
||||||
Start-Process -FilePath $uvicorn `
|
Start-Process -FilePath $uvicorn `
|
||||||
-ArgumentList "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload" `
|
-ArgumentList "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload" `
|
||||||
-WorkingDirectory $apiPath `
|
-WorkingDirectory $apiPath `
|
||||||
@@ -104,6 +106,32 @@ if ($uvicornRunning) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Start Flutter Web Server
|
||||||
|
if ($flutterRunning) {
|
||||||
|
$flutterProcess = Get-NetTCPConnection -LocalPort 8081 -State Listen -ErrorAction SilentlyContinue |
|
||||||
|
Select-Object -First 1 -ExpandProperty OwningProcess
|
||||||
|
Write-Host "[SKIP] Flutter Web is already running (PID: $flutterProcess)" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host "[4/4] Starting Flutter Web Server..." -ForegroundColor Green
|
||||||
|
Start-Process -FilePath "python" `
|
||||||
|
-ArgumentList "-m", "http.server", "8081", "--bind", "0.0.0.0" `
|
||||||
|
-WorkingDirectory $flutterWebPath `
|
||||||
|
-WindowStyle Hidden
|
||||||
|
|
||||||
|
# Wait for Flutter Web to start listening on port 8081
|
||||||
|
Write-Host " Waiting for Flutter Web to initialize" -NoNewline -ForegroundColor Gray
|
||||||
|
if (Wait-ForPort -Port 8081 -TimeoutSeconds 10) {
|
||||||
|
Write-Host ""
|
||||||
|
$flutterProcess = Get-NetTCPConnection -LocalPort 8081 -State Listen -ErrorAction SilentlyContinue |
|
||||||
|
Select-Object -First 1 -ExpandProperty OwningProcess
|
||||||
|
Write-Host " [OK] Flutter Web started (PID: $flutterProcess)" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " [ERROR] Flutter Web failed to start listening on port 8081" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "========================================" -ForegroundColor Green
|
Write-Host "========================================" -ForegroundColor Green
|
||||||
Write-Host "Services Started Successfully!" -ForegroundColor Green
|
Write-Host "Services Started Successfully!" -ForegroundColor Green
|
||||||
@@ -113,6 +141,7 @@ Write-Host "GeViServer: Running on ports 7700-7703" -ForegroundColor Cyan
|
|||||||
Write-Host "SDK Bridge: Running on port 50051 (gRPC)" -ForegroundColor Cyan
|
Write-Host "SDK Bridge: Running on port 50051 (gRPC)" -ForegroundColor Cyan
|
||||||
Write-Host "Python API: http://localhost:8000" -ForegroundColor Cyan
|
Write-Host "Python API: http://localhost:8000" -ForegroundColor Cyan
|
||||||
Write-Host "Swagger UI: http://localhost:8000/docs" -ForegroundColor Cyan
|
Write-Host "Swagger UI: http://localhost:8000/docs" -ForegroundColor Cyan
|
||||||
|
Write-Host "Flutter Web: http://localhost:8081" -ForegroundColor Cyan
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "To check status, run: .\status-services.ps1" -ForegroundColor Yellow
|
Write-Host "To check status, run: .\status-services.ps1" -ForegroundColor Yellow
|
||||||
Write-Host "To stop services, run: .\stop-services.ps1" -ForegroundColor Yellow
|
Write-Host "To stop services, run: .\stop-services.ps1" -ForegroundColor Yellow
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Stop Geutebruck API Services
|
# Stop Geutebruck API Services
|
||||||
# This script stops Python API, SDK Bridge, and GeViServer
|
# This script stops Flutter Web, Python API, SDK Bridge, and GeViServer
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# .\stop-services.ps1 # Stop all services
|
# .\stop-services.ps1 # Stop all services
|
||||||
@@ -20,42 +20,53 @@ if ($KeepGeViServer) {
|
|||||||
Write-Host "========================================" -ForegroundColor Cyan
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
|
|
||||||
|
# Stop Flutter Web Server
|
||||||
|
$flutterPort = Get-NetTCPConnection -LocalPort 8081 -State Listen -ErrorAction SilentlyContinue
|
||||||
|
if ($flutterPort) {
|
||||||
|
$flutterPid = $flutterPort | Select-Object -First 1 -ExpandProperty OwningProcess
|
||||||
|
Write-Host "[1/4] Stopping Flutter Web (PID: $flutterPid)..." -ForegroundColor Yellow
|
||||||
|
Stop-Process -Id $flutterPid -Force
|
||||||
|
Write-Host " [OK] Flutter Web stopped" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "[1/4] Flutter Web is not running" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
|
||||||
# Stop Python API
|
# Stop Python API
|
||||||
$uvicorn = Get-Process -Name "uvicorn"
|
$uvicorn = Get-Process -Name "uvicorn"
|
||||||
if ($uvicorn) {
|
if ($uvicorn) {
|
||||||
Write-Host "[1/3] Stopping Python API (PID: $($uvicorn.Id))..." -ForegroundColor Yellow
|
Write-Host "[2/4] Stopping Python API (PID: $($uvicorn.Id))..." -ForegroundColor Yellow
|
||||||
Stop-Process -Name "uvicorn" -Force
|
Stop-Process -Name "uvicorn" -Force
|
||||||
Write-Host " [OK] Python API stopped" -ForegroundColor Green
|
Write-Host " [OK] Python API stopped" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "[1/3] Python API is not running" -ForegroundColor Gray
|
Write-Host "[2/4] Python API is not running" -ForegroundColor Gray
|
||||||
}
|
}
|
||||||
|
|
||||||
# Stop SDK Bridge
|
# Stop SDK Bridge
|
||||||
$sdkBridge = Get-Process -Name "GeViScopeBridge"
|
$sdkBridge = Get-Process -Name "GeViScopeBridge"
|
||||||
if ($sdkBridge) {
|
if ($sdkBridge) {
|
||||||
Write-Host "[2/3] Stopping SDK Bridge (PID: $($sdkBridge.Id))..." -ForegroundColor Yellow
|
Write-Host "[3/4] Stopping SDK Bridge (PID: $($sdkBridge.Id))..." -ForegroundColor Yellow
|
||||||
Stop-Process -Name "GeViScopeBridge" -Force
|
Stop-Process -Name "GeViScopeBridge" -Force
|
||||||
Write-Host " [OK] SDK Bridge stopped" -ForegroundColor Green
|
Write-Host " [OK] SDK Bridge stopped" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "[2/3] SDK Bridge is not running" -ForegroundColor Gray
|
Write-Host "[3/4] SDK Bridge is not running" -ForegroundColor Gray
|
||||||
}
|
}
|
||||||
|
|
||||||
# Stop GeViServer (unless -KeepGeViServer flag is set)
|
# Stop GeViServer (unless -KeepGeViServer flag is set)
|
||||||
if (-not $KeepGeViServer) {
|
if (-not $KeepGeViServer) {
|
||||||
$geviServer = Get-Process -Name "GeViServer"
|
$geviServer = Get-Process -Name "GeViServer"
|
||||||
if ($geviServer) {
|
if ($geviServer) {
|
||||||
Write-Host "[3/3] Stopping GeViServer (PID: $($geviServer.Id))..." -ForegroundColor Yellow
|
Write-Host "[4/4] Stopping GeViServer (PID: $($geviServer.Id))..." -ForegroundColor Yellow
|
||||||
Stop-Process -Name "GeViServer" -Force
|
Stop-Process -Name "GeViServer" -Force
|
||||||
Write-Host " [OK] GeViServer stopped" -ForegroundColor Green
|
Write-Host " [OK] GeViServer stopped" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "[3/3] GeViServer is not running" -ForegroundColor Gray
|
Write-Host "[4/4] GeViServer is not running" -ForegroundColor Gray
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$geviServer = Get-Process -Name "GeViServer"
|
$geviServer = Get-Process -Name "GeViServer"
|
||||||
if ($geviServer) {
|
if ($geviServer) {
|
||||||
Write-Host "[3/3] Keeping GeViServer running (PID: $($geviServer.Id))" -ForegroundColor Green
|
Write-Host "[4/4] Keeping GeViServer running (PID: $($geviServer.Id))" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "[3/3] GeViServer is not running (cannot keep)" -ForegroundColor Yellow
|
Write-Host "[4/4] GeViServer is not running (cannot keep)" -ForegroundColor Yellow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
95
geutebruck_app/lib/data/services/server_cache_service.dart
Normal file
95
geutebruck_app/lib/data/services/server_cache_service.dart
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import '../models/action_template.dart';
|
||||||
|
|
||||||
|
/// Service for caching server lists to prevent categories from disappearing
|
||||||
|
/// when SDK Bridge is temporarily unavailable during service restarts
|
||||||
|
class ServerCacheService {
|
||||||
|
static const String _gcoreServersKey = 'cached_gcore_servers';
|
||||||
|
static const String _gscServersKey = 'cached_gsc_servers';
|
||||||
|
static const String _cacheTimestampKey = 'server_cache_timestamp';
|
||||||
|
|
||||||
|
/// Save G-Core servers to cache
|
||||||
|
static Future<void> cacheGCoreServers(List<ServerInfo> servers) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final json = jsonEncode(servers.map((s) => s.toJson()).toList());
|
||||||
|
await prefs.setString(_gcoreServersKey, json);
|
||||||
|
await prefs.setInt(_cacheTimestampKey, DateTime.now().millisecondsSinceEpoch);
|
||||||
|
print('[ServerCache] Cached ${servers.length} G-Core servers');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save GSC servers to cache
|
||||||
|
static Future<void> cacheGSCServers(List<ServerInfo> servers) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final json = jsonEncode(servers.map((s) => s.toJson()).toList());
|
||||||
|
await prefs.setString(_gscServersKey, json);
|
||||||
|
await prefs.setInt(_cacheTimestampKey, DateTime.now().millisecondsSinceEpoch);
|
||||||
|
print('[ServerCache] Cached ${servers.length} GSC servers');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save both server lists
|
||||||
|
static Future<void> cacheServers({
|
||||||
|
required List<ServerInfo> gcoreServers,
|
||||||
|
required List<ServerInfo> gscServers,
|
||||||
|
}) async {
|
||||||
|
await cacheGCoreServers(gcoreServers);
|
||||||
|
await cacheGSCServers(gscServers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load cached G-Core servers
|
||||||
|
static Future<List<ServerInfo>> getCachedGCoreServers() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final json = prefs.getString(_gcoreServersKey);
|
||||||
|
if (json == null) {
|
||||||
|
print('[ServerCache] No cached G-Core servers found');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final list = jsonDecode(json) as List<dynamic>;
|
||||||
|
final servers = list.map((item) => ServerInfo.fromJson(item as Map<String, dynamic>)).toList();
|
||||||
|
print('[ServerCache] Loaded ${servers.length} cached G-Core servers');
|
||||||
|
return servers;
|
||||||
|
} catch (e) {
|
||||||
|
print('[ServerCache] Error loading cached G-Core servers: $e');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load cached GSC servers
|
||||||
|
static Future<List<ServerInfo>> getCachedGSCServers() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final json = prefs.getString(_gscServersKey);
|
||||||
|
if (json == null) {
|
||||||
|
print('[ServerCache] No cached GSC servers found');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final list = jsonDecode(json) as List<dynamic>;
|
||||||
|
final servers = list.map((item) => ServerInfo.fromJson(item as Map<String, dynamic>)).toList();
|
||||||
|
print('[ServerCache] Loaded ${servers.length} cached GSC servers');
|
||||||
|
return servers;
|
||||||
|
} catch (e) {
|
||||||
|
print('[ServerCache] Error loading cached GSC servers: $e');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get cache age in milliseconds
|
||||||
|
static Future<int?> getCacheAge() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final timestamp = prefs.getInt(_cacheTimestampKey);
|
||||||
|
if (timestamp == null) return null;
|
||||||
|
return DateTime.now().millisecondsSinceEpoch - timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear all cached server data
|
||||||
|
static Future<void> clearCache() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.remove(_gcoreServersKey);
|
||||||
|
await prefs.remove(_gscServersKey);
|
||||||
|
await prefs.remove(_cacheTimestampKey);
|
||||||
|
print('[ServerCache] Cache cleared');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import '../../../domain/entities/action_mapping.dart';
|
|||||||
import '../../../data/models/action_output.dart';
|
import '../../../data/models/action_output.dart';
|
||||||
import '../../../data/models/action_template.dart';
|
import '../../../data/models/action_template.dart';
|
||||||
import '../../../data/services/action_template_service.dart';
|
import '../../../data/services/action_template_service.dart';
|
||||||
|
import '../../../data/services/server_cache_service.dart';
|
||||||
import '../../../core/constants/api_constants.dart';
|
import '../../../core/constants/api_constants.dart';
|
||||||
import '../../../core/storage/token_manager.dart';
|
import '../../../core/storage/token_manager.dart';
|
||||||
import '../../blocs/action_mapping/action_mapping_bloc.dart';
|
import '../../blocs/action_mapping/action_mapping_bloc.dart';
|
||||||
@@ -73,6 +74,13 @@ class _ActionMappingFormScreenState extends State<ActionMappingFormScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadActionTemplates() async {
|
Future<void> _loadActionTemplates() async {
|
||||||
|
print('[ActionTemplates] Starting to load action templates...');
|
||||||
|
|
||||||
|
// Load cached servers first as fallback
|
||||||
|
final cachedGCoreServers = await ServerCacheService.getCachedGCoreServers();
|
||||||
|
final cachedGSCServers = await ServerCacheService.getCachedGSCServers();
|
||||||
|
print('[ActionTemplates] Cached servers - G-Core: ${cachedGCoreServers.length}, GSC: ${cachedGSCServers.length}');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get auth token from TokenManager
|
// Get auth token from TokenManager
|
||||||
final token = TokenManager().accessToken;
|
final token = TokenManager().accessToken;
|
||||||
@@ -85,22 +93,58 @@ class _ActionMappingFormScreenState extends State<ActionMappingFormScreen> {
|
|||||||
final categoriesResponse = await service.getActionCategories();
|
final categoriesResponse = await service.getActionCategories();
|
||||||
final templates = await service.getActionTemplates();
|
final templates = await service.getActionTemplates();
|
||||||
|
|
||||||
|
// Use API data if available, otherwise use cached data
|
||||||
|
final gcoreServers = categoriesResponse.servers.gcoreServers.isNotEmpty
|
||||||
|
? categoriesResponse.servers.gcoreServers
|
||||||
|
: cachedGCoreServers;
|
||||||
|
|
||||||
|
final gscServers = categoriesResponse.servers.gscServers.isNotEmpty
|
||||||
|
? categoriesResponse.servers.gscServers
|
||||||
|
: cachedGSCServers;
|
||||||
|
|
||||||
|
print('[ActionTemplates] API returned - G-Core: ${categoriesResponse.servers.gcoreServers.length}, GSC: ${categoriesResponse.servers.gscServers.length}');
|
||||||
|
print('[ActionTemplates] Using - G-Core: ${gcoreServers.length}, GSC: ${gscServers.length}');
|
||||||
|
|
||||||
|
// Cache the server lists if we got them from API
|
||||||
|
if (categoriesResponse.servers.gcoreServers.isNotEmpty || categoriesResponse.servers.gscServers.isNotEmpty) {
|
||||||
|
await ServerCacheService.cacheServers(
|
||||||
|
gcoreServers: categoriesResponse.servers.gcoreServers,
|
||||||
|
gscServers: categoriesResponse.servers.gscServers,
|
||||||
|
);
|
||||||
|
print('[ActionTemplates] Cached server lists to local storage');
|
||||||
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_categories = categoriesResponse.categories;
|
_categories = categoriesResponse.categories;
|
||||||
_templates = templates;
|
_templates = templates;
|
||||||
_gcoreServers = categoriesResponse.servers.gcoreServers;
|
_gcoreServers = gcoreServers;
|
||||||
_gscServers = categoriesResponse.servers.gscServers;
|
_gscServers = gscServers;
|
||||||
_isLoadingTemplates = false;
|
_isLoadingTemplates = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
print('[ActionTemplates] Successfully loaded templates and ${gcoreServers.length + gscServers.length} servers');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
print('[ActionTemplates] Error loading from API: $e');
|
||||||
|
|
||||||
|
// Fall back to cached data
|
||||||
setState(() {
|
setState(() {
|
||||||
|
_gcoreServers = cachedGCoreServers;
|
||||||
|
_gscServers = cachedGSCServers;
|
||||||
_isLoadingTemplates = false;
|
_isLoadingTemplates = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
print('[ActionTemplates] Fell back to cached servers - G-Core: ${cachedGCoreServers.length}, GSC: ${cachedGSCServers.length}');
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
final hasCache = cachedGCoreServers.isNotEmpty || cachedGSCServers.isNotEmpty;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('Failed to load action templates: $e'),
|
content: Text(
|
||||||
backgroundColor: Colors.orange,
|
hasCache
|
||||||
|
? 'Using cached server data (API unavailable)'
|
||||||
|
: 'Failed to load action templates: $e',
|
||||||
|
),
|
||||||
|
backgroundColor: hasCache ? Colors.orange : Colors.red,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,17 @@ class ActionMappingsListScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ActionMappingsListScreenState extends State<ActionMappingsListScreen> {
|
class _ActionMappingsListScreenState extends State<ActionMappingsListScreen> {
|
||||||
bool _showSearch = false;
|
|
||||||
final TextEditingController _searchController = TextEditingController();
|
final TextEditingController _searchController = TextEditingController();
|
||||||
|
final Set<String> _selectedMappings = {};
|
||||||
|
bool _selectAll = false;
|
||||||
|
|
||||||
|
// Filter states
|
||||||
|
bool? _filterEnabled; // null = all, true = enabled only, false = disabled only
|
||||||
|
String _searchQuery = '';
|
||||||
|
|
||||||
|
// Sort states
|
||||||
|
int? _sortColumnIndex;
|
||||||
|
bool _sortAscending = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@@ -27,30 +36,106 @@ class _ActionMappingsListScreenState extends State<ActionMappingsListScreen> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<ActionMapping> _getFilteredMappings(List<ActionMapping> allMappings) {
|
||||||
|
var filtered = allMappings;
|
||||||
|
|
||||||
|
// Apply enabled/disabled filter
|
||||||
|
if (_filterEnabled != null) {
|
||||||
|
filtered = filtered.where((m) => m.enabled == _filterEnabled).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply search filter
|
||||||
|
if (_searchQuery.isNotEmpty) {
|
||||||
|
final query = _searchQuery.toLowerCase();
|
||||||
|
filtered = filtered.where((m) {
|
||||||
|
return m.name.toLowerCase().contains(query) ||
|
||||||
|
m.inputAction.toLowerCase().contains(query) ||
|
||||||
|
m.outputActions.any((o) => o.action.toLowerCase().contains(query)) ||
|
||||||
|
(m.description?.toLowerCase().contains(query) ?? false);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply sorting
|
||||||
|
if (_sortColumnIndex != null) {
|
||||||
|
filtered.sort((a, b) {
|
||||||
|
int comparison = 0;
|
||||||
|
switch (_sortColumnIndex) {
|
||||||
|
case 0: // Name
|
||||||
|
comparison = a.name.toLowerCase().compareTo(b.name.toLowerCase());
|
||||||
|
break;
|
||||||
|
case 1: // Input Action
|
||||||
|
comparison = a.inputAction.toLowerCase().compareTo(b.inputAction.toLowerCase());
|
||||||
|
break;
|
||||||
|
case 2: // Output Actions
|
||||||
|
final aOutputs = a.outputActions.map((o) => o.action).join(', ').toLowerCase();
|
||||||
|
final bOutputs = b.outputActions.map((o) => o.action).join(', ').toLowerCase();
|
||||||
|
comparison = aOutputs.compareTo(bOutputs);
|
||||||
|
break;
|
||||||
|
case 3: // Status
|
||||||
|
comparison = a.enabled == b.enabled ? 0 : (a.enabled ? -1 : 1);
|
||||||
|
break;
|
||||||
|
case 4: // Executions
|
||||||
|
comparison = a.executionCount.compareTo(b.executionCount);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return _sortAscending ? comparison : -comparison;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _toggleSelectAll(List<ActionMapping> mappings) {
|
||||||
|
setState(() {
|
||||||
|
if (_selectAll) {
|
||||||
|
_selectedMappings.clear();
|
||||||
|
} else {
|
||||||
|
_selectedMappings.addAll(mappings.map((m) => m.id));
|
||||||
|
}
|
||||||
|
_selectAll = !_selectAll;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showBatchDeleteDialog(BuildContext context) {
|
||||||
|
if (_selectedMappings.isEmpty) return;
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogContext) => AlertDialog(
|
||||||
|
title: Text('Delete ${_selectedMappings.length} Action Mappings'),
|
||||||
|
content: Text(
|
||||||
|
'Are you sure you want to delete ${_selectedMappings.length} selected action mapping${_selectedMappings.length != 1 ? 's' : ''}?'
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
for (final id in _selectedMappings) {
|
||||||
|
context.read<ActionMappingBloc>().add(DeleteActionMappingEvent(id));
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_selectedMappings.clear();
|
||||||
|
_selectAll = false;
|
||||||
|
});
|
||||||
|
Navigator.of(dialogContext).pop();
|
||||||
|
},
|
||||||
|
child: const Text('Delete All', style: TextStyle(color: Colors.red)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
drawer: const AppDrawer(currentRoute: '/action-mappings'),
|
drawer: const AppDrawer(currentRoute: '/action-mappings'),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: _showSearch
|
title: Row(
|
||||||
? TextField(
|
|
||||||
controller: _searchController,
|
|
||||||
autofocus: true,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
hintText: 'Search action mappings...',
|
|
||||||
border: InputBorder.none,
|
|
||||||
hintStyle: TextStyle(color: Colors.white70),
|
|
||||||
),
|
|
||||||
style: const TextStyle(color: Colors.white),
|
|
||||||
onChanged: (query) {
|
|
||||||
if (query.isEmpty) {
|
|
||||||
context.read<ActionMappingBloc>().add(const LoadActionMappings());
|
|
||||||
} else {
|
|
||||||
context.read<ActionMappingBloc>().add(SearchActionMappings(query));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: Row(
|
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.link, size: 24),
|
const Icon(Icons.link, size: 24),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
@@ -58,20 +143,6 @@ class _ActionMappingsListScreenState extends State<ActionMappingsListScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
// Search toggle button
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(_showSearch ? Icons.close : Icons.search),
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
_showSearch = !_showSearch;
|
|
||||||
if (!_showSearch) {
|
|
||||||
_searchController.clear();
|
|
||||||
context.read<ActionMappingBloc>().add(const LoadActionMappings());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
tooltip: _showSearch ? 'Close search' : 'Search',
|
|
||||||
),
|
|
||||||
// Sync button with dirty count badge
|
// Sync button with dirty count badge
|
||||||
BlocBuilder<ActionMappingBloc, ActionMappingState>(
|
BlocBuilder<ActionMappingBloc, ActionMappingState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
@@ -158,24 +229,136 @@ class _ActionMappingsListScreenState extends State<ActionMappingsListScreen> {
|
|||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
// Add Action Mapping button
|
// Toolbar with search, filters, and batch actions
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Row(
|
color: Colors.grey[100],
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
// Search and Add button row
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: _searchController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Search by name, input, output, or description...',
|
||||||
|
prefixIcon: const Icon(Icons.search),
|
||||||
|
suffixIcon: _searchQuery.isNotEmpty
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_searchController.clear();
|
||||||
|
_searchQuery = '';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.white,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_searchQuery = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.push('/action-mappings/create');
|
context.push('/action-mappings/create');
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
label: const Text('Add Action Mapping'),
|
label: const Text('Add Mapping'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Filter chips and batch actions
|
||||||
|
BlocBuilder<ActionMappingBloc, ActionMappingState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
final allMappings = state is ActionMappingLoaded ? state.mappings : <ActionMapping>[];
|
||||||
|
final filteredMappings = _getFilteredMappings(allMappings);
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
// Filter chips
|
||||||
|
const Text('Filters: ', style: TextStyle(fontWeight: FontWeight.w500)),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
ChoiceChip(
|
||||||
|
label: const Text('All'),
|
||||||
|
selected: _filterEnabled == null,
|
||||||
|
onSelected: (selected) {
|
||||||
|
setState(() {
|
||||||
|
_filterEnabled = null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
ChoiceChip(
|
||||||
|
label: const Text('Enabled'),
|
||||||
|
selected: _filterEnabled == true,
|
||||||
|
onSelected: (selected) {
|
||||||
|
setState(() {
|
||||||
|
_filterEnabled = selected ? true : null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
ChoiceChip(
|
||||||
|
label: const Text('Disabled'),
|
||||||
|
selected: _filterEnabled == false,
|
||||||
|
onSelected: (selected) {
|
||||||
|
setState(() {
|
||||||
|
_filterEnabled = selected ? false : null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
|
||||||
|
// Batch actions
|
||||||
|
if (_selectedMappings.isNotEmpty) ...[
|
||||||
|
Text(
|
||||||
|
'${_selectedMappings.length} selected',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_selectedMappings.clear();
|
||||||
|
_selectAll = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const Text('Clear'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: () => _showBatchDeleteDialog(context),
|
||||||
|
icon: const Icon(Icons.delete, size: 18),
|
||||||
|
label: const Text('Delete Selected'),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Action Mapping list
|
// Table
|
||||||
Expanded(
|
Expanded(
|
||||||
child: BlocConsumer<ActionMappingBloc, ActionMappingState>(
|
child: BlocConsumer<ActionMappingBloc, ActionMappingState>(
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
@@ -231,9 +414,9 @@ class _ActionMappingsListScreenState extends State<ActionMappingsListScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (state is ActionMappingLoaded) {
|
} else if (state is ActionMappingLoaded) {
|
||||||
final mappings = state.mappings;
|
final filteredMappings = _getFilteredMappings(state.mappings);
|
||||||
|
|
||||||
if (mappings.isEmpty) {
|
if (filteredMappings.isEmpty) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -241,14 +424,18 @@ class _ActionMappingsListScreenState extends State<ActionMappingsListScreen> {
|
|||||||
Icon(Icons.link_off, size: 64, color: Colors.grey[400]),
|
Icon(Icons.link_off, size: 64, color: Colors.grey[400]),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'No action mappings found',
|
_searchQuery.isNotEmpty || _filterEnabled != null
|
||||||
|
? 'No matching action mappings'
|
||||||
|
: 'No action mappings found',
|
||||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
color: Colors.grey[600],
|
color: Colors.grey[600],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'Add an action mapping to get started',
|
_searchQuery.isNotEmpty || _filterEnabled != null
|
||||||
|
? 'Try adjusting your search or filters'
|
||||||
|
: 'Add an action mapping to get started',
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
color: Colors.grey[500],
|
color: Colors.grey[500],
|
||||||
),
|
),
|
||||||
@@ -258,14 +445,7 @@ class _ActionMappingsListScreenState extends State<ActionMappingsListScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ListView.builder(
|
return _buildCompactTable(context, filteredMappings);
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
itemCount: mappings.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final mapping = mappings[index];
|
|
||||||
return _buildActionMappingCard(context, mapping);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else if (state is ActionMappingError) {
|
} else if (state is ActionMappingError) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -298,7 +478,6 @@ class _ActionMappingsListScreenState extends State<ActionMappingsListScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle ActionMappingInitial or any other unknown states with a loading indicator
|
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -308,243 +487,195 @@ class _ActionMappingsListScreenState extends State<ActionMappingsListScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildActionMappingCard(BuildContext context, ActionMapping mapping) {
|
Widget _buildCompactTable(BuildContext context, List<ActionMapping> mappings) {
|
||||||
final hasParameters = mapping.inputParameters.isNotEmpty ||
|
return SingleChildScrollView(
|
||||||
mapping.outputActions.any((o) => o.parameters.isNotEmpty);
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: DataTable(
|
||||||
|
columnSpacing: 24,
|
||||||
|
horizontalMargin: 12,
|
||||||
|
headingRowHeight: 48,
|
||||||
|
dataRowHeight: 56,
|
||||||
|
showCheckboxColumn: true,
|
||||||
|
sortColumnIndex: _sortColumnIndex,
|
||||||
|
sortAscending: _sortAscending,
|
||||||
|
columns: [
|
||||||
|
DataColumn(
|
||||||
|
label: const Text('Name', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
onSort: (columnIndex, ascending) {
|
||||||
|
setState(() {
|
||||||
|
_sortColumnIndex = columnIndex;
|
||||||
|
_sortAscending = ascending;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
DataColumn(
|
||||||
|
label: const Text('Input Action', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
onSort: (columnIndex, ascending) {
|
||||||
|
setState(() {
|
||||||
|
_sortColumnIndex = columnIndex;
|
||||||
|
_sortAscending = ascending;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
DataColumn(
|
||||||
|
label: const Text('Output Actions', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
onSort: (columnIndex, ascending) {
|
||||||
|
setState(() {
|
||||||
|
_sortColumnIndex = columnIndex;
|
||||||
|
_sortAscending = ascending;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
DataColumn(
|
||||||
|
label: const Text('Status', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
onSort: (columnIndex, ascending) {
|
||||||
|
setState(() {
|
||||||
|
_sortColumnIndex = columnIndex;
|
||||||
|
_sortAscending = ascending;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
DataColumn(
|
||||||
|
label: const Text('Executions', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
numeric: true,
|
||||||
|
onSort: (columnIndex, ascending) {
|
||||||
|
setState(() {
|
||||||
|
_sortColumnIndex = columnIndex;
|
||||||
|
_sortAscending = ascending;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const DataColumn(
|
||||||
|
label: Text('Actions', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
rows: mappings.map((mapping) {
|
||||||
|
final isSelected = _selectedMappings.contains(mapping.id);
|
||||||
|
|
||||||
return Card(
|
return DataRow(
|
||||||
margin: const EdgeInsets.only(bottom: 12.0),
|
selected: isSelected,
|
||||||
child: ExpansionTile(
|
onSelectChanged: (selected) {
|
||||||
leading: CircleAvatar(
|
setState(() {
|
||||||
backgroundColor: mapping.enabled
|
if (selected == true) {
|
||||||
? Colors.green.withOpacity(0.2)
|
_selectedMappings.add(mapping.id);
|
||||||
: Colors.grey.withOpacity(0.2),
|
} else {
|
||||||
child: Icon(
|
_selectedMappings.remove(mapping.id);
|
||||||
Icons.link,
|
}
|
||||||
color: mapping.enabled ? Colors.green : Colors.grey,
|
_selectAll = _selectedMappings.length == mappings.length;
|
||||||
),
|
});
|
||||||
),
|
},
|
||||||
title: Row(
|
cells: [
|
||||||
|
// Name
|
||||||
|
DataCell(
|
||||||
|
Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Text(
|
||||||
child: Text(
|
|
||||||
mapping.name,
|
mapping.name,
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
if (mapping.description != null && mapping.description!.isNotEmpty)
|
||||||
|
Text(
|
||||||
|
mapping.description!,
|
||||||
|
style: TextStyle(fontSize: 11, color: Colors.grey[600]),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
|
||||||
|
// Input Action
|
||||||
|
DataCell(
|
||||||
|
Text(
|
||||||
|
mapping.inputAction,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Output Actions
|
||||||
|
DataCell(
|
||||||
|
Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
mapping.outputActions.map((o) => o.action).join(', '),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Status
|
||||||
|
DataCell(
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
color: mapping.enabled ? Colors.green.withOpacity(0.1) : Colors.grey.withOpacity(0.1),
|
||||||
|
border: Border.all(
|
||||||
color: mapping.enabled ? Colors.green : Colors.grey,
|
color: mapping.enabled ? Colors.green : Colors.grey,
|
||||||
|
),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
mapping.enabled ? 'Enabled' : 'Disabled',
|
mapping.enabled ? 'Enabled' : 'Disabled',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: mapping.enabled ? Colors.green[700] : Colors.grey[700],
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
subtitle: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
if (mapping.description != null && mapping.description!.isNotEmpty)
|
|
||||||
Text(
|
|
||||||
mapping.description!,
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const Text('Input: ', style: TextStyle(fontWeight: FontWeight.w500)),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
mapping.inputAction,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (mapping.inputParameters.isNotEmpty)
|
|
||||||
Container(
|
|
||||||
margin: const EdgeInsets.only(left: 4),
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.blue.withOpacity(0.2),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border.all(color: Colors.blue.withOpacity(0.3)),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'${mapping.inputParameters.length} param${mapping.inputParameters.length != 1 ? 's' : ''}',
|
|
||||||
style: TextStyle(fontSize: 10, color: Colors.blue[700]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 2),
|
|
||||||
...mapping.outputActions.asMap().entries.map((entry) {
|
|
||||||
final index = entry.key;
|
|
||||||
final output = entry.value;
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 2),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
index == 0 ? 'Output: ' : ' ',
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
output.action,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (output.parameters.isNotEmpty)
|
|
||||||
Container(
|
|
||||||
margin: const EdgeInsets.only(left: 4),
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.orange.withOpacity(0.2),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border.all(color: Colors.orange.withOpacity(0.3)),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'${output.parameters.length} param${output.parameters.length != 1 ? 's' : ''}',
|
|
||||||
style: TextStyle(fontSize: 10, color: Colors.orange[700]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
if (mapping.executionCount > 0) ...[
|
|
||||||
const SizedBox(height: 2),
|
|
||||||
Text('Executions: ${mapping.executionCount}'),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
children: [
|
|
||||||
if (hasParameters)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// Input Parameters
|
|
||||||
if (mapping.inputParameters.isNotEmpty) ...[
|
|
||||||
const Divider(),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
'Input Parameters',
|
|
||||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.blue[700],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
...mapping.inputParameters.entries.map((entry) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 4),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: 120,
|
|
||||||
child: Text(
|
|
||||||
'${entry.key}:',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
fontSize: 12,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
entry.value.toString(),
|
|
||||||
style: const TextStyle(fontSize: 12),
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
// Output Action Parameters
|
|
||||||
...mapping.outputActions.asMap().entries.map((entry) {
|
|
||||||
final index = entry.key;
|
|
||||||
final output = entry.value;
|
|
||||||
if (output.parameters.isEmpty) return const SizedBox.shrink();
|
|
||||||
|
|
||||||
return Column(
|
// Executions
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
DataCell(
|
||||||
children: [
|
|
||||||
const Divider(),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
Text(
|
||||||
'Output Action ${index + 1}: ${output.action}',
|
'${mapping.executionCount}',
|
||||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.orange[700],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
...output.parameters.entries.map((paramEntry) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 4),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: 120,
|
|
||||||
child: Text(
|
|
||||||
'${paramEntry.key}:',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
paramEntry.value.toString(),
|
|
||||||
style: const TextStyle(fontSize: 12),
|
style: const TextStyle(fontSize: 12),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
// Actions
|
||||||
);
|
DataCell(
|
||||||
}),
|
Row(
|
||||||
],
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
trailing: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.edit),
|
icon: const Icon(Icons.edit, size: 20),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.push('/action-mappings/edit/${mapping.id}', extra: mapping);
|
context.push('/action-mappings/edit/${mapping.id}', extra: mapping);
|
||||||
},
|
},
|
||||||
tooltip: 'Edit',
|
tooltip: 'Edit',
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.delete, color: Colors.red),
|
icon: const Icon(Icons.delete, size: 20, color: Colors.red),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_showDeleteConfirmation(context, mapping);
|
_showDeleteConfirmation(context, mapping);
|
||||||
},
|
},
|
||||||
tooltip: 'Delete',
|
tooltip: 'Delete',
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,6 +693,9 @@ class _ActionMappingsListScreenState extends State<ActionMappingsListScreen> {
|
|||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.read<ActionMappingBloc>().add(DeleteActionMappingEvent(mapping.id));
|
context.read<ActionMappingBloc>().add(DeleteActionMappingEvent(mapping.id));
|
||||||
|
setState(() {
|
||||||
|
_selectedMappings.remove(mapping.id);
|
||||||
|
});
|
||||||
Navigator.of(dialogContext).pop();
|
Navigator.of(dialogContext).pop();
|
||||||
},
|
},
|
||||||
child: const Text('Delete', style: TextStyle(color: Colors.red)),
|
child: const Text('Delete', style: TextStyle(color: Colors.red)),
|
||||||
|
|||||||
@@ -18,7 +18,127 @@ class ServersManagementScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ServersManagementScreenState extends State<ServersManagementScreen> {
|
class _ServersManagementScreenState extends State<ServersManagementScreen> {
|
||||||
|
final TextEditingController _searchController = TextEditingController();
|
||||||
|
final Set<String> _selectedServers = {};
|
||||||
|
bool _selectAll = false;
|
||||||
|
|
||||||
|
// Filter states
|
||||||
String _filterType = 'all'; // 'all', 'gcore', 'geviscope'
|
String _filterType = 'all'; // 'all', 'gcore', 'geviscope'
|
||||||
|
bool? _filterEnabled; // null = all, true = enabled only, false = disabled only
|
||||||
|
String _searchQuery = '';
|
||||||
|
|
||||||
|
// Sort states
|
||||||
|
int? _sortColumnIndex;
|
||||||
|
bool _sortAscending = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_searchController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Server> _getFilteredServers(List<Server> allServers) {
|
||||||
|
var filtered = allServers;
|
||||||
|
|
||||||
|
// Apply type filter
|
||||||
|
if (_filterType == 'gcore') {
|
||||||
|
filtered = filtered.where((s) => s.type == ServerType.gcore).toList();
|
||||||
|
} else if (_filterType == 'geviscope') {
|
||||||
|
filtered = filtered.where((s) => s.type == ServerType.geviscope).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply enabled/disabled filter
|
||||||
|
if (_filterEnabled != null) {
|
||||||
|
filtered = filtered.where((s) => s.enabled == _filterEnabled).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply search filter
|
||||||
|
if (_searchQuery.isNotEmpty) {
|
||||||
|
final query = _searchQuery.toLowerCase();
|
||||||
|
filtered = filtered.where((s) {
|
||||||
|
return s.alias.toLowerCase().contains(query) ||
|
||||||
|
s.host.toLowerCase().contains(query) ||
|
||||||
|
s.user.toLowerCase().contains(query);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply sorting
|
||||||
|
if (_sortColumnIndex != null) {
|
||||||
|
filtered.sort((a, b) {
|
||||||
|
int comparison = 0;
|
||||||
|
switch (_sortColumnIndex) {
|
||||||
|
case 0: // Alias
|
||||||
|
comparison = a.alias.toLowerCase().compareTo(b.alias.toLowerCase());
|
||||||
|
break;
|
||||||
|
case 1: // Host
|
||||||
|
comparison = a.host.toLowerCase().compareTo(b.host.toLowerCase());
|
||||||
|
break;
|
||||||
|
case 2: // User
|
||||||
|
comparison = a.user.toLowerCase().compareTo(b.user.toLowerCase());
|
||||||
|
break;
|
||||||
|
case 3: // Type
|
||||||
|
comparison = a.type.toString().compareTo(b.type.toString());
|
||||||
|
break;
|
||||||
|
case 4: // Status
|
||||||
|
comparison = a.enabled == b.enabled ? 0 : (a.enabled ? -1 : 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return _sortAscending ? comparison : -comparison;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _toggleSelectAll(List<Server> servers) {
|
||||||
|
setState(() {
|
||||||
|
if (_selectAll) {
|
||||||
|
_selectedServers.clear();
|
||||||
|
} else {
|
||||||
|
_selectedServers.addAll(servers.map((s) => s.id));
|
||||||
|
}
|
||||||
|
_selectAll = !_selectAll;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showBatchDeleteDialog(BuildContext context) {
|
||||||
|
if (_selectedServers.isEmpty) return;
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogContext) => AlertDialog(
|
||||||
|
title: Text('Delete ${_selectedServers.length} Servers'),
|
||||||
|
content: Text(
|
||||||
|
'Are you sure you want to delete ${_selectedServers.length} selected server${_selectedServers.length != 1 ? 's' : ''}?'
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
// Get all selected servers to determine their types
|
||||||
|
final bloc = context.read<ServerBloc>();
|
||||||
|
final state = bloc.state;
|
||||||
|
if (state is ServerLoaded) {
|
||||||
|
for (final id in _selectedServers) {
|
||||||
|
final server = state.servers.firstWhere((s) => s.id == id);
|
||||||
|
bloc.add(DeleteServerEvent(id, server.type));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_selectedServers.clear();
|
||||||
|
_selectAll = false;
|
||||||
|
});
|
||||||
|
Navigator.of(dialogContext).pop();
|
||||||
|
},
|
||||||
|
child: const Text('Delete All', style: TextStyle(color: Colors.red)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -119,29 +239,163 @@ class _ServersManagementScreenState extends State<ServersManagementScreen> {
|
|||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
// Filter tabs
|
// Toolbar with search, filters, and batch actions
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Row(
|
color: Colors.grey[100],
|
||||||
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_buildFilterChip('All Servers', 'all'),
|
// Search and Add button row
|
||||||
const SizedBox(width: 8),
|
Row(
|
||||||
_buildFilterChip('G-Core', 'gcore'),
|
children: [
|
||||||
const SizedBox(width: 8),
|
Expanded(
|
||||||
_buildFilterChip('GeViScope', 'geviscope'),
|
child: TextField(
|
||||||
const Spacer(),
|
controller: _searchController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Search by alias, host, or user...',
|
||||||
|
prefixIcon: const Icon(Icons.search),
|
||||||
|
suffixIcon: _searchQuery.isNotEmpty
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_searchController.clear();
|
||||||
|
_searchQuery = '';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.white,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_searchQuery = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_showAddServerDialog(context);
|
_showAddServerDialog(context);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
label: const Text('Add Server'),
|
label: const Text('Add Server'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Filter chips and batch actions
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
// Type filter chips
|
||||||
|
const Text('Type: ', style: TextStyle(fontWeight: FontWeight.w500)),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
ChoiceChip(
|
||||||
|
label: const Text('All'),
|
||||||
|
selected: _filterType == 'all',
|
||||||
|
onSelected: (selected) {
|
||||||
|
setState(() {
|
||||||
|
_filterType = 'all';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
ChoiceChip(
|
||||||
|
label: const Text('G-Core'),
|
||||||
|
selected: _filterType == 'gcore',
|
||||||
|
onSelected: (selected) {
|
||||||
|
setState(() {
|
||||||
|
_filterType = selected ? 'gcore' : 'all';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
ChoiceChip(
|
||||||
|
label: const Text('GeViScope'),
|
||||||
|
selected: _filterType == 'geviscope',
|
||||||
|
onSelected: (selected) {
|
||||||
|
setState(() {
|
||||||
|
_filterType = selected ? 'geviscope' : 'all';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 24),
|
||||||
|
|
||||||
|
// Status filter chips
|
||||||
|
const Text('Status: ', style: TextStyle(fontWeight: FontWeight.w500)),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
ChoiceChip(
|
||||||
|
label: const Text('All'),
|
||||||
|
selected: _filterEnabled == null,
|
||||||
|
onSelected: (selected) {
|
||||||
|
setState(() {
|
||||||
|
_filterEnabled = null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
ChoiceChip(
|
||||||
|
label: const Text('Enabled'),
|
||||||
|
selected: _filterEnabled == true,
|
||||||
|
onSelected: (selected) {
|
||||||
|
setState(() {
|
||||||
|
_filterEnabled = selected ? true : null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
ChoiceChip(
|
||||||
|
label: const Text('Disabled'),
|
||||||
|
selected: _filterEnabled == false,
|
||||||
|
onSelected: (selected) {
|
||||||
|
setState(() {
|
||||||
|
_filterEnabled = selected ? false : null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
|
||||||
|
// Batch actions
|
||||||
|
if (_selectedServers.isNotEmpty) ...[
|
||||||
|
Text(
|
||||||
|
'${_selectedServers.length} selected',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_selectedServers.clear();
|
||||||
|
_selectAll = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const Text('Clear'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: () => _showBatchDeleteDialog(context),
|
||||||
|
icon: const Icon(Icons.delete, size: 18),
|
||||||
|
label: const Text('Delete Selected'),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Server list
|
// Table
|
||||||
Expanded(
|
Expanded(
|
||||||
child: BlocConsumer<ServerBloc, ServerState>(
|
child: BlocConsumer<ServerBloc, ServerState>(
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
@@ -197,7 +451,7 @@ class _ServersManagementScreenState extends State<ServersManagementScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (state is ServerLoaded) {
|
} else if (state is ServerLoaded) {
|
||||||
final filteredServers = _filterServers(state.servers);
|
final filteredServers = _getFilteredServers(state.servers);
|
||||||
|
|
||||||
if (filteredServers.isEmpty) {
|
if (filteredServers.isEmpty) {
|
||||||
return Center(
|
return Center(
|
||||||
@@ -207,14 +461,18 @@ class _ServersManagementScreenState extends State<ServersManagementScreen> {
|
|||||||
Icon(Icons.dns_outlined, size: 64, color: Colors.grey[400]),
|
Icon(Icons.dns_outlined, size: 64, color: Colors.grey[400]),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'No servers found',
|
_searchQuery.isNotEmpty || _filterEnabled != null || _filterType != 'all'
|
||||||
|
? 'No matching servers'
|
||||||
|
: 'No servers found',
|
||||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
color: Colors.grey[600],
|
color: Colors.grey[600],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'Add a server to get started',
|
_searchQuery.isNotEmpty || _filterEnabled != null || _filterType != 'all'
|
||||||
|
? 'Try adjusting your search or filters'
|
||||||
|
: 'Add a server to get started',
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
color: Colors.grey[500],
|
color: Colors.grey[500],
|
||||||
),
|
),
|
||||||
@@ -224,14 +482,7 @@ class _ServersManagementScreenState extends State<ServersManagementScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ListView.builder(
|
return _buildCompactTable(context, filteredServers);
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
itemCount: filteredServers.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final server = filteredServers[index];
|
|
||||||
return _buildServerCard(context, server);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else if (state is ServerError) {
|
} else if (state is ServerError) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -264,8 +515,6 @@ class _ServersManagementScreenState extends State<ServersManagementScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle ServerInitial or any other unknown states with a loading indicator
|
|
||||||
// instead of "No data" to prevent confusion during state transitions
|
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -275,95 +524,190 @@ class _ServersManagementScreenState extends State<ServersManagementScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Server> _filterServers(List<Server> servers) {
|
Widget _buildCompactTable(BuildContext context, List<Server> servers) {
|
||||||
if (_filterType == 'all') {
|
return SingleChildScrollView(
|
||||||
return servers;
|
child: Padding(
|
||||||
} else if (_filterType == 'gcore') {
|
padding: const EdgeInsets.all(16.0),
|
||||||
return servers.where((s) => s.type == ServerType.gcore).toList();
|
child: DataTable(
|
||||||
} else {
|
columnSpacing: 24,
|
||||||
return servers.where((s) => s.type == ServerType.geviscope).toList();
|
horizontalMargin: 12,
|
||||||
}
|
headingRowHeight: 48,
|
||||||
}
|
dataRowHeight: 56,
|
||||||
|
showCheckboxColumn: true,
|
||||||
Widget _buildFilterChip(String label, String value) {
|
sortColumnIndex: _sortColumnIndex,
|
||||||
final isSelected = _filterType == value;
|
sortAscending: _sortAscending,
|
||||||
return FilterChip(
|
columns: [
|
||||||
label: Text(label),
|
DataColumn(
|
||||||
selected: isSelected,
|
label: const Text('Alias', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
onSelected: (selected) {
|
onSort: (columnIndex, ascending) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_filterType = value;
|
_sortColumnIndex = columnIndex;
|
||||||
|
_sortAscending = ascending;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
),
|
||||||
}
|
DataColumn(
|
||||||
|
label: const Text('Host', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
onSort: (columnIndex, ascending) {
|
||||||
|
setState(() {
|
||||||
|
_sortColumnIndex = columnIndex;
|
||||||
|
_sortAscending = ascending;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
DataColumn(
|
||||||
|
label: const Text('User', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
onSort: (columnIndex, ascending) {
|
||||||
|
setState(() {
|
||||||
|
_sortColumnIndex = columnIndex;
|
||||||
|
_sortAscending = ascending;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
DataColumn(
|
||||||
|
label: const Text('Type', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
onSort: (columnIndex, ascending) {
|
||||||
|
setState(() {
|
||||||
|
_sortColumnIndex = columnIndex;
|
||||||
|
_sortAscending = ascending;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
DataColumn(
|
||||||
|
label: const Text('Status', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
onSort: (columnIndex, ascending) {
|
||||||
|
setState(() {
|
||||||
|
_sortColumnIndex = columnIndex;
|
||||||
|
_sortAscending = ascending;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const DataColumn(
|
||||||
|
label: Text('Actions', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
rows: servers.map((server) {
|
||||||
|
final isSelected = _selectedServers.contains(server.id);
|
||||||
|
|
||||||
Widget _buildServerCard(BuildContext context, Server server) {
|
return DataRow(
|
||||||
return Card(
|
selected: isSelected,
|
||||||
margin: const EdgeInsets.only(bottom: 12.0),
|
onSelectChanged: (selected) {
|
||||||
child: ListTile(
|
setState(() {
|
||||||
leading: CircleAvatar(
|
if (selected == true) {
|
||||||
backgroundColor: server.type == ServerType.gcore
|
_selectedServers.add(server.id);
|
||||||
? Colors.green.withOpacity(0.2)
|
} else {
|
||||||
: Colors.purple.withOpacity(0.2),
|
_selectedServers.remove(server.id);
|
||||||
child: Icon(
|
}
|
||||||
Icons.dns,
|
_selectAll = _selectedServers.length == servers.length;
|
||||||
color: server.type == ServerType.gcore ? Colors.green : Colors.purple,
|
});
|
||||||
),
|
},
|
||||||
),
|
cells: [
|
||||||
title: Row(
|
// Alias
|
||||||
children: [
|
DataCell(
|
||||||
Text(
|
Text(
|
||||||
server.alias,
|
server.alias,
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
),
|
||||||
|
|
||||||
|
// Host
|
||||||
|
DataCell(
|
||||||
|
Text(
|
||||||
|
server.host,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// User
|
||||||
|
DataCell(
|
||||||
|
Text(
|
||||||
|
server.user,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Type
|
||||||
|
DataCell(
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
color: server.type == ServerType.gcore
|
||||||
|
? Colors.green.withOpacity(0.1)
|
||||||
|
: Colors.purple.withOpacity(0.1),
|
||||||
|
border: Border.all(
|
||||||
|
color: server.type == ServerType.gcore ? Colors.green : Colors.purple,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
server.type == ServerType.gcore ? 'G-Core' : 'GeViScope',
|
||||||
|
style: TextStyle(
|
||||||
|
color: server.type == ServerType.gcore ? Colors.green[700] : Colors.purple[700],
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Status
|
||||||
|
DataCell(
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: server.enabled ? Colors.green.withOpacity(0.1) : Colors.grey.withOpacity(0.1),
|
||||||
|
border: Border.all(
|
||||||
color: server.enabled ? Colors.green : Colors.grey,
|
color: server.enabled ? Colors.green : Colors.grey,
|
||||||
|
),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
server.enabled ? 'Enabled' : 'Disabled',
|
server.enabled ? 'Enabled' : 'Disabled',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: server.enabled ? Colors.green[700] : Colors.grey[700],
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
subtitle: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
// Actions
|
||||||
children: [
|
DataCell(
|
||||||
const SizedBox(height: 4),
|
Row(
|
||||||
Text('Host: ${server.host}'),
|
|
||||||
Text('User: ${server.user}'),
|
|
||||||
Text('Type: ${server.type == ServerType.gcore ? "G-Core" : "GeViScope"}'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
isThreeLine: true,
|
|
||||||
trailing: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.edit),
|
icon: const Icon(Icons.edit, size: 20),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.push('/servers/edit/${server.id}', extra: server);
|
context.push('/servers/edit/${server.id}', extra: server);
|
||||||
},
|
},
|
||||||
tooltip: 'Edit',
|
tooltip: 'Edit',
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.delete, color: Colors.red),
|
icon: const Icon(Icons.delete, size: 20, color: Colors.red),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_showDeleteConfirmation(context, server);
|
_showDeleteConfirmation(context, server);
|
||||||
},
|
},
|
||||||
tooltip: 'Delete',
|
tooltip: 'Delete',
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,6 +725,9 @@ class _ServersManagementScreenState extends State<ServersManagementScreen> {
|
|||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.read<ServerBloc>().add(DeleteServerEvent(server.id, server.type));
|
context.read<ServerBloc>().add(DeleteServerEvent(server.id, server.type));
|
||||||
|
setState(() {
|
||||||
|
_selectedServers.remove(server.id);
|
||||||
|
});
|
||||||
Navigator.of(dialogContext).pop();
|
Navigator.of(dialogContext).pop();
|
||||||
},
|
},
|
||||||
child: const Text('Delete', style: TextStyle(color: Colors.red)),
|
child: const Text('Delete', style: TextStyle(color: Colors.red)),
|
||||||
|
|||||||
@@ -112,6 +112,10 @@ class _ActionPickerDialogState extends State<ActionPickerDialog> {
|
|||||||
_selectedActionName = actionName;
|
_selectedActionName = actionName;
|
||||||
_selectedTemplate = widget.templates[actionName];
|
_selectedTemplate = widget.templates[actionName];
|
||||||
|
|
||||||
|
print('[DEBUG] _selectAction: action=$actionName, categoryPrefix=$_categoryPrefix');
|
||||||
|
print('[DEBUG] _selectAction: gscServers count=${widget.gscServers.length}');
|
||||||
|
print('[DEBUG] _selectAction: gcoreServers count=${widget.gcoreServers.length}');
|
||||||
|
|
||||||
// Clear previous parameter controllers
|
// Clear previous parameter controllers
|
||||||
for (final controller in _paramControllers.values) {
|
for (final controller in _paramControllers.values) {
|
||||||
controller.dispose();
|
controller.dispose();
|
||||||
@@ -121,6 +125,7 @@ class _ActionPickerDialogState extends State<ActionPickerDialog> {
|
|||||||
|
|
||||||
// Initialize new parameter controllers
|
// Initialize new parameter controllers
|
||||||
if (_selectedTemplate != null) {
|
if (_selectedTemplate != null) {
|
||||||
|
print('[DEBUG] _selectAction: template parameters=${_selectedTemplate!.parameters}');
|
||||||
for (final param in _selectedTemplate!.parameters) {
|
for (final param in _selectedTemplate!.parameters) {
|
||||||
_paramControllers[param] = TextEditingController();
|
_paramControllers[param] = TextEditingController();
|
||||||
_paramEnabled[param] = false; // Start with parameters disabled
|
_paramEnabled[param] = false; // Start with parameters disabled
|
||||||
@@ -130,29 +135,41 @@ class _ActionPickerDialogState extends State<ActionPickerDialog> {
|
|||||||
if (_categoryPrefix == 'gcore') {
|
if (_categoryPrefix == 'gcore') {
|
||||||
// Add G-Core server parameter
|
// Add G-Core server parameter
|
||||||
const serverParam = 'GCoreServer';
|
const serverParam = 'GCoreServer';
|
||||||
|
print('[DEBUG] Adding G-Core server parameter');
|
||||||
if (!_paramControllers.containsKey(serverParam)) {
|
if (!_paramControllers.containsKey(serverParam)) {
|
||||||
_paramControllers[serverParam] = TextEditingController();
|
_paramControllers[serverParam] = TextEditingController();
|
||||||
|
print('[DEBUG] Created new controller for $serverParam');
|
||||||
}
|
}
|
||||||
_paramEnabled[serverParam] = true;
|
_paramEnabled[serverParam] = true;
|
||||||
// Auto-select first enabled server if available
|
// Auto-select first enabled server if available
|
||||||
final enabledServers = widget.gcoreServers.where((s) => s.enabled).toList();
|
final enabledServers = widget.gcoreServers.where((s) => s.enabled).toList();
|
||||||
if (enabledServers.isNotEmpty) {
|
if (enabledServers.isNotEmpty) {
|
||||||
_paramControllers[serverParam]?.text = enabledServers.first.alias;
|
_paramControllers[serverParam]?.text = enabledServers.first.alias;
|
||||||
|
print('[DEBUG] Set $serverParam to ${enabledServers.first.alias}');
|
||||||
}
|
}
|
||||||
} else if (_categoryPrefix == 'gsc') {
|
} else if (_categoryPrefix == 'gsc') {
|
||||||
// Add GSC server parameter
|
// Add GSC server parameter
|
||||||
const serverParam = 'GscServer';
|
const serverParam = 'GscServer';
|
||||||
|
print('[DEBUG] Adding GSC server parameter');
|
||||||
if (!_paramControllers.containsKey(serverParam)) {
|
if (!_paramControllers.containsKey(serverParam)) {
|
||||||
_paramControllers[serverParam] = TextEditingController();
|
_paramControllers[serverParam] = TextEditingController();
|
||||||
|
print('[DEBUG] Created new controller for $serverParam');
|
||||||
}
|
}
|
||||||
_paramEnabled[serverParam] = true;
|
_paramEnabled[serverParam] = true;
|
||||||
// Auto-select first enabled server if available
|
// Auto-select first enabled server if available
|
||||||
final enabledServers = widget.gscServers.where((s) => s.enabled).toList();
|
final enabledServers = widget.gscServers.where((s) => s.enabled).toList();
|
||||||
|
print('[DEBUG] GSC enabled servers: ${enabledServers.map((s) => s.alias).toList()}');
|
||||||
if (enabledServers.isNotEmpty) {
|
if (enabledServers.isNotEmpty) {
|
||||||
_paramControllers[serverParam]?.text = enabledServers.first.alias;
|
_paramControllers[serverParam]?.text = enabledServers.first.alias;
|
||||||
|
print('[DEBUG] Set $serverParam to ${enabledServers.first.alias}');
|
||||||
|
} else {
|
||||||
|
print('[DEBUG] WARNING: No enabled GSC servers found!');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
print('[DEBUG] Final _paramEnabled keys: ${_paramEnabled.keys.toList()}');
|
||||||
|
print('[DEBUG] Final _paramControllers keys: ${_paramControllers.keys.toList()}');
|
||||||
|
|
||||||
// Auto-fill caption if empty
|
// Auto-fill caption if empty
|
||||||
if (_captionController.text.isEmpty) {
|
if (_captionController.text.isEmpty) {
|
||||||
_captionController.text = actionName;
|
_captionController.text = actionName;
|
||||||
@@ -197,13 +214,47 @@ class _ActionPickerDialogState extends State<ActionPickerDialog> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate server parameter for GSC/G-Core actions
|
||||||
|
if (_categoryPrefix == 'gsc') {
|
||||||
|
final gscServerValue = _paramControllers['GscServer']?.text.trim() ?? '';
|
||||||
|
if (gscServerValue.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('GSC server is required. Please configure a GeViScope server first.'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
duration: Duration(seconds: 4),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (_categoryPrefix == 'gcore') {
|
||||||
|
final gcoreServerValue = _paramControllers['GCoreServer']?.text.trim() ?? '';
|
||||||
|
if (gcoreServerValue.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('G-Core server is required. Please configure a G-Core server first.'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
duration: Duration(seconds: 4),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print('[DEBUG] _onOk: Building parameters...');
|
||||||
|
print('[DEBUG] _onOk: _categoryPrefix=$_categoryPrefix');
|
||||||
|
print('[DEBUG] _onOk: _paramEnabled=${_paramEnabled}');
|
||||||
|
|
||||||
// Build parameters map from enabled parameters
|
// Build parameters map from enabled parameters
|
||||||
final parameters = <String, dynamic>{};
|
final parameters = <String, dynamic>{};
|
||||||
for (final entry in _paramEnabled.entries) {
|
for (final entry in _paramEnabled.entries) {
|
||||||
|
print('[DEBUG] _onOk: Checking param ${entry.key}, enabled=${entry.value}');
|
||||||
if (entry.value) {
|
if (entry.value) {
|
||||||
final value = _paramControllers[entry.key]?.text.trim() ?? '';
|
final value = _paramControllers[entry.key]?.text.trim() ?? '';
|
||||||
|
print('[DEBUG] _onOk: Param ${entry.key} value="$value"');
|
||||||
if (value.isNotEmpty) {
|
if (value.isNotEmpty) {
|
||||||
parameters[entry.key] = value;
|
parameters[entry.key] = value;
|
||||||
|
print('[DEBUG] _onOk: Added param ${entry.key}=$value');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -220,12 +271,16 @@ class _ActionPickerDialogState extends State<ActionPickerDialog> {
|
|||||||
parameters['Delay'] = delay;
|
parameters['Delay'] = delay;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
print('[DEBUG] _onOk: Final parameters=$parameters');
|
||||||
|
|
||||||
// Create ActionOutput with the actual action name (NOT the caption!)
|
// Create ActionOutput with the actual action name (NOT the caption!)
|
||||||
final result = ActionOutput(
|
final result = ActionOutput(
|
||||||
action: _selectedActionName!,
|
action: _selectedActionName!,
|
||||||
parameters: parameters,
|
parameters: parameters,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
print('[DEBUG] _onOk: Created ActionOutput: action=$_selectedActionName, parameters=$parameters');
|
||||||
|
|
||||||
Navigator.of(context).pop(result);
|
Navigator.of(context).pop(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,15 +292,19 @@ class _ActionPickerDialogState extends State<ActionPickerDialog> {
|
|||||||
enhanced.addAll(widget.categories);
|
enhanced.addAll(widget.categories);
|
||||||
|
|
||||||
// Add G-Core variants for applicable categories
|
// Add G-Core variants for applicable categories
|
||||||
|
// ALWAYS show these categories - even if no servers are configured
|
||||||
|
// User can see the category but won't be able to select a server if none exist
|
||||||
for (final category in _serverCategories) {
|
for (final category in _serverCategories) {
|
||||||
if (widget.categories.containsKey(category) && widget.gcoreServers.isNotEmpty) {
|
if (widget.categories.containsKey(category)) {
|
||||||
enhanced['G-Core: $category'] = widget.categories[category]!;
|
enhanced['G-Core: $category'] = widget.categories[category]!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add GSC variants for applicable categories
|
// Add GSC variants for applicable categories
|
||||||
|
// ALWAYS show these categories - even if no servers are configured
|
||||||
|
// User can see the category but won't be able to select a server if none exist
|
||||||
for (final category in _serverCategories) {
|
for (final category in _serverCategories) {
|
||||||
if (widget.categories.containsKey(category) && widget.gscServers.isNotEmpty) {
|
if (widget.categories.containsKey(category)) {
|
||||||
enhanced['GSC: $category'] = widget.categories[category]!;
|
enhanced['GSC: $category'] = widget.categories[category]!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -258,12 +317,15 @@ class _ActionPickerDialogState extends State<ActionPickerDialog> {
|
|||||||
if (displayCategory.startsWith('G-Core: ')) {
|
if (displayCategory.startsWith('G-Core: ')) {
|
||||||
_categoryPrefix = 'gcore';
|
_categoryPrefix = 'gcore';
|
||||||
_selectedCategory = displayCategory.substring(8); // Remove "G-Core: "
|
_selectedCategory = displayCategory.substring(8); // Remove "G-Core: "
|
||||||
|
print('[DEBUG] Parsed category: prefix=gcore, base=$_selectedCategory');
|
||||||
} else if (displayCategory.startsWith('GSC: ')) {
|
} else if (displayCategory.startsWith('GSC: ')) {
|
||||||
_categoryPrefix = 'gsc';
|
_categoryPrefix = 'gsc';
|
||||||
_selectedCategory = displayCategory.substring(5); // Remove "GSC: "
|
_selectedCategory = displayCategory.substring(5); // Remove "GSC: "
|
||||||
|
print('[DEBUG] Parsed category: prefix=gsc, base=$_selectedCategory');
|
||||||
} else {
|
} else {
|
||||||
_categoryPrefix = null;
|
_categoryPrefix = null;
|
||||||
_selectedCategory = displayCategory;
|
_selectedCategory = displayCategory;
|
||||||
|
print('[DEBUG] Parsed category: prefix=null, base=$_selectedCategory');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user