Add basic server activity log view

This commit is contained in:
DaneEveritt 2022-06-12 15:16:48 -04:00
parent 0b4936ff1c
commit 2f1c8ae91d
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
8 changed files with 153 additions and 2 deletions

View file

@ -0,0 +1,28 @@
import useUserSWRContentKey from '@/plugins/useUserSWRContentKey';
import useSWR, { ConfigInterface, responseInterface } from 'swr';
import { ActivityLog, Transformers } from '@definitions/user';
import { AxiosError } from 'axios';
import http, { PaginatedResult, QueryBuilderParams, withQueryBuilderParams } from '@/api/http';
import { toPaginatedSet } from '@definitions/helpers';
import useFilteredObject from '@/plugins/useFilteredObject';
import { ServerContext } from '@/state/server';
export type ActivityLogFilters = QueryBuilderParams<'ip' | 'event', 'timestamp'>;
const useActivityLogs = (filters?: ActivityLogFilters, config?: ConfigInterface<PaginatedResult<ActivityLog>, AxiosError>): responseInterface<PaginatedResult<ActivityLog>, AxiosError> => {
const uuid = ServerContext.useStoreState(state => state.server.data?.uuid);
const key = useUserSWRContentKey([ 'server', 'activity', JSON.stringify(useFilteredObject(filters || {})) ]);
return useSWR<PaginatedResult<ActivityLog>>(key, async () => {
const { data } = await http.get(`/api/client/servers/${uuid}/activity`, {
params: {
...withQueryBuilderParams(filters),
include: [ 'actor' ],
},
});
return toPaginatedSet(data, Transformers.toActivityLog);
}, { revalidateOnMount: false, ...(config || {}) });
};
export { useActivityLogs };

View file

@ -19,7 +19,9 @@ export default ({ meta }: { meta: Record<string, unknown> }) => {
hideCloseIcon
title={'Metadata'}
>
<pre>{JSON.stringify(meta, null, 2)}</pre>
<pre className={'bg-gray-900 rounded p-2 overflow-x-scroll font-mono text-sm leading-relaxed'}>
{JSON.stringify(meta, null, 2)}
</pre>
<Dialog.Buttons>
<Button.Text onClick={() => setOpen(false)}>Close</Button.Text>
</Dialog.Buttons>

View file

@ -0,0 +1,65 @@
import React, { useEffect, useState } from 'react';
import { useActivityLogs } from '@/api/server/activity';
import ServerContentBlock from '@/components/elements/ServerContentBlock';
import { useFlashKey } from '@/plugins/useFlash';
import FlashMessageRender from '@/components/FlashMessageRender';
import Spinner from '@/components/elements/Spinner';
import ActivityLogEntry from '@/components/elements/activity/ActivityLogEntry';
import PaginationFooter from '@/components/elements/table/PaginationFooter';
import { ActivityLogFilters } from '@/api/account/activity';
import { Link } from 'react-router-dom';
import classNames from 'classnames';
import { styles as btnStyles } from '@/components/elements/button';
import { XCircleIcon } from '@heroicons/react/solid';
export default () => {
const { clearAndAddHttpError } = useFlashKey('server:activity');
const [ filters, setFilters ] = useState<ActivityLogFilters>({ page: 1, sorts: { timestamp: -1 } });
const { data, isValidating, error } = useActivityLogs(filters, {
revalidateOnMount: true,
revalidateOnFocus: false,
});
useEffect(() => {
const parsed = new URLSearchParams(location.search);
setFilters(value => ({ ...value, filters: { ip: parsed.get('ip'), event: parsed.get('event') } }));
}, [ location.search ]);
useEffect(() => {
clearAndAddHttpError(error);
}, [ error ]);
return (
<ServerContentBlock title={'Activity Log'}>
<FlashMessageRender byKey={'server:activity'}/>
{(filters.filters?.event || filters.filters?.ip) &&
<div className={'flex justify-end mb-2'}>
<Link
to={'#'}
className={classNames(btnStyles.button, btnStyles.text, 'w-full sm:w-auto')}
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'}>
{data?.items.map((activity) => (
<ActivityLogEntry key={activity.timestamp.toString() + activity.event} activity={activity}>
<span/>
</ActivityLogEntry>
))}
</div>
}
{data && <PaginationFooter
pagination={data.pagination}
onPageSelect={page => setFilters(value => ({ ...value, page }))}
/>}
</ServerContentBlock>
);
};

View file

@ -12,6 +12,7 @@ import AccountOverviewContainer from '@/components/dashboard/AccountOverviewCont
import AccountApiContainer from '@/components/dashboard/AccountApiContainer';
import AccountSSHContainer from '@/components/dashboard/ssh/AccountSSHContainer';
import ActivityLogContainer from '@/components/dashboard/activity/ActivityLogContainer';
import ServerActivityLogContainer from '@/components/server/ServerActivityLogContainer';
// Each of the router files is already code split out appropriately — so
// all of the items above will only be loaded in when that router is loaded.
@ -133,5 +134,11 @@ export default {
name: 'Settings',
component: SettingsContainer,
},
{
path: '/activity',
permission: 'activity.*',
name: 'Activity',
component: ServerActivityLogContainer,
},
],
} as Routes;