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

View File

@@ -2,6 +2,7 @@ import { useState } from "react";
import { Control, Controller, UseFormReset } from "react-hook-form";
import { faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { Button, DatePicker, FormControl, Select, SelectItem } from "@app/components/v2";
import { useWorkspace } from "@app/context";
@@ -19,11 +20,13 @@ const userAgentTypes = Object.entries(userAgentTTypeoNameMap).map(([value, label
}));
type Props = {
presetActor?: string;
className?: string;
control: Control<AuditLogFilterFormData>;
reset: UseFormReset<AuditLogFilterFormData>;
};
export const LogsFilter = ({ control, reset }: Props) => {
export const LogsFilter = ({ presetActor, className, control, reset }: Props) => {
const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false);
@@ -69,8 +72,13 @@ export const LogsFilter = ({ control, reset }: Props) => {
};
return (
<div className="sticky top-20 z-10 flex items-center justify-between bg-bunker-800">
<div className="flex items-center">
<div
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
control={control}
name="eventType"
@@ -79,7 +87,7 @@ export const LogsFilter = ({ control, reset }: Props) => {
label="Event"
errorText={error?.message}
isError={Boolean(error)}
className="mr-4 w-40"
className="w-40"
>
<Select
{...(field.value ? { value: field.value } : { placeholder: "Select" })}
@@ -96,7 +104,7 @@ export const LogsFilter = ({ control, reset }: Props) => {
</FormControl>
)}
/>
{!isLoading && data && data.length > 0 && (
{!isLoading && data && data.length > 0 && !presetActor && (
<Controller
control={control}
name="actor"
@@ -105,7 +113,7 @@ export const LogsFilter = ({ control, reset }: Props) => {
label="Actor"
errorText={error?.message}
isError={Boolean(error)}
className="mr-4 w-40"
className="w-40"
>
<Select
{...(field.value ? { value: field.value } : { placeholder: "Select" })}
@@ -127,7 +135,7 @@ export const LogsFilter = ({ control, reset }: Props) => {
label="Source"
errorText={error?.message}
isError={Boolean(error)}
className="mr-4 w-40"
className="w-40"
>
<Select
{...(field.value ? { value: field.value } : { placeholder: "Select" })}
@@ -149,12 +157,7 @@ export const LogsFilter = ({ control, reset }: Props) => {
control={control}
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl
label="Start date"
errorText={error?.message}
isError={Boolean(error)}
className="mr-4"
>
<FormControl label="Start date" errorText={error?.message} isError={Boolean(error)}>
<DatePicker
value={field.value || undefined}
onChange={(date) => {
@@ -195,26 +198,25 @@ export const LogsFilter = ({ control, reset }: Props) => {
}}
/>
</div>
<div>
<Button
isLoading={false}
colorSchema="primary"
variant="outline_bg"
type="submit"
leftIcon={<FontAwesomeIcon icon={faFilterCircleXmark} className="mr-2" />}
onClick={() =>
reset({
eventType: undefined,
actor: undefined,
userAgentType: undefined,
startDate: undefined,
endDate: undefined
})
}
>
Clear filters
</Button>
</div>
<Button
isLoading={false}
colorSchema="primary"
variant="outline_bg"
className="mt-1.5"
type="submit"
leftIcon={<FontAwesomeIcon icon={faFilterCircleXmark} />}
onClick={() =>
reset({
eventType: undefined,
actor: presetActor,
userAgentType: undefined,
startDate: undefined,
endDate: undefined
})
}
>
Clear filters
</Button>
</div>
);
};

View File

@@ -12,7 +12,19 @@ import { LogsFilter } from "./LogsFilter";
import { LogsTable } from "./LogsTable";
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 router = useRouter();
@@ -21,6 +33,7 @@ export const LogsSection = () => {
const { control, reset, watch } = useForm<AuditLogFilterFormData>({
resolver: yupResolver(auditLogFilterFormSchema),
defaultValues: {
actor: presetActor,
page: 1,
perPage: 10,
startDate: new Date(new Date().setDate(new Date().getDate() - 1)), // day before today
@@ -43,8 +56,16 @@ export const LogsSection = () => {
return (
<div>
<LogsFilter control={control} reset={reset} />
{showFilters && (
<LogsFilter
className={filterClassName}
presetActor={presetActor}
control={control}
reset={reset}
/>
)}
<LogsTable
isOrgAuditLogs={isOrgAuditLogs}
eventType={eventType}
userAgentType={userAgentType}
actor={actor}

View File

@@ -25,15 +25,22 @@ type Props = {
actor?: string;
startDate?: Date;
endDate?: Date;
isOrgAuditLogs?: boolean;
};
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 { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } = useGetAuditLogs(
currentWorkspace?.id ?? "",
{
eventType,
userAgentType,
@@ -41,7 +48,8 @@ export const LogsTable = ({ eventType, userAgentType, actor, startDate, endDate
startDate,
endDate,
limit: AUDIT_LOG_LIMIT
}
},
!isOrgAuditLogs ? currentWorkspace?.id ?? "" : null
);
const isEmpty = !isLoading && !data?.pages?.[0].length;
@@ -54,6 +62,7 @@ export const LogsTable = ({ eventType, userAgentType, actor, startDate, endDate
<Tr>
<Th>Timestamp</Th>
<Th>Event</Th>
{isOrgAuditLogs && <Th>Project</Th>}
<Th>Actor</Th>
<Th>Source</Th>
<Th>Metadata</Th>
@@ -64,7 +73,11 @@ export const LogsTable = ({ eventType, userAgentType, actor, startDate, endDate
data?.pages?.map((group, i) => (
<Fragment key={`auditlog-item-${i + 1}`}>
{group.map((auditLog) => (
<LogsTableRow auditLog={auditLog} key={`audit-log-${auditLog.id}`} />
<LogsTableRow
isOrgAuditLogs={isOrgAuditLogs}
auditLog={auditLog}
key={`audit-log-${auditLog.id}`}
/>
))}
</Fragment>
))}

View File

@@ -5,9 +5,10 @@ import { Actor, AuditLog, Event } from "@app/hooks/api/auditLogs/types";
type Props = {
auditLog: AuditLog;
isOrgAuditLogs?: boolean;
};
export const LogsTableRow = ({ auditLog }: Props) => {
export const LogsTableRow = ({ auditLog, isOrgAuditLogs }: Props) => {
const renderActor = (actor: Actor) => {
switch (actor.type) {
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`}>
<Td>{formatDate(auditLog.createdAt)}</Td>
<Td>{`${eventToNameMap[auditLog.event.type]}`}</Td>
{isOrgAuditLogs && <Td>{auditLog.project.name}</Td>}
{renderActor(auditLog.actor)}
<Td>
<p>{userAgentTTypeoNameMap[auditLog.userAgentType]}</p>