<?phpnamespace App\View;use App\Model\Cache;use App\Feature\FeatureCommands;use App\Model\Connection;/** * What can be done to the selected object, as XenCenter's menus offer it: the right click's menu, the Server / VM / Storage... * menus of the menu bar and the toolbar's buttons all ask here, so they agree. An item: ['id' => 'vm.start', 'text' => 'Start', * 'enabled' => bool, 'icon' => ..., 'shortcut' => ...] or '-'. */final class Commands{ /** @return list<array<string, mixed>|string> the right click's menu */ public static function context(?Connection $c, string $ref, string $kind): array { // + New Storage / New Network, SR actions, cross-pool migration (src/Feature/Storage) return \App\Feature\Storage\Menus::context($c, $ref, $kind, match ($kind) { 'root' => [self::item('server.add', 'Add…', true, 'add-server'), self::item('server.connectAll', 'Connect All'), self::item('server.disconnectAll', 'Disconnect All'), ...\App\Feature\Pools\PoolCommands::rootItems()], 'disconnected' => [self::item('server.connect', 'Connect', $c?->state !== Connection::CONNECTING), self::item('server.remove', 'Remove from XenCenter')], 'pool' => [self::item('vm.new', 'New VM…', true, 'new-vm'), '-', ...\App\Feature\Pools\PoolCommands::poolItems($c), self::item('server.disconnect', 'Disconnect'), self::item('server.reconnect', 'Reconnect'), self::item('server.remove', 'Remove from XenCenter'), '-', ...FeatureCommands::poolItems($c, $ref), '-', self::item('object.properties', 'Properties')], 'host' => self::hostItems($c, $ref), 'vm' => self::vmItems($c, $ref), 'template' => [self::item('template.newVm', 'New VM wizard…', true, 'new-vm'), '-', self::item('template.delete', 'Delete Template…', empty($c?->cache->field($ref, 'is_default_template'))), '-', self::item('object.properties', 'Properties')], 'snapshot' => [self::item('snapshot.revert', 'Revert To…'), self::item('snapshot.delete', 'Delete Snapshot…')], 'sr' => [self::item('sr.rescan', 'Rescan'), '-', ...FeatureCommands::srItems(), self::item('object.properties', 'Properties')], 'network' => [self::item('object.properties', 'Properties')], default => [], }); } /** A demo pool is in the tree (File > Remove the Demo Pool). */ public static function hasDemo(): bool { foreach (\App\Model\Session::$current->connections ?? [] as $c) { if ($c->profile->method === 'demo') { return true; } } return false; } /** @return list<array<string, mixed>|string> */ public static function hostItems(?Connection $c, string $host): array { $r = $c?->cache->get($host) ?? []; $enabled = (bool)($r['enabled'] ?? true); $busy = $c !== null && $c->cache->vmsOn($host) !== []; return [ self::item('vm.new', 'New VM…', true, 'new-vm'), '-', self::item('host.maintenance', $enabled ? 'Enter Maintenance Mode…' : 'Exit Maintenance Mode'), self::item('host.reboot', 'Reboot', !$busy, 'reboot'), self::item('host.shutdown', 'Shut Down', !$busy, 'power-off'), ...\App\Feature\Pools\PoolCommands::hostItems($c, $host), '-', ...FeatureCommands::hostItems($c, $host), '-', self::item('server.disconnect', 'Disconnect'), self::item('object.properties', 'Properties'), ]; } /** @return list<array<string, mixed>|string> */ public static function vmItems(?Connection $c, string $vm): array { $r = $c?->cache->get($vm) ?? []; $state = (string)($r['power_state'] ?? ''); $running = $state === 'Running'; $items = [ self::item('vm.start', 'Start', $state === 'Halted', 'power-on'), self::item('vm.shutdown', 'Shut Down', $running, 'power-off'), self::item('vm.reboot', 'Reboot', $running, 'reboot'), self::item('vm.suspend', 'Suspend', $running, 'suspend'), self::item('vm.resume', 'Resume', $state === 'Suspended', 'resume'), self::item($state === 'Paused' ? 'vm.unpause' : 'vm.pause', $state === 'Paused' ? 'Unpause' : 'Pause', $running || $state === 'Paused'), '-', self::item('vm.forceShutdown', 'Force Shut Down', $running || $state === 'Paused'), self::item('vm.forceReboot', 'Force Reboot', $running), '-', ]; if ($c !== null) { foreach ($c->cache->hosts() as $host => $h) { if ($running && $host !== ($r['resident_on'] ?? '') && ($h['enabled'] ?? true)) { $items[] = self::item('vm.migrate:' . $host, 'Migrate to ' . $h['name_label'], true, 'migrate'); } elseif ($state === 'Halted' && ($h['enabled'] ?? true)) { $items[] = self::item('vm.startOn:' . $host, 'Start on ' . $h['name_label'], true, 'power-on'); } } } return [...$items, '-', self::item('vm.snapshot', 'Take a Snapshot…', true, 'snapshot'), self::item('vm.copy', 'Copy VM…', $state === 'Halted'), self::item('vm.template', 'Convert to Template…', $state === 'Halted'), self::item('vm.delete', 'Delete VM…', $state === 'Halted'), '-', ...FeatureCommands::vmItems($c, $vm), self::item('object.properties', 'Properties'), ]; } /** @return list<array<string, mixed>|string> a menu of the menu bar */ public static function menu(string $menu, ?Connection $c, string $ref, string $kind, array $view): array { $connected = $c !== null && $c->isConnected(); return \App\Feature\Storage\Menus::menu($menu, $c, $ref, $kind, match ($menu) { 'File' => [self::item('server.add', 'Add New Server…', true, 'add-server'), self::item('server.demo', 'Open the Demo Pool'), self::item('server.removeDemo', 'Remove the Demo Pool', self::hasDemo()), ...FeatureCommands::fileItems($c), '-', self::item('server.connect', 'Connect', $c !== null && !$connected), self::item('server.disconnect', 'Disconnect', $connected), self::item('server.remove', 'Remove from XenCenter', $c !== null), '-', self::item('app.exit', 'Exit', \JumPHP\Kernel\Platform::isDesktop())], 'View' => [self::check('view.infrastructure', 'Infrastructure', $view['mode'] === 'infrastructure'), self::check('view.objects', 'Objects', $view['mode'] === 'objects'), self::check('view.notifications', 'Notifications', $view['mode'] === 'notifications'), self::check('view.folders', 'Folders', $view['mode'] === 'folders'), self::check('view.tags', 'Tags', $view['mode'] === 'tags'), '-', self::check('view.templates', 'Show Templates', $view['templates']), self::check('view.networks', 'Show Networks', $view['networks']), '-', self::item('view.refresh', 'Refresh', $connected)], 'Pool' => \App\Feature\Pools\PoolCommands::poolMenu($c, $ref, $kind), 'Server' => $kind === 'host' ? self::hostItems($c, $ref) : [self::item('server.add', 'Add…', true, 'add-server'), self::item('server.connectAll', 'Connect All'), self::item('server.disconnectAll', 'Disconnect All')], 'VM' => $kind === 'vm' ? self::vmItems($c, $ref) : [self::item('vm.new', 'New VM…', $connected, 'new-vm')], 'Storage' => $kind === 'sr' ? self::context($c, $ref, 'sr') : [self::item('sr.none', 'Select a storage repository first', false)], 'Templates' => [self::item('template.newVm', 'New VM wizard…', $kind === 'template', 'new-vm'), self::check('view.templates', 'Show Templates', $view['templates'])], 'Tools' => [self::check('tools.darkTitle', 'Compact toolbar', $view['compact'] ?? false), '-', self::item('tools.masterPassword', 'Master Password…')], 'Help' => [self::item('help.about', 'About XenCenter (JumPHP)…'), self::item('help.xcp', 'XCP-ng documentation')], default => [], }); } /** The toolbar: what each button does on this selection, and whether it can. @return array<string, array{text: string, enabled: bool, id: string}> */ public static function toolbar(?Connection $c, string $ref, string $kind): array { $connected = $c !== null && $c->isConnected(); $state = $kind === 'vm' ? (string)$c?->cache->field($ref, 'power_state', '') : ''; $host = $kind === 'host'; // a server reboots / shuts down only with no VM running on it (maintenance mode moves them away first) $hostIdle = $host && $c !== null && $c->cache->vmsOn($ref) === []; return [ 'shutDown' => ['id' => $host ? 'host.shutdown' : 'vm.shutdown', 'text' => 'Shut Down', 'enabled' => $state === 'Running' || $hostIdle], 'reboot' => ['id' => $host ? 'host.reboot' : 'vm.reboot', 'text' => 'Reboot', 'enabled' => $state === 'Running' || $hostIdle], 'startResume' => $state === 'Suspended' ? ['id' => 'vm.resume', 'text' => 'Resume', 'enabled' => true] : ['id' => 'vm.start', 'text' => 'Start', 'enabled' => $state === 'Halted'], 'suspend' => ['id' => 'vm.suspend', 'text' => 'Suspend', 'enabled' => $state === 'Running'], 'newVm' => ['id' => 'vm.new', 'text' => 'New VM', 'enabled' => $connected], ] + \App\Feature\Storage\Menus::toolbar($c); } /** @return array<string, mixed> */ public static function item(string $id, string $text, bool $enabled = true, string $icon = '', ?string $shortcut = null): array { return array_filter(['id' => $id, 'text' => $text, 'enabled' => $enabled, 'icon' => $icon, 'shortcut' => $shortcut], fn ($v) => $v !== null && $v !== ''); } /** @return array<string, mixed> */ private static function check(string $id, string $text, bool $checked): array { return ['id' => $id, 'text' => $text, 'enabled' => true, 'checked' => $checked]; } /** The XAPI call of a power command: [title, method, extra params]. */ public static function power(string $command): ?array { return match ($command) { 'vm.start' => ['Starting VM', 'VM.start', [false, false]], 'vm.shutdown' => ['Shutting down VM', 'VM.clean_shutdown', []], 'vm.forceShutdown' => ['Forcing VM shut down', 'VM.hard_shutdown', []], 'vm.reboot' => ['Rebooting VM', 'VM.clean_reboot', []], 'vm.forceReboot' => ['Forcing VM reboot', 'VM.hard_reboot', []], 'vm.suspend' => ['Suspending VM', 'VM.suspend', []], 'vm.resume' => ['Resuming VM', 'VM.resume', [false, false]], 'vm.pause' => ['Pausing VM', 'VM.pause', []], 'vm.unpause' => ['Unpausing VM', 'VM.unpause', []], default => null, }; } /** Commands XenCenter asks a confirmation for: the question. */ public static function confirmation(string $command, string $name, ?Cache $cache = null): ?string { return match ($command) { 'vm.shutdown' => "Are you sure you want to shut down VM '$name'?", 'vm.forceShutdown' => "Are you sure you want to force VM '$name' to shut down?\n\nThis is like pulling the plug: unsaved work in the VM is lost.", 'vm.reboot' => "Are you sure you want to reboot VM '$name'?", 'vm.forceReboot' => "Are you sure you want to force VM '$name' to reboot?\n\nThis is like pressing the reset button: unsaved work is lost.", 'vm.suspend' => "Are you sure you want to suspend VM '$name'?", 'vm.delete' => "Are you sure you want to delete VM '$name'?\n\nIts virtual disks are deleted with it. This cannot be undone.", 'template.delete' => "Are you sure you want to delete the template '$name'?", 'snapshot.delete' => "Are you sure you want to delete the snapshot '$name'?", 'snapshot.revert' => "Reverting to snapshot '$name' discards the current state of the VM.\n\nDo you want to revert?", 'host.reboot' => "Are you sure you want to reboot server '$name'?", 'host.shutdown' => "Are you sure you want to shut down server '$name'?\n\nYou will have to power it on again by hand (or by IPMI).", 'host.maintenance' => $cache !== null && !$cache->field((string)$cache->byName('host', $name), 'enabled', true) ? null : "Put server '$name' into maintenance mode?\n\nIts running VMs are migrated to the other servers of the pool.", 'vm.template' => "Convert VM '$name' to a template?\n\nA template cannot be started; new VMs are made from it. This cannot be undone here.", 'server.remove' => "Remove '$name' from XenCenter?\n\nNothing changes on the server.", 'server.removeDemo' => "Remove the demo pool from XenCenter?\n\nFile > Open the Demo Pool brings it back.", default => \App\Feature\Pools\PoolCommands::confirmation($command, $name, $cache), }; }}