- 2.x - sites finishing

This commit is contained in:
Saeed Vaziry
2024-10-06 16:06:51 +02:00
parent 3c50e2c947
commit c24b4b7333
82 changed files with 1250 additions and 345 deletions

View File

@ -56,7 +56,7 @@ protected function getHeaderActions(): array
Select::make('user')
->rules(fn (callable $get) => CreateCronJob::rules($get())['user'])
->options([
'vito' => 'vito',
'vito' => $this->server->ssh_user,
'root' => 'root',
]),
Select::make('frequency')

View File

@ -16,7 +16,6 @@
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
use Filament\Support\Enums\MaxWidth;
use Throwable;
class Index extends Page
{
@ -228,21 +227,12 @@ protected function getHeaderActions(): array
]),
])
->modalSubmitActionLabel('Create')
->action(function ($input) {
$this->authorize('create', Server::class);
$this->validate();
try {
$server = app(CreateServerAction::class)->create(auth()->user(), $input);
->action(function (array $data) {
run_action($this, function () use ($data) {
$server = app(CreateServerAction::class)->create(auth()->user(), $data);
$this->redirect(View::getUrl(['server' => $server]));
} catch (Throwable $e) {
Notification::make()
->title($e->getMessage())
->danger()
->send();
}
});
}),
];
}

View File

