Skip to content

Commit e849343

Browse files
committed
feat: Add cache management widget with clear and optimize buttons, and player level distribution chart
1 parent e85f09a commit e849343

8 files changed

Lines changed: 379 additions & 1 deletion

File tree

app/Filament/Pages/Dashboard.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public function getWidgets(): array
3434
\SilkPanel\WidgetsDashboard\Widgets\StatsOverviewWidget::class,
3535
\SilkPanel\WidgetsDashboard\Widgets\PackageUpdateWidget::class,
3636
\SilkPanel\WidgetsDashboard\Widgets\LicenseOverviewWidget::class,
37+
\App\Filament\Widgets\PlayerLevelDistributionWidget::class,
3738
];
3839
}
3940
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
<?php
2+
3+
namespace App\Filament\Widgets;
4+
5+
use App\Models\Setting;
6+
use Filament\Widgets\ChartWidget;
7+
use SilkPanel\SilkroadModels\Models\Shard\AbstractChar;
8+
9+
class PlayerLevelDistributionWidget extends ChartWidget
10+
{
11+
protected static ?int $sort = 10;
12+
13+
protected ?string $maxHeight = '280px';
14+
15+
protected int | string | array $columnSpan = [
16+
'default' => 'full',
17+
'lg' => 3,
18+
'xl' => 3,
19+
];
20+
21+
public function getHeading(): string
22+
{
23+
return __('filament/characters.widget_level_distribution.heading');
24+
}
25+
26+
public function getDescription(): ?string
27+
{
28+
return __('filament/characters.widget_level_distribution.description');
29+
}
30+
31+
protected function getData(): array
32+
{
33+
/** @var AbstractChar $charModel */
34+
$charModel = app(AbstractChar::class);
35+
36+
$step = 10;
37+
38+
// 1. Prefer the configured level cap (sro_cap setting)
39+
// 2. Fall back to the actual max level in the DB
40+
$cap = (int) Setting::get('sro_cap', 0);
41+
$max = $cap > 0
42+
? $cap
43+
: (int) $charModel->newQuery()
44+
->where('CharID', '!=', 0)
45+
->where('CharName16', '!=', 'dummy')
46+
->max('CurLevel');
47+
48+
// Round up to the next full step so the last bucket is complete
49+
$max = (int) (ceil($max / $step) * $step);
50+
51+
// Build buckets: 1–10, 11–20, ...
52+
$buckets = [];
53+
for ($from = 1; $from <= $max; $from += $step) {
54+
$to = $from + $step - 1;
55+
$buckets[] = ['from' => $from, 'to' => $to, 'label' => "{$from}{$to}"];
56+
}
57+
58+
$counts = $charModel->newQuery()
59+
->where('CharID', '!=', 0)
60+
->where('CharName16', '!=', 'dummy')
61+
->where('CurLevel', '>=', 1)
62+
->selectRaw('CurLevel, COUNT(*) as cnt')
63+
->groupBy('CurLevel')
64+
->pluck('cnt', 'CurLevel');
65+
66+
$data = [];
67+
$labels = [];
68+
$colors = [];
69+
70+
foreach ($buckets as $bucket) {
71+
$total = 0;
72+
for ($lvl = $bucket['from']; $lvl <= $bucket['to']; $lvl++) {
73+
$total += $counts->get($lvl, 0);
74+
}
75+
76+
// Skip completely empty high-level buckets at the tail
77+
$data[] = $total;
78+
$labels[] = $bucket['label'];
79+
80+
// Color gradient: low levels = blue, mid = purple, high = amber
81+
$ratio = ($bucket['from'] - 1) / ($max - 1);
82+
if ($ratio < 0.5) {
83+
$r = (int) (99 + (124 - 99) * ($ratio * 2));
84+
$g = (int) (102 + (58 - 102) * ($ratio * 2));
85+
$b = (int) (241 + (237 - 241) * ($ratio * 2));
86+
} else {
87+
$r = (int) (124 + (245 - 124) * (($ratio - 0.5) * 2));
88+
$g = (int) (58 + (158 - 58) * (($ratio - 0.5) * 2));
89+
$b = (int) (237 + (11 - 237) * (($ratio - 0.5) * 2));
90+
}
91+
$colors[] = "rgba({$r},{$g},{$b},0.85)";
92+
}
93+
94+
return [
95+
'datasets' => [
96+
[
97+
'label' => __('filament/characters.widget_level_distribution.dataset_label'),
98+
'data' => $data,
99+
'backgroundColor' => $colors,
100+
'borderColor' => array_map(
101+
fn($c) => str_replace('0.85', '1', $c),
102+
$colors
103+
),
104+
'borderWidth' => 1,
105+
'borderRadius' => 6,
106+
],
107+
],
108+
'labels' => $labels,
109+
];
110+
}
111+
112+
protected function getType(): string
113+
{
114+
return 'bar';
115+
}
116+
117+
protected function getOptions(): array
118+
{
119+
return [
120+
'plugins' => [
121+
'legend' => ['display' => false],
122+
'tooltip' => [
123+
'displayColors' => false,
124+
'titlePrefix' => 'Level ',
125+
],
126+
],
127+
'scales' => [
128+
'x' => [
129+
'title' => [
130+
'display' => true,
131+
'text' => __('filament/characters.widget_level_distribution.x_label'),
132+
'color' => 'rgb(107,114,128)',
133+
'font' => ['size' => 11],
134+
],
135+
'grid' => ['display' => false],
136+
],
137+
'y' => [
138+
'title' => [
139+
'display' => true,
140+
'text' => __('filament/characters.widget_level_distribution.y_label'),
141+
'color' => 'rgb(107,114,128)',
142+
'font' => ['size' => 11],
143+
],
144+
'beginAtZero' => true,
145+
'ticks' => ['precision' => 0],
146+
],
147+
],
148+
];
149+
}
150+
}

