Skip to content

Commit 219493c

Browse files
royendoclaude
andauthored
pricing/phase-1: Rill Slots UI on project status overview (#9087)
* 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 <noreply@anthropic.com> * View only; remove CTA * code qual * fix: migrate `ManageSlotsModal` to Svelte 5 runes and event attributes - Replace `export let` with `$props()` and `$bindable()` for the `open` prop - Replace `$:` reactive declarations with `$derived`, `$derived.by`, `$state`, and `$effect` - Replace `on:click` event directives with native `onclick` attributes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: resolve `state_referenced_locally` warning in `ManageSlotsModal` Initialize selectedSlots with 0 instead of capturing the initial prop value; the $effect already syncs it from currentSlots when the dialog opens. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * tied to 9166, only show for Free Plan and Growth * Update DeploymentSection.svelte * Update DeploymentSection.svelte * Update DeploymentSection.svelte * remove dead code and plan logic * free trial too * slots * Update utils.ts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * add Upgrade to Pro in Team Plan UI too Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Delete ManageSlotsModal.svelte * fix Plan plan in email due to appending of plan in mail template * asreq * doesn support commented out variables * prettier * apparently doesnt like any commented out code * Update DeploymentSection.svelte * export to function --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c6680d3 commit 219493c

6 files changed

Lines changed: 117 additions & 11 deletions

File tree

admin/billing/orb.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -669,9 +669,9 @@ func getPlanDisplayName(externalID string) string {
669669
case "managed":
670670
return "Managed"
671671
case "free_plan":
672-
return "Free Plan"
672+
return "Free"
673673
case "pro_plan":
674-
return "Pro Plan"
674+
return "Pro"
675675
default:
676676
return "Enterprise"
677677
}

web-admin/src/features/billing/plans/ProPlan.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
} = $props();
2020
</script>
2121

22-
<SettingsContainer title={plan?.displayName ?? "Pro Plan"}>
22+
<SettingsContainer title={plan?.displayName ?? "Pro"}>
2323
<div slot="body">
2424
Next billing cycle will start on
2525
<b>{getNextBillingCycleDate(subscription.currentBillingCycleEndDate)}</b>.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script lang="ts">
2+
export let slots: number;
3+
</script>
4+
5+
<span>
6+
{slots * 4} GiB RAM, {slots} vCPU
7+
<span class="text-fg-tertiary text-xs ml-1">
8+
({slots}
9+
{slots === 1 ? "Compute unit" : "Compute units"})
10+
</span>
11+
</span>

web-admin/src/features/projects/status/overview/DeploymentSection.svelte

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@
22
import { page } from "$app/stores";
33
import {
44
createAdminServiceGetProject,
5+
createAdminServiceGetBillingSubscription,
56
V1DeploymentStatus,
67
} from "@rilldata/web-admin/client";
8+
import {
9+
isFreePlan,
10+
isProPlan,
11+
isTrialPlan,
12+
} from "@rilldata/web-admin/features/billing/plans/utils";
713
import { extractBranchFromPath } from "@rilldata/web-admin/features/branches/branch-utils";
814
import { useDashboardsLastUpdated } from "@rilldata/web-admin/features/dashboards/listing/selectors";
915
import { useGithubLastSynced } from "@rilldata/web-admin/features/projects/selectors";
@@ -30,6 +36,7 @@
3036
import { getGitUrlFromRemote } from "@rilldata/web-common/features/project/deploy/github-utils";
3137
import ProjectClone from "./ProjectClone.svelte";
3238
import OverviewCard from "@rilldata/web-common/features/projects/status/overview/OverviewCard.svelte";
39+
import ClusterSize from "./ClusterSize.svelte";
3340
3441
export let organization: string;
3542
export let project: string;
@@ -105,17 +112,30 @@
105112
$: aiConnector = instance?.projectConnectors?.find(
106113
(c) => c.name === instance?.aiConnector,
107114
);
115+
116+
// Slots
117+
$: currentSlots = Number(projectData?.prodSlots) || 0;
118+
119+
// Billing plan detection
120+
$: subscriptionQuery = createAdminServiceGetBillingSubscription(organization);
121+
$: planName = $subscriptionQuery?.data?.subscription?.plan?.name ?? "";
122+
$: showSlots =
123+
isTrialPlan(planName) || isFreePlan(planName) || isProPlan(planName);
108124
</script>
109125

