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 { FormTagInput } from './FormTagInput';
|
||||||
export { MarkdownViewer } from './MarkdownViewer';
|
export { MarkdownViewer } from './MarkdownViewer';
|
||||||
export { GradientStatCard } from './GradientStatCard';
|
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 type { ReactElement } from "react";
|
||||||
import { Layout } from "@/components/layout/Layout";
|
import { Layout } from "@/components/layout/Layout";
|
||||||
import {
|
import {
|
||||||
@ -8,19 +8,18 @@ import {
|
|||||||
FilterDropdown,
|
FilterDropdown,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
type Column,
|
type Column,
|
||||||
|
MoreFilters,
|
||||||
|
FormSelect,
|
||||||
|
FormField,
|
||||||
} from "@/components/shared";
|
} from "@/components/shared";
|
||||||
import {
|
import {
|
||||||
Download,
|
Download,
|
||||||
ArrowUpDown,
|
|
||||||
Search,
|
Search,
|
||||||
ChevronDown,
|
|
||||||
SlidersHorizontal,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { auditLogService } from "@/services/audit-log-service";
|
import { auditLogService } from "@/services/audit-log-service";
|
||||||
import { tenantService } from "@/services/tenant-service";
|
import { tenantService } from "@/services/tenant-service";
|
||||||
import type { AuditLog } from "@/types/audit-log";
|
import type { AuditLog } from "@/types/audit-log";
|
||||||
import type { Tenant } from "@/types/tenant";
|
import type { Tenant } from "@/types/tenant";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { useAppTheme } from "@/hooks/useAppTheme";
|
import { useAppTheme } from "@/hooks/useAppTheme";
|
||||||
|
|
||||||
// Helper function to format date
|
// Helper function to format date
|
||||||
@ -82,7 +81,7 @@ const getStatusColor = (status: number | null): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AuditLogs = (): ReactElement => {
|
const AuditLogs = (): ReactElement => {
|
||||||
const { primaryColor } = useAppTheme();
|
useAppTheme();
|
||||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||||
const [auditLogs, setAuditLogs] = useState<AuditLog[]>([]);
|
const [auditLogs, setAuditLogs] = useState<AuditLog[]>([]);
|
||||||
const [tenants, setTenants] = useState<Tenant[]>([]);
|
const [tenants, setTenants] = useState<Tenant[]>([]);
|
||||||
@ -125,13 +124,12 @@ const AuditLogs = (): ReactElement => {
|
|||||||
const [orderBy, setOrderBy] = useState<string[] | null>(null);
|
const [orderBy, setOrderBy] = useState<string[] | null>(null);
|
||||||
const [search, setSearch] = useState<string>("");
|
const [search, setSearch] = useState<string>("");
|
||||||
const [debouncedSearch, setDebouncedSearch] = useState<string>("");
|
const [debouncedSearch, setDebouncedSearch] = useState<string>("");
|
||||||
const [showMoreFilters, setShowMoreFilters] = useState<boolean>(false);
|
const [isMoreFiltersOpen, setIsMoreFiltersOpen] = useState<boolean>(false);
|
||||||
|
const [tempResourceType, setTempResourceType] = useState<string | null>(null);
|
||||||
const hasExtraFilters = useMemo(
|
const [tempModule, setTempModule] = useState<string | null>(null);
|
||||||
() =>
|
const [tempOrderBy, setTempOrderBy] = useState<string[] | null>(null);
|
||||||
Boolean(moduleFilter || methodFilter || startDate || endDate || orderBy),
|
const [tempStartDate, setTempStartDate] = useState<string>("");
|
||||||
[moduleFilter, methodFilter, startDate, endDate, orderBy],
|
const [tempEndDate, setTempEndDate] = useState<string>("");
|
||||||
);
|
|
||||||
|
|
||||||
// View modal
|
// View modal
|
||||||
const [viewModalOpen, setViewModalOpen] = useState<boolean>(false);
|
const [viewModalOpen, setViewModalOpen] = useState<boolean>(false);
|
||||||
@ -183,11 +181,7 @@ const AuditLogs = (): ReactElement => {
|
|||||||
fetchModules();
|
fetchModules();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (hasExtraFilters) {
|
|
||||||
setShowMoreFilters(true);
|
|
||||||
}
|
|
||||||
}, [hasExtraFilters]);
|
|
||||||
|
|
||||||
const fetchAuditLogs = async (): Promise<void> => {
|
const fetchAuditLogs = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
@ -574,43 +568,132 @@ const AuditLogs = (): ReactElement => {
|
|||||||
placeholder="All Actions"
|
placeholder="All Actions"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Resource Filter */}
|
{/* More Filters Popover */}
|
||||||
<FilterDropdown
|
<MoreFilters
|
||||||
label="Resource Type"
|
title="Filter Logs"
|
||||||
options={resourceTypes}
|
isOpen={isMoreFiltersOpen}
|
||||||
value={resourceTypeFilter}
|
onOpenToggle={(open) => {
|
||||||
onChange={(value) => {
|
if (open) {
|
||||||
setResourceTypeFilter(value as string | null);
|
setTempResourceType(resourceTypeFilter);
|
||||||
setCurrentPage(1);
|
setTempModule(moduleFilter);
|
||||||
|
setTempOrderBy(orderBy);
|
||||||
|
setTempStartDate(startDate);
|
||||||
|
setTempEndDate(endDate);
|
||||||
|
}
|
||||||
|
setIsMoreFiltersOpen(open);
|
||||||
}}
|
}}
|
||||||
placeholder="All Resources"
|
onApply={() => {
|
||||||
isSearchable
|
setResourceTypeFilter(tempResourceType);
|
||||||
/>
|
setModuleFilter(tempModule);
|
||||||
|
setOrderBy(tempOrderBy);
|
||||||
<button
|
setStartDate(tempStartDate);
|
||||||
type="button"
|
setEndDate(tempEndDate);
|
||||||
onClick={() => setShowMoreFilters((open) => !open)}
|
setCurrentPage(1);
|
||||||
className={cn(
|
setIsMoreFiltersOpen(false);
|
||||||
"inline-flex items-center gap-1.5 h-10 px-3 rounded-md text-sm font-medium border bg-white transition-colors",
|
}}
|
||||||
showMoreFilters || hasExtraFilters
|
onCancel={() => setIsMoreFiltersOpen(false)}
|
||||||
? "border-[rgba(8,76,200,0.35)] text-[#0f1724]"
|
hasActiveFilters={Boolean(resourceTypeFilter || moduleFilter || orderBy || startDate || endDate)}
|
||||||
: "border-[rgba(0,0,0,0.08)] text-[#475569] hover:border-[#084cc8]/30",
|
|
||||||
)}
|
|
||||||
style={
|
|
||||||
showMoreFilters || hasExtraFilters
|
|
||||||
? { borderColor: `${primaryColor}55`, color: primaryColor }
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<SlidersHorizontal className="w-3.5 h-3.5 shrink-0" />
|
{/* Dropdowns row 1 */}
|
||||||
More filters
|
<div className="flex flex-col sm:flex-row items-start justify-center gap-[20px] self-stretch">
|
||||||
<ChevronDown
|
{/* Resource Type */}
|
||||||
className={cn(
|
<div className="flex-1 w-full">
|
||||||
"w-3.5 h-3.5 shrink-0 opacity-70 transition-transform",
|
<FormSelect
|
||||||
showMoreFilters && "rotate-180",
|
label="Resource Type"
|
||||||
)}
|
placeholder="All Resources"
|
||||||
/>
|
options={resourceTypes}
|
||||||
</button>
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
@ -625,118 +708,6 @@ const AuditLogs = (): ReactElement => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
|
|||||||
@ -9,8 +9,11 @@ import {
|
|||||||
StatusBadge,
|
StatusBadge,
|
||||||
SearchBox,
|
SearchBox,
|
||||||
type Column,
|
type Column,
|
||||||
|
MoreFilters,
|
||||||
|
FormSelect,
|
||||||
|
FormField,
|
||||||
} from "@/components/shared";
|
} from "@/components/shared";
|
||||||
import { Download, ArrowUpDown } from "lucide-react";
|
import { Download } from "lucide-react";
|
||||||
import { auditLogService } from "@/services/audit-log-service";
|
import { auditLogService } from "@/services/audit-log-service";
|
||||||
import { moduleService } from "@/services/module-service";
|
import { moduleService } from "@/services/module-service";
|
||||||
import type { AuditLog } from "@/types/audit-log";
|
import type { AuditLog } from "@/types/audit-log";
|
||||||
@ -120,7 +123,7 @@ const AuditLogs = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Filter state
|
// Filter state
|
||||||
const [methodFilter, setMethodFilter] = useState<string | null>(null);
|
const methodFilter = null;
|
||||||
const [actionFilter, setActionFilter] = useState<string | null>(null);
|
const [actionFilter, setActionFilter] = useState<string | null>(null);
|
||||||
const [resourceTypeFilter, setResourceTypeFilter] = useState<string | null>(
|
const [resourceTypeFilter, setResourceTypeFilter] = useState<string | null>(
|
||||||
null,
|
null,
|
||||||
@ -131,8 +134,12 @@ const AuditLogs = ({
|
|||||||
const [orderBy, setOrderBy] = useState<string[] | null>(null);
|
const [orderBy, setOrderBy] = useState<string[] | null>(null);
|
||||||
const [search, setSearch] = useState<string>("");
|
const [search, setSearch] = useState<string>("");
|
||||||
const [debouncedSearch, setDebouncedSearch] = useState<string>("");
|
const [debouncedSearch, setDebouncedSearch] = useState<string>("");
|
||||||
|
// More Filters popover state
|
||||||
// View modal
|
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 [viewModalOpen, setViewModalOpen] = useState<boolean>(false);
|
||||||
const [selectedAuditLogId, setSelectedAuditLogId] = useState<string | null>(
|
const [selectedAuditLogId, setSelectedAuditLogId] = useState<string | null>(
|
||||||
null,
|
null,
|
||||||
@ -522,7 +529,7 @@ const AuditLogs = ({
|
|||||||
{/* Table Container */}
|
{/* Table Container */}
|
||||||
<div className="overflow-hidden">
|
<div className="overflow-hidden">
|
||||||
{/* Table Header with Filters */}
|
{/* 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">
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
||||||
{/* Search and Filters */}
|
{/* Search and Filters */}
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
@ -535,41 +542,26 @@ const AuditLogs = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{isTenantAdmin && (
|
{isTenantAdmin && (
|
||||||
<>
|
/* Action Filter */
|
||||||
{/* Action Filter */}
|
<FilterDropdown
|
||||||
<FilterDropdown
|
label="Action"
|
||||||
label="Action"
|
options={[
|
||||||
options={[
|
{ value: "LOGIN", label: "LOGIN" },
|
||||||
{ value: "LOGIN", label: "LOGIN" },
|
{ value: "LOGOUT", label: "LOGOUT" },
|
||||||
{ value: "LOGOUT", label: "LOGOUT" },
|
{ value: "CREATE", label: "CREATE" },
|
||||||
{ value: "CREATE", label: "CREATE" },
|
{ value: "UPDATE", label: "UPDATE" },
|
||||||
{ value: "UPDATE", label: "UPDATE" },
|
{ value: "DELETE", label: "DELETE" },
|
||||||
{ value: "DELETE", label: "DELETE" },
|
{ value: "SUBMIT", label: "SUBMIT" },
|
||||||
{ value: "SUBMIT", label: "SUBMIT" },
|
{ value: "APPROVE", label: "APPROVE" },
|
||||||
{ value: "APPROVE", label: "APPROVE" },
|
{ value: "REJECT", label: "REJECT" },
|
||||||
{ value: "REJECT", label: "REJECT" },
|
]}
|
||||||
]}
|
value={actionFilter}
|
||||||
value={actionFilter}
|
onChange={(value) => {
|
||||||
onChange={(value) => {
|
setActionFilter(value as string | null);
|
||||||
setActionFilter(value as string | null);
|
setCurrentPage(1);
|
||||||
setCurrentPage(1);
|
}}
|
||||||
}}
|
placeholder="All Actions"
|
||||||
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
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Module Filter */}
|
{/* Module Filter */}
|
||||||
@ -584,27 +576,110 @@ const AuditLogs = ({
|
|||||||
placeholder="All Modules"
|
placeholder="All Modules"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Sort Filter - Always show as it's useful for everyone */}
|
{/* More Filters Popover */}
|
||||||
<FilterDropdown
|
<MoreFilters
|
||||||
label="Sort by"
|
title="Filter Logs"
|
||||||
options={[
|
isOpen={isMoreFiltersOpen}
|
||||||
{ value: ["created_at", "desc"], label: "Newest First" },
|
onOpenToggle={(open) => {
|
||||||
{ value: ["created_at", "asc"], label: "Oldest First" },
|
if (open) {
|
||||||
{ value: ["action", "asc"], label: "Action (A-Z)" },
|
setTempResourceType(resourceTypeFilter);
|
||||||
{
|
setTempOrderBy(orderBy);
|
||||||
value: ["resource_type", "asc"],
|
setTempStartDate(startDate);
|
||||||
label: "Resource Type (A-Z)",
|
setTempEndDate(endDate);
|
||||||
},
|
}
|
||||||
]}
|
setIsMoreFiltersOpen(open);
|
||||||
value={orderBy}
|
|
||||||
onChange={(value) => {
|
|
||||||
setOrderBy(value as string[] | null);
|
|
||||||
setCurrentPage(1);
|
|
||||||
}}
|
}}
|
||||||
placeholder="Newest"
|
onApply={() => {
|
||||||
showIcon
|
setResourceTypeFilter(tempResourceType);
|
||||||
icon={<ArrowUpDown className="w-3.5 h-3.5 text-[#475569]" />}
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
@ -621,58 +696,6 @@ const AuditLogs = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user