feat: audit logs on organization-level

This commit is contained in:
Daniel Hougaard
2024-09-08 19:19:55 +04:00
parent 8d66272ab2
commit ad644db512
4 changed files with 78 additions and 40 deletions
@@ -2,6 +2,7 @@ import { useState } from "react";
import { Control, Controller, UseFormReset } from "react-hook-form"; import { Control, Controller, UseFormReset } from "react-hook-form";
import { faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons"; import { faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { Button, DatePicker, FormControl, Select, SelectItem } from "@app/components/v2"; import { Button, DatePicker, FormControl, Select, SelectItem } from "@app/components/v2";
import { useWorkspace } from "@app/context"; import { useWorkspace } from "@app/context";
@@ -19,11 +20,13 @@ const userAgentTypes = Object.entries(userAgentTTypeoNameMap).map(([value, label
})); }));
type Props = { type Props = {
presetActor?: string;
className?: string;
control: Control<AuditLogFilterFormData>; control: Control<AuditLogFilterFormData>;
reset: UseFormReset<AuditLogFilterFormData>; reset: UseFormReset<AuditLogFilterFormData>;
}; };
export const LogsFilter = ({ control, reset }: Props) => { export const LogsFilter = ({ presetActor, className, control, reset }: Props) => {
const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false); const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false); const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false);
@@ -69,8 +72,13 @@ export const LogsFilter = ({ control, reset }: Props) => {
}; };
return ( return (
<div className="sticky top-20 z-10 flex items-center justify-between bg-bunker-800"> <div
<div className="flex items-center"> className={twMerge(
"sticky top-20 z-10 flex items-center justify-between bg-bunker-800",
className
)}
>
<div className="flex items-center space-x-2">
<Controller <Controller
control={control} control={control}
name="eventType" name="eventType"
@@ -79,7 +87,7 @@ export const LogsFilter = ({ control, reset }: Props) => {
label="Event" label="Event"
errorText={error?.message} errorText={error?.message}
isError={Boolean(error)} isError={Boolean(error)}
className="mr-4 w-40" className="w-40"
> >
<Select <Select
{...(field.value ? { value: field.value } : { placeholder: "Select" })} {...(field.value ? { value: field.value } : { placeholder: "Select" })}
@@ -96,7 +104,7 @@ export const LogsFilter = ({ control, reset }: Props) => {
</FormControl> </FormControl>
)} )}
/> />
{!isLoading && data && data.length > 0 && ( {!isLoading && data && data.length > 0 && !presetActor && (
<Controller <Controller
control={control} control={control}
name="actor" name="actor"
@@ -105,7 +113,7 @@ export const LogsFilter = ({ control, reset }: Props) => {
label="Actor" label="Actor"
errorText={error?.message} errorText={error?.message}
isError={Boolean(error)} isError={Boolean(error)}
className="mr-4 w-40" className="w-40"
> >
<Select <Select
{...(field.value ? { value: field.value } : { placeholder: "Select" })} {...(field.value ? { value: field.value } : { placeholder: "Select" })}
@@ -127,7 +135,7 @@ export const LogsFilter = ({ control, reset }: Props) => {
label="Source" label="Source"
errorText={error?.message} errorText={error?.message}
isError={Boolean(error)} isError={Boolean(error)}
className="mr-4 w-40" className="w-40"
> >
<Select <Select
{...(field.value ? { value: field.value } : { placeholder: "Select" })} {...(field.value ? { value: field.value } : { placeholder: "Select" })}
@@ -149,12 +157,7 @@ export const LogsFilter = ({ control, reset }: Props) => {
control={control} control={control}
render={({ field: { onChange, ...field }, fieldState: { error } }) => { render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return ( return (
<FormControl <FormControl label="Start date" errorText={error?.message} isError={Boolean(error)}>
label="Start date"
errorText={error?.message}
isError={Boolean(error)}
className="mr-4"
>
<DatePicker <DatePicker
value={field.value || undefined} value={field.value || undefined}
onChange={(date) => { onChange={(date) => {
@@ -195,17 +198,17 @@ export const LogsFilter = ({ control, reset }: Props) => {
}} }}
/> />
</div> </div>
<div>
<Button <Button
isLoading={false} isLoading={false}
colorSchema="primary" colorSchema="primary"
variant="outline_bg" variant="outline_bg"
className="mt-1.5"
type="submit" type="submit"
leftIcon={<FontAwesomeIcon icon={faFilterCircleXmark} className="mr-2" />} leftIcon={<FontAwesomeIcon icon={faFilterCircleXmark} />}
onClick={() => onClick={() =>
reset({ reset({
eventType: undefined, eventType: undefined,
actor: undefined, actor: presetActor,
userAgentType: undefined, userAgentType: undefined,
startDate: undefined, startDate: undefined,
endDate: undefined endDate: undefined
@@ -215,6 +218,5 @@ export const LogsFilter = ({ control, reset }: Props) => {
Clear filters Clear filters
</Button> </Button>
</div> </div>
</div>
); );
}; };
@@ -12,7 +12,19 @@ import { LogsFilter } from "./LogsFilter";
import { LogsTable } from "./LogsTable"; import { LogsTable } from "./LogsTable";
import { AuditLogFilterFormData, auditLogFilterFormSchema } from "./types"; import { AuditLogFilterFormData, auditLogFilterFormSchema } from "./types";
export const LogsSection = () => { type Props = {
presetActor?: string;
showFilters?: boolean;
filterClassName?: string;
isOrgAuditLogs?: boolean;
};
export const LogsSection = ({
presetActor,
filterClassName,
isOrgAuditLogs,
showFilters
}: Props) => {
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const router = useRouter(); const router = useRouter();
@@ -21,6 +33,7 @@ export const LogsSection = () => {
const { control, reset, watch } = useForm<AuditLogFilterFormData>({ const { control, reset, watch } = useForm<AuditLogFilterFormData>({
resolver: yupResolver(auditLogFilterFormSchema), resolver: yupResolver(auditLogFilterFormSchema),
defaultValues: { defaultValues: {
actor: presetActor,
page: 1, page: 1,
perPage: 10, perPage: 10,
startDate: new Date(new Date().setDate(new Date().getDate() - 1)), // day before today startDate: new Date(new Date().setDate(new Date().getDate() - 1)), // day before today
@@ -43,8 +56,16 @@ export const LogsSection = () => {
return ( return (
<div> <div>
<LogsFilter control={control} reset={reset} /> {showFilters && (
<LogsFilter
className={filterClassName}
presetActor={presetActor}
control={control}
reset={reset}
/>
)}
<LogsTable <LogsTable
isOrgAuditLogs={isOrgAuditLogs}
eventType={eventType} eventType={eventType}
userAgentType={userAgentType} userAgentType={userAgentType}
actor={actor} actor={actor}
@@ -25,15 +25,22 @@ type Props = {
actor?: string; actor?: string;
startDate?: Date; startDate?: Date;
endDate?: Date; endDate?: Date;
isOrgAuditLogs?: boolean;
}; };
const AUDIT_LOG_LIMIT = 15; const AUDIT_LOG_LIMIT = 15;
export const LogsTable = ({ eventType, userAgentType, actor, startDate, endDate }: Props) => { export const LogsTable = ({
eventType,
userAgentType,
actor,
startDate,
endDate,
isOrgAuditLogs
}: Props) => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } = useGetAuditLogs( const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } = useGetAuditLogs(
currentWorkspace?.id ?? "",
{ {
eventType, eventType,
userAgentType, userAgentType,
@@ -41,7 +48,8 @@ export const LogsTable = ({ eventType, userAgentType, actor, startDate, endDate
startDate, startDate,
endDate, endDate,
limit: AUDIT_LOG_LIMIT limit: AUDIT_LOG_LIMIT
} },
!isOrgAuditLogs ? currentWorkspace?.id ?? "" : null
); );
const isEmpty = !isLoading && !data?.pages?.[0].length; const isEmpty = !isLoading && !data?.pages?.[0].length;
@@ -54,6 +62,7 @@ export const LogsTable = ({ eventType, userAgentType, actor, startDate, endDate
<Tr> <Tr>
<Th>Timestamp</Th> <Th>Timestamp</Th>
<Th>Event</Th> <Th>Event</Th>
{isOrgAuditLogs && <Th>Project</Th>}
<Th>Actor</Th> <Th>Actor</Th>
<Th>Source</Th> <Th>Source</Th>
<Th>Metadata</Th> <Th>Metadata</Th>
@@ -64,7 +73,11 @@ export const LogsTable = ({ eventType, userAgentType, actor, startDate, endDate
data?.pages?.map((group, i) => ( data?.pages?.map((group, i) => (
<Fragment key={`auditlog-item-${i + 1}`}> <Fragment key={`auditlog-item-${i + 1}`}>
{group.map((auditLog) => ( {group.map((auditLog) => (
<LogsTableRow auditLog={auditLog} key={`audit-log-${auditLog.id}`} /> <LogsTableRow
isOrgAuditLogs={isOrgAuditLogs}
auditLog={auditLog}
key={`audit-log-${auditLog.id}`}
/>
))} ))}
</Fragment> </Fragment>
))} ))}
@@ -5,9 +5,10 @@ import { Actor, AuditLog, Event } from "@app/hooks/api/auditLogs/types";
type Props = { type Props = {
auditLog: AuditLog; auditLog: AuditLog;
isOrgAuditLogs?: boolean;
}; };
export const LogsTableRow = ({ auditLog }: Props) => { export const LogsTableRow = ({ auditLog, isOrgAuditLogs }: Props) => {
const renderActor = (actor: Actor) => { const renderActor = (actor: Actor) => {
switch (actor.type) { switch (actor.type) {
case ActorType.USER: case ActorType.USER:
@@ -469,6 +470,7 @@ export const LogsTableRow = ({ auditLog }: Props) => {
<Tr className={`log-${auditLog.id} h-10 border-x-0 border-b border-t-0`}> <Tr className={`log-${auditLog.id} h-10 border-x-0 border-b border-t-0`}>
<Td>{formatDate(auditLog.createdAt)}</Td> <Td>{formatDate(auditLog.createdAt)}</Td>
<Td>{`${eventToNameMap[auditLog.event.type]}`}</Td> <Td>{`${eventToNameMap[auditLog.event.type]}`}</Td>
{isOrgAuditLogs && <Td>{auditLog.project.name}</Td>}
{renderActor(auditLog.actor)} {renderActor(auditLog.actor)}
<Td> <Td>
<p>{userAgentTTypeoNameMap[auditLog.userAgentType]}</p> <p>{userAgentTTypeoNameMap[auditLog.userAgentType]}</p>