Skip to content

feat: add mass backup button on project page#254

Open
romzes5000 wants to merge 9 commits intoPortabase:mainfrom
romzes5000:feat/mass-backup-button
Open

feat: add mass backup button on project page#254
romzes5000 wants to merge 9 commits intoPortabase:mainfrom
romzes5000:feat/mass-backup-button

Conversation

@romzes5000
Copy link
Copy Markdown
Contributor

@romzes5000 romzes5000 commented Apr 6, 2026

Summary

Adds a Backup button to the project detail page header that queues backup jobs for all databases in the project at once. Databases that already have a backup waiting or in progress are automatically skipped.

Motivation

When managing projects with many databases, triggering backups one by one is tedious. This feature lets project admins/owners start a backup for every database in a single click — the same workflow as the existing per-database "Backup" button, but at the project level.

Changes

File Change
src/components/wrappers/dashboard/projects/mass-backup-button/mass-backup-project.action.ts New — server action: validates org membership & role, checks for active backups, inserts waiting backup rows inside a single transaction
src/components/wrappers/dashboard/projects/mass-backup-button/mass-backup-button.tsx New — client component using ButtonWithConfirm (same pattern as BackupButton)
app/(customer)/dashboard/(organization)/projects/[projectId]/page.tsx Adds MassBackupButton to the project toolbar (visible to owner/admin only)

Design decisions

  • Consistent UX — uses the same icon (DatabaseZap), confirmation dialog copy ("Create Backup" / "Yes, create backup" / "No, cancel"), and toast pattern as the existing single-database BackupButton
  • Authorization — server action checks org membership and rejects member role; unified error message ("Project not found or access denied") prevents project enumeration
  • Atomicity — active backup check and new backup inserts run inside a single DB transaction to prevent race conditions (TOCTOU)
  • No internal error leakage — raw Postgres error messages are logged server-side only, never returned to the client

Test plan

  • Open a project page with multiple databases → click "Backup" → confirm → verify toast shows created/skipped count
  • Click "Backup" again immediately → verify toast "All databases already have a backup in progress or waiting"
  • Log in as member role → verify the button is not visible
  • Open a project with no databases → verify the button is disabled, confirmation shows "Add databases…" message
  • Open two browser tabs, click "Backup" simultaneously → verify no duplicate waiting rows are created

Summary by CodeRabbit

  • New Features
    • Mass Backup: initiate backups for all project databases with confirmation prompts, progress and error notifications, and safeguards when databases are already backing up.
    • Pagination enhancements: increased default cards per page for projects and databases and added selectable "cards per page" options so users can control page size.

romzes5000 and others added 4 commits April 5, 2026 14:16
Add an optional pageSizeOptions prop to CardsWithPagination that renders
a "Cards per page" dropdown selector, consistent with the existing
TablePaginationSize pattern used in DataTable.

- New PaginationSize component (standalone, no TanStack Table dependency)
- CardsWithPagination: manage pageSize as state, reset to page 1 on change
- Project databases page: increase default from 6 to 20, add selector [10, 20, 50]
- Projects list page: increase default from 9 to 12, add selector [12, 24, 48]
- Fully backward compatible: without pageSizeOptions, behavior is unchanged

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Validate pageSize in handlePageSizeChange (reject 0/negative/NaN)
- Guard empty pageSizeOptions array from rendering empty selector
- Restore className on PaginationNavigation for backward compatibility
- Add aria-label to SelectTrigger for accessibility on small screens

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…t in options

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a "Backup" button to the project detail page header that queues
backup jobs for all databases in the project at once.

- Server action with org membership + role check (not "member")
- Skips databases that already have a backup waiting/ongoing
- Check + insert inside a single transaction to prevent race conditions
- Unified error messages to prevent project enumeration
- No internal error details leaked to the client

Made-with: Cursor
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 6, 2026

📝 Walkthrough

Walkthrough

