Entrar
PT
games/xencenter-xenadmin-xcp-jumphp / src / Gui / DashboardView.php · commit fb1b7c9
<?phpnamespace App\Gui;use App\Model\Connection;use JumPHP\Gui\Pointer;use JumPHP\Gui\Theme;use JumPHP\Renderer\Renderer;/** * The Dashboard tab of the root node: one card per connection (Feature\Pools\Dashboard::cards()), in a grid that wraps to the width * (cards at least MIN_WIDTH dp wide), a summary line above, a vertical scroll. A click on a card emits select (the tree id of that * pool or server); on the Connect button of a disconnected one, connect (the connection's index). */final class DashboardView extends Canvas{    public const DEFAULTS = ['height' => 'fill', 'width' => 'fill', 'background' => '#ffffff', 'border' => false];    public const MIN_WIDTH = 280;    public const CARD_HEIGHT = 176;    public const GAP = 14;    public const TOP = 34;    /** @var list<array<string, mixed>> */    public array $cards = [];    public string $summary = '';    public int $hover = -1;    public static function events(): array    {        return ['select', 'connect', 'contextmenu'];    }    /** @param list<array<string, mixed>> $cards */    public function setCards(array $cards, string $summary): void    {        $this->cards = $cards;        $this->summary = $summary;    }    /** @return array{0: int, 1: float} columns, card width (pixels) */    private function grid(Theme $t): array    {        $gap = $t->px(self::GAP);        $columns = max(1, (int)floor(($this->w - $t->px(8) + $gap) / ($t->px(self::MIN_WIDTH) + $gap)));        return [$columns, ($this->w - $t->px(8) - $gap * ($columns - 1)) / $columns];    }    /** @return array{0: float, 1: float, 2: float, 3: float} a card's box in view coordinates (scroll applied) */    public function cardRect(Theme $t, int $i): array    {        [$columns, $w] = $this->grid($t);        $gap = $t->px(self::GAP);        $h = $t->px(self::CARD_HEIGHT);        return [$this->x + $t->px(4) + ($i % $columns) * ($w + $gap), $this->y + $t->px(self::TOP) + intdiv($i, $columns) * ($h + $gap) - $this->scrollY, $w, $h];    }    /** @return array{0: float, 1: float, 2: float, 3: float} the Connect button of a card */    public function connectRect(Theme $t, int $i): array    {        [$x, $y, $w, $h] = $this->cardRect($t, $i);        return [$x + $w - $t->px(104), $y + $h - $t->px(42), $t->px(92), $t->px(30)];    }    protected function contentHeight(Theme $t): float    {        [$columns] = $this->grid($t);        $rows = (int)ceil(count($this->cards) / $columns);        return $t->px(self::TOP) + $rows * ($t->px(self::CARD_HEIGHT) + $t->px(self::GAP));    }    /** The card under a point, or -1. */    public function cardAt(Theme $t, float $px, float $py): int    {        foreach (array_keys($this->cards) as $i) {            [$x, $y, $w, $h] = $this->cardRect($t, $i);            if ($px >= $x && $px < $x + $w && $py >= $y && $py < $y + $h && $py >= $this->y + $t->px(self::TOP) - $t->px(4)) {                return $i;            }        }        return -1;    }    protected function paint(Renderer $r, Theme $t): void    {        $font = $t->font();        $small = $t->font($t->fontSize * 0.85);        $bold = $t->font($t->fontSize * 1.1, true);        $small->draw($r, self::fit($small, \JumPHP\Util\Lang::t($this->summary), $this->w - $t->px(8)), $this->x + $t->px(4), $this->y + $t->px(8), 1, '#57606a');        if ($this->cards === []) {            return;        }        $mouse = $this->form?->engineOrNull()?->input->getMouse();        $this->hover = $mouse === null ? -1 : $this->cardAt($t, $mouse->x, $mouse->y);        foreach ($this->cards as $i => $card) {            [$x, $y, $w, $h] = $this->cardRect($t, $i);            if ($y > $this->y + $this->h || $y + $h < $this->y) {                continue;            }            $connected = $card['state'] === Connection::CONNECTED;            $t->frame($r, $x, $y, $w, $h, $this->hover === $i ? '#f5f8fd' : '#ffffff', $this->hover === $i ? '#9dbbe8' : '#dde2ea', $t->px(8));            $pad = $t->px(14);            $icon = $connected ? ($card['pool'] ? 'pool' : 'host') : ($card['state'] === Connection::CONNECTING ? 'connecting' : 'disconnected');            Icons::draw($r, $icon, $x + $pad, $y + $pad, $t->px(24));            [$stateText, $stateColor] = match ($card['state']) {                Connection::CONNECTED => ['Connected', Icons::RUNNING],                Connection::CONNECTING => ['Connecting…', Icons::PAUSED],                Connection::FAILED => ['Failed', Icons::HALTED],                default => ['Disconnected', '#8a919c'],            };            $stateText = \JumPHP\Util\Lang::t($stateText);            $pillW = $small->width($stateText) + $t->px(16);            $t->box($r, $x + $w - $pad - $pillW, $y + $pad, $pillW, $t->px(22), $stateColor, $t->px(11));            $small->draw($r, $stateText, $x + $w - $pad - $pillW / 2, $y + $pad + ($t->px(22) - $small->height()) / 2, 1, '#ffffff', 'center');            $textX = $x + $pad + $t->px(34);            $bold->draw($r, self::fit($bold, (string)$card['name'], $x + $w - $pad - $pillW - $t->px(8) - $textX), $textX, $y + $pad - $t->px(1), 1, '#1d3c6e');            $small->draw($r, self::fit($small, (string)$card['detail'], $x + $w - $pad - $pillW - $t->px(8) - $textX), $textX, $y + $pad + $bold->height(), 1, '#6b7480');            $line = $y + $pad + $t->px(46);            if (!$connected) {                $small->draw($r, self::fit($small, \JumPHP\Util\Lang::t($card['state'] === Connection::CONNECTING ? 'Logging in to XAPI…' : 'Not connected: its figures show once connected.'),                    $w - 2 * $pad), $x + $pad, $line, 1, '#57606a');                if ($card['state'] !== Connection::CONNECTING) {                    [$bx, $by, $bw, $bh] = $this->connectRect($t, $i);                    $t->box($r, $bx, $by, $bw, $bh, '#2f6fd6', $t->px(5));                    $font->draw($r, \JumPHP\Util\Lang::t('Connect'), $bx + $bw / 2, $by + ($bh - $font->height()) / 2, 1, '#ffffff', 'center');                }                continue;            }            $font->draw($r, self::fit($font, ($card['pool'] ? $card['hosts'] . ($card['hosts'] === 1 ? ' server' : ' servers') : 'Standalone server')                . ' · ' . $card['running'] . ' of ' . $card['vms'] . ' VMs running', $w - 2 * $pad), $x + $pad, $line, 1, '#24292f');            $line += $font->lineHeight() + $t->px(8);            $this->bar($r, $t, $x + $pad, $line, $w - 2 * $pad, 'CPU', $card['cpu'], $card['cpu'] === null ? 'waiting for figures…' : round($card['cpu'] * 100) . '%');            $line += $t->px(30);            $this->bar($r, $t, $x + $pad, $line, $w - 2 * $pad, 'Memory', $card['memory'], (string)$card['memoryText']);            $line += $t->px(30);            $alerts = (int)$card['alerts'];            Icons::draw($r, $alerts > 0 ? 'warning' : 'info', $x + $pad, $line, $t->px(16));            $small->draw($r, $alerts === 0 ? \JumPHP\Util\Lang::t('No alerts') : $alerts . ($alerts === 1 ? ' alert' : ' alerts'), $x + $pad + $t->px(22), $line + ($t->px(16) - $small->height()) / 2, 1,                $alerts > 0 ? '#9a6700' : '#57606a');        }    }    /** A labelled usage bar: green, orange above 60 %, red above 85 %. */    private function bar(Renderer $r, Theme $t, float $x, float $y, float $w, string $label, ?float $ratio, string $text): void    {        $small = $t->font($t->fontSize * 0.85);        $labelW = $t->px(62);        $small->draw($r, \JumPHP\Util\Lang::t($label), $x, $y + ($t->px(14) - $small->height()) / 2, 1, '#57606a');        $bw = max($t->px(20), $w - $labelW);        $t->box($r, $x + $labelW, $y, $bw, $t->px(14), '#e8ecf1', $t->px(4));        if ($ratio !== null) {            $ratio = max(0.0, min(1.0, $ratio));            $color = $ratio > 0.85 ? Icons::HALTED : ($ratio > 0.6 ? Icons::PAUSED : Icons::RUNNING);            if ($ratio > 0) {                $t->box($r, $x + $labelW, $y, max($t->px(4), $bw * $ratio), $t->px(14), $color, $t->px(4));            }        }        $small->draw($r, self::fit($small, $text, $bw - $t->px(8)), $x + $labelW + $bw - $t->px(4), $y + $t->px(16), 1, '#6b7480', 'right');    }    public function pointerUp(Pointer $p, bool $inside): void    {        if ($this->popupUp($p) || $this->barUp() || !$inside || $this->form === null) {            return;        }        $t = $this->form->theme;        $i = $this->cardAt($t, $p->x, $p->y);        if ($i < 0) {            return;        }        $card = $this->cards[$i];        [$bx, $by, $bw, $bh] = $this->connectRect($t, $i);        if (!in_array($card['state'], [Connection::CONNECTED, Connection::CONNECTING], true) && $p->x >= $bx && $p->x < $bx + $bw && $p->y >= $by && $p->y < $by + $bh) {            $this->emit('connect', (int)$card['index']);            return;        }        $this->emit('select', (string)$card['id']);    }}