@ -26,6 +26,8 @@ class LogsList extends Widget
public ?string $label = '';
protected $listeners = ['$refresh'];
protected function getTableQuery(): Builder
{
return ServerLog::query()

View File

@ -11,7 +11,7 @@
use Filament\Notifications\Notification;
use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\ActionGroup;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget;
@ -33,9 +33,10 @@ protected function getTableQuery(): Builder
protected function getTableColumns(): array
{
return [
ImageColumn::make('image_url')
IconColumn::make('id')
->label('Service')
->size(24),
->icon(fn (Service $record) => 'icon-'.$record->name)
->width(24),
TextColumn::make('name')
->sortable(),
TextColumn::make('version')
@ -59,21 +60,22 @@ public function getTable(): Table
return $this->table
->actions([
ActionGroup::make([
$this->serviceAction('start'),
$this->serviceAction('stop'),
$this->serviceAction('restart'),
$this->serviceAction('disable'),
$this->serviceAction('enable'),
$this->serviceAction('start', 'heroicon-o-play'),
$this->serviceAction('stop', 'heroicon-o-stop'),
$this->serviceAction('restart', 'heroicon-o-arrow-path'),
$this->serviceAction('disable', 'heroicon-o-x-mark'),
$this->serviceAction('enable', 'heroicon-o-check'),
$this->uninstallAction(),
]),
]);
}
private function serviceAction(string $type): Action
private function serviceAction(string $type, string $icon): Action
{
return Action::make($type)
->authorize(fn (Service $service) => auth()->user()?->can($type, $service))
->authorize(fn (Service $service) => auth()->user()?->can('update', $service))
->label(ucfirst($type).' Service')
->icon($icon)
->action(function (Service $service) use ($type) {
try {
app(Manage::class)->$type($service);
@ -95,6 +97,7 @@ private function uninstallAction(): Action
return Action::make('uninstall')
->authorize(fn (Service $service) => auth()->user()?->can('delete', $service))
->label('Uninstall Service')
->icon('heroicon-o-trash')
->color('danger')
->requiresConfirmation()
->action(function (Service $service) {

View File

@ -2,7 +2,15 @@
namespace App\Web\Pages\Servers\Sites\Pages\Queues;
use App\Actions\Queue\CreateQueue;
use App\Web\Pages\Servers\Sites\Page;
use Filament\Actions\Action;
use Filament\Actions\CreateAction;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Support\Enums\MaxWidth;
class Index extends Page
{
@ -17,6 +25,54 @@ public static function canAccess(): bool
public function getWidgets(): array
{
return [];
return [
[Widgets\QueuesList::class, ['site' => $this->site]],
];
}
protected function getHeaderActions(): array
{
return [
Action::make('read-the-docs')
->label('Read the Docs')
->icon('heroicon-o-document-text')
->color('gray')
->url('https://vitodeploy.com/sites/queues.html')
->openUrlInNewTab(),
CreateAction::make('create')
->icon('heroicon-o-plus')
->createAnother(false)
->modalWidth(MaxWidth::ExtraLarge)
->label('New Queue')
->form([
TextInput::make('command')
->rules(CreateQueue::rules($this->server)['command'])
->helperText('Example: php /home/vito/your-site/artisan queue:work'),
Select::make('user')
->rules(fn (callable $get) => CreateQueue::rules($this->server)['user'])
->options([
'vito' => $this->server->ssh_user,
'root' => 'root',
]),
TextInput::make('numprocs')
->default(1)
->rules(CreateQueue::rules($this->server)['numprocs'])
->helperText('Number of processes'),
Grid::make()
->schema([
Checkbox::make('auto_start')
->default(false),
Checkbox::make('auto_restart')
->default(false),
]),
])
->using(function (array $data) {
run_action($this, function () use ($data) {
app(CreateQueue::class)->create($this->site, $data);
$this->dispatch('$refresh');
});
}),
];
}
}

View File

@ -0,0 +1,160 @@
<?php
namespace App\Web\Pages\Servers\Sites\Pages\Queues\Widgets;
use App\Actions\Queue\DeleteQueue;
use App\Actions\Queue\EditQueue;
use App\Actions\Queue\GetQueueLogs;
use App\Actions\Queue\ManageQueue;
use App\Models\Queue;
use App\Models\Site;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Support\Enums\MaxWidth;
use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\ActionGroup;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Actions\EditAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget as Widget;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\View\ComponentAttributeBag;
class QueuesList extends Widget
{
public Site $site;
protected $listeners = ['$refresh'];
protected function getTableQuery(): Builder
{
return Queue::query()->where('site_id', $this->site->id);
}
protected static ?string $heading = '';
protected function getTableColumns(): array
{
return [
TextColumn::make('command')
->limit(20)
->copyable()
->tooltip(fn (Queue $record) => $record->command)
->searchable()
->sortable(),
TextColumn::make('created_at')
->formatStateUsing(fn (Queue $record) => $record->created_at_by_timezone)
->sortable(),
TextColumn::make('status')
->label('Status')
->badge()
->color(fn (Queue $record) => Queue::$statusColors[$record->status])
->searchable()
->sortable(),
];
}
public function getTable(): Table
{
return $this->table
->actions([
ActionGroup::make([
$this->editAction(),
$this->operationAction('start', 'heroicon-o-play'),
$this->operationAction('stop', 'heroicon-o-stop'),
$this->operationAction('restart', 'heroicon-o-arrow-path'),
$this->logsAction(),
$this->deleteAction(),
]),
]);
}
private function operationAction(string $type, string $icon): Action
{
return Action::make($type)
->authorize(fn (Queue $record) => auth()->user()->can('update', [$record, $this->site, $this->site->server]))
->label(ucfirst($type).' queue')
->icon($icon)
->action(function (Queue $record) use ($type) {
run_action($this, function () use ($record, $type) {
app(ManageQueue::class)->$type($record);
$this->dispatch('$refresh');
});
});
}
private function logsAction(): Action
{
return Action::make('logs')
->icon('heroicon-o-eye')
->authorize(fn (Queue $record) => auth()->user()->can('view', [$record, $this->site, $this->site->server]))
->modalHeading('View Log')
->modalContent(function (Queue $record) {
return view('components.console-view', [
'slot' => app(GetQueueLogs::class)->getLogs($record),
'attributes' => new ComponentAttributeBag,
]);
})
->modalSubmitAction(false)
->modalCancelActionLabel('Close');
}
private function editAction(): Action
{
return EditAction::make('edit')
->icon('heroicon-o-pencil-square')
->authorize(fn (Queue $record) => auth()->user()->can('update', [$record, $this->site, $this->site->server]))
->modalWidth(MaxWidth::ExtraLarge)
->fillForm(fn (Queue $record) => [
'command' => $record->command,
'user' => $record->user,
'numprocs' => $record->numprocs,
'auto_start' => $record->auto_start,
'auto_restart' => $record->auto_restart,
])
->form([
TextInput::make('command')
->rules(EditQueue::rules($this->site->server)['command'])
->helperText('Example: php /home/vito/your-site/artisan queue:work'),
Select::make('user')
->rules(fn (callable $get) => EditQueue::rules($this->site->server)['user'])
->options([
'vito' => $this->site->server->ssh_user,
'root' => 'root',
]),
TextInput::make('numprocs')
->default(1)
->rules(EditQueue::rules($this->site->server)['numprocs'])
->helperText('Number of processes'),
Grid::make()
->schema([
Checkbox::make('auto_start')
->default(false),
Checkbox::make('auto_restart')
->default(false),
]),
])
->using(function (Queue $record, array $data) {
run_action($this, function () use ($record, $data) {
app(EditQueue::class)->edit($record, $data);
$this->dispatch('$refresh');
});
});
}
private function deleteAction(): Action
{
return DeleteAction::make('delete')
->icon('heroicon-o-trash')
->authorize(fn (Queue $record) => auth()->user()->can('delete', [$record, $this->site, $this->site->server]))
->using(function (Queue $record) {
run_action($this, function () use ($record) {
app(DeleteQueue::class)->delete($record);
$this->dispatch('$refresh');
});
});
}
}

View File

@ -2,7 +2,17 @@
namespace App\Web\Pages\Servers\Sites\Pages\SSL;
use App\Actions\SSL\CreateSSL;
use App\Models\Ssl;
use App\Web\Pages\Servers\Sites\Page;
use Filament\Actions\Action;
use Filament\Actions\CreateAction;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Get;
use Filament\Support\Enums\MaxWidth;
class Index extends Page
{
@ -12,11 +22,60 @@ class Index extends Page
public static function canAccess(): bool
{
return true;
return auth()->user()?->can('viewAny', [Ssl::class, static::getSiteFromRoute(), static::getServerFromRoute()]) ?? false;
}
public function getWidgets(): array
{
return [];
return [
[Widgets\SslsList::class, ['site' => $this->site]],
];
}
protected function getHeaderActions(): array
{
return [
Action::make('read-the-docs')
->label('Read the Docs')
->icon('heroicon-o-document-text')
->color('gray')
->url('https://vitodeploy.com/sites/ssl.html')
->openUrlInNewTab(),
CreateAction::make('create')
->label('New Certificate')
->icon('heroicon-o-lock-closed')
->form([
Select::make('type')
->options(
collect(config('core.ssl_types'))->mapWithKeys(fn ($type) => [$type => $type])
)
->rules(fn (Get $get) => CreateSSL::rules($get())['type'])
->reactive(),
Textarea::make('certificate')
->rows(5)
->rules(fn (Get $get) => CreateSSL::rules($get())['certificate'])
->visible(fn (Get $get) => $get('type') === 'custom'),
Textarea::make('private')
->label('Private Key')
->rows(5)
->rules(fn (Get $get) => CreateSSL::rules($get())['private'])
->visible(fn (Get $get) => $get('type') === 'custom'),
DatePicker::make('expires_at')
->format('Y-m-d')
->rules(fn (Get $get) => CreateSSL::rules($get())['expires_at'])
->visible(fn (Get $get) => $get('type') === 'custom'),
Checkbox::make('aliases')
->label("Set SSL for site's aliases as well"),
])
->createAnother(false)
->modalWidth(MaxWidth::Large)
->using(function (array $data) {
run_action($this, function () use ($data) {
app(CreateSSL::class)->create($this->site, $data);
$this->dispatch('$refresh');
});
}),
];
}
}

View File

@ -0,0 +1,81 @@
<?php
namespace App\Web\Pages\Servers\Sites\Pages\SSL\Widgets;
use App\Actions\SSL\DeleteSSL;
use App\Models\Site;
use App\Models\Ssl;
use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget as Widget;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\View\ComponentAttributeBag;
class SslsList extends Widget
{
public Site $site;
protected $listeners = ['$refresh'];
protected function getTableQuery(): Builder
{
return Ssl::query()->where('site_id', $this->site->id);
}
protected static ?string $heading = '';
protected function getTableColumns(): array
{
return [
TextColumn::make('type')
->searchable()
->sortable(),
TextColumn::make('created_at')
->formatStateUsing(fn (Ssl $record) => $record->created_at_by_timezone)
->sortable(),
TextColumn::make('expires_at')
->formatStateUsing(fn (Ssl $record) => $record->getDateTimeByTimezone($record->expires_at))
->sortable(),
TextColumn::make('status')
->label('Status')
->badge()
->color(fn (Ssl $record) => Ssl::$statusColors[$record->status])
->searchable()
->sortable(),
];
}
public function getTable(): Table
{
return $this->table
->actions([
Action::make('logs')
->hiddenLabel()
->tooltip('Logs')
->icon('heroicon-o-eye')
->authorize(fn (Ssl $record) => auth()->user()->can('view', [$record, $this->site, $this->site->server]))
->modalHeading('View Log')
->modalContent(function (Ssl $record) {
return view('components.console-view', [
'slot' => $record->log?->getContent(),
'attributes' => new ComponentAttributeBag,
]);
})
->modalSubmitAction(false)
->modalCancelActionLabel('Close'),
DeleteAction::make('delete')
->hiddenLabel()
->tooltip('Delete')
->icon('heroicon-o-trash')
->authorize(fn (Ssl $record) => auth()->user()->can('delete', [$record, $this->site, $this->site->server]))
->using(function (Ssl $record) {
run_action($this, function () use ($record) {
app(DeleteSSL::class)->delete($record);
$this->dispatch('$refresh');
});
}),
]);
}
}

View File

@ -2,6 +2,7 @@
namespace App\Web\Pages\Servers\Sites;
use App\Actions\Site\DeleteSite;
use App\SSH\Services\Webserver\Webserver;
use App\Web\Fields\CodeEditorField;
use Filament\Actions\Action;
@ -40,16 +41,23 @@ private function deleteAction(): Action
{
return DeleteAction::make()
->icon('heroicon-o-trash')
->record($this->server)
->record($this->site)
->modalHeading('Delete Site')
->modalDescription('Once your site is deleted, all of its resources and data will be permanently deleted and can\'t be restored');
->modalDescription('Once your site is deleted, all of its resources and data will be permanently deleted and can\'t be restored')
->using(function () {
run_action($this, function () {
app(DeleteSite::class)->delete($this->site);
$this->redirect(Index::getUrl(['server' => $this->server]));
});
});
}
private function vhostAction(): Action
{
return Action::make('vhost')
->color('gray')
->icon('si-nginx')
->icon('icon-nginx')
->label('VHost')
->modalSubmitActionLabel('Save')
->form([

View File

@ -79,11 +79,19 @@ public function getWidgets(): array
public function getHeaderActions(): array
{
$actions = [];
$actions = [
Action::make('read-the-docs')
->label('Read the Docs')
->icon('heroicon-o-document-text')
->color('gray')
->url('https://vitodeploy.com/sites/application.html')
->openUrlInNewTab(),
];
$actionsGroup = [];
if (in_array(SiteFeature::DEPLOYMENT, $this->site->type()->supportedFeatures())) {
$actions[] = $this->deployAction();
$actionsGroup[] = $this->autoDeploymentAction();
$actionsGroup[] = $this->deploymentScriptAction();
}
@ -130,6 +138,27 @@ private function deployAction(): Action
});
}
private function autoDeploymentAction(): Action
{
return Action::make('auto-deployment')
->label(fn () => $this->site->isAutoDeployment() ? 'Disable Auto Deployment' : 'Enable Auto Deployment')
->modalHeading(fn () => $this->site->isAutoDeployment() ? 'Disable Auto Deployment' : 'Enable Auto Deployment')
->modalIconColor(fn () => $this->site->isAutoDeployment() ? 'red' : 'green')
->requiresConfirmation()
->action(function () {
run_action($this, function () {
$this->site->isAutoDeployment()
? $this->site->disableAutoDeployment()
: $this->site->enableAutoDeployment();
Notification::make()
->success()
->title('Auto deployment '.($this->site->isAutoDeployment() ? 'disabled' : 'enabled').'!')
->send();
});
});
}
private function deploymentScriptAction(): Action
{
return Action::make('deployment-script')

View File

@ -7,6 +7,7 @@
use App\Web\Pages\Servers\Sites\Settings;
use App\Web\Pages\Servers\Sites\View;
use Filament\Tables\Actions\Action;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget as Widget;
@ -26,6 +27,10 @@ protected function getTableQuery(): Builder
protected function getTableColumns(): array
{
return [
IconColumn::make('type')
->icon(fn (Site $record) => 'icon-'.$record->type)
->tooltip(fn (Site $record) => $record->type)
->width(24),
TextColumn::make('domain')
->searchable()
->sortable(),

View File

@ -6,6 +6,7 @@
use App\Web\Pages\Servers\Settings;
use App\Web\Pages\Servers\View;
use Filament\Tables\Actions\Action;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget as Widget;
@ -23,6 +24,10 @@ protected function getTableQuery(): Builder
protected function getTableColumns(): array
{
return [
IconColumn::make('provider')
->icon(fn (Server $record) => 'icon-'.$record->provider)
->tooltip(fn (Server $record) => $record->provider)
->width(24),
TextColumn::make('name')
->searchable()
->sortable(),

View File

@ -8,7 +8,7 @@
use Filament\Support\Enums\MaxWidth;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Actions\EditAction;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget as Widget;
@ -26,9 +26,9 @@ protected function getTableQuery(): Builder
protected function getTableColumns(): array
{
return [
ImageColumn::make('image_url')
->label('Provider')
->size(24),
IconColumn::make('provider')
->icon(fn (ServerProvider $record) => 'icon-'.$record->provider)
->width(24),
TextColumn::make('name')
->default(fn ($record) => $record->profile)
->label('Name')

View File

@ -8,7 +8,7 @@
use Filament\Support\Enums\MaxWidth;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Actions\EditAction;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget as Widget;
@ -28,9 +28,9 @@ protected function getTableQuery(): Builder
protected function getTableColumns(): array
{
return [
ImageColumn::make('image_url')
->label('Provider')
->size(24),
IconColumn::make('provider')
->icon(fn (SourceControl $record) => 'icon-'.$record->provider)
->width(24),
TextColumn::make('name')
->default(fn (SourceControl $record) => $record->profile)
->label('Name')

View File

@ -8,7 +8,7 @@
use Filament\Support\Enums\MaxWidth;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Actions\EditAction;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget as Widget;
@ -26,11 +26,12 @@ protected function getTableQuery(): Builder
protected function getTableColumns(): array
{
return [
ImageColumn::make('image_url')
->label('Provider')
->size(24),
IconColumn::make('provider')
->icon(fn (StorageProvider $record) => 'icon-'.$record->provider)
->tooltip(fn (StorageProvider $record) => $record->provider)
->width(24),
TextColumn::make('name')
->default(fn ($record) => $record->profile)
->default(fn (StorageProvider $record) => $record->profile)
->label('Name')
->searchable()
->sortable(),

View File

@ -5,57 +5,91 @@
use App\Actions\Tag\SyncTags;
use App\Models\Server;
use App\Models\Site;
use Filament\Forms\Components\Actions\Action as FormAction;
use Filament\Forms\Components\Select;
use Filament\Infolists\Components\Actions\Action;
use Filament\Infolists\Components\Actions\Action as InfolistAction;
use Filament\Notifications\Notification;
use Filament\Support\Enums\MaxWidth;
use Filament\Tables\Actions\Action as TableAction;
class EditTags
{
/**
* @param Site|Server $taggable
*/
public static function infolist(mixed $taggable): Action
public static function infolist(mixed $taggable): InfolistAction
{
return Action::make('edit_tags')
return InfolistAction::make('edit_tags')
->icon('heroicon-o-pencil-square')
->tooltip('Edit Tags')
->hiddenLabel()
->modalSubmitActionLabel('Save')
->modalHeading('Edit Tags')
->modalWidth(MaxWidth::Medium)
->form([
Select::make('tags')
->default($taggable->tags()->pluck('tags.id')->toArray())
->options(function () {
return auth()->user()->currentProject->tags()->pluck('name', 'id')->toArray();
})
->nestedRecursiveRules(SyncTags::rules(auth()->user()->currentProject->id)['tags.*'])
->suffixAction(
\Filament\Forms\Components\Actions\Action::make('create_tag')
->icon('heroicon-o-plus')
->tooltip('Create a new tag')
->modalSubmitActionLabel('Create')
->modalHeading('Create Tag')
->modalWidth(MaxWidth::Medium)
->form(Create::form())
->action(function (array $data) {
Create::action($data);
}),
)
->multiple(),
])
->action(function (array $data) use ($taggable) {
app(SyncTags::class)->sync(auth()->user(), [
'taggable_id' => $taggable->id,
'taggable_type' => get_class($taggable),
'tags' => $data['tags'],
]);
->form(static::form($taggable))
->action(static::action($taggable));
}
Notification::make()
->success()
->title('Tags updated!')
->send();
});
/**
* @param Site|Server $taggable
*/
public static function table(mixed $taggable): TableAction
{
return TableAction::make('edit_tags')
->icon('heroicon-o-pencil-square')
->tooltip('Edit Tags')
->hiddenLabel()
->modalSubmitActionLabel('Save')
->modalHeading('Edit Tags')
->modalWidth(MaxWidth::Medium)
->form(static::form($taggable))
->action(static::action($taggable));
}
/**
* @param Site|Server $taggable
*/
private static function form(mixed $taggable): array
{
return [
Select::make('tags')
->default($taggable->tags()->pluck('tags.id')->toArray())
->options(function () {
return auth()->user()->currentProject->tags()->pluck('name', 'id')->toArray();
})
->nestedRecursiveRules(SyncTags::rules(auth()->user()->currentProject->id)['tags.*'])
->suffixAction(
FormAction::make('create_tag')
->icon('heroicon-o-plus')
->tooltip('Create a new tag')
->modalSubmitActionLabel('Create')
->modalHeading('Create Tag')
->modalWidth(MaxWidth::Medium)
->form(Create::form())
->action(function (array $data) {
Create::action($data);
}),
)
->multiple(),
];
}
/**
* @param Site|Server $taggable
*/
private static function action(mixed $taggable): \Closure
{
return function (array $data) use ($taggable) {
app(SyncTags::class)->sync(auth()->user(), [
'taggable_id' => $taggable->id,
'taggable_type' => get_class($taggable),
'tags' => $data['tags'],
]);
Notification::make()
->success()
->title('Tags updated!')
->send();
};
}
}