From efacadba10f4fe163e746922fc3298d39c097f6e Mon Sep 17 00:00:00 2001 From: Saeed Vaziry Date: Sun, 1 Jun 2025 12:02:25 +0200 Subject: [PATCH] #591 - node --- .../NodeJS/InstallNewNodeJsVersion.php | 51 ------------ app/Actions/NodeJS/UninstallNodeJS.php | 60 --------------- app/Actions/PHP/UninstallPHP.php | 60 --------------- app/Http/Controllers/NodeController.php | 50 ++++++++++++ resources/js/layouts/server/layout.tsx | 2 +- resources/js/pages/console/index.tsx | 2 +- resources/js/pages/monitoring/index.tsx | 12 +-- .../js/pages/node/components/columns.tsx | 77 +++++++++++++++++++ .../js/pages/node/components/default-cli.tsx | 64 +++++++++++++++ resources/js/pages/node/index.tsx | 42 ++++++++++ .../pages/services/components/uninstall.tsx | 6 +- tests/Feature/NodeJSTest.php | 61 ++------------- 12 files changed, 252 insertions(+), 235 deletions(-) delete mode 100755 app/Actions/NodeJS/InstallNewNodeJsVersion.php delete mode 100755 app/Actions/NodeJS/UninstallNodeJS.php delete mode 100755 app/Actions/PHP/UninstallPHP.php create mode 100644 app/Http/Controllers/NodeController.php create mode 100644 resources/js/pages/node/components/columns.tsx create mode 100644 resources/js/pages/node/components/default-cli.tsx create mode 100644 resources/js/pages/node/index.tsx diff --git a/app/Actions/NodeJS/InstallNewNodeJsVersion.php b/app/Actions/NodeJS/InstallNewNodeJsVersion.php deleted file mode 100755 index d9cfdea1..00000000 --- a/app/Actions/NodeJS/InstallNewNodeJsVersion.php +++ /dev/null @@ -1,51 +0,0 @@ - $input - */ - public function install(Server $server, array $input): void - { - $nodejs = new Service([ - 'server_id' => $server->id, - 'type' => 'nodejs', - 'type_data' => [], - 'name' => 'nodejs', - 'version' => $input['version'], - 'status' => ServiceStatus::INSTALLING, - 'is_default' => false, - ]); - $nodejs->save(); - - dispatch(function () use ($nodejs): void { - $nodejs->handler()->install(); - $nodejs->status = ServiceStatus::READY; - $nodejs->save(); - })->catch(function () use ($nodejs): void { - $nodejs->delete(); - })->onConnection('ssh'); - } - - /** - * @return array> - */ - public static function rules(Server $server): array - { - return [ - 'version' => [ - 'required', - Rule::in(config('core.nodejs_versions')), - Rule::notIn(array_merge($server->installedNodejsVersions(), [NodeJS::NONE])), - ], - ]; - } -} diff --git a/app/Actions/NodeJS/UninstallNodeJS.php b/app/Actions/NodeJS/UninstallNodeJS.php deleted file mode 100755 index 4edde1a2..00000000 --- a/app/Actions/NodeJS/UninstallNodeJS.php +++ /dev/null @@ -1,60 +0,0 @@ - $input - * - * @throws ValidationException - */ - public function uninstall(Server $server, array $input): void - { - $this->validate($server, $input); - - /** @var Service $nodejs */ - $nodejs = $server->nodejs($input['version']); - $nodejs->status = ServiceStatus::UNINSTALLING; - $nodejs->save(); - - dispatch(function () use ($nodejs): void { - $nodejs->handler()->uninstall(); - $nodejs->delete(); - })->catch(function () use ($nodejs): void { - $nodejs->status = ServiceStatus::FAILED; - $nodejs->save(); - })->onConnection('ssh'); - } - - /** - * @param array $input - * - * @throws ValidationException - */ - private function validate(Server $server, array $input): void - { - Validator::make($input, [ - 'version' => 'required|string', - ])->validate(); - - if (! in_array($input['version'], $server->installedNodejsVersions())) { - throw ValidationException::withMessages( - ['version' => __('This version is not installed')] - ); - } - - $hasSite = $server->sites()->where('nodejs_version', $input['version'])->first(); - if ($hasSite) { - throw ValidationException::withMessages( - ['version' => __('Cannot uninstall this version because some sites are using it!')] - ); - } - } -} diff --git a/app/Actions/PHP/UninstallPHP.php b/app/Actions/PHP/UninstallPHP.php deleted file mode 100755 index 33e01593..00000000 --- a/app/Actions/PHP/UninstallPHP.php +++ /dev/null @@ -1,60 +0,0 @@ - $input - * - * @throws ValidationException - */ - public function uninstall(Server $server, array $input): void - { - $this->validate($server, $input); - - /** @var Service $php */ - $php = $server->php($input['version']); - $php->status = ServiceStatus::UNINSTALLING; - $php->save(); - - dispatch(function () use ($php): void { - $php->handler()->uninstall(); - $php->delete(); - })->catch(function () use ($php): void { - $php->status = ServiceStatus::FAILED; - $php->save(); - })->onConnection('ssh'); - } - - /** - * @param array $input - * - * @throws ValidationException - */ - private function validate(Server $server, array $input): void - { - Validator::make($input, [ - 'version' => 'required|string', - ])->validate(); - - if (! in_array($input['version'], $server->installedPHPVersions())) { - throw ValidationException::withMessages( - ['version' => __('This version is not installed')] - ); - } - - $hasSite = $server->sites()->where('php_version', $input['version'])->first(); - if ($hasSite) { - throw ValidationException::withMessages( - ['version' => __('Cannot uninstall this version because some sites are using it!')] - ); - } - } -} diff --git a/app/Http/Controllers/NodeController.php b/app/Http/Controllers/NodeController.php new file mode 100644 index 00000000..1dd503e2 --- /dev/null +++ b/app/Http/Controllers/NodeController.php @@ -0,0 +1,50 @@ +authorize('viewAny', [Service::class, $server]); + + $installedVersions = Service::query() + ->where('type', 'nodejs') + ->where('server_id', $server->id) + ->simplePaginate(config('web.pagination_size')); + + return Inertia::render('node/index', [ + 'installedVersions' => ServiceResource::collection($installedVersions), + ]); + } + + /** + * @throws SSHError + */ + #[Post('/{service}/default-cli', name: 'node.default-cli')] + public function defaultCli(Request $request, Server $server, Service $service): RedirectResponse + { + $this->authorize('update', $service); + + app(ChangeDefaultCli::class)->change($server, $request->input()); + + return back()->with('success', 'Default Node CLI changed to '.$service->version.'.'); + } +} diff --git a/resources/js/layouts/server/layout.tsx b/resources/js/layouts/server/layout.tsx index 2d8091d9..2d86108a 100644 --- a/resources/js/layouts/server/layout.tsx +++ b/resources/js/layouts/server/layout.tsx @@ -102,7 +102,7 @@ export default function ServerLayout({ children }: { children: ReactNode }) { }, { title: 'NodeJS', - href: '#', + href: route('node', { server: page.props.server.id }), icon: NodeIcon, isDisabled: isMenuDisabled, }, diff --git a/resources/js/pages/console/index.tsx b/resources/js/pages/console/index.tsx index dd4288c1..ab7f5b02 100644 --- a/resources/js/pages/console/index.tsx +++ b/resources/js/pages/console/index.tsx @@ -241,7 +241,7 @@ export default function Console() { type="text" value={command} onChange={(e) => setCommand(e.target.value)} - className="ml-2 h-auto flex-grow border-0 bg-transparent! p-0 shadow-none ring-0 outline-none focus:ring-0 focus:outline-none focus-visible:ring-0" + className="ml-2 h-auto flex-grow border-0 bg-transparent! px-0 shadow-none ring-0 outline-none focus:ring-0 focus:outline-none focus-visible:ring-0" autoComplete="off" autoFocus /> diff --git a/resources/js/pages/monitoring/index.tsx b/resources/js/pages/monitoring/index.tsx index 699a1c14..51808092 100644 --- a/resources/js/pages/monitoring/index.tsx +++ b/resources/js/pages/monitoring/index.tsx @@ -68,16 +68,16 @@ export default function Monitoring() {
- {page.props.lastMetric ? kbToGb(page.props.lastMetric.memory_used) + ' GB' : 'N/A'} Used + {page.props.lastMetric ? kbToGb(page.props.lastMetric.memory_used) + ' GB' : 'N/A'}
- {page.props.lastMetric ? kbToGb(page.props.lastMetric.memory_free) + ' GB' : 'N/A'} Free + {page.props.lastMetric ? kbToGb(page.props.lastMetric.memory_free) + ' GB' : 'N/A'}
- {page.props.lastMetric ? kbToGb(page.props.lastMetric.memory_total) + ' GB' : 'N/A'} Total + {page.props.lastMetric ? kbToGb(page.props.lastMetric.memory_total) + ' GB' : 'N/A'}
@@ -88,16 +88,16 @@ export default function Monitoring() {
- {page.props.lastMetric ? mbToGb(page.props.lastMetric.disk_used) + ' GB' : 'N/A'} Used + {page.props.lastMetric ? mbToGb(page.props.lastMetric.disk_used) + ' GB' : 'N/A'}
- {page.props.lastMetric ? mbToGb(page.props.lastMetric.disk_free) + ' GB' : 'N/A'} Free + {page.props.lastMetric ? mbToGb(page.props.lastMetric.disk_free) + ' GB' : 'N/A'}
- {page.props.lastMetric ? mbToGb(page.props.lastMetric.disk_total) + ' GB' : 'N/A'} Total + {page.props.lastMetric ? mbToGb(page.props.lastMetric.disk_total) + ' GB' : 'N/A'}
diff --git a/resources/js/pages/node/components/columns.tsx b/resources/js/pages/node/components/columns.tsx new file mode 100644 index 00000000..72661311 --- /dev/null +++ b/resources/js/pages/node/components/columns.tsx @@ -0,0 +1,77 @@ +import { ColumnDef } from '@tanstack/react-table'; +import { DropdownMenu, DropdownMenuContent, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; +import { Button } from '@/components/ui/button'; +import { MoreVerticalIcon } from 'lucide-react'; +import React from 'react'; +import { Service } from '@/types/service'; +import { Badge } from '@/components/ui/badge'; +import DateTime from '@/components/date-time'; +import Uninstall from '@/pages/services/components/uninstall'; +import { Action } from '@/pages/services/components/action'; +import DefaultCli from '@/pages/node/components/default-cli'; + +export const columns: ColumnDef[] = [ + { + accessorKey: 'version', + header: 'Version', + enableColumnFilter: true, + enableSorting: true, + }, + { + accessorKey: 'created_at', + header: 'Installed at', + enableColumnFilter: true, + enableSorting: true, + cell: ({ row }) => { + return ; + }, + }, + { + accessorKey: 'is_default', + header: 'Default cli', + enableColumnFilter: true, + enableSorting: true, + cell: ({ row }) => { + return {row.original.is_default ? 'Yes' : 'No'}; + }, + }, + { + accessorKey: 'status', + header: 'Status', + enableColumnFilter: true, + enableSorting: true, + cell: ({ row }) => { + return {row.original.status}; + }, + }, + { + id: 'actions', + enableColumnFilter: false, + enableSorting: false, + cell: ({ row }) => { + return ( +
+ + + + + + + + + + + + + + + + +
+ ); + }, + }, +]; diff --git a/resources/js/pages/node/components/default-cli.tsx b/resources/js/pages/node/components/default-cli.tsx new file mode 100644 index 00000000..c3da2916 --- /dev/null +++ b/resources/js/pages/node/components/default-cli.tsx @@ -0,0 +1,64 @@ +import { Service } from '@/types/service'; +import React, { FormEvent, useState } from 'react'; +import { useForm } from '@inertiajs/react'; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { DropdownMenuItem } from '@/components/ui/dropdown-menu'; +import { Button } from '@/components/ui/button'; +import { LoaderCircleIcon } from 'lucide-react'; +import InputError from '@/components/ui/input-error'; + +export default function DefaultCli({ service }: { service: Service }) { + const [open, setOpen] = useState(false); + const form = useForm<{ + version: string; + }>({ + version: service.version, + }); + + const submit = (e: FormEvent) => { + e.preventDefault(); + form.post(route('node.default-cli', { server: service.server_id, service: service.id }), { + onSuccess: () => { + setOpen(false); + }, + }); + }; + + return ( + + + e.preventDefault()}> + Make default cli + + + + + Make default cli + Make default cli + +
+

Are you sure you want to make Nodejs {form.data.version} the default cli?

+ +
+ + + + + + +
+
+ ); +} diff --git a/resources/js/pages/node/index.tsx b/resources/js/pages/node/index.tsx new file mode 100644 index 00000000..1ba9ae92 --- /dev/null +++ b/resources/js/pages/node/index.tsx @@ -0,0 +1,42 @@ +import { Head, usePage } from '@inertiajs/react'; +import { Server } from '@/types/server'; +import { PaginatedData } from '@/types'; +import ServerLayout from '@/layouts/server/layout'; +import HeaderContainer from '@/components/header-container'; +import Heading from '@/components/heading'; +import { Button } from '@/components/ui/button'; +import { PlusIcon } from 'lucide-react'; +import Container from '@/components/container'; +import { Service } from '@/types/service'; +import InstallService from '@/pages/services/components/install'; +import { DataTable } from '@/components/data-table'; +import { columns } from '@/pages/node/components/columns'; + +export default function PHP() { + const page = usePage<{ + server: Server; + installedVersions: PaginatedData; + }>(); + + return ( + + + + + + +
+ + + +
+
+ + +
+
+ ); +} diff --git a/resources/js/pages/services/components/uninstall.tsx b/resources/js/pages/services/components/uninstall.tsx index dc8ab428..19935bc5 100644 --- a/resources/js/pages/services/components/uninstall.tsx +++ b/resources/js/pages/services/components/uninstall.tsx @@ -15,6 +15,7 @@ import { DropdownMenuItem } from '@/components/ui/dropdown-menu'; import { Button } from '@/components/ui/button'; import { LoaderCircleIcon } from 'lucide-react'; import FormSuccessful from '@/components/form-successful'; +import InputError from '@/components/ui/input-error'; export default function Uninstall({ service }: { service: Service }) { const [open, setOpen] = useState(false); @@ -39,7 +40,10 @@ export default function Uninstall({ service }: { service: Service }) { Uninstall service Uninstall service -

Are you sure you want to uninstall this service? This action cannot be undone.

+
+

Are you sure you want to uninstall this service? This action cannot be undone.

+ +
diff --git a/tests/Feature/NodeJSTest.php b/tests/Feature/NodeJSTest.php index f5ed652a..1cbab054 100644 --- a/tests/Feature/NodeJSTest.php +++ b/tests/Feature/NodeJSTest.php @@ -6,64 +6,13 @@ use App\Enums\ServiceStatus; use App\Facades\SSH; use App\Models\Service; -use App\Web\Pages\Servers\NodeJS\Index; -use App\Web\Pages\Servers\NodeJS\Widgets\NodeJSList; use Illuminate\Foundation\Testing\RefreshDatabase; -use Livewire\Livewire; use Tests\TestCase; class NodeJSTest extends TestCase { use RefreshDatabase; - public function test_install_new_nodejs(): void - { - SSH::fake(); - - $this->actingAs($this->user); - - Livewire::test(Index::class, ['server' => $this->server]) - ->callAction('install', [ - 'version' => NodeJS::V16, - ]) - ->assertSuccessful(); - - $this->assertDatabaseHas('services', [ - 'server_id' => $this->server->id, - 'type' => 'nodejs', - 'version' => NodeJS::V16, - 'status' => ServiceStatus::READY, - ]); - } - - public function test_uninstall_nodejs(): void - { - SSH::fake(); - - $this->actingAs($this->user); - - $php = new Service([ - 'server_id' => $this->server->id, - 'type' => 'nodejs', - 'type_data' => [], - 'name' => 'nodejs', - 'version' => NodeJS::V16, - 'status' => ServiceStatus::READY, - 'is_default' => true, - ]); - $php->save(); - - Livewire::test(NodeJSList::class, [ - 'server' => $this->server, - ]) - ->callTableAction('uninstall', $php->id) - ->assertSuccessful(); - - $this->assertDatabaseMissing('services', [ - 'id' => $php->id, - ]); - } - public function test_change_default_nodejs_cli(): void { SSH::fake(); @@ -81,11 +30,13 @@ public function test_change_default_nodejs_cli(): void 'is_default' => false, ]); - Livewire::test(NodeJSList::class, [ - 'server' => $this->server, + $this->post(route('node.default-cli', [ + 'server' => $this->server->id, + 'service' => $service->id, + ]), [ + 'version' => NodeJS::V16, ]) - ->callTableAction('default-nodejs-cli', $service->id) - ->assertSuccessful(); + ->assertSessionDoesntHaveErrors(); $service->refresh();