Deploy Ladill Hosting / deploy (push) Successful in 30s
Add shared confirm-dialog component, Alpine ladillConfirm store, and swap browser confirms for consistent bottom-sheet modals across user, admin, and hosting flows. Co-authored-by: Cursor <cursoragent@cursor.com>
559 lines
47 KiB
PHP
559 lines
47 KiB
PHP
<x-hosting-panel-layout :account="$account">
|
|
<x-slot name="title">Application Installer - {{ $account->username }}</x-slot>
|
|
<x-slot name="header">Application Installer</x-slot>
|
|
|
|
<div class="space-y-6" x-data="{ selectedApp: '' }">
|
|
@if(session('error'))
|
|
<div class="rounded-lg bg-red-50 border border-red-200 p-4">
|
|
<p class="text-sm text-red-800">{{ session('error') }}</p>
|
|
</div>
|
|
@endif
|
|
|
|
@php
|
|
$sslProvisioningSites = $account->sites->filter(fn($site) => $site->ssl_status === 'provisioning');
|
|
@endphp
|
|
|
|
@if($sslProvisioningSites->isNotEmpty())
|
|
<div class="rounded-xl border border-amber-200 bg-amber-50 p-4">
|
|
<div class="flex items-start gap-3">
|
|
<div class="flex-shrink-0">
|
|
<svg class="h-5 w-5 text-amber-500 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-sm font-medium text-amber-800">SSL Certificate Provisioning</h3>
|
|
<p class="mt-1 text-sm text-amber-700">
|
|
SSL certificates are being provisioned for
|
|
<strong>{{ $sslProvisioningSites->pluck('domain')->join(', ') }}</strong>.
|
|
Please wait until this completes before installing applications. This usually takes 1-2 minutes.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- Active Installations In Progress (at top for visibility) --}}
|
|
@php
|
|
$pendingInstallations = \App\Models\AppInstallation::whereIn('hosted_site_id', $account->sites->pluck('id'))
|
|
->where('status', 'installing')
|
|
->with('site')
|
|
->get();
|
|
@endphp
|
|
@if($pendingInstallations->count() > 0)
|
|
<div x-data="installationProgress({{ $pendingInstallations->map(fn($p) => ['id' => $p->id, 'app_type' => $p->app_type, 'domain' => $p->site->domain, 'progress' => $p->progress, 'progress_message' => $p->progress_message])->toJson() }}, {{ $account->id }})">
|
|
<template x-for="item in installationItems" :key="item.id">
|
|
<div class="rounded-xl border overflow-hidden mb-4 transition-all duration-500"
|
|
:class="item.status === 'active' ? 'border-emerald-200 bg-emerald-50' : item.status === 'failed' ? 'border-red-200 bg-red-50' : 'border-indigo-200 bg-indigo-50'">
|
|
<div class="px-6 py-4 border-b transition-colors duration-500"
|
|
:class="item.status === 'active' ? 'border-emerald-200 bg-emerald-100/50' : item.status === 'failed' ? 'border-red-200 bg-red-100/50' : 'border-indigo-200 bg-indigo-100/50'">
|
|
<h3 class="text-base font-semibold flex items-center gap-2 transition-colors duration-500"
|
|
:class="item.status === 'active' ? 'text-emerald-900' : item.status === 'failed' ? 'text-red-900' : 'text-indigo-900'">
|
|
<template x-if="item.status === 'installing'">
|
|
<svg class="h-5 w-5 animate-spin text-indigo-600" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
</template>
|
|
<template x-if="item.status === 'active'">
|
|
<svg class="h-5 w-5 text-emerald-600" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
|
</template>
|
|
<template x-if="item.status === 'failed'">
|
|
<svg class="h-5 w-5 text-red-600" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
|
|
</template>
|
|
<span x-text="item.status === 'active' ? 'Installation Complete!' : item.status === 'failed' ? 'Installation Failed' : 'Installing...'"></span>
|
|
</h3>
|
|
<p class="text-sm mt-1 transition-colors duration-500"
|
|
:class="item.status === 'active' ? 'text-emerald-700' : item.status === 'failed' ? 'text-red-700' : 'text-indigo-700'"
|
|
x-text="item.status === 'active' ? 'Your application is ready to use.' : item.status === 'failed' ? 'Something went wrong during installation.' : 'This may take a few minutes.'"></p>
|
|
</div>
|
|
<div class="p-6">
|
|
<div class="flex items-center gap-4 mb-4">
|
|
<div class="h-12 w-12 rounded-xl bg-white flex items-center justify-center flex-shrink-0 transition-colors duration-500"
|
|
:class="item.status === 'active' ? 'border-emerald-200 border' : item.status === 'failed' ? 'border-red-200 border' : 'border-indigo-200 border'">
|
|
<img :src="'/images/app_logos/' + item.app_type + '.svg'" :alt="item.app_type" class="h-7 w-7">
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<p class="text-sm font-semibold transition-colors duration-500"
|
|
:class="item.status === 'active' ? 'text-emerald-900' : item.status === 'failed' ? 'text-red-900' : 'text-indigo-900'"
|
|
x-text="item.app_type.charAt(0).toUpperCase() + item.app_type.slice(1)"></p>
|
|
<p class="text-xs transition-colors duration-500"
|
|
:class="item.status === 'active' ? 'text-emerald-600' : item.status === 'failed' ? 'text-red-600' : 'text-indigo-600'"
|
|
x-text="item.domain"></p>
|
|
</div>
|
|
<div class="text-right" x-show="item.status === 'installing'">
|
|
<p class="text-2xl font-bold text-indigo-600" x-text="item.progress + '%'"></p>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Progress bar (only during installation) --}}
|
|
<div class="space-y-2" x-show="item.status === 'installing'" x-transition>
|
|
<div class="h-2 w-full bg-indigo-200 rounded-full overflow-hidden">
|
|
<div class="h-full bg-indigo-600 rounded-full transition-all duration-500 ease-out"
|
|
:style="'width: ' + item.progress + '%'"></div>
|
|
</div>
|
|
<p class="text-xs text-indigo-700" x-text="item.progress_message"></p>
|
|
</div>
|
|
|
|
{{-- Success actions --}}
|
|
<div class="flex items-center gap-3 pt-2" x-show="item.status === 'active'" x-transition>
|
|
<a :href="'https://' + item.domain" target="_blank" class="inline-flex items-center gap-2 rounded-lg bg-emerald-600 px-4 py-2 text-sm font-medium text-white hover:bg-emerald-700 transition">
|
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>
|
|
Visit Your Site
|
|
</a>
|
|
<button @click="item.dismissed = true; $nextTick(() => { if (installationItems.every(i => i.dismissed)) window.location.reload(); })" class="inline-flex items-center gap-1 text-sm text-emerald-700 hover:text-emerald-900">
|
|
Dismiss
|
|
</button>
|
|
</div>
|
|
|
|
{{-- Failed actions --}}
|
|
<div class="pt-2" x-show="item.status === 'failed'" x-transition>
|
|
<p class="text-xs text-red-600 mb-3" x-text="item.error_message || 'Please try again or contact support if the issue persists.'"></p>
|
|
<button @click="window.location.reload()" class="inline-flex items-center gap-2 rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 transition">
|
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
|
|
Try Again
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- Failed Installations --}}
|
|
@php
|
|
$failedInstallations = \App\Models\AppInstallation::whereIn('hosted_site_id', $account->sites->pluck('id'))
|
|
->where('status', 'failed')
|
|
->with('site')
|
|
->get();
|
|
@endphp
|
|
@if($failedInstallations->count() > 0)
|
|
<div class="rounded-xl border border-red-200 bg-red-50 overflow-hidden">
|
|
<div class="px-6 py-4 border-b border-red-200 bg-red-100/50">
|
|
<h3 class="text-base font-semibold text-red-900 flex items-center gap-2">
|
|
<svg class="h-5 w-5 text-red-600" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
|
|
Failed Installations
|
|
</h3>
|
|
</div>
|
|
<div class="divide-y divide-red-200">
|
|
@foreach($failedInstallations as $failed)
|
|
<div class="p-6">
|
|
<div class="flex items-center gap-4">
|
|
<div class="h-10 w-10 rounded-lg bg-white border border-red-200 flex items-center justify-center flex-shrink-0">
|
|
<img src="{{ asset('images/app_logos/' . $failed->app_type . '.svg') }}" alt="{{ ucfirst($failed->app_type) }}" class="h-6 w-6 opacity-50">
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<p class="text-sm font-medium text-red-900">{{ ucfirst($failed->app_type) }} on {{ $failed->site->domain }}</p>
|
|
<p class="text-xs text-red-600 mt-0.5">{{ $failed->error_message ?: 'Installation failed. Please try again.' }}</p>
|
|
</div>
|
|
<form action="{{ route('hosting.panel.apps.install', $account) }}" method="POST" class="flex-shrink-0">
|
|
@csrf
|
|
<input type="hidden" name="app" value="{{ $failed->app_type }}">
|
|
<input type="hidden" name="site_id" value="{{ $failed->hosted_site_id }}">
|
|
<button type="submit" class="inline-flex items-center gap-1.5 rounded-lg bg-red-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-red-700 transition">
|
|
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
|
|
Retry
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
@if(!empty($isWordPressPlan))
|
|
{{-- WordPress-Only Installer --}}
|
|
<div class="rounded-xl border border-slate-200 bg-white overflow-hidden">
|
|
<div class="relative bg-gradient-to-br from-[#21759b]/5 via-white to-[#21759b]/5">
|
|
<div class="px-6 py-8 sm:px-8">
|
|
<div class="flex flex-col items-center text-center max-w-md mx-auto">
|
|
<div class="h-16 w-16 rounded-2xl bg-gradient-to-br from-[#21759b]/10 to-[#21759b]/20 flex items-center justify-center mb-5">
|
|
@include('partials.wordpress-icon', ['class' => 'h-10 w-10'])
|
|
</div>
|
|
<h3 class="text-lg font-bold text-slate-900">Install WordPress</h3>
|
|
<p class="text-sm text-slate-500 mt-1.5">Your hosting plan is optimised for WordPress. Install it in one click with a database and admin credentials set up automatically.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<form action="{{ route('hosting.panel.apps.install', $account) }}" method="POST" class="border-t border-slate-200 px-6 py-6 sm:px-8 space-y-5">
|
|
@csrf
|
|
<input type="hidden" name="app" value="wordpress">
|
|
|
|
<div class="grid gap-5 sm:grid-cols-2">
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-700 mb-1.5">Install To Domain</label>
|
|
@if($account->sites->count() > 0)
|
|
<select name="site_id" required class="w-full rounded-lg border border-slate-200 px-4 py-2.5 text-sm focus:border-indigo-500 focus:ring-indigo-500 bg-white">
|
|
@foreach($account->sites as $site)
|
|
<option value="{{ $site->id }}">{{ $site->domain }}</option>
|
|
@endforeach
|
|
</select>
|
|
@else
|
|
<p class="text-sm text-amber-600 bg-amber-50 border border-amber-200 rounded-lg p-3">You need to <a href="{{ route('hosting.panel.domains', $account) }}" class="underline font-medium">add a domain</a> first.</p>
|
|
@endif
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-700 mb-1.5">PHP Version</label>
|
|
<select name="php_version" class="w-full rounded-lg border border-slate-200 px-4 py-2.5 text-sm focus:border-indigo-500 focus:ring-indigo-500 bg-white">
|
|
<option value="8.4">PHP 8.4 (Recommended)</option>
|
|
<option value="8.3">PHP 8.3</option>
|
|
<option value="8.2">PHP 8.2</option>
|
|
<option value="8.1">PHP 8.1</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-700 mb-1.5">Subdirectory <span class="text-slate-400 font-normal">(optional)</span></label>
|
|
<input type="text" name="directory" placeholder="Leave empty to install at domain root" pattern="[a-zA-Z0-9_-]*" maxlength="100" class="w-full rounded-lg border border-slate-200 px-4 py-2.5 text-sm font-mono focus:border-indigo-500 focus:ring-indigo-500">
|
|
<p class="mt-1.5 text-xs text-slate-500">Install in a subfolder instead of the domain root.</p>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-3 pt-2">
|
|
<button type="submit" class="inline-flex items-center gap-2 rounded-lg bg-[#21759b] px-5 py-2.5 text-sm font-semibold text-white hover:bg-[#1a5f7f] transition shadow-sm">
|
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
|
|
Install WordPress
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
@else
|
|
{{-- Available Applications - Modern Grid --}}
|
|
<div class="rounded-xl border border-slate-200 bg-white overflow-hidden">
|
|
<div class="px-6 py-4 border-b border-slate-200">
|
|
<h3 class="text-base font-semibold text-slate-900">Available Applications</h3>
|
|
<p class="text-sm text-slate-500 mt-1">One-click installation for popular CMS platforms and frameworks.</p>
|
|
</div>
|
|
<div class="p-6">
|
|
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
@foreach($availableApps as $app)
|
|
<div @click="selectedApp = '{{ $app['id'] }}'; $nextTick(() => { document.getElementById('install-form').scrollIntoView({ behavior: 'smooth', block: 'center' }); })" class="group relative rounded-xl border border-slate-200 bg-white p-5 hover:border-indigo-300 hover:shadow-md transition-all duration-200 cursor-pointer">
|
|
<div class="flex flex-col items-center text-center">
|
|
<div class="h-14 w-14 rounded-xl bg-gradient-to-br from-slate-50 to-slate-100 flex items-center justify-center mb-4 group-hover:scale-105 transition-transform">
|
|
<img src="{{ asset('images/app_logos/' . $app['id'] . '.svg') }}" alt="{{ $app['name'] }}" class="h-9 w-9">
|
|
</div>
|
|
<h4 class="text-sm font-semibold text-slate-900">{{ $app['name'] }}</h4>
|
|
<p class="text-xs text-slate-500 mt-1 line-clamp-2">{{ $app['description'] }}</p>
|
|
<div class="mt-3 flex flex-wrap justify-center gap-1.5">
|
|
<span class="inline-flex items-center rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-medium text-slate-600">{{ ucfirst($app['category']) }}</span>
|
|
@if(in_array($app['id'], ['wordpress', 'joomla', 'drupal', 'opencart', 'magento', 'laravel']))
|
|
<span class="inline-flex items-center rounded-full bg-indigo-50 px-2 py-0.5 text-[10px] font-medium text-indigo-600">PHP</span>
|
|
@elseif($app['id'] === 'nodejs')
|
|
<span class="inline-flex items-center rounded-full bg-green-50 px-2 py-0.5 text-[10px] font-medium text-green-600">JavaScript</span>
|
|
@elseif($app['id'] === 'python')
|
|
<span class="inline-flex items-center rounded-full bg-yellow-50 px-2 py-0.5 text-[10px] font-medium text-yellow-700">Python</span>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Install Application Form --}}
|
|
<div id="install-form" class="rounded-xl border border-slate-200 bg-white p-6 scroll-mt-6">
|
|
<div class="flex items-start gap-4 mb-6">
|
|
<div class="h-10 w-10 rounded-lg bg-indigo-100 flex items-center justify-center flex-shrink-0">
|
|
<svg class="h-5 w-5 text-indigo-600" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/></svg>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-base font-semibold text-slate-900">Install New Application</h3>
|
|
<p class="text-sm text-slate-500 mt-0.5">CMS applications automatically provision a database and generate admin credentials.</p>
|
|
</div>
|
|
</div>
|
|
<form action="{{ route('hosting.panel.apps.install', $account) }}" method="POST" class="space-y-5">
|
|
@csrf
|
|
<div class="grid gap-5 sm:grid-cols-2">
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-700 mb-1.5">Application</label>
|
|
<select name="app" x-model="selectedApp" required class="w-full rounded-lg border border-slate-200 px-4 py-2.5 text-sm focus:border-indigo-500 focus:ring-indigo-500 bg-white">
|
|
<option value="">Select an application...</option>
|
|
@foreach($availableApps as $app)
|
|
<option value="{{ $app['id'] }}">{{ $app['name'] }} — {{ $app['description'] }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-700 mb-1.5">Install To Domain</label>
|
|
@if($account->sites->count() > 0)
|
|
<select name="site_id" required class="w-full rounded-lg border border-slate-200 px-4 py-2.5 text-sm focus:border-indigo-500 focus:ring-indigo-500 bg-white">
|
|
@foreach($account->sites as $site)
|
|
<option value="{{ $site->id }}">{{ $site->domain }}</option>
|
|
@endforeach
|
|
</select>
|
|
@else
|
|
<p class="text-sm text-amber-600 bg-amber-50 border border-amber-200 rounded-lg p-3">You need to <a href="{{ route('hosting.panel.domains', $account) }}" class="underline font-medium">add a domain</a> first.</p>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
{{-- PHP Version Selector (only for PHP apps) --}}
|
|
<div x-show="['wordpress', 'joomla', 'drupal', 'opencart', 'magento', 'laravel'].includes(selectedApp)" x-transition>
|
|
<label class="block text-sm font-medium text-slate-700 mb-1.5">PHP Version</label>
|
|
<select name="php_version" class="w-full rounded-lg border border-slate-200 px-4 py-2.5 text-sm focus:border-indigo-500 focus:ring-indigo-500 bg-white">
|
|
<option value="8.4">PHP 8.4 (Default)</option>
|
|
<option value="8.3">PHP 8.3</option>
|
|
<option value="8.2">PHP 8.2</option>
|
|
<option value="8.1">PHP 8.1</option>
|
|
</select>
|
|
<p class="mt-1.5 text-xs text-slate-500">
|
|
<span x-show="selectedApp === 'magento'" class="text-amber-600 font-medium">Magento requires PHP 8.1, 8.2, or 8.3</span>
|
|
<span x-show="selectedApp !== 'magento'">Select the PHP version for your application. Most apps work best with PHP 8.3 or 8.4.</span>
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-700 mb-1.5">Subdirectory <span class="text-slate-400 font-normal">(optional)</span></label>
|
|
<input type="text" name="directory" placeholder="e.g., blog, shop (leave empty for root)" pattern="[a-zA-Z0-9_-]*" maxlength="100" class="w-full rounded-lg border border-slate-200 px-4 py-2.5 text-sm font-mono focus:border-indigo-500 focus:ring-indigo-500">
|
|
<p class="mt-1.5 text-xs text-slate-500">Install in a subfolder instead of the domain root.</p>
|
|
</div>
|
|
<div class="flex items-center gap-3 pt-2">
|
|
<button type="submit" class="inline-flex items-center gap-2 rounded-lg bg-indigo-600 px-5 py-2.5 text-sm font-medium text-white hover:bg-indigo-700 transition shadow-sm">
|
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
|
|
Install Application
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- Current Installations --}}
|
|
@php
|
|
$installedSites = $account->sites->filter(fn($site) => $site->installed_app)->load('appInstallation');
|
|
@endphp
|
|
@if($installedSites->count() > 0)
|
|
<div class="rounded-xl border border-slate-200 bg-white overflow-hidden">
|
|
<div class="px-6 py-4 border-b border-slate-200">
|
|
<h3 class="text-base font-semibold text-slate-900">Current Installations</h3>
|
|
<p class="text-sm text-slate-500 mt-1">View your application credentials and access details below.</p>
|
|
</div>
|
|
<div class="divide-y divide-slate-200">
|
|
@foreach($installedSites as $site)
|
|
<div class="p-6" x-data="{ showCredentials: false }">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="h-10 w-10 rounded-lg bg-slate-100 flex items-center justify-center flex-shrink-0">
|
|
<img src="{{ asset('images/app_logos/' . $site->installed_app . '.svg') }}" alt="{{ ucfirst($site->installed_app) }}" class="h-6 w-6" onerror="this.style.display='none'; this.nextElementSibling.style.display='block';">
|
|
<svg class="h-6 w-6 text-slate-400 hidden" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>
|
|
</div>
|
|
<div>
|
|
<p class="text-sm font-medium text-slate-900">{{ ucfirst($site->installed_app) }}</p>
|
|
<p class="text-xs text-slate-500">{{ $site->domain }}</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
@if($site->installed_app_version)
|
|
<span class="text-xs text-slate-500">v{{ $site->installed_app_version }}</span>
|
|
@endif
|
|
<span class="inline-flex items-center rounded-full bg-emerald-100 px-2 py-0.5 text-xs font-medium text-emerald-700">Active</span>
|
|
<a href="https://{{ $site->domain }}" target="_blank" class="text-xs text-indigo-600 hover:text-indigo-800">Visit Site →</a>
|
|
@if($site->appInstallation)
|
|
<button @click="showCredentials = !showCredentials" class="inline-flex items-center gap-1 text-xs text-slate-600 hover:text-slate-800">
|
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
<path x-show="!showCredentials" stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
|
<path x-show="!showCredentials" stroke-linecap="round" stroke-linejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
|
<path x-show="showCredentials" stroke-linecap="round" stroke-linejoin="round" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"/>
|
|
</svg>
|
|
<span x-text="showCredentials ? 'Hide' : 'Show'"></span> Credentials
|
|
</button>
|
|
@endif
|
|
<x-confirm-dialog
|
|
:name="'delete-app-'.$site->id"
|
|
:title="'Delete '.ucfirst($site->installed_app).'?'"
|
|
message="This will remove all application files and cannot be undone."
|
|
:action="route('hosting.panel.apps.delete', [$account, $site])"
|
|
method="DELETE"
|
|
confirm-label="Delete"
|
|
>
|
|
<x-slot:trigger>
|
|
<button type="button" class="inline-flex items-center gap-1.5 rounded-lg border border-red-200 bg-white px-3 py-1.5 text-xs font-medium text-red-600 hover:bg-red-50 transition">
|
|
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
|
Delete
|
|
</button>
|
|
</x-slot:trigger>
|
|
</x-confirm-dialog>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Credentials Panel --}}
|
|
@if($site->appInstallation)
|
|
<div x-show="showCredentials" x-collapse class="mt-4 rounded-lg bg-slate-50 border border-slate-200 p-4">
|
|
<div class="grid gap-4 sm:grid-cols-2">
|
|
{{-- Admin Credentials --}}
|
|
<div class="space-y-3">
|
|
<h4 class="text-sm font-semibold text-slate-700 flex items-center gap-2">
|
|
<svg class="h-4 w-4 text-slate-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>
|
|
Admin Access
|
|
</h4>
|
|
<div class="space-y-2">
|
|
<div>
|
|
<label class="block text-xs text-slate-500 mb-0.5">Admin URL</label>
|
|
<div class="flex items-center gap-2">
|
|
<a href="{{ $site->appInstallation->admin_url }}" target="_blank" class="text-sm text-indigo-600 hover:text-indigo-800 font-mono break-all">{{ $site->appInstallation->admin_url }}</a>
|
|
<button onclick="navigator.clipboard.writeText('{{ $site->appInstallation->admin_url }}')" class="text-slate-400 hover:text-slate-600 flex-shrink-0" title="Copy">
|
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs text-slate-500 mb-0.5">Username</label>
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-sm font-mono text-slate-900">{{ $site->appInstallation->admin_username }}</span>
|
|
<button onclick="navigator.clipboard.writeText('{{ $site->appInstallation->admin_username }}')" class="text-slate-400 hover:text-slate-600" title="Copy">
|
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs text-slate-500 mb-0.5">Password</label>
|
|
@if($site->appInstallation->admin_password)
|
|
<div class="flex items-center gap-2" x-data="{ showPassword: false }">
|
|
<span class="text-sm font-mono text-slate-900" x-text="showPassword ? '{{ $site->appInstallation->admin_password }}' : '••••••••••••'"></span>
|
|
<button @click="showPassword = !showPassword" class="text-slate-400 hover:text-slate-600" title="Toggle visibility">
|
|
<svg x-show="!showPassword" class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
|
|
<svg x-show="showPassword" class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"/></svg>
|
|
</button>
|
|
<button onclick="navigator.clipboard.writeText('{{ $site->appInstallation->admin_password }}')" class="text-slate-400 hover:text-slate-600" title="Copy">
|
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
|
|
</button>
|
|
</div>
|
|
@else
|
|
<p class="text-sm text-slate-500 italic">Use the application's password reset if needed</p>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Database Credentials --}}
|
|
@if($site->appInstallation->database_name)
|
|
<div class="space-y-3">
|
|
<h4 class="text-sm font-semibold text-slate-700 flex items-center gap-2">
|
|
<svg class="h-4 w-4 text-slate-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"/></svg>
|
|
Database Access
|
|
</h4>
|
|
<div class="space-y-2">
|
|
<div>
|
|
<label class="block text-xs text-slate-500 mb-0.5">Database Name</label>
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-sm font-mono text-slate-900">{{ $site->appInstallation->database_name }}</span>
|
|
<button onclick="navigator.clipboard.writeText('{{ $site->appInstallation->database_name }}')" class="text-slate-400 hover:text-slate-600" title="Copy">
|
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs text-slate-500 mb-0.5">Username</label>
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-sm font-mono text-slate-900">{{ $site->appInstallation->database_username }}</span>
|
|
<button onclick="navigator.clipboard.writeText('{{ $site->appInstallation->database_username }}')" class="text-slate-400 hover:text-slate-600" title="Copy">
|
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs text-slate-500 mb-0.5">Password</label>
|
|
@if($site->appInstallation->database_password)
|
|
<div class="flex items-center gap-2" x-data="{ showDbPassword: false }">
|
|
<span class="text-sm font-mono text-slate-900" x-text="showDbPassword ? '{{ $site->appInstallation->database_password }}' : '••••••••••••'"></span>
|
|
<button @click="showDbPassword = !showDbPassword" class="text-slate-400 hover:text-slate-600" title="Toggle visibility">
|
|
<svg x-show="!showDbPassword" class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
|
|
<svg x-show="showDbPassword" class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"/></svg>
|
|
</button>
|
|
<button onclick="navigator.clipboard.writeText('{{ $site->appInstallation->database_password }}')" class="text-slate-400 hover:text-slate-600" title="Copy">
|
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
|
|
</button>
|
|
</div>
|
|
@else
|
|
<p class="text-sm text-slate-500 italic">Not available</p>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<div class="mt-4 pt-3 border-t border-slate-200">
|
|
<p class="text-xs text-amber-600 flex items-center gap-1">
|
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
|
|
Keep these credentials secure. We recommend changing your password after first login.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
</div>
|
|
|
|
@push('scripts')
|
|
<script>
|
|
function installationProgress(initialItems, accountId) {
|
|
return {
|
|
installationItems: initialItems.map(item => ({
|
|
...item,
|
|
status: 'installing',
|
|
dismissed: false,
|
|
error_message: null
|
|
})),
|
|
polling: null,
|
|
|
|
init() {
|
|
if (this.installationItems.length === 0) return;
|
|
|
|
// Start polling
|
|
this.poll();
|
|
this.polling = setInterval(() => this.poll(), 2000);
|
|
},
|
|
|
|
async poll() {
|
|
let hasInstalling = false;
|
|
|
|
for (const item of this.installationItems) {
|
|
if (item.status !== 'installing') continue;
|
|
|
|
try {
|
|
const response = await fetch(`/hosting/panel/${accountId}/apps/${item.id}/progress`);
|
|
if (!response.ok) continue;
|
|
|
|
const data = await response.json();
|
|
item.progress = data.progress;
|
|
item.progress_message = data.progress_message;
|
|
|
|
if (data.status === 'active') {
|
|
item.status = 'active';
|
|
item.progress = 100;
|
|
item.progress_message = 'Installation complete!';
|
|
} else if (data.status === 'failed') {
|
|
item.status = 'failed';
|
|
item.error_message = data.error_message;
|
|
} else {
|
|
hasInstalling = true;
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to fetch progress:', e);
|
|
hasInstalling = true;
|
|
}
|
|
}
|
|
|
|
if (!hasInstalling && this.polling) {
|
|
clearInterval(this.polling);
|
|
this.polling = null;
|
|
}
|
|
},
|
|
|
|
destroy() {
|
|
if (this.polling) {
|
|
clearInterval(this.polling);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
</script>
|
|
@endpush
|
|
</x-hosting-panel-layout>
|