Compare commits

..

3 Commits

Author SHA1 Message Date
c6f5b6cab8 Replace custom HTML activity log table with Filament EmbeddedTable
The activity tab on the user Logs page used a raw HTML table with
Tailwind classes. This replaces it with a proper Filament embedded
table widget (ActivityLogTable) for consistent styling, pagination,
badges, and dark mode support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 16:50:49 +00:00
root
8acc55a799 Refine database warning banner and docs 2026-02-06 18:46:16 +00:00
root
a5742a3156 Add confirmations for service disabling 2026-02-06 17:00:13 +00:00
12 changed files with 120 additions and 65 deletions

View File

@@ -620,7 +620,15 @@ All administrative actions are logged to the `audit_logs` table.
- **USE Tailwind classes** - Only when absolutely necessary for minor adjustments
- **MUST be responsive** - All pages must work on mobile, tablet, and desktop
### Warning Banners
- Use Filament `Section::make()` for warning banners (no raw HTML).
- Always set `->icon('heroicon-o-exclamation-triangle')` and `->iconColor('warning')`.
- Keep banners non-collapsible: `->collapsed(false)->collapsible(false)`.
- Put the full message in `->description()` and keep the heading short.
### Allowed Components
Use these Filament native components exclusively:
| Category | Components |

View File

@@ -1 +1 @@
VERSION=0.9-rc54
VERSION=0.9-rc56

View File

@@ -575,7 +575,9 @@ class Security extends Page implements HasActions, HasForms, HasTable
->visible(fn () => $this->fail2banRunning)
->requiresConfirmation()
->modalHeading(__('Disable Fail2ban'))
->modalDescription(__('Fail2ban will be stopped and disabled. You can re-enable it later from this tab.'))
->modalIcon('heroicon-o-exclamation-triangle')
->modalIconColor('warning')
->modalDescription(__('Warning: Fail2ban will be stopped and disabled. You can re-enable it later from this tab.'))
->action('disableFail2ban'),
])
->schema([
@@ -665,9 +667,12 @@ class Security extends Page implements HasActions, HasForms, HasTable
->color(fn () => $this->clamavRunning ? 'warning' : 'success')
->size('sm')
->action(fn () => $this->clamavRunning ? $this->disableClamav() : $this->enableClamav())
->requiresConfirmation(fn () => ! $this->clamavRunning)
->requiresConfirmation()
->modalHeading(fn () => $this->clamavRunning ? __('Disable ClamAV') : __('Enable ClamAV'))
->modalIcon('heroicon-o-exclamation-triangle')
->modalIconColor('warning')
->modalDescription(fn () => $this->clamavRunning
? __('ClamAV will be stopped and disabled. You can re-enable it later.')
? __('Warning: This will stop and disable ClamAV. You can re-enable it later.')
: __('Starting ClamAV daemon uses ~500MB RAM. Continue?')),
FormAction::make('updateSignatures')
->label(__('Update Signatures'))

View File

@@ -692,17 +692,17 @@ class ServerSettings extends Page implements HasActions, HasForms
protected function databaseTabContent(): array
{
return [
Section::make(__('Warning: Changing database settings can impact performance or cause outages'))
->description(__('Apply changes only if you understand their effects, and prefer doing so during maintenance windows.'))
->icon('heroicon-o-exclamation-triangle')
->iconColor('warning')
->collapsed(false)
->collapsible(false)
->compact(),
Section::make(__('Database Tuning'))
->description(__('Adjust MariaDB/MySQL global variables.'))
->icon('heroicon-o-circle-stack')
->schema([
Placeholder::make('database_tuning_warning')
->content(new HtmlString(
'<div class="rounded-lg bg-warning-500/10 p-4 text-sm text-warning-700 dark:text-warning-400">'.
'<strong>'.__('Warning:').'</strong> '.
__('Changing database settings can impact performance or cause outages. Apply changes only if you understand their effects, and prefer doing so during maintenance windows.').
'</div>'
)),
EmbeddedTable::make(DatabaseTuningTable::class),
]),
];

View File

@@ -208,7 +208,9 @@ class Services extends Page implements HasActions, HasForms, HasTable
->visible(fn (array $record): bool => $record['is_active'])
->requiresConfirmation()
->modalHeading(__('Stop Service'))
->modalDescription(fn (array $record): string => __('Are you sure you want to stop :service? This may affect running websites and services.', ['service' => $record['name']]))
->modalIcon('heroicon-o-exclamation-triangle')
->modalIconColor('warning')
->modalDescription(fn (array $record): string => __('Warning: This will stop :service and may affect running websites and services. Are you sure you want to continue?', ['service' => $record['name']]))
->modalSubmitActionLabel(__('Stop Service'))
->action(fn (array $record) => $this->executeServiceAction($record['service'], 'stop')),
Action::make('restart')
@@ -236,7 +238,9 @@ class Services extends Page implements HasActions, HasForms, HasTable
->visible(fn (array $record): bool => $record['is_enabled'])
->requiresConfirmation()
->modalHeading(__('Disable Service'))
->modalDescription(fn (array $record): string => __("Are you sure you want to disable :service? It won't start automatically on boot.", ['service' => $record['name']]))
->modalIcon('heroicon-o-exclamation-triangle')
->modalIconColor('warning')
->modalDescription(fn (array $record): string => __('Warning: This will disable :service and it will not start automatically on boot. Are you sure you want to continue?', ['service' => $record['name']]))
->modalSubmitActionLabel(__('Disable Service'))
->action(fn (array $record) => $this->executeServiceAction($record['service'], 'disable')),
])

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Filament\Jabali\Pages;
use App\Models\AuditLog;
use App\Filament\Jabali\Widgets\ActivityLogTable;
use App\Services\Agent\AgentClient;
use BackedEnum;
use Filament\Actions\Action;
@@ -16,6 +16,7 @@ use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Components\EmbeddedTable;
use Filament\Schemas\Components\View;
use Filament\Schemas\Schema;
use Illuminate\Contracts\Support\Htmlable;
@@ -119,7 +120,7 @@ class Logs extends Page implements HasActions, HasForms
'activity' => Tab::make(__('Activity Log'))
->icon('heroicon-o-clipboard-document-list')
->schema([
View::make('filament.jabali.pages.logs-tab-activity'),
EmbeddedTable::make(ActivityLogTable::class),
]),
]),
]);
@@ -227,15 +228,6 @@ class Logs extends Page implements HasActions, HasForms
->send();
}
public function getActivityLogs()
{
return AuditLog::query()
->where('user_id', Auth::id())
->latest()
->limit(50)
->get();
}
public function generateStats(): void
{
if (! $this->selectedDomain) {

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace App\Filament\Jabali\Widgets;
use App\Models\AuditLog;
use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions;
use Filament\Schemas\Concerns\InteractsWithSchemas;
use Filament\Schemas\Contracts\HasSchemas;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Table;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class ActivityLogTable extends Component implements HasTable, HasSchemas, HasActions
{
use InteractsWithTable;
use InteractsWithSchemas;
use InteractsWithActions;
public function table(Table $table): Table
{
return $table
->query(
AuditLog::query()
->where('user_id', Auth::id())
->latest()
)
->columns([
TextColumn::make('created_at')
->label(__('Time'))
->dateTime('M d, H:i')
->color('gray'),
TextColumn::make('category')
->label(__('Category'))
->badge()
->color(fn (string $state): string => match ($state) {
'domain' => 'info',
'email' => 'primary',
'database' => 'warning',
'auth' => 'gray',
'firewall' => 'danger',
'service' => 'success',
default => 'gray',
}),
TextColumn::make('action')
->label(__('Action'))
->badge()
->color(fn (string $state): string => match ($state) {
'create', 'created' => 'success',
'update', 'updated' => 'warning',
'delete', 'deleted' => 'danger',
'login' => 'info',
default => 'gray',
}),
TextColumn::make('description')
->label(__('Description'))
->limit(60)
->wrap(),
TextColumn::make('ip_address')
->label(__('IP'))
->color('gray'),
])
->defaultPaginationPageOption(25)
->striped()
->emptyStateHeading(__('No activity recorded yet'))
->emptyStateDescription(__('Recent actions performed in your account will appear here.'))
->emptyStateIcon('heroicon-o-clipboard-document-list');
}
public function render()
{
return $this->getTable()->render();
}
}

View File

@@ -185,3 +185,5 @@ class BackupSchedule extends Model
return $timezone;
}
}

