Add date filter and pagination component to audit log v2

This commit is contained in:
Tuan Dang
2023-08-08 12:52:34 +07:00
parent 1ac607b42e
commit 6cb8cf53f8
13 changed files with 235 additions and 105 deletions
@@ -1,6 +1,6 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { Key, Membership, MembershipOrg, User, IUser } from "../../models"; import { IUser, Key, Membership, MembershipOrg, User } from "../../models";
import { EventType } from "../../ee/models"; import { EventType } from "../../ee/models";
import { deleteMembership as deleteMember, findMembership } from "../../helpers/membership"; import { deleteMembership as deleteMember, findMembership } from "../../helpers/membership";
import { sendMail } from "../../helpers/nodemailer"; import { sendMail } from "../../helpers/nodemailer";
@@ -17,8 +17,8 @@ import {
} from "../../services/FolderService"; } from "../../services/FolderService";
import { ADMIN, MEMBER } from "../../variables"; import { ADMIN, MEMBER } from "../../variables";
import { validateMembership } from "../../helpers/membership"; import { validateMembership } from "../../helpers/membership";
import { FolderVersion, EventType } from "../../ee/models"; import { EventType, FolderVersion } from "../../ee/models";
import { EESecretService, EEAuditLogService } from "../../ee/services"; import { EEAuditLogService, EESecretService } from "../../ee/services";
// verify workspace id/environment // verify workspace id/environment
export const createFolder = async (req: Request, res: Response) => { export const createFolder = async (req: Request, res: Response) => {
@@ -8,8 +8,8 @@ import {
ServiceTokenData, ServiceTokenData,
Workspace, Workspace,
} from "../../models"; } from "../../models";
import { SecretVersion, EventType } from "../../ee/models"; import { EventType, SecretVersion } from "../../ee/models";
import { EELicenseService, EEAuditLogService } from "../../ee/services"; import { EEAuditLogService, EELicenseService } from "../../ee/services";
import { BadRequestError, WorkspaceNotFoundError } from "../../utils/errors"; import { BadRequestError, WorkspaceNotFoundError } from "../../utils/errors";
import _ from "lodash"; import _ from "lodash";
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables"; import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables";
@@ -4,6 +4,7 @@ import { Membership, Secret, ServiceTokenData, User } from "../../../models";
import { import {
ActorType, ActorType,
AuditLog, AuditLog,
EventType,
FolderVersion, FolderVersion,
IPType, IPType,
ISecretVersion, ISecretVersion,
@@ -13,14 +14,13 @@ import {
ServiceActor, ServiceActor,
TFolderRootVersionSchema, TFolderRootVersionSchema,
TrustedIP, TrustedIP,
UserActor, UserActor
EventType
} from "../../models"; } from "../../models";
import { EESecretService } from "../../services"; import { EESecretService } from "../../services";
import { getLatestSecretVersionIds } from "../../helpers/secretVersion"; import { getLatestSecretVersionIds } from "../../helpers/secretVersion";
import Folder, { TFolderSchema } from "../../../models/folder"; import Folder, { TFolderSchema } from "../../../models/folder";
import { searchByFolderId } from "../../../services/FolderService"; import { searchByFolderId } from "../../../services/FolderService";
import { EELicenseService, EEAuditLogService } from "../../services"; import { EEAuditLogService, EELicenseService } from "../../services";
import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip"; import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip";
/** /**
@@ -611,7 +611,10 @@ export const getWorkspaceAuditLogs = async (req: Request, res: Response) => {
const offset: number = parseInt(req.query.offset as string); const offset: number = parseInt(req.query.offset as string);
const limit: number = parseInt(req.query.limit as string); const limit: number = parseInt(req.query.limit as string);
const auditLogs = await AuditLog.find({ const startDate = req.query.startDate as string;
const endDate = req.query.endDate as string;
const query = {
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
...(eventType ? { ...(eventType ? {
"event.type": eventType "event.type": eventType
@@ -626,14 +629,25 @@ export const getWorkspaceAuditLogs = async (req: Request, res: Response) => {
} : { } : {
"actor.metadata.serviceId": actor.split("-", 2)[1] "actor.metadata.serviceId": actor.split("-", 2)[1]
}) })
} : {}),
...(startDate || endDate ? {
createdAt: {
...(startDate && { $gte: new Date(startDate) }),
...(endDate && { $lte: new Date(endDate) })
}
} : {}) } : {})
}) }
const auditLogs = await AuditLog.find(query)
.sort({ createdAt: -1 }) .sort({ createdAt: -1 })
.skip(offset) .skip(offset)
.limit(limit); .limit(limit);
const totalCount = await AuditLog.countDocuments(query);
return res.status(200).send({ return res.status(200).send({
auditLogs auditLogs,
totalCount
}); });
} }
+5 -3
View File
@@ -96,9 +96,11 @@ router.get(
param("workspaceId").exists().trim(), param("workspaceId").exists().trim(),
query("eventType").isString().isIn(Object.values(EventType)).optional({ nullable: true }), query("eventType").isString().isIn(Object.values(EventType)).optional({ nullable: true }),
query("userAgentType").isString().isIn(Object.values(UserAgentType)).optional({ nullable: true }), query("userAgentType").isString().isIn(Object.values(UserAgentType)).optional({ nullable: true }),
query("actor").isString().optional({ nullable: true }), query("actor").optional({ nullable: true }),
query("offset").default("0"), query("startDate").isISO8601().withMessage("Invalid start date format").optional({ nullable: true }),
query("limit").default("20"), query("endDate").isISO8601().withMessage("Invalid end date format").optional({ nullable: true }),
query("offset"),
query("limit"),
validateRequest, validateRequest,
workspaceController.getWorkspaceAuditLogs workspaceController.getWorkspaceAuditLogs
); );
+1 -1
View File
@@ -1,6 +1,6 @@
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { Types } from "mongoose"; import { Types } from "mongoose";
import Folder, { TFolderSchema, TFolderRootSchema } from "../models/folder"; import Folder, { TFolderRootSchema, TFolderSchema } from "../models/folder";
import { ResourceNotFoundError } from "../utils/errors"; import { ResourceNotFoundError } from "../utils/errors";
type TAppendFolderDTO = { type TAppendFolderDTO = {
@@ -21,7 +21,7 @@ export type PaginationProps = {
perPage?: number; perPage?: number;
onChangePage: (pageNumber: number) => void; onChangePage: (pageNumber: number) => void;
onChangePerPage: (newRows: number) => void; onChangePerPage: (newRows: number) => void;
className: string; className?: string;
perPageList?: number[]; perPageList?: number[];
}; };
+10 -6
View File
@@ -31,15 +31,19 @@ export const useGetAuditLogs = (workspaceId: string, filters: AuditLogFilters) =
params.append("actor", filters.actor); params.append("actor", filters.actor);
} }
if (filters.actor) { if (filters.startDate) {
params.append("actor", filters.actor); params.append("startDate", filters.startDate.toISOString());
} }
params.append("offset ", String(filters.offset)); if (filters.endDate) {
params.append("limit ", String(filters.limit)); params.append("endDate", filters.endDate.toISOString());
}
params.append("offset", String(filters.offset));
params.append("limit", String(filters.limit));
const { data } = await apiRequest.get<{ auditLogs: AuditLog[] }>(`/api/v1/workspace/${workspaceId}/audit-logs`, { params }); const { data } = await apiRequest.get<{ auditLogs: AuditLog[], totalCount: number }>(`/api/v1/workspace/${workspaceId}/audit-logs`, { params });
return data.auditLogs; return data;
} }
}); });
} }
@@ -323,4 +323,6 @@ export type AuditLogFilters = {
actor?: string; actor?: string;
offset: number; offset: number;
limit: number; limit: number;
startDate?: Date;
endDate?: Date;
} }
@@ -1,18 +1,19 @@
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 { import {
Button, Button,
DatePicker,
FormControl, FormControl,
Select, Select,
SelectItem} from "@app/components/v2"; SelectItem} from "@app/components/v2";
import { useWorkspace } from "@app/context"; import { useWorkspace } from "@app/context";
import { useGetAuditLogActorFilterOpts } from "@app/hooks/api"; import { useGetAuditLogActorFilterOpts } from "@app/hooks/api";
import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants";
import { eventToNameMap, userAgentTTypeoNameMap } from "~/hooks/api/auditLogs/constants"; import { ActorType } from "@app/hooks/api/auditLogs/enums";
import { ActorType } from "~/hooks/api/auditLogs/enums"; import { Actor } from "@app/hooks/api/auditLogs/types";
import { Actor } from "~/hooks/api/auditLogs/types";
import { AuditLogFilterFormData } from "./types"; import { AuditLogFilterFormData } from "./types";
@@ -28,6 +29,9 @@ export const LogsFilter = ({
control, control,
reset reset
}: Props) => { }: Props) => {
const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false);
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { data, isLoading } = useGetAuditLogActorFilterOpts(currentWorkspace?._id ?? ""); const { data, isLoading } = useGetAuditLogActorFilterOpts(currentWorkspace?._id ?? "");
@@ -58,65 +62,41 @@ export const LogsFilter = ({
return ( return (
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div className="flex items-center"> <div className="flex items-center">
<div className="w-40 mr-4"> <Controller
<Controller control={control}
control={control} name="eventType"
name="eventType" render={({ field: { onChange, ...field }, fieldState: { error } }) => (
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( <FormControl
<FormControl label="Event"
label="Event" errorText={error?.message}
errorText={error?.message} isError={Boolean(error)}
isError={Boolean(error)} className="w-40 mr-4"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
> >
<Select {eventTypes.map(({ label, value }) => (
defaultValue={field.value} <SelectItem value={String(value || "")} key={label}>
{...field} {label}
onValueChange={(e) => onChange(e)} </SelectItem>
className="w-full" ))}
> </Select>
{eventTypes.map(({ label, value }) => ( </FormControl>
<SelectItem value={String(value || "")} key={label}> )}
{label} />
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</div>
{!isLoading && data && data.length > 0 && ( {!isLoading && data && data.length > 0 && (
<div className="w-40 mr-4">
<Controller
control={control}
name="actor"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Actor"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{data.map((actor) => renderActorSelectItem(actor))}
</Select>
</FormControl>
)}
/>
</div>
)}
<div className="w-40">
<Controller <Controller
control={control} control={control}
name="userAgentType" name="actor"
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl <FormControl
label="Source" label="Actor"
errorText={error?.message} errorText={error?.message}
isError={Boolean(error)} isError={Boolean(error)}
className="w-40 mr-4"
> >
<Select <Select
defaultValue={field.value} defaultValue={field.value}
@@ -124,16 +104,90 @@ export const LogsFilter = ({
onValueChange={(e) => onChange(e)} onValueChange={(e) => onChange(e)}
className="w-full" className="w-full"
> >
{userAgentTypes.map(({ label, value }) => ( {data.map((actor) => renderActorSelectItem(actor))}
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select> </Select>
</FormControl> </FormControl>
)} )}
/> />
</div> )}
<Controller
control={control}
name="userAgentType"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Source"
errorText={error?.message}
isError={Boolean(error)}
className="w-40 mr-4"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{userAgentTypes.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
name="startDate"
control={control}
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl
label="Start date"
errorText={error?.message}
isError={Boolean(error)}
className="mr-4"
>
<DatePicker
value={field.value || undefined}
onChange={date => {
onChange(date);
setIsStartDatePickerOpen(false);
}}
popUpProps={{
open: isStartDatePickerOpen,
onOpenChange: setIsStartDatePickerOpen
}}
popUpContentProps={{}}
/>
</FormControl>
);
}}
/>
<Controller
name="endDate"
control={control}
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl
label="End date"
errorText={error?.message}
isError={Boolean(error)}
>
<DatePicker
value={field.value || undefined}
onChange={date => {
onChange(date);
setIsEndDatePickerOpen(false);
}}
popUpProps={{
open: isEndDatePickerOpen,
onOpenChange: setIsEndDatePickerOpen
}}
popUpContentProps={{}}
/>
</FormControl>
);
}}
/>
</div> </div>
<div> <div>
<Button <Button
@@ -143,9 +197,11 @@ export const LogsFilter = ({
type="submit" type="submit"
leftIcon={<FontAwesomeIcon icon={faFilterCircleXmark} className="mr-2" />} leftIcon={<FontAwesomeIcon icon={faFilterCircleXmark} className="mr-2" />}
onClick={() => reset({ onClick={() => reset({
eventType: "", eventType: undefined,
actor: "", actor: "",
userAgentType: "" userAgentType: "",
startDate: undefined,
endDate: undefined
})} })}
> >
Clear filters Clear filters
@@ -1,24 +1,35 @@
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import { EventType, UserAgentType } from "~/hooks/api/auditLogs/enums"; import { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
import { LogsFilter } from "./LogsFilter"; 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 = () => { export const LogsSection = () => {
const { const {
control, control,
reset, reset,
watch, watch,
setValue,
} = useForm<AuditLogFilterFormData>({ } = useForm<AuditLogFilterFormData>({
resolver: yupResolver(auditLogFilterFormSchema) resolver: yupResolver(auditLogFilterFormSchema),
defaultValues: {
page: 1,
perPage: 10
}
}); });
const eventType = watch("eventType") as EventType | undefined; const eventType = watch("eventType") as EventType | undefined;
const userAgentType = watch("userAgentType") as UserAgentType | undefined; const userAgentType = watch("userAgentType") as UserAgentType | undefined;
const actor = watch("actor") as string | undefined; const actor = watch("actor");
const startDate = watch("startDate");
const endDate = watch("endDate");
const page = watch("page") as number;
const perPage = watch("perPage") as number;
return ( return (
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"> <div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
@@ -35,6 +46,11 @@ export const LogsSection = () => {
eventType={eventType} eventType={eventType}
userAgentType={userAgentType} userAgentType={userAgentType}
actor={actor} actor={actor}
startDate={startDate}
endDate={endDate}
page={page}
perPage={perPage}
setValue={setValue}
/> />
</div> </div>
); );
@@ -2,6 +2,7 @@ import { faFile } from "@fortawesome/free-solid-svg-icons";
import { import {
EmptyState, EmptyState,
Pagination,
Table, Table,
TableContainer, TableContainer,
TableSkeleton, TableSkeleton,
@@ -9,33 +10,44 @@ import {
Td, Td,
Th, Th,
THead, THead,
Tr Tr} from "@app/components/v2";
} from "@app/components/v2";
import { useWorkspace } from "@app/context"; import { useWorkspace } from "@app/context";
import { useGetAuditLogs } from "@app/hooks/api"; import { useGetAuditLogs } from "@app/hooks/api";
import { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
import { EventType, UserAgentType } from "~/hooks/api/auditLogs/enums";
import { LogsTableRow } from "./LogsTableRow"; import { LogsTableRow } from "./LogsTableRow";
import { SetValueType } from "./types";
type Props = { type Props = {
eventType: EventType | undefined; eventType?: EventType;
userAgentType: UserAgentType | undefined; userAgentType?: UserAgentType;
actor: string | undefined; actor?: string;
startDate?: Date;
endDate?: Date;
page: number;
perPage: number;
setValue: SetValueType;
} }
export const LogsTable = ({ export const LogsTable = ({
eventType, eventType,
userAgentType, userAgentType,
actor actor,
startDate,
endDate,
page,
perPage,
setValue
}: Props) => { }: Props) => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { data, isLoading } = useGetAuditLogs(currentWorkspace?._id ?? "", { const { data, isLoading } = useGetAuditLogs(currentWorkspace?._id ?? "", {
eventType, eventType,
userAgentType, userAgentType,
actor, actor,
offset: 0, // TODO: update with pagination startDate,
limit: 20 // TODO: update with pagination endDate,
offset: (page - 1) * perPage,
limit: perPage
}); });
return ( return (
@@ -51,14 +63,14 @@ export const LogsTable = ({
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{!isLoading && data && data.map((auditLog) => ( {!isLoading && data?.auditLogs && data?.auditLogs.map((auditLog) => (
<LogsTableRow <LogsTableRow
auditLog={auditLog} auditLog={auditLog}
key={`audit-log-${auditLog._id}`} key={`audit-log-${auditLog._id}`}
/> />
))} ))}
{isLoading && <TableSkeleton innerKey="logs-table" columns={5} key="logs" />} {isLoading && <TableSkeleton innerKey="logs-table" columns={5} key="logs" />}
{!isLoading && data && data.length === 0 && ( {!isLoading && data?.auditLogs && data?.auditLogs.length === 0 && (
<Tr> <Tr>
<Td colSpan={5}> <Td colSpan={5}>
<EmptyState <EmptyState
@@ -70,6 +82,17 @@ export const LogsTable = ({
)} )}
</TBody> </TBody>
</Table> </Table>
{!isLoading && data?.totalCount !== undefined && (
<Pagination
count={data?.totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setValue("page", newPage)}
onChangePerPage={(newPerPage) => setValue("perPage", newPerPage)}
/>
)}
</TableContainer> </TableContainer>
); );
} }
// TODO: retrieve count
@@ -1,6 +1,6 @@
import * as yup from "yup"; import * as yup from "yup";
import { EventType, UserAgentType } from "~/hooks/api/auditLogs/enums"; import { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
export const auditLogFilterFormSchema = yup.object({ export const auditLogFilterFormSchema = yup.object({
eventType: yup.string() eventType: yup.string()
@@ -8,6 +8,19 @@ export const auditLogFilterFormSchema = yup.object({
actor: yup.string(), actor: yup.string(),
userAgentType: yup.string() userAgentType: yup.string()
.oneOf(Object.values(UserAgentType), "Invalid user agent type"), .oneOf(Object.values(UserAgentType), "Invalid user agent type"),
startDate: yup.date(),
endDate: yup.date().min(yup.ref("startDate"), "End date cannot be before start date"),
page: yup.number(),
perPage: yup.number()
}).required(); }).required();
export type AuditLogFilterFormData = yup.InferType<typeof auditLogFilterFormSchema>; export type AuditLogFilterFormData = yup.InferType<typeof auditLogFilterFormSchema>;
export type SetValueType = (
name: keyof AuditLogFilterFormData,
value: any,
options?: {
shouldValidate?: boolean,
shouldDirty?: boolean
}
) => void;