'folder:/Production/Web' > the objects in it (their XAPI refs: an object is in one folder); * - Tags: 'tag:linux' > the objects having it, as 'tag:linux|' (an object with two tags is shown twice: ids stay unique). * Folders of every connected pool are merged, as XenCenter does. */ final class OrganizeTree { public const FOLDER = 'folder:'; public const TAG = 'tag:'; /** @return list> */ public static function folders(Session $session): array { $objects = []; // folder => list of nodes foreach ($session->connections as $c) { if ($c->isConnected()) { foreach (Organizer::objects($c->cache) as $ref => $class) { $folder = Organizer::folderOf($c->cache, $ref); if ($folder !== '') { $objects[$folder][] = self::objectNode($c->cache, $ref, $class, $ref); } } } } $paths = Organizer::folders($session); $build = function (string $parent) use (&$build, $paths, $objects): array { $out = []; foreach ($paths as $path) { if (Organizer::parent($path) === $parent) { $children = [...$build($path), ...self::sorted($objects[$path] ?? [])]; $out[] = ['id' => self::FOLDER . $path, 'text' => Organizer::leaf($path), 'icon' => 'folder', 'children' => $children, 'open' => true]; } } return $out; }; return $build(''); } /** @return list> */ public static function tags(Session $session): array { $byTag = []; foreach ($session->connections as $c) { if ($c->isConnected()) { foreach (Organizer::objects($c->cache) as $ref => $class) { foreach (Organizer::tagsOf($c->cache, $ref) as $tag) { $byTag[$tag][] = self::objectNode($c->cache, $ref, $class, self::TAG . $tag . '|' . $ref); } } } } uksort($byTag, 'strnatcasecmp'); $out = []; foreach ($byTag as $tag => $nodes) { $out[] = ['id' => self::TAG . $tag, 'text' => (string)$tag, 'suffix' => ' (' . count($nodes) . ')', 'icon' => 'tag', 'children' => self::sorted($nodes), 'open' => true]; } return $out; } /** @return array */ private static function objectNode(Cache $cache, string $ref, string $class, string $id): array { $r = $cache->get($ref) ?? []; return ['id' => $id, 'text' => $cache->name($ref), 'icon' => Icons::of($class, $r, $cache)]; } /** @param list> $nodes @return list> */ private static function sorted(array $nodes): array { usort($nodes, fn ($a, $b) => strnatcasecmp((string)$a['text'], (string)$b['text'])); return $nodes; } /** The object a node of these views stands for: 'tag:linux|OpaqueRef:…' -> 'OpaqueRef:…'; any other id as it is. */ public static function refOf(string $id): string { if (str_starts_with($id, self::TAG) && ($at = strrpos($id, '|')) !== false) { return substr($id, $at + 1); } return $id; } public static function isFolder(string $id): bool { return str_starts_with($id, self::FOLDER); } public static function isTag(string $id): bool { return str_starts_with($id, self::TAG) && !str_contains($id, '|'); } /** '/Production/Web' of 'folder:/Production/Web', the tag of 'tag:linux'. */ public static function nameOf(string $id): string { return substr($id, strlen(self::isFolder($id) ? self::FOLDER : self::TAG)); } /** * MainForm::selection() for the ids of these views, null for the others: a folder or a tag node is a 'group' (its Search tab) * on the first connected server; an object under a tag is that object. * @return array{0: ?Connection, 1: string, 2: string}|null */ public static function selection(Session $session, string $id): ?array { if (self::isFolder($id) || self::isTag($id)) { foreach ($session->connections as $c) { if ($c->isConnected()) { return [$c, $id, 'group']; } } return [null, $id, 'group']; } if (str_starts_with($id, self::TAG)) { $ref = self::refOf($id); $c = $session->connectionOf($ref); return [$c, $ref, Pages::kind($c, $ref)]; } return null; } }