Files
pilot/gnome-pilot-extension/ui/pilotWindow.js
2026-01-10 20:24:11 +01:00

462 lines
13 KiB
JavaScript

// ui/pilotWindow.js - Fenêtre principale de l'extension Pilot Control
import GObject from 'gi://GObject';
import Gtk from 'gi://Gtk';
import Adw from 'gi://Adw';
import GLib from 'gi://GLib';
import {MetricEditDialog} from './metricEditDialog.js';
import {CommandEditDialog} from './commandEditDialog.js';
/**
* Fenêtre principale avec les sections Services, Telemetry, Commands
*/
export const PilotWindow = GObject.registerClass(
class PilotWindow extends Adw.Window {
_init(extension, yamlConfig, serviceManager) {
super._init({
title: 'Pilot Control Panel',
default_width: 800,
default_height: 600,
});
this._extension = extension;
this._yamlConfig = yamlConfig;
this._serviceManager = serviceManager;
this._buildUI();
this._loadData();
}
/**
* Construit l'interface utilisateur
*/
_buildUI() {
// Header bar
const headerBar = new Adw.HeaderBar();
// Bouton refresh
const refreshButton = new Gtk.Button({
icon_name: 'view-refresh-symbolic',
tooltip_text: 'Reload configuration',
});
refreshButton.connect('clicked', () => {
this._loadData();
});
headerBar.pack_end(refreshButton);
// Bouton save
const saveButton = new Gtk.Button({
icon_name: 'document-save-symbolic',
tooltip_text: 'Save configuration',
});
saveButton.connect('clicked', () => {
this._saveConfig();
});
headerBar.pack_end(saveButton);
// Toolbar view (GNOME 45+)
const toolbarView = new Adw.ToolbarView();
toolbarView.add_top_bar(headerBar);
// Main content box
const mainBox = new Gtk.Box({
orientation: Gtk.Orientation.VERTICAL,
margin_top: 12,
margin_bottom: 12,
margin_start: 12,
margin_end: 12,
spacing: 12,
});
// Scrolled window
const scrolledWindow = new Gtk.ScrolledWindow({
vexpand: true,
hscrollbar_policy: Gtk.PolicyType.NEVER,
});
scrolledWindow.set_child(mainBox);
toolbarView.set_content(scrolledWindow);
this.set_content(toolbarView);
// Section: Service Control
mainBox.append(this._buildServiceSection());
// Section: Telemetry Metrics
mainBox.append(this._buildTelemetrySection());
// Section: Commands
mainBox.append(this._buildCommandsSection());
}
/**
* Construit la section Service Control
*/
_buildServiceSection() {
const group = new Adw.PreferencesGroup({
title: 'Service Control',
description: 'Manage the Pilot systemd service',
});
// Service status row
this._serviceStatusRow = new Adw.ActionRow({
title: 'Service Status',
subtitle: 'Unknown',
});
const serviceSwitch = new Gtk.Switch({
valign: Gtk.Align.CENTER,
});
serviceSwitch.connect('notify::active', (sw) => {
if (sw.active) {
this._serviceManager.startService();
} else {
this._serviceManager.stopService();
}
GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => {
this._updateServiceStatus();
return GLib.SOURCE_REMOVE;
});
});
this._serviceSwitch = serviceSwitch;
this._serviceStatusRow.add_suffix(serviceSwitch);
this._serviceStatusRow.activatable_widget = serviceSwitch;
group.add(this._serviceStatusRow);
// Service auto-start row
this._serviceEnableRow = new Adw.ActionRow({
title: 'Auto-start Service',
subtitle: 'Enable service at system startup',
});
const enableSwitch = new Gtk.Switch({
valign: Gtk.Align.CENTER,
});
enableSwitch.connect('notify::active', (sw) => {
if (sw.active) {
this._serviceManager.enableService();
} else {
this._serviceManager.disableService();
}
});
this._serviceEnableSwitch = enableSwitch;
this._serviceEnableRow.add_suffix(enableSwitch);
this._serviceEnableRow.activatable_widget = enableSwitch;
group.add(this._serviceEnableRow);
// Restart button row
const restartRow = new Adw.ActionRow({
title: 'Restart Service',
subtitle: 'Apply configuration changes',
});
const restartButton = new Gtk.Button({
label: 'Restart',
valign: Gtk.Align.CENTER,
});
restartButton.connect('clicked', () => {
this._serviceManager.restartService();
GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => {
this._updateServiceStatus();
return GLib.SOURCE_REMOVE;
});
});
restartRow.add_suffix(restartButton);
group.add(restartRow);
return group;
}
/**
* Construit la section Telemetry
*/
_buildTelemetrySection() {
const group = new Adw.PreferencesGroup({
title: 'Telemetry Metrics',
description: 'Configure system monitoring metrics',
});
// Global telemetry switch
this._telemetryGlobalRow = new Adw.ActionRow({
title: 'Enable Telemetry',
subtitle: 'Master switch for all metrics',
});
const telemetrySwitch = new Gtk.Switch({
valign: Gtk.Align.CENTER,
});
telemetrySwitch.connect('notify::active', (sw) => {
this._yamlConfig.setTelemetryEnabled(sw.active);
this._markDirty();
});
this._telemetrySwitch = telemetrySwitch;
this._telemetryGlobalRow.add_suffix(telemetrySwitch);
this._telemetryGlobalRow.activatable_widget = telemetrySwitch;
group.add(this._telemetryGlobalRow);
// Container pour les métriques individuelles
this._telemetryMetricsBox = new Gtk.Box({
orientation: Gtk.Orientation.VERTICAL,
spacing: 0,
});
group.add(this._telemetryMetricsBox);
return group;
}
/**
* Construit la section Commands
*/
_buildCommandsSection() {
const group = new Adw.PreferencesGroup({
title: 'Commands',
description: 'Configure allowed system commands',
});
// Global commands switch
this._commandsGlobalRow = new Adw.ActionRow({
title: 'Enable Commands',
subtitle: 'Master switch for all commands',
});
const commandsSwitch = new Gtk.Switch({
valign: Gtk.Align.CENTER,
});
commandsSwitch.connect('notify::active', (sw) => {
this._yamlConfig.setCommandsEnabled(sw.active);
this._markDirty();
});
this._commandsSwitch = commandsSwitch;
this._commandsGlobalRow.add_suffix(commandsSwitch);
this._commandsGlobalRow.activatable_widget = commandsSwitch;
group.add(this._commandsGlobalRow);
// Allowlist editor row
this._commandsAllowlistRow = new Adw.ActionRow({
title: 'Allowed Commands',
subtitle: 'Click to edit the allowlist',
});
const editButton = new Gtk.Button({
icon_name: 'document-edit-symbolic',
valign: Gtk.Align.CENTER,
});
editButton.connect('clicked', () => {
this._editCommandsAllowlist();
});
this._commandsAllowlistRow.add_suffix(editButton);
this._commandsAllowlistRow.set_activatable(true);
this._commandsAllowlistRow.connect('activated', () => {
this._editCommandsAllowlist();
});
group.add(this._commandsAllowlistRow);
return group;
}
/**
* Charge les données depuis la config YAML
*/
_loadData() {
const config = this._yamlConfig.load();
if (!config) {
this._showError('Failed to load configuration');
return;
}
// Update service status
this._updateServiceStatus();
// Update telemetry section
const telemetryEnabled = config.features?.telemetry?.enabled || false;
this._telemetrySwitch.active = telemetryEnabled;
// Clear existing metrics
let child = this._telemetryMetricsBox.get_first_child();
while (child) {
const next = child.get_next_sibling();
this._telemetryMetricsBox.remove(child);
child = next;
}
// Add metrics
const metrics = this._yamlConfig.getTelemetryMetrics();
for (const [name, metricConfig] of Object.entries(metrics)) {
this._addMetricRow(name, metricConfig);
}
// Update commands section
const commandsEnabled = config.features?.commands?.enabled || false;
this._commandsSwitch.active = commandsEnabled;
const allowlist = this._yamlConfig.getCommandsAllowlist();
this._commandsAllowlistRow.subtitle = `${allowlist.length} commands allowed`;
this._dirtyConfig = false;
}
/**
* Ajoute une ligne pour une métrique
*/
_addMetricRow(name, metricConfig) {
const row = new Adw.ActionRow({
title: metricConfig.name || name,
subtitle: `Interval: ${metricConfig.interval_s || 'N/A'}s`,
});
// Switch pour enable/disable
const metricSwitch = new Gtk.Switch({
active: metricConfig.enabled || false,
valign: Gtk.Align.CENTER,
});
metricSwitch.connect('notify::active', (sw) => {
this._yamlConfig.updateTelemetryMetric(name, {enabled: sw.active});
this._markDirty();
});
// Bouton edit
const editButton = new Gtk.Button({
icon_name: 'document-edit-symbolic',
valign: Gtk.Align.CENTER,
});
editButton.connect('clicked', () => {
this._editMetric(name, metricConfig);
});
row.add_suffix(metricSwitch);
row.add_suffix(editButton);
this._telemetryMetricsBox.append(row);
}
/**
* Édite une métrique
*/
_editMetric(name, currentConfig) {
const dialog = new MetricEditDialog(this, name, currentConfig);
dialog.connect('response', (dlg, responseId) => {
if (responseId === Gtk.ResponseType.OK) {
const updates = dialog.getUpdates();
this._yamlConfig.updateTelemetryMetric(name, updates);
this._markDirty();
this._loadData();
}
dialog.destroy();
});
dialog.present();
}
/**
* Édite la allowlist des commandes
*/
_editCommandsAllowlist() {
const currentAllowlist = this._yamlConfig.getCommandsAllowlist();
const dialog = new CommandEditDialog(this, currentAllowlist);
dialog.connect('response', (dlg, responseId) => {
if (responseId === Gtk.ResponseType.OK) {
const newAllowlist = dialog.getAllowlist();
this._yamlConfig.updateCommandsAllowlist(newAllowlist);
this._markDirty();
this._loadData();
}
dialog.destroy();
});
dialog.present();
}
/**
* Met à jour le status du service
*/
_updateServiceStatus() {
const isActive = this._serviceManager.isServiceActive();
const isEnabled = this._serviceManager.isServiceEnabled();
this._serviceSwitch.active = isActive;
this._serviceEnableSwitch.active = isEnabled;
const statusText = isActive ? '🟢 Running' : '🔴 Stopped';
this._serviceStatusRow.subtitle = statusText;
}
/**
* Marque la config comme modifiée
*/
_markDirty() {
this._dirtyConfig = true;
}
/**
* Sauvegarde la configuration
*/
_saveConfig() {
if (!this._dirtyConfig) {
this._showInfo('No changes to save');
return;
}
const success = this._yamlConfig.save();
if (success) {
this._dirtyConfig = false;
this._showInfo('Configuration saved successfully');
// Recharger le service
this._serviceManager.reloadService();
GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => {
this._updateServiceStatus();
return GLib.SOURCE_REMOVE;
});
} else {
this._showError('Failed to save configuration');
}
}
/**
* Affiche un message d'information
*/
_showInfo(message) {
const toast = new Adw.Toast({
title: message,
timeout: 2,
});
// Note: Toast overlay nécessite Adw.ToastOverlay
// Pour l'instant, on utilise console.log
console.log(`Info: ${message}`);
}
/**
* Affiche un message d'erreur
*/
_showError(message) {
const dialog = new Adw.MessageDialog({
transient_for: this,
heading: 'Error',
body: message,
});
dialog.add_response('ok', 'OK');
dialog.set_default_response('ok');
dialog.set_close_response('ok');
dialog.present();
}
});