Skip to content
Open
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
27 changes: 27 additions & 0 deletions web-admin/src/features/projects/status/ModelSizeCell.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script lang="ts">
import { formatMemorySize } from "@rilldata/web-common/lib/number-formatting/memory-size";

export let sizeBytes: string | number | undefined;

$: formattedSize = formatSize(sizeBytes);

function formatSize(bytes: string | number | undefined): string {
if (bytes === undefined || bytes === null || bytes === "-1") return "-";

let numBytes: number;
if (typeof bytes === "number") {
numBytes = bytes;
} else {
numBytes = parseInt(bytes, 10);
}

if (isNaN(numBytes) || numBytes < 0) return "-";
return formatMemorySize(numBytes);
}
</script>

<div class="truncate text-right tabular-nums">
<span class:text-gray-500={formattedSize === "-"}>
{formattedSize}
</span>
</div>
12 changes: 10 additions & 2 deletions web-admin/src/features/projects/status/ProjectResources.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,19 @@
import Button from "web-common/src/components/button/Button.svelte";
import ProjectResourcesTable from "./ProjectResourcesTable.svelte";
import RefreshAllSourcesAndModelsConfirmDialog from "./RefreshAllSourcesAndModelsConfirmDialog.svelte";
import { useResources } from "./selectors";
import { useResources, useModelTableSizes } from "./selectors";
import { isResourceReconciling } from "@rilldata/web-admin/lib/refetch-interval-store";

const queryClient = useQueryClient();
const createTrigger = createRuntimeServiceCreateTrigger();

let isConfirmDialogOpen = false;
let tableSizes: any;

$: ({ instanceId } = $runtime);

$: resources = useResources(instanceId);
$: tableSizes = useModelTableSizes(instanceId, $resources.data?.resources);

$: hasReconcilingResources = $resources.data?.resources?.some(
isResourceReconciling,
Expand Down Expand Up @@ -65,7 +67,13 @@
Error loading resources: {$resources.error?.message}
</div>
{:else if $resources.data}
<ProjectResourcesTable data={$resources?.data?.resources} />
<ProjectResourcesTable
data={$resources?.data?.resources}
tableSizes={$tableSizes?.data ?? new Map()}
/>
{#if $tableSizes?.isLoading}
<div class="mt-2 text-xs text-gray-500">Loading model sizes...</div>
{/if}
{/if}
</section>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@
import type { ColumnDef } from "@tanstack/svelte-table";
import { flexRender } from "@tanstack/svelte-table";
import ActionsCell from "./ActionsCell.svelte";
import ModelSizeCell from "./ModelSizeCell.svelte";
import NameCell from "./NameCell.svelte";
import RefreshCell from "./RefreshCell.svelte";
import RefreshResourceConfirmDialog from "./RefreshResourceConfirmDialog.svelte";
import ResourceErrorMessage from "./ResourceErrorMessage.svelte";

export let data: V1Resource[];
export let tableSizes: Map<string, string | number> = new Map();

let isConfirmDialogOpen = false;
let dialogResourceName = "";
Expand Down Expand Up @@ -85,7 +87,7 @@
closeRefreshDialog();
};

// Create columns definition as a constant to prevent unnecessary re-creation
// Create columns definition as a constant - key block handles re-renders
const columns: ColumnDef<V1Resource, any>[] = [
{
accessorKey: "title",
Expand All @@ -104,6 +106,45 @@
name: getValue() as string,
}),
},
{
id: "size",
accessorFn: (row) => {
// Only for models
if (row.meta.name.kind !== ResourceKind.Model) return undefined;

const connector = row.model?.state?.resultConnector;
const tableName = row.model?.state?.resultTable;
if (!connector || !tableName) return undefined;

const key = `${connector}:${tableName}`;
return tableSizes.get(key);
},
header: "Size",
sortingFn: (rowA, rowB) => {
const sizeA = rowA.getValue("size") as string | number | undefined;
const sizeB = rowB.getValue("size") as string | number | undefined;

let numA = -1;
if (sizeA && sizeA !== "-1") {
numA = typeof sizeA === "number" ? sizeA : parseInt(sizeA, 10);
}

let numB = -1;
if (sizeB && sizeB !== "-1") {
numB = typeof sizeB === "number" ? sizeB : parseInt(sizeB, 10);
}

return numB - numA; // Descending
},
sortDescFirst: true,
cell: ({ getValue }) =>
flexRender(ModelSizeCell, {
sizeBytes: getValue() as string | number | undefined,
}),
meta: {
widthPercent: 0,
},
},
{
accessorFn: (row) => row.meta.reconcileStatus,
header: "Status",
Expand Down Expand Up @@ -190,11 +231,13 @@
);
</script>

