Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 100 additions & 28 deletions packages/playground/src/components/k8s_deployment_table.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
<span>
This might happen because the node is down or it's not reachable
<span v-if="showEncryption">
or the deployment{{ count - items.length > 1 ? "s are" : " is" }} encrypted by another key
</span>.
or the deployment{{ count - items.length > 1 ? "s are" : " is" }} encrypted by another key </span>.
</span>
<v-tooltip location="top" text="Show failed deployments">
<template #activator="{ props: tooltipProps }">
Expand Down Expand Up @@ -166,9 +165,9 @@ import { onMounted, ref } from "vue";
import { getNodeHealthColor, NodeHealth } from "@/utils/get_nodes";

import { useProfileManager } from "../stores";
import { getGrid, updateGrid } from "../utils/grid";
import { getGrid } from "../utils/grid";
import { markAsFromAnotherClient } from "../utils/helpers";
import { type K8S, type LoadedDeployments, loadK8s, mergeLoadedDeployments } from "../utils/load_deployment";
import { loadK8s, mergeLoadedDeployments, getGridClient } from "../utils/load_deployment";
const profileManager = useProfileManager();
const showDialog = ref(false);
const showEncryption = ref(false);
Expand All @@ -188,35 +187,108 @@ const loading = ref(false);

onMounted(loadDeployments);
async function loadDeployments() {
const start = performance.now();
items.value = [];
loading.value = true;
const grid = await getGrid(profileManager.profile!, props.projectName);
const chunk1 = await loadK8s(grid!);
const chunk2 = await loadK8s(updateGrid(grid!, { projectName: props.projectName.toLowerCase() }));
let chunk3: LoadedDeployments<K8S> = { count: 0, items: [], failedDeployments: [] };

if (showAllDeployments.value) {
chunk3 = await loadK8s(updateGrid(grid!, { projectName: "" }));
chunk3.items = chunk3.items.map(i => {
return !i.projectName || i.projectName === "Kubernetes" ? markAsFromAnotherClient(i) : i;
try {
const grid = await getGrid(profileManager.profile!, props.projectName);
if (!grid) {
loading.value = false;
console.error("Failed to initialize grid connection");
return;
}
const shouldLoadAllDeployments = showAllDeployments.value;
const results = await Promise.allSettled([
loadK8s(grid),
loadK8s(await getGridClient(grid.clientOptions, props.projectName.toLowerCase())),
shouldLoadAllDeployments
? loadK8s(await getGridClient(grid.clientOptions, ""))
: Promise.resolve({ count: 0, items: [], failedDeployments: [] }),
]);
const chunk1 =
results[0].status === "fulfilled"
? results[0].value
: (() => {
console.error("Failed to load K8s deployments from default project:", results[0].reason);
return { count: 0, items: [], failedDeployments: [] };
})();
const chunk2 =
results[1].status === "fulfilled"
? results[1].value
: (() => {
console.error(`Failed to load K8s deployments from project "${props.projectName}":`, results[1].reason);
return { count: 0, items: [], failedDeployments: [] };
})();
const chunk3 =
results[2].status === "fulfilled"
? results[2].value
: (() => {
console.error("Failed to load K8s deployments from all projects:", results[2].reason);
return { count: 0, items: [], failedDeployments: [] };
})();
if (chunk3.items) {
chunk3.items = chunk3.items.map(i => {
return !i.projectName || i.projectName === "Kubernetes" ? markAsFromAnotherClient(i) : i;
});
}
const clusters = mergeLoadedDeployments(chunk1, chunk2, chunk3);
failedDeployments.value = clusters.failedDeployments;
count.value = clusters.count;
items.value = clusters.items.map(item => {
const master = item.masters[0];
const publicIP = master.publicIP?.ip;
return {
...item,
name: item.deploymentName,
ipv4: publicIP ? publicIP.split("/")?.[0] || publicIP : "-",
ipv6: master.publicIP?.ip6?.replace(/\/64$/, "") || "-",
planetary: master.planetary || "-",
workersLength: item.workers.length,
billing: undefined,
wireguard: undefined,
detailsLoading: false,
};
});

await Promise.allSettled(items.value.map(item => fetchClusterDetails(item)));
} catch (error) {
console.error("Error loading deployments:", error);
items.value = [];
count.value = 0;
failedDeployments.value = [];
} finally {
loading.value = false;
const end = performance.now();
console.log(`Time taken: ${(end - start) / 1000} seconds`);
}
}

async function fetchClusterDetails(item: any) {
if (item.detailsLoading || (item.billing !== undefined && item.wireguard !== undefined)) return;
item.detailsLoading = true;
try {
const grid = await getGrid(profileManager.profile!, item.projectName || props.projectName);
if (!grid) {
item.detailsLoading = false;
return;
}

const clusters = mergeLoadedDeployments(chunk1, chunk2, chunk3);
failedDeployments.value = clusters.failedDeployments;

count.value = clusters.count;
items.value = clusters.items.map((item: any) => {
item.name = item.deploymentName;
item.ipv4 = item.masters[0].publicIP?.ip?.split("/")?.[0] || item.masters[0].publicIP?.ip || "-";
item.ipv6 = item.masters[0].publicIP?.ip6.replace(/\/64$/, "") || "-";
item.planetary = item.masters[0].planetary || "-";
item.workersLength = item.workers.length;
item.billing = item.masters[0].billing;
item.created = item.masters[0].created;
return item;
});
loading.value = false;
const [consumption, wireguardConfig] = await Promise.allSettled([
grid.contracts.getConsumption({ id: item.masters[0].contractId }),
grid.networks.getWireGuardConfigs({
name: item.masters[0].interfaces[0].network,
ipRange: item.masters[0].interfaces[0].ip,
}),
]);

item.billing =
consumption.status === "fulfilled" && consumption.value ? consumption.value.amountBilled : "No Data Available";

item.wireguard =
wireguardConfig.status === "fulfilled" && wireguardConfig.value?.[0] ? wireguardConfig.value[0] : undefined;
} finally {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a catch block; to at least log the errors

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

item.detailsLoading = false;
}
}

defineExpose({ loadDeployments });
Expand Down
83 changes: 58 additions & 25 deletions packages/playground/src/components/vm_deployment_table.vue
Original file line number Diff line number Diff line change
Expand Up @@ -176,13 +176,13 @@

<script lang="ts" setup>
import { capitalize, computed, onMounted, ref } from "vue";
import type { GridClient } from "@threefold/grid_client";

import { getNodeHealthColor, NodeHealth } from "@/utils/get_nodes";

import { useGrid } from "../stores";
import { updateGrid } from "../utils/grid";
import { markAsFromAnotherClient } from "../utils/helpers";
import { type LoadedDeployments, loadVms, mergeLoadedDeployments } from "../utils/load_deployment";
import { loadVms, mergeLoadedDeployments, getGridClient } from "../utils/load_deployment";

const props = defineProps<{
projectName: string;
Expand Down Expand Up @@ -219,9 +219,31 @@ onMounted(loadDeployments);
async function loadDomains() {
try {
loading.value = true;
updateGrid(grid, { projectName: props.projectName.toLowerCase() });
const grid = await getGridClient(gridStore.client.clientOptions, props.projectName.toLowerCase());
const gateways = await grid!.gateway.list();
const gws = await Promise.all(gateways.map(name => grid!.gateway.get_name({ name })));
const gwsResults = await Promise.allSettled(gateways.map(name => grid!.gateway.get_name({ name })));
const gws = gwsResults
.filter(result => result.status === "fulfilled")
.map(result => (result as PromiseFulfilledResult<any>).value);

const failedGateways = gwsResults
.map((result, index) => ({ result, index }))
.filter(({ result }) => result.status === "rejected")
.map(({ result, index }) => ({
name: gateways[index],
reason: (result as PromiseRejectedResult).reason,
}));

if (failedGateways.length > 0) {
console.error("Failed to load some gateway deployments:", failedGateways);

count.value = gateways.length;
failedDeployments.value = failedGateways.map(fg => ({
name: fg.name,
error: fg.reason?.message || fg.reason || "Unknown error",
}));
}

items.value = gws.map(gw => {
(gw as any).name = gw[0].workloads[0].name;
return gw;
Expand All @@ -233,7 +255,23 @@ async function loadDomains() {
}
}

async function loadDeploymentChunks(grid: GridClient, projectName: string, showAll: boolean) {
const loadTasks = [loadVms(grid), loadVms(await getGridClient(grid.clientOptions, projectName.toLowerCase()))];

// Only load all deployments for VM projects when showAll is enabled
const shouldLoadAllDeployments = showAll && projectName.toLowerCase() === ProjectName.VM.toLowerCase();
if (shouldLoadAllDeployments) {
loadTasks.push(loadVms(await getGridClient(grid.clientOptions, "")));
} else {
// Add a resolved promise to maintain consistent array length
loadTasks.push(Promise.resolve({ count: 0, items: [], failedDeployments: [] }));
}

return Promise.allSettled(loadTasks);
}

async function loadDeployments() {
const start = performance.now();
if (props.projectName.toLowerCase() === ProjectName.Domains.toLowerCase()) {
return loadDomains();
}
Expand All @@ -242,29 +280,25 @@ async function loadDeployments() {

items.value = [];
loading.value = true;
updateGrid(grid, { projectName: props.projectName });
try {
const chunk1 = await loadVms(grid!);
if (chunk1.count > 0 && migrateGateways) {
await migrateModule(grid!.gateway);
}

const chunk2 = await loadVms(updateGrid(grid!, { projectName: props.projectName.toLowerCase() }));
if (chunk2.count > 0 && migrateGateways) {
await migrateModule(grid!.gateway);
}

let chunk3: LoadedDeployments<any[]> = { count: 0, items: [], failedDeployments: [] };
if (showAllDeployments.value) {
chunk3 =
props.projectName.toLowerCase() === ProjectName.VM.toLowerCase()
? await loadVms(updateGrid(grid!, { projectName: "" }))
: { count: 0, items: [], failedDeployments: [] };
const results = await loadDeploymentChunks(grid!, props.projectName, showAllDeployments.value);
const [chunk1, chunk2, chunk3] = results.map((result, index) => {
if (result.status === "fulfilled") {
return result.value;
} else {
console.error(`Failed to load VM chunk ${index + 1}:`, result.reason);
return { count: 0, items: [], failedDeployments: [] };
}
});

if (chunk3.count > 0 && migrateGateways) {
if (migrateGateways) {
const hasDeployments = chunk1.count > 0 || chunk2.count > 0 || chunk3.count > 0;
if (hasDeployments) {
await migrateModule(grid!.gateway);
}
}

if (chunk3.items) {
chunk3.items = chunk3.items.map(markAsFromAnotherClient);
}

Expand All @@ -277,8 +311,8 @@ async function loadDeployments() {
} finally {
loading.value = false;
}

loading.value = false;
const end = performance.now();
console.log(`Time taken: ${(end - start) / 1000} seconds`);
}

const filteredHeaders = computed(() => {
Expand Down Expand Up @@ -451,7 +485,6 @@ import { ProjectName } from "../types";
import { migrateModule } from "../utils/migration";
import AccessDeploymentAlert from "./AccessDeploymentAlert.vue";
import ListTable from "./list_table.vue";
import { GridClient } from "@threefold/grid_client";

export default {
name: "VmDeploymentTable",
Expand Down
48 changes: 48 additions & 0 deletions packages/playground/src/utils/batch_process.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
export interface BatchProcessResult<R> {
results: R[];
errors: Array<{ batchIndex: number; error: Error }>;
hasErrors: boolean;
}

export async function batchProcess<T, R>(
items: T[],
batchSize: number,
processFn: (batch: T[]) => Promise<R[]>,
): Promise<BatchProcessResult<R>> {
const batches: T[][] = [];

for (let i = 0; i < items.length; i += batchSize) {
batches.push(items.slice(i, i + batchSize));
}

const batchResults = await Promise.allSettled(batches.map(batch => processFn(batch)));

const results: R[] = [];
const errors: Array<{ batchIndex: number; error: Error }> = [];

batchResults.forEach((result, index) => {
if (result.status === "fulfilled") {
const batchResult = result.value;
if (Array.isArray(batchResult)) {
results.push(...batchResult);
} else {
console.error(`Batch ${index} returned non-array result:`, batchResult);
}
} else {
errors.push({
batchIndex: index,
error: result.reason instanceof Error ? result.reason : new Error(String(result.reason)),
});
}
});

if (errors.length > 0) {
console.error(`Batch processing completed with ${errors.length} failed batches:`, errors);
}

return {
results,
errors,
hasErrors: errors.length > 0,
};
}
30 changes: 30 additions & 0 deletions packages/playground/src/utils/get_nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ref } from "vue";

import { gridProxyClient } from "@/clients";
import type { NodeHealthColor, NodeStatusColor, NodeTypeColor } from "@/types";
import { getNodeInfo } from "@/utils/contracts";
const requestPageNumber = ref<number>(1);
const offlineNodes = ref<NodeInfo[]>([]);
type NodeFilters = FilterOptions & {
Expand Down Expand Up @@ -201,3 +202,32 @@ export function convert(value: string | undefined) {
}

export const convertToBytes = convert;

// Create module-level cache
const nodeInfoCache = new Map<number, any>();

/**
* Retrieves node information with caching to avoid redundant API calls.
* @param {number[]} nodeIds - Array of node IDs to get information for.
* @returns {Promise<any[]>} A Promise that resolves to an array of node information objects.
*/
export async function getNodeInfoWithCache(nodeIds: number[]) {
const uncachedIds = nodeIds.filter(id => !nodeInfoCache.has(id));
const uniqueUncachedIds = Array.from(new Set(uncachedIds));

if (uniqueUncachedIds.length > 0) {
const newInfo = await getNodeInfo(uniqueUncachedIds, []);
for (const [id, info] of Object.entries(newInfo)) {
nodeInfoCache.set(Number(id), info);
}
}
return nodeIds.map(id => nodeInfoCache.get(id));
}

/**
* Clears the node info cache. Primarily used for testing.
* @internal
*/
export function clearNodeInfoCache() {
nodeInfoCache.clear();
}
Loading