}> */ public static function portal(): array { return [ ['label' => null, 'items' => [ ['dashboard', 'gauge', 'overview', null], ['cloud', 'cloud', 'cloud', null], // The only portal entry with a condition. Start carries no // custom domain and cannot buy one, so the tab would lead // straight to the 403 its page raises. ['domain', 'globe', 'domain', 'use-custom-domain'], ['users', 'users', 'users', null], ['backups', 'database', 'backups', null], // Route heisst 'portal.security' statt bloss 'security' — der // Pfad '/security' gehoert schon der oeffentlichen // Aufklaerungsseite (routes/web.php), siehe deren Kommentar. ['portal.security', 'shield', 'security', null], ]], ['label' => __('dashboard.nav_group.contract'), 'items' => [ ['billing', 'box', 'billing', null], ['invoices', 'receipt', 'invoices', null], ['settings', 'settings', 'settings', null], ['support', 'life-buoy', 'support', null], ]], ]; } /** * Die Konsole, geordnet nach dem, was man mit ihr tut. * * Es waren achtundzwanzig Eintraege in sieben Gruppen, und „System" war der * Platz fuer alles, was sonst nirgends hinpasste: Mail-Einrichtung neben * einem Rechtsdokument, neben den persoenlichen Kontoeinstellungen, neben * der Mitarbeiterverwaltung — und ganz unten die Seite, die sagt, was * liegt. Der Betreiber hat es so beschrieben: „offene Punkte ist der letzte * Punkt, dann Rolle drueber und Einstellungen wieder drueber". * * Zwei Regeln ordnen es jetzt. Was man einmal einrichtet, verlaesst die * Leiste in den Einrichtungsbereich (admin.setup). Und die drei Seiten, auf * denen etwas auf jemanden WARTET, stehen ganz oben, mit einer Zahl daneben * — das ist die Frage, mit der man eine Konsole oeffnet. * * Jede Seite steht genau EINMAL. Stoerungen sind aus „Betrieb" nach oben * gezogen und Zahlungsprobleme aus „Geld"; sie sind dort nicht zusaetzlich * geblieben. Eine Seite an zwei Stellen ist ihre eigene Verwirrung. * * Ein Eintrag ist [Route, Symbol, Uebersetzungsschluessel, Berechtigung], * optional gefolgt vom Schluessel, unter dem attentionCounts() seine Zahl * fuehrt. * * @return array}> */ public static function console(): array { return [ // Allein und ohne Ueberschrift: die Seite, auf der man landet. ['label' => null, 'items' => [ ['admin.overview', 'gauge', 'overview', null], ]], // Wo etwas auf jemanden wartet. Der einzige Block, der eine Zahl // traegt — und der einzige, der nach Dringlichkeit sortiert ist // statt nach Gegenstand. ['label' => __('admin.nav_group.attention'), 'items' => [ // Ohne Berechtigung: wer die Konsole oeffnen darf, soll wissen, // worauf er sich verlassen kann und worauf nicht. ['admin.open-work', 'alert-triangle', 'open_work', null, 'open_work'], ['admin.incidents', 'bell', 'incidents', null, 'incidents'], ['admin.payment-problems', 'alert-triangle', 'payment_problems', 'billing.manage', 'payment_problems'], ]], // Wer bei uns ist und was er hat. ['label' => __('admin.nav_group.customers'), 'items' => [ ['admin.customers', 'users', 'customers', null], ['admin.instances', 'box', 'instances', null], ]], // Die Maschinen und ihre Adressen — der Bestand, den man ansieht. ['label' => __('admin.nav_group.fleet'), 'items' => [ ['admin.hosts', 'server', 'hosts', null], ['admin.datacenters', 'database', 'datacenters', null], ['admin.capacity', 'database', 'capacity', 'hosts.manage'], // Hostnamen und ihre Zertifikate: was hier steht, entscheidet, // ob eine Adresse ANTWORTET. Kein Einrichten, sondern Bestand. ['admin.proxy-hosts', 'globe', 'proxy_hosts', 'site.manage'], // Eine LISTE VON GEGENSTELLEN, die man beim Arbeiten ansieht, // so wie die Hostliste — nicht etwas, das man einrichtet. ['admin.vpn', 'shield', 'vpn', null], ]], // Was gerade laeuft. Vorgaenge, kein Bestand. Stoerungen standen // hier und stehen jetzt oben: sie sind kein Vorgang, den man // verfolgt, sondern einer, der jemanden braucht. ['label' => __('admin.nav_group.operations'), 'items' => [ ['admin.provisioning', 'activity', 'provisioning', null], ['admin.maintenance', 'alert-triangle', 'maintenance', null], // Laeuft, statt eingerichtet zu werden — deshalb hier und nicht // im Einrichtungsbereich neben admin.mail. ['admin.mail-pace', 'gauge', 'mail_pace', 'mail.manage'], ]], // Alles, wo Geld drinsteht. ['label' => __('admin.nav_group.billing'), 'items' => [ ['admin.revenue', 'trending-up', 'revenue', null], ['admin.invoices', 'file-text', 'invoices', 'site.manage'], // Eigener Eintrag, kein Abschnitt der Einstellungen: was hier // gesetzt wird, steht auf einem Rechtsdokument, und neben dem // Sichtbarkeitsschalter aendert das jemand im Vorbeigehen. ['admin.finance', 'receipt', 'finance', 'site.manage'], ]], // Was geschrieben wird — gelesen beim Antworten. Die Vorlagen und // ihre Vorschau sind dagegen etwas, das man einmal schreibt: sie // stehen im Einrichtungsbereich. ['label' => __('admin.nav_group.post'), 'items' => [ ['admin.inbox', 'mail', 'inbox', 'customers.manage'], ['admin.mail-log', 'send', 'mail_log', 'customers.manage'], ]], // Ohne Ueberschrift und ganz unten: eine Tuer, keine Gruppe. ['label' => null, 'items' => [ ['admin.setup', 'settings', 'setup', null], ]], ]; } /** * Die Zahlen fuer den Block „Braucht dich". * * Diese Leiste rendert auf JEDER Konsolenseite. Ohne Zwischenspeicher * waeren das zwei Abfragen je Seitenaufruf — eine Abgabe, die man spaeter * sucht, wenn die Seiten langsam werden. Eine Minute ist kurz genug, dass * niemand eine geloeste Stoerung noch lange gezaehlt sieht, und lang genug, * dass ein Klick durch fuenf Seiten die Datenbank einmal fragt statt zehnmal. * * „Offene Punkte" kostet ohnehin nichts: das Register liegt im Code. * * @return array */ public static function attentionCounts(): array { return Cache::remember('nav.attention', now()->addMinute(), fn () => [ 'open_work' => count(OpenWork::all()), 'incidents' => Incident::query()->whereNull('resolved_at')->count(), // Beides, weil die Seite beides zeigt: eine offene Mahnung und ein // gescheiterter Bezahlvorgang sind zwei Wege zu demselben Problem. 'payment_problems' => DunningCase::query()->whereNull('settled_at')->count() + FailedCheckout::query()->whereNull('resolved_at')->count(), // Die Aktualisierung, obwohl sie in keinem Eintrag der Leiste // steht: sie steht im FUSS, neben der laufenden Version. // // Der Betreiber hat es so gemeldet: „Einstellungen ist in der // Einrichtung, wegen den Versionsupdates — man sieht es nicht, // ohne genau hinzuklicken." Die Seite dorthin zu verschieben war // richtig (ihr Konto-Teil gehoert dahin), die Folge nicht: eine // verfuegbare Aktualisierung ist nichts, wonach man sucht. // // Hier statt in einem eigenen Eintrag, weil die Version im Fuss // ohnehin schon steht. Ein vierter Eintrag unter „Braucht dich" // haette dieselbe Auskunft an einer zweiten Stelle wiederholt. 'update' => app(UpdateChannel::class)->state()['available'] ? 1 : 0, ]); } /** * Der Einrichtungsbereich: die neun Seiten, die man einmal einrichtet. * * Sie stehen hier und nicht im Livewire-Bauteil, das sie zeichnet — aus * demselben Grund, aus dem die Seitenleiste hier steht: es ist Navigation, * und wer sie liest, sind drei (die Kachelseite, die Markierung „du bist * hier" und die Brotkrume). Drei Leser, eine Liste. * * Dieselbe Form wie ein Eintrag der Leiste, und dieselbe Regel: ein Array * bei der Berechtigung heisst „eine davon genuegt", nie „alle". * * @return array> */ public static function setup(): array { return [ // Womit und woraus dieses Haus schreibt. 'delivery' => [ ['admin.mail', 'mail', 'mail', 'mail.manage'], ['admin.templates', 'file-text', 'templates', 'customers.manage'], ['admin.mail.preview', 'send', 'mail_preview', 'mail.manage'], ], // Was verkauft wird. 'offer' => [ ['admin.plans', 'tag', 'plans', 'plans.manage'], ], // Wer hereindarf und womit dieses Haus nach aussen spricht. 'access' => [ ['admin.roles', 'users', 'roles', 'staff.manage'], ['admin.settings', 'settings', 'settings', null], ['admin.integrations', 'plug', 'integrations', ['hosts.manage', 'secrets.manage']], ], // Der Zustand des Hauses selbst. 'house' => [ ['admin.readiness', 'shield-check', 'readiness', ['hosts.manage', 'secrets.manage']], ['admin.dpa', 'file-text', 'dpa', 'dpa.manage'], ], ]; } /** * Ist dieser Eintrag der, auf dem man gerade steht? * * Fuer alles ausser dem Einrichtungsbereich ist das die Route selbst. Der * Einrichtungsbereich bleibt zusaetzlich markiert, solange man auf einer * der Seiten steht, die er sammelt — sonst verliert die Leiste auf allen * neun verschobenen Seiten ihr „du bist hier", und man steht in einer * Konsole, die nicht mehr sagt, wo man ist. (Codex R15, P2 am Umbau.) */ public static function isCurrent(string $route, bool $console = false): bool { if ($console ? AdminArea::routeIs($route) : request()->routeIs($route)) { return true; } if ($console && $route === 'admin.setup') { foreach (self::setup() as $kacheln) { foreach ($kacheln as [$ziel]) { if (AdminArea::routeIs($ziel)) { return true; } } } } return false; } /** * The label of the entry the current request belongs to. * * Matched on the route rather than the path, because the console's path * changes between host-bound and fallback mode while its route names do * not. Null when the page is not in the navigation at all — a detail view, * a modal — and the caller then supplies its own title. */ public static function currentLabel(bool $console = false): ?string { $prefix = $console ? 'admin.nav.' : 'dashboard.nav.'; foreach ($console ? self::console() : self::portal() as $group) { foreach ($group['items'] as [$route, , $key]) { $matches = $console ? AdminArea::routeIs($route) : request()->routeIs($route); if ($matches) { return __($prefix.$key); } } } // Die neun verschobenen Seiten stehen nicht mehr in der Leiste. Ohne // diesen zweiten Blick verloeren sie ihre Brotkrume und die Konsole // sagte auf ihnen nur noch „Konsole" — derselbe Schaden wie die // fehlende Markierung, aus derselben Ursache. if ($console) { foreach (self::setup() as $kacheln) { foreach ($kacheln as [$route, , $key]) { if (AdminArea::routeIs($route)) { return __($prefix.$key); } } } } return null; } }