add audit page
This commit is contained in:
parent
0d9a1ec002
commit
9df4401c47
|
|
@ -109,7 +109,7 @@ export function CommandActionButtons({ roomName, selectedDevices = [] }: Command
|
|||
// All rendered commands are sourced from sensitiveCommands — send via sensitive mutation
|
||||
await executeSensitiveMutation.mutateAsync({
|
||||
roomName,
|
||||
command: confirmDialog.command.commandContent,
|
||||
command: confirmDialog.command.commandName,
|
||||
password: sensitivePassword,
|
||||
});
|
||||
|
||||
|
|
|
|||
23
src/components/columns/agent-column.tsx
Normal file
23
src/components/columns/agent-column.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import type { Version } from "@/types/file";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
|
||||
export const agentColumns: ColumnDef<Version>[] = [
|
||||
{ accessorKey: "version", header: "Phiên bản" },
|
||||
{ accessorKey: "fileName", header: "Tên file" },
|
||||
{
|
||||
accessorKey: "updatedAt",
|
||||
header: "Thời gian cập nhật",
|
||||
cell: ({ getValue }) =>
|
||||
getValue()
|
||||
? new Date(getValue() as string).toLocaleString("vi-VN")
|
||||
: "N/A",
|
||||
},
|
||||
{
|
||||
accessorKey: "requestUpdateAt",
|
||||
header: "Thời gian yêu cầu cập nhật",
|
||||
cell: ({ getValue }) =>
|
||||
getValue()
|
||||
? new Date(getValue() as string).toLocaleString("vi-VN")
|
||||
: "N/A",
|
||||
},
|
||||
];
|
||||
70
src/components/columns/apps-column.tsx
Normal file
70
src/components/columns/apps-column.tsx
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
// components/columns/apps-column.tsx
|
||||
import type { Version } from "@/types/file";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Check, X } from "lucide-react";
|
||||
|
||||
// Không gọi hook ở đây — nhận isPending từ ngoài truyền vào
|
||||
export function createAppsColumns(isPending: boolean): ColumnDef<Version>[] {
|
||||
return [
|
||||
{ accessorKey: "version", header: "Phiên bản" },
|
||||
{ accessorKey: "fileName", header: "Tên file" },
|
||||
{
|
||||
accessorKey: "updatedAt",
|
||||
header: () => (
|
||||
<div className="whitespace-normal max-w-xs">Thời gian cập nhật</div>
|
||||
),
|
||||
cell: ({ getValue }) =>
|
||||
getValue()
|
||||
? new Date(getValue() as string).toLocaleString("vi-VN")
|
||||
: "N/A",
|
||||
},
|
||||
{
|
||||
accessorKey: "requestUpdateAt",
|
||||
header: () => (
|
||||
<div className="whitespace-normal max-w-xs">
|
||||
Thời gian yêu cầu cài đặt/tải xuống
|
||||
</div>
|
||||
),
|
||||
cell: ({ getValue }) =>
|
||||
getValue()
|
||||
? new Date(getValue() as string).toLocaleString("vi-VN")
|
||||
: "N/A",
|
||||
},
|
||||
{
|
||||
id: "required",
|
||||
header: () => (
|
||||
<div className="whitespace-normal max-w-xs">Đã thêm vào danh sách</div>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const isRequired = row.original.isRequired;
|
||||
return isRequired ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<Check className="h-4 w-4 text-green-600" />
|
||||
<span className="text-sm text-green-600">Có</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
<X className="h-4 w-4 text-gray-400" />
|
||||
<span className="text-sm text-gray-400">Không</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
{
|
||||
id: "select",
|
||||
header: () => <div className="whitespace-normal max-w-xs">Chọn</div>,
|
||||
cell: ({ row }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={row.getIsSelected?.() ?? false}
|
||||
onChange={row.getToggleSelectedHandler?.()}
|
||||
disabled={isPending} // ← nhận từ tham số, không gọi hook
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
98
src/components/columns/audit-column.tsx
Normal file
98
src/components/columns/audit-column.tsx
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { type ColumnDef } from "@tanstack/react-table";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import type { Audits } from "@/types/audit";
|
||||
|
||||
export const auditColumns: ColumnDef<Audits>[] = [
|
||||
{
|
||||
header: "Thời gian",
|
||||
accessorKey: "dateTime",
|
||||
cell: ({ getValue }) => {
|
||||
const v = getValue() as string;
|
||||
const d = v ? new Date(v) : null;
|
||||
return d ? (
|
||||
<div className="text-sm whitespace-nowrap">
|
||||
<div className="font-medium">{d.toLocaleDateString("vi-VN")}</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
{d.toLocaleTimeString("vi-VN")}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-muted-foreground">—</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "User",
|
||||
accessorKey: "username",
|
||||
cell: ({ getValue }) => (
|
||||
<span className="font-medium text-sm whitespace-nowrap">
|
||||
{getValue() as string}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Loại",
|
||||
accessorKey: "apiCall",
|
||||
cell: ({ getValue }) => {
|
||||
const v = (getValue() as string) ?? "";
|
||||
if (!v) return <span className="text-muted-foreground">—</span>;
|
||||
return (
|
||||
<code className="text-xs bg-muted px-1.5 py-0.5 rounded whitespace-nowrap">
|
||||
{v}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Hành động",
|
||||
accessorKey: "action",
|
||||
cell: ({ getValue }) => (
|
||||
<code className="text-xs bg-muted px-1.5 py-0.5 rounded whitespace-nowrap">
|
||||
{getValue() as string}
|
||||
</code>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "URL",
|
||||
accessorKey: "url",
|
||||
cell: ({ getValue }) => (
|
||||
<code className="text-xs text-muted-foreground max-w-[180px] truncate block">
|
||||
{(getValue() as string) ?? "—"}
|
||||
</code>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Kết quả",
|
||||
accessorKey: "isSuccess",
|
||||
cell: ({ getValue }) => {
|
||||
const v = getValue();
|
||||
if (v == null) return <span className="text-muted-foreground">—</span>;
|
||||
return v ? (
|
||||
<Badge variant="outline" className="text-green-600 border-green-600 whitespace-nowrap">
|
||||
Thành công
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="text-red-600 border-red-600 whitespace-nowrap">
|
||||
Thất bại
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Nội dung request",
|
||||
accessorKey: "requestPayload",
|
||||
cell: ({ getValue }) => {
|
||||
const v = getValue() as string;
|
||||
if (!v) return <span className="text-muted-foreground">—</span>;
|
||||
let preview = v;
|
||||
try {
|
||||
preview = JSON.stringify(JSON.parse(v));
|
||||
} catch {}
|
||||
return (
|
||||
<span className="text-xs text-muted-foreground max-w-[200px] truncate block">
|
||||
{preview}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
180
src/components/dialogs/audit-detail-dialog.tsx
Normal file
180
src/components/dialogs/audit-detail-dialog.tsx
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import type { Audits } from "@/types/audit";
|
||||
|
||||
function JsonDisplay({ value }: { value: string | null | undefined }) {
|
||||
if (!value) return <span className="text-muted-foreground">—</span>;
|
||||
try {
|
||||
return (
|
||||
<pre className="text-xs bg-muted/60 p-2.5 rounded-md overflow-auto whitespace-pre-wrap break-all leading-relaxed max-h-48 font-mono">
|
||||
{JSON.stringify(JSON.parse(value), null, 2)}
|
||||
</pre>
|
||||
);
|
||||
} catch {
|
||||
return <span className="text-xs break-all font-mono">{value}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
interface AuditDetailDialogProps {
|
||||
audit: Audits | null;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function AuditDetailDialog({
|
||||
audit,
|
||||
open,
|
||||
onClose,
|
||||
}: AuditDetailDialogProps) {
|
||||
if (!audit) return null;
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(o) => !o && onClose()}>
|
||||
<DialogContent className="max-w-2xl w-full max-h-[85vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
Chi tiết audit
|
||||
<span className="text-muted-foreground font-normal text-sm">
|
||||
#{audit.id}
|
||||
</span>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="grid grid-cols-2 gap-x-6 gap-y-3 pt-1">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide">
|
||||
Thời gian
|
||||
</p>
|
||||
<p className="text-sm font-medium">
|
||||
{audit.dateTime
|
||||
? new Date(audit.dateTime).toLocaleString("vi-VN")
|
||||
: "—"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide">
|
||||
User
|
||||
</p>
|
||||
<p className="text-sm font-medium">{audit.username}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide">
|
||||
API Call
|
||||
</p>
|
||||
<code className="text-xs bg-muted px-1.5 py-0.5 rounded">
|
||||
{audit.apiCall ?? "—"}
|
||||
</code>
|
||||
</div>
|
||||
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide">
|
||||
Kết quả
|
||||
</p>
|
||||
<div>
|
||||
{audit.isSuccess == null ? (
|
||||
<span className="text-muted-foreground text-sm">—</span>
|
||||
) : audit.isSuccess ? (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-green-600 border-green-600"
|
||||
>
|
||||
Thành công
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-red-600 border-red-600"
|
||||
>
|
||||
Thất bại
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide">
|
||||
Hành động
|
||||
</p>
|
||||
<code className="text-xs bg-muted px-1.5 py-0.5 rounded">
|
||||
{audit.action}
|
||||
</code>
|
||||
</div>
|
||||
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide">
|
||||
URL
|
||||
</p>
|
||||
<code className="text-xs text-muted-foreground break-all">
|
||||
{audit.url ?? "—"}
|
||||
</code>
|
||||
</div>
|
||||
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide">
|
||||
Bảng
|
||||
</p>
|
||||
<p className="text-sm">{audit.tableName ?? "—"}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide">
|
||||
Entity ID
|
||||
</p>
|
||||
<p className="text-sm">{audit.entityId ?? "—"}</p>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2 space-y-0.5">
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide">
|
||||
Lỗi
|
||||
</p>
|
||||
<p className="text-sm text-red-600">{audit.errorMessage ?? "—"}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide">
|
||||
Nội dung request
|
||||
</p>
|
||||
<JsonDisplay value={audit.requestPayload} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide">
|
||||
Giá trị cũ
|
||||
</p>
|
||||
<JsonDisplay value={audit.oldValues} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide">
|
||||
Giá trị mới
|
||||
</p>
|
||||
<JsonDisplay value={audit.newValues} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide">
|
||||
Kết quả
|
||||
</p>
|
||||
<p className="text-sm">{audit.isSuccess == null ? "—" : audit.isSuccess ? "Thành công" : "Thất bại"}</p>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
73
src/components/filters/audit-filter-bar.tsx
Normal file
73
src/components/filters/audit-filter-bar.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
interface AuditFilterBarProps {
|
||||
username: string | null;
|
||||
action: string | null;
|
||||
from: string | null;
|
||||
to: string | null;
|
||||
isLoading: boolean;
|
||||
isFetching: boolean;
|
||||
onUsernameChange: (v: string | null) => void;
|
||||
onActionChange: (v: string | null) => void;
|
||||
onFromChange: (v: string | null) => void;
|
||||
onToChange: (v: string | null) => void;
|
||||
onSearch: () => void;
|
||||
onReset: () => void;
|
||||
}
|
||||
|
||||
export function AuditFilterBar({
|
||||
username,
|
||||
action,
|
||||
from,
|
||||
to,
|
||||
isLoading,
|
||||
isFetching,
|
||||
onUsernameChange,
|
||||
onActionChange,
|
||||
onFromChange,
|
||||
onToChange,
|
||||
onSearch,
|
||||
onReset,
|
||||
}: AuditFilterBarProps) {
|
||||
return (
|
||||
<div className="flex gap-2 mb-4 flex-wrap items-end">
|
||||
<Input
|
||||
className="w-36"
|
||||
placeholder="Username"
|
||||
value={username ?? ""}
|
||||
onChange={(e) => onUsernameChange(e.target.value || null)}
|
||||
/>
|
||||
|
||||
<Input
|
||||
className="w-44"
|
||||
placeholder="Hành động..."
|
||||
value={action ?? ""}
|
||||
onChange={(e) => onActionChange(e.target.value || null)}
|
||||
/>
|
||||
|
||||
<Input
|
||||
className="w-36"
|
||||
type="date"
|
||||
value={from ?? ""}
|
||||
onChange={(e) => onFromChange(e.target.value || null)}
|
||||
/>
|
||||
|
||||
<Input
|
||||
className="w-36"
|
||||
type="date"
|
||||
value={to ?? ""}
|
||||
onChange={(e) => onToChange(e.target.value || null)}
|
||||
/>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={onSearch} disabled={isFetching || isLoading} size="sm">
|
||||
Tìm
|
||||
</Button>
|
||||
<Button variant="outline" onClick={onReset} size="sm">
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -82,4 +82,12 @@ export const API_ENDPOINTS = {
|
|||
TOGGLE_PERMISSION: (roleId: number, permissionId: number) =>
|
||||
`${BASE_URL}/Role/${roleId}/permissions/${permissionId}/toggle`,
|
||||
},
|
||||
DASHBOARD:
|
||||
{
|
||||
|
||||
}
|
||||
,
|
||||
AUDIT: {
|
||||
GET_AUDITS: `${BASE_URL}/Audit/audits`,
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ export * from "./useDeviceCommQueries";
|
|||
// Command Queries
|
||||
export * from "./useCommandQueries";
|
||||
|
||||
// Audit Queries
|
||||
export * from "./useAuditQueries";
|
||||
|
||||
// Permission Queries
|
||||
export * from "./usePermissionQueries";
|
||||
|
||||
|
|
|
|||
37
src/hooks/queries/useAuditQueries.ts
Normal file
37
src/hooks/queries/useAuditQueries.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { useQuery } from "@tanstack/react-query";
|
||||
import * as auditService from "@/services/audit.service";
|
||||
import type { PageResult, Audits } from "@/types/audit";
|
||||
|
||||
const AUDIT_QUERY_KEYS = {
|
||||
all: ["audit"] as const,
|
||||
list: () => [...AUDIT_QUERY_KEYS.all, "list"] as const,
|
||||
audits: (params: any) => [...AUDIT_QUERY_KEYS.all, "audits", params] as const,
|
||||
};
|
||||
|
||||
export function useGetAudits(
|
||||
params: {
|
||||
pageNumber?: number;
|
||||
pageSize?: number;
|
||||
username?: string | null;
|
||||
action?: string | null;
|
||||
from?: string | null;
|
||||
to?: string | null;
|
||||
} = { pageNumber: 1, pageSize: 20 },
|
||||
enabled = true
|
||||
) {
|
||||
const { pageNumber = 1, pageSize = 20, username, action, from, to } = params;
|
||||
|
||||
return useQuery<PageResult<Audits>>({
|
||||
queryKey: AUDIT_QUERY_KEYS.audits({ pageNumber, pageSize, username, action, from, to }),
|
||||
queryFn: () =>
|
||||
auditService.getAudits(
|
||||
pageNumber,
|
||||
pageSize,
|
||||
username ?? null,
|
||||
action ?? null,
|
||||
from ?? null,
|
||||
to ?? null
|
||||
),
|
||||
enabled,
|
||||
});
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ import { Route as AuthDeviceIndexRouteImport } from './routes/_auth/device/index
|
|||
import { Route as AuthDashboardIndexRouteImport } from './routes/_auth/dashboard/index'
|
||||
import { Route as AuthCommandsIndexRouteImport } from './routes/_auth/commands/index'
|
||||
import { Route as AuthBlacklistsIndexRouteImport } from './routes/_auth/blacklists/index'
|
||||
import { Route as AuthAuditsIndexRouteImport } from './routes/_auth/audits/index'
|
||||
import { Route as AuthAppsIndexRouteImport } from './routes/_auth/apps/index'
|
||||
import { Route as AuthAgentIndexRouteImport } from './routes/_auth/agent/index'
|
||||
import { Route as authLoginIndexRouteImport } from './routes/(auth)/login/index'
|
||||
|
|
@ -29,6 +30,7 @@ import { Route as AuthProfileUserNameIndexRouteImport } from './routes/_auth/pro
|
|||
import { Route as AuthUserRoleRoleIdIndexRouteImport } from './routes/_auth/user/role/$roleId/index'
|
||||
import { Route as AuthUserChangePasswordUserNameIndexRouteImport } from './routes/_auth/user/change-password/$userName/index'
|
||||
import { Route as AuthRoomsRoomNameFolderStatusIndexRouteImport } from './routes/_auth/rooms/$roomName/folder-status/index'
|
||||
import { Route as AuthRoomsRoomNameConnectIndexRouteImport } from './routes/_auth/rooms/$roomName/connect/index'
|
||||
import { Route as AuthRoleIdEditIndexRouteImport } from './routes/_auth/role/$id/edit/index'
|
||||
|
||||
const AuthRoute = AuthRouteImport.update({
|
||||
|
|
@ -75,6 +77,11 @@ const AuthBlacklistsIndexRoute = AuthBlacklistsIndexRouteImport.update({
|
|||
path: '/blacklists/',
|
||||
getParentRoute: () => AuthRoute,
|
||||
} as any)
|
||||
const AuthAuditsIndexRoute = AuthAuditsIndexRouteImport.update({
|
||||
id: '/audits/',
|
||||
path: '/audits/',
|
||||
getParentRoute: () => AuthRoute,
|
||||
} as any)
|
||||
const AuthAppsIndexRoute = AuthAppsIndexRouteImport.update({
|
||||
id: '/apps/',
|
||||
path: '/apps/',
|
||||
|
|
@ -134,6 +141,12 @@ const AuthRoomsRoomNameFolderStatusIndexRoute =
|
|||
path: '/rooms/$roomName/folder-status/',
|
||||
getParentRoute: () => AuthRoute,
|
||||
} as any)
|
||||
const AuthRoomsRoomNameConnectIndexRoute =
|
||||
AuthRoomsRoomNameConnectIndexRouteImport.update({
|
||||
id: '/rooms/$roomName/connect/',
|
||||
path: '/rooms/$roomName/connect/',
|
||||
getParentRoute: () => AuthRoute,
|
||||
} as any)
|
||||
const AuthRoleIdEditIndexRoute = AuthRoleIdEditIndexRouteImport.update({
|
||||
id: '/role/$id/edit/',
|
||||
path: '/role/$id/edit/',
|
||||
|
|
@ -145,6 +158,7 @@ export interface FileRoutesByFullPath {
|
|||
'/login': typeof authLoginIndexRoute
|
||||
'/agent': typeof AuthAgentIndexRoute
|
||||
'/apps': typeof AuthAppsIndexRoute
|
||||
'/audits': typeof AuthAuditsIndexRoute
|
||||
'/blacklists': typeof AuthBlacklistsIndexRoute
|
||||
'/commands': typeof AuthCommandsIndexRoute
|
||||
'/dashboard': typeof AuthDashboardIndexRoute
|
||||
|
|
@ -158,6 +172,7 @@ export interface FileRoutesByFullPath {
|
|||
'/rooms/$roomName': typeof AuthRoomsRoomNameIndexRoute
|
||||
'/user/create': typeof AuthUserCreateIndexRoute
|
||||
'/role/$id/edit': typeof AuthRoleIdEditIndexRoute
|
||||
'/rooms/$roomName/connect': typeof AuthRoomsRoomNameConnectIndexRoute
|
||||
'/rooms/$roomName/folder-status': typeof AuthRoomsRoomNameFolderStatusIndexRoute
|
||||
'/user/change-password/$userName': typeof AuthUserChangePasswordUserNameIndexRoute
|
||||
'/user/role/$roleId': typeof AuthUserRoleRoleIdIndexRoute
|
||||
|
|
@ -167,6 +182,7 @@ export interface FileRoutesByTo {
|
|||
'/login': typeof authLoginIndexRoute
|
||||
'/agent': typeof AuthAgentIndexRoute
|
||||
'/apps': typeof AuthAppsIndexRoute
|
||||
'/audits': typeof AuthAuditsIndexRoute
|
||||
'/blacklists': typeof AuthBlacklistsIndexRoute
|
||||
'/commands': typeof AuthCommandsIndexRoute
|
||||
'/dashboard': typeof AuthDashboardIndexRoute
|
||||
|
|
@ -180,6 +196,7 @@ export interface FileRoutesByTo {
|
|||
'/rooms/$roomName': typeof AuthRoomsRoomNameIndexRoute
|
||||
'/user/create': typeof AuthUserCreateIndexRoute
|
||||
'/role/$id/edit': typeof AuthRoleIdEditIndexRoute
|
||||
'/rooms/$roomName/connect': typeof AuthRoomsRoomNameConnectIndexRoute
|
||||
'/rooms/$roomName/folder-status': typeof AuthRoomsRoomNameFolderStatusIndexRoute
|
||||
'/user/change-password/$userName': typeof AuthUserChangePasswordUserNameIndexRoute
|
||||
'/user/role/$roleId': typeof AuthUserRoleRoleIdIndexRoute
|
||||
|
|
@ -191,6 +208,7 @@ export interface FileRoutesById {
|
|||
'/(auth)/login/': typeof authLoginIndexRoute
|
||||
'/_auth/agent/': typeof AuthAgentIndexRoute
|
||||
'/_auth/apps/': typeof AuthAppsIndexRoute
|
||||
'/_auth/audits/': typeof AuthAuditsIndexRoute
|
||||
'/_auth/blacklists/': typeof AuthBlacklistsIndexRoute
|
||||
'/_auth/commands/': typeof AuthCommandsIndexRoute
|
||||
'/_auth/dashboard/': typeof AuthDashboardIndexRoute
|
||||
|
|
@ -204,6 +222,7 @@ export interface FileRoutesById {
|
|||
'/_auth/rooms/$roomName/': typeof AuthRoomsRoomNameIndexRoute
|
||||
'/_auth/user/create/': typeof AuthUserCreateIndexRoute
|
||||
'/_auth/role/$id/edit/': typeof AuthRoleIdEditIndexRoute
|
||||
'/_auth/rooms/$roomName/connect/': typeof AuthRoomsRoomNameConnectIndexRoute
|
||||
'/_auth/rooms/$roomName/folder-status/': typeof AuthRoomsRoomNameFolderStatusIndexRoute
|
||||
'/_auth/user/change-password/$userName/': typeof AuthUserChangePasswordUserNameIndexRoute
|
||||
'/_auth/user/role/$roleId/': typeof AuthUserRoleRoleIdIndexRoute
|
||||
|
|
@ -215,6 +234,7 @@ export interface FileRouteTypes {
|
|||
| '/login'
|
||||
| '/agent'
|
||||
| '/apps'
|
||||
| '/audits'
|
||||
| '/blacklists'
|
||||
| '/commands'
|
||||
| '/dashboard'
|
||||
|
|
@ -228,6 +248,7 @@ export interface FileRouteTypes {
|
|||
| '/rooms/$roomName'
|
||||
| '/user/create'
|
||||
| '/role/$id/edit'
|
||||
| '/rooms/$roomName/connect'
|
||||
| '/rooms/$roomName/folder-status'
|
||||
| '/user/change-password/$userName'
|
||||
| '/user/role/$roleId'
|
||||
|
|
@ -237,6 +258,7 @@ export interface FileRouteTypes {
|
|||
| '/login'
|
||||
| '/agent'
|
||||
| '/apps'
|
||||
| '/audits'
|
||||
| '/blacklists'
|
||||
| '/commands'
|
||||
| '/dashboard'
|
||||
|
|
@ -250,6 +272,7 @@ export interface FileRouteTypes {
|
|||
| '/rooms/$roomName'
|
||||
| '/user/create'
|
||||
| '/role/$id/edit'
|
||||
| '/rooms/$roomName/connect'
|
||||
| '/rooms/$roomName/folder-status'
|
||||
| '/user/change-password/$userName'
|
||||
| '/user/role/$roleId'
|
||||
|
|
@ -260,6 +283,7 @@ export interface FileRouteTypes {
|
|||
| '/(auth)/login/'
|
||||
| '/_auth/agent/'
|
||||
| '/_auth/apps/'
|
||||
| '/_auth/audits/'
|
||||
| '/_auth/blacklists/'
|
||||
| '/_auth/commands/'
|
||||
| '/_auth/dashboard/'
|
||||
|
|
@ -273,6 +297,7 @@ export interface FileRouteTypes {
|
|||
| '/_auth/rooms/$roomName/'
|
||||
| '/_auth/user/create/'
|
||||
| '/_auth/role/$id/edit/'
|
||||
| '/_auth/rooms/$roomName/connect/'
|
||||
| '/_auth/rooms/$roomName/folder-status/'
|
||||
| '/_auth/user/change-password/$userName/'
|
||||
| '/_auth/user/role/$roleId/'
|
||||
|
|
@ -349,6 +374,13 @@ declare module '@tanstack/react-router' {
|
|||
preLoaderRoute: typeof AuthBlacklistsIndexRouteImport
|
||||
parentRoute: typeof AuthRoute
|
||||
}
|
||||
'/_auth/audits/': {
|
||||
id: '/_auth/audits/'
|
||||
path: '/audits'
|
||||
fullPath: '/audits'
|
||||
preLoaderRoute: typeof AuthAuditsIndexRouteImport
|
||||
parentRoute: typeof AuthRoute
|
||||
}
|
||||
'/_auth/apps/': {
|
||||
id: '/_auth/apps/'
|
||||
path: '/apps'
|
||||
|
|
@ -426,6 +458,13 @@ declare module '@tanstack/react-router' {
|
|||
preLoaderRoute: typeof AuthRoomsRoomNameFolderStatusIndexRouteImport
|
||||
parentRoute: typeof AuthRoute
|
||||
}
|
||||
'/_auth/rooms/$roomName/connect/': {
|
||||
id: '/_auth/rooms/$roomName/connect/'
|
||||
path: '/rooms/$roomName/connect'
|
||||
fullPath: '/rooms/$roomName/connect'
|
||||
preLoaderRoute: typeof AuthRoomsRoomNameConnectIndexRouteImport
|
||||
parentRoute: typeof AuthRoute
|
||||
}
|
||||
'/_auth/role/$id/edit/': {
|
||||
id: '/_auth/role/$id/edit/'
|
||||
path: '/role/$id/edit'
|
||||
|
|
@ -439,6 +478,7 @@ declare module '@tanstack/react-router' {
|
|||
interface AuthRouteChildren {
|
||||
AuthAgentIndexRoute: typeof AuthAgentIndexRoute
|
||||
AuthAppsIndexRoute: typeof AuthAppsIndexRoute
|
||||
AuthAuditsIndexRoute: typeof AuthAuditsIndexRoute
|
||||
AuthBlacklistsIndexRoute: typeof AuthBlacklistsIndexRoute
|
||||
AuthCommandsIndexRoute: typeof AuthCommandsIndexRoute
|
||||
AuthDashboardIndexRoute: typeof AuthDashboardIndexRoute
|
||||
|
|
@ -452,6 +492,7 @@ interface AuthRouteChildren {
|
|||
AuthRoomsRoomNameIndexRoute: typeof AuthRoomsRoomNameIndexRoute
|
||||
AuthUserCreateIndexRoute: typeof AuthUserCreateIndexRoute
|
||||
AuthRoleIdEditIndexRoute: typeof AuthRoleIdEditIndexRoute
|
||||
AuthRoomsRoomNameConnectIndexRoute: typeof AuthRoomsRoomNameConnectIndexRoute
|
||||
AuthRoomsRoomNameFolderStatusIndexRoute: typeof AuthRoomsRoomNameFolderStatusIndexRoute
|
||||
AuthUserChangePasswordUserNameIndexRoute: typeof AuthUserChangePasswordUserNameIndexRoute
|
||||
AuthUserRoleRoleIdIndexRoute: typeof AuthUserRoleRoleIdIndexRoute
|
||||
|
|
@ -460,6 +501,7 @@ interface AuthRouteChildren {
|
|||
const AuthRouteChildren: AuthRouteChildren = {
|
||||
AuthAgentIndexRoute: AuthAgentIndexRoute,
|
||||
AuthAppsIndexRoute: AuthAppsIndexRoute,
|
||||
AuthAuditsIndexRoute: AuthAuditsIndexRoute,
|
||||
AuthBlacklistsIndexRoute: AuthBlacklistsIndexRoute,
|
||||
AuthCommandsIndexRoute: AuthCommandsIndexRoute,
|
||||
AuthDashboardIndexRoute: AuthDashboardIndexRoute,
|
||||
|
|
@ -473,6 +515,7 @@ const AuthRouteChildren: AuthRouteChildren = {
|
|||
AuthRoomsRoomNameIndexRoute: AuthRoomsRoomNameIndexRoute,
|
||||
AuthUserCreateIndexRoute: AuthUserCreateIndexRoute,
|
||||
AuthRoleIdEditIndexRoute: AuthRoleIdEditIndexRoute,
|
||||
AuthRoomsRoomNameConnectIndexRoute: AuthRoomsRoomNameConnectIndexRoute,
|
||||
AuthRoomsRoomNameFolderStatusIndexRoute:
|
||||
AuthRoomsRoomNameFolderStatusIndexRoute,
|
||||
AuthUserChangePasswordUserNameIndexRoute:
|
||||
|
|
|
|||
|
|
@ -7,11 +7,10 @@ import {
|
|||
useUpdateAgent,
|
||||
} from "@/hooks/queries";
|
||||
import { toast } from "sonner";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import type { AxiosProgressEvent } from "axios";
|
||||
import type { Version } from "@/types/file";
|
||||
import { ErrorFetchingPage } from "@/components/pages/error-fetching-page";
|
||||
|
||||
import { agentColumns } from "@/components/columns/agent-column";
|
||||
export const Route = createFileRoute("/_auth/agent/")({
|
||||
head: () => ({ meta: [{ title: "Quản lý Agent" }] }),
|
||||
component: AgentsPage,
|
||||
|
|
@ -71,26 +70,7 @@ function AgentsPage() {
|
|||
};
|
||||
|
||||
// Cột bảng
|
||||
const columns: ColumnDef<Version>[] = [
|
||||
{ accessorKey: "version", header: "Phiên bản" },
|
||||
{ accessorKey: "fileName", header: "Tên file" },
|
||||
{
|
||||
accessorKey: "updatedAt",
|
||||
header: "Thời gian cập nhật",
|
||||
cell: ({ getValue }) =>
|
||||
getValue()
|
||||
? new Date(getValue() as string).toLocaleString("vi-VN")
|
||||
: "N/A",
|
||||
},
|
||||
{
|
||||
accessorKey: "requestUpdateAt",
|
||||
header: "Thời gian yêu cầu cập nhật",
|
||||
cell: ({ getValue }) =>
|
||||
getValue()
|
||||
? new Date(getValue() as string).toLocaleString("vi-VN")
|
||||
: "N/A",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
return (
|
||||
<AppManagerTemplate<Version>
|
||||
|
|
@ -98,7 +78,7 @@ function AgentsPage() {
|
|||
description="Quản lý và theo dõi các phiên bản Agent"
|
||||
data={versionList}
|
||||
isLoading={isLoading}
|
||||
columns={columns}
|
||||
columns={agentColumns}
|
||||
onUpload={handleUpload}
|
||||
onUpdate={handleUpdate}
|
||||
updateLoading={updateMutation.isPending}
|
||||
|
|
|
|||
|
|
@ -11,12 +11,10 @@ import {
|
|||
useDownloadFiles,
|
||||
} from "@/hooks/queries";
|
||||
import { toast } from "sonner";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import type { AxiosProgressEvent } from "axios";
|
||||
import type { Version } from "@/types/file";
|
||||
import { Check, X } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { createAppsColumns } from "@/components/columns/apps-column";
|
||||
export const Route = createFileRoute("/_auth/apps/")({
|
||||
head: () => ({ meta: [{ title: "Quản lý phần mềm" }] }),
|
||||
component: AppsComponent,
|
||||
|
|
@ -51,62 +49,10 @@ function AppsComponent() {
|
|||
|
||||
const deleteRequiredFileMutation = useDeleteRequiredFile();
|
||||
|
||||
// Cột bảng
|
||||
const columns: ColumnDef<Version>[] = [
|
||||
{ accessorKey: "version", header: "Phiên bản" },
|
||||
{ accessorKey: "fileName", header: "Tên file" },
|
||||
{
|
||||
accessorKey: "updatedAt",
|
||||
header: () => <div className="whitespace-normal max-w-xs">Thời gian cập nhật</div>,
|
||||
cell: ({ getValue }) =>
|
||||
getValue()
|
||||
? new Date(getValue() as string).toLocaleString("vi-VN")
|
||||
: "N/A",
|
||||
},
|
||||
{
|
||||
accessorKey: "requestUpdateAt",
|
||||
header: () => <div className="whitespace-normal max-w-xs">Thời gian yêu cầu cài đặt/tải xuống</div>,
|
||||
cell: ({ getValue }) =>
|
||||
getValue()
|
||||
? new Date(getValue() as string).toLocaleString("vi-VN")
|
||||
: "N/A",
|
||||
},
|
||||
{
|
||||
id: "required",
|
||||
header: () => <div className="whitespace-normal max-w-xs">Đã thêm vào danh sách</div>,
|
||||
cell: ({ row }) => {
|
||||
const isRequired = row.original.isRequired;
|
||||
return isRequired ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<Check className="h-4 w-4 text-green-600" />
|
||||
<span className="text-sm text-green-600">Có</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
<X className="h-4 w-4 text-gray-400" />
|
||||
<span className="text-sm text-gray-400">Không</span>
|
||||
</div>
|
||||
const columns = useMemo(
|
||||
() => createAppsColumns(installMutation.isPending),
|
||||
[installMutation.isPending]
|
||||
);
|
||||
},
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
{
|
||||
id: "select",
|
||||
header: () => <div className="whitespace-normal max-w-xs">Chọn</div>,
|
||||
cell: ({ row }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={row.getIsSelected?.() ?? false}
|
||||
onChange={row.getToggleSelectedHandler?.()}
|
||||
disabled={installMutation.isPending}
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
];
|
||||
|
||||
// Upload file MSI
|
||||
const handleUpload = async (
|
||||
fd: FormData,
|
||||
|
|
|
|||
92
src/routes/_auth/audits/index.tsx
Normal file
92
src/routes/_auth/audits/index.tsx
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useGetAudits } from "@/hooks/queries";
|
||||
import type { Audits } from "@/types/audit";
|
||||
import { AuditListTemplate } from "@/template/audit-list-template";
|
||||
import { auditColumns } from "@/components/columns/audit-column";
|
||||
|
||||
export const Route = createFileRoute("/_auth/audits/")({
|
||||
head: () => ({ meta: [{ title: "Audit Logs" }] }),
|
||||
loader: async ({ context }) => {
|
||||
context.breadcrumbs = [{ title: "Audit logs", path: "#" }];
|
||||
},
|
||||
component: AuditsPage,
|
||||
});
|
||||
|
||||
function AuditsPage() {
|
||||
const [pageNumber, setPageNumber] = useState(1);
|
||||
const [pageSize] = useState(20);
|
||||
const [username, setUsername] = useState<string | null>(null);
|
||||
const [action, setAction] = useState<string | null>(null);
|
||||
const [from, setFrom] = useState<string | null>(null);
|
||||
const [to, setTo] = useState<string | null>(null);
|
||||
const [selectedAudit, setSelectedAudit] = useState<Audits | null>(null);
|
||||
|
||||
const { data, isLoading, refetch, isFetching } = useGetAudits(
|
||||
{
|
||||
pageNumber,
|
||||
pageSize,
|
||||
username,
|
||||
action,
|
||||
from,
|
||||
to,
|
||||
},
|
||||
true
|
||||
) as any;
|
||||
|
||||
const items: Audits[] = data?.items ?? [];
|
||||
const total: number = data?.totalCount ?? 0;
|
||||
const pageCount = Math.max(1, Math.ceil(total / pageSize));
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [pageNumber, pageSize]);
|
||||
|
||||
const handleSearch = () => {
|
||||
setPageNumber(1);
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setUsername(null);
|
||||
setAction(null);
|
||||
setFrom(null);
|
||||
setTo(null);
|
||||
setPageNumber(1);
|
||||
refetch();
|
||||
};
|
||||
|
||||
return (
|
||||
<AuditListTemplate
|
||||
// data
|
||||
items={items}
|
||||
total={total}
|
||||
columns={auditColumns}
|
||||
isLoading={isLoading}
|
||||
isFetching={isFetching}
|
||||
// pagination
|
||||
pageNumber={pageNumber}
|
||||
pageSize={pageSize}
|
||||
pageCount={pageCount}
|
||||
canPreviousPage={pageNumber > 1}
|
||||
canNextPage={pageNumber < pageCount}
|
||||
onPreviousPage={() => setPageNumber((p) => Math.max(1, p - 1))}
|
||||
onNextPage={() => setPageNumber((p) => Math.min(pageCount, p + 1))}
|
||||
// filter
|
||||
username={username}
|
||||
action={action}
|
||||
from={from}
|
||||
to={to}
|
||||
onUsernameChange={setUsername}
|
||||
onActionChange={setAction}
|
||||
onFromChange={setFrom}
|
||||
onToChange={setTo}
|
||||
onSearch={handleSearch}
|
||||
onReset={handleReset}
|
||||
// detail dialog
|
||||
selectedAudit={selectedAudit}
|
||||
onRowClick={setSelectedAudit}
|
||||
onDialogClose={() => setSelectedAudit(null)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -11,7 +11,6 @@ import {
|
|||
useSendCommand,
|
||||
} from "@/hooks/queries";
|
||||
import { toast } from "sonner";
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
|
||||
import { Check, X, Edit2, Trash2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/_auth/rooms/$roomName/connect/')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/_auth/rooms/$roomName/connect/"!</div>
|
||||
}
|
||||
19
src/services/audit.service.ts
Normal file
19
src/services/audit.service.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import axios from "@/config/axios";
|
||||
import { API_ENDPOINTS } from "@/config/api";
|
||||
import type { PageResult, Audits } from "@/types/audit";
|
||||
|
||||
export async function getAudits(
|
||||
pageNumber = 1,
|
||||
pageSize = 20,
|
||||
username?: string | null,
|
||||
action?: string | null,
|
||||
from?: string | null,
|
||||
to?: string | null
|
||||
): Promise<PageResult<Audits>> {
|
||||
const response = await axios.get<PageResult<Audits>>(API_ENDPOINTS.AUDIT.GET_AUDITS, {
|
||||
params: { pageNumber, pageSize, username, action, from, to },
|
||||
});
|
||||
|
||||
// API trả về camelCase khớp với PageResult<Audits> — dùng trực tiếp, không cần map
|
||||
return response.data;
|
||||
}
|
||||
242
src/template/audit-list-template.tsx
Normal file
242
src/template/audit-list-template.tsx
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
import {
|
||||
type ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableHead,
|
||||
TableCell,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { type Audits } from "@/types/audit";
|
||||
import { AuditFilterBar } from "@/components/filters/audit-filter-bar";
|
||||
import { AuditDetailDialog } from "@/components/dialogs/audit-detail-dialog";
|
||||
|
||||
interface AuditListTemplateProps {
|
||||
// data
|
||||
items: Audits[];
|
||||
total: number;
|
||||
columns: ColumnDef<Audits>[];
|
||||
isLoading: boolean;
|
||||
isFetching: boolean;
|
||||
|
||||
// pagination
|
||||
pageNumber: number;
|
||||
pageSize: number;
|
||||
pageCount: number;
|
||||
onPreviousPage: () => void;
|
||||
onNextPage: () => void;
|
||||
canPreviousPage: boolean;
|
||||
canNextPage: boolean;
|
||||
|
||||
// filter
|
||||
username: string | null;
|
||||
action: string | null;
|
||||
from: string | null;
|
||||
to: string | null;
|
||||
onUsernameChange: (v: string | null) => void;
|
||||
onActionChange: (v: string | null) => void;
|
||||
onFromChange: (v: string | null) => void;
|
||||
onToChange: (v: string | null) => void;
|
||||
onSearch: () => void;
|
||||
onReset: () => void;
|
||||
|
||||
// detail dialog
|
||||
selectedAudit: Audits | null;
|
||||
onRowClick: (audit: Audits) => void;
|
||||
onDialogClose: () => void;
|
||||
}
|
||||
|
||||
export function AuditListTemplate({
|
||||
items,
|
||||
total,
|
||||
columns,
|
||||
isLoading,
|
||||
isFetching,
|
||||
pageNumber,
|
||||
pageSize,
|
||||
pageCount,
|
||||
onPreviousPage,
|
||||
onNextPage,
|
||||
canPreviousPage,
|
||||
canNextPage,
|
||||
username,
|
||||
action,
|
||||
from,
|
||||
to,
|
||||
onUsernameChange,
|
||||
onActionChange,
|
||||
onFromChange,
|
||||
onToChange,
|
||||
onSearch,
|
||||
onReset,
|
||||
selectedAudit,
|
||||
onRowClick,
|
||||
onDialogClose,
|
||||
}: AuditListTemplateProps) {
|
||||
const table = useReactTable({
|
||||
data: items,
|
||||
columns,
|
||||
state: {
|
||||
pagination: { pageIndex: Math.max(0, pageNumber - 1), pageSize },
|
||||
},
|
||||
pageCount,
|
||||
manualPagination: true,
|
||||
onPaginationChange: (updater) => {
|
||||
const next =
|
||||
typeof updater === "function"
|
||||
? updater({ pageIndex: Math.max(0, pageNumber - 1), pageSize })
|
||||
: updater;
|
||||
const newPage = (next.pageIndex ?? 0) + 1;
|
||||
if (newPage > pageNumber) onNextPage();
|
||||
else if (newPage < pageNumber) onPreviousPage();
|
||||
},
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="w-full px-4 md:px-6 space-y-4">
|
||||
<div>
|
||||
<h1 className="text-2xl md:text-3xl font-bold">Nhật ký hoạt động</h1>
|
||||
<p className="text-muted-foreground mt-1 text-sm">
|
||||
Xem nhật ký audit hệ thống
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base">Danh sách audit</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
Lọc theo người dùng, loại, hành động và khoảng thời gian. Nhấn vào
|
||||
dòng để xem chi tiết.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<AuditFilterBar
|
||||
username={username}
|
||||
action={action}
|
||||
from={from}
|
||||
to={to}
|
||||
isLoading={isLoading}
|
||||
isFetching={isFetching}
|
||||
onUsernameChange={onUsernameChange}
|
||||
onActionChange={onActionChange}
|
||||
onFromChange={onFromChange}
|
||||
onToChange={onToChange}
|
||||
onSearch={onSearch}
|
||||
onReset={onReset}
|
||||
/>
|
||||
|
||||
<div className="rounded-md border overflow-x-auto">
|
||||
<Table className="min-w-[640px] w-full">
|
||||
<TableHeader className="sticky top-0 bg-background z-10">
|
||||
{table.getHeaderGroups().map((hg) => (
|
||||
<TableRow key={hg.id}>
|
||||
{hg.headers.map((header) => (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
className="text-xs font-semibold whitespace-nowrap"
|
||||
>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{isLoading || isFetching ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="text-center py-10 text-muted-foreground text-sm"
|
||||
>
|
||||
Đang tải...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : table.getRowModel().rows.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="text-center py-10 text-muted-foreground text-sm"
|
||||
>
|
||||
Không có dữ liệu
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
className="hover:bg-muted/40 cursor-pointer"
|
||||
onClick={() => onRowClick(row.original)}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="py-2.5 align-middle">
|
||||
{cell.column.columnDef.cell
|
||||
? flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)
|
||||
: String(cell.getValue() ?? "")}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mt-4">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Hiển thị {items.length} / {total} mục
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={!canPreviousPage || isFetching}
|
||||
onClick={onPreviousPage}
|
||||
>
|
||||
Trước
|
||||
</Button>
|
||||
<span className="text-sm tabular-nums">
|
||||
{pageNumber} / {pageCount}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={!canNextPage || isFetching}
|
||||
onClick={onNextPage}
|
||||
>
|
||||
Sau
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<AuditDetailDialog
|
||||
audit={selectedAudit}
|
||||
open={!!selectedAudit}
|
||||
onClose={onDialogClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { AppWindow, Building, CircleX, Folder, Home, ShieldCheck, Terminal, UserPlus} from "lucide-react";
|
||||
import { AppWindow, Building, CircleX, ClipboardList, Folder, Home, ShieldCheck, Terminal, UserPlus} from "lucide-react";
|
||||
import { PermissionEnum } from "./permission";
|
||||
|
||||
enum AppSidebarSectionCode {
|
||||
|
|
@ -93,6 +93,17 @@ export const appSidebarSection = {
|
|||
permissions: [PermissionEnum.VIEW_USER],
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Audits",
|
||||
items: [
|
||||
{
|
||||
title: "Lịch sử hoạt động",
|
||||
url: "/audits",
|
||||
icon: ClipboardList,
|
||||
permissions: [PermissionEnum.VIEW_AUDIT_LOGS],
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
};
|
||||
|
|
|
|||
29
src/types/audit.ts
Normal file
29
src/types/audit.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
export interface Audits {
|
||||
id: number;
|
||||
username: string;
|
||||
dateTime: string; // ISO string
|
||||
|
||||
// request identity
|
||||
apiCall?: string; // Controller.ActionName
|
||||
url?: string;
|
||||
requestPayload?: string; // request body (redacted)
|
||||
|
||||
// DB fields — null if request didn't touch DB
|
||||
action?: string;
|
||||
tableName?: string;
|
||||
entityId?: string;
|
||||
oldValues?: string;
|
||||
newValues?: string;
|
||||
|
||||
// result
|
||||
isSuccess?: boolean;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export interface PageResult<T> {
|
||||
items: T[];
|
||||
totalCount: number;
|
||||
pageNumber: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
|
@ -44,6 +44,7 @@ export enum PermissionEnum {
|
|||
EDIT_COMMAND = 53,
|
||||
DEL_COMMAND = 54,
|
||||
SEND_COMMAND = 55,
|
||||
SEND_SENSITIVE_COMMAND = 56,
|
||||
|
||||
//DEVICE_OPERATION
|
||||
DEVICE_OPERATION = 70,
|
||||
|
|
@ -59,10 +60,12 @@ export enum PermissionEnum {
|
|||
VIEW_ACCOUNT_ROOM = 115,
|
||||
EDIT_ACCOUNT_ROOM = 116,
|
||||
|
||||
|
||||
//WARNING_OPERATION
|
||||
WARNING_OPERATION = 140,
|
||||
VIEW_WARNING = 141,
|
||||
|
||||
|
||||
//USER_OPERATION
|
||||
USER_OPERATION = 150,
|
||||
VIEW_USER_ROLE = 151,
|
||||
|
|
@ -80,7 +83,7 @@ export enum PermissionEnum {
|
|||
DEL_ROLE = 164,
|
||||
|
||||
// AGENT
|
||||
APP_OPERATION = 170,
|
||||
AGENT_OPERATION = 170,
|
||||
VIEW_AGENT = 171,
|
||||
UPDATE_AGENT = 173,
|
||||
SEND_UPDATE_COMMAND = 174,
|
||||
|
|
@ -94,9 +97,13 @@ export enum PermissionEnum {
|
|||
ADD_APP_TO_SELECTED = 185,
|
||||
DEL_APP_FROM_SELECTED = 186,
|
||||
|
||||
// AUDIT
|
||||
AUDIT_OPERATION = 190,
|
||||
VIEW_AUDIT_LOGS = 191,
|
||||
|
||||
//Undefined
|
||||
UNDEFINED = 9999,
|
||||
|
||||
//Allow All
|
||||
ALLOW_ALL = 0,
|
||||
ALLOW_ALL = 0
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user