instance(); if ($instance === null) { $this->dispatch('notify', message: __('dashboard.no_customer_action')); return; } try { $run = app(RestartInstance::class)($instance); } catch (AuthorizationException) { $this->dispatch('notify', message: __('cloud.restart_denied')); return; } // Null is every ordinary "not now": a machine still being built, one // whose service has ended, or one with a run already in flight. The // customer gets told to try again rather than being left with a button // that appears to have done nothing. $this->dispatch('notify', message: __($run === null ? 'cloud.restart_busy' : 'cloud.restart_started')); } /** * The customer's service instance — the one the whole page is about. * * Resolved here rather than inside render() so the restart action and the * card cannot end up talking about two different machines. */ private function instance(): ?Instance { return $this->customer()?->instances() ->whereIn('status', ['active', 'provisioning', 'cancellation_scheduled']) ->latest('id')->first(); } public function render() { $locale = app()->getLocale(); $growth = [180, 188, 195, 199, 204, 210, 214, 219, 223, 228, 231, 235]; // The card is built from the customer's real service instance, so the // maintenance badge below can be scoped to exactly that instance's host. $customer = $this->customer(); $shown = $this->instance(); $maintenance = MaintenanceWindow::forInstance($shown)->first(); // The instance carries what was actually provisioned, and the contract // what was bought. Neither is the catalogue, which only describes what // we would sell someone new — never what this customer already has. $contract = $shown?->subscription; $planKey = $shown?->plan ?? 'team'; // The whole allowance, packs included, asked of the one class that adds // them up. Reading `quota_gb` here drew the ring against the package // alone, so a customer who had bought two packs saw themselves at 90 % // full of a quota their cloud was no longer enforcing. $quota = StorageAllowance::for($shown)->totalGb() ?: 500; // Usage metering is not wired yet — scale the illustrative curve into the // instance's quota so the chart and the "x / y GB" label never disagree. $curveMax = max($growth); if ($curveMax > $quota) { $growth = array_map(fn ($v) => (int) round($v / $curveMax * $quota), $growth); } $used = $growth[count($growth) - 1]; // address() rather than `custom_domain ?: subdomain`: a domain whose // DNS proof has not been read is not this instance's address, and // printing it here would tell a customer to visit a hostname that does // not resolve to them. $domain = $shown?->subdomain ? $shown->address(ProvisioningSettings::dnsZone()) : 'cloud.example.com'; return view('livewire.cloud', [ 'instance' => [ 'name' => $customer ? __('cloud.instance_name', ['name' => $customer->name]) : __('cloud.title'), 'domain' => $domain, 'status' => $shown->status ?? 'active', // A granted package shows without a price at all — not "free", // not struck through — so a later conversion to paid does not // read as a negotiation. 'plan' => $contract?->isGranted() ? __('cloud.plan_line_granted', ['plan' => 'CluPilot Cloud '.__('billing.plan.'.$planKey)]) : __('cloud.plan_line', [ 'plan' => 'CluPilot Cloud '.__('billing.plan.'.$planKey), // The line ends in "/mo", so a yearly contract has to be // divided down before it goes in. 'price' => (int) round(($contract?->monthlyPriceCents() ?? 0) / 100), ]), 'location' => __('cloud.datacenter'), 'storageUsed' => $used, 'storageQuota' => $quota, 'seats' => __('billing.seats_count', ['count' => $contract?->seats ?? 0]), 'performance' => __('billing.perf.'.($contract?->performance ?? 'standard')), ], 'storageChart' => [ 'type' => 'line', 'data' => [ 'labels' => ['', '', '', '', '', '', '', '', '', '', '', __('cloud.now')], 'datasets' => [[ 'label' => 'GB', 'data' => $growth, 'borderColor' => 'token:accent', 'backgroundColor' => 'token:accent/0.10', 'fill' => true, 'tension' => 0.35, 'pointRadius' => 0, 'borderWidth' => 2, ]], ], 'options' => [ 'scales' => [ 'x' => ['grid' => ['display' => false]], 'y' => ['beginAtZero' => false, 'grid' => ['color' => 'token:border']], ], 'plugins' => ['legend' => ['display' => false]], ], ], 'storageLabel' => Number::format($used, locale: $locale).' / '.Number::format($quota, locale: $locale).' GB', 'maintenance' => $maintenance, // A plan change writes the new CPU and memory into the VM's // configuration and a running guest only takes them at its next cold // boot. Nobody restarts somebody's cloud as a side effect of a // purchase, so the customer is told instead — otherwise they are // paying for cores they cannot see and have no way of finding out. 'restartPending' => $shown?->restartIsPending() ?? false, // Whether there is a machine a restart could reach at all. The // button is drawn either way — hiding it would leave a customer // wondering where it went during a build — but it is only live when // pressing it can do something. 'canRestart' => $shown?->hasLiveMachine() ?? false, ]); } }