From 1d4f47c52489115c5599e56b2782d7cb8ab0968c Mon Sep 17 00:00:00 2001 From: Alaa228 Date: Thu, 19 Oct 2023 14:37:59 +0300 Subject: [PATCH] Dedicated Nodes in vue3 (#1180) * implement explorer nodes page and finish filtering * remove map.js file and use cdn version * add nodes html file * Push the code to sync the code to work on another issue. * Working on changing the style, creating types for input filter, and refactoring the code. * Added the nodes filter inside a watch to update the nodes on change. * Added rules * WIP: working on refactor the filter fields to be in one model. * Implement explorer nodes table with improved code structure and await http_grid_client support for additional fields. * Installed the @threefold/gridproxy_client package to sync the newly added filds * Enhance the nodes table * Lower folders and components name, removed an empty file and move the node table component into the nodes folder instead of common/filters * WIP: working on fixing the comments. * add dedicated nodes page * Push the node if as a query param on click on the table row to open the sheet on mount if there is node selected. * Moved the resources charts to an isolated component. * add filters component and validations * add todo * use v-expansion-panel * add table tabs * add table component * Moved the getMetricsUrl script to be inside the node details component. * Update the filter-nodes component, added a small refactor to the nodes view. * Update the getStatus function to return the standBy status, updated the toReadableDate function to return nodes less than a day. * Update and improve the GrafanaStatistics class to work against the GridProxyClient. * Added new 'update:state' event to efficiently update node statistics in the Node Details Card component. * add table * Update node details page with node cards. * add reserve button * Updated the node explorer details dialog, enhanced the preformance. * Added GPU, Public config details card, enhanced the responcive, wating on the supporting the GPU info in the grid proxy client * add unresrve && add discounts * add reserve button function * expanded content * Updated the node GPU details card, moved the getNodeStatusColor method in the helpers, hide the getNodeHealth button in case the node is offline. * Fix the issue of filtering the farms with ids. * Fix comments. * add node details component * Updated the node health component in the deployment details component. * Improve the way of loading the node on clicking on the row of the table to be loaded inside the node-details component instead of the nodes view. * Removed unused console log. * handle refreshing table after any transaction * Made the reset filters button disabled if there is no values in the form. * Enhance the node filters validations. * Used the gqlClient from the clients instead of intialize it * update gpu card && refresh table * handle pagination * change table to v-data-table-server * apply filter component && remove mine * update title to dedicated nodes * apply pr comments * apply filters in giga bytes * add progress circular * Fix comments, disable the form filters while requesting, fix typo. * add min-width to table && make link clickable * Disable gpu, gateways switches, and the status dropdown while requesting to load some nodes. * Load all node gpu cards in the details. * use existing methods & handle max limit in filter * remove unnecessary casting from price * replace free resources with total from proxy * apply pr comments * use custom method && arrange filters as tabke * add debounce * apply pr comments * fix build * fix reloading issue * fix bug * remove unused import && fix toast * remove sorting * disable sort * center error && set loading true when error occurs * fix centering gpu alert * fix reloading spinner after error in gpu * Added slot for GPU details. * Used custom toast instead. * Update the GPU card details component. * Rename the isFormLoading field in the filter form to be formDisabled instead. * update renaming in filters * disable and loading btn until refreshing table * use existing card components * remove unnecessary logic * remove rentable tab * apply pr comments * apply pe comments * apply comments * add pr comments * handle loading and reload in gpu card * load both gpu and node if node details failed * minimize and use reloadNodeDetails in mount direct * add missing brackets --------- Co-authored-by: islam Co-authored-by: Mahmoud Emad --- .../grid_client/src/modules/calculator.ts | 25 +- packages/grid_client/src/modules/contracts.ts | 16 + packages/grid_client/src/modules/models.ts | 6 +- .../gridproxy_client/src/modules/gateways.ts | 1 + packages/playground/src/App.vue | 3 +- .../playground/src/explorer/utils/types.ts | 1 + .../components/dedicated_nodes_table.vue | 273 ++++++++++++++++++ .../src/portal/components/node_details.vue | 227 +++++++++++++++ .../portal/components/reserve_action_btn.vue | 141 +++++++++ .../src/portal/dedicated_nodes_view.vue | 26 ++ packages/playground/src/router/index.ts | 5 + packages/playground/src/utils/filter_nodes.ts | 90 +++++- packages/playground/src/utils/helpers.ts | 11 + packages/tfchain_client/src/contracts.ts | 10 + 14 files changed, 829 insertions(+), 6 deletions(-) create mode 100644 packages/playground/src/portal/components/dedicated_nodes_table.vue create mode 100644 packages/playground/src/portal/components/node_details.vue create mode 100644 packages/playground/src/portal/components/reserve_action_btn.vue create mode 100644 packages/playground/src/portal/dedicated_nodes_view.vue diff --git a/packages/grid_client/src/modules/calculator.ts b/packages/grid_client/src/modules/calculator.ts index b6a975ad92..478700caa0 100644 --- a/packages/grid_client/src/modules/calculator.ts +++ b/packages/grid_client/src/modules/calculator.ts @@ -4,6 +4,19 @@ import { expose } from "../helpers/expose"; import { validateInput } from "../helpers/validator"; import { CalculatorModel, CUModel, SUModel } from "./models"; +interface PricingInfo { + dedicatedPrice: number; + dedicatedPackage: { + package: string; + discount: number; + }; + sharedPrice: number; + sharedPackage: { + package: string; + discount: number; + }; +} + class Calculator { client: TFClient; @@ -60,7 +73,7 @@ class Calculator { } @expose @validateInput - async calculate(options: CalculatorModel) { + async calculate(options: CalculatorModel): Promise { let balance = 0; const pricing = await this.pricing(options); @@ -110,9 +123,15 @@ class Calculator { sharedPrice = (sharedPrice - sharedPrice * (discountPackages[sharedPackage].discount / 100)) / 10000000; return { dedicatedPrice: dedicatedPrice, - dedicatedPackage: dedicatedPackage, + dedicatedPackage: { + package: dedicatedPackage, + discount: discountPackages[dedicatedPackage].discount, + }, sharedPrice: sharedPrice, - sharedPackage: sharedPackage, + sharedPackage: { + package: sharedPackage, + discount: discountPackages[sharedPackage].discount, + }, }; } diff --git a/packages/grid_client/src/modules/contracts.ts b/packages/grid_client/src/modules/contracts.ts index 3d1118d68f..187afab58e 100644 --- a/packages/grid_client/src/modules/contracts.ts +++ b/packages/grid_client/src/modules/contracts.ts @@ -18,12 +18,14 @@ import { ContractsByTwinId, ContractState, CreateServiceContractModel, + GetActiveContractsModel, GetDedicatedNodePriceModel, GetServiceContractModel, NameContractCreateModel, NameContractGetModel, NodeContractCreateModel, NodeContractUpdateModel, + RentContractCreateModel, RentContractGetModel, ServiceContractApproveModel, ServiceContractBillModel, @@ -78,6 +80,14 @@ class Contracts { async create_name(options: NameContractCreateModel) { return (await this.client.contracts.createName(options)).apply(); } + + @expose + @validateInput + @checkBalance + async createRent(options: RentContractCreateModel) { + return (await this.client.contracts.createRent(options)).apply(); + } + @expose @validateInput async get(options: ContractGetModel) { @@ -101,6 +111,12 @@ class Contracts { return await this.client.contracts.getDedicatedNodeExtraFee(options); } + @expose + @validateInput + async getActiveContracts(options: GetActiveContractsModel) { + return await this.client.contracts.getActiveContracts(options); + } + @expose @validateInput async activeRentContractForNode(options: RentContractGetModel) { diff --git a/packages/grid_client/src/modules/models.ts b/packages/grid_client/src/modules/models.ts index 8d93c898c1..86e8a3b54b 100644 --- a/packages/grid_client/src/modules/models.ts +++ b/packages/grid_client/src/modules/models.ts @@ -284,7 +284,6 @@ class RentContractDeleteModel { class ContractGetModel { @Expose() @IsInt() @Min(1) id: number; } - class ContractGetByNodeIdAndHashModel { @Expose() @IsInt() @Min(1) node_id: number; @Expose() @IsString() @IsNotEmpty() hash: string; @@ -681,6 +680,10 @@ class ListenToMintCompletedModel { @Expose() @IsNotEmpty() @IsString() address: string; } +class GetActiveContractsModel { + @Expose() @IsInt() @IsNotEmpty() @Min(1) nodeId: number; +} + export { AlgorandAccountCreateModel, AlgorandAccountInitModel, @@ -805,4 +808,5 @@ export { GetDedicatedNodePriceModel, SwapToStellarModel, ListenToMintCompletedModel, + GetActiveContractsModel, }; diff --git a/packages/gridproxy_client/src/modules/gateways.ts b/packages/gridproxy_client/src/modules/gateways.ts index 43045c8505..cf50cd5c3d 100644 --- a/packages/gridproxy_client/src/modules/gateways.ts +++ b/packages/gridproxy_client/src/modules/gateways.ts @@ -73,6 +73,7 @@ export interface GridNode { twin: Twin; stats: NodeStats; cards: GPUCard[]; + num_gpu: number; } export class GatewaysClient extends AbstractClient { diff --git a/packages/playground/src/App.vue b/packages/playground/src/App.vue index d4416741af..af29623b52 100644 --- a/packages/playground/src/App.vue +++ b/packages/playground/src/App.vue @@ -185,8 +185,8 @@ const routes: AppRoute[] = [ ], }, { - title: "Explorer", icon: "mdi-database-search-outline", + title: "Explorer", items: [ { title: "Statistics", @@ -392,6 +392,7 @@ export default { bottom: 15px; right: 25px; } + .v-tooltip > .v-overlay__content { opacity: 10; color: white; diff --git a/packages/playground/src/explorer/utils/types.ts b/packages/playground/src/explorer/utils/types.ts index 55c1263690..a8ae970fbe 100644 --- a/packages/playground/src/explorer/utils/types.ts +++ b/packages/playground/src/explorer/utils/types.ts @@ -119,4 +119,5 @@ export const nodeInitializer: GridNode = { twin: { twinId: 0, accountId: "", publicKey: "", relay: "" }, stats: nodeStatsInitializer, cards: [], + num_gpu: 0, }; diff --git a/packages/playground/src/portal/components/dedicated_nodes_table.vue b/packages/playground/src/portal/components/dedicated_nodes_table.vue new file mode 100644 index 0000000000..89c21dae90 --- /dev/null +++ b/packages/playground/src/portal/components/dedicated_nodes_table.vue @@ -0,0 +1,273 @@ + + + + + + + diff --git a/packages/playground/src/portal/components/node_details.vue b/packages/playground/src/portal/components/node_details.vue new file mode 100644 index 0000000000..249c0d364c --- /dev/null +++ b/packages/playground/src/portal/components/node_details.vue @@ -0,0 +1,227 @@ + + + + + diff --git a/packages/playground/src/portal/components/reserve_action_btn.vue b/packages/playground/src/portal/components/reserve_action_btn.vue new file mode 100644 index 0000000000..a6a25bc7f9 --- /dev/null +++ b/packages/playground/src/portal/components/reserve_action_btn.vue @@ -0,0 +1,141 @@ + + + diff --git a/packages/playground/src/portal/dedicated_nodes_view.vue b/packages/playground/src/portal/dedicated_nodes_view.vue new file mode 100644 index 0000000000..21ad274886 --- /dev/null +++ b/packages/playground/src/portal/dedicated_nodes_view.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/packages/playground/src/router/index.ts b/packages/playground/src/router/index.ts index 116158eda7..4470c77642 100644 --- a/packages/playground/src/router/index.ts +++ b/packages/playground/src/router/index.ts @@ -42,6 +42,11 @@ const router = createRouter({ component: () => import("../portal/transfer_view.vue"), meta: { title: "Transfer" }, }, + { + path: "dedicated-nodes", + component: () => import("../portal/dedicated_nodes_view.vue"), + meta: { title: "Dedicated Nodes" }, + }, ], }, diff --git a/packages/playground/src/utils/filter_nodes.ts b/packages/playground/src/utils/filter_nodes.ts index c159bef073..41933b321d 100644 --- a/packages/playground/src/utils/filter_nodes.ts +++ b/packages/playground/src/utils/filter_nodes.ts @@ -3,7 +3,7 @@ import type { FilterOptions, GridClient, NodeInfo } from "@threefold/grid_client import type { AsyncRule, SyncRule } from "@/components/input_validator.vue"; import type { NodeFilters } from "@/components/select_node.vue"; -import { isNumeric, min, startsWith, validateResourceMaxNumber } from "./validators"; +import { isAlphanumeric, isDecimal, isNumeric, min, startsWith, validateResourceMaxNumber } from "./validators"; export interface NodeGPUCardType { id: string; @@ -60,6 +60,16 @@ export type FilterInputs = { freeMru: NodeInputFilterType; }; +// Input fields for dedicated nodes +export type DedicatedNodeFilters = { + total_sru: NodeInputFilterType; + total_hru: NodeInputFilterType; + total_mru: NodeInputFilterType; + total_cru: NodeInputFilterType; + gpu_vendor_name: NodeInputFilterType; + gpu_device_name: NodeInputFilterType; +}; + // Default input Initialization export const inputsInitializer: FilterInputs = { nodeId: { @@ -147,3 +157,81 @@ export const inputsInitializer: FilterInputs = { type: "text", }, }; + +export const DedicatedNodeInitializer: DedicatedNodeFilters = { + total_cru: { + label: "Total CPU (Cores)", + placeholder: "Filter by total Cores.", + type: "text", + rules: [ + [isNumeric("This Field accepts only a valid number."), min("This Field must be a number larger than 0.", 1)], + ], + }, + total_mru: { + label: "Total RAM (GB)", + placeholder: "Filter by total Memory.", + type: "text", + rules: [ + [ + isNumeric("This Field accepts only a valid number."), + min("This Field must be a number larger than 0.", 1), + validateResourceMaxNumber("This value is out of range."), + ], + ], + }, + total_sru: { + label: "Total SSD (GB)", + placeholder: "Filter by total SSD.", + type: "text", + rules: [ + [ + isNumeric("This Field accepts only a valid number."), + min("This Field must be a number larger than 0.", 1), + validateResourceMaxNumber("This value is out of range."), + ], + ], + }, + total_hru: { + label: "Total HDD (GB)", + placeholder: "Filter by total HDD.", + type: "text", + rules: [ + [ + isNumeric("This Field accepts only a valid number."), + min("This Field must be a number larger than 0.", 1), + validateResourceMaxNumber("This value is out of range."), + ], + ], + }, + + gpu_device_name: { + label: "GPU's device name", + placeholder: "Filter by GPU's device name.", + rules: [ + [ + (value: string) => { + const allowedPattern = /^[A-Za-z0-9[\]/,.]+$/; + if (!allowedPattern.test(value)) { + isAlphanumeric("This Field accepts only letters and numbers."); + } + }, + ], + ], + type: "text", + }, + gpu_vendor_name: { + label: "GPU's vendor name", + placeholder: "Filter by GPU's vendor name.", + type: "text", + rules: [ + [ + (value: string) => { + const allowedPattern = /^[A-Za-z0-9[\]/,.]+$/; + if (!allowedPattern.test(value)) { + isAlphanumeric("This Field accepts only letters and numbers."); + } + }, + ], + ], + }, +}; diff --git a/packages/playground/src/utils/helpers.ts b/packages/playground/src/utils/helpers.ts index 067182dda3..b37f1ceecc 100644 --- a/packages/playground/src/utils/helpers.ts +++ b/packages/playground/src/utils/helpers.ts @@ -57,3 +57,14 @@ export function getDashboardURL(network: string) { export function getCardName(card: NodeGPUCardType): string { return card.vendor + " - " + card.device; } + +export function toGigaBytes(value?: number) { + const giga = 1024 ** 3; + + if (value === undefined || value === null || isNaN(value) || value === 0) { + return 0; + } + + const gb = value / giga; + return parseFloat(gb.toFixed(2)); +} diff --git a/packages/tfchain_client/src/contracts.ts b/packages/tfchain_client/src/contracts.ts index 5f3188bac5..159fe72d21 100644 --- a/packages/tfchain_client/src/contracts.ts +++ b/packages/tfchain_client/src/contracts.ts @@ -70,6 +70,10 @@ interface QueryContractsGetOptions { id: number; } +interface ActiveContractsOptions { + nodeId: number; +} + interface QueryContractsGetContractByActiveRentOptions { nodeId: number; } @@ -104,6 +108,12 @@ class QueryContracts { return res.toPrimitive() as number; } + @checkConnection + async getActiveContracts(options: ActiveContractsOptions): Promise { + const res = await this.client.api.query.smartContractModule.activeNodeContracts(options.nodeId); + return res.toPrimitive() as number[]; + } + @checkConnection async getContractIdByName(options: QueryContractGetContractByNameOptions): Promise { const res = await this.client.api.query.smartContractModule.contractIDByNameRegistration(options.name);