mirror of
https://github.com/vitodeploy/vito.git
synced 2025-07-01 05:56:16 +00:00
#591 - php
This commit is contained in:
@ -21,6 +21,13 @@ public function change(Server $server, array $input): void
|
|||||||
$this->validate($server, $input);
|
$this->validate($server, $input);
|
||||||
/** @var Service $service */
|
/** @var Service $service */
|
||||||
$service = $server->php($input['version']);
|
$service = $server->php($input['version']);
|
||||||
|
|
||||||
|
if ($service->is_default) {
|
||||||
|
throw ValidationException::withMessages(
|
||||||
|
['version' => __('This version is already the default CLI')]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** @var PHP $handler */
|
/** @var PHP $handler */
|
||||||
$handler = $service->handler();
|
$handler = $service->handler();
|
||||||
$handler->setDefaultCli();
|
$handler->setDefaultCli();
|
||||||
|
@ -1,54 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Actions\PHP;
|
|
||||||
|
|
||||||
use App\Enums\PHP;
|
|
||||||
use App\Enums\ServiceStatus;
|
|
||||||
use App\Models\Server;
|
|
||||||
use App\Models\Service;
|
|
||||||
use Illuminate\Validation\Rule;
|
|
||||||
|
|
||||||
class InstallNewPHP
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $input
|
|
||||||
*/
|
|
||||||
public function install(Server $server, array $input): void
|
|
||||||
{
|
|
||||||
$php = new Service([
|
|
||||||
'server_id' => $server->id,
|
|
||||||
'type' => 'php',
|
|
||||||
'type_data' => [
|
|
||||||
'extensions' => [],
|
|
||||||
'settings' => config('core.php_settings'),
|
|
||||||
],
|
|
||||||
'name' => 'php',
|
|
||||||
'version' => $input['version'],
|
|
||||||
'status' => ServiceStatus::INSTALLING,
|
|
||||||
'is_default' => false,
|
|
||||||
]);
|
|
||||||
$php->save();
|
|
||||||
|
|
||||||
dispatch(function () use ($php): void {
|
|
||||||
$php->handler()->install();
|
|
||||||
$php->status = ServiceStatus::READY;
|
|
||||||
$php->save();
|
|
||||||
})->catch(function () use ($php): void {
|
|
||||||
$php->delete();
|
|
||||||
})->onConnection('ssh');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, array<string>>
|
|
||||||
*/
|
|
||||||
public static function rules(Server $server): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'version' => [
|
|
||||||
'required',
|
|
||||||
Rule::in(config('core.php_versions')),
|
|
||||||
Rule::notIn(array_merge($server->installedPHPVersions(), [PHP::NONE])),
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,6 +5,7 @@
|
|||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use App\Models\Service;
|
use App\Models\Service;
|
||||||
use App\SSH\Services\PHP\PHP;
|
use App\SSH\Services\PHP\PHP;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
@ -12,11 +13,11 @@ class InstallPHPExtension
|
|||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $input
|
* @param array<string, mixed> $input
|
||||||
*
|
|
||||||
* @throws ValidationException
|
|
||||||
*/
|
*/
|
||||||
public function install(Server $server, array $input): Service
|
public function install(Server $server, array $input): Service
|
||||||
{
|
{
|
||||||
|
Validator::make($input, self::rules($server))->validate();
|
||||||
|
|
||||||
/** @var Service $service */
|
/** @var Service $service */
|
||||||
$service = $server->php($input['version']);
|
$service = $server->php($input['version']);
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
use App\Models\Service;
|
use App\Models\Service;
|
||||||
use Illuminate\Filesystem\FilesystemAdapter;
|
use Illuminate\Filesystem\FilesystemAdapter;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
@ -21,6 +22,8 @@ class UpdatePHPIni
|
|||||||
*/
|
*/
|
||||||
public function update(Server $server, array $input): void
|
public function update(Server $server, array $input): void
|
||||||
{
|
{
|
||||||
|
Validator::make($input, self::rules($server))->validate();
|
||||||
|
|
||||||
/** @var Service $service */
|
/** @var Service $service */
|
||||||
$service = $server->php($input['version']);
|
$service = $server->php($input['version']);
|
||||||
|
|
||||||
|
@ -26,6 +26,7 @@ public function install(Server $server, array $input): Service
|
|||||||
'version' => $input['version'],
|
'version' => $input['version'],
|
||||||
'status' => ServiceStatus::INSTALLING,
|
'status' => ServiceStatus::INSTALLING,
|
||||||
]);
|
]);
|
||||||
|
$service->is_default = ! $server->defaultService($input['type']);
|
||||||
|
|
||||||
Validator::make($input, $service->handler()->creationRules($input))->validate();
|
Validator::make($input, $service->handler()->creationRules($input))->validate();
|
||||||
|
|
||||||
|
87
app/Http/Controllers/PHPController.php
Normal file
87
app/Http/Controllers/PHPController.php
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Actions\PHP\ChangeDefaultCli;
|
||||||
|
use App\Actions\PHP\GetPHPIni;
|
||||||
|
use App\Actions\PHP\InstallPHPExtension;
|
||||||
|
use App\Actions\PHP\UpdatePHPIni;
|
||||||
|
use App\Exceptions\SSHError;
|
||||||
|
use App\Http\Resources\ServiceResource;
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Models\Service;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Inertia\Response;
|
||||||
|
use Spatie\RouteAttributes\Attributes\Get;
|
||||||
|
use Spatie\RouteAttributes\Attributes\Middleware;
|
||||||
|
use Spatie\RouteAttributes\Attributes\Patch;
|
||||||
|
use Spatie\RouteAttributes\Attributes\Post;
|
||||||
|
use Spatie\RouteAttributes\Attributes\Prefix;
|
||||||
|
|
||||||
|
#[Prefix('servers/{server}/php')]
|
||||||
|
#[Middleware(['auth', 'has-project'])]
|
||||||
|
class PHPController extends Controller
|
||||||
|
{
|
||||||
|
#[Get('/', name: 'php')]
|
||||||
|
public function index(Server $server): Response
|
||||||
|
{
|
||||||
|
$this->authorize('viewAny', [Service::class, $server]);
|
||||||
|
|
||||||
|
$installedVersions = Service::query()
|
||||||
|
->where('type', 'php')
|
||||||
|
->where('server_id', $server->id)
|
||||||
|
->simplePaginate(config('web.pagination_size'));
|
||||||
|
|
||||||
|
return Inertia::render('php/index', [
|
||||||
|
'installedVersions' => ServiceResource::collection($installedVersions),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Get('/{service}/ini', name: 'php.ini')]
|
||||||
|
public function ini(Request $request, Server $server, Service $service): JsonResponse
|
||||||
|
{
|
||||||
|
$this->authorize('view', $service);
|
||||||
|
|
||||||
|
$ini = app(GetPHPIni::class)->getIni($server, $request->input());
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'ini' => $ini,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Patch('/{service}/ini', name: 'php.ini.update')]
|
||||||
|
public function updateIni(Request $request, Server $server, Service $service): RedirectResponse
|
||||||
|
{
|
||||||
|
$this->authorize('update', $service);
|
||||||
|
|
||||||
|
app(UpdatePHPIni::class)->update($server, $request->input());
|
||||||
|
|
||||||
|
return back()->with('success', 'PHP ini file updated successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Post('/{service}/install-extension', name: 'php.install-extension')]
|
||||||
|
public function installExtension(Request $request, Server $server, Service $service): RedirectResponse
|
||||||
|
{
|
||||||
|
$this->authorize('update', $service);
|
||||||
|
|
||||||
|
app(InstallPHPExtension::class)->install($server, $request->input());
|
||||||
|
|
||||||
|
return back()->with('info', 'PHP extension is being installed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws SSHError
|
||||||
|
*/
|
||||||
|
#[Post('/{service}/default-cli', name: 'php.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 PHP CLI changed to '.$service->version.'.');
|
||||||
|
}
|
||||||
|
}
|
2280
package-lock.json
generated
2280
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -29,6 +29,7 @@
|
|||||||
"@headlessui/react": "^2.2.0",
|
"@headlessui/react": "^2.2.0",
|
||||||
"@hookform/resolvers": "^5.0.1",
|
"@hookform/resolvers": "^5.0.1",
|
||||||
"@inertiajs/react": "^2.0.0",
|
"@inertiajs/react": "^2.0.0",
|
||||||
|
"@monaco-editor/react": "^4.7.0",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.13",
|
"@radix-ui/react-alert-dialog": "^1.1.13",
|
||||||
"@radix-ui/react-avatar": "^1.1.3",
|
"@radix-ui/react-avatar": "^1.1.3",
|
||||||
"@radix-ui/react-checkbox": "^1.1.4",
|
"@radix-ui/react-checkbox": "^1.1.4",
|
||||||
|
32
resources/js/icons/node.tsx
Normal file
32
resources/js/icons/node.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { LucideProps } from 'lucide-react';
|
||||||
|
|
||||||
|
export const NodeIcon = React.forwardRef<SVGSVGElement, LucideProps>(
|
||||||
|
({ color = 'currentColor', size = 24, strokeWidth = 2, className, ...rest }, ref) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
ref={ref}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
version="1.1"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
fill={color}
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
className={className}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill={color}
|
||||||
|
d="M24.007,45.419c-0.574,0-1.143-0.15-1.646-0.44l-5.24-3.103c-0.783-0.438-0.401-0.593-0.143-0.682 c1.044-0.365,1.255-0.448,2.369-1.081c0.117-0.067,0.27-0.043,0.39,0.028l4.026,2.389c0.145,0.079,0.352,0.079,0.486,0l15.697-9.061 c0.145-0.083,0.24-0.251,0.24-0.424V14.932c0-0.181-0.094-0.342-0.243-0.432L24.253,5.446c-0.145-0.086-0.338-0.086-0.483,0 L8.082,14.499c-0.152,0.086-0.249,0.255-0.249,0.428v18.114c0,0.173,0.094,0.338,0.244,0.42l4.299,2.483 c2.334,1.167,3.76-0.208,3.76-1.591V16.476c0-0.255,0.2-0.452,0.456-0.452h1.988c0.248,0,0.452,0.196,0.452,0.452v17.886 c0,3.112-1.697,4.9-4.648,4.9c-0.908,0-1.623,0-3.619-0.982l-4.118-2.373C5.629,35.317,5,34.216,5,33.042V14.928 c0-1.179,0.629-2.279,1.646-2.861L22.36,3.002c0.994-0.562,2.314-0.562,3.301,0l15.694,9.069C42.367,12.656,43,13.753,43,14.932 v18.114c0,1.175-0.633,2.271-1.646,2.861L25.66,44.971c-0.503,0.291-1.073,0.44-1.654,0.44"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill={color}
|
||||||
|
d="M28.856,32.937c-6.868,0-8.308-3.153-8.308-5.797c0-0.251,0.203-0.452,0.455-0.452h2.028 c0.224,0,0.413,0.163,0.448,0.384c0.306,2.066,1.218,3.108,5.371,3.108c3.308,0,4.715-0.747,4.715-2.502 c0-1.01-0.401-1.76-5.54-2.263c-4.299-0.424-6.955-1.371-6.955-4.809c0-3.167,2.672-5.053,7.147-5.053 c5.026,0,7.517,1.745,7.831,5.493c0.012,0.13-0.035,0.255-0.122,0.35c-0.086,0.09-0.208,0.145-0.334,0.145h-2.039 c-0.212,0-0.397-0.149-0.44-0.354c-0.491-2.173-1.678-2.868-4.904-2.868c-3.611,0-4.031,1.257-4.031,2.2 c0,1.143,0.495,1.477,5.367,2.122c4.825,0.64,7.116,1.544,7.116,4.935c0,3.418-2.853,5.379-7.827,5.379"
|
||||||
|
/>{' '}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default NodeIcon;
|
25
resources/js/icons/php.tsx
Normal file
25
resources/js/icons/php.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { LucideProps } from 'lucide-react';
|
||||||
|
|
||||||
|
export const PHPIcon = React.forwardRef<SVGSVGElement, LucideProps>(
|
||||||
|
({ color = 'currentColor', size = 24, strokeWidth = 2, className, ...rest }, ref) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
ref={ref}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
version="1.1"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
fill={color}
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
className={className}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<path d="M170.322 349.808c-2.4-15.66-9-28.38-25.020-34.531-6.27-2.4-11.7-6.78-17.88-9.54-7.020-3.15-14.16-6.15-21.57-8.1-5.61-1.5-10.83 1.020-14.16 5.94-3.15 4.62-0.87 8.97 1.77 12.84 2.97 4.35 6.27 8.49 9.6 12.57 5.52 6.78 11.37 13.29 16.74 20.161 5.13 6.57 9.51 13.86 8.76 22.56-1.65 19.080-10.29 34.891-24.21 47.76-1.53 1.38-4.23 2.37-6.21 2.19-8.88-0.96-16.95-4.32-23.46-10.53-7.47-7.11-6.33-15.48 2.61-20.67 2.13-1.23 4.35-2.37 6.3-3.87 5.46-4.11 7.29-11.13 4.32-17.22-1.41-2.94-3-6.12-5.34-8.25-11.43-10.41-22.651-21.151-34.891-30.63-29.671-23.041-44.91-53.52-47.251-90.421-2.64-40.981 6.87-79.231 28.5-114.242 8.19-13.29 17.73-25.951 32.37-32.52 9.96-4.47 20.88-6.99 31.531-9.78 29.311-7.71 58.89-13.5 89.401-8.34 26.28 4.41 45.511 17.94 54.331 43.77 5.79 16.89 7.17 34.35 5.37 52.231-3.54 35.131-29.49 66.541-63.331 75.841-14.67 4.020-22.68 1.77-31.5-10.44-6.33-8.79-11.58-18.36-17.25-27.631-0.84-1.38-1.44-2.97-2.16-4.44-0.69-1.47-1.44-2.88-2.16-4.35 2.13 15.24 5.67 29.911 13.98 42.99 4.5 7.11 10.5 12.36 19.29 13.14 32.34 2.91 59.641-7.71 79.021-33.721 21.69-29.101 26.461-62.581 20.19-97.831-1.23-6.96-3.3-13.77-4.77-20.7-0.99-4.47 0.78-7.77 5.19-9.33 2.040-0.69 4.14-1.26 6.18-1.68 26.461-5.7 53.221-7.59 80.191-4.86 30.601 3.060 59.551 11.46 85.441 28.471 40.531 26.67 65.641 64.621 79.291 110.522 1.98 6.66 2.28 13.95 2.46 20.971 0.12 4.68-2.88 5.91-6.45 2.97-3.93-3.21-7.53-6.87-10.92-10.65-3.15-3.57-5.67-7.65-8.73-11.4-2.37-2.94-4.44-2.49-5.58 1.17-0.72 2.22-1.35 4.41-1.98 6.63-7.080 25.26-18.24 48.3-36.33 67.711-2.52 2.73-4.77 6.78-5.070 10.38-0.78 9.96-1.35 20.13-0.39 30.060 1.98 21.331 5.070 42.57 7.47 63.871 1.35 12.030-2.52 19.11-13.83 23.281-7.95 2.91-16.47 5.040-24.87 5.64-13.38 0.93-26.88 0.27-40.32 0.27-0.36-15 0.93-29.731-13.17-37.771 2.73-11.13 5.88-21.69 7.77-32.49 1.56-8.97 0.24-17.79-6.060-25.14-5.91-6.93-13.32-8.82-20.101-4.86-20.43 11.91-41.671 11.97-63.301 4.17-9.93-3.6-16.86-1.56-22.351 7.5-5.91 9.75-8.4 20.7-7.74 31.771 0.84 13.95 3.27 27.75 5.13 41.64 1.020 7.77 0.15 9.78-7.56 11.76-17.13 4.35-34.56 4.83-52.081 3.42-0.93-0.090-1.86-0.48-2.46-0.63-0.87-14.55 0.66-29.671-16.68-37.411 7.68-16.29 6.63-33.18 3.99-50.070l-0.060-0.15zM66.761 292.718c2.55-2.4 4.59-6.15 5.31-9.6 1.8-8.64-4.68-20.22-12.18-23.43-3.99-1.74-7.47-1.11-10.29 2.070-6.87 7.77-13.65 15.63-20.401 23.521-1.14 1.35-2.16 2.94-2.97 4.53-2.7 5.19-1.11 8.97 4.65 10.38 3.48 0.87 7.080 1.050 10.65 1.56 9.3-0.9 18.3-2.46 25.23-9v-0.030zM67.541 206.347c-0.030-6.18-5.19-11.34-11.28-11.37-6.27-0.030-11.67 5.58-11.46 11.76 0.27 6.21 5.43 11.19 11.61 11.070 6.24-0.090 11.22-5.19 11.16-11.43l-0.030-0.030z"></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default PHPIcon;
|
@ -24,6 +24,8 @@ import ServerHeader from '@/pages/servers/components/header';
|
|||||||
import Layout from '@/layouts/app/layout';
|
import Layout from '@/layouts/app/layout';
|
||||||
import { usePage, usePoll } from '@inertiajs/react';
|
import { usePage, usePoll } from '@inertiajs/react';
|
||||||
import { Site } from '@/types/site';
|
import { Site } from '@/types/site';
|
||||||
|
import PHPIcon from '@/icons/php';
|
||||||
|
import NodeIcon from '@/icons/node';
|
||||||
|
|
||||||
export default function ServerLayout({ children }: { children: ReactNode }) {
|
export default function ServerLayout({ children }: { children: ReactNode }) {
|
||||||
usePoll(7000);
|
usePoll(7000);
|
||||||
@ -92,6 +94,18 @@ export default function ServerLayout({ children }: { children: ReactNode }) {
|
|||||||
]
|
]
|
||||||
: [],
|
: [],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'PHP',
|
||||||
|
href: route('php', { server: page.props.server.id }),
|
||||||
|
icon: PHPIcon,
|
||||||
|
isDisabled: isMenuDisabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'NodeJS',
|
||||||
|
href: '#',
|
||||||
|
icon: NodeIcon,
|
||||||
|
isDisabled: isMenuDisabled,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Firewall',
|
title: 'Firewall',
|
||||||
href: route('firewall', { server: page.props.server.id }),
|
href: route('firewall', { server: page.props.server.id }),
|
||||||
|
81
resources/js/pages/php/components/columns.tsx
Normal file
81
resources/js/pages/php/components/columns.tsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
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 PHPIni from '@/pages/php/components/ini';
|
||||||
|
import Extensions from '@/pages/php/components/extensions';
|
||||||
|
import DefaultCli from '@/pages/php/components/default-cli';
|
||||||
|
export const columns: ColumnDef<Service>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: 'version',
|
||||||
|
header: 'Version',
|
||||||
|
enableColumnFilter: true,
|
||||||
|
enableSorting: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'created_at',
|
||||||
|
header: 'Installed at',
|
||||||
|
enableColumnFilter: true,
|
||||||
|
enableSorting: true,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return <DateTime date={row.original.created_at} />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'is_default',
|
||||||
|
header: 'Default cli',
|
||||||
|
enableColumnFilter: true,
|
||||||
|
enableSorting: true,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return <Badge variant={row.original.is_default ? 'default' : 'outline'}>{row.original.is_default ? 'Yes' : 'No'}</Badge>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'status',
|
||||||
|
header: 'Status',
|
||||||
|
enableColumnFilter: true,
|
||||||
|
enableSorting: true,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return <Badge variant={row.original.status_color}>{row.original.status}</Badge>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'actions',
|
||||||
|
enableColumnFilter: false,
|
||||||
|
enableSorting: false,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
<DropdownMenu modal={false}>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
|
<span className="sr-only">Open menu</span>
|
||||||
|
<MoreVerticalIcon />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<Extensions service={row.original} />
|
||||||
|
<PHPIni service={row.original} type="fpm" />
|
||||||
|
<PHPIni service={row.original} type="cli" />
|
||||||
|
<DefaultCli service={row.original} />
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<Action type="start" service={row.original} />
|
||||||
|
<Action type="stop" service={row.original} />
|
||||||
|
<Action type="restart" service={row.original} />
|
||||||
|
<Action type="enable" service={row.original} />
|
||||||
|
<Action type="disable" service={row.original} />
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<Uninstall service={row.original} />
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
64
resources/js/pages/php/components/default-cli.tsx
Normal file
64
resources/js/pages/php/components/default-cli.tsx
Normal file
@ -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('php.default-cli', { server: service.server_id, service: service.id }), {
|
||||||
|
onSuccess: () => {
|
||||||
|
setOpen(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<DropdownMenuItem disabled={service.is_default} onSelect={(e) => e.preventDefault()}>
|
||||||
|
Make default cli
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Make default cli</DialogTitle>
|
||||||
|
<DialogDescription className="sr-only">Make default cli</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-2 p-4">
|
||||||
|
<p>Are you sure you want to make PHP {form.data.version} the default cli?</p>
|
||||||
|
<InputError message={form.errors.version} />
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button variant="outline">Cancel</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<Button form="install-extension-form" disabled={form.processing} onClick={submit} className="ml-2">
|
||||||
|
{form.processing && <LoaderCircleIcon className="animate-spin" />}
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
87
resources/js/pages/php/components/extensions.tsx
Normal file
87
resources/js/pages/php/components/extensions.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { Service } from '@/types/service';
|
||||||
|
import React, { FormEvent, useState } from 'react';
|
||||||
|
import { useForm, usePage } from '@inertiajs/react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
||||||
|
import { Form, FormField, FormFields } from '@/components/ui/form';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { LoaderCircleIcon } from 'lucide-react';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { SharedData } from '@/types';
|
||||||
|
import InputError from '@/components/ui/input-error';
|
||||||
|
|
||||||
|
export default function Extensions({ service }: { service: Service }) {
|
||||||
|
const page = usePage<SharedData>();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const form = useForm<{
|
||||||
|
extension: string;
|
||||||
|
version: string;
|
||||||
|
}>({
|
||||||
|
extension: '',
|
||||||
|
version: service.version,
|
||||||
|
});
|
||||||
|
|
||||||
|
const submit = (e: FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
form.post(route('php.install-extension', { server: service.server_id, service: service.id }), {
|
||||||
|
onSuccess: () => {
|
||||||
|
setOpen(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>Extensions</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Install extension</DialogTitle>
|
||||||
|
<DialogDescription className="sr-only">Install php extension</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<Form id="install-extension-form" className="p-4" onSubmit={submit}>
|
||||||
|
<FormFields>
|
||||||
|
<FormField>
|
||||||
|
<Label htmlFor="extension">Extension</Label>
|
||||||
|
<Select value={form.data.extension} onValueChange={(value) => form.setData('extension', value)}>
|
||||||
|
<SelectTrigger id="extension">
|
||||||
|
<SelectValue placeholder="Select an extension" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
{page.props.configs.php_extensions.map((extension: string) => (
|
||||||
|
<SelectItem key={`extension-${extension}`} value={extension} disabled={service.type_data.extensions?.includes(extension)}>
|
||||||
|
{extension} {service.type_data.extensions?.includes(extension) && <span>(installed)</span>}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<InputError message={form.errors.extension} />
|
||||||
|
</FormField>
|
||||||
|
</FormFields>
|
||||||
|
</Form>
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button variant="outline">Cancel</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<Button form="install-extension-form" disabled={form.processing} onClick={submit} className="ml-2">
|
||||||
|
{form.processing && <LoaderCircleIcon className="animate-spin" />}
|
||||||
|
Install
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
107
resources/js/pages/php/components/ini.tsx
Normal file
107
resources/js/pages/php/components/ini.tsx
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { Service } from '@/types/service';
|
||||||
|
import React, { FormEvent, useState } from 'react';
|
||||||
|
import { useForm } from '@inertiajs/react';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { Editor, useMonaco } from '@monaco-editor/react';
|
||||||
|
import { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
|
||||||
|
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
||||||
|
import { Form } from '@/components/ui/form';
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { LoaderCircleIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
export default function PHPIni({ service, type }: { service: Service; type: 'fpm' | 'cli' }) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const form = useForm<{
|
||||||
|
ini: string;
|
||||||
|
type: 'fpm' | 'cli';
|
||||||
|
version: string;
|
||||||
|
}>({
|
||||||
|
ini: '',
|
||||||
|
type: type,
|
||||||
|
version: service.version,
|
||||||
|
});
|
||||||
|
|
||||||
|
const submit = (e: FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
form.patch(route('php.ini.update', { server: service.server_id, service: service.id }), {
|
||||||
|
onSuccess: () => {
|
||||||
|
setOpen(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: ['php.ini', service.server_id, service.id, type],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await axios.get(
|
||||||
|
route('php.ini', {
|
||||||
|
server: service.server_id,
|
||||||
|
service: service.id,
|
||||||
|
version: service.version,
|
||||||
|
type: type,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
if (response.data?.ini) {
|
||||||
|
form.setData('ini', response.data.ini);
|
||||||
|
}
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
retry: false,
|
||||||
|
enabled: open,
|
||||||
|
});
|
||||||
|
|
||||||
|
const monaco = useMonaco();
|
||||||
|
monaco?.languages.register({ id: 'ini' });
|
||||||
|
monaco?.languages.setMonarchTokensProvider('ini', {
|
||||||
|
tokenizer: {
|
||||||
|
root: [
|
||||||
|
[/^\[.*]$/, 'keyword'],
|
||||||
|
[/^[^=]+(?==)/, 'attribute.name'],
|
||||||
|
[/=.+$/, 'attribute.value'],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet open={open} onOpenChange={setOpen}>
|
||||||
|
<SheetTrigger asChild>
|
||||||
|
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>Edit {type} ini</DropdownMenuItem>
|
||||||
|
</SheetTrigger>
|
||||||
|
<SheetContent className="sm:max-w-5xl">
|
||||||
|
<SheetHeader>
|
||||||
|
<SheetTitle>Edit {type} ini</SheetTitle>
|
||||||
|
<SheetDescription className="sr-only">You can edit the {type} ini file for this service. Make sure to save your changes.</SheetDescription>
|
||||||
|
</SheetHeader>
|
||||||
|
<Form id="update-ini-form" className="h-full" onSubmit={submit}>
|
||||||
|
{query.isSuccess ? (
|
||||||
|
<Editor
|
||||||
|
defaultLanguage="ini"
|
||||||
|
defaultValue={query.data.ini}
|
||||||
|
theme="vs-dark"
|
||||||
|
className="h-full"
|
||||||
|
onChange={(value) => form.setData('ini', value ?? '')}
|
||||||
|
options={{
|
||||||
|
fontSize: 15,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Skeleton className="h-full w-full" />
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
<SheetFooter>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button form="update-ini-form" disabled={form.processing || query.isLoading} onClick={submit} className="ml-2">
|
||||||
|
{(form.processing || query.isLoading) && <LoaderCircleIcon className="animate-spin" />}
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<SheetClose asChild>
|
||||||
|
<Button variant="outline">Cancel</Button>
|
||||||
|
</SheetClose>
|
||||||
|
</div>
|
||||||
|
</SheetFooter>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
48
resources/js/pages/php/index.tsx
Normal file
48
resources/js/pages/php/index.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
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 { BookOpenIcon, 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/php/components/columns';
|
||||||
|
|
||||||
|
export default function PHP() {
|
||||||
|
const page = usePage<{
|
||||||
|
server: Server;
|
||||||
|
installedVersions: PaginatedData<Service>;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ServerLayout>
|
||||||
|
<Head title={`PHP - ${page.props.server.name}`} />
|
||||||
|
|
||||||
|
<Container className="max-w-5xl">
|
||||||
|
<HeaderContainer>
|
||||||
|
<Heading title="PHP" description="Here you can manage PHP" />
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<a href="https://vitodeploy.com/docs/servers/php" target="_blank">
|
||||||
|
<Button variant="outline">
|
||||||
|
<BookOpenIcon />
|
||||||
|
<span className="hidden lg:block">Docs</span>
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
<InstallService name="php">
|
||||||
|
<Button>
|
||||||
|
<PlusIcon />
|
||||||
|
<span className="hidden lg:block">Install</span>
|
||||||
|
</Button>
|
||||||
|
</InstallService>
|
||||||
|
</div>
|
||||||
|
</HeaderContainer>
|
||||||
|
|
||||||
|
<DataTable columns={columns} paginatedData={page.props.installedVersions} />
|
||||||
|
</Container>
|
||||||
|
</ServerLayout>
|
||||||
|
);
|
||||||
|
}
|
63
resources/js/pages/services/components/action.tsx
Normal file
63
resources/js/pages/services/components/action.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { Service } from '@/types/service';
|
||||||
|
import React, { 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 FormSuccessful from '@/components/form-successful';
|
||||||
|
|
||||||
|
export function Action({ type, service }: { type: 'start' | 'stop' | 'restart' | 'enable' | 'disable'; service: Service }) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const form = useForm();
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
form.post(route(`services.${type}`, { server: service.server_id, service: service }), {
|
||||||
|
onSuccess: () => {
|
||||||
|
setOpen(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<DropdownMenuItem onSelect={(e) => e.preventDefault()} className="capitalize">
|
||||||
|
{type}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
<span className="capitalize">{type}</span> service
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="sr-only">{type} service</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<p className="p-4">Are you sure you want to {type} the service?</p>
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button variant="outline">Cancel</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<Button
|
||||||
|
variant={['disable', 'stop'].includes(type) ? 'destructive' : 'default'}
|
||||||
|
disabled={form.processing}
|
||||||
|
onClick={submit}
|
||||||
|
className="capitalize"
|
||||||
|
>
|
||||||
|
{form.processing && <LoaderCircleIcon className="animate-spin" />}
|
||||||
|
<FormSuccessful successful={form.recentlySuccessful} />
|
||||||
|
{type}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
@ -1,119 +1,15 @@
|
|||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
import {
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from '@/components/ui/dialog';
|
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useForm } from '@inertiajs/react';
|
import { MoreVerticalIcon } from 'lucide-react';
|
||||||
import { LoaderCircleIcon, MoreVerticalIcon } from 'lucide-react';
|
import React from 'react';
|
||||||
import FormSuccessful from '@/components/form-successful';
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
import { Service } from '@/types/service';
|
import { Service } from '@/types/service';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import DateTime from '@/components/date-time';
|
import DateTime from '@/components/date-time';
|
||||||
|
import Uninstall from '@/pages/services/components/uninstall';
|
||||||
function Uninstall({ service }: { service: Service }) {
|
import { Action } from '@/pages/services/components/action';
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const form = useForm();
|
|
||||||
|
|
||||||
const submit = () => {
|
|
||||||
form.delete(route('services.destroy', { server: service.server_id, service: service }), {
|
|
||||||
onSuccess: () => {
|
|
||||||
setOpen(false);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<DropdownMenuItem variant="destructive" onSelect={(e) => e.preventDefault()}>
|
|
||||||
Uninstall
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Uninstall service</DialogTitle>
|
|
||||||
<DialogDescription className="sr-only">Uninstall service</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<p className="p-4">Are you sure you want to uninstall this service? This action cannot be undone.</p>
|
|
||||||
<DialogFooter>
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button variant="outline">Cancel</Button>
|
|
||||||
</DialogClose>
|
|
||||||
<Button variant="destructive" disabled={form.processing} onClick={submit}>
|
|
||||||
{form.processing && <LoaderCircleIcon className="animate-spin" />}
|
|
||||||
<FormSuccessful successful={form.recentlySuccessful} />
|
|
||||||
Uninstall
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Action({ type, service }: { type: 'start' | 'stop' | 'restart' | 'enable' | 'disable'; service: Service }) {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const form = useForm();
|
|
||||||
|
|
||||||
const submit = () => {
|
|
||||||
form.post(route(`services.${type}`, { server: service.server_id, service: service }), {
|
|
||||||
onSuccess: () => {
|
|
||||||
setOpen(false);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()} className="capitalize">
|
|
||||||
{type}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>
|
|
||||||
<span className="capitalize">{type}</span> service
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription className="sr-only">{type} service</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<p className="p-4">Are you sure you want to {type} the service?</p>
|
|
||||||
<DialogFooter>
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button variant="outline">Cancel</Button>
|
|
||||||
</DialogClose>
|
|
||||||
<Button
|
|
||||||
variant={['disable', 'stop'].includes(type) ? 'destructive' : 'default'}
|
|
||||||
disabled={form.processing}
|
|
||||||
onClick={submit}
|
|
||||||
className="capitalize"
|
|
||||||
>
|
|
||||||
{form.processing && <LoaderCircleIcon className="animate-spin" />}
|
|
||||||
<FormSuccessful successful={form.recentlySuccessful} />
|
|
||||||
{type}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const columns: ColumnDef<Service>[] = [
|
export const columns: ColumnDef<Service>[] = [
|
||||||
// {
|
|
||||||
// accessorKey: 'id',
|
|
||||||
// header: 'Service',
|
|
||||||
// enableColumnFilter: true,
|
|
||||||
// enableSorting: true,
|
|
||||||
// cell: ({ row }) => {
|
|
||||||
// return <img src={row.original.icon} className="size-7 rounded-sm" alt={`${row.original.name} icon`} />;
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
accessorKey: 'name',
|
accessorKey: 'name',
|
||||||
header: 'Name',
|
header: 'Name',
|
||||||
|
@ -19,7 +19,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { LoaderCircleIcon } from 'lucide-react';
|
import { LoaderCircleIcon } from 'lucide-react';
|
||||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
|
||||||
export default function InstallService({ children }: { children: ReactNode }) {
|
export default function InstallService({ name, children }: { name?: string; children: ReactNode }) {
|
||||||
const page = usePage<
|
const page = usePage<
|
||||||
{
|
{
|
||||||
server: Server;
|
server: Server;
|
||||||
@ -33,7 +33,7 @@ export default function InstallService({ children }: { children: ReactNode }) {
|
|||||||
version: string;
|
version: string;
|
||||||
}>({
|
}>({
|
||||||
type: '',
|
type: '',
|
||||||
name: '',
|
name: name ?? '',
|
||||||
version: '',
|
version: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -52,36 +52,38 @@ export default function InstallService({ children }: { children: ReactNode }) {
|
|||||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-lg">
|
<DialogContent className="sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Install service</DialogTitle>
|
<DialogTitle>Install {name ?? 'service'}</DialogTitle>
|
||||||
<DialogDescription className="sr-only">Install new service</DialogDescription>
|
<DialogDescription className="sr-only">Install new {name ?? 'service'}</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<Form id="install-service-form" onSubmit={submit} className="p-4">
|
<Form id="install-service-form" onSubmit={submit} className="p-4">
|
||||||
<FormFields>
|
<FormFields>
|
||||||
{/*service*/}
|
{/*service*/}
|
||||||
<FormField>
|
{!name && (
|
||||||
<Label htmlFor="name">Name</Label>
|
<FormField>
|
||||||
<Select
|
<Label htmlFor="name">Name</Label>
|
||||||
value={form.data.name}
|
<Select
|
||||||
onValueChange={(value) => {
|
value={form.data.name}
|
||||||
form.setData('name', value);
|
onValueChange={(value) => {
|
||||||
form.setData('version', '');
|
form.setData('name', value);
|
||||||
}}
|
form.setData('version', '');
|
||||||
>
|
}}
|
||||||
<SelectTrigger id="name">
|
>
|
||||||
<SelectValue placeholder="Select a service" />
|
<SelectTrigger id="name">
|
||||||
</SelectTrigger>
|
<SelectValue placeholder="Select a service" />
|
||||||
<SelectContent>
|
</SelectTrigger>
|
||||||
<SelectGroup>
|
<SelectContent>
|
||||||
{Object.entries(page.props.configs.service_types).map(([key]) => (
|
<SelectGroup>
|
||||||
<SelectItem key={`service-${key}`} value={key}>
|
{Object.entries(page.props.configs.service_types).map(([key]) => (
|
||||||
{key}
|
<SelectItem key={`service-${key}`} value={key}>
|
||||||
</SelectItem>
|
{key}
|
||||||
))}
|
</SelectItem>
|
||||||
</SelectGroup>
|
))}
|
||||||
</SelectContent>
|
</SelectGroup>
|
||||||
</Select>
|
</SelectContent>
|
||||||
<InputError message={form.errors.type || form.errors.name} />
|
</Select>
|
||||||
</FormField>
|
<InputError message={form.errors.type || form.errors.name} />
|
||||||
|
</FormField>
|
||||||
|
)}
|
||||||
|
|
||||||
{/*version*/}
|
{/*version*/}
|
||||||
<FormField>
|
<FormField>
|
||||||
|
56
resources/js/pages/services/components/uninstall.tsx
Normal file
56
resources/js/pages/services/components/uninstall.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { Service } from '@/types/service';
|
||||||
|
import React, { 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 FormSuccessful from '@/components/form-successful';
|
||||||
|
|
||||||
|
export default function Uninstall({ service }: { service: Service }) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const form = useForm();
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
form.delete(route('services.destroy', { server: service.server_id, service: service }), {
|
||||||
|
onSuccess: () => {
|
||||||
|
setOpen(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<DropdownMenuItem variant="destructive" onSelect={(e) => e.preventDefault()}>
|
||||||
|
Uninstall
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Uninstall service</DialogTitle>
|
||||||
|
<DialogDescription className="sr-only">Uninstall service</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<p className="p-4">Are you sure you want to uninstall this service? This action cannot be undone.</p>
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button variant="outline">Cancel</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<Button variant="destructive" disabled={form.processing} onClick={submit}>
|
||||||
|
{form.processing && <LoaderCircleIcon className="animate-spin" />}
|
||||||
|
<FormSuccessful successful={form.recentlySuccessful} />
|
||||||
|
Uninstall
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
3
resources/js/types/index.d.ts
vendored
3
resources/js/types/index.d.ts
vendored
@ -26,7 +26,7 @@ export interface NavItem {
|
|||||||
title: string;
|
title: string;
|
||||||
href: string;
|
href: string;
|
||||||
onlyActivePath?: string;
|
onlyActivePath?: string;
|
||||||
icon?: LucideIcon | null;
|
icon?: LucideIcon | string | null;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
children?: NavItem[];
|
children?: NavItem[];
|
||||||
@ -68,6 +68,7 @@ export interface Configs {
|
|||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
};
|
};
|
||||||
metrics_periods: string[];
|
metrics_periods: string[];
|
||||||
|
php_extensions: string[];
|
||||||
|
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
5
resources/js/types/service.d.ts
vendored
5
resources/js/types/service.d.ts
vendored
@ -2,7 +2,10 @@ export interface Service {
|
|||||||
id: number;
|
id: number;
|
||||||
server_id: number;
|
server_id: number;
|
||||||
type: string;
|
type: string;
|
||||||
type_data: unknown;
|
type_data: {
|
||||||
|
extensions?: string[];
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
name: string;
|
name: string;
|
||||||
version: string;
|
version: string;
|
||||||
unit: number;
|
unit: number;
|
||||||
|
@ -6,67 +6,13 @@
|
|||||||
use App\Enums\ServiceStatus;
|
use App\Enums\ServiceStatus;
|
||||||
use App\Facades\SSH;
|
use App\Facades\SSH;
|
||||||
use App\Models\Service;
|
use App\Models\Service;
|
||||||
use App\Web\Pages\Servers\PHP\Index;
|
|
||||||
use App\Web\Pages\Servers\PHP\Widgets\PHPList;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Livewire\Livewire;
|
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
class PHPTest extends TestCase
|
class PHPTest extends TestCase
|
||||||
{
|
{
|
||||||
use RefreshDatabase;
|
use RefreshDatabase;
|
||||||
|
|
||||||
public function test_install_new_php(): void
|
|
||||||
{
|
|
||||||
SSH::fake();
|
|
||||||
|
|
||||||
$this->actingAs($this->user);
|
|
||||||
|
|
||||||
Livewire::test(Index::class, ['server' => $this->server])
|
|
||||||
->callAction('install', [
|
|
||||||
'version' => '8.1',
|
|
||||||
])
|
|
||||||
->assertSuccessful();
|
|
||||||
|
|
||||||
$this->assertDatabaseHas('services', [
|
|
||||||
'server_id' => $this->server->id,
|
|
||||||
'type' => 'php',
|
|
||||||
'version' => '8.1',
|
|
||||||
'status' => ServiceStatus::READY,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_uninstall_php(): void
|
|
||||||
{
|
|
||||||
SSH::fake();
|
|
||||||
|
|
||||||
$this->actingAs($this->user);
|
|
||||||
|
|
||||||
$php = new Service([
|
|
||||||
'server_id' => $this->server->id,
|
|
||||||
'type' => 'php',
|
|
||||||
'type_data' => [
|
|
||||||
'extensions' => [],
|
|
||||||
'settings' => config('core.php_settings'),
|
|
||||||
],
|
|
||||||
'name' => 'php',
|
|
||||||
'version' => '8.1',
|
|
||||||
'status' => ServiceStatus::READY,
|
|
||||||
'is_default' => true,
|
|
||||||
]);
|
|
||||||
$php->save();
|
|
||||||
|
|
||||||
Livewire::test(PHPList::class, [
|
|
||||||
'server' => $this->server,
|
|
||||||
])
|
|
||||||
->callTableAction('uninstall', $php->id)
|
|
||||||
->assertSuccessful();
|
|
||||||
|
|
||||||
$this->assertDatabaseMissing('services', [
|
|
||||||
'id' => $php->id,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_change_default_php_cli(): void
|
public function test_change_default_php_cli(): void
|
||||||
{
|
{
|
||||||
SSH::fake();
|
SSH::fake();
|
||||||
@ -85,11 +31,13 @@ public function test_change_default_php_cli(): void
|
|||||||
'is_default' => false,
|
'is_default' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Livewire::test(PHPList::class, [
|
$this->post(route('php.default-cli', [
|
||||||
'server' => $this->server,
|
'server' => $this->server,
|
||||||
|
'service' => $php->id,
|
||||||
|
]), [
|
||||||
|
'version' => '8.1',
|
||||||
])
|
])
|
||||||
->callTableAction('default-php-cli', $php->id)
|
->assertSessionDoesntHaveErrors();
|
||||||
->assertSuccessful();
|
|
||||||
|
|
||||||
$php->refresh();
|
$php->refresh();
|
||||||
|
|
||||||
@ -102,17 +50,18 @@ public function test_install_extension(): void
|
|||||||
|
|
||||||
$this->actingAs($this->user);
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
Livewire::test(PHPList::class, [
|
|
||||||
'server' => $this->server,
|
|
||||||
])
|
|
||||||
->callTableAction('install-extension', $this->server->php()->id, [
|
|
||||||
'extension' => 'gmp',
|
|
||||||
])
|
|
||||||
->assertSuccessful();
|
|
||||||
|
|
||||||
$php = $this->server->php('8.2');
|
$php = $this->server->php('8.2');
|
||||||
|
|
||||||
$this->assertContains('gmp', $php->type_data['extensions']);
|
$this->post(route('php.install-extension', [
|
||||||
|
'server' => $this->server,
|
||||||
|
'service' => $php->id,
|
||||||
|
]), [
|
||||||
|
'version' => '8.2',
|
||||||
|
'extension' => 'gmp',
|
||||||
|
])
|
||||||
|
->assertSessionDoesntHaveErrors();
|
||||||
|
|
||||||
|
$this->assertContains('gmp', $php->refresh()->type_data['extensions']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -124,15 +73,20 @@ public function test_get_php_ini(string $version, string $type): void
|
|||||||
|
|
||||||
$this->actingAs($this->user);
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
Livewire::test(PHPList::class, [
|
$php = $this->server->php('8.2');
|
||||||
|
|
||||||
|
$this->get(route('php.ini', [
|
||||||
'server' => $this->server,
|
'server' => $this->server,
|
||||||
])
|
'service' => $php->id,
|
||||||
->callTableAction('php-ini-'.$type, $this->server->php()->id, [
|
'version' => '8.2',
|
||||||
'ini' => 'new-ini',
|
'type' => $type,
|
||||||
])
|
]))
|
||||||
->assertSuccessful();
|
->assertSessionDoesntHaveErrors();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<array<int, string>>
|
||||||
|
*/
|
||||||
public static function php_ini_data(): array
|
public static function php_ini_data(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
Reference in New Issue
Block a user