[Feature] Addon implemented

This commit is contained in:
AGuyNamedJens 2024-05-22 14:26:18 +02:00
parent 159ba02c84
commit 130a3b308b
5 changed files with 95 additions and 60 deletions

View file

@ -14,7 +14,7 @@ use Illuminate\Http\Request;
use Illuminate\Support\Collection;
class ProductController extends Controller
{
{
private $pterodactyl;
public function __construct(PterodactylSettings $ptero_settings)
@ -97,30 +97,41 @@ class ProductController extends Controller
}
/**
* @param Node $node
* @param Int $location
* @param Egg $egg
* @return Collection|JsonResponse
*/
public function getProductsBasedOnNode(Egg $egg, Node $node)
public function getProductsBasedOnLocation(Egg $egg, Int $location)
{
if (is_null($egg->id) || is_null($node->id)) {
return response()->json('node and egg id is required', '400');
if (is_null($egg->id) || is_null($location)) {
return response()->json('location and egg id is required', '400');
}
// Get all nodes in this location
$nodes = Node::query()
->where('location_id', '=', $location)
->get();
$products = Product::query()
->where('disabled', '=', false)
->whereHas('nodes', function (Builder $builder) use ($node) {
$builder->where('id', '=', $node->id);
->whereHas('nodes', function (Builder $builder) use ($nodes) {
$builder->whereIn('id', $nodes->map(function ($node) {
return $node->id;
}));
})
->whereHas('eggs', function (Builder $builder) use ($egg) {
$builder->where('id', '=', $egg->id);
})
->get();
$pteroNode = $this->pterodactyl->getNode($node->id);
// Instead of the old node check, we will check if the product fits in any given node in the location
foreach ($products as $key => $product) {
if ($product->memory > ($pteroNode['memory'] * ($pteroNode['memory_overallocate'] + 100) / 100) - $pteroNode['allocated_resources']['memory'] || $product->disk > ($pteroNode['disk'] * ($pteroNode['disk_overallocate'] + 100) / 100) - $pteroNode['allocated_resources']['disk']) {
$product->doesNotFit = true;
$product->doesNotFit = false;
foreach ($nodes as $node) {
$pteroNode = $this->pterodactyl->getNode($node->id);
if ($product->memory > ($pteroNode['memory'] * ($pteroNode['memory_overallocate'] + 100) / 100) - $pteroNode['allocated_resources']['memory'] || $product->disk > ($pteroNode['disk'] * ($pteroNode['disk_overallocate'] + 100) / 100) - $pteroNode['allocated_resources']['disk']) {
$product->doesNotFit = true;
}
}
}

View file

@ -141,13 +141,10 @@ class ServerController extends Controller
$product = Product::findOrFail(FacadesRequest::input('product'));
// Get node resource allocation info
$node = $product->nodes()->findOrFail(FacadesRequest::input('node'));
$nodeName = $node->name;
// Check if node has enough memory and disk space
$checkResponse = $this->pterodactyl->checkNodeResources($node, $product->memory, $product->disk);
if ($checkResponse == false) {
return redirect()->route('servers.index')->with('error', __("The node '" . $nodeName . "' doesn't have the required memory or disk left to allocate this product."));
$location = FacadesRequest::input('location');
$availableNode = $this->getAvailableNode($location, $product);
if (!$availableNode) {
return redirect()->route('servers.index')->with('error', __("The chosen location doesn't have the required memory or disk left to allocate this product."));
}
// Min. Credits
@ -179,7 +176,7 @@ class ServerController extends Controller
/** Store a newly created resource in storage. */
public function store(Request $request, UserSettings $user_settings, ServerSettings $server_settings, GeneralSettings $generalSettings)
{
/** @var Node $node */
/** @var Location $location */
/** @var Egg $egg */
/** @var Product $product */
$validate_configuration = $this->validateConfigurationRules($user_settings, $server_settings, $generalSettings);
@ -190,15 +187,23 @@ class ServerController extends Controller
$request->validate([
'name' => 'required|max:191',
'node' => 'required|exists:nodes,id',
'location' => 'required|exists:locations,id',
'egg' => 'required|exists:eggs,id',
'product' => 'required|exists:products,id',
]);
//get required resources
// Get the product and egg
$product = Product::query()->findOrFail($request->input('product'));
$egg = $product->eggs()->findOrFail($request->input('egg'));
$node = $product->nodes()->findOrFail($request->input('node'));
// Get an available node
$location = $request->input('location');
$availableNode = $this->getAvailableNode($location, $product);
$node = Node::query()->find($availableNode);
if(!$node) {
return redirect()->route('servers.index')->with('error', __("No nodes satisfying the requirements for automatic deployment on this location were found."));
}
$server = $request->user()->servers()->create([
'name' => $request->input('name'),
@ -316,7 +321,7 @@ class ServerController extends Controller
})
->get();
// Set the each product eggs array to just contain the eggs name
// Set each product eggs array to just contain the eggs name
foreach ($products as $product) {
$product->eggs = $product->eggs->pluck('name')->toArray();
if ($product->memory - $currentProduct->memory > ($pteroNode['memory'] * ($pteroNode['memory_overallocate'] + 100) / 100) - $pteroNode['allocated_resources']['memory'] || $product->disk - $currentProduct->disk > ($pteroNode['disk'] * ($pteroNode['disk_overallocate'] + 100) / 100) - $pteroNode['allocated_resources']['disk']) {
@ -356,8 +361,8 @@ class ServerController extends Controller
// Check if node has enough memory and disk space
$requireMemory = $newProduct->memory - $oldProduct->memory;
$requiredisk = $newProduct->disk - $oldProduct->disk;
$checkResponse = $this->pterodactyl->checkNodeResources($node, $requireMemory, $requiredisk);
if ($checkResponse == false) {
$nodeFree = $this->pterodactyl->checkNodeResources($node, $requireMemory, $requiredisk);
if (!$nodeFree) {
return redirect()->route('servers.index')->with('error', __("The node '" . $nodeName . "' doesn't have the required memory or disk left to upgrade the server."));
}
@ -412,4 +417,30 @@ class ServerController extends Controller
return redirect()->route('servers.show', ['server' => $server->id])->with('error', __('Not Enough Balance for Upgrade'));
}
}
/**
* @param string $location
* @param Product $product
* @return int | null Node ID
*/
private function getAvailableNode(string $location, Product $product)
{
$collection = Node::query()->where('location_id', $location)->get();
// loop through nodes and check if the node has enough resources
foreach ($collection as $node) {
// Check if the node has enough memory and disk space
$freeNode = $this->pterodactyl->checkNodeResources($node, $product->memory, $product->disk);
// Remove the node from the collection if it doesn't have enough resources
if (!$freeNode) {
$collection->forget($node['id']);
}
}
if($collection->isEmpty()) {
return null;
}
return $collection->first()['id'];
}
}

View file

@ -71,7 +71,7 @@
"Change Status": "Change Status",
"Profile updated": "Profile updated",
"Server limit reached!": "Server limit reached!",
"The node '\" . $nodeName . \"' doesn't have the required memory or disk left to allocate this product.": "The node '\" . $nodeName . \"' doesn't have the required memory or disk left to allocate this product.",
"The chosen location doesn't have the required memory or disk left to allocate this product.": "The chosen location doesn't have the required memory or disk left to allocate this product.",
"You are required to verify your email address before you can create a server.": "You are required to verify your email address before you can create a server.",
"You are required to link your discord account before you can create a server.": "You are required to link your discord account before you can create a server.",
"Server created": "Server created",

View file

@ -95,7 +95,7 @@ Route::middleware(['auth', 'checkSuspended'])->group(function () {
//routes made for server create page to fetch product info
Route::get('/products/nodes/egg/{egg?}', [FrontProductController::class, 'getNodesBasedOnEgg'])->name('products.nodes.egg');
Route::get('/products/locations/egg/{egg?}', [FrontProductController::class, 'getLocationsBasedOnEgg'])->name('products.locations.egg');
Route::get('/products/products/{egg?}/{node?}', [FrontProductController::class, 'getProductsBasedOnNode'])->name('products.products.node');
Route::get('/products/products/{egg?}/{location?}', [FrontProductController::class, 'getProductsBasedOnLocation'])->name('products.products.location');
//payments
Route::get('checkout/{shopProduct}', [PaymentController::class, 'checkOut'])->name('checkout');

View file

@ -133,32 +133,25 @@
</div>
</div>
<div class="form-group">
<label for="node">{{ __('Node') }}</label>
<select name="node" required id="node" x-model="selectedNode"
:disabled="!fetchedLocations" @change="fetchProducts();" class="custom-select">
<option x-text="getNodeInputText()" disabled selected hidden value="null">
<div class="form-group">
<label for="location">{{ __('Location') }}</label>
<select name="location" required id="location" x-model="selectedLocation" :disabled="!fetchedLocations"
@change="fetchProducts();" class="custom-select">
<option x-text="getLocationInputText()" disabled selected hidden value="null">
</option>
<template x-for="location in locations" :key="location.id">
<option x-text="location.name" :value="location.id">
</option>
<template x-for="location in locations" :key="location.id">
<optgroup :label="location.name">
<template x-for="node in location.nodes" :key="node.id">
<option x-text="node.name" :value="node.id">
</option>
</template>
</optgroup>
</template>
</template>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="w-100"></div>
<div class="col" x-show="selectedNode != null">
<div class="col" x-show="selectedLocation != null">
<div class="row mt-4 justify-content-center">
<template x-for="product in products" :key="product.id">
<div class="card col-xl-3 col-lg-3 col-md-4 col-sm-10 mr-2 ml-2 ">
@ -248,7 +241,7 @@
product.doesNotFit == true ||
submitClicked ? 'disabled' : ''"
class="btn btn-primary btn-block mt-2" @click="setProduct(product.id);"
x-text="product.doesNotFit == true ? '{{ __('Server cant fit on this Node') }}' : (product.minimum_credits > user.credits || product.price > user.credits ? '{{ __('Not enough') }} {{ $credits_display_name }}!' : '{{ __('Create server') }}')">
x-text="product.doesNotFit == true ? '{{ __('Server cant fit on this Location') }}' : (product.minimum_credits > user.credits || product.price > user.credits ? '{{ __('Not enough') }} {{ $credits_display_name }}!' : '{{ __('Create server') }}')">
</button>
</div>
@ -278,13 +271,13 @@
name: null,
selectedNest: null,
selectedEgg: null,
selectedNode: null,
selectedLocation: null,
selectedProduct: null,
//selected objects based on input
selectedNestObject: {},
selectedEggObject: {},
selectedNodeObject: {},
selectedLocationObject: {},
selectedProductObject: {},
//values
@ -309,7 +302,7 @@
this.locations = [];
this.products = [];
this.selectedEgg = 'null';
this.selectedNode = 'null';
this.selectedLocation = 'null';
this.selectedProduct = 'null';
this.eggs = this.eggsSave.filter(egg => egg.nest_id == this.selectedNest)
@ -343,7 +336,7 @@
this.fetchedProducts = false;
this.locations = [];
this.products = [];
this.selectedNode = 'null';
this.selectedLocation = 'null';
this.selectedProduct = 'null';
let response = await axios.get(`{{ route('products.locations.egg') }}/${this.selectedEgg}`)
@ -354,7 +347,7 @@
//automatically select the first entry if there is only 1
if (this.locations.length === 1 && this.locations[0]?.nodes?.length === 1) {
this.selectedNode = this.locations[0]?.nodes[0]?.id;
this.selectedLocation = this.locations[0]?.id;
await this.fetchProducts();
return;
}
@ -366,7 +359,7 @@
/**
* @description fetch all available products based on the selected node
* @note called whenever a node is selected
* @see selectedNode
* @see selectedLocation
*/
async fetchProducts() {
this.loading = true;
@ -375,7 +368,7 @@
this.selectedProduct = 'null';
let response = await axios.get(
`{{ route('products.products.node') }}/${this.selectedEgg}/${this.selectedNode}`)
`{{ route('products.products.location') }}/${this.selectedEgg}/${this.selectedLocation}`)
.catch(console.error)
this.fetchedProducts = true;
@ -410,10 +403,10 @@
this.selectedNestObject = this.nests.find(nest => nest.id == this.selectedNest) ?? {}
this.selectedEggObject = this.eggs.find(egg => egg.id == this.selectedEgg) ?? {}
this.selectedNodeObject = {};
this.selectedLocationObject = {};
this.locations.forEach(location => {
if (!this.selectedNodeObject?.id) {
this.selectedNodeObject = location.nodes.find(node => node.id == this.selectedNode) ??
if (!this.selectedLocationObject?.id) {
this.selectedLocationObject = location.nodes.find(node => node.id == this.selectedLocation) ??
{};
}
})
@ -429,17 +422,17 @@
isFormValid() {
if (Object.keys(this.selectedNestObject).length === 0) return false;
if (Object.keys(this.selectedEggObject).length === 0) return false;
if (Object.keys(this.selectedNodeObject).length === 0) return false;
if (Object.keys(this.selectedLocationObject).length === 0) return false;
if (Object.keys(this.selectedProductObject).length === 0) return false;
return !!this.name;
},
getNodeInputText() {
getLocationInputText() {
if (this.fetchedLocations) {
if (this.locations.length > 0) {
return '{{ __('Please select a node ...') }}';
return '{{ __('Please select a location ...') }}';
}
return '{{ __('No nodes found matching current configuration') }}'
return '{{ __('No location found matching current configuration') }}'
}
return '{{ __('---') }}';
},