Adds a mass-backup feature (client button + server action) to enqueue backups for project databases; introduces selectable page sizes in pagination, updates default cards-per-page values for projects and project databases, and adds a GitLab CI job to build/push a production Docker image.

Changes

Cohort / File(s) Summary
Mass Backup UI
src/components/wrappers/dashboard/projects/mass-backup-button/mass-backup-button.tsx
New client React component MassBackupButton (exports MassBackupButtonProps) using React Query mutation, confirmation dialog, disabled/pending handling, toast notifications, query invalidation, and router refresh on success.
Mass Backup Server Action
src/components/wrappers/dashboard/projects/mass-backup-button/mass-backup-project.action.ts
New server action massBackupProjectAction ("use server", schema-validated). Validates project and membership/role, queries databases, inserts backup rows in a transaction for databases without waiting/ongoing backups, and returns { count, skipped } or structured errors; exports MassBackupProjectResult type.
Project Page Integration
app/(customer)/dashboard/(organization)/projects/[projectId]/page.tsx
Renders MassBackupButton in non-member header, passing projectId and databaseCount derived from proj.databases.length. Also increased cardsPerPage for project databases (6→20) and added pageSizeOptions={[10,20,50]}.
Projects List Pagination
app/(customer)/dashboard/(organization)/projects/page.tsx
Adjusted CardsWithPagination for projects: cardsPerPage changed from 9→12 and pageSizeOptions added ([12,24,48]).
CardsWithPagination Component
src/components/wrappers/common/cards-with-pagination.tsx
Added optional pageSizeOptions?: number[]; introduced internal pageSize state, handlePageSizeChange, and updated pagination math to use pageSize. Conditionally renders PaginationSize when options provided.
Pagination Size Control
src/components/wrappers/common/pagination/pagination-size.tsx
New PaginationSize component (exports PaginationSizeProps) rendering a "Cards per page" select; validates effective page size against options and calls onPageSizeChange on selection.
CI Configuration
.gitlab-ci.yml
New GitLab CI pipeline that builds and pushes a production Docker image to the Container Registry, tags by commit/ref and optionally latest on default branch; uses docker:dind and prod build target.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as MassBackupButton
    participant Mutation as ReactQueryMutation
    participant Server as massBackupProjectAction
    participant DB as Database
    participant Toast as ToastService
    participant Router as Router

    User->>UI: Click "Mass backup"
    UI->>User: Show confirmation dialog
    User->>UI: Confirm
    UI->>Mutation: mutate(projectId)
    Mutation->>Server: call massBackupProjectAction(projectId)
    Server->>Server: validate project, membership, role
    Server->>DB: fetch project databases and existing backups
    Server->>DB: insert backup rows in transaction for eligible DBs
    DB-->>Server: return created/skipped counts
    Server-->>Mutation: return success or failure payload
    Mutation->>Toast: show success or error
    Mutation->>Router: router.refresh() / invalidate queries
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I nibbled code between my paws,

One click sends backups on their cause,
Vaults queued up, a tidy spree,
Databases sleep, safe as can be,
Hops of joy for dev and me.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding a mass backup button to the project page, which is the primary feature introduced across the modified and new files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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
Copy Markdown

