Break out rows for activity; show metadata icon

This commit is contained in:
DaneEveritt 2022-06-12 15:08:26 -04:00
parent 33823b65de
commit 0b4936ff1c
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
4 changed files with 125 additions and 67 deletions

View file

@ -21,7 +21,7 @@ class ActivityLogController extends ClientApiController
AllowedFilter::partial('event'), AllowedFilter::partial('event'),
]) ])
->allowedSorts(['timestamp']) ->allowedSorts(['timestamp'])
->paginate(min($request->query('per_page', 50), 100)) ->paginate(min($request->query('per_page', 25), 100))
->appends($request->query()); ->appends($request->query());
return $this->fractal->collection($activity) return $this->fractal->collection($activity)

View file

@ -3,17 +3,15 @@ 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';
import { format, formatDistanceToNowStrict } from 'date-fns';
import { Link } from 'react-router-dom'; 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 Tooltip from '@/components/elements/tooltip/Tooltip';
import { DesktopComputerIcon, XCircleIcon } from '@heroicons/react/solid'; import { DesktopComputerIcon, XCircleIcon } from '@heroicons/react/solid';
import { useLocation } from 'react-router'; import { useLocation } from 'react-router';
import Spinner from '@/components/elements/Spinner'; import Spinner from '@/components/elements/Spinner';
import { styles as btnStyles } from '@/components/elements/button/index'; import { styles as btnStyles } from '@/components/elements/button/index';
import classNames from 'classnames'; import classNames from 'classnames';
import Translate from '@/components/elements/Translate'; import ActivityLogEntry from '@/components/elements/activity/ActivityLogEntry';
import Tooltip from '@/components/elements/tooltip/Tooltip';
export default () => { export default () => {
const location = useLocation(); const location = useLocation();
@ -35,15 +33,6 @@ export default () => {
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'}/>
@ -63,59 +52,13 @@ export default () => {
: :
<div className={'bg-gray-700'}> <div className={'bg-gray-700'}>
{data?.items.map((activity) => ( {data?.items.map((activity) => (
<div <ActivityLogEntry key={activity.timestamp.toString() + activity.event} activity={activity}>
key={`${activity.event}|${activity.timestamp.toString()}`} {typeof activity.properties.useragent === 'string' &&
className={'grid grid-cols-10 py-4 border-b-2 border-gray-800 last:rounded-b last:border-0'} <Tooltip content={activity.properties.useragent} placement={'top'}>
> <DesktopComputerIcon className={'ml-2 w-4 h-4 cursor-pointer'}/>
<div className={'col-span-2 sm:col-span-1 flex items-center justify-center select-none'}> </Tooltip>
<div className={'flex items-center w-8 h-8 rounded-full bg-gray-600 overflow-hidden'}> }
{activity.relationships.actor ? </ActivityLogEntry>
<img src={activity.relationships.actor.image} alt={'User avatar'}/>
:
<UserIcon className={'w-5 h-5 mx-auto'}/>
}
</div>
</div>
<div className={'col-span-8 sm:col-span-9'}>
<div className={'flex items-center text-gray-50'}>
{activity.relationships.actor?.username || 'system'}
<span className={'text-gray-400'}>&nbsp;&mdash;&nbsp;</span>
<Link
to={`?${queryTo({ event: activity.event })}`}
className={'transition-colors duration-75 active:text-cyan-400 hover:text-cyan-400'}
>
{activity.event}
</Link>
{typeof activity.properties.useragent === 'string' &&
<Tooltip content={activity.properties.useragent} placement={'top'}>
<DesktopComputerIcon className={'ml-2 w-4 h-4 cursor-pointer'}/>
</Tooltip>
}
</div>
<p className={'mt-1 text-sm break-words line-clamp-2 pr-4'}>
<Translate ns={'activity'} values={activity.properties}>
{activity.event.replace(':', '.')}
</Translate>
</p>
<div className={'mt-1 flex items-center text-sm'}>
<Link
to={`?${queryTo({ ip: activity.ip })}`}
className={'transition-colors duration-75 active:text-cyan-400 hover:text-cyan-400'}
>
{activity.ip}
</Link>
<span className={'text-gray-400'}>&nbsp;|&nbsp;</span>
<Tooltip
placement={'right'}
content={format(activity.timestamp, 'MMM do, yyyy h:mma')}
>
<span>
{formatDistanceToNowStrict(activity.timestamp, { addSuffix: true })}
</span>
</Tooltip>
</div>
</div>
</div>
))} ))}
</div> </div>
} }

View file

@ -0,0 +1,80 @@
import React from 'react';
import { UserIcon } from '@heroicons/react/outline';
import { Link } from 'react-router-dom';
import Tooltip from '@/components/elements/tooltip/Tooltip';
import Translate from '@/components/elements/Translate';
import { format, formatDistanceToNowStrict } from 'date-fns';
import { ActivityLog } from '@definitions/user';
import { useLocation } from 'react-router';
import ActivityLogMetaButton from '@/components/elements/activity/ActivityLogMetaButton';
interface Props {
activity: ActivityLog;
children?: React.ReactNode;
}
export default ({ activity, children }: Props) => {
const location = useLocation();
const actor = activity.relationships.actor;
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 (
<div className={'grid grid-cols-10 py-4 border-b-2 border-gray-800 last:rounded-b last:border-0 group'}>
<div className={'col-span-2 sm:col-span-1 flex items-center justify-center select-none'}>
<div className={'flex items-center w-8 h-8 rounded-full bg-gray-600 overflow-hidden'}>
{actor ?
<img src={actor.image} alt={'User avatar'}/>
:
<UserIcon className={'w-5 h-5 mx-auto'}/>
}
</div>
</div>
<div className={'col-span-8 sm:col-span-9 flex'}>
<div className={'flex-1'}>
<div className={'flex items-center text-gray-50'}>
<Tooltip placement={'top'} content={actor?.email || 'System User'}>
<span>{actor?.username || 'System'}</span>
</Tooltip>
<span className={'text-gray-400'}>&nbsp;&mdash;&nbsp;</span>
<Link
to={`?${queryTo({ event: activity.event })}`}
className={'transition-colors duration-75 active:text-cyan-400 hover:text-cyan-400'}
>
{activity.event}
</Link>
{children}
</div>
<p className={'mt-1 text-sm break-words line-clamp-2 pr-4'}>
<Translate ns={'activity'} values={activity.properties}>
{activity.event.replace(':', '.')}
</Translate>
</p>
<div className={'mt-1 flex items-center text-sm'}>
<Link
to={`?${queryTo({ ip: activity.ip })}`}
className={'transition-colors duration-75 active:text-cyan-400 hover:text-cyan-400'}
>
{activity.ip}
</Link>
<span className={'text-gray-400'}>&nbsp;|&nbsp;</span>
<Tooltip
placement={'right'}
content={format(activity.timestamp, 'MMM do, yyyy h:mma')}
>
<span>
{formatDistanceToNowStrict(activity.timestamp, { addSuffix: true })}
</span>
</Tooltip>
</div>
</div>
<ActivityLogMetaButton meta={activity.properties}/>
</div>
</div>
);
};

View file

@ -0,0 +1,35 @@
import React, { useState } from 'react';
import { isEmptyObject } from '@/helpers';
import { ClipboardListIcon } from '@heroicons/react/outline';
import { Dialog } from '@/components/elements/dialog';
import { Button } from '@/components/elements/button/index';
export default ({ meta }: { meta: Record<string, unknown> }) => {
const [ open, setOpen ] = useState(false);
if (isEmptyObject(meta)) {
return null;
}
return (
<div className={'self-center mx-4'}>
<Dialog
open={open}
onClose={() => setOpen(false)}
hideCloseIcon
title={'Metadata'}
>
<pre>{JSON.stringify(meta, null, 2)}</pre>
<Dialog.Buttons>
<Button.Text onClick={() => setOpen(false)}>Close</Button.Text>
</Dialog.Buttons>
</Dialog>
<button
className={'p-2 transition-colors duration-100 text-gray-400 group-hover:text-gray-300 group-hover:hover:text-gray-50'}
onClick={() => setOpen(true)}
>
<ClipboardListIcon className={'w-5 h-5'}/>
</button>
</div>
);
};