[PULSE-13] Org Audit Log UI with advanced filtering #10
Reference in New Issue
Block a user
No description provided.
Delete Branch "staging"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Work Item
PULSE-13
Summary
Changes
OrganizationSettings.tsx: Added the Audit Log tab, data table, and filter grid logic.lib/api/audit.ts: Created API client for fetching and filtering audit logs.Test Plan
[ ] Navigate to Organization Settings -> Audit log.
[ ] Verify the table loads with recent events.
[ ] Test the "Log ID" filter with a known ID (e.g.,
8a2b3c).[ ] Test the "Action" filter (e.g.,
site_created) and Date Range inputs.[ ] Verify that pagination works correctly.
Greptile Overview
Greptile Summary
Added a comprehensive audit log feature to Organization Settings with filtering and pagination capabilities. The implementation uses a trigger-based approach to coordinate filter changes and pagination, ensuring proper data fetching without loops. All previously identified issues have been addressed:
auditFetchTriggerstatehandleTabChangeloadAuditfrom debounced effect dependenciesuseEffectfor ref updates removedThe audit log displays actor, action, resource type, and timestamp for each entry with support for filtering by Log ID, action type, and date range.
Confidence Score: 4/5
Important Files Changed
Sequence Diagram
2 files reviewed, 2 comments
Edit Code Review Agent Settings | Greptile
Tab state desync
activeTabis initialized fromsearchParams, and there’s an effect to sync when the URL changes, but clicking the sidebar buttons only callssetActiveTab(...)and never updates?tab=in the URL. This means the URL can remain on a different tab, breaking deep-linking/back/forward navigation consistency (e.g., refresh brings you back to the old tab from the URL, not the one you clicked).Prompt To Fix With AI
Filters don't trigger reload
In the Audit Log tab, changing
auditActionFilter,auditLogIdFilter,auditStartDate, orauditEndDatedoes not actually refetch data becauseloadAudit()is only invoked by theuseEffectthat depends onactiveTab/currentOrgId/loadAudit(andloadAudit’s identity doesn’t change when its deps change). As a result, the UI shows updated filter inputs but still displays stale table data until the user changes tabs or pagination. Consider invokingloadAudit()when any filter changes (and resettingauditPageas needed).Prompt To Fix With AI
Created a handleTabChange function that updates both the local activeTab state and the URL query parameter (?tab=...).
Updated all sidebar navigation buttons to use this handler, ensuring that clicking a tab updates the URL, preserving state across refreshes and navigation.
Introduced a debounced useEffect that listens to changes in auditActionFilter, auditLogIdFilter, auditStartDate, and auditEndDate.
When any filter changes, it now waits 500ms (debounce) and then automatically triggers loadAudit() and resets the page to 0.
Refactored loadAudit to use a useRef for filter values, ensuring the function identity remains stable while always accessing the latest state. This prevents excessive re-renders while ensuring data freshness.
2 files reviewed, 2 comments
Edit Code Review Agent Settings | Greptile
Additional Comments (2)
components/settings/OrganizationSettings.tsxURL tab state desync
Sidebar tab buttons call
setActiveTab(...)directly (e.g.onClick={() => setActiveTab('general')}), but nothing updates the?tab=query param. SinceactiveTabis initialized/synced fromuseSearchParams()(components/settings/OrganizationSettings.tsx:44-47and:155-160), the URL and UI can drift (refresh/back/forward can land on a different tab than the one last clicked). You likely want a single tab-change handler that updates both state and the URL, and use it for all tab buttons.Also appears at: components/settings/OrganizationSettings.tsx:333-356 (members/billing/audit buttons).
Prompt To Fix With AI
components/settings/OrganizationSettings.tsxFilter + pagination inconsistency
loadAudit()computesoffsetfromauditPage(offset: auditPage * auditPageSize). When a user changes filters while on a later page,auditPageis not reset to 0, so the request can legitimately return an empty page even though matching results exist on earlier pages. This will happen in normal use (e.g., go to page 3 then narrowAction). Consider resettingauditPageto 0 on any filter change (not just when clicking “Clear Filters”).Prompt To Fix With AI
2 files reviewed, 3 comments
Edit Code Review Agent Settings | Greptile
@@ -153,6 +183,46 @@ export default function OrganizationSettings() {}}, [activeTab, currentOrgId, loadSubscription, loadInvoices])infinite loop risk:
loadAuditin dependency arraysince
loadAuditdepends onauditPage(line 208), changing filters triggers the debounced effect, which callsloadAudit()and resetsauditPageto 0 (line 215). this causesloadAuditto be recreated, potentially re-triggering the effectremove
loadAuditfrom dependencies since you're already usingfiltersRefto access latest values:Prompt To Fix With AI
double API call when page changes
when user clicks pagination,
setAuditPagetriggers this effect AND the debounced filter effect (211-219) sinceloadAuditis in both dependency arrays. causes 2 API callsadd
auditPageto the debounced effect's dependencies and remove this separate effectPrompt To Fix With AI
@@ -0,0 +1,47 @@/**auditFetchbypasses the standard client's token refreshlib/api/client.tshas automatic token refresh on 401 (lines 103-173). this custom fetch duplicates error handling but lacks that retry mechanism. if the access token expires, audit log calls fail instead of auto-refreshinguse
apiRequestfromclient.tsinstead (note: may need to adjust endpoint format to match)Prompt To Fix With AI
@@ -0,0 +1,47 @@/**Replaced the custom auditFetch with the standard apiRequest from lib/api/client.ts, which includes automatic token refresh logic.
Crucial Update in client.ts: Modified apiRequest to support legacy endpoints starting with /api/ (like /api/audit) without double-prepending /api/v1, ensuring compatibility with the backend.
@@ -153,6 +183,46 @@ export default function OrganizationSettings() {}}, [activeTab, currentOrgId, loadSubscription, loadInvoices])Fix:
Introduced a auditFetchTrigger state to reliably trigger fetches when filters change, even if the page number remains 0.
Debounced Effect: Removed loadAudit from dependencies (fixing the loop). Now it only resets the page and increments the trigger.
Fetch Effect: Merged the pagination logic. It now listens to loadAudit (which changes on page change) AND auditFetchTrigger (which changes on filter change).
@@ -153,6 +183,46 @@ export default function OrganizationSettings() {}}, [activeTab, currentOrgId, loadSubscription, loadInvoices])Fix:
Introduced a auditFetchTrigger state to reliably trigger fetches when filters change, even if the page number remains 0.
Debounced Effect: Removed loadAudit from dependencies (fixing the loop). Now it only resets the page and increments the trigger.
Fetch Effect: Merged the pagination logic. It now listens to loadAudit (which changes on page change) AND auditFetchTrigger (which changes on filter change).
2 files reviewed, 2 comments
Edit Code Review Agent Settings | Greptile
@@ -77,6 +79,34 @@ export default function OrganizationSettings() {const [orgSlug, setOrgSlug] = useState('')this
useEffectis unnecessary - refs can be updated directly during render without causing issuesPrompt To Fix With AI
if filters are already empty and
auditPageis 0, clicking Clear Filters won't trigger a refetch (no state changes = no debounced effect)Prompt To Fix With AI
@@ -77,6 +79,34 @@ export default function OrganizationSettings() {const [orgSlug, setOrgSlug] = useState('')Issue: Unnecessary useEffect for updating filtersRef.
Fix: Removed the useEffect and now updating filtersRef.current synchronously during the render cycle.
Why: Refs can be safely updated during render for this use case, avoiding an extra effect hook and simplifying the component lifecycle.
Issue: "Clear Filters" button doesn't trigger a refetch if filters are already empty and page is 0 (no state change).
Fix: Added setAuditFetchTrigger(prev => prev + 1) to the onClick handler.
Why: This forces the useEffect dependent on auditFetchTrigger to run, ensuring the audit log reloads even if filter state values remain unchanged.