@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: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/`(customer)/dashboard/(organization)/projects/[projectId]/page.tsx:
- Around line 85-89: The MassBackupButton is being passed disabled={isMember}
while it is only rendered inside the !isMember branch, so disabled will always
be false; remove the redundant disabled prop from the JSX (the MassBackupButton
usage with projectId and databaseCount) or replace it with a meaningful
condition if there are other disable scenarios, and then update the
MassBackupButtonProps/type (e.g., MassBackupButtonProps) to make disabled
optional or remove the prop entirely so the component signature matches the
updated usage.

In
`@src/components/wrappers/dashboard/projects/mass-backup-button/mass-backup-button.tsx`:
- Around line 23-41: The mutation currently performs side effects inside
mutationFn; change mutationFn (used in useMutation) to only call
massBackupProjectAction(projectId) and return its result (do not call toast,
queryClient.invalidateQueries, or router.refresh there). Add onSuccess callback
to inspect result.data.success and run toast.success,
queryClient.invalidateQueries({ queryKey: ["database-data"] }), and
router.refresh when appropriate, and add onError (or onSuccess branch for
result.data.actionError) to call toast.error with
result?.data?.actionError?.message || result?.serverError || "Failed to create
backup."; ensure useMutation references the same mutation variable and that any
non-HTTP errors thrown by massBackupProjectAction are propagated to onError.
- Around line 70-72: The confirm.onClick currently receives an async function
wrapper (onClick: async () => { await handleAction(); }) which returns a Promise
and can produce unhandled rejections; change it to a synchronous fire-and-forget
call using the mutation API (e.g., replace the async wrapper with () =>
mutation.mutate(...) and remove the handleAction wrapper), or alternatively wrap
the async call in a try/catch invoked inside a non-async function; update
references to handleAction and ensure you call mutation.mutate (not
mutation.mutateAsync) so ButtonWithConfirm.confirm.onClick is a () => void.

In
`@src/components/wrappers/dashboard/projects/mass-backup-button/mass-backup-project.action.ts`:
- Around line 91-96: The loop is performing sequential inserts which is slow;
replace the per-item await loop over toBackup with a single batch insert by
constructing an array of objects like { databaseId: d.id, status: "waiting" }
from toBackup and passing that array to
tx.insert(drizzleDb.schemas.backup).values(...); ensure you still await the
single tx.insert(...) call (and include any necessary returning or timestamp
fields if required by backup schema) so all rows are inserted in one operation
instead of many awaits in the loop.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 49f1d8f5-ce26-448b-9a10-72f6a6dc8171

📥 Commits

Reviewing files that changed from the base of the PR and between db9d23b and 94c32cc.

📒 Files selected for processing (3)
  • app/(customer)/dashboard/(organization)/projects/[projectId]/page.tsx
  • src/components/wrappers/dashboard/projects/mass-backup-button/mass-backup-button.tsx
  • src/components/wrappers/dashboard/projects/mass-backup-button/mass-backup-project.action.ts
📜 Review details
🔇 Additional comments (3)
src/components/wrappers/dashboard/projects/mass-backup-button/mass-backup-project.action.ts (2)

40-58: LGTM: Authorization logic is well-implemented.

The membership check correctly verifies both organization membership and role permissions, returning a unified error message to prevent project enumeration attacks.


76-102: Duplicate backups are already prevented by database constraint.

A unique index (database_id, status) already exists on the backups table in migration 0000_awesome_nomad.sql, preventing multiple backups with the same status per database. This makes the race condition concern moot—any concurrent insert of a duplicate waiting backup will fail at the constraint level rather than succeeding.

However, constraint violations are caught by the generic error handler and return a 500 error without distinguishing the specific cause. Consider distinguishing constraint violations to provide a clearer error message (e.g., "Backup already in progress for this database") rather than a generic failure.

src/components/wrappers/dashboard/projects/mass-backup-button/mass-backup-button.tsx (1)

47-82: LGTM: Button configuration and disabled state logic.

The disabled state correctly combines all relevant conditions, and the ButtonWithConfirm integration properly handles the confirmation flow with appropriate UX messaging for the zero-database case.

- Remove redundant `disabled` prop (always false inside `!isMember` block)
- Move side effects from mutationFn to onSuccess/onError callbacks
- Replace async onClick with synchronous mutation.mutate
- Use batch insert instead of sequential loop in transaction
- Remove unused `sql` import

Made-with: Cursor
Copy link
Copy Markdown

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/components/wrappers/dashboard/projects/mass-backup-button/mass-backup-project.action.ts`:
- Around line 50-58: Replace the negative membership.role check with a positive
allowlist: instead of using membership.role === "member", define an explicit
set/array of permitted roles (e.g., allowedRoles including "owner" and "admin")
and verify membership.role is in that set before allowing the action; update the
conditional that currently returns the error object (the block that references
membership, membership.role, and parsedInput) to return the same error when
membership is missing or membership.role is not in the allowedRoles set.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 84162bc2-4fe7-4fa9-b1ba-5d98abe2d1c3

