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:' for a server not connected, 'type:' 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> */ 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 */ 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 */ 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 */ 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> */ 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> */ 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 ref => its tags: the search box matches them too */ private array $tagText = []; /** @return list> 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> 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; } }