From 82ef6c1408c7cebee8c00d8ac989e379f7a8bf70 Mon Sep 17 00:00:00 2001 From: Charles Date: Fri, 2 May 2025 12:15:05 -0400 Subject: [PATCH] Add server power actions to new context menu (#1321) * add server power action context menu * Update app/Filament/App/Resources/ServerResource/Pages/ListServers.php Co-authored-by: Boy132 * Cleanup * Add missed enable --------- Co-authored-by: Boy132 Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com> --- .../ServerResource/Pages/ListServers.php | 85 +++++++++++++++++-- composer.json | 1 + composer.lock | 82 +++++++++++++++++- lang/en/exceptions.php | 1 + .../filament-context-menu-styles.css | 1 + 5 files changed, 162 insertions(+), 8 deletions(-) create mode 100644 public/css/aymanalhattami/filament-context-menu/filament-context-menu-styles.css diff --git a/app/Filament/App/Resources/ServerResource/Pages/ListServers.php b/app/Filament/App/Resources/ServerResource/Pages/ListServers.php index a961d5036..66651ab56 100644 --- a/app/Filament/App/Resources/ServerResource/Pages/ListServers.php +++ b/app/Filament/App/Resources/ServerResource/Pages/ListServers.php @@ -6,15 +6,22 @@ use App\Enums\ServerResourceType; use App\Filament\App\Resources\ServerResource; use App\Filament\Components\Tables\Columns\ServerEntryColumn; use App\Filament\Server\Pages\Console; +use App\Models\Permission; use App\Models\Server; +use App\Repositories\Daemon\DaemonPowerRepository; +use AymanAlhattami\FilamentContextMenu\Columns\ContextMenuTextColumn; +use Filament\Notifications\Notification; use Filament\Resources\Components\Tab; use Filament\Resources\Pages\ListRecords; +use Filament\Tables\Actions\Action; use Filament\Tables\Columns\ColumnGroup; use Filament\Tables\Columns\Layout\Stack; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Http\Client\ConnectionException; +use Livewire\Attributes\On; class ListServers extends ListRecords { @@ -24,12 +31,51 @@ class ListServers extends ListRecords public const WARNING_THRESHOLD = 0.7; + private DaemonPowerRepository $daemonPowerRepository; + + public function boot(): void + { + $this->daemonPowerRepository = new DaemonPowerRepository(); + } + public function table(Table $table): Table { $baseQuery = auth()->user()->accessibleServers(); + $menuOptions = function (Server $server) { + $status = $server->retrieveStatus(); + + return [ + Action::make('start') + ->color('primary') + ->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_START, $server)) + ->visible(fn () => $status->isStartable()) + ->dispatch('powerAction', ['server' => $server, 'action' => 'start']) + ->icon('tabler-player-play-filled'), + Action::make('restart') + ->color('gray') + ->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_RESTART, $server)) + ->visible(fn () => $status->isRestartable()) + ->dispatch('powerAction', ['server' => $server, 'action' => 'restart']) + ->icon('tabler-refresh'), + Action::make('stop') + ->color('danger') + ->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server)) + ->visible(fn () => $status->isStoppable()) + ->dispatch('powerAction', ['server' => $server, 'action' => 'stop']) + ->icon('tabler-player-stop-filled'), + Action::make('kill') + ->color('danger') + ->tooltip('This can result in data corruption and/or data loss!') + ->dispatch('powerAction', ['server' => $server, 'action' => 'kill']) + ->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server)) + ->visible(fn () => $status->isKillable()) + ->icon('tabler-alert-square'), + ]; + }; + $viewOne = [ - TextColumn::make('condition') + ContextMenuTextColumn::make('condition') ->label('') ->default('unknown') ->wrap() @@ -37,20 +83,24 @@ class ListServers extends ListRecords ->alignCenter() ->tooltip(fn (Server $server) => $server->formatResource('uptime', type: ServerResourceType::Time)) ->icon(fn (Server $server) => $server->condition->getIcon()) - ->color(fn (Server $server) => $server->condition->getColor()), + ->color(fn (Server $server) => $server->condition->getColor()) + ->contextMenuActions($menuOptions) + ->enableContextMenu(fn (Server $server) => !$server->isInConflictState()), ]; $viewTwo = [ - TextColumn::make('name') + ContextMenuTextColumn::make('name') ->label('') ->size('md') - ->searchable(), - TextColumn::make('') + ->searchable() + ->contextMenuActions($menuOptions) + ->enableContextMenu(fn (Server $server) => !$server->isInConflictState()), + ContextMenuTextColumn::make('allocation.address') ->label('') ->badge() ->copyable(request()->isSecure()) - ->copyMessage(fn (Server $server, string $state) => 'Copied ' . $server->allocation->address) - ->state(fn (Server $server) => $server->allocation->address), + ->contextMenuActions($menuOptions) + ->enableContextMenu(fn (Server $server) => !$server->isInConflictState()), ]; $viewThree = [ @@ -190,4 +240,25 @@ class ListServers extends ListRecords return null; } + + #[On('powerAction')] + public function powerAction(Server $server, string $action): void + { + try { + $this->daemonPowerRepository->setServer($server)->send($action); + + Notification::make() + ->title('Power Action') + ->body($action . ' sent to ' . $server->name) + ->success() + ->send(); + + $this->redirect(self::getUrl(['activeTab' => $this->activeTab])); + } catch (ConnectionException) { + Notification::make() + ->title(trans('exceptions.node.error_connecting', ['node' => $server->node->name])) + ->danger() + ->send(); + } + } } diff --git a/composer.json b/composer.json index 6fd8e5d00..82b155385 100644 --- a/composer.json +++ b/composer.json @@ -10,6 +10,7 @@ "ext-zip": "*", "abdelhamiderrahmouni/filament-monaco-editor": "^0.2.5", "aws/aws-sdk-php": "^3.342", + "aymanalhattami/filament-context-menu": "^1.0", "calebporzio/sushi": "^2.5", "chillerlan/php-qrcode": "^5.0.2", "dedoc/scramble": "^0.12.10", diff --git a/composer.lock b/composer.lock index 1301326bb..c66a27179 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,11 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], +<<<<<<< HEAD + "content-hash": "27da5be2bf613398e6fa75cdf8131bf7", +======= "content-hash": "5c5a89d4207dd9efb7a8c3a410aa838c", +>>>>>>> main "packages": [ { "name": "abdelhamiderrahmouni/filament-monaco-editor", @@ -1115,6 +1119,82 @@ }, "time": "2025-05-01T18:05:02+00:00" }, + { + "name": "aymanalhattami/filament-context-menu", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/aymanalhattami/filament-context-menu.git", + "reference": "5118d36303e86891d3037e6e26882d548b880b9c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aymanalhattami/filament-context-menu/zipball/5118d36303e86891d3037e6e26882d548b880b9c", + "reference": "5118d36303e86891d3037e6e26882d548b880b9c", + "shasum": "" + }, + "require": { + "filament/filament": "^3.0", + "php": "^8.2", + "spatie/laravel-package-tools": "^1.15.0" + }, + "require-dev": { + "larastan/larastan": "^2.0", + "laravel/pint": "^1.0", + "nunomaduro/collision": "^8.0", + "orchestra/testbench": "^9.0", + "pestphp/pest": "^3.0", + "pestphp/pest-plugin-arch": "^3.0", + "pestphp/pest-plugin-laravel": "^3.0", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Skeleton": "AymanAlhattami\\FilamentContextMenu\\Facades\\FilamentContextMenu" + }, + "providers": [ + "AymanAlhattami\\FilamentContextMenu\\FilamentContextMenuServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "AymanAlhattami\\FilamentContextMenu\\": "src/", + "AymanAlhattami\\FilamentContextMenu\\Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ayman Alhattami", + "email": "ayman.m.alhattami@gmail.com", + "role": "Developer" + } + ], + "description": "context menu (right click menu) for filament", + "homepage": "https://github.com/aymanalhattami/filament-context-menu", + "keywords": [ + "ayman alhattami", + "context menu", + "filament", + "filament admin panel", + "filament context menu", + "filament_context_menu", + "laravel" + ], + "support": { + "issues": "https://github.com/aymanalhattami/filament-context-menu/issues", + "source": "https://github.com/aymanalhattami/filament-context-menu" + }, + "time": "2024-09-22T10:47:31+00:00" + }, { "name": "blade-ui-kit/blade-heroicons", "version": "2.6.0", @@ -15903,7 +15983,7 @@ "ext-pdo": "*", "ext-zip": "*" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { "php": "8.2" }, diff --git a/lang/en/exceptions.php b/lang/en/exceptions.php index 711ce7ac9..ed17349d2 100644 --- a/lang/en/exceptions.php +++ b/lang/en/exceptions.php @@ -4,6 +4,7 @@ return [ 'daemon_connection_failed' => 'There was an exception while attempting to communicate with the daemon resulting in a HTTP/:code response code. This exception has been logged.', 'node' => [ 'servers_attached' => 'A node must have no servers linked to it in order to be deleted.', + 'error_connecting' => 'Error connecting to :node', 'daemon_off_config_updated' => 'The daemon configuration has been updated, however there was an error encountered while attempting to automatically update the configuration file on the Daemon. You will need to manually update the configuration file (config.yml) for the daemon to apply these changes.', ], 'allocations' => [ diff --git a/public/css/aymanalhattami/filament-context-menu/filament-context-menu-styles.css b/public/css/aymanalhattami/filament-context-menu/filament-context-menu-styles.css new file mode 100644 index 000000000..49943cf90 --- /dev/null +++ b/public/css/aymanalhattami/filament-context-menu/filament-context-menu-styles.css @@ -0,0 +1 @@ +/*! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.fixed{position:fixed}.relative{position:relative}.z-50{z-index:50}.my-2{margin-top:.5rem;margin-bottom:.5rem}.mt-1{margin-top:.25rem}.block{display:block}.flex{display:flex}.h-px{height:1px}.w-full{width:100%}.min-w-48{min-width:12rem}.max-w-2xl{max-width:42rem}.cursor-default{cursor:default}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize{resize:both}.justify-between{justify-content:space-between}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.rounded{border-radius:.25rem}.rounded-md{border-radius:.375rem}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.p-2{padding:.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.pl-8{padding-left:2rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-neutral-800{--tw-text-opacity:1;color:rgb(38 38 38/var(--tw-text-opacity))}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid #0000;outline-offset:2px}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-gray-950\/5{--tw-ring-color:#0307120d}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.context-menu-filament-action a,.context-menu-filament-action button{width:100%;justify-content:flex-start}.hover\:bg-neutral-100:hover{--tw-bg-opacity:1;background-color:rgb(245 245 245/var(--tw-bg-opacity))}.data-\[disabled\]\:pointer-events-none[data-disabled]{pointer-events:none}.data-\[disabled\]\:opacity-50[data-disabled]{opacity:.5}@media (prefers-color-scheme:dark){.dark\:bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}.dark\:bg-white\/5{background-color:#ffffff0d}.dark\:text-gray-200{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity))}.dark\:ring-white\/10{--tw-ring-color:#ffffff1a}.dark\:hover\:bg-white\/5:hover{background-color:#ffffff0d}} \ No newline at end of file