app/Livewire/ClearCacheButton.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
namespace App\Livewire;
4+
5+
use Filament\Notifications\Notification;
6+
use Illuminate\Support\Facades\Artisan;
7+
use Livewire\Component;
8+
9+
class ClearCacheButton extends Component
10+
{
11+
public bool $showModal = false;
12+
13+
public function openModal(): void
14+
{
15+
$this->showModal = true;
16+
}
17+
18+
public function closeModal(): void
19+
{
20+
$this->showModal = false;
21+
}
22+
23+
public function clearCache(): void
24+
{
25+
Artisan::call('cache:clear');
26+
Artisan::call('config:clear');
27+
Artisan::call('view:clear');
28+
Artisan::call('route:clear');
29+
30+
$this->showModal = false;
31+
32+
Notification::make()
33+
->title(__('filament/cache.cleared_success'))
34+
->success()
35+
->send();
36+
}
37+
38+
public function optimize(): void
39+
{
40+
Notification::make()
41+
->title(__('filament/cache.optimized_success'))
42+
->success()
43+
->send();
44+
45+
Artisan::call('optimize');
46+
47+
$this->showModal = false;
48+
}
49+
50+
public function render()
51+
{
52+
return view('livewire.clear-cache-button');
53+
}
54+
}

app/Providers/Filament/AdminPanelProvider.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ public function panel(Panel $panel): Panel
8181
fn() => view('livewire.saas-messages-hook'),
8282
);
8383