<VirtualizedTable
data={tableData}
{columns}
columnLayout="minmax(95px, 108px) minmax(100px, 3fr) 48px minmax(80px, 2fr) minmax(100px, 2fr) 56px"
/>
{#key tableSizes}
<VirtualizedTable
data={tableData}
{columns}
columnLayout="minmax(95px, 108px) minmax(100px, 3fr) 100px 48px minmax(80px, 2fr) minmax(100px, 2fr) 56px"
/>
{/key}

<RefreshResourceConfirmDialog
bind:open={isConfirmDialogOpen}
Expand Down
199 changes: 199 additions & 0 deletions web-admin/src/features/projects/status/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import {
} from "@rilldata/web-admin/client";
import {
createRuntimeServiceListResources,
createConnectorServiceOLAPListTables,
type V1ListResourcesResponse,
type V1Resource,
} from "@rilldata/web-common/runtime-client";
import { ResourceKind } from "@rilldata/web-common/features/entity-management/resource-selectors";
import { createSmartRefetchInterval } from "@rilldata/web-admin/lib/refetch-interval-store";
import { readable } from "svelte/store";

export function useProjectDeployment(orgName: string, projName: string) {
return createAdminServiceGetProject<V1Deployment | undefined>(
Expand Down Expand Up @@ -47,3 +50,199 @@ export function useResources(instanceId: string) {
},
);
}

// Cache stores by instanceId and connector array to prevent recreating them
const modelSizesStoreCache = new Map<
string,
{ store: any; unsubscribe: () => void }
>();

// Keep preloaded query subscriptions alive so they don't get cancelled
const preloadedQuerySubscriptions = new Map<string, Set<() => void>>();

// Preload queries to ensure they start immediately and keep them alive
function preloadConnectorQueries(instanceId: string, connectorArray: string[]) {
const preloadKey = `${instanceId}:${connectorArray.join(",")}`;

// Only preload once per connector set
if (preloadedQuerySubscriptions.has(preloadKey)) {
return;
}

const subscriptions = new Set<() => void>();

for (const connector of connectorArray) {
const query = createConnectorServiceOLAPListTables(
{
instanceId,
connector,
},
{
query: {
enabled: true,
},
},
);

// Eagerly subscribe to keep the query alive
const unsubscribe = query.subscribe(() => {});
subscriptions.add(unsubscribe);
}

preloadedQuerySubscriptions.set(preloadKey, subscriptions);
}

function createCachedStore(
cacheKey: string,
instanceId: string,
connectorArray: string[],
) {
// Check if we already have a cached store
if (modelSizesStoreCache.has(cacheKey)) {
return modelSizesStoreCache.get(cacheKey)!.store;
}

// Preload queries immediately so they start running before store subscribers attach
preloadConnectorQueries(instanceId, connectorArray);

// If no connectors, return an empty readable store
if (connectorArray.length === 0) {
const emptyStore = readable(
{
data: new Map<string, string | number>(),
isLoading: false,
isError: false,
},
() => {},
);
modelSizesStoreCache.set(cacheKey, {
store: emptyStore,
unsubscribe: () => {},
});
return emptyStore;
}

// Create a new store with pagination support
const store = readable(
{
data: new Map<string, string | number>(),
isLoading: true,
isError: false,
},
(set) => {
const connectorTables = new Map<string, Array<any>>();
const connectorLoading = new Map<string, boolean>();
const connectorError = new Map<string, boolean>();
const subscriptions = new Set<() => void>();

const updateAndNotify = () => {
const sizeMap = new Map<string, string | number>();
let isLoading = false;
let isError = false;

for (const connector of connectorArray) {
if (connectorLoading.get(connector)) isLoading = true;
if (connectorError.get(connector)) isError = true;

for (const table of connectorTables.get(connector) || []) {
if (
table.name &&
table.physicalSizeBytes !== undefined &&
table.physicalSizeBytes !== null
) {
const key = `${connector}:${table.name}`;
sizeMap.set(key, table.physicalSizeBytes as string | number);
}
}
}

set({ data: sizeMap, isLoading, isError });
};

const fetchPage = (connector: string, pageToken?: string) => {
const query = createConnectorServiceOLAPListTables(
{
instanceId,
connector,
...(pageToken && { pageToken }),
} as any,
{
query: {
enabled: true,
},
},
);

const unsubscribe = query.subscribe((result: any) => {
connectorLoading.set(connector, result.isLoading);
connectorError.set(connector, result.isError);

if (result.data?.tables) {
const existing = connectorTables.get(connector) || [];
connectorTables.set(connector, [
...existing,
...result.data.tables,
]);
}

// If query completed and has more pages, fetch the next page
if (!result.isLoading && result.data?.nextPageToken) {
unsubscribe();
subscriptions.delete(unsubscribe);
fetchPage(connector, result.data.nextPageToken);
}

updateAndNotify();
});

subscriptions.add(unsubscribe);
};

// Start fetching for all connectors
for (const connector of connectorArray) {
connectorLoading.set(connector, true);
connectorError.set(connector, false);
connectorTables.set(connector, []);
fetchPage(connector);
}

return () => {
for (const unsub of subscriptions) {
unsub();
}
};
},
);

// Eagerly subscribe to keep queries alive across component re-renders
const unsubscribe = store.subscribe(() => {});
modelSizesStoreCache.set(cacheKey, { store, unsubscribe });

return store;
}

export function useModelTableSizes(
instanceId: string,
resources: V1Resource[] | undefined,
) {
// Extract unique connectors from model resources
const uniqueConnectors = new Set<string>();

if (resources) {
for (const resource of resources) {
if (resource?.meta?.name?.kind === ResourceKind.Model) {
const connector = resource.model?.state?.resultConnector;
const table = resource.model?.state?.resultTable;

if (connector && table) {
uniqueConnectors.add(connector);
}
}
}
}

const connectorArray = Array.from(uniqueConnectors).sort();
const cacheKey = `${instanceId}:${connectorArray.join(",")}`;

return createCachedStore(cacheKey, instanceId, connectorArray);
}
Loading