From a033e89b7a2a622d9646b4c67016f5658b662516 Mon Sep 17 00:00:00 2001 From: royendo <67675319+royendo@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:04:17 -0400 Subject: [PATCH 01/31] Add Rill Slots UI to project status overview - Add ManageSlotsModal for viewing and adjusting project slot allocation - Add slots-utils with pricing constants and slot calculation helpers - Update DeploymentSection to show current slot count with link to details - Add olapInfo utility for OLAP engine display info - Update status layout with Deployments nav tab - Add display-utils for deployment status labels and styling Co-Authored-By: Claude Opus 4.6 --- .../features/projects/status/display-utils.ts | 25 +- .../status/overview/DeploymentSection.svelte | 86 +++++- .../status/overview/ManageSlotsModal.svelte | 267 ++++++++++++++++++ .../projects/status/overview/olapInfo.ts | 98 +++++++ .../status/overview/slots-utils.test.ts | 38 +++ .../projects/status/overview/slots-utils.ts | 28 ++ .../[project]/-/status/+layout.svelte | 5 + 7 files changed, 533 insertions(+), 14 deletions(-) create mode 100644 web-admin/src/features/projects/status/overview/ManageSlotsModal.svelte create mode 100644 web-admin/src/features/projects/status/overview/olapInfo.ts create mode 100644 web-admin/src/features/projects/status/overview/slots-utils.test.ts create mode 100644 web-admin/src/features/projects/status/overview/slots-utils.ts diff --git a/web-admin/src/features/projects/status/display-utils.ts b/web-admin/src/features/projects/status/display-utils.ts index 7b4bd133f07..a88ee5b9643 100644 --- a/web-admin/src/features/projects/status/display-utils.ts +++ b/web-admin/src/features/projects/status/display-utils.ts @@ -107,16 +107,31 @@ export function getOlapEngineLabel(connector: V1Connector | undefined): string { isDuckDB && (String(connector.config?.path ?? "").startsWith("md:") || !!connector.config?.token); + const chcHostFields = ["host", "resolved_host", "dsn"]; + const isClickHouseCloud = + connector.type === "clickhouse" && + chcHostFields.some((field) => + String((connector.config as Record)?.[field] ?? "") + .toLowerCase() + .includes(".clickhouse.cloud"), + ); - const name = formatConnectorName( - isMotherDuck ? "motherduck" : connector.type, - ); + let connectorTypeName = connector.type; + if (isMotherDuck) connectorTypeName = "motherduck"; + const name = formatConnectorName(connectorTypeName); // Show management suffix for non-default-DuckDB connectors - const showSuffix = connector.provision || isMotherDuck || !isDuckDB; + const showSuffix = connector.provision || isMotherDuck || isClickHouseCloud || !isDuckDB; if (!showSuffix) return name; - const suffix = connector.provision ? "Rill-managed" : "Self-managed"; + let suffix: string; + if (connector.provision) { + suffix = "Rill-managed"; + } else if (isClickHouseCloud) { + suffix = "Cloud"; + } else { + suffix = "Self-managed"; + } return `${name} (${suffix})`; } diff --git a/web-admin/src/features/projects/status/overview/DeploymentSection.svelte b/web-admin/src/features/projects/status/overview/DeploymentSection.svelte index 92d67e9535f..485d5ec8911 100644 --- a/web-admin/src/features/projects/status/overview/DeploymentSection.svelte +++ b/web-admin/src/features/projects/status/overview/DeploymentSection.svelte @@ -1,8 +1,14 @@ - +
+ {#if canManage && isFree && !$subscriptionQuery?.isLoading} + + Upgrade to Growth + + {/if} + +
@@ -140,7 +172,9 @@
OLAP Engine - {olapEngineLabel} + + {olapEngineLabel} +
@@ -155,9 +189,25 @@ {/if}
+ + {#if !$subscriptionQuery?.isLoading && !isEnterprise} + + {/if}
+ diff --git a/web-admin/src/features/projects/status/overview/ManageSlotsModal.svelte b/web-admin/src/features/projects/status/overview/ManageSlotsModal.svelte new file mode 100644 index 00000000000..270faf3fae3 --- /dev/null +++ b/web-admin/src/features/projects/status/overview/ManageSlotsModal.svelte @@ -0,0 +1,267 @@ + + + { + open = isOpen; + }} +> + + + Manage Slots + + {#if viewOnly} + Based on your current plan, we recommend the following slot + configuration. Upgrade to Growth to customize your slot allocation. + {:else} + All deployments are billed at ${SLOT_RATE_PER_HR}/slot/hr. + Monthly estimates assume ~{HOURS_PER_MONTH} hours/month. + {#if isRillManaged} + Minimum {DEFAULT_MANAGED_SLOTS} slots for Rill-managed deployments. + {:else} + Minimum {DEFAULT_SELF_MANAGED_SLOTS} slots for self-managed OLAP deployments. + {/if} + {/if} + + + + +
+
+ Cluster Size + Slots + Est. $/mo +
+
+ {#each visibleTiers as tier} + + {/each} +
+
+ {#if !viewOnly} + + {/if} + + +

+ Want to stop billing entirely? + (open = false)} + > + Hibernate this project + + from the project settings page. +

+ + +
+
+ + diff --git a/web-admin/src/features/projects/status/overview/olapInfo.ts b/web-admin/src/features/projects/status/overview/olapInfo.ts new file mode 100644 index 00000000000..6ca5244f446 --- /dev/null +++ b/web-admin/src/features/projects/status/overview/olapInfo.ts @@ -0,0 +1,98 @@ +import type { V1Connector } from "@rilldata/web-common/runtime-client"; +import type { RuntimeClient } from "@rilldata/web-common/runtime-client/v2"; +import { queryServiceQuery } from "@rilldata/web-common/runtime-client/v2/gen/query-service"; +import { createQuery } from "@tanstack/svelte-query"; + +export interface OlapInfo { + replicas: number; + vcpus: number; + memory: string; +} + +// Standardized columns: replicas, vcpus, memory +const MOTHERDUCK_SQL = ` +SELECT + 1 AS replicas, + MAX(CASE WHEN name = 'threads' THEN CAST(value AS INT) END) AS vcpus, + MAX(CASE WHEN name = 'memory_limit' THEN value END) AS memory +FROM duckdb_settings() +WHERE name IN ('threads', 'memory_limit') +`.trim(); + +// Self-managed ClickHouse +const SELF_MANAGED_CLICKHOUSE_SQL = ` +SELECT + replicas, + if(cgroup_cpu > 0, cgroup_cpu, os_cpu) AS vcpus, + formatReadableSize(if(server_mem > 0, server_mem, os_mem)) AS memory +FROM ( + SELECT + (SELECT value FROM system.asynchronous_metrics WHERE metric = 'CGroupMaxCPU') AS cgroup_cpu, + (SELECT value FROM system.asynchronous_metrics WHERE metric = 'OSProcessorCount') AS os_cpu, + (SELECT toUInt64(value) FROM system.server_settings WHERE name = 'max_server_memory_usage') AS server_mem, + (SELECT toUInt64(value) FROM system.asynchronous_metrics WHERE metric = 'OSPhysicalMemoryTotal') AS os_mem +) AS hw +CROSS JOIN (SELECT count() AS replicas FROM system.clusters WHERE cluster = 'default') AS cl +`.trim(); + +// ClickHouse Cloud — same query for now; update when CHC-specific SQL is confirmed +const CLICKHOUSE_CLOUD_SQL = SELF_MANAGED_CLICKHOUSE_SQL; + +export function isMotherDuck(connector: V1Connector): boolean { + return ( + connector.type === "duckdb" && + (String(connector.config?.path ?? "").startsWith("md:") || + !!connector.config?.token) + ); +} + +export function isClickHouseCloud(connector: V1Connector): boolean { + if (connector.type !== "clickhouse") return false; + const cfg = connector.config as Record | undefined; + return ["host", "resolved_host", "dsn"].some((field) => + String(cfg?.[field] ?? "") + .toLowerCase() + .includes(".clickhouse.cloud"), + ); +} + +function getOlapInfoSQL(connector: V1Connector | undefined): string | null { + if (!connector) return null; + if (isMotherDuck(connector)) return MOTHERDUCK_SQL; + if (connector.type === "clickhouse") { + return isClickHouseCloud(connector) + ? CLICKHOUSE_CLOUD_SQL + : SELF_MANAGED_CLICKHOUSE_SQL; + } + return null; +} + +export function useOlapInfo( + client: RuntimeClient, + connector: V1Connector | undefined, +) { + const sql = getOlapInfoSQL(connector); + const connectorName = connector?.name; + + return createQuery({ + queryKey: ["olap-info", client.instanceId, connectorName], + queryFn: async ({ signal }) => { + if (!sql || !connectorName) return null; + const res = await queryServiceQuery( + client, + { connector: connectorName, sql, priority: -1, limit: 0 }, + { signal }, + ); + const row = res.data?.[0]; + if (!row) return null; + return { + replicas: Number(row["replicas"] ?? 1), + vcpus: Number(row["vcpus"] ?? 0), + memory: String(row["memory"] ?? ""), + } as OlapInfo; + }, + enabled: !!sql && !!client.instanceId, + staleTime: Infinity, + refetchOnWindowFocus: false, + }); +} diff --git a/web-admin/src/features/projects/status/overview/slots-utils.test.ts b/web-admin/src/features/projects/status/overview/slots-utils.test.ts new file mode 100644 index 00000000000..ab47d78b807 --- /dev/null +++ b/web-admin/src/features/projects/status/overview/slots-utils.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect } from "vitest"; +import { + SLOT_TIERS, + POPULAR_SLOTS, + ALL_SLOTS, + DEFAULT_MANAGED_SLOTS, + DEFAULT_SELF_MANAGED_SLOTS, +} from "./slots-utils"; + +describe("slots-utils", () => { + it("all tiers list has expected entries", () => { + expect(SLOT_TIERS).toHaveLength(ALL_SLOTS.length); + }); + + it("popular slots list has expected entries", () => { + expect(POPULAR_SLOTS).toHaveLength(6); + }); + + it("managed default is 2 slots", () => { + expect(DEFAULT_MANAGED_SLOTS).toBe(2); + }); + + it("self-managed default is 4 slots", () => { + expect(DEFAULT_SELF_MANAGED_SLOTS).toBe(4); + }); + + it("all slot values are at least managed minimum", () => { + for (const s of ALL_SLOTS) { + expect(s).toBeGreaterThanOrEqual(DEFAULT_MANAGED_SLOTS); + } + }); + + it("tiers have correct bill calculations", () => { + const tier = SLOT_TIERS[0]; // 2 slots + expect(tier.slots).toBe(2); + expect(tier.rillBill).toBe(Math.round(2 * 0.15 * 730)); + }); +}); diff --git a/web-admin/src/features/projects/status/overview/slots-utils.ts b/web-admin/src/features/projects/status/overview/slots-utils.ts new file mode 100644 index 00000000000..d8cc8b90c3d --- /dev/null +++ b/web-admin/src/features/projects/status/overview/slots-utils.ts @@ -0,0 +1,28 @@ +export const SLOT_RATE_PER_HR = 0.15; +export const HOURS_PER_MONTH = 730; + +// Default slots by deployment type +export const DEFAULT_MANAGED_SLOTS = 2; // Rill-managed (DuckDB) +export const DEFAULT_SELF_MANAGED_SLOTS = 4; // Self-managed (MotherDuck, ClickHouse, Druid, Pinot, StarRocks) + +export interface SlotTier { + slots: number; + instance: string; + rillBill: number; +} + +function tier(slots: number, rate = SLOT_RATE_PER_HR): SlotTier { + return { + slots, + instance: `${slots * 4}GiB / ${slots}vCPU`, + rillBill: Math.round(slots * rate * HOURS_PER_MONTH), + }; +} + +// Popular slot values shown by default +export const POPULAR_SLOTS = [2, 3, 4, 8, 16, 30]; + +// All available slot values including intermediate sizes +export const ALL_SLOTS = [2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 20, 24, 28, 30]; + +export const SLOT_TIERS: SlotTier[] = ALL_SLOTS.map((s) => tier(s)); diff --git a/web-admin/src/routes/[organization]/[project]/-/status/+layout.svelte b/web-admin/src/routes/[organization]/[project]/-/status/+layout.svelte index 2167242ec2d..fa0e8bd3f51 100644 --- a/web-admin/src/routes/[organization]/[project]/-/status/+layout.svelte +++ b/web-admin/src/routes/[organization]/[project]/-/status/+layout.svelte @@ -13,6 +13,11 @@ route: "", hasPermission: true, }, + { + label: "Deployments", + route: "/deployments", + hasPermission: true, + }, { label: "Resources", route: "/resources", From 1b7bb223f693fd5eb3b9c945fc30ac6ad8b21457 Mon Sep 17 00:00:00 2001 From: royendo <67675319+royendo@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:16:41 -0400 Subject: [PATCH 02/31] View only; remove CTA --- .../features/projects/status/display-utils.ts | 3 ++- .../status/overview/DeploymentSection.svelte | 24 +++++-------------- .../status/overview/ManageSlotsModal.svelte | 14 +++++++---- .../[project]/-/status/+layout.svelte | 5 ---- 4 files changed, 18 insertions(+), 28 deletions(-) diff --git a/web-admin/src/features/projects/status/display-utils.ts b/web-admin/src/features/projects/status/display-utils.ts index a88ee5b9643..0f87db2e733 100644 --- a/web-admin/src/features/projects/status/display-utils.ts +++ b/web-admin/src/features/projects/status/display-utils.ts @@ -121,7 +121,8 @@ export function getOlapEngineLabel(connector: V1Connector | undefined): string { const name = formatConnectorName(connectorTypeName); // Show management suffix for non-default-DuckDB connectors - const showSuffix = connector.provision || isMotherDuck || isClickHouseCloud || !isDuckDB; + const showSuffix = + connector.provision || isMotherDuck || isClickHouseCloud || !isDuckDB; if (!showSuffix) return name; let suffix: string; diff --git a/web-admin/src/features/projects/status/overview/DeploymentSection.svelte b/web-admin/src/features/projects/status/overview/DeploymentSection.svelte index 485d5ec8911..49db1cde1d3 100644 --- a/web-admin/src/features/projects/status/overview/DeploymentSection.svelte +++ b/web-admin/src/features/projects/status/overview/DeploymentSection.svelte @@ -5,8 +5,8 @@ V1DeploymentStatus, } from "@rilldata/web-admin/client"; import { - isFreePlan, - isGrowthPlan, + isTrialPlan, + // isGrowthPlan, isEnterprisePlan, } from "@rilldata/web-admin/features/billing/plans/utils"; import { useDashboardsLastUpdated } from "@rilldata/web-admin/features/dashboards/listing/selectors"; @@ -87,18 +87,15 @@ // Billing plan detection $: subscriptionQuery = createAdminServiceGetBillingSubscription(organization); $: planName = $subscriptionQuery?.data?.subscription?.plan?.name ?? ""; - $: isFree = isFreePlan(planName); + $: isFree = isTrialPlan(planName); $: isEnterprise = planName !== "" && isEnterprisePlan(planName);
{#if canManage && isFree && !$subscriptionQuery?.isLoading} - - Upgrade to Growth + + Upgrade to Teams {/if} Rill Slots - - - {currentSlots} - View details - - + {currentSlots}
{/if}
- diff --git a/web-admin/src/features/projects/status/overview/DeploymentSection.svelte b/web-admin/src/features/projects/status/overview/DeploymentSection.svelte index cd437f6106a..adc9bcdba2e 100644 --- a/web-admin/src/features/projects/status/overview/DeploymentSection.svelte +++ b/web-admin/src/features/projects/status/overview/DeploymentSection.svelte @@ -1,4 +1,5 @@ + + From fc1daee2c79425d7fa735e0fdca8c5c427233dcd Mon Sep 17 00:00:00 2001 From: royendo <67675319+royendo@users.noreply.github.com> Date: Fri, 27 Mar 2026 13:30:02 -0400 Subject: [PATCH 05/31] deployments fill in page; wait for DES --- .../status/deployments/DeploymentsPage.svelte | 285 +++++++++++++++--- .../status/overview/DeploymentSection.svelte | 13 +- .../status/overview/ManageSlotsModal.svelte | 28 +- 3 files changed, 267 insertions(+), 59 deletions(-) diff --git a/web-admin/src/features/projects/status/deployments/DeploymentsPage.svelte b/web-admin/src/features/projects/status/deployments/DeploymentsPage.svelte index 018ee1992a7..93ca94e7ea5 100644 --- a/web-admin/src/features/projects/status/deployments/DeploymentsPage.svelte +++ b/web-admin/src/features/projects/status/deployments/DeploymentsPage.svelte @@ -16,6 +16,7 @@ import { SLOT_RATE_PER_HR, HOURS_PER_MONTH, + SLOT_TIERS, } from "../overview/slots-utils"; import ManageSlotsModal from "../overview/ManageSlotsModal.svelte"; @@ -38,7 +39,8 @@ // Self-managed: any non-DuckDB OLAP connector (ClickHouse, MotherDuck, Druid, Pinot, StarRocks) $: olapType = (projectData as any)?.olapConnector ?? ""; $: isRillManaged = olapType === "" || olapType === "duckdb"; - let slotsModalOpen = false; + let prodModalOpen = false; + let devModalOpen = false; // Billing $: subscriptionQuery = createAdminServiceGetBillingSubscription(organization); @@ -46,13 +48,27 @@ $: isTrial = isTrialPlan(planName); $: isEnterprise = planName !== "" && isEnterprisePlan(planName); - // Estimated costs - $: rillMonthlyCost = Math.round(currentSlots * SLOT_RATE_PER_HR * HOURS_PER_MONTH); + // Slot types + $: prodSlots = currentSlots; + $: devSlots = 2; // TODO: wire to project data when dev slots are available + $: totalSlots = prodSlots + devSlots; + + // Cluster info + $: prodTier = SLOT_TIERS.find((t) => t.slots === prodSlots); + $: prodClusterLabel = prodTier?.instance ?? `${prodSlots * 4}GiB / ${prodSlots}vCPU`; + $: devClusterLabel = devSlots > 0 ? `${devSlots * 4}GiB / ${devSlots}vCPU` : "—"; + $: prodMonthlyCost = Math.round(prodSlots * SLOT_RATE_PER_HR * HOURS_PER_MONTH); + $: devMonthlyCost = Math.round(devSlots * SLOT_RATE_PER_HR * HOURS_PER_MONTH); + $: totalMonthlyCost = Math.round(totalSlots * SLOT_RATE_PER_HR * HOURS_PER_MONTH); + + // Bar percentages + $: prodPct = totalSlots > 0 ? (prodSlots / totalSlots) * 100 : 50; + $: devPct = totalSlots > 0 ? (devSlots / totalSlots) * 100 : 50; {#if !isEnterprise}
- + - - -
-
- Rill Slots - {currentSlots} - - @ ${SLOT_RATE_PER_HR}/slot/hr - (~${rillMonthlyCost.toLocaleString()}/mo) +
+ + {totalSlots} {totalSlots === 1 ? "slot" : "slots"} total + · ~${totalMonthlyCost.toLocaleString()}/mo - + See price breakdown
+ + +
+
+ {#if prodSlots > 0} +
+ + Prod · {prodSlots} + +
+ {/if} + {#if devSlots > 0} +
+ + Dev · {devSlots} + +
+ {:else} +
+ + Dev · 0 + +
+ {/if} +
+
+ + +
+ +
+
+
+ +

Production

+
+ {#if canManage && !$subscriptionQuery?.isLoading} + + {/if} +
+
+
+ {prodClusterLabel} + Cluster Size +
+
+
+ Slots + {prodSlots} +
+
+ Est. cost + ~${prodMonthlyCost.toLocaleString()}/mo +
+
+ Rate + ${SLOT_RATE_PER_HR}/slot/hr +
+
+
+
+ + +
+
+
+ +

Development

+
+ {#if canManage && !$subscriptionQuery?.isLoading} + + {/if} +
+
+
+ {devClusterLabel} + Cluster Size +
+
+
+ Slots + {devSlots} +
+
+ Est. cost + + {devSlots > 0 ? `~$${devMonthlyCost.toLocaleString()}/mo` : "—"} + +
+
+ Rate + ${SLOT_RATE_PER_HR}/slot/hr +
+
+
+
+
diff --git a/web-admin/src/features/projects/status/overview/DeploymentSection.svelte b/web-admin/src/features/projects/status/overview/DeploymentSection.svelte index adc9bcdba2e..44025a2d23d 100644 --- a/web-admin/src/features/projects/status/overview/DeploymentSection.svelte +++ b/web-admin/src/features/projects/status/overview/DeploymentSection.svelte @@ -22,6 +22,7 @@ getStatusDotClass, getStatusLabel, } from "../display-utils"; + import { SLOT_TIERS } from "./slots-utils"; import { getGitUrlFromRemote } from "@rilldata/web-common/features/project/deploy/github-utils"; import ProjectClone from "./ProjectClone.svelte"; import OverviewCard from "./OverviewCard.svelte"; @@ -81,8 +82,10 @@ (c) => c.name === instance?.aiConnector, ); - // Slots + // Slots / Cluster size $: currentSlots = Number(projectData?.prodSlots) || 0; + $: currentTier = SLOT_TIERS.find((t) => t.slots === currentSlots); + $: clusterLabel = currentTier?.instance ?? `${currentSlots * 4}GiB / ${currentSlots}vCPU`; $: canManage = $proj.data?.projectPermissions?.manageProject ?? false; // Billing plan detection @@ -194,13 +197,14 @@ {#if !$subscriptionQuery?.isLoading && !isEnterprise}
- Rill Slots + Cluster Size - {currentSlots} + {clusterLabel} + ({currentSlots} {currentSlots === 1 ? "slot" : "slots"}) View details @@ -244,6 +248,9 @@ .slots-count { @apply text-sm text-fg-primary font-medium tabular-nums; } + .slots-secondary { + @apply text-xs text-fg-tertiary; + } .slots-detail { @apply text-xs text-primary-500; } diff --git a/web-admin/src/features/projects/status/overview/ManageSlotsModal.svelte b/web-admin/src/features/projects/status/overview/ManageSlotsModal.svelte index 7d0ee26e0a2..46c4bac3fbf 100644 --- a/web-admin/src/features/projects/status/overview/ManageSlotsModal.svelte +++ b/web-admin/src/features/projects/status/overview/ManageSlotsModal.svelte @@ -16,8 +16,13 @@ HOURS_PER_MONTH, DEFAULT_MANAGED_SLOTS, DEFAULT_SELF_MANAGED_SLOTS, + type SlotTier, } from "./slots-utils"; + function tierForSlots(slots: number): SlotTier | undefined { + return SLOT_TIERS.find((t) => t.slots === slots); + } + export let open = false; export let organization: string; export let project: string; @@ -74,8 +79,11 @@ await queryClient.refetchQueries({ queryKey: getAdminServiceGetProjectQueryKey(organization, project), }); + const newTier = tierForSlots(selectedSlots); eventBus.emit("notification", { - message: `Slots updated to ${selectedSlots}`, + message: newTier + ? `Cluster size updated to ${newTier.instance}` + : `Cluster size updated to ${selectedSlots} slots`, }); open = false; } catch (err) { @@ -96,15 +104,15 @@ > - Manage Slots + Manage Cluster Size - All deployments are billed at ${SLOT_RATE_PER_HR}/slot/hr. Monthly - estimates assume ~{HOURS_PER_MONTH} hours/month. + Choose the vCPU and memory allocation for your deployment. + Monthly estimates assume ~{HOURS_PER_MONTH} hours at ${SLOT_RATE_PER_HR}/slot/hr. {#if !isTrial} {#if isRillManaged} - Minimum {DEFAULT_MANAGED_SLOTS} slots for Rill-managed deployments. + Minimum {DEFAULT_MANAGED_SLOTS * 4}GiB / {DEFAULT_MANAGED_SLOTS}vCPU for Rill-managed deployments. {:else} - Minimum {DEFAULT_SELF_MANAGED_SLOTS} slots for self-managed OLAP deployments. + Minimum {DEFAULT_SELF_MANAGED_SLOTS * 4}GiB / {DEFAULT_SELF_MANAGED_SLOTS}vCPU for self-managed OLAP deployments. {/if} {/if} @@ -114,7 +122,6 @@
Cluster Size - Slots Est. $/mo
@@ -130,9 +137,7 @@ > {tier.instance} - - - {tier.slots} + ({tier.slots} {tier.slots === 1 ? "slot" : "slots"}) {#if tier.slots === currentSlots} current {/if} @@ -218,6 +223,9 @@ .tier-cell-wide { @apply flex-[2]; } + .slot-label { + @apply text-xs text-fg-tertiary font-normal; + } .current-badge { @apply text-[10px] text-primary-600 bg-primary-100 px-1.5 py-0.5 rounded-full leading-none font-medium; } From df0bc92c854192d0597922650a6996a2d90905d0 Mon Sep 17 00:00:00 2001 From: royendo <67675319+royendo@users.noreply.github.com> Date: Fri, 27 Mar 2026 13:50:47 -0400 Subject: [PATCH 06/31] code qual --- .../features/projects/status/deployments/DeploymentsPage.svelte | 1 - .../features/projects/status/overview/DeploymentSection.svelte | 1 - 2 files changed, 2 deletions(-) diff --git a/web-admin/src/features/projects/status/deployments/DeploymentsPage.svelte b/web-admin/src/features/projects/status/deployments/DeploymentsPage.svelte index 93ca94e7ea5..c117bf4ce75 100644 --- a/web-admin/src/features/projects/status/deployments/DeploymentsPage.svelte +++ b/web-admin/src/features/projects/status/deployments/DeploymentsPage.svelte @@ -40,7 +40,6 @@ $: olapType = (projectData as any)?.olapConnector ?? ""; $: isRillManaged = olapType === "" || olapType === "duckdb"; let prodModalOpen = false; - let devModalOpen = false; // Billing $: subscriptionQuery = createAdminServiceGetBillingSubscription(organization); diff --git a/web-admin/src/features/projects/status/overview/DeploymentSection.svelte b/web-admin/src/features/projects/status/overview/DeploymentSection.svelte index 44025a2d23d..f91272d4aa3 100644 --- a/web-admin/src/features/projects/status/overview/DeploymentSection.svelte +++ b/web-admin/src/features/projects/status/overview/DeploymentSection.svelte @@ -1,5 +1,4 @@ {#if !isEnterprise} @@ -133,7 +155,7 @@ {#if canManage && !$subscriptionQuery?.isLoading} diff --git a/web-admin/src/features/projects/status/overview/ManageSlotsModal.svelte b/web-admin/src/features/projects/status/overview/ManageSlotsModal.svelte index b676ab45c4c..3a494bc2ce4 100644 --- a/web-admin/src/features/projects/status/overview/ManageSlotsModal.svelte +++ b/web-admin/src/features/projects/status/overview/ManageSlotsModal.svelte @@ -23,36 +23,50 @@ return SLOT_TIERS.find((t) => t.slots === slots); } - export let open = false; - export let organization: string; - export let project: string; - export let currentSlots: number; - export let isRillManaged = true; - export let isTrial = false; + let { + open = $bindable(false), + organization, + project, + currentSlots, + isRillManaged = true, + isTrial = false, + }: { + open?: boolean; + organization: string; + project: string; + currentSlots: number; + isRillManaged?: boolean; + isTrial?: boolean; + } = $props(); // Free trials have no minimum; Growth+: 2 for Rill-managed, 4 for self-managed - $: minSlots = + let minSlots = $derived( isTrial && isRillManaged ? 1 : isRillManaged ? DEFAULT_MANAGED_SLOTS - : DEFAULT_SELF_MANAGED_SLOTS; + : DEFAULT_SELF_MANAGED_SLOTS, + ); - let selectedSlots = currentSlots; - $: if (open) { - selectedSlots = currentSlots; - showAllSizes = false; - } + let selectedSlots = $state(currentSlots); + let showAllSizes = $state(false); - let showAllSizes = false; + $effect(() => { + if (open) { + selectedSlots = currentSlots; + showAllSizes = false; + } + }); const updateProject = createAdminServiceUpdateProject(); // Filter tiers to only show slots >= minimum - $: availableTiers = SLOT_TIERS.filter((t) => t.slots >= minSlots); + let availableTiers = $derived( + SLOT_TIERS.filter((t) => t.slots >= minSlots), + ); // Ensure the current slot count always appears in the popular list - $: popularSlotsWithExtras = (() => { + let popularSlotsWithExtras = $derived((() => { let slots = POPULAR_SLOTS.filter((s) => s >= minSlots); if ( currentSlots >= minSlots && @@ -62,13 +76,17 @@ slots.push(currentSlots); } return slots.sort((a, b) => a - b); - })(); + })()); - $: visibleTiers = showAllSizes - ? availableTiers - : availableTiers.filter((t) => popularSlotsWithExtras.includes(t.slots)); + let visibleTiers = $derived( + showAllSizes + ? availableTiers + : availableTiers.filter((t) => + popularSlotsWithExtras.includes(t.slots), + ), + ); - $: hasChanged = selectedSlots !== currentSlots; + let hasChanged = $derived(selectedSlots !== currentSlots); async function applySlotChange() { try { @@ -134,7 +152,7 @@ class:tier-active={tier.slots === currentSlots} class:tier-selected={tier.slots === selectedSlots && tier.slots !== currentSlots} - on:click={() => { + onclick={() => { selectedSlots = tier.slots; }} > @@ -157,7 +175,7 @@
@@ -168,7 +186,7 @@ (open = false)} + onclick={() => (open = false)} > Hibernate this project @@ -176,13 +194,13 @@

- From c15fc87b61a4e39d2781721225e9d27bb760f3d3 Mon Sep 17 00:00:00 2001 From: royendo <67675319+royendo@users.noreply.github.com> Date: Tue, 7 Apr 2026 10:12:46 -0400 Subject: [PATCH 10/31] local code review --- .../status/deployments/DeploymentsPage.svelte | 533 +++++++++++------- .../status/overview/DeploymentSection.svelte | 40 +- .../status/overview/ManageSlotsModal.svelte | 28 +- 3 files changed, 341 insertions(+), 260 deletions(-) diff --git a/web-admin/src/features/projects/status/deployments/DeploymentsPage.svelte b/web-admin/src/features/projects/status/deployments/DeploymentsPage.svelte index 99ab8017ea9..5173e0ea84b 100644 --- a/web-admin/src/features/projects/status/deployments/DeploymentsPage.svelte +++ b/web-admin/src/features/projects/status/deployments/DeploymentsPage.svelte @@ -2,19 +2,12 @@ import { createAdminServiceGetProject, createAdminServiceGetBillingSubscription, + createAdminServiceListDeployments, V1DeploymentStatus, } from "@rilldata/web-admin/client"; - import { - isTrialPlan, - isEnterprisePlan, - } from "@rilldata/web-admin/features/billing/plans/utils"; - import { useProjectDeployment } from "../selectors"; + import { isEnterprisePlan } from "@rilldata/web-admin/features/billing/plans/utils"; import { getStatusDotClass, getStatusLabel } from "../display-utils"; - import { - SLOT_RATE_PER_HR, - HOURS_PER_MONTH, - SLOT_TIERS, - } from "../overview/slots-utils"; + import { SLOT_RATE_PER_HR, HOURS_PER_MONTH } from "../overview/slots-utils"; import ManageSlotsModal from "../overview/ManageSlotsModal.svelte"; let { @@ -25,13 +18,6 @@ project: string; } = $props(); - // Deployment - let projectDeployment = $derived(useProjectDeployment(organization, project)); - let deployment = $derived($projectDeployment.data); - let deploymentStatus = $derived( - deployment?.status ?? V1DeploymentStatus.DEPLOYMENT_STATUS_UNSPECIFIED, - ); - // Project let proj = $derived(createAdminServiceGetProject(organization, project)); let projectData = $derived($proj.data?.project); @@ -41,10 +27,8 @@ let canManage = $derived( $proj.data?.projectPermissions?.manageProject ?? false, ); - // Self-managed: any non-DuckDB OLAP connector (ClickHouse, MotherDuck, Druid, Pinot, StarRocks) - let olapType = $derived((projectData as any)?.olapConnector ?? ""); - let isRillManaged = $derived(olapType === "" || olapType === "duckdb"); let prodModalOpen = $state(false); + let devModalOpen = $state(false); // Billing let subscriptionQuery = $derived( @@ -53,185 +37,282 @@ let planName = $derived( $subscriptionQuery?.data?.subscription?.plan?.name ?? "", ); - let isTrial = $derived(isTrialPlan(planName)); let isEnterprise = $derived(planName !== "" && isEnterprisePlan(planName)); + // Billing cycle dates + let cycleStart = $derived( + $subscriptionQuery?.data?.subscription?.currentBillingCycleStartDate, + ); + let cycleEnd = $derived( + $subscriptionQuery?.data?.subscription?.currentBillingCycleEndDate, + ); + + function formatCycleDate(dateStr: string | undefined): string { + if (!dateStr) return ""; + const d = new Date(dateStr); + return d.toLocaleDateString(undefined, { month: "short", day: "numeric" }); + } + // Slot types let prodSlots = $derived(currentSlots); let devSlots = $derived(2); // TODO: wire to project data when dev slots are available let totalSlots = $derived(prodSlots + devSlots); - // Cluster info - let prodTier = $derived(SLOT_TIERS.find((t) => t.slots === prodSlots)); - let prodClusterLabel = $derived( - prodTier?.instance ?? `${prodSlots * 4}GiB / ${prodSlots}vCPU`, - ); - let devClusterLabel = $derived( - devSlots > 0 ? `${devSlots * 4}GiB / ${devSlots}vCPU` : "\u2014", - ); + // Cluster info (split into number + unit for display) + let prodMemory = $derived(prodSlots * 4); + let prodCpu = $derived(prodSlots); + let devMemory = $derived(devSlots * 4); + let devCpu = $derived(devSlots); + + // Cost calculations (with decimals) let prodMonthlyCost = $derived( - Math.round(prodSlots * SLOT_RATE_PER_HR * HOURS_PER_MONTH), + (prodSlots * SLOT_RATE_PER_HR * HOURS_PER_MONTH).toFixed(2), ); let devMonthlyCost = $derived( - Math.round(devSlots * SLOT_RATE_PER_HR * HOURS_PER_MONTH), + (devSlots * SLOT_RATE_PER_HR * HOURS_PER_MONTH).toFixed(2), ); let totalMonthlyCost = $derived( - Math.round(totalSlots * SLOT_RATE_PER_HR * HOURS_PER_MONTH), + (totalSlots * SLOT_RATE_PER_HR * HOURS_PER_MONTH).toFixed(2), ); // Bar percentages let prodPct = $derived(totalSlots > 0 ? (prodSlots / totalSlots) * 100 : 50); let devPct = $derived(totalSlots > 0 ? (devSlots / totalSlots) * 100 : 50); + + // Dev deployments table + let deploymentsQuery = $derived( + createAdminServiceListDeployments(organization, project), + ); + let primaryBranch = $derived(projectData?.primaryBranch ?? "main"); + let devDeployments = $derived( + ($deploymentsQuery.data?.deployments ?? []).filter( + (d) => d.branch && d.branch !== primaryBranch, + ), + ); {#if !isEnterprise}
-