Skip to content

Commit 0bfba30

Browse files
committed
Add filtering support for activity logs
1 parent c6e8b89 commit 0bfba30

File tree

3 files changed

+99
-48
lines changed

3 files changed

+99
-48
lines changed

resources/scripts/api/account/activity.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,19 @@ import useUserSWRContentKey from '@/plugins/useUserSWRContentKey';
22
import useSWR, { ConfigInterface, responseInterface } from 'swr';
33
import { ActivityLog, Transformers } from '@definitions/user';
44
import { AxiosError } from 'axios';
5-
import http, { PaginatedResult } from '@/api/http';
5+
import http, { PaginatedResult, QueryBuilderParams, withQueryBuilderParams } from '@/api/http';
66
import { toPaginatedSet } from '@definitions/helpers';
77

8-
const useActivityLogs = (page = 1, config?: ConfigInterface<PaginatedResult<ActivityLog>, AxiosError>): responseInterface<PaginatedResult<ActivityLog>, AxiosError> => {
9-
const key = useUserSWRContentKey([ 'account', 'activity', page.toString() ]);
8+
export type ActivityLogFilters = QueryBuilderParams<'ip' | 'event', 'timestamp'>;
9+
10+
const useActivityLogs = (filters?: ActivityLogFilters, config?: ConfigInterface<PaginatedResult<ActivityLog>, AxiosError>): responseInterface<PaginatedResult<ActivityLog>, AxiosError> => {
11+
const key = useUserSWRContentKey([ 'account', 'activity', JSON.stringify(filters) ]);
1012

1113
return useSWR<PaginatedResult<ActivityLog>>(key, async () => {
1214
const { data } = await http.get('/api/client/account/activity', {
1315
params: {
16+
...withQueryBuilderParams(filters),
1417
include: [ 'actor' ],
15-
sort: '-timestamp',
16-
page: page,
1718
},
1819
});
1920

resources/scripts/api/http.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export function getPaginationSet (data: any): PaginationDataSet {
117117
type QueryBuilderFilterValue = string | number | boolean | null;
118118

119119
export interface QueryBuilderParams<FilterKeys extends string = string, SortKeys extends string = string> {
120+
page?: number;
120121
filters?: {
121122
[K in FilterKeys]?: QueryBuilderFilterValue | Readonly<QueryBuilderFilterValue[]>;
122123
};
@@ -150,6 +151,7 @@ export const withQueryBuilderParams = (data?: QueryBuilderParams): Record<string
150151

151152
return {
152153
...filters,
153-
sorts: !sorts.length ? undefined : sorts.join(','),
154+
sort: !sorts.length ? undefined : sorts.join(','),
155+
page: data.page,
154156
};
155157
};
Lines changed: 90 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useEffect, useState } from 'react';
2-
import { useActivityLogs } from '@/api/account/activity';
2+
import { ActivityLogFilters, useActivityLogs } from '@/api/account/activity';
33
import { useFlashKey } from '@/plugins/useFlash';
44
import PageContentBlock from '@/components/elements/PageContentBlock';
55
import FlashMessageRender from '@/components/FlashMessageRender';
@@ -8,66 +8,114 @@ import { Link } from 'react-router-dom';
88
import PaginationFooter from '@/components/elements/table/PaginationFooter';
99
import { UserIcon } from '@heroicons/react/outline';
1010
import Tooltip from '@/components/elements/tooltip/Tooltip';
11-
import { DesktopComputerIcon } from '@heroicons/react/solid';
11+
import { DesktopComputerIcon, XCircleIcon } from '@heroicons/react/solid';
12+
import { useLocation } from 'react-router';
13+
import Spinner from '@/components/elements/Spinner';
14+
import { styles as btnStyles } from '@/components/elements/button/index';
15+
import classNames from 'classnames';
1216

1317
export default () => {
18+
const location = useLocation();
1419
const { clearAndAddHttpError } = useFlashKey('account');
15-
const [ page, setPage ] = useState(1);
16-
const { data, isValidating: _, error } = useActivityLogs(page, {
20+
const [ filters, setFilters ] = useState<ActivityLogFilters>({ page: 1, sorts: { timestamp: -1 } });
21+
const { data, isValidating, error } = useActivityLogs(filters, {
1722
revalidateOnMount: true,
1823
revalidateOnFocus: false,
1924
});
2025

26+
useEffect(() => {
27+
const parsed = new URLSearchParams(location.search);
28+
29+
setFilters(value => ({ ...value, filters: { ip: parsed.get('ip'), event: parsed.get('event') } }));
30+
}, [ location.search ]);
31+
2132
useEffect(() => {
2233
clearAndAddHttpError(error);
2334
}, [ error ]);
2435

36+
const queryTo = (params: Record<string, string>): string => {
37+
const current = new URLSearchParams(location.search);
38+
Object.keys(params).forEach(key => {
39+
current.set(key, params[key]);
40+
});
41+
42+
return current.toString();
43+
};
44+
2545
return (
2646
<PageContentBlock title={'Account Activity Log'}>
2747
<FlashMessageRender byKey={'account'}/>
28-
<div className={'bg-gray-700'}>
29-
{data?.items.map((activity) => (
30-
<div
31-
key={`${activity.event}|${activity.timestamp.toString()}`}
32-
className={'grid grid-cols-10 py-4 border-b-2 border-gray-800 last:rounded-b last:border-0'}
48+
{(filters.filters?.event || filters.filters?.ip) &&
49+
<div className={'flex justify-end mb-2'}>
50+
<Link
51+
to={'#'}
52+
className={classNames(btnStyles.button, btnStyles.text)}
53+
onClick={() => setFilters(value => ({ ...value, filters: {} }))}
3354
>
34-
<div className={'col-span-1 flex items-center justify-center select-none'}>
35-
<div className={'flex items-center w-8 h-8 rounded-full bg-gray-600 overflow-hidden'}>
36-
{activity.relationships.actor ?
37-
<img src={activity.relationships.actor.image} alt={'User avatar'}/>
38-
:
39-
<UserIcon className={'w-5 h-5 mx-auto'}/>
40-
}
55+
Clear Filters <XCircleIcon className={'w-4 h-4 ml-2'}/>
56+
</Link>
57+
</div>
58+
}
59+
{!data && isValidating ?
60+
<Spinner centered/>
61+
:
62+
<div className={'bg-gray-700'}>
63+
{data?.items.map((activity) => (
64+
<div
65+
key={`${activity.event}|${activity.timestamp.toString()}`}
66+
className={'grid grid-cols-10 py-4 border-b-2 border-gray-800 last:rounded-b last:border-0'}
67+
>
68+
<div className={'col-span-1 flex items-center justify-center select-none'}>
69+
<div className={'flex items-center w-8 h-8 rounded-full bg-gray-600 overflow-hidden'}>
70+
{activity.relationships.actor ?
71+
<img src={activity.relationships.actor.image} alt={'User avatar'}/>
72+
:
73+
<UserIcon className={'w-5 h-5 mx-auto'}/>
74+
}
75+
</div>
4176
</div>
42-
</div>
43-
<div className={'col-span-9'}>
44-
<div className={'flex items-center text-gray-50'}>
45-
{activity.relationships.actor?.username || 'system'}
46-
<span className={'text-gray-400'}>&nbsp;&mdash;&nbsp;</span>
47-
<Link to={`#event=${activity.event}`}>
48-
{activity.event}
49-
</Link>
50-
{typeof activity.properties.useragent === 'string' &&
51-
<Tooltip content={activity.properties.useragent} placement={'top'}>
52-
<DesktopComputerIcon className={'ml-2 w-4 h-4 cursor-pointer'}/>
77+
<div className={'col-span-9'}>
78+
<div className={'flex items-center text-gray-50'}>
79+
{activity.relationships.actor?.username || 'system'}
80+
<span className={'text-gray-400'}>&nbsp;&mdash;&nbsp;</span>
81+
<Link
82+
to={`?${queryTo({ event: activity.event })}`}
83+
className={'transition-colors duration-75 hover:text-cyan-400'}
84+
>
85+
{activity.event}
86+
</Link>
87+
{typeof activity.properties.useragent === 'string' &&
88+
<Tooltip content={activity.properties.useragent} placement={'top'}>
89+
<DesktopComputerIcon className={'ml-2 w-4 h-4 cursor-pointer'}/>
90+
</Tooltip>
91+
}
92+
</div>
93+
<div className={'mt-1 flex items-center text-sm'}>
94+
<Link
95+
to={`?${queryTo({ ip: activity.ip })}`}
96+
className={'transition-colors duration-75 hover:text-cyan-400'}
97+
>
98+
{activity.ip}
99+
</Link>
100+
<span className={'text-gray-400'}>&nbsp;|&nbsp;</span>
101+
<Tooltip
102+
placement={'right'}
103+
content={format(activity.timestamp, 'MMM do, yyyy h:mma')}
104+
>
105+
<span>
106+
{formatDistanceToNowStrict(activity.timestamp, { addSuffix: true })}
107+
</span>
53108
</Tooltip>
54-
}
55-
</div>
56-
{/* <p className={'mt-1'}>{activity.description || JSON.stringify(activity.properties)}</p> */}
57-
<div className={'mt-1 flex items-center text-sm'}>
58-
<Link to={`#ip=${activity.ip}`}>{activity.ip}</Link>
59-
<span className={'text-gray-400'}>&nbsp;|&nbsp;</span>
60-
<Tooltip placement={'right'} content={format(activity.timestamp, 'MMM do, yyyy h:mma')}>
61-
<span>
62-
{formatDistanceToNowStrict(activity.timestamp, { addSuffix: true })}
63-
</span>
64-
</Tooltip>
109+
</div>
65110
</div>
66111
</div>
67-
</div>
68-
))}
69-
</div>
70-
{data && <PaginationFooter pagination={data.pagination} onPageSelect={setPage}/>}
112+
))}
113+
</div>
114+
}
115+
{data && <PaginationFooter
116+
pagination={data.pagination}
117+
onPageSelect={page => setFilters(value => ({ ...value, page }))}
118+
/>}
71119
</PageContentBlock>
72120
);
73121
};

0 commit comments

Comments
 (0)