Add filtering support for activity logs
This commit is contained in:
parent
c6e8b893c8
commit
0bfba306bf
3 changed files with 99 additions and 48 deletions
|
@ -2,18 +2,19 @@ import useUserSWRContentKey from '@/plugins/useUserSWRContentKey';
|
||||||
import useSWR, { ConfigInterface, responseInterface } from 'swr';
|
import useSWR, { ConfigInterface, responseInterface } from 'swr';
|
||||||
import { ActivityLog, Transformers } from '@definitions/user';
|
import { ActivityLog, Transformers } from '@definitions/user';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import http, { PaginatedResult } from '@/api/http';
|
import http, { PaginatedResult, QueryBuilderParams, withQueryBuilderParams } from '@/api/http';
|
||||||
import { toPaginatedSet } from '@definitions/helpers';
|
import { toPaginatedSet } from '@definitions/helpers';
|
||||||
|
|
||||||
const useActivityLogs = (page = 1, config?: ConfigInterface<PaginatedResult<ActivityLog>, AxiosError>): responseInterface<PaginatedResult<ActivityLog>, AxiosError> => {
|
export type ActivityLogFilters = QueryBuilderParams<'ip' | 'event', 'timestamp'>;
|
||||||
const key = useUserSWRContentKey([ 'account', 'activity', page.toString() ]);
|
|
||||||
|
const useActivityLogs = (filters?: ActivityLogFilters, config?: ConfigInterface<PaginatedResult<ActivityLog>, AxiosError>): responseInterface<PaginatedResult<ActivityLog>, AxiosError> => {
|
||||||
|
const key = useUserSWRContentKey([ 'account', 'activity', JSON.stringify(filters) ]);
|
||||||
|
|
||||||
return useSWR<PaginatedResult<ActivityLog>>(key, async () => {
|
return useSWR<PaginatedResult<ActivityLog>>(key, async () => {
|
||||||
const { data } = await http.get('/api/client/account/activity', {
|
const { data } = await http.get('/api/client/account/activity', {
|
||||||
params: {
|
params: {
|
||||||
|
...withQueryBuilderParams(filters),
|
||||||
include: [ 'actor' ],
|
include: [ 'actor' ],
|
||||||
sort: '-timestamp',
|
|
||||||
page: page,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -117,6 +117,7 @@ export function getPaginationSet (data: any): PaginationDataSet {
|
||||||
type QueryBuilderFilterValue = string | number | boolean | null;
|
type QueryBuilderFilterValue = string | number | boolean | null;
|
||||||
|
|
||||||
export interface QueryBuilderParams<FilterKeys extends string = string, SortKeys extends string = string> {
|
export interface QueryBuilderParams<FilterKeys extends string = string, SortKeys extends string = string> {
|
||||||
|
page?: number;
|
||||||
filters?: {
|
filters?: {
|
||||||
[K in FilterKeys]?: QueryBuilderFilterValue | Readonly<QueryBuilderFilterValue[]>;
|
[K in FilterKeys]?: QueryBuilderFilterValue | Readonly<QueryBuilderFilterValue[]>;
|
||||||
};
|
};
|
||||||
|
@ -150,6 +151,7 @@ export const withQueryBuilderParams = (data?: QueryBuilderParams): Record<string
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...filters,
|
...filters,
|
||||||
sorts: !sorts.length ? undefined : sorts.join(','),
|
sort: !sorts.length ? undefined : sorts.join(','),
|
||||||
|
page: data.page,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useActivityLogs } from '@/api/account/activity';
|
import { ActivityLogFilters, useActivityLogs } from '@/api/account/activity';
|
||||||
import { useFlashKey } from '@/plugins/useFlash';
|
import { useFlashKey } from '@/plugins/useFlash';
|
||||||
import PageContentBlock from '@/components/elements/PageContentBlock';
|
import PageContentBlock from '@/components/elements/PageContentBlock';
|
||||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||||
|
@ -8,23 +8,57 @@ import { Link } from 'react-router-dom';
|
||||||
import PaginationFooter from '@/components/elements/table/PaginationFooter';
|
import PaginationFooter from '@/components/elements/table/PaginationFooter';
|
||||||
import { UserIcon } from '@heroicons/react/outline';
|
import { UserIcon } from '@heroicons/react/outline';
|
||||||
import Tooltip from '@/components/elements/tooltip/Tooltip';
|
import Tooltip from '@/components/elements/tooltip/Tooltip';
|
||||||
import { DesktopComputerIcon } from '@heroicons/react/solid';
|
import { DesktopComputerIcon, XCircleIcon } from '@heroicons/react/solid';
|
||||||
|
import { useLocation } from 'react-router';
|
||||||
|
import Spinner from '@/components/elements/Spinner';
|
||||||
|
import { styles as btnStyles } from '@/components/elements/button/index';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
|
const location = useLocation();
|
||||||
const { clearAndAddHttpError } = useFlashKey('account');
|
const { clearAndAddHttpError } = useFlashKey('account');
|
||||||
const [ page, setPage ] = useState(1);
|
const [ filters, setFilters ] = useState<ActivityLogFilters>({ page: 1, sorts: { timestamp: -1 } });
|
||||||
const { data, isValidating: _, error } = useActivityLogs(page, {
|
const { data, isValidating, error } = useActivityLogs(filters, {
|
||||||
revalidateOnMount: true,
|
revalidateOnMount: true,
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const parsed = new URLSearchParams(location.search);
|
||||||
|
|
||||||
|
setFilters(value => ({ ...value, filters: { ip: parsed.get('ip'), event: parsed.get('event') } }));
|
||||||
|
}, [ location.search ]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
clearAndAddHttpError(error);
|
clearAndAddHttpError(error);
|
||||||
}, [ error ]);
|
}, [ error ]);
|
||||||
|
|
||||||
|
const queryTo = (params: Record<string, string>): string => {
|
||||||
|
const current = new URLSearchParams(location.search);
|
||||||
|
Object.keys(params).forEach(key => {
|
||||||
|
current.set(key, params[key]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return current.toString();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContentBlock title={'Account Activity Log'}>
|
<PageContentBlock title={'Account Activity Log'}>
|
||||||
<FlashMessageRender byKey={'account'}/>
|
<FlashMessageRender byKey={'account'}/>
|
||||||
|
{(filters.filters?.event || filters.filters?.ip) &&
|
||||||
|
<div className={'flex justify-end mb-2'}>
|
||||||
|
<Link
|
||||||
|
to={'#'}
|
||||||
|
className={classNames(btnStyles.button, btnStyles.text)}
|
||||||
|
onClick={() => setFilters(value => ({ ...value, filters: {} }))}
|
||||||
|
>
|
||||||
|
Clear Filters <XCircleIcon className={'w-4 h-4 ml-2'}/>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{!data && isValidating ?
|
||||||
|
<Spinner centered/>
|
||||||
|
:
|
||||||
<div className={'bg-gray-700'}>
|
<div className={'bg-gray-700'}>
|
||||||
{data?.items.map((activity) => (
|
{data?.items.map((activity) => (
|
||||||
<div
|
<div
|
||||||
|
@ -44,7 +78,10 @@ export default () => {
|
||||||
<div className={'flex items-center text-gray-50'}>
|
<div className={'flex items-center text-gray-50'}>
|
||||||
{activity.relationships.actor?.username || 'system'}
|
{activity.relationships.actor?.username || 'system'}
|
||||||
<span className={'text-gray-400'}> — </span>
|
<span className={'text-gray-400'}> — </span>
|
||||||
<Link to={`#event=${activity.event}`}>
|
<Link
|
||||||
|
to={`?${queryTo({ event: activity.event })}`}
|
||||||
|
className={'transition-colors duration-75 hover:text-cyan-400'}
|
||||||
|
>
|
||||||
{activity.event}
|
{activity.event}
|
||||||
</Link>
|
</Link>
|
||||||
{typeof activity.properties.useragent === 'string' &&
|
{typeof activity.properties.useragent === 'string' &&
|
||||||
|
@ -53,11 +90,18 @@ export default () => {
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
{/* <p className={'mt-1'}>{activity.description || JSON.stringify(activity.properties)}</p> */}
|
|
||||||
<div className={'mt-1 flex items-center text-sm'}>
|
<div className={'mt-1 flex items-center text-sm'}>
|
||||||
<Link to={`#ip=${activity.ip}`}>{activity.ip}</Link>
|
<Link
|
||||||
|
to={`?${queryTo({ ip: activity.ip })}`}
|
||||||
|
className={'transition-colors duration-75 hover:text-cyan-400'}
|
||||||
|
>
|
||||||
|
{activity.ip}
|
||||||
|
</Link>
|
||||||
<span className={'text-gray-400'}> | </span>
|
<span className={'text-gray-400'}> | </span>
|
||||||
<Tooltip placement={'right'} content={format(activity.timestamp, 'MMM do, yyyy h:mma')}>
|
<Tooltip
|
||||||
|
placement={'right'}
|
||||||
|
content={format(activity.timestamp, 'MMM do, yyyy h:mma')}
|
||||||
|
>
|
||||||
<span>
|
<span>
|
||||||
{formatDistanceToNowStrict(activity.timestamp, { addSuffix: true })}
|
{formatDistanceToNowStrict(activity.timestamp, { addSuffix: true })}
|
||||||
</span>
|
</span>
|
||||||
|
@ -67,7 +111,11 @@ export default () => {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{data && <PaginationFooter pagination={data.pagination} onPageSelect={setPage}/>}
|
}
|
||||||
|
{data && <PaginationFooter
|
||||||
|
pagination={data.pagination}
|
||||||
|
onPageSelect={page => setFilters(value => ({ ...value, page }))}
|
||||||
|
/>}
|
||||||
</PageContentBlock>
|
</PageContentBlock>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue