Break out rows for activity; show metadata icon
This commit is contained in:
parent
33823b65de
commit
0b4936ff1c
4 changed files with 125 additions and 67 deletions
|
@ -21,7 +21,7 @@ class ActivityLogController extends ClientApiController
|
|||
AllowedFilter::partial('event'),
|
||||
])
|
||||
->allowedSorts(['timestamp'])
|
||||
->paginate(min($request->query('per_page', 50), 100))
|
||||
->paginate(min($request->query('per_page', 25), 100))
|
||||
->appends($request->query());
|
||||
|
||||
return $this->fractal->collection($activity)
|
||||
|
|
|
@ -3,17 +3,15 @@ import { ActivityLogFilters, useActivityLogs } from '@/api/account/activity';
|
|||
import { useFlashKey } from '@/plugins/useFlash';
|
||||
import PageContentBlock from '@/components/elements/PageContentBlock';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import { format, formatDistanceToNowStrict } from 'date-fns';
|
||||
import { Link } from 'react-router-dom';
|
||||
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 { useLocation } from 'react-router';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
import { styles as btnStyles } from '@/components/elements/button/index';
|
||||
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 () => {
|
||||
const location = useLocation();
|
||||
|
@ -35,15 +33,6 @@ export default () => {
|
|||
clearAndAddHttpError(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 (
|
||||
<PageContentBlock title={'Account Activity Log'}>
|
||||
<FlashMessageRender byKey={'account'}/>
|
||||
|
@ -63,59 +52,13 @@ export default () => {
|
|||
:
|
||||
<div className={'bg-gray-700'}>
|
||||
{data?.items.map((activity) => (
|
||||
<div
|
||||
key={`${activity.event}|${activity.timestamp.toString()}`}
|
||||
className={'grid grid-cols-10 py-4 border-b-2 border-gray-800 last:rounded-b last:border-0'}
|
||||
>
|
||||
<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'}>
|
||||
{activity.relationships.actor ?
|
||||
<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'}> — </span>
|
||||
<Link
|
||||
to={`?${queryTo({ event: activity.event })}`}
|
||||
className={'transition-colors duration-75 active:text-cyan-400 hover:text-cyan-400'}
|
||||
>
|
||||
{activity.event}
|
||||
</Link>
|
||||
<ActivityLogEntry key={activity.timestamp.toString() + activity.event} activity={activity}>
|
||||
{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'}> | </span>
|
||||
<Tooltip
|
||||
placement={'right'}
|
||||
content={format(activity.timestamp, 'MMM do, yyyy h:mma')}
|
||||
>
|
||||
<span>
|
||||
{formatDistanceToNowStrict(activity.timestamp, { addSuffix: true })}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ActivityLogEntry>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -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'}> — </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'}> | </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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
Loading…
Reference in a new issue