diff --git a/packages/playground/docs/build.md b/packages/playground/docs/build.md index b252bdee85..88180c4ab2 100644 --- a/packages/playground/docs/build.md +++ b/packages/playground/docs/build.md @@ -30,6 +30,8 @@ bash ../scripts/build-env.sh - SENTRY_DSN - ENABLE_TELEMETRY +- The use can provide a single URL or multiple URLs separated with a comma, for `GRAPHQL_URL, GRIDPROXY_URL, SUBSTRATE_URL, ACTIVATION_SERVICE_URL, RELAY_DOMAIN, STATS_URL`, If the user provides multiple URLs, it will be considered as a priority list, which means the dashboard will try to connect over the first URL in the list; if it fails, it will move to the next one, and so on. + - The backend payments are done with stellar so you need to decide which network of stellar you want to connect to ```bash diff --git a/packages/playground/index.html b/packages/playground/index.html index 72f6262517..67f78846b6 100644 --- a/packages/playground/index.html +++ b/packages/playground/index.html @@ -41,7 +41,12 @@ }); logo - Loading dashboard. Please wait... + Loading dashboard. Please wait... + +
+ +
diff --git a/packages/playground/package.json b/packages/playground/package.json index 7a95653d5d..87bb7a5f78 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -16,6 +16,7 @@ "@threefold/graphql_client": "2.5.0", "@threefold/grid_client": "2.5.0", "@threefold/gridproxy_client": "2.5.0", + "@threefold/monitoring": "2.5.0", "@threefold/types": "2.5.0", "@types/ip": "^1.1.3", "@types/md5": "^2.3.5", diff --git a/packages/playground/public/config.js b/packages/playground/public/config.js index 3c87acb26b..96b19eb9fd 100644 --- a/packages/playground/public/config.js +++ b/packages/playground/public/config.js @@ -1,16 +1,19 @@ window.env = { NETWORK: "dev", - GRAPHQL_URL: "https://graphql.dev.grid.tf/graphql", - GRIDPROXY_URL: "https://gridproxy.dev.grid.tf", - SUBSTRATE_URL: "wss://tfchain.dev.grid.tf/ws", - ACTIVATION_SERVICE_URL: "https://activation.dev.grid.tf/activation/activate", - RELAY_DOMAIN: "wss://relay.dev.grid.tf", + GRAPHQL_STACKS: ["https://graphql.dev.grid.tf/graphql", "https://graphql.02.dev.grid.tf/graphql"], + GRIDPROXY_STACKS: ["https://gridproxy.dev.grid.tf", "https://gridproxy.02.dev.grid.tf"], + SUBSTRATE_STACKS: ["wss://tfchain.dev.grid.tf/ws", "wss://tfchain.02.dev.grid.tf/ws"], + ACTIVATION_SERVICE_STACKS: [ + "https://activation.dev.grid.tf/activation/activate", + "https://activation.02.dev.grid.tf/activation/activate", + ], + RELAY_STACKS: ["wss://relay.dev.grid.tf", "wss://relay.02.dev.grid.tf"], BRIDGE_TFT_ADDRESS: "GDHJP6TF3UXYXTNEZ2P36J5FH7W4BJJQ4AYYAXC66I2Q2AH5B6O6BCFG", STELLAR_NETWORK: "test", STELLAR_HORIZON_URL: "https://horizon-testnet.stellar.org", TFT_ASSET_ISSUER: "GA47YZA3PKFUZMPLQ3B5F2E3CJIB57TGGU7SPCQT2WAEYKN766PWIMB3", MINTING_URL: "https://alpha.minting.tfchain.grid.tf", - STATS_URL: "https://stats.dev.grid.tf", + STATS_STACKS: ["https://stats.dev.grid.tf", "https://stats.02.dev.grid.tf"], TIMEOUT: +"10000", PAGE_SIZE: +"20", MANUAL_URL: "https://www.manual.grid.tf", diff --git a/packages/playground/public/loader/loader.css b/packages/playground/public/loader/loader.css index 28d5167c91..49f52c77d3 100644 --- a/packages/playground/public/loader/loader.css +++ b/packages/playground/public/loader/loader.css @@ -23,6 +23,54 @@ margin-top: 30vh; } +.app-monitor-container { + flex-direction: column; + align-items: center; + margin-top: 20px; + display: none; + border: 1px solid #fff; + border-radius: 5px; + padding: 5px 25px; +} + +.app-monitor-container.active { + display: flex; +} + +.app-monitor-status { + text-align: left; + color: #fff; + margin: 0; + font-family: sans-serif; + max-width: 500px; + line-height: 1; +} + +.service-name { + color: #fff; + width: 100px; + padding: 2px 0; + font-family: sans-serif; + line-height: 1; + font-weight: bold; + text-align: left; +} + +.service-status { + display: inline; + font-family: sans-serif; + line-height: 1; + font-weight: bold; + font-size: x-large; +} + +.service-reachable { + color: rgb(var(--v-theme-success, "76, 175, 80")); +} + +.service-unreachable { + color: rgb(207 102 121); +} .app-loader-logo { max-width: 100%; display: block; @@ -86,7 +134,9 @@ } } -.app-loader-msg { +.app-loader-msg, +.app-monitor-msg { + display: none; color: #fff; margin: 0; margin-top: 20px; @@ -96,6 +146,14 @@ text-align: center; } +.app-monitor-msg { + display: none; +} + +.app-loader-msg.active, +.app-monitor-msg.active { + display: block; +} .app-loader-refresh { border: none; outline: none; @@ -134,4 +192,4 @@ .app-loader-refresh:focus { box-shadow: 0 0 0 2px rgba(115, 221, 195, 0.5); -} +} \ No newline at end of file diff --git a/packages/playground/public/loader/loader.js b/packages/playground/public/loader/loader.js index 69dd8f766b..71e521e429 100644 --- a/packages/playground/public/loader/loader.js +++ b/packages/playground/public/loader/loader.js @@ -10,6 +10,12 @@ const msgElement = document.querySelector(".app-loader-msg"); /** @type { HTMLButtonElement} */ const refreshBtn = document.querySelector(".app-loader-refresh"); +/** @type { HTMLDivElement } */ +const monitor = document.querySelector(".app-monitor-container"); + +/** @type { HTMLUListElement} */ +const monitorList = document.querySelector(".app-monitor-status"); + const slowConnectionTime = 60 * 1000; const noConnectionTime = 120 * 1000; const appLoaderContainerTime = 0.3 * 1000; @@ -58,3 +64,25 @@ window.$$appLoader = () => { } }, welcomeMsgTime); }; +window.$$showMonitorError = urls => { + if (msgElement) msgElement.classList.remove("active"); + + if (monitor) monitor.classList.add("active"); + + const monitorMsgElement = document.querySelector(".app-monitor-msg"); + if (monitorMsgElement) { + monitorMsgElement.classList.add("active"); + monitorMsgElement.textContent = "Can't reach some services on provided stacks, Please try again"; + } + + if (monitorList) { + monitorList.innerHTML = Object.entries(urls).map(createElement).join(" "); + } + refreshBtn && refreshBtn.classList.add("active"); +}; + +function createElement([serviceName, serviceStatus]) { + return `
  • ${serviceName}

    ${serviceStatus !== null ? "✓" : "✗"}

  • `; +} diff --git a/packages/playground/scripts/build-env.sh b/packages/playground/scripts/build-env.sh index ecd2581631..83f4cf3ff8 100644 --- a/packages/playground/scripts/build-env.sh +++ b/packages/playground/scripts/build-env.sh @@ -15,46 +15,46 @@ STELLAR_ENV_Vars=( case $MODE in "dev") - GRAPHQL_URL="${GRAPHQL_URL:-https://graphql.dev.grid.tf/graphql}" - GRIDPROXY_URL="${GRIDPROXY_URL:-https://gridproxy.dev.grid.tf}" - SUBSTRATE_URL="${SUBSTRATE_URL:-wss://tfchain.dev.grid.tf/ws}" - ACTIVATION_SERVICE_URL="${ACTIVATION_SERVICE_URL:-https://activation.dev.grid.tf/activation/activate}" - RELAY_DOMAIN="${RELAY_DOMAIN:-wss://relay.dev.grid.tf}" + GRAPHQL_URL="${GRAPHQL_URL:-"https://graphql.dev.grid.tf/graphql,https://graphql.02.dev.grid.tf/graphql"}" + GRIDPROXY_URL="${GRIDPROXY_URL:-"https://gridproxy.dev.grid.tf,https://gridproxy.02.dev.grid.tf"}" + SUBSTRATE_URL="${SUBSTRATE_URL:-"wss://tfchain.dev.grid.tf/ws,wss://tfchain.02.dev.grid.tf/ws"}" + ACTIVATION_SERVICE_URL="${ACTIVATION_SERVICE_URL:-"https://activation.dev.grid.tf/activation/activate,https://activation.02.dev.grid.tf/activation/activate"}" + RELAY_DOMAIN="${RELAY_DOMAIN:-"wss://relay.dev.grid.tf,wss://relay.02.dev.grid.tf"}" BRIDGE_TFT_ADDRESS="${BRIDGE_TFT_ADDRESS:-GDHJP6TF3UXYXTNEZ2P36J5FH7W4BJJQ4AYYAXC66I2Q2AH5B6O6BCFG}" - STATS_URL="${STATS_URL:-https://stats.dev.grid.tf}" + STATS_URL="${STATS_URL:-"https://stats.dev.grid.tf,https://stats.02.dev.grid.tf"}" STELLAR_NETWORK="${STELLAR_NETWORK:-test}" SENTRY_DSN="https://b9af6796f176d1f02837a06f0da3caee@dev.sentry.grid.tf/2" ;; "qa") - GRAPHQL_URL="${GRAPHQL_URL:-https://graphql.qa.grid.tf/graphql}" - GRIDPROXY_URL="${GRIDPROXY_URL:-https://gridproxy.qa.grid.tf}" - SUBSTRATE_URL="${SUBSTRATE_URL:-wss://tfchain.qa.grid.tf/ws}" - ACTIVATION_SERVICE_URL="${ACTIVATION_SERVICE_URL:-https://activation.qa.grid.tf/activation/activate}" - RELAY_DOMAIN="${RELAY_DOMAIN:-wss://relay.qa.grid.tf}" + GRAPHQL_URL="${GRAPHQL_URL:-"https://graphql.qa.grid.tf/graphql,https://graphql.02.qa.grid.tf/graphql"}" + GRIDPROXY_URL="${GRIDPROXY_URL:-"https://gridproxy.qa.grid.tf,https://gridproxy.02.qa.grid.tf"}" + SUBSTRATE_URL="${SUBSTRATE_URL:-"wss://tfchain.qa.grid.tf/ws,wss://tfchain.02.qa.grid.tf/ws"}" + ACTIVATION_SERVICE_URL="${ACTIVATION_SERVICE_URL:-"https://activation.qa.grid.tf/activation/activate,https://activation.02.qa.grid.tf/activation/activate"}" + RELAY_DOMAIN="${RELAY_DOMAIN:-"wss://relay.qa.grid.tf,wss://relay.02.qa.grid.tf"}" BRIDGE_TFT_ADDRESS="${BRIDGE_TFT_ADDRESS:-GAQH7XXFBRWXT2SBK6AHPOLXDCLXVFAKFSOJIRMRNCDINWKHGI6UYVKM}" - STATS_URL="${STATS_URL:-https://stats.qa.grid.tf}" + STATS_URL="${STATS_URL:-"https://stats.qa.grid.tf,https://stats.02.qa.grid.tf"}" STELLAR_NETWORK="${STELLAR_NETWORK:-test}" SENTRY_DSN="https://b9af6796f176d1f02837a06f0da3caee@dev.sentry.grid.tf/2" ;; "test") - GRAPHQL_URL="${GRAPHQL_URL:-https://graphql.test.grid.tf/graphql}" - GRIDPROXY_URL="${GRIDPROXY_URL:-https://gridproxy.test.grid.tf}" - SUBSTRATE_URL="${SUBSTRATE_URL:-wss://tfchain.test.grid.tf/ws}" - ACTIVATION_SERVICE_URL="${ACTIVATION_SERVICE_URL:-https://activation.test.grid.tf/activation/activate}" - RELAY_DOMAIN="${RELAY_DOMAIN:-wss://relay.test.grid.tf}" + GRAPHQL_URL="${GRAPHQL_URL:-"https://graphql.test.grid.tf/graphql,https://graphql.02.test.grid.tf/graphql"}" + GRIDPROXY_URL="${GRIDPROXY_URL:-"https://gridproxy.test.grid.tf,https://gridproxy.02.test.grid.tf"}" + SUBSTRATE_URL="${SUBSTRATE_URL:-"wss://tfchain.test.grid.tf/ws,wss://tfchain.02.test.grid.tf/ws"}" + ACTIVATION_SERVICE_URL="${ACTIVATION_SERVICE_URL:-"https://activation.test.grid.tf/activation/activate,https://activation.02.test.grid.tf/activation/activate"}" + RELAY_DOMAIN="${RELAY_DOMAIN:-"wss://relay.test.grid.tf,wss://relay.02.test.grid.tf"}" BRIDGE_TFT_ADDRESS="${BRIDGE_TFT_ADDRESS:-GA2CWNBUHX7NZ3B5GR4I23FMU7VY5RPA77IUJTIXTTTGKYSKDSV6LUA4}" - STATS_URL="${STATS_URL:-https://stats.test.grid.tf}" + STATS_URL="${STATS_URL:-"https://stats.test.grid.tf,https://stats.02.test.grid.tf"}" STELLAR_NETWORK="${STELLAR_NETWORK:-main}" SENTRY_DSN="https://b9af6796f176d1f02837a06f0da3caee@dev.sentry.grid.tf/2" ;; "main") - GRAPHQL_URL="${GRAPHQL_URL:-https://graphql.grid.tf/graphql}" - GRIDPROXY_URL="${GRIDPROXY_URL:-https://gridproxy.grid.tf}" - SUBSTRATE_URL="${SUBSTRATE_URL:-wss://tfchain.grid.tf/ws}" - ACTIVATION_SERVICE_URL="${ACTIVATION_SERVICE_URL:-https://activation.grid.tf/activation/activate}" - RELAY_DOMAIN="${RELAY_DOMAIN:-wss://relay.grid.tf}" + GRAPHQL_URL="${GRAPHQL_URL:-"https://graphql.grid.tf/graphql,https://graphql.02.grid.tf/graphql"}" + GRIDPROXY_URL="${GRIDPROXY_URL:-"https://gridproxy.grid.tf,https://gridproxy.02.grid.tf"}" + SUBSTRATE_URL="${SUBSTRATE_URL:-"wss://tfchain.grid.tf/ws,wss://tfchain.02.grid.tf/ws"}" + ACTIVATION_SERVICE_URL="${ACTIVATION_SERVICE_URL:-"https://activation.grid.tf/activation/activate,https://activation.02.grid.tf/activation/activate"}" + RELAY_DOMAIN="${RELAY_DOMAIN:-"wss://relay.grid.tf,wss://relay.02.grid.tf"}" BRIDGE_TFT_ADDRESS="${BRIDGE_TFT_ADDRESS:-GBNOTAYUMXVO5QDYWYO2SOCOYIJ3XFIP65GKOQN7H65ZZSO6BK4SLWSC}" - STATS_URL="${STATS_URL:-https://stats.grid.tf}" + STATS_URL="${STATS_URL:-"https://stats.grid.tf,https://stats.02.grid.tf"}" STELLAR_NETWORK="${STELLAR_NETWORK:-main}" SENTRY_DSN="https://598bcc658bd99042ab429166035f8278@sentry.grid.tf/2" ;; @@ -81,20 +81,35 @@ case $STELLAR_NETWORK in ;; esac +parss_array(){ + local service_urls=$1 + toString=($(echo "$service_urls" | tr ',' "\n")) + for item in "${toString[@]}"; do + quoted_string+="'$item' " + done + + # add single quate to each elament + quoted_string=${quoted_string// /,} + + # remove trailing comma + echo "${quoted_string%?}" + +} + configs=" window.env = { NETWORK: '$MODE', - GRAPHQL_URL: '$GRAPHQL_URL', - GRIDPROXY_URL: '$GRIDPROXY_URL', - SUBSTRATE_URL: '$SUBSTRATE_URL', - ACTIVATION_SERVICE_URL: '$ACTIVATION_SERVICE_URL', - RELAY_DOMAIN: '$RELAY_DOMAIN', + GRAPHQL_STACKS: "[$(parss_array "$GRAPHQL_URL")]", + GRIDPROXY_STACKS: "[$(parss_array "$GRIDPROXY_URL")]", + SUBSTRATE_STACKS: "[$(parss_array "$SUBSTRATE_URL")]", + ACTIVATION_SERVICE_STACKS: "[$(parss_array "$ACTIVATION_SERVICE_URL")]", + RELAY_STACKS: "[$(parss_array "$RELAY_DOMAIN")]", BRIDGE_TFT_ADDRESS: '$BRIDGE_TFT_ADDRESS', STELLAR_NETWORK: '$STELLAR_NETWORK', STELLAR_HORIZON_URL: '$STELLAR_HORIZON_URL', TFT_ASSET_ISSUER: '$TFT_ASSET_ISSUER', MINTING_URL: '$MINTING_URL', - STATS_URL: '$STATS_URL', + STATS_STACKS: "[$(parss_array "$STATS_URL")]", TIMEOUT: +'$TIMEOUT', PAGE_SIZE: +'$PAGE_SIZE', MANUAL_URL: '$MANUAL_URL', @@ -104,7 +119,7 @@ window.env = { " # decide the config file path -[ -d dist ] && file="dist/config.js" || file="config.js" +[ -d public ] && file="public/config.js" || file="config.js" # override the content of the config file & echo the result echo $configs > $file diff --git a/packages/playground/src/Monitor.vue b/packages/playground/src/Monitor.vue new file mode 100644 index 0000000000..76bbc61e45 --- /dev/null +++ b/packages/playground/src/Monitor.vue @@ -0,0 +1,26 @@ + + + diff --git a/packages/playground/src/clients/index.ts b/packages/playground/src/clients/index.ts index a9b33d6a04..71bfb8697f 100644 --- a/packages/playground/src/clients/index.ts +++ b/packages/playground/src/clients/index.ts @@ -1,7 +1,6 @@ import TFGridGqlClient from "@threefold/graphql_client"; import GridProxyClient from "@threefold/gridproxy_client"; import { QueryClient } from "@threefold/tfchain_client"; - const gqlClient = new TFGridGqlClient(window.env.GRAPHQL_URL); const gridProxyClient = new GridProxyClient(window.env.GRIDPROXY_URL); const queryClient = new QueryClient(window.env.SUBSTRATE_URL); diff --git a/packages/playground/src/components/logger.vue b/packages/playground/src/components/logger.vue index 848bd70e39..f2e0928cee 100644 --- a/packages/playground/src/components/logger.vue +++ b/packages/playground/src/components/logger.vue @@ -203,7 +203,7 @@ export default { let _interceptorQueue: LI[] = []; async function interceptMessage(instance: LI) { - if (connectDB.value.error) { + if (connectDB.value.error ) { _interceptorQueue.push(instance); return; } diff --git a/packages/playground/src/config.ts b/packages/playground/src/config.ts index 0a9c9201e5..6194406320 100644 --- a/packages/playground/src/config.ts +++ b/packages/playground/src/config.ts @@ -1,30 +1,37 @@ +import { + ActivationMonitor, + GraphQLMonitor, + GridProxyMonitor, + RMBMonitor, + ServiceUrlManager, + StatsMonitor, + TFChainMonitor, +} from "@threefold/monitoring"; import { marked } from "marked"; -import type { App, Component } from "vue"; +import { type App, type Component, defineAsyncComponent } from "vue"; import CopyInputWrapper from "./components/copy_input_wrapper.vue"; -import DTabs from "./components/dynamic_tabs.vue"; import Filters from "./components/filter.vue"; import FormValidator from "./components/form_validator.vue"; import InputTooltip from "./components/input_tooltip.vue"; import InputValidator from "./components/input_validator.vue"; import TfSelectCountry from "./components/node_selector/select_location_internals/TfSelectCountry.vue"; import TfSelectRegion from "./components/node_selector/select_location_internals/TfSelectRegion.vue"; -import TfSelectionDetails from "./components/node_selector/TfSelectionDetails.vue"; import PasswordInputWrapper from "./components/password_input_wrapper.vue"; import ViewLayout from "./components/view_layout.vue"; -import WebletLayout from "./components/weblet_layout.vue"; import * as validators from "./utils/validators"; + const GLOBAL_COMPONENTS: { [key: string]: Component } = { PasswordInputWrapper, - WebletLayout, + WebletLayout: defineAsyncComponent(() => import("./components/weblet_layout.vue")), CopyInputWrapper, - DTabs, + DTabs: defineAsyncComponent(() => import("./components/dynamic_tabs.vue")), InputValidator, FormValidator, ViewLayout, InputTooltip, Filters, - TfSelectionDetails, + TfSelectionDetails: defineAsyncComponent(() => import("./components/node_selector/TfSelectionDetails.vue")), TfSelectRegion, TfSelectCountry, }; @@ -68,3 +75,43 @@ function defineGlobalProps(app: App) { app.config.globalProperties.validators = validators; app.config.globalProperties.MANUAL_URL = window.env.MANUAL_URL; } +/** + * Configures global environment variables based on available service URLs. + * + * This asynchronous function initializes a `ServiceUrlManager` with a set of predefined services and their URLs. + * It then retrieves the available service stacks and updates the global `window.env` object with the URLs for each service. + * If any service URLs are not available, an error is displayed and the function returns `false`. + * If all service URLs are successfully retrieved and set, the function returns `true`. + * + * @returns A promise that resolves to `true` if all service URLs are successfully set, or `false` if any service URL is missing. + */ +export async function setGlobalEnv() { + const { GRIDPROXY_STACKS, GRAPHQL_STACKS, STATS_STACKS, RELAY_STACKS, SUBSTRATE_STACKS, ACTIVATION_SERVICE_STACKS } = + window.env; + const urlManger = new ServiceUrlManager({ + services: [ + { URLs: GRIDPROXY_STACKS, service: new GridProxyMonitor() }, + { URLs: GRAPHQL_STACKS, service: new GraphQLMonitor() }, + { URLs: STATS_STACKS, service: new StatsMonitor() }, + { URLs: SUBSTRATE_STACKS, service: new TFChainMonitor() }, + { URLs: ACTIVATION_SERVICE_STACKS, service: new ActivationMonitor() }, + { URLs: RELAY_STACKS, service: new RMBMonitor() }, + ], + silent: true, + }); + const result = await urlManger.getAvailableServicesStack(); + + if (Object.values(result).includes(null)) { + window.$$showMonitorError(result); + return false; + } + const { GridProxy, Stats, TFChain, GraphQl, Activation, RMB } = result; + + window.env.GRIDPROXY_URL = GridProxy!; + window.env.STATS_URL = Stats!; + window.env.GRAPHQL_URL = GraphQl!; + window.env.SUBSTRATE_URL = TFChain!; + window.env.ACTIVATION_SERVICE_URL = Activation!; + window.env.RELAY_DOMAIN = RMB!; + return true; +} diff --git a/packages/playground/src/global-components.d.ts b/packages/playground/src/global-components.d.ts index 4a96d6c38e..50de9e277a 100644 --- a/packages/playground/src/global-components.d.ts +++ b/packages/playground/src/global-components.d.ts @@ -38,7 +38,14 @@ declare module "@vue/runtime-core" { declare global { interface Window { $$appLoader: () => void; + $$showMonitorError: (urls: { [key: string]: string | null }) => void; env: { + GRAPHQL_STACKS: string[]; + GRIDPROXY_STACKS: string[]; + SUBSTRATE_STACKS: string[]; + ACTIVATION_SERVICE_STACKS: string[]; + RELAY_STACKS: string[]; + STATS_STACKS: string[]; NETWORK: NetworkEnv; GRAPHQL_URL: string; GRIDPROXY_URL: string; diff --git a/packages/playground/src/main.ts b/packages/playground/src/main.ts index d8955a2949..b6462b2866 100644 --- a/packages/playground/src/main.ts +++ b/packages/playground/src/main.ts @@ -7,12 +7,12 @@ import { createApp } from "vue"; import vuetify from "@/plugins/vuetify"; -import App from "./App.vue"; import { defineGlobals } from "./config"; +import Monitor from "./Monitor.vue"; import router from "./router"; import { normalizeError } from "./utils/helpers"; -const app = createApp(App); +const app = createApp(Monitor); app.config.errorHandler = error => { console.error(