From f3f2e4074233e5409ac16e147e6097f6b0b28516 Mon Sep 17 00:00:00 2001 From: Eric Wang Date: Sun, 26 Feb 2023 23:04:32 +0000 Subject: [PATCH] add v3.7.0-beta --- CHANGELOG.md | 94 ++++--- .../Maintenance/PruneUsersCommand.php | 2 +- app/Data/Server/Proxmox/Config/DiskData.php | 26 +- app/Data/Server/Proxmox/Config/MediaData.php | 12 + .../Allocation/NoAvailableBusException.php | 13 - .../NoAvailableDiskInterfaceException.php | 13 + .../Client/Servers/SettingsController.php | 6 +- .../Requests/Admin/Nodes/StoreNodeRequest.php | 2 +- .../Admin/Nodes/UpdateNodeRequest.php | 1 - .../Servers/Settings/UpdateBuildRequest.php | 5 +- .../Settings/UpdateGeneralInfoRequest.php | 2 +- .../Admin/Servers/StoreServerRequest.php | 4 +- .../Settings/ReinstallServerRequest.php | 4 +- .../Servers/Settings/RenameServerRequest.php | 2 +- .../Servers/Settings/UpdateNetworkRequest.php | 4 +- .../Settings/UpdateSecurityRequest.php | 4 +- app/Jobs/Node/PruneUsersJob.php | 2 +- ...main.php => EnglishKeyboardCharacters.php} | 12 +- app/Rules/{Network => }/Fqdn.php | 2 +- app/Rules/{Network => }/Hostname.php | 2 +- app/Rules/Password.php | 30 +++ .../Nodes/{Access => }/UserPruneService.php | 2 +- app/Services/Servers/AllocationService.php | 24 +- .../Servers/BuildModificationService.php | 10 +- .../scripts/api/server/useServerDetails.ts | 28 +- .../dashboard/DashboardContainer.tsx | 14 +- .../scripts/components/elements/Drawer.tsx | 69 +++-- .../servers/settings/BootOrderContainer.tsx | 219 ---------------- .../servers/settings/HardwareContainer.tsx | 128 +-------- .../servers/settings/NetworkContainer.tsx | 123 +-------- .../servers/settings/SecurityContainer.tsx | 9 +- .../partials/hardware/BootOrderCard.tsx | 248 ++++++++++++++++++ .../partials/hardware/HardwareDetailsCard.tsx | 85 ++++++ .../hardware/MediaCard.tsx} | 12 +- .../{ => partials/hardware}/MediaRow.tsx | 0 .../partials/network/NameserversCard.tsx | 119 +++++++++ resources/scripts/main.tsx | 1 + .../util/registerCustomYupValidationRules.ts | 28 ++ resources/views/app.blade.php | 77 +++--- 39 files changed, 780 insertions(+), 658 deletions(-) create mode 100644 app/Data/Server/Proxmox/Config/MediaData.php delete mode 100644 app/Exceptions/Service/Server/Allocation/NoAvailableBusException.php create mode 100644 app/Exceptions/Service/Server/Allocation/NoAvailableDiskInterfaceException.php rename app/Rules/{Network/Domain.php => EnglishKeyboardCharacters.php} (54%) rename app/Rules/{Network => }/Fqdn.php (99%) rename app/Rules/{Network => }/Hostname.php (94%) create mode 100644 app/Rules/Password.php rename app/Services/Nodes/{Access => }/UserPruneService.php (94%) delete mode 100644 resources/scripts/components/servers/settings/BootOrderContainer.tsx create mode 100644 resources/scripts/components/servers/settings/partials/hardware/BootOrderCard.tsx create mode 100644 resources/scripts/components/servers/settings/partials/hardware/HardwareDetailsCard.tsx rename resources/scripts/components/servers/settings/{MediaContainer.tsx => partials/hardware/MediaCard.tsx} (73%) rename resources/scripts/components/servers/settings/{ => partials/hardware}/MediaRow.tsx (100%) create mode 100644 resources/scripts/components/servers/settings/partials/network/NameserversCard.tsx create mode 100644 resources/scripts/util/registerCustomYupValidationRules.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index b0bc59ed2f5..995a6207145 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. ## v3.7.0-beta +### Fixed + +- Overallocation check logic whenever an administrator tries to update a server's build #16 +- Issue where you can't use IPv6 for nameservers + ### Changed - Made account_password required by default for creating new servers and server installations @@ -13,6 +18,12 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. - Refactored menu component to reduce bundle size - Refactored Server Usages and Rate Limit sync for better scaling - Minor frontend styling +- Refactored disk data transfer object +- All password inputs (except for Convoy user account password) has these two validation rules: + 1. `/^[A-Za-z0-9!@#$%^&*()_+\-=[\]{}|;\':",.\/<>?\\ ]*$/` for checking if the password contains only characters on + the U.S. English keyboard. + 2. `/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])(?=.{8,})/` for checking if the password contains 8 + characters, 1 uppercase, 1 lowercase, 1 number and 1 special character. ### Added @@ -31,7 +42,9 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. ### Note -If you are developing automation software for Convoy, please implement these regular expressions in your code. Otherwise, your code will error when you send invalid requests. +If you are developing automation software for Convoy, please implement these regular expressions in your code. +Otherwise, your code will error when you send invalid requests. + - server `account_password` validation - `/^[A-z0-9!@£$%^&*()\'~*_+\-]+$/` to detect special characters from other language - `/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])(?=.{8,})/u` minimum password requirements @@ -70,88 +83,89 @@ If you are developing automation software for Convoy, please implement these reg ### Changed -- Settings layout for client side server settings +- Settings layout for client side server settings ### Fixed -- possibility that `address_ids` will cause an exception when it's null when creating a new server +- possibility that `address_ids` will cause an exception when it's null when creating a new server ## v3.4.0-beta ### Changed -- Refactored routes +- Refactored routes ### Added -- Navigation Bar Context. Now switching pages are even more seamless +- Navigation Bar Context. Now switching pages are even more seamless ## v3.3.0-beta ### Fixed -- Lack of cancel button when deleting an API key +- Lack of cancel button when deleting an API key ### Added -- Ability for administrators to impersonate the client view for a server and also visit the server's configuration in the admin area. -- Warnings when creating a new node to disable privilege separation and grant root permissions. +- Ability for administrators to impersonate the client view for a server and also visit the server's configuration in + the admin area. +- Warnings when creating a new node to disable privilege separation and grant root permissions. ## v3.2.0-beta ### Fixed -- Broken network syncing when updating an address's assigned server -- Text alignment for server counter on the users table in the admin area +- Broken network syncing when updating an address's assigned server +- Text alignment for server counter on the users table in the admin area ### Added -- Hyperlinks to the owner on the servers table +- Hyperlinks to the owner on the servers table ## v3.1.2-beta ### Fixed -- Scoped routing bindings in RouteServiceProvider that were breaking some routes +- Scoped routing bindings in RouteServiceProvider that were breaking some routes ## v3.1.1-beta ### Fixed -- Conflicting named routes that would break route optimization/caching +- Conflicting named routes that would break route optimization/caching ## v3.1.0-beta ### Fixed -- IP Address updating -- Server build updating +- IP Address updating +- Server build updating ### Removed -- Option to sync or not sync network settings when deleting an IP address - - The default behavior is always to sync +- Option to sync or not sync network settings when deleting an IP address + - The default behavior is always to sync ## v3.0.0-beta (Tuxedo) ### Fixed -- Server installs -- a bunch of other stuff +- Server installs +- a bunch of other stuff ### Changed -- from proprietary hard-coded api tokens to Bearer tokens for the application/external api -- the whole entire frontend +- from proprietary hard-coded api tokens to Bearer tokens for the application/external api +- the whole entire frontend ### Added -- Server hostnames -- Node location grouping +- Server hostnames +- Node location grouping ### Notes -- This release is so big that I can't really summarize everything +- This release is so big that I can't really summarize everything ![The maxwell cat meme is the mascot for v3](https://imgur.com/mowvogE.png) @@ -159,43 +173,47 @@ If you are developing automation software for Convoy, please implement these reg ### Fixed -- Inability to delete IP address from the admin user interface +- Inability to delete IP address from the admin user interface ## v2.0.2-beta ### Fixed -- FQDN validator for the hostname field when adding a new node +- FQDN validator for the hostname field when adding a new node ## v2.0.1-beta ### Fixed -- Problem where validation errors for the SSH key wouldn't show up -- Bug where user couldn't unset a SSH key after saving one +- Problem where validation errors for the SSH key wouldn't show up +- Bug where user couldn't unset a SSH key after saving one ## v2.0.0-beta (Bombay) ### Added -- Storing of CPU, memory, disk, snapshots, backups, and bandwidth limits -- Added server suspensions -- Added real-time status updates of server installs (though it will be deprecated in v3.x.x) -- Automatic bandwidth throttler when a user exceeds the bandwidth limit +- Storing of CPU, memory, disk, snapshots, backups, and bandwidth limits +- Added server suspensions +- Added real-time status updates of server installs (though it will be deprecated in v3.x.x) +- Automatic bandwidth throttler when a user exceeds the bandwidth limit ### Changed -- Internally, server details are now passed around the application using Laravel Data by Spatie. Though in v3.x.x, we are planning on switching to Data Transfer Objects by Spatie. We pulled the wrong package and didn't realize until one month in using the package LOL. -- Virtual machines are now limited to one disk. Multiple disks may be supported when a daemon is available in the future. -- The built-in web server is now Caddy instead of Nginx. This provides auto SSL out of the box. +- Internally, server details are now passed around the application using Laravel Data by Spatie. Though in v3.x.x, we + are planning on switching to Data Transfer Objects by Spatie. We pulled the wrong package and didn't realize until one + month in using the package LOL. +- Virtual machines are now limited to one disk. Multiple disks may be supported when a daemon is available in the + future. +- The built-in web server is now Caddy instead of Nginx. This provides auto SSL out of the box. ### Fixed -- The commands in the node viewing page for installing the VNC Broker and templates. +- The commands in the node viewing page for installing the VNC Broker and templates. ### Known Bugs -- The real-time server installation communication is known to be buggy and will be resolved in v3.x.x -- Editing the server field for IP Addresses will sometime result in the first server of the node to be used. This will be resolved in v3.x.x +- The real-time server installation communication is known to be buggy and will be resolved in v3.x.x +- Editing the server field for IP Addresses will sometime result in the first server of the node to be used. This will + be resolved in v3.x.x ![The Bombay cat breed is the mascot for v2](https://imgur.com/fP6oxn9.png) diff --git a/app/Console/Commands/Maintenance/PruneUsersCommand.php b/app/Console/Commands/Maintenance/PruneUsersCommand.php index 409a371d9dc..07221584759 100644 --- a/app/Console/Commands/Maintenance/PruneUsersCommand.php +++ b/app/Console/Commands/Maintenance/PruneUsersCommand.php @@ -4,7 +4,7 @@ use Convoy\Jobs\Node\PruneUsersJob; use Convoy\Models\Node; -use Convoy\Services\Nodes\Access\UserPruneService; +use Convoy\Services\Nodes\UserPruneService; use Illuminate\Console\Command; use Illuminate\Console\View\Components\Task; diff --git a/app/Data/Server/Proxmox/Config/DiskData.php b/app/Data/Server/Proxmox/Config/DiskData.php index 8c2aa738b40..6eb7b5abc15 100644 --- a/app/Data/Server/Proxmox/Config/DiskData.php +++ b/app/Data/Server/Proxmox/Config/DiskData.php @@ -7,13 +7,23 @@ class DiskData extends Data { +// public function __construct( +// #[In(['disk', 'media'])] +// public string $type, +// public string $name, +// public int $size, +// public ?string $display_name, +// ) +// { +// } + public function __construct( - #[In(['disk', 'media'])] - public string $type, - public string $name, - public int $size, - public ?string $display_name, - ) - { - } + public string $interface, + public bool $is_primary_disk, + + public bool $is_media, + public ?string $media_name, + + public int $size, + ){} } diff --git a/app/Data/Server/Proxmox/Config/MediaData.php b/app/Data/Server/Proxmox/Config/MediaData.php new file mode 100644 index 00000000000..90ab6c13034 --- /dev/null +++ b/app/Data/Server/Proxmox/Config/MediaData.php @@ -0,0 +1,12 @@ +where('name', '=', $device->name)->first() === null) { - array_push($unconfiguredDevices, $device->toArray()); + if ($configuredDevices->where('interface', '=', $device->interface)->first() === null) { + array_push($unconfiguredDevices, $device); } } @@ -124,7 +124,7 @@ public function getMedia(Request $request, Server $server) } $media = array_map(function ($iso) use ($disks) { - if ($disks->where('display_name', '=', $iso['name'])->first()) { + if ($disks->where('media_name', '=', $iso['name'])->first()) { return [ 'mounted' => true, ...$iso, diff --git a/app/Http/Requests/Admin/Nodes/StoreNodeRequest.php b/app/Http/Requests/Admin/Nodes/StoreNodeRequest.php index 60de30841ee..1e056842471 100644 --- a/app/Http/Requests/Admin/Nodes/StoreNodeRequest.php +++ b/app/Http/Requests/Admin/Nodes/StoreNodeRequest.php @@ -3,7 +3,7 @@ namespace Convoy\Http\Requests\Admin\Nodes; use Convoy\Models\Node; -use Convoy\Rules\Network\Fqdn; +use Convoy\Rules\Fqdn; use Illuminate\Foundation\Http\FormRequest; class StoreNodeRequest extends FormRequest diff --git a/app/Http/Requests/Admin/Nodes/UpdateNodeRequest.php b/app/Http/Requests/Admin/Nodes/UpdateNodeRequest.php index 0ab8a385bcc..ff6333cd744 100644 --- a/app/Http/Requests/Admin/Nodes/UpdateNodeRequest.php +++ b/app/Http/Requests/Admin/Nodes/UpdateNodeRequest.php @@ -37,7 +37,6 @@ public function rules() public function withValidator(Validator $validator) { - $validator->after(function (Validator $validator) { $node = $this->parameter('node', Node::class); // multiply memory by memory_overallocate (which indicates how much you can go over) percentage diff --git a/app/Http/Requests/Admin/Servers/Settings/UpdateBuildRequest.php b/app/Http/Requests/Admin/Servers/Settings/UpdateBuildRequest.php index cc32a5a388f..04ccf02a623 100644 --- a/app/Http/Requests/Admin/Servers/Settings/UpdateBuildRequest.php +++ b/app/Http/Requests/Admin/Servers/Settings/UpdateBuildRequest.php @@ -47,12 +47,11 @@ public function withValidator(Validator $validator) // check if the memory and disk isn't exceeding the node limits $node = Node::findOrFail($server->node_id)->load('servers'); - $nodeMemoryLimit = ($node->memory * (($node->memory_overallocate / 100) + 1)) - $node->memory_allocated; - $nodeDiskLimit = ($node->disk * (($node->disk_overallocate / 100) + 1)) - $node->disk_allocated; + $nodeMemoryLimit = ($node->memory * (($node->memory_overallocate / 100) + 1)) - ($node->memory_allocated - $server->memory); + $nodeDiskLimit = ($node->disk * (($node->disk_overallocate / 100) + 1)) - ($node->disk_allocated - $server->disk); $memory = intval($this->input('memory')); $disk = intval($this->input('disk')); - if ($memory > $nodeMemoryLimit || $memory < 0) { $validator->errors()->add('memory', 'The memory value exceeds the node\'s limit.'); } diff --git a/app/Http/Requests/Admin/Servers/Settings/UpdateGeneralInfoRequest.php b/app/Http/Requests/Admin/Servers/Settings/UpdateGeneralInfoRequest.php index b7c145df024..1a273b9f10b 100644 --- a/app/Http/Requests/Admin/Servers/Settings/UpdateGeneralInfoRequest.php +++ b/app/Http/Requests/Admin/Servers/Settings/UpdateGeneralInfoRequest.php @@ -4,7 +4,7 @@ use Convoy\Http\Requests\FormRequest; use Convoy\Models\Server; -use Convoy\Rules\Network\Hostname; +use Convoy\Rules\Hostname; class UpdateGeneralInfoRequest extends FormRequest { diff --git a/app/Http/Requests/Admin/Servers/StoreServerRequest.php b/app/Http/Requests/Admin/Servers/StoreServerRequest.php index d63c8996a70..8970294bde9 100644 --- a/app/Http/Requests/Admin/Servers/StoreServerRequest.php +++ b/app/Http/Requests/Admin/Servers/StoreServerRequest.php @@ -6,6 +6,8 @@ use Convoy\Models\IPAddress; use Convoy\Models\Node; use Convoy\Models\Server; +use Convoy\Rules\EnglishKeyboardCharacters; +use Convoy\Rules\Password; use Illuminate\Validation\Validator; /** @@ -37,7 +39,7 @@ public function rules() 'limits.bandwidth' => $rules['bandwidth_limit'], 'limits.address_ids' => 'sometimes|nullable|array', 'limits.address_ids.*' => 'integer|exists:ip_addresses,id', - 'account_password' => ['required', 'string', 'min:8', 'max:191', 'regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])(?=.{8,})/u', 'regex:/^[A-z0-9!@£$%^&*()\'~*_+\-]+$/'], + 'account_password' => ['required', 'string', 'min:8', 'max:191', new Password(), new EnglishKeyboardCharacters(),], 'should_create_server' => 'present|boolean', 'template_uuid' => 'required_if:create_server,1|string|exists:templates,uuid', 'start_on_completion' => 'present|boolean', diff --git a/app/Http/Requests/Client/Servers/Settings/ReinstallServerRequest.php b/app/Http/Requests/Client/Servers/Settings/ReinstallServerRequest.php index f4b35ae9c10..9be380813a4 100644 --- a/app/Http/Requests/Client/Servers/Settings/ReinstallServerRequest.php +++ b/app/Http/Requests/Client/Servers/Settings/ReinstallServerRequest.php @@ -5,6 +5,8 @@ use Convoy\Http\Requests\FormRequest; use Convoy\Models\Server; use Convoy\Models\Template; +use Convoy\Rules\EnglishKeyboardCharacters; +use Convoy\Rules\Password; class ReinstallServerRequest extends FormRequest { @@ -27,7 +29,7 @@ public function rules() { return [ 'template_uuid' => 'required|string|exists:templates,uuid', - 'account_password' => ['required', 'string', 'min:8', 'max:191', 'regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])(?=.{8,})/u'], + 'account_password' => ['required', 'string', 'min:8', 'max:191', new Password(), new EnglishKeyboardCharacters()], 'start_on_completion' => 'present|boolean', ]; } diff --git a/app/Http/Requests/Client/Servers/Settings/RenameServerRequest.php b/app/Http/Requests/Client/Servers/Settings/RenameServerRequest.php index b78e37a9a13..2d1cb0f8b73 100644 --- a/app/Http/Requests/Client/Servers/Settings/RenameServerRequest.php +++ b/app/Http/Requests/Client/Servers/Settings/RenameServerRequest.php @@ -3,7 +3,7 @@ namespace Convoy\Http\Requests\Client\Servers\Settings; use Convoy\Models\Server; -use Convoy\Rules\Network\Hostname; +use Convoy\Rules\Hostname; use Illuminate\Foundation\Http\FormRequest; class RenameServerRequest extends FormRequest diff --git a/app/Http/Requests/Client/Servers/Settings/UpdateNetworkRequest.php b/app/Http/Requests/Client/Servers/Settings/UpdateNetworkRequest.php index 947acccaa99..d9f0b87d6ed 100644 --- a/app/Http/Requests/Client/Servers/Settings/UpdateNetworkRequest.php +++ b/app/Http/Requests/Client/Servers/Settings/UpdateNetworkRequest.php @@ -2,7 +2,7 @@ namespace Convoy\Http\Requests\Client\Servers\Settings; -use Convoy\Rules\Network\Domain; +use Convoy\Rules\Domain; use Illuminate\Foundation\Http\FormRequest; class UpdateNetworkRequest extends FormRequest @@ -26,7 +26,7 @@ public function rules() { return [ 'nameservers' => ['array', 'present'], - 'nameservers.*' => ['string', new Domain], + 'nameservers.*' => ['string', 'ip'], ]; } } diff --git a/app/Http/Requests/Client/Servers/Settings/UpdateSecurityRequest.php b/app/Http/Requests/Client/Servers/Settings/UpdateSecurityRequest.php index c33f35f527e..06fec2af5e3 100644 --- a/app/Http/Requests/Client/Servers/Settings/UpdateSecurityRequest.php +++ b/app/Http/Requests/Client/Servers/Settings/UpdateSecurityRequest.php @@ -3,6 +3,8 @@ namespace Convoy\Http\Requests\Client\Servers\Settings; use Convoy\Enums\Server\Cloudinit\AuthenticationType; +use Convoy\Rules\EnglishKeyboardCharacters; +use Convoy\Rules\Password; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rules\Enum; use Illuminate\Validation\Validator; @@ -30,7 +32,7 @@ public function rules() return [ 'type' => [new Enum(AuthenticationType::class), 'required'], 'ssh_keys' => ['nullable', 'string', 'exclude_unless:type,sshkeys'], - 'password' => ['string', 'min:8', 'max:191', 'regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])(?=.{8,})/u', 'regex:/^[A-z0-9!@£$%^&*()\'~*_+\-]+$/', 'exclude_unless:type,cipassword'], + 'password' => ['string', 'min:8', 'max:191', new Password(), new EnglishKeyboardCharacters(), 'exclude_unless:type,cipassword'], ]; } diff --git a/app/Jobs/Node/PruneUsersJob.php b/app/Jobs/Node/PruneUsersJob.php index ae43a275cff..61c0dabacb7 100644 --- a/app/Jobs/Node/PruneUsersJob.php +++ b/app/Jobs/Node/PruneUsersJob.php @@ -3,7 +3,7 @@ namespace Convoy\Jobs\Node; use Convoy\Models\Node; -use Convoy\Services\Nodes\Access\UserPruneService; +use Convoy\Services\Nodes\UserPruneService; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Contracts\Queue\ShouldQueue; diff --git a/app/Rules/Network/Domain.php b/app/Rules/EnglishKeyboardCharacters.php similarity index 54% rename from app/Rules/Network/Domain.php rename to app/Rules/EnglishKeyboardCharacters.php index 116c87b22ef..a4a65115413 100644 --- a/app/Rules/Network/Domain.php +++ b/app/Rules/EnglishKeyboardCharacters.php @@ -1,10 +1,10 @@ ?\\ ]*$/', $value); } /** @@ -39,6 +35,6 @@ public function passes($attribute, $value) */ public function message() { - return ':attribute can only contain letters, numbers, and periods.'; + return 'The :attribute must contain characters from the English keyboard.'; } } diff --git a/app/Rules/Network/Fqdn.php b/app/Rules/Fqdn.php similarity index 99% rename from app/Rules/Network/Fqdn.php rename to app/Rules/Fqdn.php index e6043d455ee..60cde95ec79 100644 --- a/app/Rules/Network/Fqdn.php +++ b/app/Rules/Fqdn.php @@ -23,7 +23,7 @@ SOFTWARE. */ -namespace Convoy\Rules\Network; +namespace Convoy\Rules; use Illuminate\Support\Arr; use Illuminate\Contracts\Validation\Rule; diff --git a/app/Rules/Network/Hostname.php b/app/Rules/Hostname.php similarity index 94% rename from app/Rules/Network/Hostname.php rename to app/Rules/Hostname.php index c6a1a26dcaa..d63e7b70fbf 100644 --- a/app/Rules/Network/Hostname.php +++ b/app/Rules/Hostname.php @@ -1,6 +1,6 @@ 'disk', - 'name' => Arr::get($rawDisk, 'key'), + 'interface' => Arr::get($rawDisk, 'key'), + 'is_primary_disk' => false, + 'is_media' => false, + 'media_name' => null, 'size' => 0, ]; @@ -46,12 +48,14 @@ public function getDisks(Server $server) $disk['size'] = $this->convertToBytes($sizeMatches[1]); if (str_contains($value, 'media')) { - $disk['type'] = 'media'; + $disk['is_media'] = true; // this piece of code adds the name of the mounted ISO if (preg_match("/\/(.*\.iso)/s", $value, $fileNameMatches)) { if ($iso = $isos->where('file_name', $fileNameMatches[1])->first()) { - $disk['display_name'] = $iso->name; + $disk['media_name'] = $iso->name; } + } elseif (str_contains($value, 'cloudinit')) { + $disk['media_name'] = 'Cloudinit'; } } else { // if its not the ISO, we'll check if its the boot disk by comparing the size to the disk size on the eloquent record of the server @@ -59,7 +63,7 @@ public function getDisks(Server $server) $lowerBound = $server->disk - 1024; if ($disk['size'] < $upperBound && $disk['size'] > $lowerBound) { - $disk['display_name'] = 'Primary Disk'; + $disk['is_primary_disk'] = true; } } @@ -80,7 +84,7 @@ public function getBootOrder(Server $server) $taggedDisks = []; foreach ($untaggedDisks as $untaggedDisk) { - if ($disk = $disks->where('name', '=', $untaggedDisk)->first()) { + if ($disk = $disks->where('interface', '=', $untaggedDisk)->first()) { array_push($taggedDisks, $disk); } } @@ -110,14 +114,14 @@ public function mountIso(Server $server, ISO $iso) // we'll be using IDE by default for now $ideIndex = 0; // max IDE index is '3' $disks = $this->getDisks($server); - if ($disks->where('display_name', '=', $iso->name)->first()) { + if ($disks->where('media_name', '=', $iso->name)->first()) { throw new IsoAlreadyMountedException(); } $arrayToCheckForAvailableIdeIndex = Arr::pluck($this->repository->setServer($server)->getAllocations(), 'key'); for ($i = 0; $i <= 4; $i++) { if ($i === 4) { - throw new NoAvailableBusException(); + throw new NoAvailableDiskInterfaceException(); } if (!in_array("ide$i", $arrayToCheckForAvailableIdeIndex)) { @@ -134,7 +138,7 @@ public function mountIso(Server $server, ISO $iso) public function unmountIso(Server $server, ISO $iso) { $disks = $this->getDisks($server); - if ($disk = $disks->where('display_name', '=', $iso->name)->first()) { + if ($disk = $disks->where('media_name', '=', $iso->name)->first()) { $this->repository->update(['delete' => $disk->name]); } else { throw new IsoAlreadyUnmountedException(); diff --git a/app/Services/Servers/BuildModificationService.php b/app/Services/Servers/BuildModificationService.php index 3fa9fddd3ca..7221862f363 100644 --- a/app/Services/Servers/BuildModificationService.php +++ b/app/Services/Servers/BuildModificationService.php @@ -2,6 +2,7 @@ namespace Convoy\Services\Servers; +use Convoy\Data\Server\Proxmox\Config\DiskData; use Convoy\Enums\Server\Power; use Convoy\Models\Server; use Convoy\Repositories\Proxmox\Server\ProxmoxAllocationRepository; @@ -38,16 +39,17 @@ public function handle(Server $server, ?bool $shouldUpdateState = true) $this->networkService->syncSettings($server); // find a disk that has a corresponding disk in the deployment - $disks = collect($proxmoxDetails->config->disks->toArray())->pluck('name')->all(); - $bootOrder = array_filter(collect($proxmoxDetails->config->boot_order->filter(fn ($disk) => $disk->type !== 'media')->toArray())->pluck('name')->toArray(), fn ($disk) => in_array($disk, $disks)); + $disks = collect($proxmoxDetails->config->disks->toArray())->pluck('interface')->all(); + $bootOrder = array_filter(collect($proxmoxDetails->config->boot_order->filter(fn (DiskData $disk) => !$disk->is_media)->toArray())->pluck('interface')->toArray(), fn ($disk) => in_array($disk, $disks)); if (count($bootOrder) > 0) { - $disk = $proxmoxDetails->config->disks->where('name', '=', Arr::first($bootOrder))->first(); + /** @var DiskData */ + $disk = $proxmoxDetails->config->disks->where('interface', '=', Arr::first($bootOrder))->first(); $diff = $eloquentDetails->limits->disk - $disk->size; if ($diff > 0) { - $this->allocationRepository->resizeDisk($diff, $disk->name); + $this->allocationRepository->resizeDisk($diff, $disk->interface); } } diff --git a/resources/scripts/api/server/useServerDetails.ts b/resources/scripts/api/server/useServerDetails.ts index 49da885b1fc..37724f5e743 100644 --- a/resources/scripts/api/server/useServerDetails.ts +++ b/resources/scripts/api/server/useServerDetails.ts @@ -3,15 +3,24 @@ import { ServerState } from '@/api/server/getStatus' import useSWR from 'swr' import http from '@/api/http' -export type DiskType = 'disk' | 'media' - -export interface Disk { - type: DiskType - name: string +export interface BaseDisk { + interface: string + isPrimaryDisk: boolean size: number - displayName: string | null } +export interface DiskWithMedia extends BaseDisk { + isMedia: true + mediaName: string +} + +export interface DiskWithoutMedia extends BaseDisk { + isMedia: false + mediaName?: null +} + +export type Disk = DiskWithMedia | DiskWithoutMedia + export interface Address { address: string cidr: number @@ -37,10 +46,11 @@ export interface ServerDetails { } export const rawDataToDisk = (data: any): Disk => ({ - type: data.type, - name: data.name, + interface: data.interface, + isPrimaryDisk: data.is_primary_disk, + isMedia: data.is_media, + mediaName: data?.media_name, size: data.size, - displayName: data.display_name, }) export const rawDataToServerDetails = (data: any): ServerDetails => ({ diff --git a/resources/scripts/components/dashboard/DashboardContainer.tsx b/resources/scripts/components/dashboard/DashboardContainer.tsx index be346d497ed..2afb48ddb70 100644 --- a/resources/scripts/components/dashboard/DashboardContainer.tsx +++ b/resources/scripts/components/dashboard/DashboardContainer.tsx @@ -8,13 +8,13 @@ import PageContentBlock from '@/components/elements/PageContentBlock' const DashboardContainer = () => { return ( - - - - - - - + {/**/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/**/} ) diff --git a/resources/scripts/components/elements/Drawer.tsx b/resources/scripts/components/elements/Drawer.tsx index 3dd1d764cc5..26a40fbf805 100644 --- a/resources/scripts/components/elements/Drawer.tsx +++ b/resources/scripts/components/elements/Drawer.tsx @@ -12,45 +12,42 @@ const Drawer = forwardRef(({ open, onClose, children }: P return ( - - -
- - -
-
+ - - - {children} - +
-
-
-
+
+
+ + + + {children} + + +
+
+
- ) + ) }) -export default Drawer \ No newline at end of file +export default Drawer diff --git a/resources/scripts/components/servers/settings/BootOrderContainer.tsx b/resources/scripts/components/servers/settings/BootOrderContainer.tsx deleted file mode 100644 index f44df62e432..00000000000 --- a/resources/scripts/components/servers/settings/BootOrderContainer.tsx +++ /dev/null @@ -1,219 +0,0 @@ -import { ServerContext } from '@/state/server' -import useSWR from 'swr' -import getBootOrder from '@/api/server/settings/getBootOrder' -import { useEffect, useMemo, useState } from 'react' -import { Disk } from '@/api/server/useServerDetails' -import useNotify from '@/util/useNotify' -import useFlash from '@/util/useFlash' -import { DndContext, DragEndEvent } from '@dnd-kit/core' -import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable' -import updateBootOrder from '@/api/server/settings/updateBootOrder' -import FormCard from '@/components/elements/FormCard' -import FlashMessageRender from '@/components/elements/FlashMessageRenderer' -import MessageBox from '@/components/elements/MessageBox' -import { restrictToVerticalAxis, restrictToWindowEdges } from '@dnd-kit/modifiers' -import SortableItem, { ChildrenPropsWithHandle } from '@/components/elements/dnd/SortableItem' -// @ts-expect-error -import DragVerticalIcon from '@/assets/images/icons/drag-vertical.svg' -import { Badge } from '@mantine/core' -import { bytesToString } from '@/util/helpers' -import { PlusIcon, XMarkIcon } from '@heroicons/react/20/solid' -import Button from '@/components/elements/Button' -import useBootOrderSWR from '@/api/server/settings/useBootOrderSWR' - -const BootOrderContainer = () => { - const uuid = ServerContext.useStoreState(state => state.server.data!.uuid) - const { data } = useBootOrderSWR(uuid, { - revalidateOnFocus: false, - revalidateOnMount: true, - revalidateOnReconnect: false, - refreshWhenOffline: false, - refreshWhenHidden: false, - refreshInterval: 0, - }) - - const [bootOrder, setBootOrder] = useState([]) - const bootOrderIds = useMemo(() => bootOrder.map(disk => disk.name), [bootOrder]) - const [unusedDevices, setUnusedDevices] = useState([]) - const notify = useNotify() - const { clearFlashes, clearAndAddHttpError } = useFlash() - - useEffect(() => { - setBootOrder(data?.bootOrder ?? []) - setUnusedDevices(data?.unusedDevices ?? []) - }, [data]) - - function handleDragEnd(event: DragEndEvent) { - const { active, over } = event - - if (over && active.id !== over.id) { - setBootOrder(items => { - const oldIndex = items.findIndex(disk => disk.name === (active.id as string)) - const newIndex = items.findIndex(disk => disk.name === (over.id as string)) - - return arrayMove(items, oldIndex, newIndex) - }) - } - } - - const removeDevice = (device: Disk) => { - setBootOrder(bootOrder.filter(disk => disk.name !== device.name)) - setUnusedDevices([...unusedDevices, device]) - } - - const addDevice = (device: Disk) => { - setBootOrder([...bootOrder, device]) - setUnusedDevices(unusedDevices.filter(disk => disk.name !== device.name)) - } - - const [loading, setLoading] = useState(false) - - const submit = () => { - setLoading(true) - clearFlashes('server:settings:hardware:boot-order') - - updateBootOrder( - uuid, - bootOrder.map(disk => disk.name) - ) - .then(() => { - setLoading(false) - notify({ - title: 'Updated', - message: 'Updated boot order', - color: 'green', - }) - }) - .catch(error => { - clearAndAddHttpError({ key: 'server:settings:hardware:boot-order', error }) - setLoading(false) - }) - } - - return ( - - - Device Configuration - -
-
Current Boot Order (the highest will be used first)
-
- {bootOrder.length === 0 && ( - - No boot device has been configured. Your VM will not start. - - )} - - - {bootOrder.map(disk => ( - - {({ attributes, listeners }: ChildrenPropsWithHandle) => ( - <> - -
- {disk.displayName ? ( -
-
- - {disk.displayName} - - {disk.type === 'media' ? Media : null} -
- - - {disk.name}, {bytesToString(disk.size)} - -
- ) : ( -
-
- {disk.name} - {disk.type === 'media' ? Media : null} -
- - - {bytesToString(disk.size)} - -
- )} - - -
- - )} -
- ))} -
-
-
-
Unused Devices
-
- {unusedDevices.length === 0 && ( -

There are no unused devices

- )} - {unusedDevices.map(device => ( -
-
- {device.displayName ? ( -
-
- {device.displayName} - {device.type === 'media' ? Media : null} -
- - - {device.name}, {bytesToString(device.size)} - -
- ) : ( -
-
- {device.name} - {device.type === 'media' ? Media : null} -
- - {bytesToString(device.size)} -
- )} - -
-
- ))} -
-
-
- - - -
- ) -} - -export default BootOrderContainer diff --git a/resources/scripts/components/servers/settings/HardwareContainer.tsx b/resources/scripts/components/servers/settings/HardwareContainer.tsx index 928c9c3f121..60eef3f79b0 100644 --- a/resources/scripts/components/servers/settings/HardwareContainer.tsx +++ b/resources/scripts/components/servers/settings/HardwareContainer.tsx @@ -1,119 +1,13 @@ -import getBootOrder from '@/api/server/settings/getBootOrder' -import { Dd, Dt } from '@/components/dashboard/ServerCard' -import Button from '@/components/elements/Button' -import Display from '@/components/elements/displays/DisplayRow' -import FormCard from '@/components/elements/FormCard' -import FormSection from '@/components/elements/FormSection' -import { ServerContext } from '@/state/server' -import { bytesToString } from '@/util/helpers' -import useFlash from '@/util/useFlash' -import useNotify from '@/util/useNotify' -import { useFormik } from 'formik' -import * as yup from 'yup' -import MediaContainer from '@/components/servers/settings/MediaContainer' -import BootOrderContainer from '@/components/servers/settings/BootOrderContainer' -import { useMemo } from 'react' - -const HardwareContainer = () => { - const server = ServerContext.useStoreState(state => state.server.data!) - const { clearFlashes, clearAndAddHttpError } = useFlash() - const notify = useNotify() - - const form = useFormik({ - initialValues: { - name: server.name, - hostname: server.hostname, - }, - validationSchema: yup.object({ - name: yup.string().required('A name is required').max(40), - hostname: yup - .string() - .matches( - /((https?):\/\/)?(www.)?[a-z0-9]+(\.[a-z]{2,}){1,3}(#?\/?[a-zA-Z0-9#]+)*\/?(\?[a-zA-Z0-9-_]+=[a-zA-Z0-9-%]+&?)?$/, - 'Enter a valid hostname' - ), - }), - onSubmit: ({ name, hostname }, { setSubmitting }) => { - clearFlashes('server:settings:hardware') - }, - }) - - const addresses = useMemo(() => [ - ...server.limits.addresses.ipv4, - ...server.limits.addresses.ipv6, - ], [server.limits.addresses]) - - return ( - <> - -
- - Hardware -
-
-
-
CPU
-
{server.limits.cpu}
-
-
-
Memory
-
{bytesToString(server.limits.memory)}
-
-
-
Disk
-
{bytesToString(server.limits.disk)}
-
-
-
-
-
Used Bandwidth
-
{bytesToString(server.usages.bandwidth)}
-
-
-
Allotted Bandwidth
-
- {server.limits.bandwidth ? bytesToString(server.limits.bandwidth) : 'unlimited'} -
-
-
- -
-
IP Addresses
- {addresses.length === 0 ? ( -
There are no addresses associated with this server.
- ) : ( - - {addresses.map(ip => ( - -
-

Address

-

- {ip.address}/{ip.cidr} -

-
-
-

Gateway

-

{ip.gateway}

-
-
-

Mac Address

-

- {ip.macAddress || 'None'} -

-
-
- ))} -
- )} -
-
-
-
-
- - - - ) -} +import MediaCard from '@/components/servers/settings/partials/hardware/MediaCard' +import BootOrderCard from '@/components/servers/settings/partials/hardware/BootOrderCard' +import HardwareDetailsCard from '@/components/servers/settings/partials/hardware/HardwareDetailsCard' + +const HardwareContainer = () => ( + <> + + + + +) export default HardwareContainer diff --git a/resources/scripts/components/servers/settings/NetworkContainer.tsx b/resources/scripts/components/servers/settings/NetworkContainer.tsx index 3a19f1d7c71..e30bfd6f5d8 100644 --- a/resources/scripts/components/servers/settings/NetworkContainer.tsx +++ b/resources/scripts/components/servers/settings/NetworkContainer.tsx @@ -1,120 +1,9 @@ -import getNetwork from '@/api/server/settings/getNetwork' -import updateNetwork from '@/api/server/settings/updateNetwork' -import Button from '@/components/elements/Button' -import FlashMessageRender from '@/components/elements/FlashMessageRenderer' -import FormCard from '@/components/elements/FormCard' -import TextInputFormik from '@/components/elements/forms/TextInputFormik' -import FormSection from '@/components/elements/FormSection' -import TextInput from '@/components/elements/inputs/TextInput' -import { ServerContext } from '@/state/server' -import useFlash from '@/util/useFlash' -import useNotify from '@/util/useNotify' -import { TrashIcon } from '@heroicons/react/20/solid' -import { FieldArray, FormikProvider, useFormik } from 'formik' -import useSWR from 'swr' -import * as yup from 'yup' +import NameserversCard from '@/components/servers/settings/partials/network/NameserversCard' -const NetworkContainer = () => { - const server = ServerContext.useStoreState(state => state.server.data!) - - const { clearFlashes, clearAndAddHttpError } = useFlash() - const notify = useNotify() - - const { data, mutate } = useSWR(['server:settings:hardware', server.uuid], () => getNetwork(server.uuid)) - - const form = useFormik({ - enableReinitialize: true, - initialValues: { - nameservers: data?.nameservers || [], - }, - validationSchema: yup.object({ - nameservers: yup.array().of( - yup - .string() - .matches(/(^(\d{1,3}\.){3}(\d{1,3})$)/, 'Invalid IPv4') - .required('A nameserver is required') - ), - }), - onSubmit: ({ nameservers }, { setSubmitting }) => { - clearFlashes('server:settings:networking') - - updateNetwork(server.id, nameservers) - .then(() => { - notify({ - title: 'Updated', - message: 'Updated network settings', - color: 'green', - }) - mutate(data => ({ ...data, nameservers }), false) - setSubmitting(false) - }) - .catch(error => { - clearAndAddHttpError({ key: 'server:settings:networking', error }) - setSubmitting(false) - }) - }, - }) - - return ( - <> - - -
- - Nameservers -
- -
- ( - <> - {form.values.nameservers.map((nameserver, idx) => ( - arrayHelpers.remove(idx)} - className='bg-transparent' - > - - - } - /> - ))} - - {form.values.nameservers.length < 2 && ( - - )} - - )} - /> -
-
-
- - - -
-
-
- - ) -} +const NetworkContainer = () => ( + <> + + +) export default NetworkContainer diff --git a/resources/scripts/components/servers/settings/SecurityContainer.tsx b/resources/scripts/components/servers/settings/SecurityContainer.tsx index f699c2d0010..64955fa7eb4 100644 --- a/resources/scripts/components/servers/settings/SecurityContainer.tsx +++ b/resources/scripts/components/servers/settings/SecurityContainer.tsx @@ -38,11 +38,10 @@ const SecurityContainer = () => { is: 'cipassword', then: yup.string().required('Must enter a password'), }) - .matches( - /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])(?=.{8,})/, - 'Must Contain 8 Characters, One Uppercase, One Lowercase, One Number and One Special Case Character' - ) - .matches(/^[A-z0-9!@£$%^&*()\'~*_+\-]+$/, 'Must not contain special characters from other languages'), + // @ts-ignore + .passwordRequirements() + // @ts-ignore + .englishKeyboardCharacters(), sshKeys: yup.string(), }), onSubmit: ({ password, sshKeys }, { setSubmitting }) => { diff --git a/resources/scripts/components/servers/settings/partials/hardware/BootOrderCard.tsx b/resources/scripts/components/servers/settings/partials/hardware/BootOrderCard.tsx new file mode 100644 index 00000000000..0c4a8c960d8 --- /dev/null +++ b/resources/scripts/components/servers/settings/partials/hardware/BootOrderCard.tsx @@ -0,0 +1,248 @@ +import { ServerContext } from '@/state/server' +import useSWR from 'swr' +import getBootOrder, { BootOrderSettings } from '@/api/server/settings/getBootOrder' +import { ReactNode, useEffect, useMemo, useState } from 'react' +import { Disk } from '@/api/server/useServerDetails' +import useNotify from '@/util/useNotify' +import useFlash, { useFlashKey } from '@/util/useFlash' +import { DndContext, DragEndEvent } from '@dnd-kit/core' +import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable' +import updateBootOrder from '@/api/server/settings/updateBootOrder' +import FormCard from '@/components/elements/FormCard' +import FlashMessageRender from '@/components/elements/FlashMessageRenderer' +import MessageBox from '@/components/elements/MessageBox' +import { restrictToVerticalAxis, restrictToWindowEdges } from '@dnd-kit/modifiers' +import SortableItem, { ChildrenPropsWithHandle } from '@/components/elements/dnd/SortableItem' +// @ts-expect-error +import DragVerticalIcon from '@/assets/images/icons/drag-vertical.svg' +import { Badge } from '@mantine/core' +import { bytesToString } from '@/util/helpers' +import { PlusIcon, XMarkIcon } from '@heroicons/react/20/solid' +import Button from '@/components/elements/Button' +import useBootOrderSWR from '@/api/server/settings/useBootOrderSWR' +import { updateNotification } from '@mantine/notifications' +import { httpErrorToHuman } from '@/api/http' + +const BootOrderCard = () => { + const uuid = ServerContext.useStoreState(state => state.server.data!.uuid) + const { data, mutate } = useBootOrderSWR(uuid) + + const notify = useNotify() + const { clearFlashes, clearAndAddHttpError } = useFlashKey('server:settings:hardware:boot-order') + + const updateOrder = (disks: string[]) => { + clearFlashes() + + notify({ + id: 'admin:node:template-groups.reorder', + loading: true, + message: 'Saving changes...', + autoClose: false, + disallowClose: true, + }) + + updateBootOrder(uuid, disks) + .then(() => { + updateNotification({ + id: 'admin:node:template-groups.reorder', + message: 'Saved order', + autoClose: 1000, + }) + }) + .catch(error => { + updateNotification({ + id: 'admin:node:template-groups.reorder', + color: 'red', + message: httpErrorToHuman(error), + autoClose: 5000, + }) + clearAndAddHttpError( error ) + }) + } + + const handleDragEnd = ({ active, over }: DragEndEvent) => { + if (over && active.id !== over.id) { + mutate(data => { + const oldIndex = data!.bootOrder.findIndex(disk => disk.interface === (active.id as string)) + const newIndex = data!.bootOrder.findIndex(disk => disk.interface === (over.id as string)) + + const bootOrder = arrayMove(data!.bootOrder, oldIndex, newIndex); + + updateOrder(bootOrder.map(disk => disk.interface)) + + return { + ...data, + bootOrder + } as BootOrderSettings + }, false) + } + } + + const removeDisk = (device: Disk) => { + mutate(data => { + const bootOrder = data!.bootOrder.filter(disk => disk.interface !== device.interface) + const unusedDevices = [ ...data!.unusedDevices, device ] + + updateOrder(bootOrder.map(disk => disk.interface)) + + return { + ...data, + bootOrder, + unusedDevices, + } as BootOrderSettings + }, false) + } + + const addDisk = (device: Disk) => { + mutate(data => { + const bootOrder = [ ...data!.bootOrder, device ] + const unusedDevices = data!.unusedDevices.filter(disk => disk.interface !== device.interface) + + updateOrder(bootOrder.map(disk => disk.interface)) + + return { + ...data, + bootOrder, + unusedDevices, + } as BootOrderSettings + }, false) + } + + return ( + + + Device Configuration + +
+
Current Boot Order (the highest will be used first)
+
+ {data?.bootOrder.length === 0 && ( + + No boot device has been configured. Your VM will not start. + + )} + + disk.interface) ?? []} + strategy={verticalListSortingStrategy} + > + {data?.bootOrder.map(disk => ( + removeDisk(disk)} className='bg-transparent p-1'> + + + } + /> + ))} + + +
+
Unused Devices
+
+ {data?.unusedDevices.length === 0 && ( +

There are no unused devices

+ )} + {data?.unusedDevices.map(device => ( + addDisk(device)} className='bg-transparent p-1'> + + + } + /> + ))} +
+
+
+
+ ) +} + +interface DiskRowProps { + disk: Disk + action: ReactNode +} + +const StaticDiskRow = ({ disk, action }: DiskRowProps) => ( +
+
+
+
+ {getName(disk)} + {getBadge(disk)} +
+ + {getDescription(disk)} +
+ {action} +
+
+) + +const DraggableDiskRow = ({ disk, action }: DiskRowProps) => ( + + {({ attributes, listeners }: ChildrenPropsWithHandle) => ( + <> + +
+
+
+ {getName(disk)} + {getBadge(disk)} +
+ + {getDescription(disk)} +
+ + {action} +
+ + )} +
+) + +const getName = (disk: Disk) => { + if (disk.isPrimaryDisk) { + return 'Primary Disk' + } + if (disk.isMedia) { + return disk.mediaName + } + return disk.interface +} + +const getBadge = (disk: Disk) => { + if (disk.isPrimaryDisk) { + return Primary + } + + if (disk.isMedia) { + return Media + } + + return null +} + +const getDescription = (disk: Disk) => { + if (disk.isPrimaryDisk || disk.isMedia) { + return `${disk.interface}, ${bytesToString(disk.size)}` + } + + return bytesToString(disk.size) +} + +export default BootOrderCard diff --git a/resources/scripts/components/servers/settings/partials/hardware/HardwareDetailsCard.tsx b/resources/scripts/components/servers/settings/partials/hardware/HardwareDetailsCard.tsx new file mode 100644 index 00000000000..586329eea96 --- /dev/null +++ b/resources/scripts/components/servers/settings/partials/hardware/HardwareDetailsCard.tsx @@ -0,0 +1,85 @@ +import { Dd, Dt } from '@/components/dashboard/ServerCard' +import Display from '@/components/elements/displays/DisplayRow' +import FormCard from '@/components/elements/FormCard' +import { ServerContext } from '@/state/server' +import { bytesToString } from '@/util/helpers' +import { useMemo } from 'react' + +const HardwareDetailsCard = () => { + const server = ServerContext.useStoreState(state => state.server.data!) + + const addresses = [ + ...server.limits.addresses.ipv4, + ...server.limits.addresses.ipv6, + ] + + return ( + <> + + + Hardware +
+
+
+
CPU
+
{server.limits.cpu}
+
+
+
Memory
+
{bytesToString(server.limits.memory)}
+
+
+
Disk
+
{bytesToString(server.limits.disk)}
+
+
+
+
+
Used Bandwidth
+
{bytesToString(server.usages.bandwidth)}
+
+
+
Allotted Bandwidth
+
+ {server.limits.bandwidth ? bytesToString(server.limits.bandwidth) : 'unlimited'} +
+
+
+ +
+
IP Addresses
+ {addresses.length === 0 ? ( +
There are no addresses associated with this server.
+ ) : ( + + {addresses.map(ip => ( + +
+

Address

+

+ {ip.address}/{ip.cidr} +

+
+
+

Gateway

+

{ip.gateway}

+
+
+

Mac Address

+

+ {ip.macAddress || 'None'} +

+
+
+ ))} +
+ )} +
+
+
+
+ + ) +} + +export default HardwareDetailsCard; \ No newline at end of file diff --git a/resources/scripts/components/servers/settings/MediaContainer.tsx b/resources/scripts/components/servers/settings/partials/hardware/MediaCard.tsx similarity index 73% rename from resources/scripts/components/servers/settings/MediaContainer.tsx rename to resources/scripts/components/servers/settings/partials/hardware/MediaCard.tsx index 82845e67f91..74c84124d6f 100644 --- a/resources/scripts/components/servers/settings/MediaContainer.tsx +++ b/resources/scripts/components/servers/settings/partials/hardware/MediaCard.tsx @@ -1,16 +1,10 @@ import FormCard from '@/components/elements/FormCard' -import Button from '@/components/elements/Button' -import useSWR from 'swr' import { ServerContext } from '@/state/server' -import getMedia from '@/api/server/settings/getMedia' -import { EyeSlashIcon } from '@heroicons/react/20/solid' -import { bytesToString } from '@/util/helpers' -import { useMemo } from 'react' import useMediaSWR from '@/api/server/settings/useMediaSWR' import FlashMessageRender from '@/components/elements/FlashMessageRenderer' -import MediaRow from '@/components/servers/settings/MediaRow' +import MediaRow from '@/components/servers/settings/partials/hardware/MediaRow' -const MediaContainer = () => { +const MediaCard = () => { const uuid = ServerContext.useStoreState(state => state.server.data!.uuid) const { data } = useMediaSWR(uuid) @@ -35,4 +29,4 @@ const MediaContainer = () => { ) } -export default MediaContainer +export default MediaCard diff --git a/resources/scripts/components/servers/settings/MediaRow.tsx b/resources/scripts/components/servers/settings/partials/hardware/MediaRow.tsx similarity index 100% rename from resources/scripts/components/servers/settings/MediaRow.tsx rename to resources/scripts/components/servers/settings/partials/hardware/MediaRow.tsx diff --git a/resources/scripts/components/servers/settings/partials/network/NameserversCard.tsx b/resources/scripts/components/servers/settings/partials/network/NameserversCard.tsx new file mode 100644 index 00000000000..487eff6c328 --- /dev/null +++ b/resources/scripts/components/servers/settings/partials/network/NameserversCard.tsx @@ -0,0 +1,119 @@ +import getNetwork from '@/api/server/settings/getNetwork' +import updateNetwork from '@/api/server/settings/updateNetwork' +import Button from '@/components/elements/Button' +import FlashMessageRender from '@/components/elements/FlashMessageRenderer' +import FormCard from '@/components/elements/FormCard' +import TextInputFormik from '@/components/elements/forms/TextInputFormik' +import { ServerContext } from '@/state/server' +import useFlash from '@/util/useFlash' +import useNotify from '@/util/useNotify' +import { TrashIcon } from '@heroicons/react/20/solid' +import { FieldArray, FormikProvider, useFormik } from 'formik' +import useSWR from 'swr' +import * as yup from 'yup' + +const NameserversCard = () => { + const server = ServerContext.useStoreState(state => state.server.data!) + + const { clearFlashes, clearAndAddHttpError } = useFlash() + const notify = useNotify() + + const { data, mutate } = useSWR(['server:settings:hardware', server.uuid], () => getNetwork(server.uuid)) + + const form = useFormik({ + enableReinitialize: true, + initialValues: { + nameservers: data?.nameservers || [], + }, + validationSchema: yup.object({ + nameservers: yup.array().of( + yup + .string() + // @ts-ignore + .ipAddress() + .required('A nameserver is required') + ), + }), + onSubmit: ({ nameservers }, { setSubmitting }) => { + clearFlashes('server:settings:networking') + + updateNetwork(server.id, nameservers) + .then(() => { + notify({ + title: 'Updated', + message: 'Updated network settings', + color: 'green', + }) + mutate(data => ({ ...data, nameservers }), false) + setSubmitting(false) + }) + .catch(error => { + clearAndAddHttpError({ key: 'server:settings:networking', error }) + setSubmitting(false) + }) + }, + }) + + return ( + <> + + +
+ + Nameservers +
+ +
+ ( + <> + {form.values.nameservers.map((nameserver, idx) => ( + arrayHelpers.remove(idx)} + className='bg-transparent' + > + + + } + /> + ))} + + {form.values.nameservers.length < 2 && ( + + )} + + )} + /> +
+
+
+ + + +
+
+
+ + ) +} + +export default NameserversCard diff --git a/resources/scripts/main.tsx b/resources/scripts/main.tsx index 265bd59beb4..f050aab2bf4 100644 --- a/resources/scripts/main.tsx +++ b/resources/scripts/main.tsx @@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client' import '@/assets/css/tailwind.css' import '@/assets/css/preflight.css' +import '@/util/registerCustomYupValidationRules' import App from '@/components/App' diff --git a/resources/scripts/util/registerCustomYupValidationRules.ts b/resources/scripts/util/registerCustomYupValidationRules.ts new file mode 100644 index 00000000000..44e76020066 --- /dev/null +++ b/resources/scripts/util/registerCustomYupValidationRules.ts @@ -0,0 +1,28 @@ +import * as yup from 'yup' + +yup.addMethod(yup.string, 'englishKeyboardCharacters', function () { + return this.matches(/^[A-Za-z0-9!@#$%^&*()_+\-=[\]{}|;':",.\/<>?\\ ]*$/, { + message: 'Invalid English keyboard characters', + excludeEmptyString: true, + }) +}) + +yup.addMethod(yup.string, 'passwordRequirements', function () { + return this.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])(?=.{8,})/, { + message: 'Must contain 8 characters, 1 uppercase, 1 lowercase, 1 number and 1 special character', + excludeEmptyString: true, + }) +}) + +yup.addMethod(yup.string, 'ipAddress', function () { + return this.test({ + name: 'ipAddress', + message: 'Invalid IP address', + test: value => { + const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/ + const ipv6Regex = /^([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}$/ + + return ipv4Regex.test(value as string) || ipv6Regex.test(value as string) + }, + }) +}) diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php index 72a8cadbcce..6e189a49aec 100644 --- a/resources/views/app.blade.php +++ b/resources/views/app.blade.php @@ -1,48 +1,49 @@ - - - + + + - {{ config('app.name', 'Laravel') }} + {{ config('app.name', 'Laravel') }} - + - - + + - - @if(!is_null(Auth::user())) - - @endif - - @if(!empty($siteConfiguration)) - - @endif - - - @viteReactRefresh - @vite('resources/scripts/main.tsx') - - - + + @if(!is_null(Auth::user())) + @endif - gtag('event', 'meta', { - 'version': '{{ config('app.version') }}' - }); + @if(!empty($siteConfiguration)) + - - -
- + @endif + + + @viteReactRefresh + @vite('resources/scripts/main.tsx') + + + + + + + +
+