84+
FilamentView::registerRenderHook(
85+
PanelsRenderHook::GLOBAL_SEARCH_BEFORE,
86+
fn() => auth()->check() ? \Illuminate\Support\Facades\Blade::render('<livewire:clear-cache-button />') : '',
87+
);
88+
8489
FilamentView::registerRenderHook(
8590
PanelsRenderHook::BODY_START,
8691
fn() => auth()->check() ? "<livewire:version-check />" : '',
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
return [
4+
'button_tooltip' => 'Clear cache',
5+
'modal_title' => 'Clear Cache',
6+
'modal_subtitle' => 'This action cannot be undone.',
7+
'modal_description' => 'Are you sure you want to clear the entire application cache? This affects the following areas:',
8+
'item_cache' => 'Application cache',
9+
'item_config' => 'Configuration cache',
10+
'item_view' => 'View cache (Blade templates)',
11+
'item_route' => 'Route cache',
12+
'cancel' => 'Cancel',
13+
'confirm' => 'Clear now',
14+
'optimize' => 'Optimize',
15+
'cleared_success' => 'Cache cleared successfully!',
16+
'optimized_success' => 'Application optimized successfully!',
17+
];

resources/lang/en/filament/characters.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,25 @@
11
<?php
22

33
return [
4+
'widget_job_distribution' => [
5+
'heading' => 'Job Distribution',
6+
'description' => 'Primary job of each character',
7+
'hunter' => 'Hunter',
8+
'trader' => 'Trader',
9+
'thief' => 'Thief',
10+
'jobless' => 'Jobless',
11+
'no_data' => 'No job data yet',
12+
],
13+
14+
'widget_level_distribution' => [
15+
'heading' => 'Player Level Distribution',
16+
'description' => 'Number of characters per level range',
17+
'dataset_label' => 'Characters',
18+
'x_label' => 'Level Range',
19+
'y_label' => 'Players',
20+
'players' => 'players',
21+
],
22+
423
'navigation' => 'Characters',
524
'navigation_parent' => 'Settings',
625

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<div>
2+
{{-- Blitz-Button in der Topbar --}}
3+
<button
4+
wire:click="openModal"
5+
title="{{ __('filament/cache.button_tooltip') }}"
6+
class="flex items-center justify-center w-9 h-9 rounded-lg border-none cursor-pointer bg-transparent text-gray-400 hover:bg-gray-100 hover:text-amber-500 dark:text-gray-500 dark:hover:bg-white/5 dark:hover:text-amber-400 transition-colors duration-150"
7+
>
8+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5">
9+
<path fill-rule="evenodd" d="M14.615 1.595a.75.75 0 0 1 .359.852L12.982 9.75h7.268a.75.75 0 0 1 .548 1.262l-10.5 11.25a.75.75 0 0 1-1.272-.71l1.992-7.302H3.818a.75.75 0 0 1-.548-1.262l10.5-11.25a.75.75 0 0 1 .845-.143Z" clip-rule="evenodd" />
10+
</svg>
11+
</button>
12+
13+
{{-- Confirmation Modal --}}
14+
@if($showModal)
15+
<div
16+
x-data
17+
x-init="$nextTick(() => $el.querySelector('[data-modal-backdrop]').focus())"
18+
class="fixed inset-0 z-[9998] flex items-center justify-center p-4"
19+
>
20+
{{-- Backdrop --}}
21+
<div
22+
data-modal-backdrop
23+
wire:click="closeModal"
24+
tabindex="-1"
25+
class="absolute inset-0 bg-black/50 backdrop-blur-sm"
26+
></div>
27+
28+
{{-- Modal Card --}}
29+
<div
30+
role="dialog"
31+
aria-modal="true"
32+
class="relative z-10 w-full max-w-md rounded-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-white/10 shadow-2xl p-7"
33+
@keydown.escape.window="$wire.closeModal()"
34+
>
35+
{{-- Icon + Title --}}
36+
<div class="flex items-center gap-3.5 mb-4">
37+
<div class="flex-shrink-0 flex items-center justify-center w-11 h-11 rounded-xl bg-amber-500/10 dark:bg-amber-400/10">
38+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5 text-amber-500 dark:text-amber-400">
39+
<path fill-rule="evenodd" d="M14.615 1.595a.75.75 0 0 1 .359.852L12.982 9.75h7.268a.75.75 0 0 1 .548 1.262l-10.5 11.25a.75.75 0 0 1-1.272-.71l1.992-7.302H3.818a.75.75 0 0 1-.548-1.262l10.5-11.25a.75.75 0 0 1 .845-.143Z" clip-rule="evenodd" />
40+
</svg>
41+
</div>
42+
<div>
43+
<h2 class="m-0 text-base font-bold text-gray-900 dark:text-white">{{ __('filament/cache.modal_title') }}</h2>
44+
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">{{ __('filament/cache.modal_subtitle') }}</p>
45+
</div>
46+
</div>
47+
48+
{{-- Body --}}
49+
<p class="text-sm text-gray-600 dark:text-gray-300 mb-5 leading-relaxed">
50+
{{ __('filament/cache.modal_description') }}
51+
</p>
52+
53+
{{-- What gets cleared --}}
54+
<ul class="mb-6 p-0 list-none flex flex-col gap-1.5">
55+
@foreach(['cache', 'config', 'view', 'route'] as $item)
56+
<li class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-300">
57+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-3.5 h-3.5 flex-shrink-0 text-amber-500 dark:text-amber-400">
58+
<path fill-rule="evenodd" d="M12.416 3.376a.75.75 0 0 1 .208 1.04l-5 7.5a.75.75 0 0 1-1.154.114l-3-3a.75.75 0 0 1 1.06-1.06l2.353 2.353 4.493-6.74a.75.75 0 0 1 1.04-.207Z" clip-rule="evenodd" />
59+
</svg>
60+
{{ __("filament/cache.item_{$item}") }}
61+
</li>
62+
@endforeach
63+
</ul>
64+
65+
{{-- Buttons --}}
66+
<div class="flex flex-wrap gap-3 justify-end">
67+
<button
68+
wire:click="closeModal"
69+
class="px-4 py-2 rounded-lg border border-gray-300 dark:border-white/10 bg-transparent text-gray-700 dark:text-gray-300 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-white/5 transition-colors"
70+
>
71+
{{ __('filament/cache.cancel') }}
72+
</button>
73+
<button
74+
wire:click="optimize"
75+
wire:loading.attr="disabled"
76+
class="px-5 py-2 rounded-lg border-none bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold cursor-pointer flex items-center gap-1.5 transition-colors disabled:opacity-60"
77+
>
78+
<span wire:loading.remove wire:target="optimize">
79+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-3.5 h-3.5">
80+
<path fill-rule="evenodd" d="M10 1a6 6 0 0 0-3.815 10.631C7.237 12.5 8 13.443 8 14.456v.644a.75.75 0 0 0 .572.729 6.016 6.016 0 0 0 2.856 0A.75.75 0 0 0 12 15.1v-.644c0-1.013.762-1.957 1.815-2.825A6 6 0 0 0 10 1ZM8.863 17.06a.75.75 0 0 0-.226 1.483 9.066 9.066 0 0 0 2.726 0 .75.75 0 0 0-.226-1.483 7.553 7.553 0 0 1-2.274 0Z" clip-rule="evenodd" />
81+
</svg>
82+
</span>
83+
<span wire:loading wire:target="optimize">
84+
<svg class="w-3.5 h-3.5 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
85+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
86+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
87+
</svg>
88+
</span>
89+
{{ __('filament/cache.optimize') }}
90+
</button>
91+
<button
92+
wire:click="clearCache"
93+
wire:loading.attr="disabled"
94+
class="px-5 py-2 rounded-lg border-none bg-amber-500 hover:bg-amber-600 text-white text-sm font-semibold cursor-pointer flex items-center gap-1.5 transition-colors disabled:opacity-60"
95+
>
96+
<span wire:loading.remove wire:target="clearCache">
97+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-3.5 h-3.5">
98+
<path fill-rule="evenodd" d="M14.615 1.595a.75.75 0 0 1 .359.852L12.982 9.75h7.268a.75.75 0 0 1 .548 1.262l-10.5 11.25a.75.75 0 0 1-1.272-.71l1.992-7.302H3.818a.75.75 0 0 1-.548-1.262l10.5-11.25a.75.75 0 0 1 .845-.143Z" clip-rule="evenodd" />
99+
</svg>
100+
</span>
101+
<span wire:loading wire:target="clearCache">
102+
<svg class="w-3.5 h-3.5 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
103+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
104+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
105+
</svg>
106+
</span>
107+
{{ __('filament/cache.confirm') }}
108+
</button>
109+
</div>
110+
</div>
111+
</div>
112+
@endif
113+
</div>

0 commit comments

Comments
 (0)