From 566a2435203a3df95f5a5aa170a5006324df7c3a Mon Sep 17 00:00:00 2001 From: = Date: Wed, 2 Jul 2025 19:36:04 +0530 Subject: [PATCH] feat: seperated date filter --- frontend/src/hooks/api/auditLogs/queries.tsx | 4 +- frontend/src/hooks/api/auditLogs/types.tsx | 4 +- .../components/LogsDateFilter.tsx | 212 +++++ .../AuditLogsPage/components/LogsFilter.tsx | 796 ++++++++---------- .../AuditLogsPage/components/LogsSection.tsx | 87 +- .../AuditLogsPage/components/LogsTable.tsx | 4 +- .../AuditLogsPage/components/types.tsx | 47 +- 7 files changed, 625 insertions(+), 529 deletions(-) create mode 100644 frontend/src/pages/organization/AuditLogsPage/components/LogsDateFilter.tsx diff --git a/frontend/src/hooks/api/auditLogs/queries.tsx b/frontend/src/hooks/api/auditLogs/queries.tsx index 3b851f1cb..52a35cb85 100644 --- a/frontend/src/hooks/api/auditLogs/queries.tsx +++ b/frontend/src/hooks/api/auditLogs/queries.tsx @@ -30,8 +30,8 @@ export const useGetAuditLogs = ( params: { ...filters, offset: pageParam, - startDate: filters?.startDate?.toISOString(), - endDate: filters?.endDate?.toISOString(), + startDate: filters.startDate.toISOString(), + endDate: filters.endDate.toISOString(), ...(filters.eventMetadata && Object.keys(filters.eventMetadata).length ? { eventMetadata: Object.entries(filters.eventMetadata) diff --git a/frontend/src/hooks/api/auditLogs/types.tsx b/frontend/src/hooks/api/auditLogs/types.tsx index 56dc11aeb..0b9e201fe 100644 --- a/frontend/src/hooks/api/auditLogs/types.tsx +++ b/frontend/src/hooks/api/auditLogs/types.tsx @@ -14,8 +14,8 @@ export type TGetAuditLogsFilter = { actor?: string; // user ID format secretPath?: string; secretKey?: string; - startDate?: Date; - endDate?: Date; + startDate: Date; + endDate: Date; limit: number; }; diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogsDateFilter.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogsDateFilter.tsx new file mode 100644 index 000000000..87957ad22 --- /dev/null +++ b/frontend/src/pages/organization/AuditLogsPage/components/LogsDateFilter.tsx @@ -0,0 +1,212 @@ +import { useState } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { faArrowRight, faCalendar, faChevronRight } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { format } from "date-fns"; +import ms from "ms"; +import { twMerge } from "tailwind-merge"; + +import { + Button, + DatePicker, + DropdownMenu, + DropdownMenuContent, + DropdownMenuTrigger, + FormControl, + Input, + Select, + SelectItem +} from "@app/components/v2"; + +import { + auditLogDateFilterFormSchema, + AuditLogDateFilterType, + TAuditLogDateFilterFormData +} from "./types"; + +type Props = { + setFilter: (data: TAuditLogDateFilterFormData) => void; + filter: TAuditLogDateFilterFormData; +}; +const RELATIVE_VALUES = ["5m", "30m", "1h", "3h", "12h"]; +export const LogsDateFilter = ({ setFilter, filter }: Props) => { + const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false); + const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false); + const [isPopupOpen, setIsPopOpen] = useState(false); + + const { control, watch, handleSubmit, formState } = useForm({ + resolver: zodResolver(auditLogDateFilterFormSchema), + values: filter + }); + const selectType = watch("type"); + const isCustomRelative = + filter.type === AuditLogDateFilterType.Relative && + !RELATIVE_VALUES.includes(filter.relativeModeValue || ""); + + const onSubmit = (data: TAuditLogDateFilterFormData) => { + const endDate = data.type === AuditLogDateFilterType.Relative ? new Date() : data.endDate; + const startDate = + data.type === AuditLogDateFilterType.Relative && data.relativeModeValue + ? new Date(Number(new Date()) - ms(data.relativeModeValue)) + : data.startDate; + setFilter({ + ...data, + startDate, + endDate + }); + setIsPopOpen(false); + }; + + return ( + setIsPopOpen(el)}> +
+ {filter.type === AuditLogDateFilterType.Relative ? ( + <> + {RELATIVE_VALUES.map((el) => ( + + ))} + + ) : ( + <> +
+ {format(filter.startDate, "yyyy-MM-dd HH:mm")} +
+
+ +
+
+ {format(filter.endDate, "yyyy-MM-dd HH:mm")} +
+ + )} + + + +
+ +
+ ( + + + + )} + /> + {selectType === AuditLogDateFilterType.Relative && ( + ( + + + + )} + /> + )} + {selectType === AuditLogDateFilterType.Absolute && ( +
+ { + return ( + + + + ); + }} + /> +
+
+ +
+ { + return ( + + + + ); + }} + /> +
+ )} +
+ +
+ + + + ); +}; diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx index 367cfefc0..f47597dcb 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx @@ -1,28 +1,15 @@ /* eslint-disable no-nested-ternary */ -import { useMemo, useState } from "react"; -import { - Control, - Controller, - UseFormGetFieldState, - UseFormReset, - UseFormResetField, - UseFormSetValue, - UseFormWatch -} from "react-hook-form"; -import { - faArrowRight, - faCaretDown, - faCheckCircle, - faFilterCircleXmark -} from "@fortawesome/free-solid-svg-icons"; +import { useMemo } from "react"; +import { Controller, useForm, UseFormGetValues } from "react-hook-form"; +import { faCaretDown, faCheckCircle, faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; import { AnimatePresence, motion } from "framer-motion"; import { twMerge } from "tailwind-merge"; import { Badge, Button, - DatePicker, DropdownMenu, DropdownMenuContent, DropdownMenuItem, @@ -44,7 +31,7 @@ import { EventType } from "@app/hooks/api/auditLogs/enums"; import { UserAgentType } from "@app/hooks/api/auth/types"; import { LogFilterItem } from "./LogFilterItem"; -import { AuditLogFilterFormData, Presets } from "./types"; +import { auditLogFilterFormSchema, Presets, TAuditLogFilterFormData } from "./types"; const eventTypes = Object.entries(eventToNameMap).map(([value, label]) => ({ label, value })); const userAgentTypes = Object.entries(userAgentTypeToNameMap).map(([value, label]) => ({ @@ -54,78 +41,54 @@ const userAgentTypes = Object.entries(userAgentTypeToNameMap).map(([value, label type Props = { presets?: Presets; - control: Control; - reset: UseFormReset; - resetField: UseFormResetField; - watch: UseFormWatch; - getFieldState: UseFormGetFieldState; - setValue: UseFormSetValue; + setFilter: (data: TAuditLogFilterFormData) => void; + filter: TAuditLogFilterFormData; }; -const getActiveFilterCount = ( - getFieldState: UseFormGetFieldState, - watch: UseFormWatch -) => { +const getActiveFilterCount = (filter: TAuditLogFilterFormData) => { const fields = [ "actor", "project", "eventType", - "startDate", - "endDate", "environment", "secretPath", "userAgentType", "secretKey" - ] as Partial[]; + ] as Partial[]; let filterCount = 0; // either start or end date should only be counted as one filter - let dateProcessed = false; - fields.forEach((field) => { - const fieldState = getFieldState(field); - - if ( - field === "userAgentType" || - field === "environment" || - field === "secretKey" || - field === "secretPath" - ) { - const value = watch(field); - - if (value !== undefined && value !== "") { - filterCount += 1; - } - } else if (fieldState.isDirty && !dateProcessed) { + const value = filter?.[field]; + if (Array.isArray(value) ? value.length : value) { filterCount += 1; - - if (field === "startDate" || field === "endDate") { - dateProcessed = true; - } } }); return filterCount; }; -export const LogsFilter = ({ - presets, - control, - reset, - resetField, - watch, - getFieldState, - setValue -}: Props) => { - const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false); - const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false); - +export const LogsFilter = ({ presets, setFilter, filter }: Props) => { const { data: workspaces = [] } = useGetUserWorkspaces(); const { currentOrg } = useOrganization(); const workspacesInOrg = workspaces.filter((ws) => ws.orgId === currentOrg?.id); + const { control, reset, watch, resetField, setValue, handleSubmit, formState } = + useForm({ + resolver: zodResolver(auditLogFilterFormSchema), + defaultValues: { + project: null, + environment: undefined, + secretKey: "", + secretPath: "", + actor: presets?.actorId, + eventType: presets?.eventType || [], + userAgentType: undefined + }, + values: filter + }); const selectedEventTypes = watch("eventType") as EventType[] | undefined; const selectedProject = watch("project"); @@ -140,408 +103,345 @@ export const LogsFilter = ({ return workspacesInOrg.find((ws) => ws.id === selectedProject.id)?.environments ?? []; }, [selectedProject, workspacesInOrg]); - const activeFilterCount = getActiveFilterCount(getFieldState, watch); + const activeFilterCount = getActiveFilterCount(filter); return ( - -
-
-
-
- Filters - - {activeFilterCount} - +
+
+
+
+
+ Filters + + {activeFilterCount} + +
+
-
+ +
+ { + resetField("eventType"); }} - variant="link" - className="text-mineshaft-400" - size="xs" > - Clear filters + ( + + + +
+ {selectedEventTypes?.length === 1 + ? eventTypes.find( + (eventType) => eventType.value === selectedEventTypes[0] + )?.label + : selectedEventTypes?.length === 0 + ? "All events" + : `${selectedEventTypes?.length} events selected`} + +
+
+ +
+ {eventTypes && eventTypes.length > 0 ? ( + eventTypes.map((eventType) => { + const isSelected = selectedEventTypes?.includes( + eventType.value as EventType + ); + + return ( + + eventTypes.length > 1 && event.preventDefault() + } + onClick={() => { + if ( + selectedEventTypes?.includes(eventType.value as EventType) + ) { + field.onChange( + selectedEventTypes?.filter( + (e: string) => e !== eventType.value + ) + ); + } else { + field.onChange([ + ...(selectedEventTypes || []), + eventType.value + ]); + } + }} + key={`event-type-${eventType.value}`} + icon={ + isSelected ? ( + + ) : ( +
+ ) + } + iconPos="left" + className="w-[28.4rem] text-sm" + > + {eventType.label} + + ); + }) + ) : ( +
+ )} +
+ + + + )} + /> + + { + resetField("userAgentType"); + }} + > + ( + + + + )} + /> + + { + resetField("project"); + resetField("environment"); + setValue("secretPath", ""); + setValue("secretKey", ""); + }} + > + ( + + { + if (e === null) { + setValue("secretPath", ""); + setValue("secretKey", ""); + } + resetField("environment"); + onChange(e); + }} + placeholder="All projects" + options={workspacesInOrg.map(({ name, id, defaultProduct }) => ({ + name, + id, + type: defaultProduct + }))} + getOptionValue={(option) => option.id} + getOptionLabel={(option) => option.name} + /> + + )} + /> + + + {showSecretsSection && ( + +
+

Secrets

+
+
+ { + resetField("environment"); + }} + > + ( + + onChange(e)} + placeholder="All environments" + options={availableEnvironments.map(({ name, slug }) => ({ + name, + slug + }))} + getOptionValue={(option) => option.slug} + getOptionLabel={(option) => option.name} + /> + + )} + /> + + { + setValue("secretPath", ""); + }} + > + ( + + onChange(e.target.value)} + /> + + )} + /> + + + { + setValue("secretKey", ""); + }} + > + ( + + + setValue("secretKey", e.target.value, { shouldDirty: true }) + } + /> + + )} + /> + + + )} + +
+
+
- -
- { - resetField("eventType"); - }} - > - ( - - - -
- {selectedEventTypes?.length === 1 - ? eventTypes.find( - (eventType) => eventType.value === selectedEventTypes[0] - )?.label - : selectedEventTypes?.length === 0 - ? "All events" - : `${selectedEventTypes?.length} events selected`} - -
-
- -
- {eventTypes && eventTypes.length > 0 ? ( - eventTypes.map((eventType) => { - const isSelected = selectedEventTypes?.includes( - eventType.value as EventType - ); - - return ( - - eventTypes.length > 1 && event.preventDefault() - } - onClick={() => { - if ( - selectedEventTypes?.includes(eventType.value as EventType) - ) { - field.onChange( - selectedEventTypes?.filter( - (e: string) => e !== eventType.value - ) - ); - } else { - field.onChange([ - ...(selectedEventTypes || []), - eventType.value - ]); - } - }} - key={`event-type-${eventType.value}`} - icon={ - isSelected ? ( - - ) : ( -
- ) - } - iconPos="left" - className="w-[28.4rem] text-sm" - > - {eventType.label} - - ); - }) - ) : ( -
- )} -
- - - - )} - /> - - { - resetField("userAgentType"); - }} - > - ( - - - - )} - /> - - - { - resetField("startDate"); - resetField("endDate"); - }} - > -
- { - return ( - - - - ); - }} - /> - -
-
- -
- - { - return ( - - - - ); - }} - /> -
- - - {showSecretsSection && ( - -
-

Secrets

-
-
- - { - resetField("project"); - resetField("environment"); - setValue("secretPath", ""); - setValue("secretKey", ""); - }} - > - ( - - { - if (e === null) { - setValue("secretPath", ""); - setValue("secretKey", ""); - } - resetField("environment"); - onChange(e); - }} - placeholder="All projects" - options={workspacesInOrg.map(({ name, id, defaultProduct }) => ({ - name, - id, - type: defaultProduct - }))} - getOptionValue={(option) => option.id} - getOptionLabel={(option) => option.name} - /> - - )} - /> - - - { - resetField("environment"); - }} - > - ( - - onChange(e)} - placeholder="All environments" - options={availableEnvironments.map(({ name, slug }) => ({ - name, - slug - }))} - getOptionValue={(option) => option.slug} - getOptionLabel={(option) => option.name} - /> - - )} - /> - - { - setValue("secretPath", ""); - }} - > - ( - - onChange(e.target.value)} - /> - - )} - /> - - - { - setValue("secretKey", ""); - }} - > - ( - - - setValue("secretKey", e.target.value, { shouldDirty: true }) - } - /> - - )} - /> - - - )} - -
-
+ ); diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogsSection.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogsSection.tsx index 1878a49e3..214202be3 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/LogsSection.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/LogsSection.tsx @@ -1,17 +1,20 @@ -import { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect, useState } from "react"; +import ms from "ms"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context"; import { withPermission } from "@app/hoc"; -import { useDebounce } from "@app/hooks"; -import { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums"; import { usePopUp } from "@app/hooks/usePopUp"; +import { LogsDateFilter } from "./LogsDateFilter"; import { LogsFilter } from "./LogsFilter"; import { LogsTable } from "./LogsTable"; -import { AuditLogFilterFormData, auditLogFilterFormSchema, Presets } from "./types"; +import { + AuditLogDateFilterType, + Presets, + TAuditLogDateFilterFormData, + TAuditLogFilterFormData +} from "./types"; type Props = { presets?: Presets; @@ -24,74 +27,44 @@ export const LogsSection = withPermission( const { subscription } = useSubscription(); const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const); - - const { control, reset, watch, getFieldState, resetField, setValue } = - useForm({ - resolver: zodResolver(auditLogFilterFormSchema), - defaultValues: { - project: null, - environment: undefined, - secretKey: "", - secretPath: "", - actor: presets?.actorId, - eventType: presets?.eventType || [], - userAgentType: undefined, - startDate: presets?.startDate ?? new Date(new Date().setDate(new Date().getDate() - 1)), - endDate: presets?.endDate ?? new Date(new Date(Date.now()).setHours(23, 59, 59, 999)) - } - }); + const [logFilter, setLogFilter] = useState({ + eventType: [] + }); + const [dateFilter, setDateFilter] = useState({ + startDate: new Date(Number(new Date()) - ms("1h")), + endDate: new Date(), + type: AuditLogDateFilterType.Relative, + relativeModeValue: "1h" + }); useEffect(() => { if (subscription && !subscription.auditLogs) { handlePopUpOpen("upgradePlan"); } }, [subscription]); - - const eventType = watch("eventType") as EventType[] | undefined; - const userAgentType = watch("userAgentType") as UserAgentType | undefined; - const actor = watch("actor"); - const projectId = watch("project")?.id; - const environment = watch("environment")?.slug; - const secretPath = watch("secretPath"); - const secretKey = watch("secretKey"); - - const startDate = watch("startDate"); - const endDate = watch("endDate"); - - const [debouncedSecretPath] = useDebounce(secretPath!, 500); - const [debouncedSecretKey] = useDebounce(secretKey!, 500); - return (
+ {showFilters && } {showFilters && ( - + )}
- { // Determine the project ID for filtering diff --git a/frontend/src/pages/organization/AuditLogsPage/components/types.tsx b/frontend/src/pages/organization/AuditLogsPage/components/types.tsx index 0e0d8cc8a..8e0ec9ba9 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/types.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/types.tsx @@ -3,23 +3,33 @@ import { z } from "zod"; import { ActorType, EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums"; import { ProjectType } from "@app/hooks/api/workspace/types"; -export const auditLogFilterFormSchema = z +export enum AuditLogDateFilterType { + Relative = "relative", + Absolute = "absolute" +} + +export const auditLogFilterFormSchema = z.object({ + eventMetadata: z.object({}).optional(), + project: z + .object({ id: z.string(), name: z.string(), type: z.nativeEnum(ProjectType) }) + .optional() + .nullable(), + environment: z.object({ name: z.string(), slug: z.string() }).optional().nullable(), + eventType: z.nativeEnum(EventType).array(), + actor: z.string().optional(), + userAgentType: z.nativeEnum(UserAgentType).optional(), + secretPath: z.string().optional(), + secretKey: z.string().optional(), + page: z.coerce.number().optional(), + perPage: z.coerce.number().optional() +}); + +export const auditLogDateFilterFormSchema = z .object({ - eventMetadata: z.object({}).optional(), - project: z - .object({ id: z.string(), name: z.string(), type: z.nativeEnum(ProjectType) }) - .optional() - .nullable(), - environment: z.object({ name: z.string(), slug: z.string() }).optional().nullable(), - eventType: z.nativeEnum(EventType).array(), - actor: z.string().optional(), - userAgentType: z.nativeEnum(UserAgentType), - secretPath: z.string().optional(), - secretKey: z.string().optional(), - startDate: z.date().optional(), - endDate: z.date().optional(), - page: z.coerce.number().optional(), - perPage: z.coerce.number().optional() + type: z.nativeEnum(AuditLogDateFilterType), + relativeModeValue: z.string().optional(), + startDate: z.date(), + endDate: z.date() }) .superRefine((el, ctx) => { if (el.endDate && el.startDate && el.endDate < el.startDate) { @@ -31,10 +41,11 @@ export const auditLogFilterFormSchema = z } }); -export type AuditLogFilterFormData = z.infer; +export type TAuditLogFilterFormData = z.infer; +export type TAuditLogDateFilterFormData = z.infer; export type SetValueType = ( - name: keyof AuditLogFilterFormData, + name: keyof TAuditLogFilterFormData, value: any, options?: { shouldValidate?: boolean;