create.blade.php 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. @extends('layouts.main')
  2. @section('content')
  3. <!-- CONTENT HEADER -->
  4. <section class="content-header">
  5. <div class="container-fluid">
  6. <div class="row mb-2">
  7. <div class="col-sm-6">
  8. <h1>{{ __('Servers') }}</h1>
  9. </div>
  10. <div class="col-sm-6">
  11. <ol class="breadcrumb float-sm-right">
  12. <li class="breadcrumb-item"><a href="{{ route('home') }}">{{ __('Dashboard') }}</a></li>
  13. <li class="breadcrumb-item"><a href="{{ route('servers.index') }}">{{ __('Servers') }}</a>
  14. <li class="breadcrumb-item"><a class="text-muted"
  15. href="{{ route('servers.create') }}">{{ __('Create') }}</a>
  16. </li>
  17. </ol>
  18. </div>
  19. </div>
  20. </div>
  21. </section>
  22. <!-- END CONTENT HEADER -->
  23. <!-- MAIN CONTENT -->
  24. <section x-data="serverApp()" class="content">
  25. <div class="container-xxl">
  26. <!-- FORM -->
  27. <form action="{{ route('servers.store') }}" x-on:submit="submitClicked = true" method="post"
  28. class="row justify-content-center">
  29. @csrf
  30. <div class="col-xl-6 col-lg-8 col-md-8 col-sm-10">
  31. <div class="card">
  32. <div class="card-header">
  33. <div class="card-title"><i class="fas fa-cogs mr-2"></i>{{ __('Server configuration') }}
  34. </div>
  35. </div>
  36. @if (!$server_creation_enabled)
  37. <div class="alert alert-warning p-2 m-2">
  38. {{ __('The creation of new servers has been disabled for regular users, enable it again') }}
  39. <a href="{{ route('admin.settings.index', "#Server") }}">{{ __('here') }}</a>.
  40. </div>
  41. @endif
  42. @if ($productCount === 0 || $nodeCount === 0 || count($nests) === 0 || count($eggs) === 0)
  43. <div class="alert alert-danger p-2 m-2">
  44. <h5><i class="icon fas fa-exclamation-circle"></i>{{ __('Error!') }}</h5>
  45. <p class="pl-4">
  46. @if (Auth::user()->hasRole("Admin"))
  47. {{ __('Make sure to link your products to nodes and eggs.') }} <br>
  48. {{ __('There has to be at least 1 valid product for server creation') }}
  49. <a href="{{ route('admin.overview.sync') }}">{{ __('Sync now') }}</a>
  50. @endif
  51. </p>
  52. <ul>
  53. @if ($productCount === 0)
  54. <li> {{ __('No products available!') }}</li>
  55. @endif
  56. @if ($nodeCount === 0)
  57. <li>{{ __('No nodes have been linked!') }}</li>
  58. @endif
  59. @if (count($nests) === 0)
  60. <li>{{ __('No nests available!') }}</li>
  61. @endif
  62. @if (count($eggs) === 0)
  63. <li>{{ __('No eggs have been linked!') }}</li>
  64. @endif
  65. </ul>
  66. </div>
  67. @endif
  68. <div x-show="loading" class="overlay dark">
  69. <i class="fas fa-2x fa-sync-alt"></i>
  70. </div>
  71. <div class="card-body">
  72. @if ($errors->any())
  73. <div class="alert alert-danger">
  74. <ul class="list-group pl-3">
  75. @foreach ($errors->all() as $error)
  76. <li>{{ $error }}</li>
  77. @endforeach
  78. </ul>
  79. </div>
  80. @endif
  81. <div class="form-group">
  82. <label for="name">{{ __('Name') }}</label>
  83. <input x-model="name" id="name" name="name" type="text" required="required"
  84. class="form-control @error('name') is-invalid @enderror">
  85. @error('name')
  86. <div class="invalid-feedback">
  87. {{ $message }}
  88. </div>
  89. @enderror
  90. </div>
  91. <div class="row">
  92. <div class="col-md-6">
  93. <div class="form-group">
  94. <label for="nest">{{ __('Software / Games') }}</label>
  95. <select class="custom-select" required name="nest" id="nest"
  96. x-model="selectedNest" @change="setEggs();">
  97. <option selected disabled hidden value="null">
  98. {{ count($nests) > 0 ? __('Please select software ...') : __('---') }}
  99. </option>
  100. @foreach ($nests as $nest)
  101. <option value="{{ $nest->id }}">{{ $nest->name }}</option>
  102. @endforeach
  103. </select>
  104. </div>
  105. </div>
  106. <div class="col-md-6">
  107. <div class="form-group">
  108. <label for="egg">{{ __('Specification ') }}</label>
  109. <div>
  110. <select id="egg" required name="egg" :disabled="eggs.length == 0"
  111. x-model="selectedEgg" @change="fetchLocations();" required="required"
  112. class="custom-select">
  113. <option x-text="getEggInputText()" selected disabled hidden value="null">
  114. </option>
  115. <template x-for="egg in eggs" :key="egg.id">
  116. <option x-text="egg.name" :value="egg.id"></option>
  117. </template>
  118. </select>
  119. </div>
  120. </div>
  121. </div>
  122. </div>
  123. <div class="form-group">
  124. <label for="node">{{ __('Node') }}</label>
  125. <select name="node" required id="node" x-model="selectedNode"
  126. :disabled="!fetchedLocations" @change="fetchProducts();" class="custom-select">
  127. <option x-text="getNodeInputText()" disabled selected hidden value="null">
  128. </option>
  129. <template x-for="location in locations" :key="location.id">
  130. <optgroup :label="location.name">
  131. <template x-for="node in location.nodes" :key="node.id">
  132. <option x-text="node.name" :value="node.id">
  133. </option>
  134. </template>
  135. </optgroup>
  136. </template>
  137. </select>
  138. </div>
  139. </div>
  140. </div>
  141. </div>
  142. <div class="w-100"></div>
  143. <div class="col" x-show="selectedNode != null">
  144. <div class="row mt-4 justify-content-center">
  145. <template x-for="product in products" :key="product.id">
  146. <div class="card col-xl-3 col-lg-3 col-md-4 col-sm-10 mr-2 ml-2 ">
  147. <div class="card-body d-flex flex-column">
  148. <h4 class="card-title" x-text="product.name"></h4>
  149. <div class="mt-2">
  150. <div>
  151. <p class="card-text text-muted mb-1">{{ __('Resource Data:') }}</p>
  152. <ul class="pl-0">
  153. <li class="d-flex justify-content-between">
  154. <span class="d-inline-block"><i class="fas fa-microchip"></i>
  155. {{ __('CPU') }}</span>
  156. <span class=" d-inline-block"
  157. x-text="product.cpu + ' {{ __('vCores') }}'"></span>
  158. </li>
  159. <li class="d-flex justify-content-between">
  160. <span class="d-inline-block"><i class="fas fa-memory"></i>
  161. {{ __('Memory') }}</span>
  162. <span class=" d-inline-block"
  163. x-text="product.memory + ' {{ __('MB') }}'"></span>
  164. </li>
  165. <li class="d-flex justify-content-between">
  166. <div>
  167. <i class="fas fa-hdd"></i>
  168. <span class="d-inline-block">
  169. {{ __('Disk') }}
  170. </span>
  171. </div>
  172. <span class="d-inline-block"
  173. x-text="product.disk + ' {{ __('MB') }}'"></span>
  174. </li>
  175. <li class="d-flex justify-content-between">
  176. <span class="d-inline-block"><i class="fas fa-save"></i>
  177. {{ __('Backups') }}</span>
  178. <span class=" d-inline-block" x-text="product.backups"></span>
  179. </li>
  180. <li class="d-flex justify-content-between">
  181. <span class="d-inline-block"><i class="fas fa-database"></i>
  182. {{ __('MySQL') }}
  183. {{ __('Databases') }}</span>
  184. <span class="d-inline-block" x-text="product.databases"></span>
  185. </li>
  186. <li class="d-flex justify-content-between">
  187. <span class="d-inline-block"><i class="fas fa-network-wired"></i>
  188. {{ __('Allocations') }}
  189. ({{ __('ports') }})</span>
  190. <span class="d-inline-block" x-text="product.allocations"></span>
  191. </li>
  192. <li class="d-flex justify-content-between">
  193. <span class="d-inline-block"><i class="fas fa-clock"></i>
  194. {{ __('Billing Period') }}</span>
  195. <span class="d-inline-block" x-text="product.billing_period"></span>
  196. </li>
  197. <li class="d-flex justify-content-between">
  198. <span class="d-inline-block"><i class="fa fa-coins"></i>
  199. {{ __('Minimum') }} {{ $credits_display_name }}</span>
  200. <span class="d-inline-block"
  201. x-text="product.minimum_credits == -1 ? {{ $min_credits_to_make_server }} : product.minimum_credits"></span>
  202. </li>
  203. </ul>
  204. </div>
  205. <div class="mt-2 mb-2">
  206. <span class="card-text text-muted">{{ __('Description') }}</span>
  207. <p class="card-text" style="white-space:pre-wrap"
  208. x-text="product.description"></p>
  209. </div>
  210. </div>
  211. <div class="mt-auto border rounded border-secondary">
  212. <div class="d-flex justify-content-between p-2">
  213. <span class="d-inline-block mr-4"
  214. x-text="'{{ __('Price') }}' + ' (' + product.billing_period + ')'">
  215. </span>
  216. <span class="d-inline-block"
  217. x-text="product.price + ' {{ $credits_display_name }}'"></span>
  218. </div>
  219. </div>
  220. <div>
  221. <input type="hidden" name="product" x-model="selectedProduct">
  222. </div>
  223. <div>
  224. <button type="submit" x-model="selectedProduct" name="product"
  225. :disabled="product.minimum_credits > user.credits || product.price > user.credits ||
  226. product.doesNotFit == true ||
  227. submitClicked"
  228. :class="product.minimum_credits > user.credits || product.price > user.credits ||
  229. product.doesNotFit == true ||
  230. submitClicked ? 'disabled' : ''"
  231. class="btn btn-primary btn-block mt-2" @click="setProduct(product.id);"
  232. 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') }}')">
  233. </button>
  234. @if (env('APP_ENV') == 'local' || $store_enabled)
  235. <template x-if="product.price > user.credits">
  236. <a href="{{ route('store.index') }}">
  237. <button type="button" class="btn btn-warning btn-block mt-2">
  238. {{ __('Buy more') }} {{ $credits_display_name }}
  239. </button>
  240. </a>
  241. </template>
  242. @endif
  243. </div>
  244. </div>
  245. </div>
  246. </template>
  247. </div>
  248. </div>
  249. </form>
  250. <!-- END FORM -->
  251. </div>
  252. </section>
  253. <!-- END CONTENT -->
  254. <script>
  255. function serverApp() {
  256. return {
  257. //loading
  258. loading: false,
  259. fetchedLocations: false,
  260. fetchedProducts: false,
  261. //input fields
  262. name: null,
  263. selectedNest: null,
  264. selectedEgg: null,
  265. selectedNode: null,
  266. selectedProduct: null,
  267. //selected objects based on input
  268. selectedNestObject: {},
  269. selectedEggObject: {},
  270. selectedNodeObject: {},
  271. selectedProductObject: {},
  272. //values
  273. user: {!! $user !!},
  274. nests: {!! $nests !!},
  275. eggsSave: {!! $eggs !!}, //store back-end eggs
  276. eggs: [],
  277. locations: [],
  278. products: [],
  279. submitClicked: false,
  280. /**
  281. * @description set available eggs based on the selected nest
  282. * @note called whenever a nest is selected
  283. * @see selectedNest
  284. */
  285. async setEggs() {
  286. this.fetchedLocations = false;
  287. this.fetchedProducts = false;
  288. this.locations = [];
  289. this.products = [];
  290. this.selectedEgg = 'null';
  291. this.selectedNode = 'null';
  292. this.selectedProduct = 'null';
  293. this.eggs = this.eggsSave.filter(egg => egg.nest_id == this.selectedNest)
  294. //automatically select the first entry if there is only 1
  295. if (this.eggs.length === 1) {
  296. this.selectedEgg = this.eggs[0].id;
  297. await this.fetchLocations();
  298. return;
  299. }
  300. this.updateSelectedObjects()
  301. },
  302. setProduct(productId) {
  303. if (!productId) return
  304. this.selectedProduct = productId;
  305. this.updateSelectedObjects();
  306. },
  307. /**
  308. * @description fetch all available locations based on the selected egg
  309. * @note called whenever a server configuration is selected
  310. * @see selectedEg
  311. */
  312. async fetchLocations() {
  313. this.loading = true;
  314. this.fetchedLocations = false;
  315. this.fetchedProducts = false;
  316. this.locations = [];
  317. this.products = [];
  318. this.selectedNode = 'null';
  319. this.selectedProduct = 'null';
  320. let response = await axios.get(`{{ route('products.locations.egg') }}/${this.selectedEgg}`)
  321. .catch(console.error)
  322. this.fetchedLocations = true;
  323. this.locations = response.data
  324. //automatically select the first entry if there is only 1
  325. if (this.locations.length === 1 && this.locations[0]?.nodes?.length === 1) {
  326. this.selectedNode = this.locations[0]?.nodes[0]?.id;
  327. await this.fetchProducts();
  328. return;
  329. }
  330. this.loading = false;
  331. this.updateSelectedObjects()
  332. },
  333. /**
  334. * @description fetch all available products based on the selected node
  335. * @note called whenever a node is selected
  336. * @see selectedNode
  337. */
  338. async fetchProducts() {
  339. this.loading = true;
  340. this.fetchedProducts = false;
  341. this.products = [];
  342. this.selectedProduct = 'null';
  343. let response = await axios.get(
  344. `{{ route('products.products.node') }}/${this.selectedEgg}/${this.selectedNode}`)
  345. .catch(console.error)
  346. this.fetchedProducts = true;
  347. // TODO: Sortable by user chosen property (cpu, ram, disk...)
  348. this.products = response.data.sort((p1, p2) => parseInt(p1.price, 10) > parseInt(p2.price, 10) &&
  349. 1 || -1)
  350. //divide cpu by 100 for each product
  351. this.products.forEach(product => {
  352. product.cpu = product.cpu / 100;
  353. })
  354. //format price to have no decimals if it is a whole number
  355. this.products.forEach(product => {
  356. if (product.price % 1 === 0) {
  357. product.price = Math.round(product.price);
  358. }
  359. })
  360. this.loading = false;
  361. this.updateSelectedObjects()
  362. },
  363. /**
  364. * @description map selected id's to selected objects
  365. * @note being used in the server info box
  366. */
  367. updateSelectedObjects() {
  368. this.selectedNestObject = this.nests.find(nest => nest.id == this.selectedNest) ?? {}
  369. this.selectedEggObject = this.eggs.find(egg => egg.id == this.selectedEgg) ?? {}
  370. this.selectedNodeObject = {};
  371. this.locations.forEach(location => {
  372. if (!this.selectedNodeObject?.id) {
  373. this.selectedNodeObject = location.nodes.find(node => node.id == this.selectedNode) ??
  374. {};
  375. }
  376. })
  377. this.selectedProductObject = this.products.find(product => product.id == this.selectedProduct) ?? {}
  378. console.log(this.selectedProduct, this.selectedProductObject, this.products)
  379. },
  380. /**
  381. * @description check if all options are selected
  382. * @return {boolean}
  383. */
  384. isFormValid() {
  385. if (Object.keys(this.selectedNestObject).length === 0) return false;
  386. if (Object.keys(this.selectedEggObject).length === 0) return false;
  387. if (Object.keys(this.selectedNodeObject).length === 0) return false;
  388. if (Object.keys(this.selectedProductObject).length === 0) return false;
  389. return !!this.name;
  390. },
  391. getNodeInputText() {
  392. if (this.fetchedLocations) {
  393. if (this.locations.length > 0) {
  394. return '{{ __('Please select a node ...') }}';
  395. }
  396. return '{{ __('No nodes found matching current configuration') }}'
  397. }
  398. return '{{ __('---') }}';
  399. },
  400. getProductInputText() {
  401. if (this.fetchedProducts) {
  402. if (this.products.length > 0) {
  403. return '{{ __('Please select a resource ...') }}';
  404. }
  405. return '{{ __('No resources found matching current configuration') }}'
  406. }
  407. return '{{ __('---') }}';
  408. },
  409. getEggInputText() {
  410. if (this.selectedNest) {
  411. return '{{ __('Please select a configuration ...') }}';
  412. }
  413. return '{{ __('---') }}';
  414. },
  415. getProductOptionText(product) {
  416. let text = product.name + ' (' + product.description + ')';
  417. if (product.minimum_credits > this.user.credits) {
  418. return '{{ __('Not enough credits!') }} | ' + text;
  419. }
  420. return text;
  421. }
  422. }
  423. }
  424. </script>
  425. @endsection