feat: add MoreFilters component and integrate into superadmin and tenant AuditLogs pages
This commit is contained in:
parent
277600edf0
commit
b16e6d9c18
96
src/components/shared/MoreFilters.tsx
Normal file
96
src/components/shared/MoreFilters.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@ -45,3 +45,4 @@ export { SearchBox } from './SearchBox';
|
||||
export { FormTagInput } from './FormTagInput';
|
||||
export { MarkdownViewer } from './MarkdownViewer';
|
||||
export { GradientStatCard } from './GradientStatCard';
|
||||
export { MoreFilters } from './MoreFilters';
|
||||
|
||||
@ -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 */}
|
||||
|
||||
@ -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 */}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user