Skip to content

Conversation

HarshMN2345
Copy link
Member

@HarshMN2345 HarshMN2345 commented Sep 10, 2025

What does this PR do?

Show badge with count next to “Additional projects”
Implement backend-driven pagination (limit/offset) for project breakdown
Update SDK getAggregation(orgId, aggregationId, limit?, offset?)
Wire loader to read page/limit from URL and pass to component
Add dependency key and {#key} to force reload on page changes

Test Plan

image

Related PRs and Issues

(If this PR is related to any other PR or resolves any issue or related to any issue link all related PR and issues here.)

Have you read the Contributing Guidelines on issues?

yes

Summary by CodeRabbit

  • New Features

    • Added pagination to Projects in Billing Plan Summary with page/limit controls.
    • Added badges for add-on counts and simplified “Additional projects” labeling.
    • Reworked totals: display overall total; removed negative credits line item.
    • Standardized action label to “Upgrade” in applicable plans.
  • Improvements

    • Smoother refresh of billing details when changing page or limit.
  • Chores

    • Updated console package source to a newer build.

Copy link

appwrite bot commented Sep 10, 2025

Console

Project ID: 688b7bf400350cbd60e9

Sites (2)
Site Status Logs Preview QR
 console-qa
688b7cf6003b1842c9dc
Ready Ready View Logs Preview URL QR Code
 console-cloud
688b7c18002b9b871a8f
Ready Ready View Logs Preview URL QR Code

Note

Cursor pagination performs better than offset pagination when loading further pages.

Copy link
Contributor

coderabbitai bot commented Sep 10, 2025

Walkthrough

  • Updated package.json to point "@appwrite.io/console" to artifact "@2515".
  • Extended Billing SDK getAggregation to accept optional limit and offset query parameters.
  • Updated billing page load (+page.ts) to compute pagination (page, limit, offset), depend on "billing:aggregation", and return limit, offset, aggregationKey.
  • Updated billing page (+page.svelte) to pass limit, offset, aggregationKey to PlanSummary.
  • Enhanced planSummary.svelte: added exported props (limit, offset, aggregationKey), introduced client-side pagination for projects, keyed re-render via aggregationKey, added badges for addon counts, reworked totals display, and adjusted rendering for addons and nested rows.

Possibly related PRs

Suggested reviewers

  • ItzNotABug
  • lohanidamodar

Pre-merge checks (3 passed)

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title “feat: pagination on billing ui table” concisely and accurately summarizes the primary feature of this pull request—adding pagination to the billing UI table—without extraneous detail or ambiguity, making it clear to reviewers what main functionality is introduced.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat-SER-347-Add-pagination-new-billing-ui

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/routes/(console)/organization-[organization]/billing/planSummary.svelte (3)

341-346: NaN bug when availableCredit is undefined

Math.min with undefined yields NaN, propagating to totalAmount.

-$: totalAmount = Math.max(currentAggregation?.amount - creditsApplied, 0);
-
-$: creditsApplied = Math.min(
-    currentAggregation?.amount ?? currentPlan?.price ?? 0,
-    availableCredit
-);
+$: creditsApplied = Math.min(
+    currentAggregation?.amount ?? currentPlan?.price ?? 0,
+    availableCredit ?? 0
+);
+$: totalAmount = Math.max((currentAggregation?.amount ?? 0) - creditsApplied, 0);

218-316: Possible runtime errors: missing optional chaining on project resource fields

project.* lookups (find) can be undefined; direct .value/.amount access will throw.

-usage: `${formatBandwidthUsage(project.bandwidth.value, currentPlan?.bandwidth)}`,
-price: formatCurrency(project.bandwidth.amount || 0)
+usage: `${formatBandwidthUsage(project.bandwidth?.value ?? 0, currentPlan?.bandwidth)}`,
+price: formatCurrency(project.bandwidth?.amount ?? 0)
...
-usage: `${formatNum(project.users.value || 0)} / ${currentPlan?.users ? formatNum(currentPlan.users) : 'Unlimited'}`,
-price: formatCurrency(project.users.amount || 0)
+usage: `${formatNum(project.users?.value ?? 0)} / ${currentPlan?.users ? formatNum(currentPlan.users) : 'Unlimited'}`,
+price: formatCurrency(project.users?.amount ?? 0)
...
-usage: `${formatNum(project.databasesReads.value || 0)} / ${currentPlan?.databasesReads ? formatNum(currentPlan.databasesReads) : 'Unlimited'}`,
-price: formatCurrency(project.databasesReads.amount || 0)
+usage: `${formatNum(project.databasesReads?.value ?? 0)} / ${currentPlan?.databasesReads ? formatNum(currentPlan.databasesReads) : 'Unlimited'}`,
+price: formatCurrency(project.databasesReads?.amount ?? 0)
...
-usage: `${formatNum(project.databasesWrites.value || 0)} / ${currentPlan?.databasesWrites ? formatNum(currentPlan.databasesWrites) : 'Unlimited'}`,
-price: formatCurrency(project.databasesWrites.amount || 0)
+usage: `${formatNum(project.databasesWrites?.value ?? 0)} / ${currentPlan?.databasesWrites ? formatNum(currentPlan.databasesWrites) : 'Unlimited'}`,
+price: formatCurrency(project.databasesWrites?.amount ?? 0)
...
-usage: `${formatNum(project.executions.value || 0)} / ${currentPlan?.executions ? formatNum(currentPlan.executions) : 'Unlimited'}`,
-price: formatCurrency(project.executions.amount || 0)
+usage: `${formatNum(project.executions?.value ?? 0)} / ${currentPlan?.executions ? formatNum(currentPlan.executions) : 'Unlimited'}`,
+price: formatCurrency(project.executions?.amount ?? 0)
...
-usage: `${formatHumanSize(project.storage.value || 0)} / ${currentPlan?.storage?.toString() || '0'} GB`,
-price: formatCurrency(project.storage.amount || 0)
+usage: `${formatHumanSize(project.storage?.value ?? 0)} / ${currentPlan?.storage?.toString() || '0'} GB`,
+price: formatCurrency(project.storage?.amount ?? 0)
...
-usage: `${formatNum(project.imageTransformations.value || 0)} / ${currentPlan?.imageTransformations ? formatNum(currentPlan.imageTransformations) : 'Unlimited'}`,
-price: formatCurrency(project.imageTransformations.amount || 0)
+usage: `${formatNum(project.imageTransformations?.value ?? 0)} / ${currentPlan?.imageTransformations ? formatNum(currentPlan.imageTransformations) : 'Unlimited'}`,
+price: formatCurrency(project.imageTransformations?.amount ?? 0)
...
-usage: `${formatNum(project.gbHours.value || 0)} / ${currentPlan?.GBHours ? formatNum(currentPlan.GBHours) : 'Unlimited'}`,
-price: formatCurrency(project.gbHours.amount || 0)
+usage: `${formatNum(project.gbHours?.value ?? 0)} / ${currentPlan?.GBHours ? formatNum(currentPlan.GBHours) : 'Unlimited'}`,
+price: formatCurrency(project.gbHours?.amount ?? 0)
...
-usage: `${formatNum(project.authPhone.value || 0)} SMS messages`,
-price: formatCurrency(project.authPhone.amount || 0)
+usage: `${formatNum(project.authPhone?.value ?? 0)} SMS messages`,
+price: formatCurrency(project.authPhone?.amount ?? 0)

Also update related progressData inputs to use ?.value ?? 0.


326-333: Avoid {@html} for links (XSS risk) and honor base path

Build the link via Svelte markup to prevent injection; also prefix with base for subpath deployments.

-{
-    id: `usage-details`,
-    cells: {
-        item: `<a href="/console/project-${String(project.region || 'default')}-${project.projectId}/settings/usage" style="text-decoration: underline; color: var(--fgcolor-accent-neutral);">Usage details</a>`,
-        usage: '',
-        price: ''
-    }
-}
+{
+    id: `usage-details`,
+    cells: { item: 'Usage details', usage: '', price: '' },
+    linkHref: `${base}/console/project-${String(project.region || 'default')}-${project.projectId}/settings/usage`
+}
-{#if child.cells?.[col.id]?.includes('<a href=')}
-    {@html child.cells?.[col.id] ?? ''}
+{#if col.id === 'item' && child.linkHref}
+    <a href={child.linkHref} rel="noopener noreferrer" class="u-underline" style="color: var(--fgcolor-accent-neutral);">
+        {child.cells?.[col.id]}
+    </a>

Also applies to: 434-436

src/routes/(console)/organization-[organization]/billing/+page.svelte (1)

110-113: Fix user-facing grammar.

“In organization's your projects” is incorrect.

Apply:

-                To avoid service disruptions in organization's your projects, please verify your
-                payment details and update them if necessary.
+                To avoid service disruptions in your organization's projects, please verify your
+                payment details and update them if necessary.
🧹 Nitpick comments (10)
src/lib/sdk/billing.ts (2)

937-950: Harden limit/offset handling (avoid NaN/negatives) and send ints

Guard against NaN and negative values; truncate to integers before sending.

-        if (typeof limit === 'number') params['limit'] = limit;
-        if (typeof offset === 'number') params['offset'] = offset;
+        if (Number.isFinite(limit) && (limit as number) > 0) {
+            params['limit'] = Math.trunc(limit as number);
+        }
+        if (Number.isFinite(offset) && (offset as number) >= 0) {
+            params['offset'] = Math.trunc(offset as number);
+        }

937-942: Return type may need optional totals for pagination

The UI reads breakdownTotal/projectsTotal for pagination. Consider extending AggregationTeam with optional totals to reflect backend shape.

Would you like me to patch the type to:

export type AggregationTeam = { /* existing */ } & {
  breakdownTotal?: number;
  projectsTotal?: number;
};

and wire tests accordingly?

src/routes/(console)/organization-[organization]/billing/planSummary.svelte (6)

154-159: Clamp URL-driven pagination to safe bounds

Prevent negative/invalid values from URL.

-$: projectsLimit = limit ?? (Number(page.url.searchParams.get('limit')) || 5);
-$: projectsOffset =
-    offset ?? ((Number(page.url.searchParams.get('page')) || 1) - 1) * projectsLimit;
+$: projectsLimit = limit ?? Math.max(1, Number(page.url.searchParams.get('limit')) || 5);
+$: projectsOffset =
+    offset ??
+    ((Math.max(1, Number(page.url.searchParams.get('page')) || 1) - 1) * projectsLimit);

781-785: Selector likely mismatched with row id

CSS targets [data-expandable-row-id='additional-projects'] but row id is "addon-projects".

-:global([data-expandable-row-id='additional-projects']),
+:global([data-expandable-row-id='addon-projects']),
 :global([data-expandable-row-id^='addon-']) {

798-804: Fix typo in comment

Minor nit.

-/* reducingh size of paginator */
+/* reduce size of paginator */

1-47: Optional: tighten utility function guards

formatHumanSize/formatBandwidthUsage handle falsy values; consider coalescing inputs to 0 explicitly for consistency.


167-187: Null-safety for addons map

currentPlan.addons is assumed defined when filtering. Consider local var to avoid accidental undefined access.

-const addons = (currentAggregation?.resources || [])
+const planAddons = currentPlan?.addons ?? {};
+const addons = (currentAggregation?.resources ?? [])
     .filter(
         (r) =>
             r.amount &&
             r.amount > 0 &&
-            Object.keys(currentPlan?.addons || {}).includes(r.resourceId) &&
-            currentPlan.addons[r.resourceId]?.price > 0
+            Object.prototype.hasOwnProperty.call(planAddons, r.resourceId) &&
+            planAddons[r.resourceId]?.price > 0
     )

31-35: Prop defaults: consider number defaults for pagination inputs

Defaulting limit/offset to numbers (0/undefined mix) causes conditional checks later. Prefer numeric defaults upstream to simplify.

src/routes/(console)/organization-[organization]/billing/+page.ts (2)

23-25: Optional: depends key may be unnecessary for query param nav.

SvelteKit re-runs load on search param changes. Keep depends('billing:aggregation') only if you plan to invalidate it elsewhere.


95-101: Avoid recomputing pagination; reuse computed values.

Prevents drift and keeps a single source of truth.

Apply:

-        // expose pagination for components
-        limit: getLimit(url, route, 5),
-        offset: pageToOffset(getPage(url) || 1, getLimit(url, route, 5)),
-        // unique key to force component refresh on page change
-        aggregationKey: `agg:${getPage(url) || 1}:${getLimit(url, route, 5)}`
+        // expose pagination for components
+        limit,
+        offset,
+        // unique key to force component refresh on page change
+        aggregationKey: `agg:${currentPage}:${limit}`
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7ba5bfd and 29c12d6.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (5)
  • package.json (1 hunks)
  • src/lib/sdk/billing.ts (1 hunks)
  • src/routes/(console)/organization-[organization]/billing/+page.svelte (1 hunks)
  • src/routes/(console)/organization-[organization]/billing/+page.ts (4 hunks)
  • src/routes/(console)/organization-[organization]/billing/planSummary.svelte (7 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/routes/(console)/organization-[organization]/billing/+page.ts (3)
src/routes/(console)/organization-[organization]/+page.ts (1)
  • load (8-42)
src/lib/helpers/load.ts (3)
  • getPage (8-10)
  • getLimit (12-16)
  • pageToOffset (4-6)
src/lib/stores/organization.ts (2)
  • organization (62-62)
  • Organization (16-38)
🪛 ESLint
src/routes/(console)/organization-[organization]/billing/planSummary.svelte

[error] 160-160: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 161-161: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: e2e
🔇 Additional comments (9)
package.json (1)

25-25: pnpm-lock update verified; confirm CI artifact resolution

  • pnpm-lock.yaml includes the new @appwrite.io/console@2515 entry.
  • Verify your CI pipeline can authenticate to pkg.pr.new, resolve and cache this PR artifact URL reliably.
src/routes/(console)/organization-[organization]/billing/planSummary.svelte (5)

196-203: Nice: badge for “Additional projects”

Clean UX improvement; keeps item text concise and surfaces the count visually.


350-365: Good use of keyed block to force re-render on page changes

{#key aggregationKey} ensures a clean remount when pagination changes.


556-575: Analytics: good switch to typed constant

Using Click.OrganizationClickUpgrade improves event hygiene.


152-153: Incorrect page store import

The page store should come from $app/stores. Using $app/state will fail.

-import { page } from '$app/state';
+import { page } from '$app/stores';

Likely an incorrect or invalid review comment.


494-503: No changes needed: PaginationComponent correctly updates the page query-param
PaginationComponent’s getLink sets/removes the page parameter on its generated links, ensuring SvelteKit navigation triggers the +page.ts loader and re-runs depends('billing:aggregation').

src/routes/(console)/organization-[organization]/billing/+page.svelte (2)

136-139: Prop pass-through for backend pagination looks correct.

Passing limit, offset, and aggregationKey to PlanSummary aligns with the new load API.


136-139: Verify PlanSummary exports and wrap in key block
Ensure the PlanSummary component exports limit, offset, and aggregationKey, and in src/routes/(console)/organization-[organization]/billing/+page.svelte wrap the <PlanSummary> invocation in

{#key data.aggregationKey}
  <PlanSummary … />
{/key}

to force a remount when the key changes.

src/routes/(console)/organization-[organization]/billing/+page.ts (1)

9-10: Imports for pagination helpers look good.

Comment on lines +40 to 48
const currentPage = getPage(url) || 1;
const limit = getLimit(url, route, 5);
const offset = pageToOffset(currentPage, limit);
billingAggregation = await sdk.forConsole.billing.getAggregation(
organization.$id,
(organization as Organization)?.billingAggregationId
(organization as Organization)?.billingAggregationId,
limit,
offset
);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Sanitize page/limit to avoid NaN/invalid values reaching the SDK.

getPage/getLimit can return NaN or <=0 from URL. Guard and normalize before computing offset and calling the SDK.

Apply:

-        const currentPage = getPage(url) || 1;
-        const limit = getLimit(url, route, 5);
-        const offset = pageToOffset(currentPage, limit);
+        const rawPage = getPage(url);
+        const rawLimit = getLimit(url, route, 5);
+        const currentPage =
+            Number.isFinite(rawPage) && rawPage > 0 ? Math.floor(rawPage) : 1;
+        const limit =
+            Number.isFinite(rawLimit) && rawLimit > 0 ? Math.floor(rawLimit) : 5;
+        const offset = pageToOffset(currentPage, limit);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const currentPage = getPage(url) || 1;
const limit = getLimit(url, route, 5);
const offset = pageToOffset(currentPage, limit);
billingAggregation = await sdk.forConsole.billing.getAggregation(
organization.$id,
(organization as Organization)?.billingAggregationId
(organization as Organization)?.billingAggregationId,
limit,
offset
);
const rawPage = getPage(url);
const rawLimit = getLimit(url, route, 5);
const currentPage =
Number.isFinite(rawPage) && rawPage > 0 ? Math.floor(rawPage) : 1;
const limit =
Number.isFinite(rawLimit) && rawLimit > 0 ? Math.floor(rawLimit) : 5;
const offset = pageToOffset(currentPage, limit);
billingAggregation = await sdk.forConsole.billing.getAggregation(
organization.$id,
(organization as Organization)?.billingAggregationId,
limit,
offset
);
🤖 Prompt for AI Agents
In src/routes/(console)/organization-[organization]/billing/+page.ts around
lines 40 to 48, getPage/getLimit can return NaN or non-positive values from the
URL which then produce an invalid offset and bad args for the SDK; before
computing offset and calling sdk.forConsole.billing.getAggregation, coerce and
validate the values: parse/convert the returned page and limit to numbers, use
Number.isFinite and >0 checks, fallback page to 1 and limit to the existing
default (5) when invalid, then compute offset with the sanitized integers and
pass those to the SDK; ensure types remain number and avoid passing NaN or <=0
to pageToOffset or the SDK.

Comment on lines +160 to +165
(currentAggregation && (currentAggregation as any).breakdownTotal) ||
(currentAggregation && (currentAggregation as any).projectsTotal) ||
(currentAggregation?.resources?.find?.((r) => r.resourceId === 'projects')?.value ??
null) ||
currentAggregation?.breakdown?.length ||
0;
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Fix ESLint “no-explicit-any” and keep strong typing for totals

Avoid casts to any by introducing a local augmentation type.

+$: type AggregationTotals = Partial<{ breakdownTotal: number; projectsTotal: number }>;
 $: totalProjects =
-    (currentAggregation && (currentAggregation as any).breakdownTotal) ||
-    (currentAggregation && (currentAggregation as any).projectsTotal) ||
+    (currentAggregation && (currentAggregation as AggregationTotals).breakdownTotal) ||
+    (currentAggregation && (currentAggregation as AggregationTotals).projectsTotal) ||
     (currentAggregation?.resources?.find?.((r) => r.resourceId === 'projects')?.value ??
         null) ||
     currentAggregation?.breakdown?.length ||
     0;

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 ESLint

[error] 160-160: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 161-161: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🤖 Prompt for AI Agents
In src/routes/(console)/organization-[organization]/billing/planSummary.svelte
around lines 160 to 165, avoid the explicit any casts by introducing a local
typed interface/alias that augments the shape of currentAggregation (e.g.,
fields breakdownTotal?: number, projectsTotal?: number, resources?: {
resourceId: string; value?: number }[], breakdown?: unknown[]), annotate
currentAggregation with that type, and then rewrite the expression to use the
typed properties with optional chaining and nullish coalescing (no as any casts)
so the totals resolution remains the same but with strong typing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant