Add DirectAdmin migration UI (Phase 1)
This commit is contained in:
732
app/Filament/Admin/Pages/DirectAdminMigration.php
Normal file
732
app/Filament/Admin/Pages/DirectAdminMigration.php
Normal file
@@ -0,0 +1,732 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Admin\Pages;
|
||||
|
||||
use App\Models\ServerImport;
|
||||
use App\Models\ServerImportAccount;
|
||||
use App\Services\Agent\AgentClient;
|
||||
use BackedEnum;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\Concerns\InteractsWithActions;
|
||||
use Filament\Actions\Contracts\HasActions;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Radio;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Page;
|
||||
use Filament\Schemas\Components\Actions as FormActions;
|
||||
use Filament\Schemas\Components\Grid;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Text;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Schemas\Components\View;
|
||||
use Filament\Schemas\Components\Wizard;
|
||||
use Filament\Schemas\Components\Wizard\Step;
|
||||
use Filament\Schemas\Schema;
|
||||
use Illuminate\Contracts\Support\Htmlable;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Livewire\Attributes\Url;
|
||||
|
||||
class DirectAdminMigration extends Page implements HasActions, HasForms
|
||||
{
|
||||
use InteractsWithActions;
|
||||
use InteractsWithForms;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-arrow-down-tray';
|
||||
|
||||
protected static ?string $navigationLabel = null;
|
||||
|
||||
protected static bool $shouldRegisterNavigation = false;
|
||||
|
||||
protected static ?string $slug = 'directadmin-migration';
|
||||
|
||||
protected string $view = 'filament.admin.pages.directadmin-migration';
|
||||
|
||||
#[Url(as: 'directadmin-step')]
|
||||
public ?string $wizardStep = null;
|
||||
|
||||
public bool $step1Complete = false;
|
||||
|
||||
public ?int $importId = null;
|
||||
|
||||
public ?string $name = null;
|
||||
|
||||
public string $importMethod = 'remote_server'; // remote_server|backup_file
|
||||
|
||||
public ?string $remoteHost = null;
|
||||
|
||||
public int $remotePort = 2222;
|
||||
|
||||
public ?string $remoteUser = null;
|
||||
|
||||
public ?string $remotePassword = null;
|
||||
|
||||
public ?string $backupPath = null;
|
||||
|
||||
public bool $importFiles = true;
|
||||
|
||||
public bool $importDatabases = true;
|
||||
|
||||
public bool $importEmails = true;
|
||||
|
||||
public bool $importSsl = true;
|
||||
|
||||
protected ?AgentClient $agent = null;
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('DirectAdmin Migration');
|
||||
}
|
||||
|
||||
public function getTitle(): string|Htmlable
|
||||
{
|
||||
return __('DirectAdmin Migration');
|
||||
}
|
||||
|
||||
public function getSubheading(): ?string
|
||||
{
|
||||
return __('Migrate DirectAdmin accounts into Jabali');
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('startOver')
|
||||
->label(__('Start Over'))
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->color('gray')
|
||||
->requiresConfirmation()
|
||||
->modalHeading(__('Start Over'))
|
||||
->modalDescription(__('This will reset the DirectAdmin migration wizard. Are you sure?'))
|
||||
->action('resetMigration'),
|
||||
];
|
||||
}
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->restoreFromSession();
|
||||
$this->restoreFromImport();
|
||||
}
|
||||
|
||||
protected function getForms(): array
|
||||
{
|
||||
return ['migrationForm'];
|
||||
}
|
||||
|
||||
public function migrationForm(Schema $schema): Schema
|
||||
{
|
||||
return $schema->schema([
|
||||
Wizard::make([
|
||||
$this->getConnectStep(),
|
||||
$this->getSelectAccountsStep(),
|
||||
$this->getConfigureStep(),
|
||||
$this->getMigrateStep(),
|
||||
])
|
||||
->persistStepInQueryString('directadmin-step'),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getConnectStep(): Step
|
||||
{
|
||||
return Step::make(__('Connect'))
|
||||
->id('connect')
|
||||
->icon('heroicon-o-link')
|
||||
->description(__('Connect to DirectAdmin or upload a backup'))
|
||||
->schema([
|
||||
Section::make(__('Source'))
|
||||
->description(__('Choose how you want to migrate DirectAdmin accounts.'))
|
||||
->icon('heroicon-o-server')
|
||||
->schema([
|
||||
Grid::make(['default' => 1, 'sm' => 2])->schema([
|
||||
TextInput::make('name')
|
||||
->label(__('Import Name'))
|
||||
->default(fn (): string => $this->name ?: ('DirectAdmin Import ' . now()->format('Y-m-d H:i')))
|
||||
->maxLength(255)
|
||||
->required(),
|
||||
Radio::make('importMethod')
|
||||
->label(__('Import Method'))
|
||||
->options([
|
||||
'remote_server' => __('Remote Server'),
|
||||
'backup_file' => __('Backup File'),
|
||||
])
|
||||
->default('remote_server')
|
||||
->live(),
|
||||
]),
|
||||
|
||||
Grid::make(['default' => 1, 'sm' => 2])
|
||||
->schema([
|
||||
TextInput::make('remoteHost')
|
||||
->label(__('Host'))
|
||||
->placeholder('directadmin.example.com')
|
||||
->required()
|
||||
->visible(fn (Get $get): bool => $get('importMethod') === 'remote_server'),
|
||||
TextInput::make('remotePort')
|
||||
->label(__('Port'))
|
||||
->numeric()
|
||||
->default(2222)
|
||||
->required()
|
||||
->visible(fn (Get $get): bool => $get('importMethod') === 'remote_server'),
|
||||
TextInput::make('remoteUser')
|
||||
->label(__('Username'))
|
||||
->required()
|
||||
->visible(fn (Get $get): bool => $get('importMethod') === 'remote_server'),
|
||||
TextInput::make('remotePassword')
|
||||
->label(__('Password'))
|
||||
->password()
|
||||
->revealable()
|
||||
->required()
|
||||
->visible(fn (Get $get): bool => $get('importMethod') === 'remote_server'),
|
||||
]),
|
||||
|
||||
FileUpload::make('backupPath')
|
||||
->label(__('DirectAdmin Backup Archive'))
|
||||
->helperText(__('Upload a .tar.gz DirectAdmin backup file.'))
|
||||
->disk('local')
|
||||
->directory('imports/directadmin')
|
||||
->preserveFilenames()
|
||||
->acceptedFileTypes([
|
||||
'application/gzip',
|
||||
'application/x-gzip',
|
||||
'application/x-tar',
|
||||
'application/octet-stream',
|
||||
])
|
||||
->required()
|
||||
->visible(fn (Get $get): bool => $get('importMethod') === 'backup_file'),
|
||||
|
||||
FormActions::make([
|
||||
Action::make('discoverAccounts')
|
||||
->label(__('Discover Accounts'))
|
||||
->icon('heroicon-o-magnifying-glass')
|
||||
->color('primary')
|
||||
->action('discoverAccounts'),
|
||||
])->alignEnd(),
|
||||
]),
|
||||
|
||||
Section::make(__('Discovery'))
|
||||
->description(__('Once accounts are discovered, proceed to select which ones to import.'))
|
||||
->icon('heroicon-o-user-group')
|
||||
->schema([
|
||||
Text::make(__('Discovered accounts will appear in the next step.'))->color('gray'),
|
||||
]),
|
||||
])
|
||||
->afterValidation(function () {
|
||||
$import = $this->getImport();
|
||||
$hasAccounts = $import?->accounts()->exists() ?? false;
|
||||
|
||||
if (! $hasAccounts) {
|
||||
Notification::make()
|
||||
->title(__('No accounts discovered'))
|
||||
->body(__('Click "Discover Accounts" to continue.'))
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
throw new Exception(__('No accounts discovered'));
|
||||
}
|
||||
|
||||
$this->step1Complete = true;
|
||||
$this->saveToSession();
|
||||
});
|
||||
}
|
||||
|
||||
protected function getSelectAccountsStep(): Step
|
||||
{
|
||||
return Step::make(__('Select Accounts'))
|
||||
->id('accounts')
|
||||
->icon('heroicon-o-users')
|
||||
->description(__('Choose which DirectAdmin accounts to migrate'))
|
||||
->schema([
|
||||
Section::make(__('DirectAdmin Accounts'))
|
||||
->description(fn (): string => $this->getAccountsStepDescription())
|
||||
->icon('heroicon-o-user-group')
|
||||
->headerActions([
|
||||
Action::make('refreshAccounts')
|
||||
->label(__('Refresh'))
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->color('gray')
|
||||
->action('refreshAccountsTable'),
|
||||
Action::make('selectAll')
|
||||
->label(__('Select All'))
|
||||
->icon('heroicon-o-check')
|
||||
->color('primary')
|
||||
->action('selectAllAccounts')
|
||||
->visible(fn (): bool => $this->getSelectedAccountsCount() < $this->getDiscoveredAccountsCount()),
|
||||
Action::make('deselectAll')
|
||||
->label(__('Deselect All'))
|
||||
->icon('heroicon-o-x-mark')
|
||||
->color('gray')
|
||||
->action('deselectAllAccounts')
|
||||
->visible(fn (): bool => $this->getSelectedAccountsCount() > 0),
|
||||
])
|
||||
->schema([
|
||||
View::make('filament.admin.pages.directadmin-accounts-table'),
|
||||
]),
|
||||
])
|
||||
->afterValidation(function () {
|
||||
if ($this->getSelectedAccountsCount() === 0) {
|
||||
Notification::make()
|
||||
->title(__('No accounts selected'))
|
||||
->body(__('Please select at least one account to migrate.'))
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
throw new Exception(__('No accounts selected'));
|
||||
}
|
||||
|
||||
$this->saveToSession();
|
||||
});
|
||||
}
|
||||
|
||||
protected function getConfigureStep(): Step
|
||||
{
|
||||
return Step::make(__('Configure'))
|
||||
->id('configure')
|
||||
->icon('heroicon-o-cog')
|
||||
->description(__('Choose what to import and map accounts'))
|
||||
->schema([
|
||||
Section::make(__('What to Import'))
|
||||
->description(__('Select which parts of each account to import.'))
|
||||
->icon('heroicon-o-check-circle')
|
||||
->schema([
|
||||
Grid::make(['default' => 1, 'sm' => 2])->schema([
|
||||
Checkbox::make('importFiles')
|
||||
->label(__('Website Files'))
|
||||
->helperText(__('Restore website files from the backup'))
|
||||
->default(true),
|
||||
Checkbox::make('importDatabases')
|
||||
->label(__('Databases'))
|
||||
->helperText(__('Restore MySQL databases and import dumps'))
|
||||
->default(true),
|
||||
Checkbox::make('importEmails')
|
||||
->label(__('Email'))
|
||||
->helperText(__('Create email domains and mailboxes (limited in Phase 1)'))
|
||||
->default(true),
|
||||
Checkbox::make('importSsl')
|
||||
->label(__('SSL'))
|
||||
->helperText(__('Install custom certificates or issue Let\'s Encrypt (Phase 3)'))
|
||||
->default(true),
|
||||
]),
|
||||
]),
|
||||
|
||||
Section::make(__('Account Mappings'))
|
||||
->description(fn (): string => __(':count account(s) selected', ['count' => $this->getSelectedAccountsCount()]))
|
||||
->icon('heroicon-o-arrow-right')
|
||||
->schema([
|
||||
View::make('filament.admin.pages.directadmin-account-config-table'),
|
||||
]),
|
||||
])
|
||||
->afterValidation(function (): void {
|
||||
$import = $this->getImport();
|
||||
if (! $import) {
|
||||
throw new Exception(__('Import job not found'));
|
||||
}
|
||||
|
||||
$import->update([
|
||||
'import_options' => [
|
||||
'files' => $this->importFiles,
|
||||
'databases' => $this->importDatabases,
|
||||
'emails' => $this->importEmails,
|
||||
'ssl' => $this->importSsl,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->saveToSession();
|
||||
$this->dispatch('directadmin-config-updated');
|
||||
});
|
||||
}
|
||||
|
||||
protected function getMigrateStep(): Step
|
||||
{
|
||||
return Step::make(__('Migrate'))
|
||||
->id('migrate')
|
||||
->icon('heroicon-o-play')
|
||||
->description(__('Run the migration and watch progress'))
|
||||
->schema([
|
||||
FormActions::make([
|
||||
Action::make('startMigration')
|
||||
->label(__('Start Migration'))
|
||||
->icon('heroicon-o-play')
|
||||
->color('success')
|
||||
->requiresConfirmation()
|
||||
->modalHeading(__('Start Migration'))
|
||||
->modalDescription(__('This will migrate :count account(s). Continue?', ['count' => $this->getSelectedAccountsCount()]))
|
||||
->action('startMigration'),
|
||||
|
||||
Action::make('newMigration')
|
||||
->label(__('New Migration'))
|
||||
->icon('heroicon-o-plus')
|
||||
->color('primary')
|
||||
->visible(fn (): bool => ($this->getImport()?->status ?? null) === 'completed')
|
||||
->action('resetMigration'),
|
||||
])->alignEnd(),
|
||||
|
||||
Section::make(__('Import Status'))
|
||||
->icon('heroicon-o-queue-list')
|
||||
->schema([
|
||||
View::make('filament.admin.pages.directadmin-migration-status-table'),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function discoverAccounts(): void
|
||||
{
|
||||
try {
|
||||
$import = $this->upsertImportForDiscovery();
|
||||
|
||||
$backupFullPath = null;
|
||||
$remotePassword = null;
|
||||
|
||||
if ($this->importMethod === 'backup_file') {
|
||||
if (! $import->backup_path) {
|
||||
throw new Exception(__('Please upload a DirectAdmin backup archive.'));
|
||||
}
|
||||
|
||||
$backupFullPath = Storage::disk('local')->path($import->backup_path);
|
||||
} else {
|
||||
$remotePassword = $this->remotePassword;
|
||||
|
||||
if (($remotePassword === null || $remotePassword === '') && filled($import->remote_password)) {
|
||||
$remotePassword = (string) $import->remote_password;
|
||||
}
|
||||
|
||||
if (! $import->remote_host || ! $import->remote_port || ! $import->remote_user || ! $remotePassword) {
|
||||
throw new Exception(__('Please enter DirectAdmin host, port, username and password.'));
|
||||
}
|
||||
}
|
||||
|
||||
$result = $this->getAgent()->importDiscover(
|
||||
$import->id,
|
||||
'directadmin',
|
||||
$import->import_method,
|
||||
$backupFullPath,
|
||||
$import->remote_host,
|
||||
$import->remote_port ? (int) $import->remote_port : null,
|
||||
$import->remote_user,
|
||||
$remotePassword,
|
||||
);
|
||||
|
||||
if (! ($result['success'] ?? false)) {
|
||||
throw new Exception((string) ($result['error'] ?? __('Discovery failed')));
|
||||
}
|
||||
|
||||
$accounts = $result['accounts'] ?? [];
|
||||
if (! is_array($accounts) || $accounts === []) {
|
||||
throw new Exception(__('No accounts were discovered.'));
|
||||
}
|
||||
|
||||
$import->accounts()->delete();
|
||||
$createdIds = [];
|
||||
|
||||
foreach ($accounts as $account) {
|
||||
if (! is_array($account)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$username = trim((string) ($account['username'] ?? ''));
|
||||
if ($username === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$record = ServerImportAccount::create([
|
||||
'server_import_id' => $import->id,
|
||||
'source_username' => $username,
|
||||
'target_username' => $username,
|
||||
'email' => (string) ($account['email'] ?? ''),
|
||||
'main_domain' => (string) ($account['main_domain'] ?? ''),
|
||||
'addon_domains' => $account['addon_domains'] ?? [],
|
||||
'subdomains' => $account['subdomains'] ?? [],
|
||||
'databases' => $account['databases'] ?? [],
|
||||
'email_accounts' => $account['email_accounts'] ?? [],
|
||||
'disk_usage' => (int) ($account['disk_usage'] ?? 0),
|
||||
'status' => 'pending',
|
||||
'progress' => 0,
|
||||
'current_task' => null,
|
||||
'import_log' => [],
|
||||
'error' => null,
|
||||
]);
|
||||
|
||||
$createdIds[] = $record->id;
|
||||
}
|
||||
|
||||
if ($createdIds === []) {
|
||||
throw new Exception(__('No valid accounts were discovered.'));
|
||||
}
|
||||
|
||||
$import->update([
|
||||
'discovered_accounts' => $accounts,
|
||||
'selected_accounts' => [],
|
||||
'status' => 'ready',
|
||||
'progress' => 0,
|
||||
'current_task' => null,
|
||||
'errors' => [],
|
||||
]);
|
||||
|
||||
$this->importId = $import->id;
|
||||
$this->step1Complete = true;
|
||||
$this->saveToSession();
|
||||
|
||||
$this->dispatch('directadmin-accounts-updated');
|
||||
|
||||
Notification::make()
|
||||
->title(__('Accounts discovered'))
|
||||
->body(__('Found :count account(s).', ['count' => count($createdIds)]))
|
||||
->success()
|
||||
->send();
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->title(__('Discovery failed'))
|
||||
->body($e->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
|
||||
public function selectAllAccounts(): void
|
||||
{
|
||||
$import = $this->getImport();
|
||||
if (! $import) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ids = $import->accounts()->pluck('id')->all();
|
||||
$import->update(['selected_accounts' => $ids]);
|
||||
|
||||
$this->dispatch('directadmin-selection-updated');
|
||||
}
|
||||
|
||||
public function deselectAllAccounts(): void
|
||||
{
|
||||
$import = $this->getImport();
|
||||
if (! $import) {
|
||||
return;
|
||||
}
|
||||
|
||||
$import->update(['selected_accounts' => []]);
|
||||
|
||||
$this->dispatch('directadmin-selection-updated');
|
||||
}
|
||||
|
||||
public function refreshAccountsTable(): void
|
||||
{
|
||||
$this->dispatch('directadmin-accounts-updated');
|
||||
$this->dispatch('directadmin-config-updated');
|
||||
}
|
||||
|
||||
public function startMigration(): void
|
||||
{
|
||||
$import = $this->getImport();
|
||||
if (! $import) {
|
||||
Notification::make()
|
||||
->title(__('Import job not found'))
|
||||
->danger()
|
||||
->send();
|
||||
return;
|
||||
}
|
||||
|
||||
$selected = $import->selected_accounts ?? [];
|
||||
if (! is_array($selected) || $selected === []) {
|
||||
Notification::make()
|
||||
->title(__('No accounts selected'))
|
||||
->body(__('Please select at least one account to migrate.'))
|
||||
->danger()
|
||||
->send();
|
||||
return;
|
||||
}
|
||||
|
||||
if ($import->import_method === 'remote_server') {
|
||||
Notification::make()
|
||||
->title(__('Remote DirectAdmin import is not available yet'))
|
||||
->body(__('For now, please download a DirectAdmin backup archive and use the "Backup File" method.'))
|
||||
->warning()
|
||||
->send();
|
||||
return;
|
||||
}
|
||||
|
||||
$import->update([
|
||||
'status' => 'importing',
|
||||
'started_at' => now(),
|
||||
]);
|
||||
|
||||
$result = $this->getAgent()->importStart($import->id);
|
||||
|
||||
if (! ($result['success'] ?? false)) {
|
||||
Notification::make()
|
||||
->title(__('Failed to start migration'))
|
||||
->body((string) ($result['error'] ?? __('Unknown error')))
|
||||
->danger()
|
||||
->send();
|
||||
return;
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title(__('Migration started'))
|
||||
->body(__('Import process has started in the background.'))
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
|
||||
public function resetMigration(): void
|
||||
{
|
||||
if ($this->importId) {
|
||||
ServerImport::whereKey($this->importId)->delete();
|
||||
}
|
||||
|
||||
session()->forget('directadmin_migration.import_id');
|
||||
|
||||
$this->wizardStep = null;
|
||||
$this->step1Complete = false;
|
||||
$this->importId = null;
|
||||
$this->name = null;
|
||||
$this->importMethod = 'remote_server';
|
||||
$this->remoteHost = null;
|
||||
$this->remotePort = 2222;
|
||||
$this->remoteUser = null;
|
||||
$this->remotePassword = null;
|
||||
$this->backupPath = null;
|
||||
$this->importFiles = true;
|
||||
$this->importDatabases = true;
|
||||
$this->importEmails = true;
|
||||
$this->importSsl = true;
|
||||
}
|
||||
|
||||
protected function getAgent(): AgentClient
|
||||
{
|
||||
return $this->agent ??= new AgentClient;
|
||||
}
|
||||
|
||||
protected function getImport(): ?ServerImport
|
||||
{
|
||||
if (! $this->importId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ServerImport::with('accounts')->find($this->importId);
|
||||
}
|
||||
|
||||
protected function upsertImportForDiscovery(): ServerImport
|
||||
{
|
||||
$name = trim((string) ($this->name ?: ''));
|
||||
if ($name === '') {
|
||||
$name = 'DirectAdmin Import ' . now()->format('Y-m-d H:i');
|
||||
}
|
||||
|
||||
$attributes = [
|
||||
'name' => $name,
|
||||
'source_type' => 'directadmin',
|
||||
'import_method' => $this->importMethod,
|
||||
'import_options' => [
|
||||
'files' => $this->importFiles,
|
||||
'databases' => $this->importDatabases,
|
||||
'emails' => $this->importEmails,
|
||||
'ssl' => $this->importSsl,
|
||||
],
|
||||
'status' => 'discovering',
|
||||
'progress' => 0,
|
||||
'current_task' => null,
|
||||
];
|
||||
|
||||
if ($this->importMethod === 'backup_file') {
|
||||
$attributes['backup_path'] = $this->backupPath;
|
||||
$attributes['remote_host'] = null;
|
||||
$attributes['remote_port'] = null;
|
||||
$attributes['remote_user'] = null;
|
||||
} else {
|
||||
$attributes['backup_path'] = null;
|
||||
$attributes['remote_host'] = $this->remoteHost ? trim($this->remoteHost) : null;
|
||||
$attributes['remote_port'] = $this->remotePort;
|
||||
$attributes['remote_user'] = $this->remoteUser ? trim($this->remoteUser) : null;
|
||||
|
||||
if (filled($this->remotePassword)) {
|
||||
$attributes['remote_password'] = $this->remotePassword;
|
||||
}
|
||||
}
|
||||
|
||||
$import = $this->importId ? ServerImport::find($this->importId) : null;
|
||||
|
||||
if ($import) {
|
||||
$import->update($attributes);
|
||||
} else {
|
||||
$import = ServerImport::create($attributes);
|
||||
$this->importId = $import->id;
|
||||
}
|
||||
|
||||
$this->saveToSession();
|
||||
|
||||
return $import->fresh();
|
||||
}
|
||||
|
||||
protected function getDiscoveredAccountsCount(): int
|
||||
{
|
||||
$import = $this->getImport();
|
||||
|
||||
return $import ? $import->accounts()->count() : 0;
|
||||
}
|
||||
|
||||
protected function getSelectedAccountsCount(): int
|
||||
{
|
||||
$import = $this->getImport();
|
||||
$selected = $import?->selected_accounts ?? [];
|
||||
|
||||
return is_array($selected) ? count($selected) : 0;
|
||||
}
|
||||
|
||||
protected function getAccountsStepDescription(): string
|
||||
{
|
||||
$selected = $this->getSelectedAccountsCount();
|
||||
$total = $this->getDiscoveredAccountsCount();
|
||||
|
||||
if ($total === 0) {
|
||||
return __('No accounts discovered yet.');
|
||||
}
|
||||
|
||||
if ($selected === 0) {
|
||||
return __(':count accounts discovered', ['count' => $total]);
|
||||
}
|
||||
|
||||
return __(':selected of :count accounts selected', ['selected' => $selected, 'count' => $total]);
|
||||
}
|
||||
|
||||
protected function saveToSession(): void
|
||||
{
|
||||
if ($this->importId) {
|
||||
session()->put('directadmin_migration.import_id', $this->importId);
|
||||
}
|
||||
|
||||
session()->save();
|
||||
}
|
||||
|
||||
protected function restoreFromSession(): void
|
||||
{
|
||||
$this->importId = session('directadmin_migration.import_id');
|
||||
}
|
||||
|
||||
protected function restoreFromImport(): void
|
||||
{
|
||||
$import = $this->getImport();
|
||||
if (! $import) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->name = $import->name;
|
||||
$this->importMethod = (string) ($import->import_method ?? 'remote_server');
|
||||
$this->backupPath = $import->backup_path;
|
||||
$this->remoteHost = $import->remote_host;
|
||||
$this->remotePort = (int) ($import->remote_port ?? 2222);
|
||||
$this->remoteUser = $import->remote_user;
|
||||
|
||||
$options = $import->import_options ?? [];
|
||||
if (is_array($options)) {
|
||||
$this->importFiles = (bool) ($options['files'] ?? true);
|
||||
$this->importDatabases = (bool) ($options['databases'] ?? true);
|
||||
$this->importEmails = (bool) ($options['emails'] ?? true);
|
||||
$this->importSsl = (bool) ($options['ssl'] ?? true);
|
||||
}
|
||||
|
||||
$this->step1Complete = $import->accounts()->exists();
|
||||
}
|
||||
}
|
||||
@@ -41,19 +41,19 @@ class Migration extends Page implements HasForms
|
||||
|
||||
public function getSubheading(): ?string
|
||||
{
|
||||
return __('Migrate cPanel accounts directly or via WHM');
|
||||
return __('Migrate cPanel, WHM, or DirectAdmin accounts into Jabali');
|
||||
}
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
if (! in_array($this->activeTab, ['cpanel', 'whm'], true)) {
|
||||
if (! in_array($this->activeTab, ['cpanel', 'whm', 'directadmin'], true)) {
|
||||
$this->activeTab = 'cpanel';
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedActiveTab(string $activeTab): void
|
||||
{
|
||||
if (! in_array($activeTab, ['cpanel', 'whm'], true)) {
|
||||
if (! in_array($activeTab, ['cpanel', 'whm', 'directadmin'], true)) {
|
||||
$this->activeTab = 'cpanel';
|
||||
}
|
||||
}
|
||||
@@ -79,6 +79,11 @@ class Migration extends Page implements HasForms
|
||||
->schema([
|
||||
View::make('filament.admin.pages.migration-whm-tab'),
|
||||
]),
|
||||
'directadmin' => Tabs\Tab::make(__('DirectAdmin Migration'))
|
||||
->icon('heroicon-o-arrow-down-tray')
|
||||
->schema([
|
||||
View::make('filament.admin.pages.migration-directadmin-tab'),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
142
app/Filament/Admin/Widgets/DirectAdminAccountConfigTable.php
Normal file
142
app/Filament/Admin/Widgets/DirectAdminAccountConfigTable.php
Normal file
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Admin\Widgets;
|
||||
|
||||
use App\Models\ServerImport;
|
||||
use App\Models\ServerImportAccount;
|
||||
use App\Models\User;
|
||||
use Filament\Actions\Concerns\InteractsWithActions;
|
||||
use Filament\Actions\Contracts\HasActions;
|
||||
use Filament\Schemas\Concerns\InteractsWithSchemas;
|
||||
use Filament\Schemas\Contracts\HasSchemas;
|
||||
use Filament\Support\Contracts\TranslatableContentDriver;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\TextInputColumn;
|
||||
use Filament\Tables\Concerns\InteractsWithTable;
|
||||
use Filament\Tables\Contracts\HasTable;
|
||||
use Filament\Tables\Table;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
class DirectAdminAccountConfigTable extends Component implements HasActions, HasSchemas, HasTable
|
||||
{
|
||||
use InteractsWithActions;
|
||||
use InteractsWithSchemas;
|
||||
use InteractsWithTable;
|
||||
|
||||
public ?int $importId = null;
|
||||
|
||||
public function mount(?int $importId = null): void
|
||||
{
|
||||
$this->importId = $importId ?: session('directadmin_migration.import_id');
|
||||
}
|
||||
|
||||
#[On('directadmin-config-updated')]
|
||||
#[On('directadmin-selection-updated')]
|
||||
public function refreshConfig(): void
|
||||
{
|
||||
$this->resetTable();
|
||||
}
|
||||
|
||||
public function makeFilamentTranslatableContentDriver(): ?TranslatableContentDriver
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function getImport(): ?ServerImport
|
||||
{
|
||||
if (! $this->importId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ServerImport::find($this->importId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int>
|
||||
*/
|
||||
protected function getSelectedAccountIds(): array
|
||||
{
|
||||
$selected = $this->getImport()?->selected_accounts ?? [];
|
||||
|
||||
return array_values(array_filter(array_map('intval', is_array($selected) ? $selected : [])));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Support\Collection<int, ServerImportAccount>
|
||||
*/
|
||||
protected function getRecords()
|
||||
{
|
||||
if (! $this->importId) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$ids = $this->getSelectedAccountIds();
|
||||
if ($ids === []) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return ServerImportAccount::query()
|
||||
->where('server_import_id', $this->importId)
|
||||
->whereIn('id', $ids)
|
||||
->orderBy('source_username')
|
||||
->get();
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->records(fn () => $this->getRecords())
|
||||
->columns([
|
||||
IconColumn::make('target_user_exists')
|
||||
->label(__('User'))
|
||||
->boolean()
|
||||
->trueIcon('heroicon-o-exclamation-triangle')
|
||||
->falseIcon('heroicon-o-user-plus')
|
||||
->trueColor('warning')
|
||||
->falseColor('success')
|
||||
->tooltip(fn (ServerImportAccount $record): string => User::where('username', $record->target_username)->exists()
|
||||
? __('User exists - migration will restore into the existing account')
|
||||
: __('New user will be created'))
|
||||
->getStateUsing(fn (ServerImportAccount $record): bool => User::where('username', $record->target_username)->exists()),
|
||||
TextColumn::make('source_username')
|
||||
->label(__('Source'))
|
||||
->weight('bold'),
|
||||
TextColumn::make('main_domain')
|
||||
->label(__('Main Domain'))
|
||||
->wrap(),
|
||||
TextInputColumn::make('target_username')
|
||||
->label(__('Target Username'))
|
||||
->rules([
|
||||
'required',
|
||||
'max:32',
|
||||
'regex:/^[a-z0-9_]+$/i',
|
||||
]),
|
||||
TextInputColumn::make('email')
|
||||
->label(__('Email'))
|
||||
->rules([
|
||||
'nullable',
|
||||
'email',
|
||||
'max:255',
|
||||
]),
|
||||
TextColumn::make('formatted_disk_usage')
|
||||
->label(__('Disk'))
|
||||
->toggleable(),
|
||||
])
|
||||
->striped()
|
||||
->paginated([10, 25, 50])
|
||||
->defaultPaginationPageOption(10)
|
||||
->emptyStateHeading(__('No accounts selected'))
|
||||
->emptyStateDescription(__('Go back and select accounts to migrate.'))
|
||||
->emptyStateIcon('heroicon-o-user-group');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return $this->getTable()->render();
|
||||
}
|
||||
}
|
||||
|
||||
155
app/Filament/Admin/Widgets/DirectAdminAccountsTable.php
Normal file
155
app/Filament/Admin/Widgets/DirectAdminAccountsTable.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Admin\Widgets;
|
||||
|
||||
use App\Models\ServerImport;
|
||||
use App\Models\ServerImportAccount;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\Concerns\InteractsWithActions;
|
||||
use Filament\Actions\Contracts\HasActions;
|
||||
use Filament\Schemas\Concerns\InteractsWithSchemas;
|
||||
use Filament\Schemas\Contracts\HasSchemas;
|
||||
use Filament\Support\Contracts\TranslatableContentDriver;
|
||||
use Filament\Support\Enums\IconSize;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Concerns\InteractsWithTable;
|
||||
use Filament\Tables\Contracts\HasTable;
|
||||
use Filament\Tables\Table;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
class DirectAdminAccountsTable extends Component implements HasActions, HasSchemas, HasTable
|
||||
{
|
||||
use InteractsWithActions;
|
||||
use InteractsWithSchemas;
|
||||
use InteractsWithTable;
|
||||
|
||||
public ?int $importId = null;
|
||||
|
||||
public function mount(?int $importId = null): void
|
||||
{
|
||||
$this->importId = $importId ?: session('directadmin_migration.import_id');
|
||||
}
|
||||
|
||||
#[On('directadmin-accounts-updated')]
|
||||
#[On('directadmin-selection-updated')]
|
||||
public function refreshAccounts(): void
|
||||
{
|
||||
$this->resetTable();
|
||||
}
|
||||
|
||||
public function makeFilamentTranslatableContentDriver(): ?TranslatableContentDriver
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function getImport(): ?ServerImport
|
||||
{
|
||||
if (! $this->importId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ServerImport::find($this->importId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int>
|
||||
*/
|
||||
protected function getSelectedAccountIds(): array
|
||||
{
|
||||
$selected = $this->getImport()?->selected_accounts ?? [];
|
||||
|
||||
return array_values(array_filter(array_map('intval', is_array($selected) ? $selected : [])));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Support\Collection<int, ServerImportAccount>
|
||||
*/
|
||||
protected function getRecords()
|
||||
{
|
||||
if (! $this->importId) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return ServerImportAccount::query()
|
||||
->where('server_import_id', $this->importId)
|
||||
->orderBy('source_username')
|
||||
->get();
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->records(fn () => $this->getRecords())
|
||||
->columns([
|
||||
IconColumn::make('is_selected')
|
||||
->label('')
|
||||
->boolean()
|
||||
->trueIcon('heroicon-s-check-circle')
|
||||
->falseIcon('heroicon-o-minus-circle')
|
||||
->trueColor('primary')
|
||||
->falseColor('gray')
|
||||
->size(IconSize::Medium)
|
||||
->getStateUsing(fn (ServerImportAccount $record): bool => in_array($record->id, $this->getSelectedAccountIds(), true)),
|
||||
TextColumn::make('source_username')
|
||||
->label(__('Username'))
|
||||
->weight('bold')
|
||||
->searchable(),
|
||||
TextColumn::make('main_domain')
|
||||
->label(__('Main Domain'))
|
||||
->wrap()
|
||||
->searchable(),
|
||||
TextColumn::make('email')
|
||||
->label(__('Email'))
|
||||
->icon('heroicon-o-envelope')
|
||||
->toggleable()
|
||||
->wrap(),
|
||||
TextColumn::make('formatted_disk_usage')
|
||||
->label(__('Disk'))
|
||||
->toggleable(),
|
||||
])
|
||||
->recordAction('toggleSelection')
|
||||
->actions([
|
||||
Action::make('toggleSelection')
|
||||
->label(fn (ServerImportAccount $record): string => in_array($record->id, $this->getSelectedAccountIds(), true) ? __('Deselect') : __('Select'))
|
||||
->icon(fn (ServerImportAccount $record): string => in_array($record->id, $this->getSelectedAccountIds(), true) ? 'heroicon-o-x-mark' : 'heroicon-o-check')
|
||||
->color(fn (ServerImportAccount $record): string => in_array($record->id, $this->getSelectedAccountIds(), true) ? 'gray' : 'primary')
|
||||
->action(function (ServerImportAccount $record): void {
|
||||
$import = $this->getImport();
|
||||
if (! $import) {
|
||||
return;
|
||||
}
|
||||
|
||||
$selected = $this->getSelectedAccountIds();
|
||||
|
||||
if (in_array($record->id, $selected, true)) {
|
||||
$selected = array_values(array_diff($selected, [$record->id]));
|
||||
} else {
|
||||
$selected[] = $record->id;
|
||||
$selected = array_values(array_unique($selected));
|
||||
}
|
||||
|
||||
$import->update(['selected_accounts' => $selected]);
|
||||
|
||||
$this->dispatch('directadmin-selection-updated');
|
||||
$this->resetTable();
|
||||
}),
|
||||
])
|
||||
->striped()
|
||||
->paginated([10, 25, 50])
|
||||
->defaultPaginationPageOption(25)
|
||||
->emptyStateHeading(__('No accounts found'))
|
||||
->emptyStateDescription(__('Discover accounts to see them here.'))
|
||||
->emptyStateIcon('heroicon-o-user-group')
|
||||
->poll(null);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return $this->getTable()->render();
|
||||
}
|
||||
}
|
||||
|
||||
186
app/Filament/Admin/Widgets/DirectAdminMigrationStatusTable.php
Normal file
186
app/Filament/Admin/Widgets/DirectAdminMigrationStatusTable.php
Normal file
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Admin\Widgets;
|
||||
|
||||
use App\Models\ServerImport;
|
||||
use App\Models\ServerImportAccount;
|
||||
use Filament\Actions\Concerns\InteractsWithActions;
|
||||
use Filament\Actions\Contracts\HasActions;
|
||||
use Filament\Schemas\Concerns\InteractsWithSchemas;
|
||||
use Filament\Schemas\Contracts\HasSchemas;
|
||||
use Filament\Support\Contracts\TranslatableContentDriver;
|
||||
use Filament\Support\Enums\FontWeight;
|
||||
use Filament\Support\Enums\IconSize;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Concerns\InteractsWithTable;
|
||||
use Filament\Tables\Contracts\HasTable;
|
||||
use Filament\Tables\Table;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
class DirectAdminMigrationStatusTable extends Component implements HasActions, HasSchemas, HasTable
|
||||
{
|
||||
use InteractsWithActions;
|
||||
use InteractsWithSchemas;
|
||||
use InteractsWithTable;
|
||||
|
||||
public ?int $importId = null;
|
||||
|
||||
public function mount(?int $importId = null): void
|
||||
{
|
||||
$this->importId = $importId ?: session('directadmin_migration.import_id');
|
||||
}
|
||||
|
||||
#[On('directadmin-selection-updated')]
|
||||
public function refreshStatus(): void
|
||||
{
|
||||
$this->resetTable();
|
||||
}
|
||||
|
||||
public function makeFilamentTranslatableContentDriver(): ?TranslatableContentDriver
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function getImport(): ?ServerImport
|
||||
{
|
||||
if (! $this->importId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ServerImport::find($this->importId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int>
|
||||
*/
|
||||
protected function getSelectedAccountIds(): array
|
||||
{
|
||||
$selected = $this->getImport()?->selected_accounts ?? [];
|
||||
|
||||
return array_values(array_filter(array_map('intval', is_array($selected) ? $selected : [])));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Support\Collection<int, ServerImportAccount>
|
||||
*/
|
||||
protected function getRecords()
|
||||
{
|
||||
if (! $this->importId) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$ids = $this->getSelectedAccountIds();
|
||||
if ($ids === []) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return ServerImportAccount::query()
|
||||
->where('server_import_id', $this->importId)
|
||||
->whereIn('id', $ids)
|
||||
->orderBy('source_username')
|
||||
->get();
|
||||
}
|
||||
|
||||
protected function shouldPoll(): bool
|
||||
{
|
||||
$import = $this->getImport();
|
||||
if (! $import) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (in_array($import->status, ['discovering', 'importing'], true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($this->getRecords() as $record) {
|
||||
if (! in_array($record->status, ['completed', 'failed', 'skipped'], true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function getStatusText(string $status): string
|
||||
{
|
||||
return match ($status) {
|
||||
'pending' => __('Waiting...'),
|
||||
'importing' => __('Importing...'),
|
||||
'completed' => __('Completed'),
|
||||
'failed' => __('Failed'),
|
||||
'skipped' => __('Skipped'),
|
||||
default => __('Unknown'),
|
||||
};
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->records(fn () => $this->getRecords())
|
||||
->columns([
|
||||
IconColumn::make('status_icon')
|
||||
->label('')
|
||||
->icon(fn (ServerImportAccount $record): string => match ($record->status) {
|
||||
'pending' => 'heroicon-o-clock',
|
||||
'importing' => 'heroicon-o-arrow-path',
|
||||
'completed' => 'heroicon-o-check-circle',
|
||||
'failed' => 'heroicon-o-x-circle',
|
||||
'skipped' => 'heroicon-o-minus-circle',
|
||||
default => 'heroicon-o-question-mark-circle',
|
||||
})
|
||||
->color(fn (ServerImportAccount $record): string => match ($record->status) {
|
||||
'pending' => 'gray',
|
||||
'importing' => 'warning',
|
||||
'completed' => 'success',
|
||||
'failed' => 'danger',
|
||||
'skipped' => 'gray',
|
||||
default => 'gray',
|
||||
})
|
||||
->size(IconSize::Small)
|
||||
->extraAttributes(fn (ServerImportAccount $record): array => $record->status === 'importing'
|
||||
? ['class' => 'animate-spin']
|
||||
: []),
|
||||
TextColumn::make('source_username')
|
||||
->label(__('Account'))
|
||||
->weight(FontWeight::Bold)
|
||||
->searchable(),
|
||||
TextColumn::make('status')
|
||||
->label(__('Status'))
|
||||
->badge()
|
||||
->formatStateUsing(fn (string $state): string => $this->getStatusText($state))
|
||||
->color(fn (ServerImportAccount $record): string => match ($record->status) {
|
||||
'pending' => 'gray',
|
||||
'importing' => 'warning',
|
||||
'completed' => 'success',
|
||||
'failed' => 'danger',
|
||||
'skipped' => 'gray',
|
||||
default => 'gray',
|
||||
}),
|
||||
TextColumn::make('current_task')
|
||||
->label(__('Current Task'))
|
||||
->wrap()
|
||||
->limit(80)
|
||||
->default(__('Waiting...')),
|
||||
TextColumn::make('progress')
|
||||
->label(__('Progress'))
|
||||
->suffix('%')
|
||||
->toggleable(),
|
||||
])
|
||||
->striped()
|
||||
->paginated(false)
|
||||
->poll($this->shouldPoll() ? '3s' : null)
|
||||
->emptyStateHeading(__('No selected accounts'))
|
||||
->emptyStateDescription(__('Select accounts and start migration.'))
|
||||
->emptyStateIcon('heroicon-o-queue-list');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return $this->getTable()->render();
|
||||
}
|
||||
}
|
||||
|
||||
615
app/Filament/Jabali/Pages/DirectAdminMigration.php
Normal file
615
app/Filament/Jabali/Pages/DirectAdminMigration.php
Normal file
@@ -0,0 +1,615 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Jabali\Pages;
|
||||
|
||||
use App\Models\ServerImport;
|
||||
use App\Models\ServerImportAccount;
|
||||
use App\Services\Agent\AgentClient;
|
||||
use BackedEnum;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\Concerns\InteractsWithActions;
|
||||
use Filament\Actions\Contracts\HasActions;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Radio;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Page;
|
||||
use Filament\Schemas\Components\Actions as FormActions;
|
||||
use Filament\Schemas\Components\Grid;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Text;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Schemas\Components\View;
|
||||
use Filament\Schemas\Components\Wizard;
|
||||
use Filament\Schemas\Components\Wizard\Step;
|
||||
use Filament\Schemas\Schema;
|
||||
use Illuminate\Contracts\Support\Htmlable;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Livewire\Attributes\Url;
|
||||
|
||||
class DirectAdminMigration extends Page implements HasActions, HasForms
|
||||
{
|
||||
use InteractsWithActions;
|
||||
use InteractsWithForms;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-arrow-down-tray';
|
||||
|
||||
protected static ?string $navigationLabel = null;
|
||||
|
||||
protected static ?int $navigationSort = 16;
|
||||
|
||||
protected static ?string $slug = 'directadmin-migration';
|
||||
|
||||
protected string $view = 'filament.jabali.pages.directadmin-migration';
|
||||
|
||||
#[Url(as: 'directadmin-step')]
|
||||
public ?string $wizardStep = null;
|
||||
|
||||
public bool $step1Complete = false;
|
||||
|
||||
public ?int $importId = null;
|
||||
|
||||
public string $importMethod = 'backup_file'; // remote_server|backup_file
|
||||
|
||||
public ?string $remoteHost = null;
|
||||
|
||||
public int $remotePort = 2222;
|
||||
|
||||
public ?string $remoteUser = null;
|
||||
|
||||
public ?string $remotePassword = null;
|
||||
|
||||
public ?string $backupPath = null;
|
||||
|
||||
public bool $importFiles = true;
|
||||
|
||||
public bool $importDatabases = true;
|
||||
|
||||
public bool $importEmails = true;
|
||||
|
||||
public bool $importSsl = true;
|
||||
|
||||
protected ?AgentClient $agent = null;
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('DirectAdmin Migration');
|
||||
}
|
||||
|
||||
public function getTitle(): string|Htmlable
|
||||
{
|
||||
return __('DirectAdmin Migration');
|
||||
}
|
||||
|
||||
public function getSubheading(): ?string
|
||||
{
|
||||
return __('Migrate your DirectAdmin account into your Jabali account');
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('startOver')
|
||||
->label(__('Start Over'))
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->color('gray')
|
||||
->requiresConfirmation()
|
||||
->modalHeading(__('Start Over'))
|
||||
->modalDescription(__('This will reset the DirectAdmin migration wizard. Are you sure?'))
|
||||
->action('resetMigration'),
|
||||
];
|
||||
}
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->restoreFromSession();
|
||||
$this->restoreFromImport();
|
||||
}
|
||||
|
||||
protected function getForms(): array
|
||||
{
|
||||
return ['migrationForm'];
|
||||
}
|
||||
|
||||
public function migrationForm(Schema $schema): Schema
|
||||
{
|
||||
return $schema->schema([
|
||||
Wizard::make([
|
||||
$this->getConnectStep(),
|
||||
$this->getConfigureStep(),
|
||||
$this->getMigrateStep(),
|
||||
])
|
||||
->persistStepInQueryString('directadmin-step'),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getConnectStep(): Step
|
||||
{
|
||||
return Step::make(__('Connect'))
|
||||
->id('connect')
|
||||
->icon('heroicon-o-link')
|
||||
->description(__('Connect to DirectAdmin or upload a backup'))
|
||||
->schema([
|
||||
Section::make(__('Source'))
|
||||
->description(__('For now, migration requires a DirectAdmin backup archive. Remote migration will be added next.'))
|
||||
->icon('heroicon-o-server')
|
||||
->schema([
|
||||
Radio::make('importMethod')
|
||||
->label(__('Import Method'))
|
||||
->options([
|
||||
'backup_file' => __('Backup File'),
|
||||
'remote_server' => __('Remote Server (Discovery only)'),
|
||||
])
|
||||
->default('backup_file')
|
||||
->live(),
|
||||
|
||||
Grid::make(['default' => 1, 'sm' => 2])
|
||||
->schema([
|
||||
TextInput::make('remoteHost')
|
||||
->label(__('Host'))
|
||||
->placeholder('directadmin.example.com')
|
||||
->required()
|
||||
->visible(fn (Get $get): bool => $get('importMethod') === 'remote_server'),
|
||||
TextInput::make('remotePort')
|
||||
->label(__('Port'))
|
||||
->numeric()
|
||||
->default(2222)
|
||||
->required()
|
||||
->visible(fn (Get $get): bool => $get('importMethod') === 'remote_server'),
|
||||
TextInput::make('remoteUser')
|
||||
->label(__('Username'))
|
||||
->required()
|
||||
->visible(fn (Get $get): bool => $get('importMethod') === 'remote_server'),
|
||||
TextInput::make('remotePassword')
|
||||
->label(__('Password'))
|
||||
->password()
|
||||
->revealable()
|
||||
->required()
|
||||
->visible(fn (Get $get): bool => $get('importMethod') === 'remote_server'),
|
||||
]),
|
||||
|
||||
FileUpload::make('backupPath')
|
||||
->label(__('DirectAdmin Backup Archive'))
|
||||
->helperText(__('Upload a .tar.gz DirectAdmin backup file.'))
|
||||
->disk('local')
|
||||
->directory('imports/directadmin')
|
||||
->preserveFilenames()
|
||||
->acceptedFileTypes([
|
||||
'application/gzip',
|
||||
'application/x-gzip',
|
||||
'application/x-tar',
|
||||
'application/octet-stream',
|
||||
])
|
||||
->required()
|
||||
->visible(fn (Get $get): bool => $get('importMethod') === 'backup_file'),
|
||||
|
||||
FormActions::make([
|
||||
Action::make('discoverAccount')
|
||||
->label(__('Discover Account'))
|
||||
->icon('heroicon-o-magnifying-glass')
|
||||
->color('primary')
|
||||
->action('discoverAccount'),
|
||||
])->alignEnd(),
|
||||
]),
|
||||
|
||||
Section::make(__('Discovery'))
|
||||
->description(__('After discovery, you can choose what to import.'))
|
||||
->icon('heroicon-o-user')
|
||||
->schema([
|
||||
Text::make(__('Discovered account details will be used for migration.'))->color('gray'),
|
||||
]),
|
||||
])
|
||||
->afterValidation(function () {
|
||||
$import = $this->getImport();
|
||||
$hasAccounts = $import?->accounts()->exists() ?? false;
|
||||
|
||||
if (! $hasAccounts) {
|
||||
Notification::make()
|
||||
->title(__('No account discovered'))
|
||||
->body(__('Click "Discover Account" to continue.'))
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
throw new Exception(__('No account discovered'));
|
||||
}
|
||||
|
||||
$this->step1Complete = true;
|
||||
$this->saveToSession();
|
||||
});
|
||||
}
|
||||
|
||||
protected function getConfigureStep(): Step
|
||||
{
|
||||
return Step::make(__('Configure'))
|
||||
->id('configure')
|
||||
->icon('heroicon-o-cog')
|
||||
->description(__('Choose what to import'))
|
||||
->schema([
|
||||
Section::make(__('What to Import'))
|
||||
->description(__('Select which parts of your account to import.'))
|
||||
->icon('heroicon-o-check-circle')
|
||||
->schema([
|
||||
Grid::make(['default' => 1, 'sm' => 2])->schema([
|
||||
Checkbox::make('importFiles')
|
||||
->label(__('Website Files'))
|
||||
->helperText(__('Restore website files from the backup'))
|
||||
->default(true),
|
||||
Checkbox::make('importDatabases')
|
||||
->label(__('Databases'))
|
||||
->helperText(__('Restore MySQL databases and import dumps'))
|
||||
->default(true),
|
||||
Checkbox::make('importEmails')
|
||||
->label(__('Email'))
|
||||
->helperText(__('Create email domains and mailboxes (limited in Phase 1)'))
|
||||
->default(true),
|
||||
Checkbox::make('importSsl')
|
||||
->label(__('SSL'))
|
||||
->helperText(__('Install custom certificates or issue Let\'s Encrypt (Phase 3)'))
|
||||
->default(true),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
->afterValidation(function (): void {
|
||||
$import = $this->getImport();
|
||||
if (! $import) {
|
||||
throw new Exception(__('Import job not found'));
|
||||
}
|
||||
|
||||
$import->update([
|
||||
'import_options' => [
|
||||
'files' => $this->importFiles,
|
||||
'databases' => $this->importDatabases,
|
||||
'emails' => $this->importEmails,
|
||||
'ssl' => $this->importSsl,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->saveToSession();
|
||||
});
|
||||
}
|
||||
|
||||
protected function getMigrateStep(): Step
|
||||
{
|
||||
return Step::make(__('Migrate'))
|
||||
->id('migrate')
|
||||
->icon('heroicon-o-play')
|
||||
->description(__('Run the migration and watch progress'))
|
||||
->schema([
|
||||
FormActions::make([
|
||||
Action::make('startMigration')
|
||||
->label(__('Start Migration'))
|
||||
->icon('heroicon-o-play')
|
||||
->color('success')
|
||||
->requiresConfirmation()
|
||||
->modalHeading(__('Start Migration'))
|
||||
->modalDescription(__('This will import data into your Jabali account. Continue?'))
|
||||
->action('startMigration'),
|
||||
|
||||
Action::make('newMigration')
|
||||
->label(__('New Migration'))
|
||||
->icon('heroicon-o-plus')
|
||||
->color('primary')
|
||||
->visible(fn (): bool => ($this->getImport()?->status ?? null) === 'completed')
|
||||
->action('resetMigration'),
|
||||
])->alignEnd(),
|
||||
|
||||
Section::make(__('Import Status'))
|
||||
->icon('heroicon-o-queue-list')
|
||||
->schema([
|
||||
View::make('filament.jabali.pages.directadmin-migration-status-table'),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function discoverAccount(): void
|
||||
{
|
||||
try {
|
||||
$user = Auth::user();
|
||||
if (! $user) {
|
||||
throw new Exception(__('You must be logged in.'));
|
||||
}
|
||||
|
||||
$import = $this->upsertImportForDiscovery();
|
||||
|
||||
$backupFullPath = null;
|
||||
$remotePassword = null;
|
||||
|
||||
if ($this->importMethod === 'backup_file') {
|
||||
if (! $import->backup_path) {
|
||||
throw new Exception(__('Please upload a DirectAdmin backup archive.'));
|
||||
}
|
||||
|
||||
$backupFullPath = Storage::disk('local')->path($import->backup_path);
|
||||
} else {
|
||||
$remotePassword = $this->remotePassword;
|
||||
|
||||
if (($remotePassword === null || $remotePassword === '') && filled($import->remote_password)) {
|
||||
$remotePassword = (string) $import->remote_password;
|
||||
}
|
||||
|
||||
if (! $import->remote_host || ! $import->remote_port || ! $import->remote_user || ! $remotePassword) {
|
||||
throw new Exception(__('Please enter DirectAdmin host, port, username and password.'));
|
||||
}
|
||||
}
|
||||
|
||||
$result = $this->getAgent()->importDiscover(
|
||||
$import->id,
|
||||
'directadmin',
|
||||
$import->import_method,
|
||||
$backupFullPath,
|
||||
$import->remote_host,
|
||||
$import->remote_port ? (int) $import->remote_port : null,
|
||||
$import->remote_user,
|
||||
$remotePassword,
|
||||
);
|
||||
|
||||
if (! ($result['success'] ?? false)) {
|
||||
throw new Exception((string) ($result['error'] ?? __('Discovery failed')));
|
||||
}
|
||||
|
||||
$accounts = $result['accounts'] ?? [];
|
||||
if (! is_array($accounts) || $accounts === []) {
|
||||
throw new Exception(__('No account was discovered.'));
|
||||
}
|
||||
|
||||
$account = null;
|
||||
if (count($accounts) === 1) {
|
||||
$account = $accounts[0];
|
||||
} else {
|
||||
// Prefer matching the provided username if multiple accounts are returned.
|
||||
foreach ($accounts as $candidate) {
|
||||
if (! is_array($candidate)) {
|
||||
continue;
|
||||
}
|
||||
if (($candidate['username'] ?? null) === $this->remoteUser) {
|
||||
$account = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (! is_array($account)) {
|
||||
throw new Exception(__('Multiple accounts were discovered. Please upload a single-user backup archive.'));
|
||||
}
|
||||
|
||||
$sourceUsername = trim((string) ($account['username'] ?? ''));
|
||||
if ($sourceUsername === '') {
|
||||
throw new Exception(__('Discovered account is missing a username.'));
|
||||
}
|
||||
|
||||
$import->accounts()->delete();
|
||||
|
||||
$record = ServerImportAccount::create([
|
||||
'server_import_id' => $import->id,
|
||||
'source_username' => $sourceUsername,
|
||||
'target_username' => $user->username,
|
||||
'email' => (string) ($account['email'] ?? ''),
|
||||
'main_domain' => (string) ($account['main_domain'] ?? ''),
|
||||
'addon_domains' => $account['addon_domains'] ?? [],
|
||||
'subdomains' => $account['subdomains'] ?? [],
|
||||
'databases' => $account['databases'] ?? [],
|
||||
'email_accounts' => $account['email_accounts'] ?? [],
|
||||
'disk_usage' => (int) ($account['disk_usage'] ?? 0),
|
||||
'status' => 'pending',
|
||||
'progress' => 0,
|
||||
'current_task' => null,
|
||||
'import_log' => [],
|
||||
'error' => null,
|
||||
]);
|
||||
|
||||
$import->update([
|
||||
'discovered_accounts' => [$account],
|
||||
'selected_accounts' => [$record->id],
|
||||
'status' => 'ready',
|
||||
'progress' => 0,
|
||||
'current_task' => null,
|
||||
'errors' => [],
|
||||
]);
|
||||
|
||||
$this->importId = $import->id;
|
||||
$this->step1Complete = true;
|
||||
$this->saveToSession();
|
||||
|
||||
$this->dispatch('directadmin-self-status-updated');
|
||||
|
||||
Notification::make()
|
||||
->title(__('Account discovered'))
|
||||
->body(__('Ready to migrate into your Jabali account (:username).', ['username' => $user->username]))
|
||||
->success()
|
||||
->send();
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->title(__('Discovery failed'))
|
||||
->body($e->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
|
||||
public function startMigration(): void
|
||||
{
|
||||
$import = $this->getImport();
|
||||
if (! $import) {
|
||||
Notification::make()
|
||||
->title(__('Import job not found'))
|
||||
->danger()
|
||||
->send();
|
||||
return;
|
||||
}
|
||||
|
||||
$selected = $import->selected_accounts ?? [];
|
||||
if (! is_array($selected) || $selected === []) {
|
||||
Notification::make()
|
||||
->title(__('No account selected'))
|
||||
->danger()
|
||||
->send();
|
||||
return;
|
||||
}
|
||||
|
||||
if ($import->import_method === 'remote_server') {
|
||||
Notification::make()
|
||||
->title(__('Remote DirectAdmin import is not available yet'))
|
||||
->body(__('For now, please download a DirectAdmin backup archive and use the "Backup File" method.'))
|
||||
->warning()
|
||||
->send();
|
||||
return;
|
||||
}
|
||||
|
||||
$import->update([
|
||||
'status' => 'importing',
|
||||
'started_at' => now(),
|
||||
]);
|
||||
|
||||
$result = $this->getAgent()->importStart($import->id);
|
||||
|
||||
if (! ($result['success'] ?? false)) {
|
||||
Notification::make()
|
||||
->title(__('Failed to start migration'))
|
||||
->body((string) ($result['error'] ?? __('Unknown error')))
|
||||
->danger()
|
||||
->send();
|
||||
return;
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title(__('Migration started'))
|
||||
->body(__('Import process has started in the background.'))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
$this->dispatch('directadmin-self-status-updated');
|
||||
}
|
||||
|
||||
public function resetMigration(): void
|
||||
{
|
||||
if ($this->importId) {
|
||||
ServerImport::whereKey($this->importId)->delete();
|
||||
}
|
||||
|
||||
session()->forget('directadmin_self_migration.import_id');
|
||||
|
||||
$this->wizardStep = null;
|
||||
$this->step1Complete = false;
|
||||
$this->importId = null;
|
||||
$this->importMethod = 'backup_file';
|
||||
$this->remoteHost = null;
|
||||
$this->remotePort = 2222;
|
||||
$this->remoteUser = null;
|
||||
$this->remotePassword = null;
|
||||
$this->backupPath = null;
|
||||
$this->importFiles = true;
|
||||
$this->importDatabases = true;
|
||||
$this->importEmails = true;
|
||||
$this->importSsl = true;
|
||||
}
|
||||
|
||||
protected function getAgent(): AgentClient
|
||||
{
|
||||
return $this->agent ??= new AgentClient;
|
||||
}
|
||||
|
||||
protected function getImport(): ?ServerImport
|
||||
{
|
||||
if (! $this->importId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ServerImport::with('accounts')->find($this->importId);
|
||||
}
|
||||
|
||||
protected function upsertImportForDiscovery(): ServerImport
|
||||
{
|
||||
$user = Auth::user();
|
||||
$name = $user ? ('DirectAdmin Import - ' . $user->username . ' - ' . now()->format('Y-m-d H:i')) : ('DirectAdmin Import ' . now()->format('Y-m-d H:i'));
|
||||
|
||||
$attributes = [
|
||||
'name' => $name,
|
||||
'source_type' => 'directadmin',
|
||||
'import_method' => $this->importMethod,
|
||||
'import_options' => [
|
||||
'files' => $this->importFiles,
|
||||
'databases' => $this->importDatabases,
|
||||
'emails' => $this->importEmails,
|
||||
'ssl' => $this->importSsl,
|
||||
],
|
||||
'status' => 'discovering',
|
||||
'progress' => 0,
|
||||
'current_task' => null,
|
||||
];
|
||||
|
||||
if ($this->importMethod === 'backup_file') {
|
||||
$attributes['backup_path'] = $this->backupPath;
|
||||
$attributes['remote_host'] = null;
|
||||
$attributes['remote_port'] = null;
|
||||
$attributes['remote_user'] = null;
|
||||
} else {
|
||||
$attributes['backup_path'] = null;
|
||||
$attributes['remote_host'] = $this->remoteHost ? trim($this->remoteHost) : null;
|
||||
$attributes['remote_port'] = $this->remotePort;
|
||||
$attributes['remote_user'] = $this->remoteUser ? trim($this->remoteUser) : null;
|
||||
|
||||
if (filled($this->remotePassword)) {
|
||||
$attributes['remote_password'] = $this->remotePassword;
|
||||
}
|
||||
}
|
||||
|
||||
$import = $this->importId ? ServerImport::find($this->importId) : null;
|
||||
|
||||
if ($import) {
|
||||
$import->update($attributes);
|
||||
} else {
|
||||
$import = ServerImport::create($attributes);
|
||||
$this->importId = $import->id;
|
||||
}
|
||||
|
||||
$this->saveToSession();
|
||||
|
||||
return $import->fresh();
|
||||
}
|
||||
|
||||
protected function saveToSession(): void
|
||||
{
|
||||
if ($this->importId) {
|
||||
session()->put('directadmin_self_migration.import_id', $this->importId);
|
||||
}
|
||||
|
||||
session()->save();
|
||||
}
|
||||
|
||||
protected function restoreFromSession(): void
|
||||
{
|
||||
$this->importId = session('directadmin_self_migration.import_id');
|
||||
}
|
||||
|
||||
protected function restoreFromImport(): void
|
||||
{
|
||||
$import = $this->getImport();
|
||||
if (! $import) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->importMethod = (string) ($import->import_method ?? 'backup_file');
|
||||
$this->backupPath = $import->backup_path;
|
||||
$this->remoteHost = $import->remote_host;
|
||||
$this->remotePort = (int) ($import->remote_port ?? 2222);
|
||||
$this->remoteUser = $import->remote_user;
|
||||
|
||||
$options = $import->import_options ?? [];
|
||||
if (is_array($options)) {
|
||||
$this->importFiles = (bool) ($options['files'] ?? true);
|
||||
$this->importDatabases = (bool) ($options['databases'] ?? true);
|
||||
$this->importEmails = (bool) ($options['emails'] ?? true);
|
||||
$this->importSsl = (bool) ($options['ssl'] ?? true);
|
||||
}
|
||||
|
||||
$this->step1Complete = $import->accounts()->exists();
|
||||
}
|
||||
}
|
||||
|
||||
170
app/Filament/Jabali/Widgets/DirectAdminMigrationStatusTable.php
Normal file
170
app/Filament/Jabali/Widgets/DirectAdminMigrationStatusTable.php
Normal file
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Jabali\Widgets;
|
||||
|
||||
use App\Models\ServerImport;
|
||||
use App\Models\ServerImportAccount;
|
||||
use Filament\Actions\Concerns\InteractsWithActions;
|
||||
use Filament\Actions\Contracts\HasActions;
|
||||
use Filament\Schemas\Concerns\InteractsWithSchemas;
|
||||
use Filament\Schemas\Contracts\HasSchemas;
|
||||
use Filament\Support\Contracts\TranslatableContentDriver;
|
||||
use Filament\Support\Enums\FontWeight;
|
||||
use Filament\Support\Enums\IconSize;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Concerns\InteractsWithTable;
|
||||
use Filament\Tables\Contracts\HasTable;
|
||||
use Filament\Tables\Table;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
class DirectAdminMigrationStatusTable extends Component implements HasActions, HasSchemas, HasTable
|
||||
{
|
||||
use InteractsWithActions;
|
||||
use InteractsWithSchemas;
|
||||
use InteractsWithTable;
|
||||
|
||||
public ?int $importId = null;
|
||||
|
||||
public function mount(?int $importId = null): void
|
||||
{
|
||||
$this->importId = $importId ?: session('directadmin_self_migration.import_id');
|
||||
}
|
||||
|
||||
#[On('directadmin-self-status-updated')]
|
||||
public function refreshStatus(): void
|
||||
{
|
||||
$this->resetTable();
|
||||
}
|
||||
|
||||
public function makeFilamentTranslatableContentDriver(): ?TranslatableContentDriver
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function getImport(): ?ServerImport
|
||||
{
|
||||
if (! $this->importId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ServerImport::find($this->importId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Support\Collection<int, ServerImportAccount>
|
||||
*/
|
||||
protected function getRecords()
|
||||
{
|
||||
if (! $this->importId) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return ServerImportAccount::query()
|
||||
->where('server_import_id', $this->importId)
|
||||
->orderBy('source_username')
|
||||
->get();
|
||||
}
|
||||
|
||||
protected function shouldPoll(): bool
|
||||
{
|
||||
$import = $this->getImport();
|
||||
if (! $import) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (in_array($import->status, ['discovering', 'importing'], true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($this->getRecords() as $record) {
|
||||
if (! in_array($record->status, ['completed', 'failed', 'skipped'], true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function getStatusText(string $status): string
|
||||
{
|
||||
return match ($status) {
|
||||
'pending' => __('Waiting...'),
|
||||
'importing' => __('Importing...'),
|
||||
'completed' => __('Completed'),
|
||||
'failed' => __('Failed'),
|
||||
'skipped' => __('Skipped'),
|
||||
default => __('Unknown'),
|
||||
};
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->records(fn () => $this->getRecords())
|
||||
->columns([
|
||||
IconColumn::make('status_icon')
|
||||
->label('')
|
||||
->icon(fn (ServerImportAccount $record): string => match ($record->status) {
|
||||
'pending' => 'heroicon-o-clock',
|
||||
'importing' => 'heroicon-o-arrow-path',
|
||||
'completed' => 'heroicon-o-check-circle',
|
||||
'failed' => 'heroicon-o-x-circle',
|
||||
'skipped' => 'heroicon-o-minus-circle',
|
||||
default => 'heroicon-o-question-mark-circle',
|
||||
})
|
||||
->color(fn (ServerImportAccount $record): string => match ($record->status) {
|
||||
'pending' => 'gray',
|
||||
'importing' => 'warning',
|
||||
'completed' => 'success',
|
||||
'failed' => 'danger',
|
||||
'skipped' => 'gray',
|
||||
default => 'gray',
|
||||
})
|
||||
->size(IconSize::Small)
|
||||
->extraAttributes(fn (ServerImportAccount $record): array => $record->status === 'importing'
|
||||
? ['class' => 'animate-spin']
|
||||
: []),
|
||||
TextColumn::make('source_username')
|
||||
->label(__('Account'))
|
||||
->weight(FontWeight::Bold)
|
||||
->searchable(),
|
||||
TextColumn::make('status')
|
||||
->label(__('Status'))
|
||||
->badge()
|
||||
->formatStateUsing(fn (string $state): string => $this->getStatusText($state))
|
||||
->color(fn (ServerImportAccount $record): string => match ($record->status) {
|
||||
'pending' => 'gray',
|
||||
'importing' => 'warning',
|
||||
'completed' => 'success',
|
||||
'failed' => 'danger',
|
||||
'skipped' => 'gray',
|
||||
default => 'gray',
|
||||
}),
|
||||
TextColumn::make('current_task')
|
||||
->label(__('Current Task'))
|
||||
->wrap()
|
||||
->limit(80)
|
||||
->default(__('Waiting...')),
|
||||
TextColumn::make('progress')
|
||||
->label(__('Progress'))
|
||||
->suffix('%')
|
||||
->toggleable(),
|
||||
])
|
||||
->striped()
|
||||
->paginated(false)
|
||||
->poll($this->shouldPoll() ? '3s' : null)
|
||||
->emptyStateHeading(__('No migration activity'))
|
||||
->emptyStateDescription(__('Discover an account and start migration.'))
|
||||
->emptyStateIcon('heroicon-o-queue-list');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return $this->getTable()->render();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12901,6 +12901,43 @@ function discoverDirectAdminRemote(string $host, int $port, string $user, string
|
||||
|
||||
$url = "https://$host:$port/CMD_API_SHOW_ALL_USERS";
|
||||
|
||||
$fetchUserConfig = function (string $username) use ($host, $port, $user, $password): ?array {
|
||||
$detailUrl = "https://$host:$port/CMD_API_SHOW_USER_CONFIG?user=" . urlencode($username);
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $detailUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||
curl_setopt($ch, CURLOPT_USERPWD, "$user:$password");
|
||||
|
||||
$userResponse = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($error || $httpCode !== 200 || !is_string($userResponse) || $userResponse === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
parse_str($userResponse, $userData);
|
||||
if (isset($userData['error']) && $userData['error'] === '1') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'username' => $username,
|
||||
'email' => $userData['email'] ?? '',
|
||||
'main_domain' => $userData['domain'] ?? '',
|
||||
'addon_domains' => [],
|
||||
'subdomains' => [],
|
||||
'databases' => [],
|
||||
'email_accounts' => [],
|
||||
'disk_usage' => ($userData['bandwidth'] ?? 0) * 1048576,
|
||||
];
|
||||
};
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
@@ -12926,6 +12963,13 @@ function discoverDirectAdminRemote(string $host, int $port, string $user, string
|
||||
parse_str($response, $data);
|
||||
|
||||
if (isset($data['error']) && $data['error'] === '1') {
|
||||
// Regular DirectAdmin users are not allowed to call CMD_API_SHOW_ALL_USERS.
|
||||
// In that case, fall back to discovering a single account using the same credentials.
|
||||
$single = $fetchUserConfig($user);
|
||||
if ($single) {
|
||||
return ['success' => true, 'accounts' => [$single]];
|
||||
}
|
||||
|
||||
return ['success' => false, 'error' => $data['text'] ?? 'Unknown error'];
|
||||
}
|
||||
|
||||
@@ -12937,35 +12981,22 @@ function discoverDirectAdminRemote(string $host, int $port, string $user, string
|
||||
$userList = [$userList];
|
||||
}
|
||||
|
||||
if (empty($userList)) {
|
||||
$single = $fetchUserConfig($user);
|
||||
if ($single) {
|
||||
return ['success' => true, 'accounts' => [$single]];
|
||||
}
|
||||
|
||||
return ['success' => false, 'error' => 'No users returned by DirectAdmin'];
|
||||
}
|
||||
|
||||
foreach ($userList as $username) {
|
||||
if (empty($username)) continue;
|
||||
|
||||
// Get user details
|
||||
$detailUrl = "https://$host:$port/CMD_API_SHOW_USER_CONFIG?user=" . urlencode($username);
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $detailUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||
curl_setopt($ch, CURLOPT_USERPWD, "$user:$password");
|
||||
|
||||
$userResponse = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
parse_str($userResponse, $userData);
|
||||
|
||||
$accounts[] = [
|
||||
'username' => $username,
|
||||
'email' => $userData['email'] ?? '',
|
||||
'main_domain' => $userData['domain'] ?? '',
|
||||
'addon_domains' => [],
|
||||
'subdomains' => [],
|
||||
'databases' => [],
|
||||
'email_accounts' => [],
|
||||
'disk_usage' => ($userData['bandwidth'] ?? 0) * 1048576,
|
||||
];
|
||||
$account = $fetchUserConfig($username);
|
||||
if ($account) {
|
||||
$accounts[] = $account;
|
||||
}
|
||||
}
|
||||
|
||||
return ['success' => true, 'accounts' => $accounts];
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
@livewire(\App\Filament\Admin\Widgets\DirectAdminAccountConfigTable::class, [
|
||||
'importId' => $this->importId,
|
||||
], key('directadmin-account-config-table-' . ($this->importId ?? 'new')))
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
@livewire(\App\Filament\Admin\Widgets\DirectAdminAccountsTable::class, [
|
||||
'importId' => $this->importId,
|
||||
], key('directadmin-accounts-table-' . ($this->importId ?? 'new')))
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
@livewire(\App\Filament\Admin\Widgets\DirectAdminMigrationStatusTable::class, [
|
||||
'importId' => $this->importId,
|
||||
], key('directadmin-migration-status-table-' . ($this->importId ?? 'new')))
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<x-filament-panels::page>
|
||||
{{ $this->migrationForm }}
|
||||
|
||||
<x-filament-actions::modals />
|
||||
</x-filament-panels::page>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
@livewire(\App\Filament\Admin\Pages\DirectAdminMigration::class, [], key('migration-directadmin'))
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
@livewire(\App\Filament\Jabali\Widgets\DirectAdminMigrationStatusTable::class, [
|
||||
'importId' => $this->importId,
|
||||
], key('directadmin-self-migration-status-table-' . ($this->importId ?? 'new')))
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<x-filament-panels::page>
|
||||
{{ $this->migrationForm }}
|
||||
|
||||
<x-filament-actions::modals />
|
||||
</x-filament-panels::page>
|
||||
|
||||
Reference in New Issue
Block a user