Skip to content

Commit

Permalink
Activity log page (#2774)
Browse files Browse the repository at this point in the history
* Add activity log retriveal network operation

* Add loading state capabilities to ActivityLogOverview

* Add ActivityLogPage

* Mount ActivityLogPage in the application
  • Loading branch information
nelsonkopliku authored Jul 24, 2024
1 parent 9b314a9 commit 68959f2
Show file tree
Hide file tree
Showing 9 changed files with 126 additions and 0 deletions.
2 changes: 2 additions & 0 deletions assets/js/common/ActivityLogOverview/ActivityLogOverview.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export const toRenderedEntry = (entry) => ({
function ActivityLogOverview({
activityLog,
activityLogDetailModalOpen = false,
loading = false,
onActivityLogEntryClick = noop,
onCloseActivityLogEntryDetails = noop,
}) {
Expand Down Expand Up @@ -156,6 +157,7 @@ function ActivityLogOverview({
<Table
config={activityLogTableConfig}
data={activityLog.map(toRenderedEntry)}
emptyStateText={loading ? 'Loading...' : 'No data available'}
/>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ export default {
type: 'array',
},
},
loading: {
description: 'Display loading state of the component',
control: { type: 'boolean' },
},
},
};

Expand All @@ -21,6 +25,13 @@ export const Default = {
},
};

export const Loading = {
args: {
loading: true,
activityLog: [],
},
};

export const Empty = {
args: {
activityLog: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ describe('Activity Log Overview', () => {
expect(screen.getByText('No data available')).toBeVisible();
});

it('should render a loading activity log', () => {
render(<ActivityLogOverview activityLog={[]} loading />);

expect(screen.getByText('Loading...')).toBeVisible();
});

const scenarios = [
{
name: LOGIN_ATTEMPT,
Expand Down
3 changes: 3 additions & 0 deletions assets/js/lib/api/activityLogs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { networkClient } from '@lib/network';

export const getActivityLog = () => networkClient.get(`/activity_log`);
41 changes: 41 additions & 0 deletions assets/js/pages/ActivityLogPage/ActivityLogPage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React, { useState, useEffect } from 'react';

import PageHeader from '@common/PageHeader';
import ActivityLogOverview from '@common/ActivityLogOverview';

import { getActivityLog } from '@lib/api/activityLogs';

function ActivityLogPage() {
const [activityLog, setActivityLog] = useState([]);
const [isLoading, setLoading] = useState(true);
const [activityLogDetailModalOpen, setActivityLogDetailModalOpen] =
useState(false);

useEffect(() => {
getActivityLog()
.then((response) => {
setActivityLog(response.data);
})
.catch(() => setActivityLog([]))
.finally(() => {
setLoading(false);
});
}, []);

return (
<>
<PageHeader className="font-bold">Activity Log</PageHeader>
<ActivityLogOverview
activityLogDetailModalOpen={activityLogDetailModalOpen}
activityLog={activityLog}
loading={isLoading}
onActivityLogEntryClick={() => setActivityLogDetailModalOpen(true)}
onCloseActivityLogEntryDetails={() =>
setActivityLogDetailModalOpen(false)
}
/>
</>
);
}

export default ActivityLogPage;
52 changes: 52 additions & 0 deletions assets/js/pages/ActivityLogPage/ActivityLogPage.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react';
import { act, screen } from '@testing-library/react';
import '@testing-library/jest-dom';

import MockAdapter from 'axios-mock-adapter';
import { renderWithRouter } from '@lib/test-utils';

import { networkClient } from '@lib/network';
import { activityLogEntryFactory } from '@lib/test-utils/factories/activityLog';

import ActivityLogPage from './ActivityLogPage';

const axiosMock = new MockAdapter(networkClient);

describe('ActivityLogPage', () => {
it('should render table without data', async () => {
axiosMock.onGet('/api/v1/activity_log').reply(200, []);
await act(async () => renderWithRouter(<ActivityLogPage />));
expect(screen.getByText('No data available')).toBeVisible();
});

it.each`
responseStatus | responseBody
${404} | ${[]}
${500} | ${{ error: 'Internal Server Error' }}
${503} | ${null}
${504} | ${''}
`(
'should render empty activity log on error `$responseStatus`',
async ({ responseStatus, responseBody }) => {
axiosMock
.onGet('/api/v1/activity_log')
.reply(responseStatus, responseBody);

await act(() => renderWithRouter(<ActivityLogPage />));

expect(screen.getByText('No data available')).toBeVisible();
}
);

it('should render tracked activity log', async () => {
axiosMock
.onGet('/api/v1/activity_log')
.reply(200, activityLogEntryFactory.buildList(5));

const { container } = await act(() =>
renderWithRouter(<ActivityLogPage />)
);

expect(container.querySelectorAll('tbody > tr')).toHaveLength(5);
});
});
3 changes: 3 additions & 0 deletions assets/js/pages/ActivityLogPage/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import ActivityLogPage from './ActivityLogPage';

export default ActivityLogPage;
6 changes: 6 additions & 0 deletions assets/js/pages/Layout/Layout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
EOS_KEYBOARD_DOUBLE_ARROW_LEFT,
EOS_KEYBOARD_DOUBLE_ARROW_RIGHT,
EOS_SUPERVISED_USER_CIRCLE_OUTLINED,
EOS_ASSIGNMENT,
} from 'eos-icons-react';

import TrentoLogo from '@static/trento-logo-stacked.svg';
Expand Down Expand Up @@ -59,6 +60,11 @@ const navigation = [
icon: EOS_SUPERVISED_USER_CIRCLE_OUTLINED,
permittedFor: ['all:users'],
},
{
name: 'Activity Log',
href: '/activity_log',
icon: EOS_ASSIGNMENT,
},
{
name: 'Settings',
href: '/settings',
Expand Down
2 changes: 2 additions & 0 deletions assets/js/trento.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import SettingsPage from '@pages/SettingsPage';
import SomethingWentWrong from '@pages/SomethingWentWrong';
import UsersPage, { CreateUserPage, EditUserPage } from '@pages/Users';
import ProfilePage from '@pages/Profile';
import ActivityLogPage from '@pages/ActivityLogPage';

import { profile } from '@lib/auth';
import { networkClient } from '@lib/network';
Expand Down Expand Up @@ -113,6 +114,7 @@ const createRouter = ({ getUser }) =>
path="hosts/:hostID/patches/:advisoryID"
element={<AdvisoryDetailsPage />}
/>
<Route path="activity_log" element={<ActivityLogPage />} />
<Route
element={
<ForbiddenGuard permitted={['all:users']} outletMode />
Expand Down

0 comments on commit 68959f2

Please sign in to comment.