Activity log
View on GitHubSurface the activity log in the UI
ActivityLogService::paginateForUser() already powers the Dashboard's
"Recent Work Activity" panel, and paginateForProject() already
powers a project's Activity issue view (see
../issue-views/README.mdIssue views (List / Board / Calendar / Activity)A project's issues render as a sortable table, a status-grouped Kanban board, a calendar laid out by issue dates, or a chronological activity feed — the sa…) — both render
the same ActivityLogs/ActivityLogItem component pair off the same
ActivityLogEntry shape. This guide adds a third surface: a "Recent
account activity" panel on the Account → Security & access settings
tab, reusing the same repository query as the Dashboard rather than adding a
new one. It uses the unpaginated getRecentForUser() because the panel
is a short fixed list; if a surface needs paging, call
paginateForUser() instead and render the shared Pagination
molecule with the activityPage/activityPerPage params, as the
Dashboard does.
Step 1 — Thread the read method into the Security & access action
File: app/Http/Controllers/SettingsController.php
public function securityAccess(Request $request): Response
{
$user = $request->user();
return Inertia::render('Settings/SecurityAccess', [
'projects' => $this->projects($request),
'sessions' => $this->userService->getUserSessions($user),
'accountActivity' => $this->activityLogService
->getRecentForUser($user->id, 15)
->map(fn ($entry) => [
'id' => $entry->id,
'body' => $entry->body,
'userName' => $entry->user?->name,
'createdAt' => $entry->created_at->diffForHumans(),
]),
]);
}
ActivityLogService is already injected into this Controller for
other settings features, so no constructor change is needed — just
call the method it already exposes. Map to the same
{ id, body, userName, createdAt } shape DashboardController and
ProjectController already use (createdAt pre-formatted server-side
via diffForHumans()) so the frontend doesn't need a second shape or
formatter for what's the same ActivityLogEntry type.
Step 2 — Pass it down to the tab
File: resources/js/Pages/Settings/SecurityAccess.tsx
interface SettingsSecurityAccessProps {
projects?: Project[];
sessions?: Session[];
accountActivity?: ActivityLogEntry[];
}
export default function SettingsSecurityAccess({
projects = [],
sessions = [],
accountActivity = [],
}: SettingsSecurityAccessProps) {
return (
<SettingsLayout tabId="security-access" projects={projects}>
<AccountSettingsSecurityTab
sessions={sessions}
accountActivity={accountActivity}
/>
</SettingsLayout>
);
}
Each settings tab is its own page (see
../settings-tabs/README.mdSettings tabs/settings is not one page — every tab under it (Preferences, Profile, Notifications, Security & access, Export in the Account section; Labels, Issue Types,…), so there
is no dispatcher to thread the prop through — the page hands it
straight to AccountSettingsSecurityTab, the same way sessions
already does today.
Step 3 — Render it with the existing components
File: resources/js/Components/Organisms/AccountSettingsContent/AccountSettingsSecurityTab.tsx
import ActivityLogs from '@/Components/Organisms/ActivityLogs/ActivityLogs';
import { ActivityLogEntry } from '@/types/ActivityLog';
interface AccountSettingsSecurityTabProps {
sessions?: Session[];
accountActivity?: ActivityLogEntry[];
}
export default function AccountSettingsSecurityTab({
sessions = [],
accountActivity = [],
}: AccountSettingsSecurityTabProps) {
return (
<div className="space-y-5">
{/* ...existing Password/Active sessions/Session expiry panels... */}
<SettingsPanel
title="Recent account activity"
description="The last actions taken on your account, across every project."
icon="Activity"
>
<div className="px-2 pb-2">
<ActivityLogs logs={accountActivity} />
</div>
</SettingsPanel>
{/* ...existing Delete account panel... */}
</div>
);
}
No new list/empty-state/icon-coloring code — ActivityLogs already
handles an empty array (its own empty state) and ActivityLogItem
already derives each entry's colored dot from its body text via
getActivityLogVisual() (resources/js/utils/activityLog.ts). This is
the payoff of the Dashboard and project Activity view already sharing
one component pair instead of each rolling its own list markup — a
third surface is a data-plumbing change, not a new UI.
Tests
tests/Feature/SettingsControllerTest.php— add a case assertingGET /settings/security-accessincludes anaccountActivityprop shaped like{ id, body, userName, createdAt }, mirroring howeverDashboardControllerTest(if one exists) already assertsactivityLogs.resources/js/Pages/Settings/SecurityAccess.test.tsx— add a case assertingaccountActivityreachesAccountSettingsSecurityTab.resources/js/Components/Organisms/AccountSettingsContent/AccountSettingsSecurityTab.test.tsx— add a case rendering with a couple ofaccountActivityentries and asserting they show up, plus a case with an empty array assertingActivityLogs's empty state renders.
