feat: add MoreFilters component and integrate into superadmin and tenant AuditLogs pages

This commit is contained in:
sibarchannayak 2026-05-29 16:45:14 +05:30
parent 277600edf0
commit b16e6d9c18
4 changed files with 368 additions and 277 deletions

View File

@ -0,0 +1,96 @@
import type { ReactElement, ReactNode } from "react";
import { SlidersHorizontal, X, ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
import { useAppTheme } from "@/hooks/useAppTheme";
import { PrimaryButton } from "./PrimaryButton";
import { SecondaryButton } from "./SecondaryButton";
interface MoreFiltersProps {
title?: string;
isOpen: boolean;
onOpenToggle: (isOpen: boolean) => void;
onApply: () => void;
onCancel: () => void;
hasActiveFilters?: boolean;
children: ReactNode;
}
export const MoreFilters = ({
title = "Filter Options",
isOpen,
onOpenToggle,
onApply,
onCancel,
hasActiveFilters = false,
children,
}: MoreFiltersProps): ReactElement => {
const { primaryColor } = useAppTheme();
return (
<div className="relative">
<button
type="button"
onClick={() => onOpenToggle(!isOpen)}
className={cn(
"inline-flex items-center gap-1.5 h-10 px-3 rounded-md text-sm font-medium border bg-white transition-colors cursor-pointer select-none",
isOpen || hasActiveFilters
? "border-[rgba(8,76,200,0.35)] text-[#0f1724]"
: "border-[rgba(0,0,0,0.08)] text-[#475569] hover:border-[#084cc8]/30"
)}
style={
isOpen || hasActiveFilters
? { borderColor: `${primaryColor}55`, color: primaryColor }
: undefined
}
>
<SlidersHorizontal className="w-3.5 h-3.5 shrink-0" />
More filters
<ChevronDown
className={cn(
"w-3.5 h-3.5 shrink-0 opacity-70 transition-transform",
isOpen && "rotate-180"
)}
/>
</button>
{isOpen && (
<div
className="absolute right-0 sm:left-0 md:right-0 sm:right-auto md:left-auto top-12 flex w-[90vw] max-w-[500px] flex-col items-start gap-[16px] rounded-[12px] bg-white p-[20px] shadow-[0_20px_25px_-5px_rgba(0,0,0,0.10),0_10px_10px_14px_rgba(135,135,135,0.04)] z-50 border border-gray-100"
>
{/* Header */}
<div className="flex items-center justify-between self-stretch">
<h2 className="text-lg font-semibold text-gray-900">
{title}
</h2>
<button type="button" onClick={() => onOpenToggle(false)}>
<X size={20} className="text-gray-500 hover:text-gray-700" />
</button>
</div>
{/* Children container */}
<div className="flex flex-col self-stretch">
{children}
</div>
{/* Footer Actions */}
<div className="flex items-start justify-end gap-[12px] self-stretch mt-2">
<SecondaryButton
type="button"
onClick={onCancel}
className="px-6 h-10 select-none cursor-pointer"
>
Cancel
</SecondaryButton>
<PrimaryButton
type="button"
onClick={onApply}
className="px-6 h-10 select-none cursor-pointer"
>
Apply filters
</PrimaryButton>
</div>
</div>
)}
</div>
);
};

View File

@ -45,3 +45,4 @@ export { SearchBox } from './SearchBox';
export { FormTagInput } from './FormTagInput';
export { MarkdownViewer } from './MarkdownViewer';
export { GradientStatCard } from './GradientStatCard';
export { MoreFilters } from './MoreFilters';

View File

@ -1,4 +1,4 @@
import { useState, useEffect, useMemo } from "react";
import { useState, useEffect } from "react";
import type { ReactElement } from "react";
import { Layout } from "@/components/layout/Layout";
import {
@ -8,19 +8,18 @@ import {
FilterDropdown,
StatusBadge,
type Column,
MoreFilters,
FormSelect,
FormField,
} from "@/components/shared";
import {
Download,
ArrowUpDown,
Search,
ChevronDown,
SlidersHorizontal,
} from "lucide-react";
import { auditLogService } from "@/services/audit-log-service";
import { tenantService } from "@/services/tenant-service";
import type { AuditLog } from "@/types/audit-log";
import type { Tenant } from "@/types/tenant";
import { cn } from "@/lib/utils";
import { useAppTheme } from "@/hooks/useAppTheme";
// Helper function to format date
@ -82,7 +81,7 @@ const getStatusColor = (status: number | null): string => {
};
const AuditLogs = (): ReactElement => {
const { primaryColor } = useAppTheme();
useAppTheme();
const [expandedId, setExpandedId] = useState<string | null>(null);
const [auditLogs, setAuditLogs] = useState<AuditLog[]>([]);
const [tenants, setTenants] = useState<Tenant[]>([]);
@ -125,13 +124,12 @@ const AuditLogs = (): ReactElement => {
const [orderBy, setOrderBy] = useState<string[] | null>(null);
const [search, setSearch] = useState<string>("");
const [debouncedSearch, setDebouncedSearch] = useState<string>("");
const [showMoreFilters, setShowMoreFilters] = useState<boolean>(false);
const hasExtraFilters = useMemo(
() =>
Boolean(moduleFilter || methodFilter || startDate || endDate || orderBy),
[moduleFilter, methodFilter, startDate, endDate, orderBy],
);
const [isMoreFiltersOpen, setIsMoreFiltersOpen] = useState<boolean>(false);
const [tempResourceType, setTempResourceType] = useState<string | null>(null);
const [tempModule, setTempModule] = useState<string | null>(null);
const [tempOrderBy, setTempOrderBy] = useState<string[] | null>(null);
const [tempStartDate, setTempStartDate] = useState<string>("");
const [tempEndDate, setTempEndDate] = useState<string>("");
// View modal
const [viewModalOpen, setViewModalOpen] = useState<boolean>(false);
@ -183,11 +181,7 @@ const AuditLogs = (): ReactElement => {
fetchModules();
}, []);
useEffect(() => {
if (hasExtraFilters) {
setShowMoreFilters(true);
}
}, [hasExtraFilters]);
const fetchAuditLogs = async (): Promise<void> => {
try {
@ -574,43 +568,132 @@ const AuditLogs = (): ReactElement => {
placeholder="All Actions"
/>
{/* Resource Filter */}
<FilterDropdown
label="Resource Type"
options={resourceTypes}
value={resourceTypeFilter}
onChange={(value) => {
setResourceTypeFilter(value as string | null);
setCurrentPage(1);
{/* More Filters Popover */}
<MoreFilters
title="Filter Logs"
isOpen={isMoreFiltersOpen}
onOpenToggle={(open) => {
if (open) {
setTempResourceType(resourceTypeFilter);
setTempModule(moduleFilter);
setTempOrderBy(orderBy);
setTempStartDate(startDate);
setTempEndDate(endDate);
}
setIsMoreFiltersOpen(open);
}}
placeholder="All Resources"
isSearchable
/>
<button
type="button"
onClick={() => setShowMoreFilters((open) => !open)}
className={cn(
"inline-flex items-center gap-1.5 h-10 px-3 rounded-md text-sm font-medium border bg-white transition-colors",
showMoreFilters || hasExtraFilters
? "border-[rgba(8,76,200,0.35)] text-[#0f1724]"
: "border-[rgba(0,0,0,0.08)] text-[#475569] hover:border-[#084cc8]/30",
)}
style={
showMoreFilters || hasExtraFilters
? { borderColor: `${primaryColor}55`, color: primaryColor }
: undefined
}
onApply={() => {
setResourceTypeFilter(tempResourceType);
setModuleFilter(tempModule);
setOrderBy(tempOrderBy);
setStartDate(tempStartDate);
setEndDate(tempEndDate);
setCurrentPage(1);
setIsMoreFiltersOpen(false);
}}
onCancel={() => setIsMoreFiltersOpen(false)}
hasActiveFilters={Boolean(resourceTypeFilter || moduleFilter || orderBy || startDate || endDate)}
>
<SlidersHorizontal className="w-3.5 h-3.5 shrink-0" />
More filters
<ChevronDown
className={cn(
"w-3.5 h-3.5 shrink-0 opacity-70 transition-transform",
showMoreFilters && "rotate-180",
)}
/>
</button>
{/* Dropdowns row 1 */}
<div className="flex flex-col sm:flex-row items-start justify-center gap-[20px] self-stretch">
{/* Resource Type */}
<div className="flex-1 w-full">
<FormSelect
label="Resource Type"
placeholder="All Resources"
options={resourceTypes}
value={tempResourceType || ""}
onValueChange={(val) => setTempResourceType(val || null)}
isSearchable
/>
</div>
{/* Module */}
<div className="flex-1 w-full">
<FormSelect
label="Module"
placeholder="All Modules"
options={modules.map((m) => ({ value: m.id, label: m.name }))}
value={tempModule || ""}
onValueChange={(val) => setTempModule(val || null)}
isSearchable
/>
</div>
</div>
{/* Dropdowns row 2 */}
<div className="flex flex-col sm:flex-row items-start justify-center gap-[20px] self-stretch">
{/* Sort by */}
<div className="flex-1 w-full">
<FormSelect
label="Sort by"
placeholder="Newest"
options={[
{ value: "created_at,desc", label: "Newest First" },
{ value: "created_at,asc", label: "Oldest First" },
]}
value={tempOrderBy ? tempOrderBy.join(",") : ""}
onValueChange={(val) => setTempOrderBy(val ? val.split(",") : null)}
/>
</div>
{/* Empty/Placeholder space for balance */}
<div className="flex-1 w-full hidden sm:block" />
</div>
{/* Date Filters Row */}
<div className="flex flex-col sm:flex-row items-start justify-center gap-[20px] self-stretch">
{/* Start Date */}
<div className="flex-1 w-full">
<FormField
label="Start Date"
type="date"
value={tempStartDate}
onChange={(e) => setTempStartDate(e.target.value)}
/>
</div>
{/* End Date */}
<div className="flex-1 w-full">
<FormField
label="End Date"
type="date"
value={tempEndDate}
onChange={(e) => setTempEndDate(e.target.value)}
/>
</div>
</div>
</MoreFilters>
{/* Clear Filters */}
{(startDate ||
endDate ||
tenantFilter ||
actionFilter ||
resourceTypeFilter ||
moduleFilter ||
methodFilter ||
search ||
orderBy) && (
<button
onClick={() => {
setStartDate("");
setEndDate("");
setTenantFilter(null);
setActionFilter(null);
setResourceTypeFilter(null);
setModuleFilter(null);
setMethodFilter(null);
setOrderBy(null);
setSearch("");
setIsMoreFiltersOpen(false);
setCurrentPage(1);
}}
className="text-xs text-[#ef4444] hover:underline shrink-0 cursor-pointer"
>
Reset All Filters
</button>
)}
</div>
{/* Actions */}
@ -625,118 +708,6 @@ const AuditLogs = (): ReactElement => {
</button>
</div>
</div>
{showMoreFilters && (
<div className="flex flex-col gap-3 border-t border-gray-50 mt-1 pt-3">
<div className="flex flex-wrap items-center gap-2">
<FilterDropdown
label="Module"
options={modules.map((m) => ({ value: m.id, label: m.name }))}
value={moduleFilter}
onChange={(value) => {
setModuleFilter(value as string | null);
setCurrentPage(1);
}}
placeholder="All Modules"
isSearchable
/>
<FilterDropdown
label="Method"
options={[
{ value: "GET", label: "GET" },
{ value: "POST", label: "POST" },
{ value: "PUT", label: "PUT" },
{ value: "DELETE", label: "DELETE" },
]}
value={methodFilter}
onChange={(value) => {
setMethodFilter(value as string | null);
setCurrentPage(1);
}}
placeholder="All Methods"
/>
<FilterDropdown
label="Sort"
options={[
{ value: ["created_at", "desc"], label: "Newest First" },
{ value: ["created_at", "asc"], label: "Oldest First" },
]}
value={orderBy}
onChange={(value) => {
setOrderBy(value as string[] | null);
setCurrentPage(1);
}}
placeholder="Newest"
showIcon
icon={<ArrowUpDown className="w-3.5 h-3.5 text-[#475569]" />}
/>
</div>
<div className="flex flex-wrap items-center gap-4 md:gap-6">
<div className="flex items-center gap-2">
<span className="text-xs font-medium text-[#475569]">
Start Date:
</span>
<input
type="date"
value={startDate}
onChange={(e) => {
setStartDate(e.target.value);
setCurrentPage(1);
}}
className="text-xs px-2 py-1.5 border border-[rgba(0,0,0,0.08)] rounded focus:outline-none focus:ring-1 focus:ring-[#112868]/20"
/>
</div>
<div className="flex items-center gap-2">
<span className="text-xs font-medium text-[#475569]">
End Date:
</span>
<input
type="date"
value={endDate}
onChange={(e) => {
setEndDate(e.target.value);
setCurrentPage(1);
}}
className="text-xs px-2 py-1.5 border border-[rgba(0,0,0,0.08)] rounded focus:outline-none focus:ring-1 focus:ring-[#112868]/20"
/>
</div>
</div>
</div>
)}
{(startDate ||
endDate ||
tenantFilter ||
actionFilter ||
resourceTypeFilter ||
moduleFilter ||
methodFilter ||
search ||
orderBy) && (
<div className="flex justify-end pt-1">
<button
onClick={() => {
setStartDate("");
setEndDate("");
setTenantFilter(null);
setActionFilter(null);
setResourceTypeFilter(null);
setModuleFilter(null);
setMethodFilter(null);
setOrderBy(null);
setSearch("");
setShowMoreFilters(false);
setCurrentPage(1);
}}
className="text-xs text-[#ef4444] hover:underline"
>
Reset All Filters
</button>
</div>
)}
</div>
{/* Table */}

View File

@ -9,8 +9,11 @@ import {
StatusBadge,
SearchBox,
type Column,
MoreFilters,
FormSelect,
FormField,
} from "@/components/shared";
import { Download, ArrowUpDown } from "lucide-react";
import { Download } from "lucide-react";
import { auditLogService } from "@/services/audit-log-service";
import { moduleService } from "@/services/module-service";
import type { AuditLog } from "@/types/audit-log";
@ -120,7 +123,7 @@ const AuditLogs = ({
});
// Filter state
const [methodFilter, setMethodFilter] = useState<string | null>(null);
const methodFilter = null;
const [actionFilter, setActionFilter] = useState<string | null>(null);
const [resourceTypeFilter, setResourceTypeFilter] = useState<string | null>(
null,
@ -131,8 +134,12 @@ const AuditLogs = ({
const [orderBy, setOrderBy] = useState<string[] | null>(null);
const [search, setSearch] = useState<string>("");
const [debouncedSearch, setDebouncedSearch] = useState<string>("");
// View modal
// More Filters popover state
const [isMoreFiltersOpen, setIsMoreFiltersOpen] = useState<boolean>(false);
const [tempResourceType, setTempResourceType] = useState<string | null>(null);
const [tempOrderBy, setTempOrderBy] = useState<string[] | null>(null);
const [tempStartDate, setTempStartDate] = useState<string>("");
const [tempEndDate, setTempEndDate] = useState<string>("");
const [viewModalOpen, setViewModalOpen] = useState<boolean>(false);
const [selectedAuditLogId, setSelectedAuditLogId] = useState<string | null>(
null,
@ -522,7 +529,7 @@ const AuditLogs = ({
{/* Table Container */}
<div className="overflow-hidden">
{/* Table Header with Filters */}
<div className="pb-2 flex flex-col gap-4">
<div className="pb-4 flex flex-col gap-4">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
{/* Search and Filters */}
<div className="flex flex-wrap items-center gap-3">
@ -535,41 +542,26 @@ const AuditLogs = ({
/>
{isTenantAdmin && (
<>
{/* Action Filter */}
<FilterDropdown
label="Action"
options={[
{ value: "LOGIN", label: "LOGIN" },
{ value: "LOGOUT", label: "LOGOUT" },
{ value: "CREATE", label: "CREATE" },
{ value: "UPDATE", label: "UPDATE" },
{ value: "DELETE", label: "DELETE" },
{ value: "SUBMIT", label: "SUBMIT" },
{ value: "APPROVE", label: "APPROVE" },
{ value: "REJECT", label: "REJECT" },
]}
value={actionFilter}
onChange={(value) => {
setActionFilter(value as string | null);
setCurrentPage(1);
}}
placeholder="All Actions"
/>
{/* Resource Type Filter */}
<FilterDropdown
label="Resource Type"
options={resourceTypes}
value={resourceTypeFilter}
onChange={(value) => {
setResourceTypeFilter(value as string | null);
setCurrentPage(1);
}}
placeholder="All Resources"
isSearchable
/>
</>
/* Action Filter */
<FilterDropdown
label="Action"
options={[
{ value: "LOGIN", label: "LOGIN" },
{ value: "LOGOUT", label: "LOGOUT" },
{ value: "CREATE", label: "CREATE" },
{ value: "UPDATE", label: "UPDATE" },
{ value: "DELETE", label: "DELETE" },
{ value: "SUBMIT", label: "SUBMIT" },
{ value: "APPROVE", label: "APPROVE" },
{ value: "REJECT", label: "REJECT" },
]}
value={actionFilter}
onChange={(value) => {
setActionFilter(value as string | null);
setCurrentPage(1);
}}
placeholder="All Actions"
/>
)}
{/* Module Filter */}
@ -584,27 +576,110 @@ const AuditLogs = ({
placeholder="All Modules"
/>
{/* Sort Filter - Always show as it's useful for everyone */}
<FilterDropdown
label="Sort by"
options={[
{ value: ["created_at", "desc"], label: "Newest First" },
{ value: ["created_at", "asc"], label: "Oldest First" },
{ value: ["action", "asc"], label: "Action (A-Z)" },
{
value: ["resource_type", "asc"],
label: "Resource Type (A-Z)",
},
]}
value={orderBy}
onChange={(value) => {
setOrderBy(value as string[] | null);
setCurrentPage(1);
{/* More Filters Popover */}
<MoreFilters
title="Filter Logs"
isOpen={isMoreFiltersOpen}
onOpenToggle={(open) => {
if (open) {
setTempResourceType(resourceTypeFilter);
setTempOrderBy(orderBy);
setTempStartDate(startDate);
setTempEndDate(endDate);
}
setIsMoreFiltersOpen(open);
}}
placeholder="Newest"
showIcon
icon={<ArrowUpDown className="w-3.5 h-3.5 text-[#475569]" />}
/>
onApply={() => {
setResourceTypeFilter(tempResourceType);
setOrderBy(tempOrderBy);
setStartDate(tempStartDate);
setEndDate(tempEndDate);
setCurrentPage(1);
setIsMoreFiltersOpen(false);
}}
onCancel={() => setIsMoreFiltersOpen(false)}
hasActiveFilters={Boolean(resourceTypeFilter || orderBy || startDate || endDate)}
>
{/* Dropdowns row */}
<div className="flex flex-col sm:flex-row items-start justify-center gap-[20px] self-stretch">
{/* Resource Type */}
<div className="flex-1 w-full">
<FormSelect
label="Resource Type"
placeholder="All Resources"
options={resourceTypes}
value={tempResourceType || ""}
onValueChange={(val) => setTempResourceType(val || null)}
isSearchable
/>
</div>
{/* Sort by */}
<div className="flex-1 w-full">
<FormSelect
label="Sort by"
placeholder="Newest"
options={[
{ value: "created_at,desc", label: "Newest First" },
{ value: "created_at,asc", label: "Oldest First" },
{ value: "action,asc", label: "Action (A-Z)" },
{ value: "resource_type,asc", label: "Resource Type (A-Z)" },
]}
value={tempOrderBy ? tempOrderBy.join(",") : ""}
onValueChange={(val) => setTempOrderBy(val ? val.split(",") : null)}
/>
</div>
</div>
{/* Date Filters Row */}
<div className="flex flex-col sm:flex-row items-start justify-center gap-[20px] self-stretch">
{/* From Date */}
<div className="flex-1 w-full">
<FormField
label="From Date"
type="date"
value={tempStartDate}
onChange={(e) => setTempStartDate(e.target.value)}
/>
</div>
{/* To Date */}
<div className="flex-1 w-full">
<FormField
label="To Date"
type="date"
value={tempEndDate}
onChange={(e) => setTempEndDate(e.target.value)}
/>
</div>
</div>
</MoreFilters>
{/* Clear Filters */}
{(startDate ||
endDate ||
actionFilter ||
resourceTypeFilter ||
moduleIdFilter ||
search ||
orderBy) && (
<button
onClick={() => {
setStartDate("");
setEndDate("");
setActionFilter(null);
setResourceTypeFilter(null);
setModuleIdFilter(null);
setOrderBy(null);
setSearch("");
setCurrentPage(1);
}}
className="text-xs hover:underline decoration-offset-2 shrink-0 cursor-pointer"
style={{ color: primaryColor }}
>
Clear all filters
</button>
)}
</div>
{/* Actions */}
@ -621,58 +696,6 @@ const AuditLogs = ({
)}
</div>
</div>
{/* Date Filters - Separated row for better spacing */}
<div className="flex flex-wrap items-center gap-3 md:gap-6 pt-1">
<div className="flex items-center gap-2">
<span className="text-xs font-medium text-[#475569]">From:</span>
<input
type="date"
value={startDate}
onChange={(e) => {
setStartDate(e.target.value);
setCurrentPage(1);
}}
className="text-xs px-2 py-1.5 border border-[rgba(0,0,0,0.08)] rounded focus:outline-none focus:ring-1 focus:ring-[#112868]/20"
/>
</div>
<div className="flex items-center gap-2">
<span className="text-xs font-medium text-[#475569]">To:</span>
<input
type="date"
value={endDate}
onChange={(e) => {
setEndDate(e.target.value);
setCurrentPage(1);
}}
className="text-xs px-2 py-1.5 border border-[rgba(0,0,0,0.08)] rounded focus:outline-none focus:ring-1 focus:ring-[#112868]/20"
/>
</div>
{(startDate ||
endDate ||
actionFilter ||
resourceTypeFilter ||
methodFilter ||
moduleIdFilter ||
search) && (
<button
onClick={() => {
setStartDate("");
setEndDate("");
setActionFilter(null);
setResourceTypeFilter(null);
setMethodFilter(null);
setModuleIdFilter(null);
setSearch("");
setCurrentPage(1);
}}
className="text-xs hover:underline decoration-offset-2"
style={{ color: primaryColor }}
>
Clear all filters
</button>
)}
</div>
</div>
{/* Table */}