169 lines
7.6 KiB
PHP
169 lines
7.6 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire;
|
|
|
|
use App\Actions\RestartInstance;
|
|
use App\Livewire\Concerns\ResolvesCustomer;
|
|
use App\Models\Instance;
|
|
use App\Models\MaintenanceWindow;
|
|
use App\Services\Billing\StorageAllowance;
|
|
use App\Support\ProvisioningSettings;
|
|
use Illuminate\Auth\Access\AuthorizationException;
|
|
use Illuminate\Support\Number;
|
|
use Livewire\Attributes\Layout;
|
|
use Livewire\Attributes\On;
|
|
use Livewire\Component;
|
|
|
|
#[Layout('layouts.portal-app')]
|
|
class Cloud extends Component
|
|
{
|
|
use ResolvesCustomer;
|
|
|
|
/**
|
|
* Restart the customer's own cloud, once ConfirmRestartCloud has said so.
|
|
*
|
|
* The modal raises the event and this method does the work, so the rule
|
|
* about who may restart what stays in one place (R23). That place is
|
|
* RestartInstance itself, not this method: the instance below is resolved
|
|
* from the signed-in customer, so it is already theirs, but the action
|
|
* re-checks anyway — a Livewire method is reachable by anyone who can POST
|
|
* to /livewire/update, and an authorisation that lives only in how a caller
|
|
* happened to look something up is one refactor away from being gone.
|
|
*/
|
|
#[On('cloud-restart-confirmed')]
|
|
public function restart(): void
|
|
{
|
|
$instance = $this->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,
|
|
]);
|
|
}
|
|
}
|