📥 Commits

Reviewing files that changed from the base of the PR and between 94c32cc and 0f394bf.

📒 Files selected for processing (3)
  • app/(customer)/dashboard/(organization)/projects/[projectId]/page.tsx
  • src/components/wrappers/dashboard/projects/mass-backup-button/mass-backup-button.tsx
  • src/components/wrappers/dashboard/projects/mass-backup-button/mass-backup-project.action.ts
📜 Review details
🔇 Additional comments (10)
src/components/wrappers/dashboard/projects/mass-backup-button/mass-backup-project.action.ts (6)

1-14: LGTM!

Clean imports and well-defined result type with appropriate count and skipped fields for the mass backup operation.


22-38: LGTM!

The project lookup correctly filters for non-archived projects, and the generic error message prevents project ID enumeration.


60-72: LGTM!

Database fetch and empty project validation are implemented correctly.


76-102: LGTM!

The transaction properly isolates the check-then-insert operation to prevent TOCTOU races. The batch insert pattern (addressing the previous review comment) is correctly implemented with proper type assertion for the status literal.


104-129: LGTM!

Good differentiation between "all databases skipped" (returns failure) and partial success. The conditional success message appropriately informs users about skipped databases.


130-141: LGTM!

Proper error handling: errors are logged server-side for debugging while returning a generic message to the client to prevent information leakage.

app/(customer)/dashboard/(organization)/projects/[projectId]/page.tsx (1)

85-88: LGTM!

Clean integration of the MassBackupButton. The redundant disabled prop has been removed (addressing the previous review comment), and the component correctly receives projectId and databaseCount from the project data.

src/components/wrappers/dashboard/projects/mass-backup-button/mass-backup-button.tsx (3)

1-15: LGTM!

Clean imports and well-defined props type for the component.


22-43: LGTM!

The mutation structure correctly follows React Query patterns with side effects in onSuccess/onError callbacks (addressing the previous review feedback). The query invalidation using ["database-data"] as a prefix key will correctly invalidate all database data queries across the project due to React Query v5's default partial matching behavior.


45-80: LGTM!

The component correctly disables when there are no databases or during pending state. The confirmation dialog provides clear context, with appropriate messaging for the empty-database edge case. The onClick handler properly uses synchronous mutation.mutate (addressing the previous review feedback).

