Makes the confirmation checkbox always visible at the bottom of the modal instead of being hidden inside the scrollable template list.
265 lines
13 KiB
PHP
265 lines
13 KiB
PHP
@php
|
|
$iconMappings = [
|
|
'ubuntu' => 'ubuntu',
|
|
'debian' => 'debian',
|
|
'centos' => 'centos',
|
|
'alpine' => 'alpine',
|
|
'freebsd' => 'freebsd',
|
|
'openbsd' => 'openbsd',
|
|
'fedora' => 'fedora',
|
|
'arch' => 'archlinux',
|
|
'opensuse' => 'opensuse',
|
|
'suse' => 'opensuse',
|
|
'gentoo' => 'gentoo',
|
|
'oracle' => null,
|
|
'alma' => 'almalinux',
|
|
'rocky' => 'rocky-linux',
|
|
'clear' => null,
|
|
'rhel' => 'redhat',
|
|
'red hat' => 'redhat',
|
|
'open cloud' => null,
|
|
'nethserver' => null,
|
|
'alt linux' => null,
|
|
'amazon' => null,
|
|
'windows' => null,
|
|
];
|
|
|
|
$iconOverrides = [
|
|
'proxmox' => asset('assets/os/proxmox.png'),
|
|
'amazon linux' => asset('assets/os/amazonlinux.png'),
|
|
'amazonlinux' => asset('assets/os/amazonlinux.png'),
|
|
'windows' => asset('assets/os/WIN.png'),
|
|
'oracle' => asset('assets/os/tux.svg'),
|
|
'clear' => asset('assets/os/tux.svg'),
|
|
'nethserver' => asset('assets/os/tux.svg'),
|
|
'open cloud' => asset('assets/os/tux.svg'),
|
|
'alt linux' => asset('assets/os/tux.svg'),
|
|
];
|
|
|
|
$badgeLabels = [
|
|
'ubuntu 24.04 (recommended)' => 'Recommended',
|
|
];
|
|
|
|
$defaultIcon = asset('assets/os/tux.svg');
|
|
|
|
$categoryDefinitions = [
|
|
'Ubuntu' => ['ubuntu'],
|
|
'Debian' => ['debian'],
|
|
'CentOS' => ['centos'],
|
|
'Enterprise / RHEL Family' => ['rhel', 'alma', 'rocky', 'oracle'],
|
|
'Community & Rolling' => ['fedora', 'arch', 'opensuse', 'gentoo', 'clear', 'alt linux'],
|
|
'Cloud & Appliances' => ['amazon', 'open cloud', 'nethserver'],
|
|
'Lightweight' => ['alpine'],
|
|
'BSD Family' => ['freebsd', 'netbsd', 'openbsd', 'dragonfly'],
|
|
'Proxmox Hypervisor' => ['proxmox'],
|
|
'Windows Server' => ['windows'],
|
|
'Other OS' => [],
|
|
];
|
|
|
|
$sortedOs = $osList->sortBy(fn ($os) => strtolower($os->name), SORT_NATURAL);
|
|
|
|
$groupedOs = [];
|
|
foreach ($categoryDefinitions as $label => $keywords) {
|
|
$groupedOs[$label] = [];
|
|
}
|
|
|
|
foreach ($sortedOs as $template) {
|
|
$normalized = \Illuminate\Support\Str::lower($template->name);
|
|
$matchedLabel = 'Other OS';
|
|
foreach ($categoryDefinitions as $label => $keywords) {
|
|
foreach ($keywords as $keyword) {
|
|
if ($keyword && \Illuminate\Support\Str::contains($normalized, $keyword)) {
|
|
$matchedLabel = $label;
|
|
break 2;
|
|
}
|
|
}
|
|
}
|
|
$groupedOs[$matchedLabel][] = $template;
|
|
}
|
|
|
|
$groupedOs = collect($groupedOs)->filter(fn ($items) => count($items) > 0);
|
|
@endphp
|
|
|
|
<div x-data="{
|
|
open: false,
|
|
activeSection: null,
|
|
pendingSection: null,
|
|
transitionDelay: 220,
|
|
toggleSection(key) {
|
|
if (this.activeSection === key) {
|
|
this.activeSection = null;
|
|
return;
|
|
}
|
|
|
|
if (this.activeSection) {
|
|
this.pendingSection = key;
|
|
this.activeSection = null;
|
|
this.$nextTick(() => {
|
|
setTimeout(() => {
|
|
this.activeSection = this.pendingSection;
|
|
this.pendingSection = null;
|
|
}, this.transitionDelay);
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
this.activeSection = key;
|
|
}
|
|
}">
|
|
@if($isProtected)
|
|
<x-button.danger
|
|
@click="open = true"
|
|
disabled
|
|
title="Reinstall is disabled: This server is protected in Proxmox">
|
|
Reinstall
|
|
</x-button.danger>
|
|
@else
|
|
<x-button.danger @click="open = true">
|
|
Reinstall
|
|
</x-button.danger>
|
|
@endif
|
|
|
|
<template x-if="open">
|
|
<div class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 overflow-y-auto">
|
|
<div class="bg-white rounded-2xl p-6 w-full max-w-4xl shadow-2xl border border-neutral/30 my-8">
|
|
<div class="flex flex-col gap-2 mb-6">
|
|
<h2 class="text-2xl font-semibold text-base">Reinstall Server</h2>
|
|
@if($isProtected)
|
|
<div class="bg-warning/10 border border-warning/30 rounded-lg p-3 mb-2">
|
|
<p class="text-sm text-warning font-semibold flex items-center gap-2">
|
|
<x-ri-error-warning-line class="size-4" />
|
|
This server has the ability to reinstall disabled. Please contact support to enable reinstall.
|
|
</p>
|
|
</div>
|
|
@endif
|
|
<p class="text-sm text-base/70">Choose a new operating system. The selected template will be installed from scratch.</p>
|
|
</div>
|
|
|
|
<div class="space-y-3 max-h-[460px] overflow-y-auto pr-1" @if($isProtected) style="pointer-events: none; opacity: 0.6;" @endif>
|
|
@foreach($groupedOs as $categoryLabel => $templates)
|
|
@php
|
|
$categoryKey = \Illuminate\Support\Str::slug($categoryLabel);
|
|
@endphp
|
|
<div class="border border-neutral/40 rounded-xl overflow-hidden" wire:key="os-category-{{ $categoryKey }}">
|
|
<button type="button" class="w-full flex items-center justify-between px-4 py-3 bg-background-secondary/60 hover:bg-background-secondary transition"
|
|
@click="toggleSection('{{ $categoryKey }}')">
|
|
<div class="flex items-center gap-3">
|
|
<x-ri-stack-line class="size-4 text-primary" />
|
|
<span class="font-semibold text-base">{{ $categoryLabel }}</span>
|
|
<span class="text-xs text-base/60">({{ count($templates) }})</span>
|
|
</div>
|
|
<x-ri-arrow-down-s-line class="size-5 transition-transform duration-200"
|
|
x-bind:class="{ 'rotate-180': activeSection === '{{ $categoryKey }}' }"></x-ri-arrow-down-s-line>
|
|
</button>
|
|
|
|
<div class="px-4 pb-4 pt-2 bg-white"
|
|
x-show="activeSection === '{{ $categoryKey }}'"
|
|
x-collapse.duration.200ms>
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
@foreach($templates as $template)
|
|
@php
|
|
$normalized = \Illuminate\Support\Str::lower($template->name);
|
|
$iconCode = null;
|
|
foreach ($iconMappings as $keyword => $code) {
|
|
if (\Illuminate\Support\Str::contains($normalized, $keyword)) {
|
|
$iconCode = $code;
|
|
break;
|
|
}
|
|
}
|
|
$iconPath = $defaultIcon;
|
|
$iconOverridePath = null;
|
|
foreach ($iconOverrides as $keyword => $path) {
|
|
if (\Illuminate\Support\Str::contains($normalized, $keyword)) {
|
|
$iconOverridePath = $path;
|
|
break;
|
|
}
|
|
}
|
|
if ($iconOverridePath) {
|
|
$iconPath = $iconOverridePath;
|
|
} elseif ($iconCode) {
|
|
$iconPath = asset('assets/os/' . $iconCode . '.svg');
|
|
}
|
|
$badge = $badgeLabels[$normalized] ?? null;
|
|
$isSelected = (string) $template->id === (string) ($os ?? '');
|
|
@endphp
|
|
<label wire:key="os-card-{{ $template->id }}"
|
|
class="relative flex items-center gap-4 rounded-xl border p-4 cursor-pointer transition-all duration-150
|
|
@if($isSelected) border-primary bg-primary/5 ring-2 ring-primary/40 shadow-lg @else border-neutral/60 hover:border-primary/60 @endif">
|
|
<input type="radio" class="sr-only" wire:model.live="os" value="{{ $template->id }}">
|
|
<div class="shrink-0">
|
|
<img src="{{ $iconPath ?? $defaultIcon }}" alt="{{ $template->name }} logo"
|
|
class="size-12 rounded-lg border border-neutral/30 bg-white object-contain p-1">
|
|
</div>
|
|
<div class="flex flex-col">
|
|
<span class="font-semibold text-base">{{ $template->name }}</span>
|
|
<span class="text-xs text-base/60">Template ID: {{ $template->id }}</span>
|
|
@if($badge)
|
|
<span class="mt-1 inline-flex items-center gap-1 text-[11px] font-semibold uppercase tracking-wide text-secondary">
|
|
<x-ri-star-smile-fill class="size-3" /> {{ $badge }}
|
|
</span>
|
|
@endif
|
|
</div>
|
|
@if($isSelected)
|
|
<x-ri-check-line class="absolute top-3 right-3 size-5 text-primary" />
|
|
@endif
|
|
</label>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
|
|
<div class="mt-4 pt-4 border-t border-neutral/30">
|
|
@if($isProtected)
|
|
<x-form.checkbox
|
|
label="I confirm that I want to proceed with reinstalling this server, which will permanently erase all existing data, configurations, and applications. This action is irreversible and cannot be undone."
|
|
wire:model="confirmReinstall"
|
|
name="confirmReinstall"
|
|
required
|
|
disabled
|
|
/>
|
|
@else
|
|
<x-form.checkbox
|
|
label="I confirm that I want to proceed with reinstalling this server, which will permanently erase all existing data, configurations, and applications. This action is irreversible and cannot be undone."
|
|
wire:model="confirmReinstall"
|
|
name="confirmReinstall"
|
|
required
|
|
/>
|
|
@endif
|
|
</div>
|
|
|
|
<div class="flex flex-row gap-4 mt-6">
|
|
<x-button.secondary @click="open = false">Cancel</x-button.secondary>
|
|
|
|
@if($isProtected)
|
|
<x-button.danger
|
|
wire:click="reinstall"
|
|
wire:loading.attr="disabled"
|
|
wire:loading.class="opacity-50"
|
|
wire:loading.attr="aria-busy"
|
|
wire:target="reinstall"
|
|
disabled
|
|
title="Reinstall is disabled: This server is protected in Proxmox"
|
|
>
|
|
<x-loading target="reinstall" />
|
|
<div wire:loading.remove wire:target="reinstall">Reinstall</div>
|
|
</x-button.danger>
|
|
@else
|
|
<x-button.danger
|
|
wire:click="reinstall"
|
|
wire:loading.attr="disabled"
|
|
wire:loading.class="opacity-50"
|
|
wire:loading.attr="aria-busy"
|
|
wire:target="reinstall"
|
|
>
|
|
<x-loading target="reinstall" />
|
|
<div wire:loading.remove wire:target="reinstall">Reinstall</div>
|
|
</x-button.danger>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div> |