110126
<OverviewCard title="Deployment">
111-
<ProjectClone
112-
slot="header-right"
113-
{organization}
114-
{project}
115-
gitRemote={projectData?.gitRemote}
116-
managedGitId={projectData?.managedGitId}
117-
disabled={!!parserReconcileError}
118-
/>
127+
<div slot="header-right" class="flex items-center gap-3">
128+
<!-- TODO: re-add "Upgrade to Pro" link when ready.
129+
Gate on: canManage && (isTrialPlan || isFreePlan || isTeamPlan) && !subscriptionQuery.isLoading
130+
-->
131+
<ProjectClone
132+
{organization}
133+
{project}
134+
gitRemote={projectData?.gitRemote}
135+
managedGitId={projectData?.managedGitId}
136+
disabled={!!parserReconcileError}
137+
/>
138+
</div>
119139

120140
<div class="info-grid">
121141
<div class="info-row">
@@ -137,6 +157,15 @@
137157
</span>
138158
</div>
139159

160+
{#if !$subscriptionQuery?.isLoading && showSlots}
161+
<div class="info-row">
162+
<span class="info-label">Cluster Size</span>
163+
<span class="info-value">
164+
<ClusterSize slots={currentSlots} />
165+
</span>
166+
</div>
167+
{/if}
168+
140169
{#if isGithubConnected}
141170
<div class="info-row">
142171
<span class="info-label">Repo</span>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { describe, it, expect } from "vitest";
2+
import {
3+
SLOT_TIERS,
4+
POPULAR_SLOTS,
5+
ALL_SLOTS,
6+
DEFAULT_MANAGED_SLOTS,
7+
DEFAULT_SELF_MANAGED_SLOTS,
8+
} from "./slots-utils";
9+
10+
describe("slots-utils", () => {
11+
it("all tiers list has expected entries", () => {
12+
expect(SLOT_TIERS).toHaveLength(ALL_SLOTS.length);
13+
});
14+
15+
it("popular slots list has expected entries", () => {
16+
expect(POPULAR_SLOTS).toHaveLength(6);
17+
});
18+
19+
it("managed default is 2 slots", () => {
20+
expect(DEFAULT_MANAGED_SLOTS).toBe(2);
21+
});
22+
23+
it("self-managed default is 4 slots", () => {
24+
expect(DEFAULT_SELF_MANAGED_SLOTS).toBe(4);
25+
});
26+
27+
it("all slot values are at least managed minimum", () => {
28+
for (const s of ALL_SLOTS) {
29+
expect(s).toBeGreaterThanOrEqual(DEFAULT_MANAGED_SLOTS);
30+
}
31+
});
32+
33+
it("tiers have correct bill calculations", () => {
34+
const tier = SLOT_TIERS[0]; // 2 slots
35+
expect(tier.slots).toBe(2);
36+
expect(tier.rillBill).toBe(Math.round(2 * 0.15 * 730));
37+
});
38+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
export const SLOT_RATE_PER_HR = 0.15;
2+
export const HOURS_PER_MONTH = 730;
3+
4+
// Default slots by deployment type
5+
export const DEFAULT_MANAGED_SLOTS = 2; // Rill-managed (DuckDB)
6+
export const DEFAULT_SELF_MANAGED_SLOTS = 4; // Self-managed (MotherDuck, ClickHouse, Druid, Pinot, StarRocks)
7+
8+
export interface SlotTier {
9+
slots: number;
10+
instance: string;
11+
rillBill: number;
12+
}
13+
14+
function tier(slots: number, rate = SLOT_RATE_PER_HR): SlotTier {
15+
return {
16+
slots,
17+
instance: `${slots * 4}GiB / ${slots}vCPU`,
18+
rillBill: Math.round(slots * rate * HOURS_PER_MONTH),
19+
};
20+
}
21+
22+
// Popular slot values shown by default
23+
export const POPULAR_SLOTS = [2, 3, 4, 8, 16, 30];
24+
25+
// All available slot values including intermediate sizes
26+
export const ALL_SLOTS = [2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 20, 24, 28, 30];
27+
28+
export const SLOT_TIERS: SlotTier[] = ALL_SLOTS.map((s) => tier(s));

0 commit comments

Comments
 (0)