Skip to content

Fix: servers per-page setting resets after session expiry - #2502

Open
xLaugh wants to merge 1 commit into
pelican:mainfrom
xLaugh:fix/servers-per-page-persistence
Open

Fix: servers per-page setting resets after session expiry#2502
xLaugh wants to merge 1 commit into
pelican:mainfrom
xLaugh:fix/servers-per-page-persistence

Conversation

@xLaugh

@xLaugh xLaugh commented Aug 9, 2026

Copy link
Copy Markdown

Summary

Filament only caches the servers table's "records per page" selection in the PHP session. Once the session expires (Laravel's default SESSION_LIFETIME is 120 minutes, and Pelican doesn't override it), the setting silently reverts to the hardcoded default (10 in grid view / 20 in list view) instead of staying at whatever the user picked.

This PR persists the selection in the user's customization column (the same mechanism already used for dashboard_layout, top_navigation, etc.), so it survives session expiry instead of only lasting a couple of hours.

  • Session-based caching (Filament's default behavior) is untouched and still takes priority when valid, for instant client-side feedback.
  • When the session doesn't have a value (expired, or a new session from a different device), the table now falls back to the value stored in customization.servers_per_page instead of the hardcoded default.
  • Scoped to the servers list (ListServers) only, per the contributing guide's "one targeted change per PR" — the same session-only limitation exists on a few other tables (egg list, activity log, file manager) but I left those out of this PR.

Test plan

Tested manually end-to-end in a local Docker dev build (Dockerfile.dev), driving a real browser against it:

  • Fresh user, no stored preference → per-page defaults to 10 (unchanged behavior)
  • Changing per-page to 20 persists customization.servers_per_page = 20 in the database
  • Server-side session files deleted (simulating expiry) while still authenticated (remember-me) → per-page still shows 20 on reload, instead of reverting to 10
  • No automated tests included yet.

Filament only caches the table's records-per-page choice in the PHP
session, so it silently reverts to the hardcoded default once the
session expires. Store the selection in the user's customization
column, mirroring the existing dashboard_layout preference, so it
survives session expiry.
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Server pagination preferences

Layer / File(s) Summary
Pagination preference contract
app/Enums/CustomizationKey.php, app/Models/User.php
Adds the ServersPerPage customization key, sets its default to 0, and validates stored values as non-negative integers.
Server list pagination integration
app/Filament/App/Resources/Servers/Pages/ListServers.php
Uses the stored preference or layout defaults for pagination. Numeric pagination changes update table state and persist the preference.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary change: preserving the servers per-page setting after session expiry.
Description check ✅ Passed The description directly explains the persistence change, scope, behavior, and manual test coverage.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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.

@xLaugh

xLaugh commented Aug 9, 2026

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
app/Filament/App/Resources/Servers/Pages/ListServers.php (1)

122-128: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add automated coverage for the pagination contract.

Add Livewire tests for fresh grid/list defaults, stored values after session expiry, valid session precedence, invalid values for each layout, and persistence without overwriting existing customization keys.

Also applies to: 158-176

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Filament/App/Resources/Servers/Pages/ListServers.php` around lines 122 -
128, Add Livewire coverage around the ListServers pagination configuration,
covering fresh grid/list defaults, stored values after session expiry, valid
session precedence, invalid stored values for both layouts, and persistence that
preserves unrelated customization keys. Anchor the tests to the pagination
behavior configured by the ListServers table and verify the expected page option
and customization outcomes for each scenario.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/Filament/App/Resources/Servers/Pages/ListServers.php`:
- Around line 169-173: Update the customization payload in the per-page
persistence flow around the user update to merge with the raw stored
customization data rather than user()->getCustomization(), which includes enum
defaults. Preserve existing stored values and update only
CustomizationKey::ServersPerPage, using an existing raw-data accessor or a
dedicated method if available.
- Around line 122-128: Update the pagination configuration in the table-building
method so Filament receives a layout-compatible session per-page value before
applying defaultPaginationPageOption(). Validate or normalize the stored value
against the current $pageOptions, ensuring incompatible grid values fall back to
the configured list default while preserving valid values for each layout.
- Around line 158-176: Update updatedTableRecordsPerPage() to validate the
selected value against the current pageOptions, accepting only an exact allowed
page-size value rather than any numeric input. Persist only
CustomizationKey::ServersPerPage, and reset the invalid selection/state so
subsequent loads use a valid default.

---

Nitpick comments:
In `@app/Filament/App/Resources/Servers/Pages/ListServers.php`:
- Around line 122-128: Add Livewire coverage around the ListServers pagination
configuration, covering fresh grid/list defaults, stored values after session
expiry, valid session precedence, invalid stored values for both layouts, and
persistence that preserves unrelated customization keys. Anchor the tests to the
pagination behavior configured by the ListServers table and verify the expected
page option and customization outcomes for each scenario.
🪄 Autofix

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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 35bc52d7-f736-440f-a6f1-a50ff9b646ff

📥 Commits

Reviewing files that changed from the base of the PR and between 32de07c and e37fc54.

📒 Files selected for processing (3)
  • app/Enums/CustomizationKey.php
  • app/Filament/App/Resources/Servers/Pages/ListServers.php
  • app/Models/User.php

Comment on lines +122 to +128
$pageOptions = $usingGrid ? [10, 20, 30, 40] : [10, 20, 50, 100];
$storedPerPage = (int) user()?->getCustomization(CustomizationKey::ServersPerPage);
$defaultPerPage = in_array($storedPerPage, $pageOptions, true) ? $storedPerPage : ($usingGrid ? 10 : 20);

return $table
->paginated($usingGrid ? [10, 20, 30, 40] : [10, 20, 50, 100])
->defaultPaginationPageOption($usingGrid ? 10 : 20)
->paginated($pageOptions)
->defaultPaginationPageOption($defaultPerPage)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate target file:"
fd -a 'ListServers.php' . || true

target="$(fd 'ListServers.php' . | head -n 1 || true)"
if [ -n "${target:-}" ]; then
  echo "Target: $target"
  wc -l "$target"
  sed -n '90,150p' "$target"
fi

echo
echo "Search pagination defaults/session hooks:"
rg -n "defaultPaginationPageOption|updatedTableRecordsPerPage|RecordsPerPage|paginationPageOption|perPage" -S app/Filament || true

echo
echo "Check Filament package version hints:"
for f in composer.lock composer.json; do
  [ -f "$f" ] && { echo "--- $f"; sed -n '/"filamentphp\/filament"\|version"/,/},/p' "$f" | head -80; }
done

Repository: pelican-dev/panel

Length of output: 5858


🏁 Script executed:

#!/bin/bash
set -euo pipefail

target="app/Filament/App/Resources/Servers/Pages/ListServers.php"
echo "--- $target outline"
ast-grep outline "$target" || true
echo
echo "--- $target relevant lines"
cat -n "$target" | sed -n '1,220p'

echo
echo "--- search for custom page option/session key in project"
rg -n "CustomizationKey::ServersPerPage|TablesRecordsPerPage|pageOptions|usingGrid|storageSession.*page|page_option|RecordsPerPage|paginationPageOption" -S app .github config || true

Repository: pelican-dev/panel

Length of output: 13605


🌐 Web query:

Filament v5.6 CanPaginateRecords resolve existing session value before defaultPaginationPageOption

💡 Result:

In Filament, the pagination records per page (the number of items shown) is resolved by the getDefaultTableRecordsPerPageSelectOption method within the CanPaginateRecords trait [1][2]. This logic prioritizes the value stored in the user's session before falling back to the configured default [2]. Specifically, the resolution order follows this hierarchy: 1. Session Value: The system first attempts to retrieve the stored per-page option from the user's session using a table-specific session key (e.g., tables.{table_class_name}_per_page) [1][2]. 2. Configured Default: If no session value exists, it falls back to the value defined by defaultPaginationPageOption() (if set) or the application-level default configuration [2]. The implementation typically looks like this [2]: public function getDefaultTableRecordsPerPageSelectOption: int | string { $option = session->get( $this->getTablePerPageSessionKey, $this->defaultTableRecordsPerPageSelectOption?? $this->getTable->getDefaultPaginationPageOption,); $pageOptions = $this->getTable->getPaginationPageOptions; // Verify the retrieved option is valid; if not, remove the invalid session entry if (in_array($option, $pageOptions)) { return $option; } session->remove($this->getTablePerPageSessionKey); return $pageOptions; } This ensures that once a user selects a preferred pagination size, it persists across requests via the session [2]. If the retrieved session value is no longer valid (e.g., the developer updated the available options), Filament will automatically remove the invalid session key and revert to the first available option [1][2]. Note that this mechanism governs the number of records displayed, which is distinct from the current page index (e.g., page 3 of 10), which is managed by Livewire via the URL query string [3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- vendor filament CanPaginateRecords if present:"
if [ -d vendor ]; then
  fd -a 'CanPaginateRecords.php' vendor | while read -r f; do
    echo "--- $f"
    sed -n '/function getDefaultTableRecordsPerPageSelectOption/,/}/p' "$f"
  done
else
  echo "vendor directory not present"
fi

echo
echo "--- composer filament dependency lines:"
sed -n '/filament/',/}/p' composer.json composer.lock 2>/dev/null | rg -n "filamentphp/filament|version|packages/tables/src/Concerns/CanPaginateRecords.php" -C 2 || true

Repository: pelican-dev/panel

Length of output: 337


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- composer relevant dependency lines:"
python3 - <<'PY'
import json, pathlib
p=json.loads(pathlib.Path('composer.lock').read_text())
for pkg in p.get('packages', [])+p.get('packages-dev', []):
    if pkg.get('name') == 'filamentphp/filament':
        print(json.dumps({k: pkg.get(k) for k in ('name','version','source','require')}, indent=2, sort_keys=True))
PY

echo
echo "--- fetch Filament tables trait with GitHub raw (version-independent source inspection):"
curl -fsSL 'https://raw.githubusercontent.com/filamentphp/filament/v5.6.0/packages/tables/src/Concerns/CanPaginateRecords.php' | sed -n '/function getDefaultTableRecordsPerPageSelectOption/,/}/p'

Repository: pelican-dev/panel

Length of output: 733


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- fetch rest of invalid session handling:"
curl -fsSL 'https://raw.githubusercontent.com/filamentphp/filament/v5.6.0/packages/tables/src/Concerns/CanPaginateRecords.php' | sed -n '/function getDefaultTableRecordsPerPageSelectOption/,/^    }/p'

echo
echo "--- fetch default session key implementation:"
curl -fsSL 'https://raw.githubusercontent.com/filamentphp/filament/v5.6.0/packages/tables/src/Concerns/CanPaginateRecords.php' | sed -n '/function getTablePerPageSessionKey/,/^    }/p'

Repository: pelican-dev/panel

Length of output: 945


Use the layout-specific default when the session value is incompatible.

Filament resolves the session-per-page value before defaultPaginationPageOption() and reuses the same session key for both table layouts. If a grid user selects 30 or 40, then switches to list view, the session value is outside [10, 20, 50, 100], so Filament removes it and falls back to 10 instead of the configured list default. Validate the session value against $pageOptions before letting Filament apply it, or use a layout-specific session key.

🧰 Tools
🪛 ast-grep (0.45.0)

[error] 125-128: Prevent SQL queries built from unsanitized input
Context: $table
->paginated($pageOptions)
->defaultPaginationPageOption($defaultPerPage)
->query(fn () => $baseQuery)
Note: [CWE-89] Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection').

(sql-injection-php)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Filament/App/Resources/Servers/Pages/ListServers.php` around lines 122 -
128, Update the pagination configuration in the table-building method so
Filament receives a layout-compatible session per-page value before applying
defaultPaginationPageOption(). Validate or normalize the stored value against
the current $pageOptions, ensuring incompatible grid values fall back to the
configured list default while preserving valid values for each layout.

Comment on lines +158 to +176
public function updatedTableRecordsPerPage(): void
{
parent::updatedTableRecordsPerPage();

$perPage = $this->getTableRecordsPerPage();

// Livewire delivers the <select> value as a numeric string, not an int.
if (!is_numeric($perPage)) {
return;
}

user()?->update([
'customization' => array_merge(
user()->getCustomization(),
[CustomizationKey::ServersPerPage->value => (int) $perPage],
),
]);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repository files around target:"
git ls-files | rg '(^|/)ListServers\.php$|composer\.json$|vendor/filamentphp/tables/src/Concerns/CanPaginateRecords\.php|vendor/filamentphp/forms/src/Components/Select\.php' || true

target=$(git ls-files | rg '(^|/)app/Filament/App/Resources/Servers/Pages/ListServers\.php$' | head -n1 || true)
if [ -n "$target" ]; then
  echo "Target: $target"
  wc -l "$target"
  sed -n '1,240p' "$target" | cat -n
fi

echo
echo "Composer Filament deps:"
if [ -f composer.json ]; then
  jq '.require // {} | with_entries(select(.key|test("filament"))) ' composer.json || true
fi

Repository: pelican-dev/panel

Length of output: 12214


🏁 Script executed:

#!/bin/bash
set -euo pipefail

target=$(git ls-files | grep '(^|/)app/Filament/App/Resources/Servers/Pages/ListServers.php$' | head -n1 || true)
if [ -n "$target" ]; then
  echo "Target exists: $target"
  ast-grep outline "$target" --view compact 2>/dev/null || true
  sed -n '130,190p' "$target" | cat -n
fi

echo
echo "Search for updatedTableRecordsPerPage usages/overloads:"
rg -n "updatedTableRecordsPerPage|getTableRecordsPerPage|pageOptions|paginate\\(" app composer.json -S || true

Repository: pelican-dev/panel

Length of output: 2693


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Behavioral probe for PHP numeric/string validation semantics referenced by the concern.
tmpdir=$(mktemp -d)
cd "$tmpdir"
php -r '
$vals = ["1000000","10.5","1e6",null,"abc"," 10 "];
foreach ($vals as $v) {
    $result = is_numeric($v);
    echo var_export($v,true) . " => is_numeric=" . var_export($result,true) . "\n";
}
' || true

Repository: pelican-dev/panel

Length of output: 312


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "composer.lock Filament version:"
python3 - <<'PY'
import json
from pathlib import Path
p=Path("composer.lock")
if p.exists():
    data=json.loads(p.read_text())
    for pkg in data.get("packages",[]):
        if pkg.get("name") in {"filament/filament","filament/tables","filament/widgets"}:
            print(pkg.get("name"), pkg.get("version"), pkg.get("source",{}).get("url"))
PY

echo
echo "Fetch Filament 5.6 pagination hook source from packagist CDN:"
url=$(python3 - <<'PY'
import json, urllib.request
from pathlib import Path
data=json.loads(urllib.request.urlopen("https://repo.packagist.org/p2/filament/tables.json", timeout=20).read())
links=[]
for n,v in data.get("packages",{}).get("filament/tables",{}).items():
    if n.startswith("v5."):
        links.append((n, v.get("version") or ""))
links=sorted(links)[-2:]
for n,v in links:
    ver=v or n
    print(n, ver)
PY
)
# fallback to v5.6 explicitly for source extraction
raw_url="https://raw.githubusercontent.com/filamentphp/filament/v5.6.0/packages/tables/src/Concerns/CanPaginateRecords.php"
echo "$raw_url"
curl -s "$raw_url" | sed -n '1,240p' | cat -n

echo
echo "Fetch table resource hook if present:"
raw_url2="https://raw.githubusercontent.com/filamentphp/filament/v5.6.0/packages/tables/src/Concerns/HasRecordsPerPageOption.php"
curl -s "$raw_url2" | sed -n '1,220p' | cat -n

Repository: pelican-dev/panel

Length of output: 2735


🌐 Web query:

GitHub filamentphp/filament v5.7.5 packages tables Concerns CanPaginateRecords updatedTableRecordsPerPage

💡 Result:

In Filament v5.7.5, the CanPaginateRecords trait is a core component within the filament/tables package used to manage table pagination logic [1]. Regarding your query about updatedTableRecordsPerPage: 1. Role of updatedTableRecordsPerPage: This method is a Livewire lifecycle hook triggered when a user changes the number of records per page in a table's pagination select input [1][2]. It is responsible for updating the pagination state, persisting this choice in the user's session, and resetting the current page to ensure the table display remains consistent [1][3]. 2. Integration with CanPaginateRecords: The CanPaginateRecords trait provides the necessary methods to handle the logic when updatedTableRecordsPerPage is called, including [1]: - getTableRecordsPerPage(): Retrieves the current records-per-page setting [1]. - getTablePerPageSessionKey(): Generates a unique session key for the table to persist the user's pagination preference [1]. - paginateTableQuery(): Uses the per-page setting (which may be an integer or the string 'all') to perform the actual query pagination [1][3]. 3. Usage: Developers typically do not need to interact with updatedTableRecordsPerPage directly unless they are overriding core pagination behavior. To customize pagination options, such as the available per-page values or the default value, you should use the paginated() and defaultPaginationPageOption() methods on the table configuration object within your table() method [4]. For example, to configure pagination options: php public function table(Table $table): Table { return $table ->paginated([10, 25, 50, 100, 'all']) ->defaultPaginationPageOption(25); } Filament automatically handles session persistence for the pagination state using the identifier returned by getTablePerPageSessionKey(), ensuring that the user's preference is remembered [1][3].

Citations:


🌐 Web query:

github raw filamentphp filament v5.7.5 packages tables src Concerns CanPaginateRecords.php updatedTableRecordsPerPage

💡 Result:

In Filament v5.7.5, the updatedTableRecordsPerPage method (or similar logic handling table records per page updates) within the CanPaginateRecords concern underwent security improvements regarding the validation of the per-page value [1]. Specifically, a commit titled "security: Validate live table records per page value" (e5d689b) modified packages/tables/src/Concerns/CanPaginateRecords.php on July 11, 2026, to ensure that the value provided for table records per page is properly validated [1]. This change prevents potential issues with insecure input when users interact with the pagination per-page select component. In the implementation of CanPaginateRecords, the method responsible for handling the update typically involves: 1. Validating the requested per-page value against the table's allowed pagination options [2][3]. 2. Updating the user's session with the new value using getTablePerPageSessionKey() [2][4][3]. 3. Resetting the table to the first page using resetPage() [4][3]. While the specific method name in your query might vary depending on the exact version or Livewire component trait usage, the core functionality for updating pagination records resides in this concern, ensuring that session storage and Livewire state remain synchronized and secure [2][1]. Filament v5.7.5 was released on July 31, 2026 [5][6].

Citations:


🌐 Web query:

raw.githubusercontent.com filamentphp filament v5.7.5 packages tables src Concerns CanPaginateRecords.php

💡 Result:

The file packages/tables/src/Concerns/CanPaginateRecords.php is a core trait within the Filament Tables package [1][2]. It provides the essential logic for managing record pagination, including configuration for pagination limits, page navigation, and query execution [1][2]. Key responsibilities and features of this trait include: - Pagination Execution: The paginateTableQuery() method handles the application of pagination to the table's database query [1][2]. It supports pagination modes such as LengthAware and is responsible for setting query limits based on the current per-page configuration [3][1]. - Per-Page Management: It manages how many records are displayed per page via methods like getTableRecordsPerPage() and getDefaultTableRecordsPerPageSelectOption(), which integrate with user sessions and configuration files to persist preferences [3][1]. - Integration with Livewire: The trait includes methods like resetPage() and getTablePaginationPageName() to ensure the table's pagination state is correctly synced with the underlying Livewire component [3][2]. - Customization: It provides hooks to customize pagination options, such as the available records-per-page select options (e.g., [5, 10, 25, 50, -1], where -1 often represents "all" records) [3]. As of August 9, 2026, the latest version of the Filament ecosystem is v5.7.5, released on July 31, 2026 [4][5]. While specific implementation details can evolve between major versions (from v2.x to v5.x), the core purpose of CanPaginateRecords remains constant within the Filament Tables architecture [3][1][2].

Citations:


🌐 Web query:

github raw ead6642f filamentphp filament v5.7.5 packages tablesConcerns CanPaginateRecords.php ? ref e7ea91f8a86929779bb7c5f4d66f32f39b581a

💡 Result:

The requested file CanPaginateRecords.php is located in the filamentphp/filament repository at packages/tables/src/Concerns/CanPaginateRecords.php [1]. The specific version of this file associated with the commit hash ead6642f (as referenced in your query) can be accessed directly on GitHub at the following URL: https://github.com/filamentphp/filament/blob/ead6642f/packages/tables/src/Concerns/CanPaginateRecords.php [1]. This trait is part of the Filament Tables package, which handles pagination logic for Livewire-based tables, including support for cursor pagination, configurable records per page, and pagination state management [1]. While Filament v5.7.5 is the current version of the package [2], code structures for core concerns like CanPaginateRecords are often maintained across specific development branches in the repository [1][3][4].

Citations:


🌐 Web query:

raw.githubusercontent.com filamentphp filament e7ea91f8a86929779bb7c5f4d66f32f39b581a packages tables src Concerns CanPaginateRecords.php

💡 Result:

The file packages/tables/src/Concerns/CanPaginateRecords.php is a core trait within the Filament PHP library's Table package [1][2]. Its primary purpose is to provide pagination functionality for table components, enabling them to transition from a Laravel Eloquent Builder query to a paginated result set [3][4]. Key responsibilities and features of this trait include: 1. Paginated Query Execution: It provides the paginateTableQuery() method, which handles the application of pagination limits (e.g., paginate, simplePaginate, or cursorPaginate) based on the table's configured pagination mode [3][4]. 2. Records Per Page Management: It manages the records-per-page logic, including support for an "all" option (which retrieves all records by executing a count query) and storing the user's preference in a session key specific to the table instance [1][5][4]. 3. UI Helper Methods: It provides various utility methods used by Filament's Table components for rendering and interaction, such as: - getTablePage(): Retrieves the current page number [4]. - getTablePaginationPageName(): Determines the query string parameter name for pagination (defaulting to "page") [1][4]. - getDefaultTableRecordsPerPageSelectOption(): Logic to determine the default pagination option based on session data or configuration [1][5][4]. Throughout the development of Filament (from 2.x to 3.x), this trait has been updated to support different pagination modes (Simple, Cursor, and LengthAware) and to resolve issues related to query counting, especially when complex queries involve GROUP BY or JOIN statements [6][4]. It is frequently involved in internal framework operations rather than being intended for direct modification by end-users [1][3].

Citations:


🌐 Web query:

GitHub filamentphp filament CanPaginateRecords updatedTableRecordsPerPage parent resetTable session GetTableRecordsPerPage SelectOptions

💡 Result:

In Filament PHP, table pagination and record counts are managed primarily through the CanPaginateRecords concern [1][2] and the Table configuration builder [3]. Key Mechanisms: 1. getTableRecordsPerPage: This method returns the currently selected number of records per page, typically retrieved from a Livewire property or session [1][2]. 2. Persistence: Filament persists the selected pagination choice in the user's session to ensure consistency across page reloads [1][2]. The session key is unique to the table class to prevent collisions, generated via getTablePerPageSessionKey [1][2]. 3. Session Logic: If a user selects a value that is no longer in the allowed options (e.g., after a developer changes the table configuration), getDefaultTableRecordsPerPageSelectOption validates the session value against current options [1][2]. If invalid, it clears the session and reverts to the first available option [1][2]. 4. Configuration: Developers typically customize these settings in the table method of their component rather than overriding internal methods directly [4][5][3]. Common Customizations: - Pagination Options: Use ->paginated([10, 25, 50, 'all']) in your table method to define available choices [3]. - Default Page Size: Use ->defaultPaginationPageOption(25) to set the initial number of records displayed [3]. - Resetting Pagination: While internal methods like resetPage exist for managing Livewire pagination states [1][4][2], these are generally invoked automatically by Filament when table filters or search states change [6]. If you need to perform advanced modifications (like custom pagination logic), you can override paginateTableQuery within your component to use simple or cursor pagination, as documented in Filament's advanced tables guide [7][3]. Sources: [1] https://github.com/filamentphp/filament/blob/3.x/packages/tables/src/Concerns/CanPaginateRecords.php [4] https://github.com/filamentphp/filament/blob/3.x/packages/tables/src/Concerns/InteractsWithTable.php [2] https://github.com/filamentphp/filament/blob/ead6642f/packages/tables/src/Concerns/CanPaginateRecords.php [5] https://github.com/filamentphp/filament/blob/ead6642f/packages/tables/src/Concerns/InteractsWithTable.php [7] https://filamentphp.com/docs/3.x/tables/advanced [6] https://github.com/filamentphp/filament/blob/2.x/packages/tables/src/Concerns/CanPaginateRecords.php [8] https://github.com/filamentphp/filament/blob/ead6642f/packages/tables/src/Contracts/HasTable.php [3] https://mintlify.wiki/filamentphp/filament/tables/overview

Citations:


🌐 Web query:

raw.githubusercontent.com filamentphp filament e7ea91f8a86929779bb7c5f4d66f32f39b581a packages tables src Concerns CanPaginateRecords.php | sed -n '1,240p'

💡 Result:

The file packages/tables/src/Concerns/CanPaginateRecords.php is a core trait within the Filament PHP package, specifically used within the tables system to manage pagination logic [1][2][3]. Purpose and Functionality: This trait is responsible for configuring and handling record pagination for tables [2][3]. It manages how the table transitions from a full query builder result to a paginated set, supporting different pagination modes such as LengthAware, Simple, and Cursor pagination [1][3]. Key Responsibilities: - Pagination Management: It provides the paginateTableQuery() method, which applies the necessary LIMIT and OFFSET (or cursor logic) to the query [2][3]. - Page Size Configuration: It tracks how many records to show per page using the $tableRecordsPerPage property and manages user-selected pagination options [1][4][3]. - Session Persistence: It handles storing and retrieving the user's preferred "records per page" setting in the session, allowing it to persist across navigation [1][4]. - URL Query String: It manages the pagination page number in the URL via getTablePaginationPageName(), which ensures that pagination state is reflected in the browser's address bar [1][4]. Evolution: The trait has undergone significant updates as Filament has evolved from version 2.x to 3.x [1][4]. Changes include improved type-hinting, support for different pagination modes (like Cursor), and internal refactoring to ensure better compatibility with Livewire and Filament's own widget system [1][5][6]. If you are inspecting this file, it is primarily used to control how data sets are broken into pages, how the "records per page" selector behaves, and how the underlying database queries are modified to fetch only the required page of data [1][7][3].

Citations:


🌐 Web query:

Github raw filamentphp filament v5.7.5 packages tables src Concerns CanPaginateRecords.php updatedTableRecordsPerPage getTableSelectOptionsForRecordsPerPage

💡 Result:

In Filament, the updatedTableRecordsPerPage method is a Livewire lifecycle hook used to handle changes when a user selects a different "records per page" option in a table [1][2]. When this method is triggered, it typically performs the following actions: 1. It updates the user's session to persist the chosen "records per page" preference [1][3]. 2. It resets the table pagination to the first page (via resetPage()) to ensure the user is not left on a page number that may no longer exist with the new record count [1]. Regarding your query about getTableSelectOptionsForRecordsPerPage and CanPaginateRecords.php: * The trait CanPaginateRecords manages pagination state and logic within Filament tables [4][1][5]. * While getTableRecordsPerPageSelectOptions (or similar variants) is commonly used to define the available options in the dropdown, the specific method name getTableSelectOptionsForRecordsPerPage does not appear as a standard public API in the modern Filament 3.x codebase [4][5]. Configuration for these options is typically handled via the table() method and the paginationPageOptions() configuration method on the Table object [3]. * In older versions (Filament v2.x), these options were often retrieved via the getTableRecordsPerPageSelectOptions method, which defaulted to reading from the configuration file [1]. If you are looking for how records are fetched for pagination, the paginateTableQuery method is responsible for applying the limit based on the selected per-page value (often handling the special '-1' value to represent "All" records by performing a count query) [4][5][6][2].

Citations:


Validate the page-size value before persisting it.

is_numeric() accepts client-mutable inputs like 1000000, 10.5, and 1e6. The table only allows [10, 20, 30, 40] for grid layouts and [10, 20, 50, 100] otherwise. Parse and validate the selected value against the current pageOptions, then persist only CustomizationKey::ServersPerPage; reset invalid state if needed so future loads use a valid default.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Filament/App/Resources/Servers/Pages/ListServers.php` around lines 158 -
176, Update updatedTableRecordsPerPage() to validate the selected value against
the current pageOptions, accepting only an exact allowed page-size value rather
than any numeric input. Persist only CustomizationKey::ServersPerPage, and reset
the invalid selection/state so subsequent loads use a valid default.

Comment on lines +169 to +173
user()?->update([
'customization' => array_merge(
user()->getCustomization(),
[CustomizationKey::ServersPerPage->value => (int) $perPage],
),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Merge into the raw customization data.

user()->getCustomization() returns enum defaults merged with stored values. For a fresh user, this includes top_navigation => 'sidebar', while User::$validationRules requires customization.top_navigation to be boolean. The model validation observer validates during saving, so this update can fail before servers_per_page is persisted. It also stores unrelated defaults as explicit user values. (raw.githubusercontent.com)

Merge into the raw stored customization array, or add a dedicated method that updates only CustomizationKey::ServersPerPage.

Suggested persistence shape
+        $rawCustomization = user()->getRawOriginal('customization');
+        $rawCustomization = is_string($rawCustomization)
+            ? json_decode($rawCustomization, true)
+            : $rawCustomization;
+
         user()?->update([
             'customization' => array_merge(
-                user()->getCustomization(),
+                is_array($rawCustomization) ? $rawCustomization : [],
                 [CustomizationKey::ServersPerPage->value => (int) $perPage],
📝 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
user()?->update([
'customization' => array_merge(
user()->getCustomization(),
[CustomizationKey::ServersPerPage->value => (int) $perPage],
),
$rawCustomization = user()->getRawOriginal('customization');
$rawCustomization = is_string($rawCustomization)
? json_decode($rawCustomization, true)
: $rawCustomization;
user()?->update([
'customization' => array_merge(
is_array($rawCustomization) ? $rawCustomization : [],
[CustomizationKey::ServersPerPage->value => (int) $perPage],
),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Filament/App/Resources/Servers/Pages/ListServers.php` around lines 169 -
173, Update the customization payload in the per-page persistence flow around
the user update to merge with the raw stored customization data rather than
user()->getCustomization(), which includes enum defaults. Preserve existing
stored values and update only CustomizationKey::ServersPerPage, using an
existing raw-data accessor or a dedicated method if available.

@Boy132

Boy132 commented Aug 10, 2026

Copy link
Copy Markdown
Member

I don't think we need an extra customization for that. The panel also stores other values in the session and that's fine.

Besides that, this issue is pretty niche because the default lifetime is 2 hours.

@xLaugh

xLaugh commented Aug 10, 2026

Copy link
Copy Markdown
Author

Fair point on consistency, but I'd frame this less as a timing edge case and more as a basic UX expectation: once someone deliberately changes this setting, they expect it to stick, that's the whole point of changing it.

As a concrete example, I have around 20 servers myself, so I bumped the per-page from 10 to 20 on purpose, to see them all on one page every time I load the dashboard. I don't want to have to re-select that every time my session happens to renew, I want it to just stay set, permanently, like any other preference. It's less about how rare a 2-hour session expiry is and more about the setting not actually behaving like a saved preference at all right now.

@lancepioch

Copy link
Copy Markdown
Member

I personally think this idea looks good to me, plus it can always be cached. I agree with the expectation that once a user sets this value, it should remain the same.

FYI, I have NOT run this branch, NOR have I tested it yet though.

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.

3 participants