登录
ZH
games/xencenter-xenadmin-xcp-jumphp / src / View / TreeBuilder.php · commit fb1b7c9
<?phpnamespace App\View;use App\Gui\Icons;use App\Model\Cache;use App\Model\Connection;use App\Model\Session;/** * The nodes of the resource tree, as XenCenter shows them: * - Infrastructure: XenCenter > pool > its servers > the VMs running there and the server's local storage; under the pool also its *   shared storage, its halted VMs not bound to a server, (optional) templates and networks. A host alone is the top node itself. * - Objects: by type (Pools, Servers, Virtual machines, Templates, Storage, Networks). * Ids are the XAPI refs (unique), 'xc' for the root, 'conn:<n>' for a server not connected, 'type:<x>' for the groups. */final class TreeBuilder{    public const ROOT = 'xc';    public function __construct(public bool $templates = false, public bool $networks = false, public string $filter = '')    {    }    /** @return list<array<string, mixed>> */    public function infrastructure(Session $session): array    {        $this->tagText = \App\Feature\Organize\Organizer::tagIndex($session);        $children = [];        foreach ($session->connections as $i => $c) {            $children[] = $this->connectionNode($c, $i);        }        return [['id' => self::ROOT, 'text' => 'XenCenter', 'icon' => 'xencenter', 'open' => true, 'children' => $this->filtered($children), 'bold' => true]];    }    /** @return array<string, mixed> */    private function connectionNode(Connection $c, int $i): array    {        if (!$c->isConnected()) {            $icon = $c->state === Connection::CONNECTING ? 'connecting' : 'disconnected';            $suffix = match ($c->state) {                Connection::CONNECTING => ' (connecting…)',                Connection::FAILED => ' (failed)',                default => '',            };            return ['id' => 'conn:' . $i, 'text' => $c->name(), 'suffix' => $suffix, 'icon' => $icon, 'muted' => true];        }        $cache = $c->cache;        $pool = (string)$cache->poolRef();        if (!$cache->isRealPool()) {            // a server alone: it is the top node            $master = (string)$cache->master();            $node = $this->hostNode($cache, $master);            $node['open'] = true;            foreach ($this->poolLevel($cache, $master) as $extra) {                $node['children'][] = $extra;            }            return $node;        }        $children = [];        foreach ($cache->hosts() as $ref => $host) {            $children[] = $this->hostNode($cache, $ref);        }        foreach ($this->poolLevel($cache, null) as $extra) {            $children[] = $extra;        }        return ['id' => $pool, 'text' => $c->name(), 'icon' => 'pool', 'open' => true, 'children' => $children];    }    /** @return array<string, mixed> */    private function hostNode(Cache $cache, string $host): array    {        $children = [];        $vms = $cache->vmsOn($host) + $cache->vmsHomedOn($host);        uasort($vms, fn ($a, $b) => strnatcasecmp((string)$a['name_label'], (string)$b['name_label']));        foreach ($vms as $ref => $vm) {            $children[] = $this->vmNode($cache, $ref, $vm);        }        foreach ($cache->srsOf($host) as $ref => $sr) {            if (empty($sr['shared'])) {                $children[] = ['id' => $ref, 'text' => $cache->name($ref), 'icon' => Icons::of('sr', $sr)];            }        }        $record = $cache->get($host) ?? [];        return ['id' => $host, 'text' => (string)($record['name_label'] ?? $host), 'icon' => Icons::of('host', $record, $cache), 'open' => true, 'children' => $children,            'suffix' => ($record['enabled'] ?? true) ? '' : ' (maintenance mode)'];    }    /** @return array<string, mixed> */    private function vmNode(Cache $cache, string $ref, array $vm): array    {        $node = ['id' => $ref, 'text' => (string)$vm['name_label'], 'icon' => Icons::of('vm', $vm)];        $snapshots = [];        foreach ($cache->snapshotsOf($ref) as $s => $snap) {            $snapshots[] = ['id' => $s, 'text' => (string)$snap['name_label'], 'icon' => 'snapshot', 'muted' => true];        }        if ($snapshots !== []) {            $node['children'] = $snapshots;        }        return $node;    }    /** What hangs under the pool (or the lone server): shared storage, VMs of no server, templates, networks. @return list<array<string, mixed>> */    private function poolLevel(Cache $cache, ?string $loneHost): array    {        $out = [];        if ($loneHost === null) {            foreach ($cache->vmsHomedOn(null) as $ref => $vm) {                $out[] = $this->vmNode($cache, $ref, $vm);            }        }        foreach ($cache->srs() as $ref => $sr) {            if (!empty($sr['shared'])) {                $out[] = ['id' => $ref, 'text' => $cache->name($ref), 'icon' => Icons::of('sr', $sr)];            }        }        if ($this->templates) {            foreach ($cache->templates() as $ref => $t) {                $out[] = ['id' => $ref, 'text' => (string)$t['name_label'], 'icon' => 'template', 'muted' => !empty($t['is_default_template'])];            }        }        if ($this->networks) {            foreach ($cache->networks() as $ref => $n) {                $out[] = ['id' => $ref, 'text' => (string)$n['name_label'], 'icon' => 'network'];            }        }        return $out;    }    /** @return list<array<string, mixed>> */    public function objects(Session $session): array    {        $this->tagText = \App\Feature\Organize\Organizer::tagIndex($session);        $groups = ['pool' => 'Pools', 'host' => 'Servers', 'vm' => 'Virtual machines', 'template' => 'Templates', 'sr' => 'Storage', 'network' => 'Networks'];        $nodes = array_fill_keys(array_keys($groups), []);        foreach ($session->connections as $c) {            if (!$c->isConnected()) {                continue;            }            $cache = $c->cache;            if ($cache->isRealPool()) {                $nodes['pool'][] = ['id' => (string)$cache->poolRef(), 'text' => $c->name(), 'icon' => 'pool'];            }            foreach ($cache->hosts() as $ref => $h) {                $nodes['host'][] = ['id' => $ref, 'text' => (string)$h['name_label'], 'icon' => Icons::of('host', $h, $cache)];            }            foreach ($cache->vms() as $ref => $vm) {                $nodes['vm'][] = $this->vmNode($cache, $ref, $vm);            }            foreach ($cache->templates() as $ref => $t) {                $nodes['template'][] = ['id' => $ref, 'text' => (string)$t['name_label'], 'icon' => 'template'];            }            foreach ($cache->srs() as $ref => $sr) {                $nodes['sr'][] = ['id' => $ref, 'text' => $cache->name($ref), 'icon' => Icons::of('sr', $sr)];            }            foreach ($cache->networks() as $ref => $n) {                $nodes['network'][] = ['id' => $ref, 'text' => (string)$n['name_label'], 'icon' => 'network'];            }        }        $children = [];        foreach ($groups as $key => $title) {            $list = $this->filtered($nodes[$key]);            usort($list, fn ($a, $b) => strnatcasecmp((string)$a['text'], (string)$b['text']));            $children[] = ['id' => 'type:' . $key, 'text' => $title, 'suffix' => ' (' . count($list) . ')', 'icon' => 'folder', 'children' => $list, 'open' => $key === 'vm'];        }        return [['id' => self::ROOT, 'text' => 'Objects by type', 'icon' => 'xencenter', 'open' => true, 'children' => $children, 'bold' => true]];    }    // Organization views (src/Feature/Organize) ---------------------------------------------------------------------------    /** @var array<string, string> ref => its tags: the search box matches them too */    private array $tagText = [];    /** @return list<array<string, mixed>> Folders: the folders (other_config "folder") and the objects in them */    public function folders(Session $session): array    {        $this->tagText = \App\Feature\Organize\Organizer::tagIndex($session);        return [['id' => self::ROOT, 'text' => 'Folders', 'icon' => 'folder', 'open' => true, 'bold' => true,            'children' => $this->filtered(\App\Feature\Organize\OrganizeTree::folders($session))]];    }    /** @return list<array<string, mixed>> Tags: each tag and the objects having it */    public function tags(Session $session): array    {        $this->tagText = \App\Feature\Organize\Organizer::tagIndex($session);        return [['id' => self::ROOT, 'text' => 'Tags', 'icon' => 'tag', 'open' => true, 'bold' => true,            'children' => $this->filtered(\App\Feature\Organize\OrganizeTree::tags($session))]];    }    /** The search box: only the nodes whose name (or one of whose tags) holds the text, with the nodes above them. */    private function filtered(array $nodes): array    {        $q = mb_strtolower(trim($this->filter));        if ($q === '') {            return $nodes;        }        $out = [];        foreach ($nodes as $node) {            $children = $this->filtered((array)($node['children'] ?? []));            $tags = $this->tagText[\App\Feature\Organize\OrganizeTree::refOf((string)$node['id'])] ?? '';            if ($children !== [] || str_contains(mb_strtolower((string)$node['text'] . "\n" . $tags), $q)) {                $node['children'] = $children;                $node['force'] = $children !== [];     // opened while the search is on, whatever the user closed before                $out[] = $node;            }        }        return $out;    }}