Comment on lines +50 to +58
if (!membership || membership.role === "member") {
return {
success: false,
actionError: {
message: "Project not found or access denied.",
messageParams: { projectId: parsedInput },
},
};
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider using an allowlist for role validation.

The current check membership.role === "member" denies only the member role, implicitly allowing any other value. Since the role column is an unconstrained text field (per schema), an unexpected value (typo, future role) would pass this check.

A positive allowlist is more defensive:

-        if (!membership || membership.role === "member") {
+        if (!membership || !["admin", "owner"].includes(membership.role)) {

This ensures only explicitly permitted roles can perform mass backups.

📝 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
if (!membership || membership.role === "member") {
return {
success: false,
actionError: {
message: "Project not found or access denied.",
messageParams: { projectId: parsedInput },
},
};
}
if (!membership || !["admin", "owner"].includes(membership.role)) {
return {
success: false,
actionError: {
message: "Project not found or access denied.",
messageParams: { projectId: parsedInput },
},
};
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/components/wrappers/dashboard/projects/mass-backup-button/mass-backup-project.action.ts`
around lines 50 - 58, Replace the negative membership.role check with a positive
allowlist: instead of using membership.role === "member", define an explicit
set/array of permitted roles (e.g., allowedRoles including "owner" and "admin")
and verify membership.role is in that set before allowing the action; update the
conditional that currently returns the error object (the block that references
membership, membership.role, and parsedInput) to return the same error when
membership is missing or membership.role is not in the allowedRoles set.

Build linux/amd64 prod target; push ref slug, short SHA, and latest on default branch.
Skip duplicate MR pipelines.

Made-with: Cursor
Copy link
Copy Markdown

@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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/`(customer)/dashboard/(organization)/projects/[projectId]/page.tsx:
- Around line 110-112: The default pagination props are inconsistent: update the
props passed to the page component so cardsPerPage and pageSizeOptions align
(either set cardsPerPage to the first entry of pageSizeOptions or reorder
pageSizeOptions so its first value equals cardsPerPage); specifically adjust the
cardsPerPage prop or the pageSizeOptions array used with the PaginationSize
component (cardsPerPage={20} and pageSizeOptions={[10,20,50]}) so the default
value appears as the first option for consistent behavior with the other pages
(e.g., make cardsPerPage match pageSizeOptions[0] or swap the array order).

In `@src/components/wrappers/common/cards-with-pagination.tsx`:
- Around line 26-28: The pageSize state in useState(cardsPerPage) (pageSize,
setPageSize) is only set on mount and won't update when the cardsPerPage prop
changes; add a sync useEffect that watches cardsPerPage and calls
setPageSize(cardsPerPage) (and optionally resets currentPage via
setCurrentPage(1)) so the component reflects prop changes; locate the useState
declarations for pageSize/setPageSize and currentPage/setCurrentPage in
cards-with-pagination.tsx and add the effect there.

In `@src/components/wrappers/common/pagination/pagination-size.tsx`:
- Around line 11-14: PaginationSize computes effectivePageSize by accessing
pageSizeOptions[0], which breaks if pageSizeOptions is an empty array; update
the logic in the PaginationSize component to guard against an empty
pageSizeOptions (e.g., treat an empty array as missing) by using a fallback
default array or fallback value: choose pageSizeOptions if it has length and
includes(props.pageSize) ? props.pageSize : pageSizeOptions[0], otherwise fall
back to props.pageSize if valid or a hardcoded default (e.g., 10); ensure
references to pageSizeOptions, props.pageSize, and effectivePageSize are
adjusted so effectivePageSize is never undefined before rendering the Select and
before calling onPageSizeChange.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8ddf8c0b-45a1-4918-abdd-c60468d3e2e0

📥 Commits

Reviewing files that changed from the base of the PR and between 0f394bf and e3ac541.

📒 Files selected for processing (4)
  • app/(customer)/dashboard/(organization)/projects/[projectId]/page.tsx
  • app/(customer)/dashboard/(organization)/projects/page.tsx
  • src/components/wrappers/common/cards-with-pagination.tsx
  • src/components/wrappers/common/pagination/pagination-size.tsx
📜 Review details
🔇 Additional comments (5)
app/(customer)/dashboard/(organization)/projects/page.tsx (1)

69-71: LGTM!

The pagination configuration is consistent - cardsPerPage={12} correctly matches the first option in pageSizeOptions={[12, 24, 48]}, ensuring proper initialization of the page size selector.

app/(customer)/dashboard/(organization)/projects/[projectId]/page.tsx (1)

85-88: LGTM!

The MassBackupButton integration is correct - props match the expected interface (projectId: string, databaseCount: number), and the component is properly placed within the !isMember block for role-based visibility. The server action provides the actual authorization enforcement.

src/components/wrappers/common/pagination/pagination-size.tsx (1)

15-34: LGTM on accessibility and UI structure.

Good implementation with proper aria-label on the SelectTrigger for screen readers, and responsive design hiding the label on smaller screens while maintaining functionality.

src/components/wrappers/common/cards-with-pagination.tsx (2)

46-50: LGTM on validation logic.

The handlePageSizeChange properly validates the input (Number.isFinite and >= 1) and resets to page 1 to avoid showing an empty page after reducing page size.


61-78: LGTM on layout structure.

The flex wrapper cleanly groups the optional PaginationSize and PaginationNavigation controls with consistent spacing. The conditional rendering via showSizeSelector correctly gates the size selector.

Comment on lines +110 to +112
cardsPerPage={20}
numberOfColumns={3}
pageSizeOptions={[10, 20, 50]}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Inconsistent pagination defaults.

cardsPerPage={20} doesn't match the first option in pageSizeOptions={[10, 20, 50]}. The PaginationSize component falls back to pageSizeOptions[0] (which is 10) when the pageSize isn't in the options array. However, since 20 is in the array, this will work correctly.

That said, this pattern is inconsistent with projects/page.tsx where cardsPerPage={12} matches the first option [12, 24, 48]. Consider aligning for consistency:

♻️ Suggested alignment

Either change the default to match the first option:

-                        cardsPerPage={20}
+                        cardsPerPage={10}

Or reorder options so the default is first:

-                        pageSizeOptions={[10, 20, 50]}
+                        pageSizeOptions={[20, 10, 50]}
📝 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
cardsPerPage={20}
numberOfColumns={3}
pageSizeOptions={[10, 20, 50]}
cardsPerPage={10}
numberOfColumns={3}
pageSizeOptions={[10, 20, 50]}
Suggested change
cardsPerPage={20}
numberOfColumns={3}
pageSizeOptions={[10, 20, 50]}
cardsPerPage={20}
numberOfColumns={3}
pageSizeOptions={[20, 10, 50]}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(customer)/dashboard/(organization)/projects/[projectId]/page.tsx around
lines 110 - 112, The default pagination props are inconsistent: update the props
passed to the page component so cardsPerPage and pageSizeOptions align (either
set cardsPerPage to the first entry of pageSizeOptions or reorder
pageSizeOptions so its first value equals cardsPerPage); specifically adjust the
cardsPerPage prop or the pageSizeOptions array used with the PaginationSize
component (cardsPerPage={20} and pageSizeOptions={[10,20,50]}) so the default
value appears as the first option for consistent behavior with the other pages
(e.g., make cardsPerPage match pageSizeOptions[0] or swap the array order).

Comment on lines +26 to +28
const [pageSize, setPageSize] = useState(cardsPerPage);
const [currentPage, setCurrentPage] = useState(1);
const totalPages = Math.ceil(data.length / cardsPerPage);
const totalPages = Math.ceil(data.length / pageSize);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

pageSize state won't sync if cardsPerPage prop changes.

The pageSize state is initialized from cardsPerPage only on first render. If the parent re-renders with a different cardsPerPage value, the component will continue using the stale initial value.

This may be intentional (user selection persists), but if the prop change should reset the state, consider adding a sync effect:

♻️ Optional: Sync state with prop changes
+import React, { ComponentType, useState, useEffect } from "react";

 // Inside component:
 const [pageSize, setPageSize] = useState(cardsPerPage);
+
+useEffect(() => {
+    setPageSize(cardsPerPage);
+    setCurrentPage(1);
+}, [cardsPerPage]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/wrappers/common/cards-with-pagination.tsx` around lines 26 -
28, The pageSize state in useState(cardsPerPage) (pageSize, setPageSize) is only
set on mount and won't update when the cardsPerPage prop changes; add a sync
useEffect that watches cardsPerPage and calls setPageSize(cardsPerPage) (and
optionally resets currentPage via setCurrentPage(1)) so the component reflects
prop changes; locate the useState declarations for pageSize/setPageSize and
currentPage/setCurrentPage in cards-with-pagination.tsx and add the effect
there.

Comment on lines +11 to +14
export const PaginationSize = (props: PaginationSizeProps) => {
const { className, onPageSizeChange, pageSizeOptions = [10, 20, 30, 40, 50] } = props;
const effectivePageSize = pageSizeOptions.includes(props.pageSize) ? props.pageSize : pageSizeOptions[0];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard against empty pageSizeOptions array.

If pageSizeOptions is passed as an empty array [], pageSizeOptions[0] will be undefined, causing effectivePageSize to be undefined and rendering issues in the Select component.

🛡️ Proposed defensive check
 export const PaginationSize = (props: PaginationSizeProps) => {
-    const { className, onPageSizeChange, pageSizeOptions = [10, 20, 30, 40, 50] } = props;
+    const { className, onPageSizeChange, pageSizeOptions: rawOptions = [10, 20, 30, 40, 50] } = props;
+    const pageSizeOptions = rawOptions.length > 0 ? rawOptions : [10, 20, 30, 40, 50];
     const effectivePageSize = pageSizeOptions.includes(props.pageSize) ? props.pageSize : pageSizeOptions[0];
📝 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
export const PaginationSize = (props: PaginationSizeProps) => {
const { className, onPageSizeChange, pageSizeOptions = [10, 20, 30, 40, 50] } = props;
const effectivePageSize = pageSizeOptions.includes(props.pageSize) ? props.pageSize : pageSizeOptions[0];
export const PaginationSize = (props: PaginationSizeProps) => {
const { className, onPageSizeChange, pageSizeOptions: rawOptions = [10, 20, 30, 40, 50] } = props;
const pageSizeOptions = rawOptions.length > 0 ? rawOptions : [10, 20, 30, 40, 50];
const effectivePageSize = pageSizeOptions.includes(props.pageSize) ? props.pageSize : pageSizeOptions[0];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/wrappers/common/pagination/pagination-size.tsx` around lines
11 - 14, PaginationSize computes effectivePageSize by accessing
pageSizeOptions[0], which breaks if pageSizeOptions is an empty array; update
the logic in the PaginationSize component to guard against an empty
pageSizeOptions (e.g., treat an empty array as missing) by using a fallback
default array or fallback value: choose pageSizeOptions if it has length and
includes(props.pageSize) ? props.pageSize : pageSizeOptions[0], otherwise fall
back to props.pageSize if valid or a hardcoded default (e.g., 10); ensure
references to pageSizeOptions, props.pageSize, and effectivePageSize are
adjusted so effectivePageSize is never undefined before rendering the Select and
before calling onPageSizeChange.

Copy link
Copy Markdown

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.gitlab-ci.yml:
- Around line 37-53: The pipeline uses a non-unique SHORT_TAG
(CI_COMMIT_SHORT_SHA) for image tagging and may publish :latest concurrently;
change SHORT_TAG to use the full CI_COMMIT_SHA (i.e. set
SHORT_TAG="${CI_COMMIT_SHA}" or replace usages of SHORT_TAG with CI_COMMIT_SHA
when building/taging) so image tags are content-addressable, and add a
resource_group to the job that retags/pushes the latest image (the block that
checks CI_COMMIT_BRANCH == CI_DEFAULT_BRANCH and runs docker tag/push for
:latest) so publication of latest is serialized across concurrent pipelines.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6716efcf-8538-4cd8-9636-c3f009154fc0

📥 Commits

Reviewing files that changed from the base of the PR and between e3ac541 and eea906f.

📒 Files selected for processing (1)
  • .gitlab-ci.yml

Comment on lines +37 to +53
- REF_TAG="${CI_COMMIT_REF_SLUG}"
- SHORT_TAG="${CI_COMMIT_SHORT_SHA}"
- |
docker build \
--platform linux/amd64 \
-f docker/dockerfile/Dockerfile \
--target prod \
-t "${CI_REGISTRY_IMAGE}:${REF_TAG}" \
-t "${CI_REGISTRY_IMAGE}:${SHORT_TAG}" \
.
- docker push "${CI_REGISTRY_IMAGE}:${REF_TAG}"
- docker push "${CI_REGISTRY_IMAGE}:${SHORT_TAG}"
- |
if [ "${CI_COMMIT_BRANCH:-}" = "${CI_DEFAULT_BRANCH}" ]; then
docker tag "${CI_REGISTRY_IMAGE}:${REF_TAG}" "${CI_REGISTRY_IMAGE}:latest"
docker push "${CI_REGISTRY_IMAGE}:latest"
fi
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

GitLab CI docs: Is CI_COMMIT_SHORT_SHA guaranteed unique across repository history? What is the recommended tag for immutable Docker images (CI_COMMIT_SHA), and what is the recommended way to prevent concurrent pipelines from overwriting latest (e.g., resource_group)?

💡 Result:

  • Is CI_COMMIT_SHORT_SHA guaranteed unique? No. GitLab defines it as the first 8 characters of CI_COMMIT_SHA [1], and GitLab explicitly notes short SHAs are a convenient reference but “not guaranteed unique” [2]. Use the full SHA when uniqueness matters.

  • Recommended tag for immutable Docker images: Tag images with the full commit SHA ($CI_COMMIT_SHA) so each build is content-addressable/traceable to an exact commit. GitLab’s own container-scanning convention publishes images using $CI_COMMIT_SHA in the image name/tag [3].

  • Prevent concurrent pipelines from overwriting latest:

    1. Serialize the “push latest” job with resource_group so only one pipeline at a time can run that critical section (mutual exclusion across pipelines) [4].
      push_latest:
        stage: deploy
        resource_group: docker-latest
        script:
          - docker push "$CI_REGISTRY_IMAGE:latest"
    2. For deployments, also enable GitLab’s “Prevent outdated deployment jobs” setting to stop older pipelines from finishing later and overwriting a newer “latest” deployment [5].

Use immutable tags and serialize latest publication.

Line 38 uses CI_COMMIT_SHORT_SHA, which is not guaranteed to be unique across your repository's history (GitLab defines it as the first 8 characters of the full SHA), and Lines 50–53 can publish latest out of order when two default-branch pipelines overlap.

🔧 Proposed fix
 build:
   stage: build
   image: docker:27-cli
+  resource_group: registry-latest-publish
   services:
     - docker:27-dind
@@
-    - SHORT_TAG="${CI_COMMIT_SHORT_SHA}"
+    - SHA_TAG="${CI_COMMIT_SHA}"
@@
         -t "${CI_REGISTRY_IMAGE}:${REF_TAG}" \
-        -t "${CI_REGISTRY_IMAGE}:${SHORT_TAG}" \
+        -t "${CI_REGISTRY_IMAGE}:${SHA_TAG}" \
         .
     - docker push "${CI_REGISTRY_IMAGE}:${REF_TAG}"
-    - docker push "${CI_REGISTRY_IMAGE}:${SHORT_TAG}"
+    - docker push "${CI_REGISTRY_IMAGE}:${SHA_TAG}"

Use the full CI_COMMIT_SHA for content-addressable image tags, and add resource_group to serialize the latest publication across concurrent pipelines.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.gitlab-ci.yml around lines 37 - 53, The pipeline uses a non-unique
SHORT_TAG (CI_COMMIT_SHORT_SHA) for image tagging and may publish :latest
concurrently; change SHORT_TAG to use the full CI_COMMIT_SHA (i.e. set
SHORT_TAG="${CI_COMMIT_SHA}" or replace usages of SHORT_TAG with CI_COMMIT_SHA
when building/taging) so image tags are content-addressable, and add a
resource_group to the job that retags/pushes the latest image (the block that
checks CI_COMMIT_BRANCH == CI_DEFAULT_BRANCH and runs docker tag/push for
:latest) so publication of latest is serialized across concurrent pipelines.

@RambokDev
Copy link
Copy Markdown
Collaborator

@romzes5000, is this PR ready to be reviewed, or does more work still need to be done?

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.

2 participants