View File

@@ -17,7 +17,7 @@
},
"require-dev": {
"fakerphp/faker": "^1.23",
"filament/blueprint": "^2.0",
"filament/blueprint": "^2.1",
"laravel/boost": "*",
"laravel/pail": "^1.2.2",
"laravel/pint": "^1.24",

8
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "194d87cc129a30c6e832109fb820097a",
"content-hash": "7083b0b087c4b503b50d3aa23cfbbfac",
"packages": [
{
"name": "anourvalar/eloquent-serialize",
@@ -8817,14 +8817,14 @@
},
{
"name": "filament/blueprint",
"version": "v2.0.1",
"version": "v2.1.0",
"dist": {
"type": "zip",
"url": "https://packages.filamentphp.com/composer/10/127/download"
"url": "https://packages.filamentphp.com/composer/10/473/download"
},
"require": {
"filament/support": "^5.0",
"laravel/boost": "^1.8"
"laravel/boost": "^1.8|^2.0"
},
"type": "library",
"license": [

View File

@@ -16,7 +16,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ -f "$SCRIPT_DIR/VERSION" ]]; then
JABALI_VERSION="$(sed -n 's/^VERSION=//p' "$SCRIPT_DIR/VERSION")"
fi
JABALI_VERSION="${JABALI_VERSION:-0.9-rc54}"
JABALI_VERSION="${JABALI_VERSION:-0.9-rc56}"
# Colors
RED='\033[0;31m'

View File

@@ -1,35 +0,0 @@
<x-filament::section class="mt-4" icon="heroicon-o-clipboard-document-list">
<x-slot name="heading">{{ __('Activity Log') }}</x-slot>
<x-slot name="description">{{ __('Recent actions performed in your account.') }}</x-slot>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-800">
<tr>
<th class="px-4 py-3 text-left fi-section-header-heading">{{ __('Time') }}</th>
<th class="px-4 py-3 text-left fi-section-header-heading">{{ __('Category') }}</th>
<th class="px-4 py-3 text-left fi-section-header-heading">{{ __('Action') }}</th>
<th class="px-4 py-3 text-left fi-section-header-heading">{{ __('Description') }}</th>
<th class="px-4 py-3 text-left fi-section-header-heading">{{ __('IP') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
@forelse($this->getActivityLogs() as $log)
<tr>
<td class="px-4 py-3 fi-section-header-description">{{ $log->created_at?->format('Y-m-d H:i') }}</td>
<td class="px-4 py-3 fi-section-header-description">{{ $log->category }}</td>
<td class="px-4 py-3 fi-section-header-description">{{ $log->action }}</td>
<td class="px-4 py-3 fi-section-header-description">{{ $log->description }}</td>
<td class="px-4 py-3 fi-section-header-description">{{ $log->ip_address }}</td>
</tr>
@empty
<tr>
<td colspan="5" class="px-4 py-6 text-center fi-section-header-description">
{{ __('No activity recorded yet.') }}
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</x-filament::section>