로그인
KO
<?phpnamespace App\Feature\Storage;use App\Model\Cache;use App\Model\Connection;use App\View\Commands;use App\View\Page;/** * Where the storage, network, disk and cross-pool commands show: the right click's menus, the menus of the menu bar, the toolbar * and the buttons over the tabs. Commands and Pages pass what they built through here (one call each). */final class Menus{    /** @param list<array<string, mixed>|string> $items @return list<array<string, mixed>|string> */    public static function context(?Connection $c, string $ref, string $kind, array $items): array    {        if ($c === null || !$c->isConnected()) {            return $items;        }        return match ($kind) {            'pool', 'host' => self::after($items, 'vm.new', [Commands::item('sr.new', 'New Storage…', true, 'new-sr'), Commands::item('network.new', 'New Network…', true, 'network')]),            'vm' => self::before($items, 'vm.snapshot', [Commands::item('vm.crossPoolMigrate', 'Migrate to Another Pool…', self::canCrossPool($c, $ref), 'migrate'), '-']),            'sr' => self::before($items, 'object.properties', [...self::srItems($c->cache, $ref), '-']),            'network' => self::before($items, 'object.properties', [Commands::item('network.new', 'New Network…', true, 'network'),                Commands::item('network.remove', 'Remove Network…', self::removable($c->cache, $ref)), '-']),            default => $items,        };    }    /** @param list<array<string, mixed>|string> $items @return list<array<string, mixed>|string> */    public static function menu(string $menu, ?Connection $c, string $ref, string $kind, array $items): array    {        $connected = $c !== null && $c->isConnected();        return match ($menu) {            'File' => self::after($items, 'server.demo', [Commands::item('server.demo2', 'Open the Second Demo Pool')]),            'Storage' => [Commands::item('sr.new', 'New Storage…', $connected, 'new-sr'), '-', ...$items],            'Server' => $kind === 'host' ? self::context($c, $ref, 'host', $items) : $items,            'VM' => $kind === 'vm' ? self::context($c, $ref, 'vm', $items) : $items,            default => $items,        };    }    /** @return array<string, array{text: string, enabled: bool, id: string}> the toolbar's New Storage button */    public static function toolbar(?Connection $c): array    {        return ['newStorage' => ['id' => 'sr.new', 'text' => 'New Storage', 'enabled' => $c !== null && $c->isConnected()]];    }    /** The buttons over a tab (and a few flags on its lines, for the buttons that need a line). */    public static function page(?Connection $c, string $ref, string $kind, string $tab, Page $page): Page    {        if ($c === null || !$c->isConnected()) {            return $page;        }        $cache = $c->cache;        switch ("$kind:$tab") {            case 'sr:General':                $attached = self::attached($cache, $ref);                foreach ($page->sections as &$section) {                    foreach ($section['rows'] as &$row) {                        if ($row[0] === 'State:') {                            $row[1] = $attached === 0 ? 'Detached' : ($attached < count((array)$cache->field($ref, 'PBDs', [])) ? 'Partially attached' : 'OK');                        }                    }                    unset($row);                }                unset($section);                $page->actions = [...$page->actions, ...self::srButtons($cache, $ref)];                break;            case 'sr:Storage':                if ($cache->field($ref, 'content_type') !== 'iso') {                    $page->actions[] = ['id' => 'disk.move', 'text' => 'Move…', 'needsRow' => true];                }                break;            case 'vm:Storage':                foreach ($page->rows as &$row) {                    $vbd = $cache->get((string)$row['id']) ?? [];                    $row['disk'] = ($vbd['type'] ?? '') !== 'CD' && $cache->get((string)($vbd['VDI'] ?? '')) !== null;                }                unset($row);                $page->actions = [['id' => 'disk.add', 'text' => 'Add…', 'style' => 'primary'], ['id' => 'disk.attach', 'text' => 'Attach…'],                    ['id' => 'disk.move', 'text' => 'Move…', 'needsRow' => 'disk'], ['id' => 'disk.detach', 'text' => 'Detach', 'needsRow' => 'disk'],                    ['id' => 'disk.delete', 'text' => 'Delete', 'needsRow' => 'disk', 'style' => 'danger']];                break;            case 'pool:Storage':            case 'host:Storage':                $page->actions = [['id' => 'sr.new', 'text' => 'New SR…', 'style' => 'primary'], ...$page->actions];                break;            case 'pool:Networking':            case 'host:Networking':                foreach ($page->rows as &$row) {                    $row['removable'] = self::removable($cache, (string)$row['id']);                }                unset($row);                $page->actions = [['id' => 'network.new', 'text' => 'Add Network…', 'style' => 'primary'], ['id' => 'network.properties', 'text' => 'Properties', 'needsRow' => true],                    ['id' => 'network.remove', 'text' => 'Remove', 'needsRow' => 'removable', 'style' => 'danger']];                break;            case 'network:General':                $page->actions[] = ['id' => 'network.remove', 'text' => 'Remove Network…', 'enabled' => self::removable($cache, $ref), 'style' => 'danger'];                break;        }        return $page;    }    // what can be done ---------------------------------------------------------------------------------------------------    /** How many hosts see the SR now (its PBDs plugged). */    public static function attached(Cache $cache, string $sr): int    {        $n = 0;        foreach ((array)$cache->field($sr, 'PBDs', []) as $pbd) {            $n += !empty($cache->field((string)$pbd, 'currently_attached')) ? 1 : 0;        }        return $n;    }    /** XCP-ng's own SRs (the tools ISOs, removable media) are not detached or destroyed. */    public static function managed(Cache $cache, string $sr): bool    {        return $cache->field($sr, 'type') !== 'udev' && ($cache->field($sr, 'other_config', [])['xenserver_tools_sr'] ?? '') !== 'true';    }    public static function isDefault(Cache $cache, string $sr): bool    {        return (string)$cache->field((string)$cache->poolRef(), 'default_SR', '') === $sr;    }    /** @return list<array<string, mixed>|string> */    private static function srItems(Cache $cache, string $sr): array    {        $attached = self::attached($cache, $sr);        $all = count((array)$cache->field($sr, 'PBDs', []));        $managed = self::managed($cache, $sr);        return [            Commands::item('sr.setDefault', 'Set as Default', !self::isDefault($cache, $sr) && $cache->field($sr, 'content_type') !== 'iso' && $attached > 0),            '-',            Commands::item('sr.detach', 'Detach…', $managed && $attached > 0),            Commands::item('sr.reattach', 'Reattach', $managed && $attached < $all),            Commands::item('sr.forget', 'Forget…', $managed),            Commands::item('sr.destroy', 'Destroy…', $managed),        ];    }    /** @return list<array<string, mixed>> */    private static function srButtons(Cache $cache, string $sr): array    {        $out = [];        foreach (self::srItems($cache, $sr) as $item) {            if (is_array($item)) {                $out[] = ['id' => $item['id'], 'text' => rtrim((string)$item['text'], '…'), 'enabled' => $item['enabled'], 'style' => in_array($item['id'], ['sr.destroy', 'sr.forget'], true) ? 'danger' : 'default'];            }        }        return $out;    }    /** A network goes when no VM uses it and no physical NIC carries it (VLANs and bonds go with it). */    public static function removable(Cache $cache, string $network): bool    {        $n = $cache->get($network);        if ($n === null || $cache->classOf($network) !== 'network' || (array)($n['VIFs'] ?? []) !== [] || ($n['other_config']['is_host_internal_management_network'] ?? '') === 'true') {            return false;        }        foreach ((array)($n['PIFs'] ?? []) as $pif) {            $p = $cache->get((string)$pif) ?? [];            if (!empty($p['physical']) || !empty($p['management'])) {                return false;            }        }        return true;    }    /** Another connected pool, and the VM running or halted. */    public static function canCrossPool(Connection $c, string $vm): bool    {        return in_array($c->cache->field($vm, 'power_state'), ['Running', 'Halted'], true) && self::otherPools($c) !== [];    }    /** @return array<int, Connection> the other connected pools, by index of the session */    public static function otherPools(Connection $c): array    {        $out = [];        foreach (\App\Model\Session::$current->connections ?? [] as $i => $other) {            if ($other !== $c && $other->isConnected()) {                $out[$i] = $other;            }        }        return $out;    }    // inserting into a menu -------------------------------------------------------------------------------------------------    /** @param list<array<string, mixed>|string> $items @param list<array<string, mixed>|string> $new @return list<array<string, mixed>|string> */    private static function after(array $items, string $id, array $new): array    {        $at = self::indexOf($items, $id);        return $at === null ? [...$items, ...$new] : [...array_slice($items, 0, $at + 1), ...$new, ...array_slice($items, $at + 1)];    }    /** @param list<array<string, mixed>|string> $items @param list<array<string, mixed>|string> $new @return list<array<string, mixed>|string> */    private static function before(array $items, string $id, array $new): array    {        $at = self::indexOf($items, $id);        return $at === null ? [...$items, ...$new] : [...array_slice($items, 0, $at), ...$new, ...array_slice($items, $at)];    }    private static function indexOf(array $items, string $id): ?int    {        foreach ($items as $i => $item) {            if (is_array($item) && ($item['id'] ?? '') === $id) {                return $i;            }        }        return null;    }}