create.blade.php 21 KB

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