mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-28 22:00:51 +00:00
Filtering working on both access and request
This commit is contained in:
@@ -1916,7 +1916,7 @@
|
||||
"noSessions": "No Sessions",
|
||||
"temporaryRequestToken": "Temporary Request Token",
|
||||
"noMoreAuthMethods": "No Valid Auth",
|
||||
"ip": "IP Address",
|
||||
"ip": "IP",
|
||||
"reason": "Reason",
|
||||
"requestLogs": "Request Logs",
|
||||
"host": "Host",
|
||||
|
||||
@@ -49,7 +49,6 @@ export async function exportAccessAuditLogs(
|
||||
)
|
||||
);
|
||||
}
|
||||
const { timeStart, timeEnd, limit, offset } = parsedQuery.data;
|
||||
|
||||
const parsedParams = queryAccessAuditLogsParams.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
@@ -60,16 +59,17 @@ export async function exportAccessAuditLogs(
|
||||
)
|
||||
);
|
||||
}
|
||||
const { orgId } = parsedParams.data;
|
||||
|
||||
const baseQuery = queryAccess(timeStart, timeEnd, orgId);
|
||||
const data = { ...parsedQuery.data, ...parsedParams.data };
|
||||
|
||||
const log = await baseQuery.limit(limit).offset(offset);
|
||||
const baseQuery = queryAccess(data);
|
||||
|
||||
const log = await baseQuery.limit(data.limit).offset(data.offset);
|
||||
|
||||
const csvData = generateCSV(log);
|
||||
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="access-audit-logs-${orgId}-${Date.now()}.csv"`);
|
||||
res.setHeader('Content-Disposition', `attachment; filename="access-audit-logs-${data.orgId}-${Date.now()}.csv"`);
|
||||
|
||||
return res.send(csvData);
|
||||
} catch (error) {
|
||||
|
||||
@@ -49,7 +49,6 @@ export async function exportActionAuditLogs(
|
||||
)
|
||||
);
|
||||
}
|
||||
const { timeStart, timeEnd, limit, offset } = parsedQuery.data;
|
||||
|
||||
const parsedParams = queryActionAuditLogsParams.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
@@ -60,16 +59,17 @@ export async function exportActionAuditLogs(
|
||||
)
|
||||
);
|
||||
}
|
||||
const { orgId } = parsedParams.data;
|
||||
|
||||
const baseQuery = queryAction(timeStart, timeEnd, orgId);
|
||||
const data = { ...parsedQuery.data, ...parsedParams.data };
|
||||
|
||||
const log = await baseQuery.limit(limit).offset(offset);
|
||||
const baseQuery = queryAction(data);
|
||||
|
||||
const log = await baseQuery.limit(data.limit).offset(data.offset);
|
||||
|
||||
const csvData = generateCSV(log);
|
||||
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="action-audit-logs-${orgId}-${Date.now()}.csv"`);
|
||||
res.setHeader('Content-Disposition', `attachment; filename="action-audit-logs-${data.orgId}-${Date.now()}.csv"`);
|
||||
|
||||
return res.send(csvData);
|
||||
} catch (error) {
|
||||
|
||||
@@ -41,6 +41,20 @@ export const queryAccessAuditLogsQuery = z.object({
|
||||
.transform((val) => Math.floor(new Date(val).getTime() / 1000))
|
||||
.optional()
|
||||
.default(new Date().toISOString()),
|
||||
action: z
|
||||
.union([z.boolean(), z.string()])
|
||||
.transform((val) => (typeof val === "string" ? val === "true" : val))
|
||||
.optional(),
|
||||
actorType: z.string().optional(),
|
||||
actorId: z.string().optional(),
|
||||
resourceId: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().positive())
|
||||
.optional(),
|
||||
actor: z.string().optional(),
|
||||
type: z.string().optional(),
|
||||
limit: z
|
||||
.string()
|
||||
.optional()
|
||||
@@ -59,7 +73,32 @@ export const queryAccessAuditLogsParams = z.object({
|
||||
orgId: z.string()
|
||||
});
|
||||
|
||||
export function queryAccess(timeStart: number, timeEnd: number, orgId: string) {
|
||||
export const queryAccessAuditLogsCombined = queryAccessAuditLogsQuery.merge(
|
||||
queryAccessAuditLogsParams
|
||||
);
|
||||
type Q = z.infer<typeof queryAccessAuditLogsCombined>;
|
||||
|
||||
function getWhere(data: Q) {
|
||||
return and(
|
||||
gt(accessAuditLog.timestamp, data.timeStart),
|
||||
lt(accessAuditLog.timestamp, data.timeEnd),
|
||||
eq(accessAuditLog.orgId, data.orgId),
|
||||
data.resourceId
|
||||
? eq(accessAuditLog.resourceId, data.resourceId)
|
||||
: undefined,
|
||||
data.actor ? eq(accessAuditLog.actor, data.actor) : undefined,
|
||||
data.actorType
|
||||
? eq(accessAuditLog.actorType, data.actorType)
|
||||
: undefined,
|
||||
data.actorId ? eq(accessAuditLog.actorId, data.actorId) : undefined,
|
||||
data.type ? eq(accessAuditLog.type, data.type) : undefined,
|
||||
data.action !== undefined
|
||||
? eq(accessAuditLog.action, data.action)
|
||||
: undefined
|
||||
);
|
||||
}
|
||||
|
||||
export function queryAccess(data: Q) {
|
||||
return db
|
||||
.select({
|
||||
orgId: accessAuditLog.orgId,
|
||||
@@ -78,31 +117,69 @@ export function queryAccess(timeStart: number, timeEnd: number, orgId: string) {
|
||||
actor: accessAuditLog.actor
|
||||
})
|
||||
.from(accessAuditLog)
|
||||
.leftJoin(resources, eq(accessAuditLog.resourceId, resources.resourceId))
|
||||
.where(
|
||||
and(
|
||||
gt(accessAuditLog.timestamp, timeStart),
|
||||
lt(accessAuditLog.timestamp, timeEnd),
|
||||
eq(accessAuditLog.orgId, orgId)
|
||||
)
|
||||
.leftJoin(
|
||||
resources,
|
||||
eq(accessAuditLog.resourceId, resources.resourceId)
|
||||
)
|
||||
.where(getWhere(data))
|
||||
.orderBy(accessAuditLog.timestamp);
|
||||
}
|
||||
|
||||
export function countAccessQuery(timeStart: number, timeEnd: number, orgId: string) {
|
||||
const countQuery = db
|
||||
.select({ count: count() })
|
||||
.from(accessAuditLog)
|
||||
.where(
|
||||
and(
|
||||
gt(accessAuditLog.timestamp, timeStart),
|
||||
lt(accessAuditLog.timestamp, timeEnd),
|
||||
eq(accessAuditLog.orgId, orgId)
|
||||
)
|
||||
);
|
||||
export function countAccessQuery(data: Q) {
|
||||
const countQuery = db
|
||||
.select({ count: count() })
|
||||
.from(accessAuditLog)
|
||||
.where(getWhere(data));
|
||||
return countQuery;
|
||||
}
|
||||
|
||||
async function queryUniqueFilterAttributes(
|
||||
timeStart: number,
|
||||
timeEnd: number,
|
||||
orgId: string
|
||||
) {
|
||||
const baseConditions = and(
|
||||
gt(accessAuditLog.timestamp, timeStart),
|
||||
lt(accessAuditLog.timestamp, timeEnd),
|
||||
eq(accessAuditLog.orgId, orgId)
|
||||
);
|
||||
|
||||
// Get unique actors
|
||||
const uniqueActors = await db
|
||||
.selectDistinct({
|
||||
actor: accessAuditLog.actor
|
||||
})
|
||||
.from(accessAuditLog)
|
||||
.where(baseConditions);
|
||||
|
||||
// Get unique locations
|
||||
const uniqueLocations = await db
|
||||
.selectDistinct({
|
||||
locations: accessAuditLog.location
|
||||
})
|
||||
.from(accessAuditLog)
|
||||
.where(baseConditions);
|
||||
|
||||
// Get unique resources with names
|
||||
const uniqueResources = await db
|
||||
.selectDistinct({
|
||||
id: accessAuditLog.resourceId,
|
||||
name: resources.name
|
||||
})
|
||||
.from(accessAuditLog)
|
||||
.leftJoin(
|
||||
resources,
|
||||
eq(accessAuditLog.resourceId, resources.resourceId)
|
||||
)
|
||||
.where(baseConditions);
|
||||
|
||||
return {
|
||||
actors: uniqueActors.map(row => row.actor).filter((actor): actor is string => actor !== null),
|
||||
resources: uniqueResources.filter((row): row is { id: number; name: string | null } => row.id !== null),
|
||||
locations: uniqueLocations.map(row => row.locations).filter((location): location is string => location !== null)
|
||||
};
|
||||
}
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/logs/access",
|
||||
@@ -130,8 +207,6 @@ export async function queryAccessAuditLogs(
|
||||
)
|
||||
);
|
||||
}
|
||||
const { timeStart, timeEnd, limit, offset } = parsedQuery.data;
|
||||
|
||||
const parsedParams = queryAccessAuditLogsParams.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
@@ -141,23 +216,31 @@ export async function queryAccessAuditLogs(
|
||||
)
|
||||
);
|
||||
}
|
||||
const { orgId } = parsedParams.data;
|
||||
|
||||
const baseQuery = queryAccess(timeStart, timeEnd, orgId);
|
||||
const data = { ...parsedQuery.data, ...parsedParams.data };
|
||||
|
||||
const log = await baseQuery.limit(limit).offset(offset);
|
||||
const baseQuery = queryAccess(data);
|
||||
|
||||
const totalCountResult = await countAccessQuery(timeStart, timeEnd, orgId);
|
||||
const log = await baseQuery.limit(data.limit).offset(data.offset);
|
||||
|
||||
const totalCountResult = await countAccessQuery(data);
|
||||
const totalCount = totalCountResult[0].count;
|
||||
|
||||
const filterAttributes = await queryUniqueFilterAttributes(
|
||||
data.timeStart,
|
||||
data.timeEnd,
|
||||
data.orgId
|
||||
);
|
||||
|
||||
return response<QueryAccessAuditLogResponse>(res, {
|
||||
data: {
|
||||
log: log,
|
||||
pagination: {
|
||||
total: totalCount,
|
||||
limit,
|
||||
offset
|
||||
}
|
||||
limit: data.limit,
|
||||
offset: data.offset
|
||||
},
|
||||
filterAttributes
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
|
||||
@@ -24,7 +24,6 @@ import { fromError } from "zod-validation-error";
|
||||
import { QueryActionAuditLogResponse } from "@server/routers/auditLogs/types";
|
||||
import response from "@server/lib/response";
|
||||
import logger from "@server/logger";
|
||||
import { metadata } from "@app/app/[orgId]/settings/layout";
|
||||
|
||||
export const queryActionAuditLogsQuery = z.object({
|
||||
// iso string just validate its a parseable date
|
||||
@@ -42,6 +41,10 @@ export const queryActionAuditLogsQuery = z.object({
|
||||
.transform((val) => Math.floor(new Date(val).getTime() / 1000))
|
||||
.optional()
|
||||
.default(new Date().toISOString()),
|
||||
action: z.string().optional(),
|
||||
actorType: z.string().optional(),
|
||||
actorId: z.string().optional(),
|
||||
actor: z.string().optional(),
|
||||
limit: z
|
||||
.string()
|
||||
.optional()
|
||||
@@ -60,7 +63,23 @@ export const queryActionAuditLogsParams = z.object({
|
||||
orgId: z.string()
|
||||
});
|
||||
|
||||
export function queryAction(timeStart: number, timeEnd: number, orgId: string) {
|
||||
export const queryActionAuditLogsCombined =
|
||||
queryActionAuditLogsQuery.merge(queryActionAuditLogsParams);
|
||||
type Q = z.infer<typeof queryActionAuditLogsCombined>;
|
||||
|
||||
function getWhere(data: Q) {
|
||||
return and(
|
||||
gt(actionAuditLog.timestamp, data.timeStart),
|
||||
lt(actionAuditLog.timestamp, data.timeEnd),
|
||||
eq(actionAuditLog.orgId, data.orgId),
|
||||
data.actor ? eq(actionAuditLog.actor, data.actor) : undefined,
|
||||
data.actorType ? eq(actionAuditLog.actorType, data.actorType) : undefined,
|
||||
data.actorId ? eq(actionAuditLog.actorId, data.actorId) : undefined,
|
||||
data.action ? eq(actionAuditLog.action, data.action) : undefined
|
||||
);
|
||||
}
|
||||
|
||||
export function queryAction(data: Q) {
|
||||
return db
|
||||
.select({
|
||||
orgId: actionAuditLog.orgId,
|
||||
@@ -72,27 +91,15 @@ export function queryAction(timeStart: number, timeEnd: number, orgId: string) {
|
||||
actor: actionAuditLog.actor
|
||||
})
|
||||
.from(actionAuditLog)
|
||||
.where(
|
||||
and(
|
||||
gt(actionAuditLog.timestamp, timeStart),
|
||||
lt(actionAuditLog.timestamp, timeEnd),
|
||||
eq(actionAuditLog.orgId, orgId)
|
||||
)
|
||||
)
|
||||
.where(getWhere(data))
|
||||
.orderBy(actionAuditLog.timestamp);
|
||||
}
|
||||
|
||||
export function countActionQuery(timeStart: number, timeEnd: number, orgId: string) {
|
||||
const countQuery = db
|
||||
.select({ count: count() })
|
||||
.from(actionAuditLog)
|
||||
.where(
|
||||
and(
|
||||
gt(actionAuditLog.timestamp, timeStart),
|
||||
lt(actionAuditLog.timestamp, timeEnd),
|
||||
eq(actionAuditLog.orgId, orgId)
|
||||
)
|
||||
);
|
||||
export function countActionQuery(data: Q) {
|
||||
const countQuery = db
|
||||
.select({ count: count() })
|
||||
.from(actionAuditLog)
|
||||
.where(getWhere(data));
|
||||
return countQuery;
|
||||
}
|
||||
|
||||
@@ -123,8 +130,6 @@ export async function queryActionAuditLogs(
|
||||
)
|
||||
);
|
||||
}
|
||||
const { timeStart, timeEnd, limit, offset } = parsedQuery.data;
|
||||
|
||||
const parsedParams = queryActionAuditLogsParams.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
@@ -134,13 +139,14 @@ export async function queryActionAuditLogs(
|
||||
)
|
||||
);
|
||||
}
|
||||
const { orgId } = parsedParams.data;
|
||||
|
||||
const baseQuery = queryAction(timeStart, timeEnd, orgId);
|
||||
const data = { ...parsedQuery.data, ...parsedParams.data };
|
||||
|
||||
const log = await baseQuery.limit(limit).offset(offset);
|
||||
const baseQuery = queryAction(data);
|
||||
|
||||
const totalCountResult = await countActionQuery(timeStart, timeEnd, orgId);
|
||||
const log = await baseQuery.limit(data.limit).offset(data.offset);
|
||||
|
||||
const totalCountResult = await countActionQuery(data);
|
||||
const totalCount = totalCountResult[0].count;
|
||||
|
||||
return response<QueryActionAuditLogResponse>(res, {
|
||||
@@ -148,8 +154,8 @@ export async function queryActionAuditLogs(
|
||||
log: log,
|
||||
pagination: {
|
||||
total: totalCount,
|
||||
limit,
|
||||
offset
|
||||
limit: data.limit,
|
||||
offset: data.offset
|
||||
}
|
||||
},
|
||||
success: true,
|
||||
|
||||
@@ -28,11 +28,26 @@ export const queryAccessAuditLogsQuery = z.object({
|
||||
.transform((val) => Math.floor(new Date(val).getTime() / 1000))
|
||||
.optional()
|
||||
.default(new Date().toISOString()),
|
||||
action: z.boolean().optional(),
|
||||
action: z
|
||||
.union([z.boolean(), z.string()])
|
||||
.transform((val) => (typeof val === "string" ? val === "true" : val))
|
||||
.optional(),
|
||||
method: z.enum(["GET", "POST", "PUT", "DELETE", "PATCH"]).optional(),
|
||||
reason: z.number().optional(),
|
||||
resourceId: z.number().optional(),
|
||||
reason: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().positive())
|
||||
.optional(),
|
||||
resourceId: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().positive())
|
||||
.optional(),
|
||||
actor: z.string().optional(),
|
||||
host: z.string().optional(),
|
||||
path: z.string().optional(),
|
||||
limit: z
|
||||
.string()
|
||||
.optional()
|
||||
@@ -65,7 +80,12 @@ function getWhere(data: Q) {
|
||||
: undefined,
|
||||
data.actor ? eq(requestAuditLog.actor, data.actor) : undefined,
|
||||
data.method ? eq(requestAuditLog.method, data.method) : undefined,
|
||||
data.reason ? eq(requestAuditLog.reason, data.reason) : undefined
|
||||
data.reason ? eq(requestAuditLog.reason, data.reason) : undefined,
|
||||
data.host ? eq(requestAuditLog.host, data.host) : undefined,
|
||||
data.path ? eq(requestAuditLog.path, data.path) : undefined,
|
||||
data.action !== undefined
|
||||
? eq(requestAuditLog.action, data.action)
|
||||
: undefined
|
||||
);
|
||||
}
|
||||
|
||||
@@ -124,6 +144,71 @@ registry.registerPath({
|
||||
responses: {}
|
||||
});
|
||||
|
||||
async function queryUniqueFilterAttributes(
|
||||
timeStart: number,
|
||||
timeEnd: number,
|
||||
orgId: string
|
||||
) {
|
||||
const baseConditions = and(
|
||||
gt(requestAuditLog.timestamp, timeStart),
|
||||
lt(requestAuditLog.timestamp, timeEnd),
|
||||
eq(requestAuditLog.orgId, orgId)
|
||||
);
|
||||
|
||||
// Get unique actors
|
||||
const uniqueActors = await db
|
||||
.selectDistinct({
|
||||
actor: requestAuditLog.actor
|
||||
})
|
||||
.from(requestAuditLog)
|
||||
.where(baseConditions);
|
||||
|
||||
// Get unique locations
|
||||
const uniqueLocations = await db
|
||||
.selectDistinct({
|
||||
locations: requestAuditLog.location
|
||||
})
|
||||
.from(requestAuditLog)
|
||||
.where(baseConditions);
|
||||
|
||||
// Get unique actors
|
||||
const uniqueHosts = await db
|
||||
.selectDistinct({
|
||||
hosts: requestAuditLog.host
|
||||
})
|
||||
.from(requestAuditLog)
|
||||
.where(baseConditions);
|
||||
|
||||
// Get unique actors
|
||||
const uniquePaths = await db
|
||||
.selectDistinct({
|
||||
paths: requestAuditLog.path
|
||||
})
|
||||
.from(requestAuditLog)
|
||||
.where(baseConditions);
|
||||
|
||||
// Get unique resources with names
|
||||
const uniqueResources = await db
|
||||
.selectDistinct({
|
||||
id: requestAuditLog.resourceId,
|
||||
name: resources.name
|
||||
})
|
||||
.from(requestAuditLog)
|
||||
.leftJoin(
|
||||
resources,
|
||||
eq(requestAuditLog.resourceId, resources.resourceId)
|
||||
)
|
||||
.where(baseConditions);
|
||||
|
||||
return {
|
||||
actors: uniqueActors.map(row => row.actor).filter((actor): actor is string => actor !== null),
|
||||
resources: uniqueResources.filter((row): row is { id: number; name: string | null } => row.id !== null),
|
||||
locations: uniqueLocations.map(row => row.locations).filter((location): location is string => location !== null),
|
||||
hosts: uniqueHosts.map(row => row.hosts).filter((host): host is string => host !== null),
|
||||
paths: uniquePaths.map(row => row.paths).filter((path): path is string => path !== null)
|
||||
};
|
||||
}
|
||||
|
||||
export async function queryRequestAuditLogs(
|
||||
req: Request,
|
||||
res: Response,
|
||||
@@ -159,6 +244,12 @@ export async function queryRequestAuditLogs(
|
||||
const totalCountResult = await countRequestQuery(data);
|
||||
const totalCount = totalCountResult[0].count;
|
||||
|
||||
const filterAttributes = await queryUniqueFilterAttributes(
|
||||
data.timeStart,
|
||||
data.timeEnd,
|
||||
data.orgId
|
||||
);
|
||||
|
||||
return response<QueryRequestAuditLogResponse>(res, {
|
||||
data: {
|
||||
log: log,
|
||||
@@ -166,7 +257,8 @@ export async function queryRequestAuditLogs(
|
||||
total: totalCount,
|
||||
limit: data.limit,
|
||||
offset: data.offset
|
||||
}
|
||||
},
|
||||
filterAttributes
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
|
||||
@@ -45,6 +45,16 @@ export type QueryRequestAuditLogResponse = {
|
||||
limit: number;
|
||||
offset: number;
|
||||
};
|
||||
filterAttributes: {
|
||||
actors: string[];
|
||||
resources: {
|
||||
id: number;
|
||||
name: string | null;
|
||||
}[];
|
||||
locations: string[];
|
||||
hosts: string[];
|
||||
paths: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export type QueryAccessAuditLogResponse = {
|
||||
@@ -69,4 +79,12 @@ export type QueryAccessAuditLogResponse = {
|
||||
limit: number;
|
||||
offset: number;
|
||||
};
|
||||
};
|
||||
filterAttributes: {
|
||||
actors: string[];
|
||||
resources: {
|
||||
id: number;
|
||||
name: string | null;
|
||||
}[];
|
||||
locations: string[];
|
||||
};
|
||||
};
|
||||
@@ -4,17 +4,19 @@ import { toast } from "@app/hooks/useToast";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { LogDataTable } from "@app/components/LogDataTable";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { DateTimeValue } from "@app/components/DateTimePicker";
|
||||
import { ArrowUpRight, Key, User } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { ColumnFilter } from "@app/components/ColumnFilter";
|
||||
|
||||
export default function GeneralPage() {
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const api = createApiClient(useEnvContext());
|
||||
const t = useTranslations();
|
||||
const { env } = useEnvContext();
|
||||
@@ -23,6 +25,33 @@ export default function GeneralPage() {
|
||||
const [rows, setRows] = useState<any[]>([]);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
const [filterAttributes, setFilterAttributes] = useState<{
|
||||
actors: string[];
|
||||
resources: {
|
||||
id: number;
|
||||
name: string | null;
|
||||
}[];
|
||||
locations: string[];
|
||||
}>({
|
||||
actors: [],
|
||||
resources: [],
|
||||
locations: []
|
||||
});
|
||||
|
||||
// Filter states - unified object for all filters
|
||||
const [filters, setFilters] = useState<{
|
||||
action?: string;
|
||||
type?: string;
|
||||
resourceId?: string;
|
||||
location?: string;
|
||||
actor?: string;
|
||||
}>({
|
||||
action: searchParams.get("action") || undefined,
|
||||
type: searchParams.get("type") || undefined,
|
||||
resourceId: searchParams.get("resourceId") || undefined,
|
||||
location: searchParams.get("location") || undefined,
|
||||
actor: searchParams.get("actor") || undefined
|
||||
});
|
||||
|
||||
// Pagination state
|
||||
const [totalCount, setTotalCount] = useState<number>(0);
|
||||
@@ -32,6 +61,20 @@ export default function GeneralPage() {
|
||||
|
||||
// Set default date range to last 24 hours
|
||||
const getDefaultDateRange = () => {
|
||||
// if the time is in the url params, use that instead
|
||||
const startParam = searchParams.get("start");
|
||||
const endParam = searchParams.get("end");
|
||||
if (startParam && endParam) {
|
||||
return {
|
||||
startDate: {
|
||||
date: new Date(startParam)
|
||||
},
|
||||
endDate: {
|
||||
date: new Date(endParam)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
|
||||
@@ -53,7 +96,12 @@ export default function GeneralPage() {
|
||||
// Trigger search with default values on component mount
|
||||
useEffect(() => {
|
||||
const defaultRange = getDefaultDateRange();
|
||||
queryDateTime(defaultRange.startDate, defaultRange.endDate);
|
||||
queryDateTime(
|
||||
defaultRange.startDate,
|
||||
defaultRange.endDate,
|
||||
0,
|
||||
pageSize
|
||||
);
|
||||
}, [orgId]); // Re-run if orgId changes
|
||||
|
||||
const handleDateRangeChange = (
|
||||
@@ -62,6 +110,12 @@ export default function GeneralPage() {
|
||||
) => {
|
||||
setDateRange({ startDate, endDate });
|
||||
setCurrentPage(0); // Reset to first page when filtering
|
||||
// put the search params in the url for the time
|
||||
updateUrlParamsForAllFilters({
|
||||
start: startDate.date?.toISOString() || "",
|
||||
end: endDate.date?.toISOString() || ""
|
||||
});
|
||||
|
||||
queryDateTime(startDate, endDate, 0, pageSize);
|
||||
};
|
||||
|
||||
@@ -83,20 +137,76 @@ export default function GeneralPage() {
|
||||
queryDateTime(dateRange.startDate, dateRange.endDate, 0, newPageSize);
|
||||
};
|
||||
|
||||
// Handle filter changes generically
|
||||
const handleFilterChange = (
|
||||
filterType: keyof typeof filters,
|
||||
value: string | undefined
|
||||
) => {
|
||||
console.log(`${filterType} filter changed:`, value);
|
||||
|
||||
// Create new filters object with updated value
|
||||
const newFilters = {
|
||||
...filters,
|
||||
[filterType]: value
|
||||
};
|
||||
|
||||
setFilters(newFilters);
|
||||
setCurrentPage(0); // Reset to first page when filtering
|
||||
|
||||
// Update URL params
|
||||
updateUrlParamsForAllFilters(newFilters);
|
||||
|
||||
// Trigger new query with updated filters (pass directly to avoid async state issues)
|
||||
queryDateTime(
|
||||
dateRange.startDate,
|
||||
dateRange.endDate,
|
||||
0,
|
||||
pageSize,
|
||||
newFilters
|
||||
);
|
||||
};
|
||||
|
||||
const updateUrlParamsForAllFilters = (
|
||||
newFilters:
|
||||
| typeof filters
|
||||
| {
|
||||
start: string;
|
||||
end: string;
|
||||
}
|
||||
) => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
Object.entries(newFilters).forEach(([key, value]) => {
|
||||
if (value) {
|
||||
params.set(key, value);
|
||||
} else {
|
||||
params.delete(key);
|
||||
}
|
||||
});
|
||||
router.replace(`?${params.toString()}`, { scroll: false });
|
||||
};
|
||||
|
||||
const queryDateTime = async (
|
||||
startDate: DateTimeValue,
|
||||
endDate: DateTimeValue,
|
||||
page: number = currentPage,
|
||||
size: number = pageSize
|
||||
size: number = pageSize,
|
||||
filtersParam?: {
|
||||
action?: string;
|
||||
type?: string;
|
||||
}
|
||||
) => {
|
||||
console.log("Date range changed:", { startDate, endDate, page, size });
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
// Use the provided filters or fall back to current state
|
||||
const activeFilters = filtersParam || filters;
|
||||
|
||||
// Convert the date/time values to API parameters
|
||||
let params: any = {
|
||||
limit: size,
|
||||
offset: page * size
|
||||
offset: page * size,
|
||||
...activeFilters
|
||||
};
|
||||
|
||||
if (startDate?.date) {
|
||||
@@ -134,6 +244,7 @@ export default function GeneralPage() {
|
||||
if (res.status === 200) {
|
||||
setRows(res.data.data.log || []);
|
||||
setTotalCount(res.data.data.pagination?.total || 0);
|
||||
setFilterAttributes(res.data.data.filterAttributes);
|
||||
console.log("Fetched logs:", res.data);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -172,16 +283,21 @@ export default function GeneralPage() {
|
||||
const exportData = async () => {
|
||||
try {
|
||||
setIsExporting(true);
|
||||
|
||||
// Prepare query params for export
|
||||
let params: any = {
|
||||
timeStart: dateRange.startDate?.date
|
||||
? new Date(dateRange.startDate.date).toISOString()
|
||||
: undefined,
|
||||
timeEnd: dateRange.endDate?.date
|
||||
? new Date(dateRange.endDate.date).toISOString()
|
||||
: undefined,
|
||||
...filters
|
||||
};
|
||||
|
||||
const response = await api.get(`/org/${orgId}/logs/access/export`, {
|
||||
responseType: "blob",
|
||||
params: {
|
||||
timeStart: dateRange.startDate?.date
|
||||
? new Date(dateRange.startDate.date).toISOString()
|
||||
: undefined,
|
||||
timeEnd: dateRange.endDate?.date
|
||||
? new Date(dateRange.endDate.date).toISOString()
|
||||
: undefined
|
||||
}
|
||||
params
|
||||
});
|
||||
|
||||
// Create a URL for the blob and trigger a download
|
||||
@@ -225,7 +341,24 @@ export default function GeneralPage() {
|
||||
{
|
||||
accessorKey: "action",
|
||||
header: ({ column }) => {
|
||||
return t("action");
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{t("action")}</span>
|
||||
<ColumnFilter
|
||||
options={[
|
||||
{ value: "true", label: "Allowed" },
|
||||
{ value: "false", label: "Denied" }
|
||||
]}
|
||||
selectedValue={filters.action}
|
||||
onValueChange={(value) =>
|
||||
handleFilterChange("action", value)
|
||||
}
|
||||
// placeholder=""
|
||||
searchPlaceholder="Search..."
|
||||
emptyMessage="None found"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
@@ -244,7 +377,26 @@ export default function GeneralPage() {
|
||||
{
|
||||
accessorKey: "location",
|
||||
header: ({ column }) => {
|
||||
return t("location");
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{t("location")}</span>
|
||||
<ColumnFilter
|
||||
options={filterAttributes.locations.map(
|
||||
(location) => ({
|
||||
value: location,
|
||||
label: location
|
||||
})
|
||||
)}
|
||||
selectedValue={filters.location}
|
||||
onValueChange={(value) =>
|
||||
handleFilterChange("location", value)
|
||||
}
|
||||
// placeholder=""
|
||||
searchPlaceholder="Search..."
|
||||
emptyMessage="None found"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
@@ -264,7 +416,26 @@ export default function GeneralPage() {
|
||||
},
|
||||
{
|
||||
accessorKey: "resourceName",
|
||||
header: t("resource"),
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{t("resource")}</span>
|
||||
<ColumnFilter
|
||||
options={filterAttributes.resources.map((res) => ({
|
||||
value: res.id.toString(),
|
||||
label: res.name || "Unnamed Resource"
|
||||
}))}
|
||||
selectedValue={filters.resourceId}
|
||||
onValueChange={(value) =>
|
||||
handleFilterChange("resourceId", value)
|
||||
}
|
||||
// placeholder=""
|
||||
searchPlaceholder="Search..."
|
||||
emptyMessage="None found"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Link
|
||||
@@ -285,26 +456,56 @@ export default function GeneralPage() {
|
||||
{
|
||||
accessorKey: "type",
|
||||
header: ({ column }) => {
|
||||
return t("type");
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{t("type")}</span>
|
||||
<ColumnFilter
|
||||
options={[
|
||||
{ value: "password", label: "Password" },
|
||||
{ value: "pincode", label: "Pincode" },
|
||||
{ value: "login", label: "Login" },
|
||||
{
|
||||
value: "whitelistedEmail",
|
||||
label: "Whitelisted Email"
|
||||
}
|
||||
]}
|
||||
selectedValue={filters.type}
|
||||
onValueChange={(value) =>
|
||||
handleFilterChange("type", value)
|
||||
}
|
||||
// placeholder=""
|
||||
searchPlaceholder="Search..."
|
||||
emptyMessage="None found"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<span className="flex items-center gap-1">
|
||||
{/* {row.original.type == "pincode" ? (
|
||||
<User className="h-4 w-4" />
|
||||
) : (
|
||||
<Key className="h-4 w-4" />
|
||||
)} */}
|
||||
{row.original.type.charAt(0).toUpperCase() +
|
||||
row.original.type.slice(1)}
|
||||
</span>
|
||||
);
|
||||
// should be capitalized first letter
|
||||
return <span>{row.original.type.charAt(0).toUpperCase() + row.original.type.slice(1) || "-"}</span>;
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "actor",
|
||||
header: ({ column }) => {
|
||||
return t("actor");
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{t("actor")}</span>
|
||||
<ColumnFilter
|
||||
options={filterAttributes.actors.map((actor) => ({
|
||||
value: actor,
|
||||
label: actor
|
||||
}))}
|
||||
selectedValue={filters.actor}
|
||||
onValueChange={(value) =>
|
||||
handleFilterChange("actor", value)
|
||||
}
|
||||
// placeholder=""
|
||||
searchPlaceholder="Search..."
|
||||
emptyMessage="None found"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
@@ -344,10 +545,24 @@ export default function GeneralPage() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-xs">
|
||||
{row.userAgent != "node" && (
|
||||
<div>
|
||||
<strong>User Agent:</strong>
|
||||
<p className="text-muted-foreground mt-1 break-all">
|
||||
{row.userAgent || "N/A"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<strong>Metadata:</strong>
|
||||
<pre className="text-muted-foreground mt-1 text-xs bg-background p-2 rounded border overflow-auto">
|
||||
{row.metadata ? JSON.stringify(JSON.parse(row.metadata), null, 2) : "N/A"}
|
||||
{row.metadata
|
||||
? JSON.stringify(
|
||||
JSON.parse(row.metadata),
|
||||
null,
|
||||
2
|
||||
)
|
||||
: "N/A"}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
@@ -362,8 +577,6 @@ export default function GeneralPage() {
|
||||
data={rows}
|
||||
persistPageSize="access-logs-table"
|
||||
title={t("accessLogs")}
|
||||
searchPlaceholder={t("searchLogs")}
|
||||
searchColumn="type"
|
||||
onRefresh={refreshData}
|
||||
isRefreshing={isRefreshing}
|
||||
onExport={exportData}
|
||||
|
||||
@@ -4,21 +4,22 @@ import { toast } from "@app/hooks/useToast";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { LogDataTable } from "@app/components/LogDataTable";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { DateTimeValue } from "@app/components/DateTimePicker";
|
||||
import { Key, RouteOff, User, Lock, Unlock, ArrowUpRight } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { ColumnFilter } from "@app/components/ColumnFilter";
|
||||
|
||||
export default function GeneralPage() {
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
const api = createApiClient(useEnvContext());
|
||||
const t = useTranslations();
|
||||
const { env } = useEnvContext();
|
||||
const { orgId } = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const [rows, setRows] = useState<any[]>([]);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
@@ -30,8 +31,60 @@ export default function GeneralPage() {
|
||||
const [pageSize, setPageSize] = useState<number>(20);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const [filterAttributes, setFilterAttributes] = useState<{
|
||||
actors: string[];
|
||||
resources: {
|
||||
id: number;
|
||||
name: string | null;
|
||||
}[];
|
||||
locations: string[];
|
||||
hosts: string[];
|
||||
paths: string[];
|
||||
}>({
|
||||
actors: [],
|
||||
resources: [],
|
||||
locations: [],
|
||||
hosts: [],
|
||||
paths: []
|
||||
});
|
||||
|
||||
// Filter states - unified object for all filters
|
||||
const [filters, setFilters] = useState<{
|
||||
action?: string;
|
||||
resourceId?: string;
|
||||
host?: string;
|
||||
location?: string;
|
||||
actor?: string;
|
||||
method?: string;
|
||||
reason?: string;
|
||||
path?: string;
|
||||
}>({
|
||||
action: searchParams.get("action") || undefined,
|
||||
host: searchParams.get("host") || undefined,
|
||||
resourceId: searchParams.get("resourceId") || undefined,
|
||||
location: searchParams.get("location") || undefined,
|
||||
actor: searchParams.get("actor") || undefined,
|
||||
method: searchParams.get("method") || undefined,
|
||||
reason: searchParams.get("reason") || undefined,
|
||||
path: searchParams.get("path") || undefined
|
||||
});
|
||||
|
||||
// Set default date range to last 24 hours
|
||||
const getDefaultDateRange = () => {
|
||||
// if the time is in the url params, use that instead
|
||||
const startParam = searchParams.get("start");
|
||||
const endParam = searchParams.get("end");
|
||||
if (startParam && endParam) {
|
||||
return {
|
||||
startDate: {
|
||||
date: new Date(startParam)
|
||||
},
|
||||
endDate: {
|
||||
date: new Date(endParam)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
|
||||
@@ -53,7 +106,12 @@ export default function GeneralPage() {
|
||||
// Trigger search with default values on component mount
|
||||
useEffect(() => {
|
||||
const defaultRange = getDefaultDateRange();
|
||||
queryDateTime(defaultRange.startDate, defaultRange.endDate);
|
||||
queryDateTime(
|
||||
defaultRange.startDate,
|
||||
defaultRange.endDate,
|
||||
0,
|
||||
pageSize
|
||||
);
|
||||
}, [orgId]); // Re-run if orgId changes
|
||||
|
||||
const handleDateRangeChange = (
|
||||
@@ -62,6 +120,12 @@ export default function GeneralPage() {
|
||||
) => {
|
||||
setDateRange({ startDate, endDate });
|
||||
setCurrentPage(0); // Reset to first page when filtering
|
||||
// put the search params in the url for the time
|
||||
updateUrlParamsForAllFilters({
|
||||
start: startDate.date?.toISOString() || "",
|
||||
end: endDate.date?.toISOString() || ""
|
||||
});
|
||||
|
||||
queryDateTime(startDate, endDate, 0, pageSize);
|
||||
};
|
||||
|
||||
@@ -83,20 +147,76 @@ export default function GeneralPage() {
|
||||
queryDateTime(dateRange.startDate, dateRange.endDate, 0, newPageSize);
|
||||
};
|
||||
|
||||
// Handle filter changes generically
|
||||
const handleFilterChange = (
|
||||
filterType: keyof typeof filters,
|
||||
value: string | undefined
|
||||
) => {
|
||||
console.log(`${filterType} filter changed:`, value);
|
||||
|
||||
// Create new filters object with updated value
|
||||
const newFilters = {
|
||||
...filters,
|
||||
[filterType]: value
|
||||
};
|
||||
|
||||
setFilters(newFilters);
|
||||
setCurrentPage(0); // Reset to first page when filtering
|
||||
|
||||
// Update URL params
|
||||
updateUrlParamsForAllFilters(newFilters);
|
||||
|
||||
// Trigger new query with updated filters (pass directly to avoid async state issues)
|
||||
queryDateTime(
|
||||
dateRange.startDate,
|
||||
dateRange.endDate,
|
||||
0,
|
||||
pageSize,
|
||||
newFilters
|
||||
);
|
||||
};
|
||||
|
||||
const updateUrlParamsForAllFilters = (
|
||||
newFilters:
|
||||
| typeof filters
|
||||
| {
|
||||
start: string;
|
||||
end: string;
|
||||
}
|
||||
) => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
Object.entries(newFilters).forEach(([key, value]) => {
|
||||
if (value) {
|
||||
params.set(key, value);
|
||||
} else {
|
||||
params.delete(key);
|
||||
}
|
||||
});
|
||||
router.replace(`?${params.toString()}`, { scroll: false });
|
||||
};
|
||||
|
||||
const queryDateTime = async (
|
||||
startDate: DateTimeValue,
|
||||
endDate: DateTimeValue,
|
||||
page: number = currentPage,
|
||||
size: number = pageSize
|
||||
size: number = pageSize,
|
||||
filtersParam?: {
|
||||
action?: string;
|
||||
type?: string;
|
||||
}
|
||||
) => {
|
||||
console.log("Date range changed:", { startDate, endDate, page, size });
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
// Use the provided filters or fall back to current state
|
||||
const activeFilters = filtersParam || filters;
|
||||
|
||||
// Convert the date/time values to API parameters
|
||||
let params: any = {
|
||||
limit: size,
|
||||
offset: page * size
|
||||
offset: page * size,
|
||||
...activeFilters
|
||||
};
|
||||
|
||||
if (startDate?.date) {
|
||||
@@ -134,6 +254,7 @@ export default function GeneralPage() {
|
||||
if (res.status === 200) {
|
||||
setRows(res.data.data.log || []);
|
||||
setTotalCount(res.data.data.pagination?.total || 0);
|
||||
setFilterAttributes(res.data.data.filterAttributes);
|
||||
console.log("Fetched logs:", res.data);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -172,18 +293,23 @@ export default function GeneralPage() {
|
||||
const exportData = async () => {
|
||||
try {
|
||||
setIsExporting(true);
|
||||
|
||||
// Prepare query params for export
|
||||
let params: any = {
|
||||
timeStart: dateRange.startDate?.date
|
||||
? new Date(dateRange.startDate.date).toISOString()
|
||||
: undefined,
|
||||
timeEnd: dateRange.endDate?.date
|
||||
? new Date(dateRange.endDate.date).toISOString()
|
||||
: undefined,
|
||||
...filters
|
||||
};
|
||||
|
||||
const response = await api.get(
|
||||
`/org/${orgId}/logs/request/export`,
|
||||
{
|
||||
responseType: "blob",
|
||||
params: {
|
||||
timeStart: dateRange.startDate?.date
|
||||
? new Date(dateRange.startDate.date).toISOString()
|
||||
: undefined,
|
||||
timeEnd: dateRange.endDate?.date
|
||||
? new Date(dateRange.endDate.date).toISOString()
|
||||
: undefined
|
||||
}
|
||||
params
|
||||
}
|
||||
);
|
||||
|
||||
@@ -269,7 +395,24 @@ export default function GeneralPage() {
|
||||
{
|
||||
accessorKey: "action",
|
||||
header: ({ column }) => {
|
||||
return t("action");
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{t("action")}</span>
|
||||
<ColumnFilter
|
||||
options={[
|
||||
{ value: "true", label: "Allowed" },
|
||||
{ value: "false", label: "Denied" }
|
||||
]}
|
||||
selectedValue={filters.action}
|
||||
onValueChange={(value) =>
|
||||
handleFilterChange("action", value)
|
||||
}
|
||||
// placeholder=""
|
||||
searchPlaceholder="Search..."
|
||||
emptyMessage="None found"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
@@ -288,7 +431,26 @@ export default function GeneralPage() {
|
||||
{
|
||||
accessorKey: "location",
|
||||
header: ({ column }) => {
|
||||
return t("location");
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{t("location")}</span>
|
||||
<ColumnFilter
|
||||
options={filterAttributes.locations.map(
|
||||
(location) => ({
|
||||
value: location,
|
||||
label: location
|
||||
})
|
||||
)}
|
||||
selectedValue={filters.location}
|
||||
onValueChange={(value) =>
|
||||
handleFilterChange("location", value)
|
||||
}
|
||||
// placeholder=""
|
||||
searchPlaceholder="Search..."
|
||||
emptyMessage="None found"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
@@ -308,7 +470,26 @@ export default function GeneralPage() {
|
||||
},
|
||||
{
|
||||
accessorKey: "resourceName",
|
||||
header: t("resource"),
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{t("resource")}</span>
|
||||
<ColumnFilter
|
||||
options={filterAttributes.resources.map((res) => ({
|
||||
value: res.id.toString(),
|
||||
label: res.name || "Unnamed Resource"
|
||||
}))}
|
||||
selectedValue={filters.resourceId}
|
||||
onValueChange={(value) =>
|
||||
handleFilterChange("resourceId", value)
|
||||
}
|
||||
// placeholder=""
|
||||
searchPlaceholder="Search..."
|
||||
emptyMessage="None found"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Link
|
||||
@@ -330,7 +511,24 @@ export default function GeneralPage() {
|
||||
{
|
||||
accessorKey: "host",
|
||||
header: ({ column }) => {
|
||||
return t("host");
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{t("host")}</span>
|
||||
<ColumnFilter
|
||||
options={filterAttributes.hosts.map((host) => ({
|
||||
value: host,
|
||||
label: host
|
||||
}))}
|
||||
selectedValue={filters.host}
|
||||
onValueChange={(value) =>
|
||||
handleFilterChange("host", value)
|
||||
}
|
||||
// placeholder=""
|
||||
searchPlaceholder="Search..."
|
||||
emptyMessage="None found"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
@@ -348,8 +546,25 @@ export default function GeneralPage() {
|
||||
{
|
||||
accessorKey: "path",
|
||||
header: ({ column }) => {
|
||||
return t("path");
|
||||
}
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{t("path")}</span>
|
||||
<ColumnFilter
|
||||
options={filterAttributes.paths.map((path) => ({
|
||||
value: path,
|
||||
label: path
|
||||
}))}
|
||||
selectedValue={filters.path}
|
||||
onValueChange={(value) =>
|
||||
handleFilterChange("path", value)
|
||||
}
|
||||
// placeholder=""
|
||||
searchPlaceholder="Search..."
|
||||
emptyMessage="None found"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
// {
|
||||
@@ -361,13 +576,65 @@ export default function GeneralPage() {
|
||||
{
|
||||
accessorKey: "method",
|
||||
header: ({ column }) => {
|
||||
return t("method");
|
||||
}
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{t("method")}</span>
|
||||
<ColumnFilter
|
||||
options={[
|
||||
{ value: "GET", label: "GET" },
|
||||
{ value: "POST", label: "POST" },
|
||||
{ value: "PUT", label: "PUT" },
|
||||
{ value: "DELETE", label: "DELETE" },
|
||||
{ value: "PATCH", label: "PATCH" },
|
||||
{ value: "HEAD", label: "HEAD" },
|
||||
{ value: "OPTIONS", label: "OPTIONS" }
|
||||
]
|
||||
}
|
||||
selectedValue={filters.method}
|
||||
onValueChange={(value) =>
|
||||
handleFilterChange("method", value)
|
||||
}
|
||||
// placeholder=""
|
||||
searchPlaceholder="Search..."
|
||||
emptyMessage="None found"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "reason",
|
||||
header: ({ column }) => {
|
||||
return t("reason");
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{t("reason")}</span>
|
||||
<ColumnFilter
|
||||
options={[
|
||||
{ value: "100", label: t("allowedByRule") },
|
||||
{ value: "101", label: t("allowedNoAuth") },
|
||||
{ value: "102", label: t("validAccessToken") },
|
||||
{ value: "103", label: t("validHeaderAuth") },
|
||||
{ value: "104", label: t("validPincode") },
|
||||
{ value: "105", label: t("validPassword") },
|
||||
{ value: "106", label: t("validEmail") },
|
||||
{ value: "107", label: t("validSSO") },
|
||||
{ value: "201", label: t("resourceNotFound") },
|
||||
{ value: "202", label: t("resourceBlocked") },
|
||||
{ value: "203", label: t("droppedByRule") },
|
||||
{ value: "204", label: t("noSessions") },
|
||||
{ value: "205", label: t("temporaryRequestToken") },
|
||||
{ value: "299", label: t("noMoreAuthMethods") }
|
||||
]}
|
||||
selectedValue={filters.reason}
|
||||
onValueChange={(value) =>
|
||||
handleFilterChange("reason", value)
|
||||
}
|
||||
// placeholder=""
|
||||
searchPlaceholder="Search..."
|
||||
emptyMessage="None found"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
@@ -380,7 +647,24 @@ export default function GeneralPage() {
|
||||
{
|
||||
accessorKey: "actor",
|
||||
header: ({ column }) => {
|
||||
return t("actor");
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{t("actor")}</span>
|
||||
<ColumnFilter
|
||||
options={filterAttributes.actors.map((actor) => ({
|
||||
value: actor,
|
||||
label: actor
|
||||
}))}
|
||||
selectedValue={filters.actor}
|
||||
onValueChange={(value) =>
|
||||
handleFilterChange("actor", value)
|
||||
}
|
||||
// placeholder=""
|
||||
searchPlaceholder="Search..."
|
||||
emptyMessage="None found"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
|
||||
104
src/components/ColumnFilter.tsx
Normal file
104
src/components/ColumnFilter.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@app/components/ui/popover";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@app/components/ui/command";
|
||||
import { CheckIcon, ChevronDownIcon, Filter } from "lucide-react";
|
||||
import { cn } from "@app/lib/cn";
|
||||
|
||||
interface FilterOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface ColumnFilterProps {
|
||||
options: FilterOption[];
|
||||
selectedValue?: string;
|
||||
onValueChange: (value: string | undefined) => void;
|
||||
placeholder?: string;
|
||||
searchPlaceholder?: string;
|
||||
emptyMessage?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ColumnFilter({
|
||||
options,
|
||||
selectedValue,
|
||||
onValueChange,
|
||||
placeholder,
|
||||
searchPlaceholder = "Search...",
|
||||
emptyMessage = "No options found",
|
||||
className
|
||||
}: ColumnFilterProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const selectedOption = options.find(option => option.value === selectedValue);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className={cn(
|
||||
"justify-between text-sm h-8 px-2",
|
||||
!selectedValue && "text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Filter className="h-4 w-4" />
|
||||
<span className="truncate">
|
||||
{selectedOption ? selectedOption.label : placeholder}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronDownIcon className="h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0 w-[200px]" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder={searchPlaceholder} />
|
||||
<CommandList>
|
||||
<CommandEmpty>{emptyMessage}</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{/* Clear filter option */}
|
||||
{selectedValue && (
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
onValueChange(undefined);
|
||||
setOpen(false);
|
||||
}}
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
Clear filter
|
||||
</CommandItem>
|
||||
)}
|
||||
{options.map((option) => (
|
||||
<CommandItem
|
||||
key={option.value}
|
||||
value={option.label}
|
||||
onSelect={() => {
|
||||
onValueChange(
|
||||
selectedValue === option.value ? undefined : option.value
|
||||
);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
selectedValue === option.value
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{option.label}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -80,9 +80,9 @@ const getDisplayText = () => {
|
||||
|
||||
return (
|
||||
<div className={cn("flex gap-4", className)}>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-2">
|
||||
{label && (
|
||||
<Label htmlFor="date-picker" className="px-1">
|
||||
<Label htmlFor="date-picker">
|
||||
{label}
|
||||
</Label>
|
||||
)}
|
||||
@@ -193,9 +193,9 @@ export function DateRangePicker({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("flex gap-4", className)}>
|
||||
<div className={cn("flex gap-4 items-center", className)}>
|
||||
<DateTimePicker
|
||||
// label={startLabel}
|
||||
label="Start"
|
||||
value={startValue}
|
||||
onChange={handleStartChange}
|
||||
placeholder="Start date & time"
|
||||
@@ -203,7 +203,7 @@ export function DateRangePicker({
|
||||
showTime={showTime}
|
||||
/>
|
||||
<DateTimePicker
|
||||
// label={endLabel}
|
||||
label="End"
|
||||
value={endValue}
|
||||
onChange={handleEndChange}
|
||||
placeholder="End date & time"
|
||||
|
||||
@@ -131,8 +131,8 @@ export function LogDataTable<TData, TValue>({
|
||||
isRefreshing,
|
||||
onExport,
|
||||
isExporting,
|
||||
searchPlaceholder = "Search...",
|
||||
searchColumn = "name",
|
||||
// searchPlaceholder = "Search...",
|
||||
// searchColumn = "name",
|
||||
defaultSort,
|
||||
tabs,
|
||||
defaultTab,
|
||||
@@ -354,7 +354,7 @@ export function LogDataTable<TData, TValue>({
|
||||
<Card>
|
||||
<CardHeader className="flex flex-col space-y-4 sm:flex-row sm:items-center sm:justify-between sm:space-y-0 pb-4">
|
||||
<div className="flex flex-row items-start w-full sm:mr-2 gap-2">
|
||||
<div className="relative w-full sm:max-w-sm">
|
||||
{/* <div className="relative w-full sm:max-w-sm">
|
||||
<Input
|
||||
placeholder={searchPlaceholder}
|
||||
value={globalFilter ?? ""}
|
||||
@@ -366,7 +366,7 @@ export function LogDataTable<TData, TValue>({
|
||||
className="w-full pl-8 m-0"
|
||||
/>
|
||||
<Search className="h-4 w-4 absolute left-2 top-1/2 transform -translate-y-1/2 text-muted-foreground" />
|
||||
</div>
|
||||
</div> */}
|
||||
<DateRangePicker
|
||||
startValue={startDate}
|
||||
endValue={endDate}
|
||||
|
||||
Reference in New Issue
Block a user