mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #3906 from Infisical/feat/audit-log-fix
feat: audit log improvement
This commit is contained in:
@@ -110,7 +110,8 @@ export const initAuditLogDbConnection = ({
|
|||||||
},
|
},
|
||||||
migrations: {
|
migrations: {
|
||||||
tableName: "infisical_migrations"
|
tableName: "infisical_migrations"
|
||||||
}
|
},
|
||||||
|
pool: { min: 0, max: 10 }
|
||||||
});
|
});
|
||||||
|
|
||||||
// we add these overrides so that auditLogDb and the primary DB are interchangeable
|
// we add these overrides so that auditLogDb and the primary DB are interchangeable
|
||||||
|
|||||||
@@ -111,15 +111,38 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim().describe(AUDIT_LOGS.EXPORT.projectId)
|
workspaceId: z.string().trim().describe(AUDIT_LOGS.EXPORT.projectId)
|
||||||
}),
|
}),
|
||||||
querystring: z.object({
|
querystring: z
|
||||||
eventType: z.nativeEnum(EventType).optional().describe(AUDIT_LOGS.EXPORT.eventType),
|
.object({
|
||||||
userAgentType: z.nativeEnum(UserAgentType).optional().describe(AUDIT_LOGS.EXPORT.userAgentType),
|
eventType: z.nativeEnum(EventType).optional().describe(AUDIT_LOGS.EXPORT.eventType),
|
||||||
startDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.startDate),
|
userAgentType: z.nativeEnum(UserAgentType).optional().describe(AUDIT_LOGS.EXPORT.userAgentType),
|
||||||
endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate),
|
startDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.startDate),
|
||||||
offset: z.coerce.number().default(0).describe(AUDIT_LOGS.EXPORT.offset),
|
endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate),
|
||||||
limit: z.coerce.number().default(20).describe(AUDIT_LOGS.EXPORT.limit),
|
offset: z.coerce.number().default(0).describe(AUDIT_LOGS.EXPORT.offset),
|
||||||
actor: z.string().optional().describe(AUDIT_LOGS.EXPORT.actor)
|
limit: z.coerce.number().max(1000).default(20).describe(AUDIT_LOGS.EXPORT.limit),
|
||||||
}),
|
actor: z.string().optional().describe(AUDIT_LOGS.EXPORT.actor)
|
||||||
|
})
|
||||||
|
.superRefine((el, ctx) => {
|
||||||
|
if (el.endDate && el.startDate) {
|
||||||
|
const startDate = new Date(el.startDate);
|
||||||
|
const endDate = new Date(el.endDate);
|
||||||
|
const maxAllowedDate = new Date(startDate);
|
||||||
|
maxAllowedDate.setMonth(maxAllowedDate.getMonth() + 3);
|
||||||
|
if (endDate < startDate) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["endDate"],
|
||||||
|
message: "End date cannot be before start date"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (endDate > maxAllowedDate) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["endDate"],
|
||||||
|
message: "Dates must be within 3 months"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
auditLogs: AuditLogsSchema.omit({
|
auditLogs: AuditLogsSchema.omit({
|
||||||
@@ -161,7 +184,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
filter: {
|
filter: {
|
||||||
...req.query,
|
...req.query,
|
||||||
projectId: req.params.workspaceId,
|
projectId: req.params.workspaceId,
|
||||||
endDate: req.query.endDate,
|
endDate: req.query.endDate || new Date().toISOString(),
|
||||||
startDate: req.query.startDate || getLastMidnightDateISO(),
|
startDate: req.query.startDate || getLastMidnightDateISO(),
|
||||||
auditLogActorId: req.query.actor,
|
auditLogActorId: req.query.actor,
|
||||||
eventType: req.query.eventType ? [req.query.eventType] : undefined
|
eventType: req.query.eventType ? [req.query.eventType] : undefined
|
||||||
|
|||||||
@@ -30,10 +30,10 @@ type TFindQuery = {
|
|||||||
actor?: string;
|
actor?: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
environment?: string;
|
environment?: string;
|
||||||
orgId?: string;
|
orgId: string;
|
||||||
eventType?: string;
|
eventType?: string;
|
||||||
startDate?: string;
|
startDate: string;
|
||||||
endDate?: string;
|
endDate: string;
|
||||||
userAgentType?: string;
|
userAgentType?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
@@ -61,18 +61,15 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
|||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
) => {
|
) => {
|
||||||
if (!orgId && !projectId) {
|
|
||||||
throw new Error("Either orgId or projectId must be provided");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Find statements
|
// Find statements
|
||||||
const sqlQuery = (tx || db.replicaNode())(TableName.AuditLog)
|
const sqlQuery = (tx || db.replicaNode())(TableName.AuditLog)
|
||||||
|
.where(`${TableName.AuditLog}.orgId`, orgId)
|
||||||
|
.whereRaw(`"${TableName.AuditLog}"."createdAt" >= ?::timestamptz`, [startDate])
|
||||||
|
.andWhereRaw(`"${TableName.AuditLog}"."createdAt" < ?::timestamptz`, [endDate])
|
||||||
// eslint-disable-next-line func-names
|
// eslint-disable-next-line func-names
|
||||||
.where(function () {
|
.where(function () {
|
||||||
if (orgId) {
|
if (projectId) {
|
||||||
void this.where(`${TableName.AuditLog}.orgId`, orgId);
|
|
||||||
} else if (projectId) {
|
|
||||||
void this.where(`${TableName.AuditLog}.projectId`, projectId);
|
void this.where(`${TableName.AuditLog}.projectId`, projectId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -135,14 +132,6 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
|||||||
void sqlQuery.whereIn("eventType", eventType);
|
void sqlQuery.whereIn("eventType", eventType);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter by date range
|
|
||||||
if (startDate) {
|
|
||||||
void sqlQuery.whereRaw(`"${TableName.AuditLog}"."createdAt" >= ?::timestamptz`, [startDate]);
|
|
||||||
}
|
|
||||||
if (endDate) {
|
|
||||||
void sqlQuery.whereRaw(`"${TableName.AuditLog}"."createdAt" <= ?::timestamptz`, [endDate]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// we timeout long running queries to prevent DB resource issues (2 minutes)
|
// we timeout long running queries to prevent DB resource issues (2 minutes)
|
||||||
const docs = await sqlQuery.timeout(1000 * 120);
|
const docs = await sqlQuery.timeout(1000 * 120);
|
||||||
|
|
||||||
@@ -174,6 +163,8 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
|||||||
try {
|
try {
|
||||||
const findExpiredLogSubQuery = (tx || db)(TableName.AuditLog)
|
const findExpiredLogSubQuery = (tx || db)(TableName.AuditLog)
|
||||||
.where("expiresAt", "<", today)
|
.where("expiresAt", "<", today)
|
||||||
|
.where("createdAt", "<", today) // to use audit log partition
|
||||||
|
.orderBy(`${TableName.AuditLog}.createdAt`, "desc")
|
||||||
.select("id")
|
.select("id")
|
||||||
.limit(AUDIT_LOG_PRUNE_BATCH_SIZE);
|
.limit(AUDIT_LOG_PRUNE_BATCH_SIZE);
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,8 @@ export const auditLogServiceFactory = ({
|
|||||||
secretPath: filter.secretPath,
|
secretPath: filter.secretPath,
|
||||||
secretKey: filter.secretKey,
|
secretKey: filter.secretKey,
|
||||||
environment: filter.environment,
|
environment: filter.environment,
|
||||||
...(filter.projectId ? { projectId: filter.projectId } : { orgId: actorOrgId })
|
orgId: actorOrgId,
|
||||||
|
...(filter.projectId ? { projectId: filter.projectId } : {})
|
||||||
});
|
});
|
||||||
|
|
||||||
return auditLogs.map(({ eventType: logEventType, actor: eActor, actorMetadata, eventMetadata, ...el }) => ({
|
return auditLogs.map(({ eventType: logEventType, actor: eActor, actorMetadata, eventMetadata, ...el }) => ({
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ export type TListProjectAuditLogDTO = {
|
|||||||
eventType?: EventType[];
|
eventType?: EventType[];
|
||||||
offset?: number;
|
offset?: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
endDate?: string;
|
endDate: string;
|
||||||
startDate?: string;
|
startDate: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
environment?: string;
|
environment?: string;
|
||||||
auditLogActorId?: string;
|
auditLogActorId?: string;
|
||||||
|
|||||||
@@ -113,52 +113,73 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.AuditLogs],
|
tags: [ApiDocsTags.AuditLogs],
|
||||||
description: "Get all audit logs for an organization",
|
description: "Get all audit logs for an organization",
|
||||||
querystring: z.object({
|
querystring: z
|
||||||
projectId: z.string().optional().describe(AUDIT_LOGS.EXPORT.projectId),
|
.object({
|
||||||
environment: z.string().optional().describe(AUDIT_LOGS.EXPORT.environment),
|
projectId: z.string().optional().describe(AUDIT_LOGS.EXPORT.projectId),
|
||||||
actorType: z.nativeEnum(ActorType).optional(),
|
environment: z.string().optional().describe(AUDIT_LOGS.EXPORT.environment),
|
||||||
secretPath: z
|
actorType: z.nativeEnum(ActorType).optional(),
|
||||||
.string()
|
secretPath: z
|
||||||
.optional()
|
.string()
|
||||||
.transform((val) => (!val ? val : removeTrailingSlash(val)))
|
.optional()
|
||||||
.describe(AUDIT_LOGS.EXPORT.secretPath),
|
.transform((val) => (!val ? val : removeTrailingSlash(val)))
|
||||||
secretKey: z.string().optional().describe(AUDIT_LOGS.EXPORT.secretKey),
|
.describe(AUDIT_LOGS.EXPORT.secretPath),
|
||||||
|
secretKey: z.string().optional().describe(AUDIT_LOGS.EXPORT.secretKey),
|
||||||
|
// eventType is split with , for multiple values, we need to transform it to array
|
||||||
|
eventType: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((val) => (val ? val.split(",") : undefined)),
|
||||||
|
userAgentType: z.nativeEnum(UserAgentType).optional().describe(AUDIT_LOGS.EXPORT.userAgentType),
|
||||||
|
eventMetadata: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((val) => {
|
||||||
|
if (!val) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
// eventType is split with , for multiple values, we need to transform it to array
|
const pairs = val.split(",");
|
||||||
eventType: z
|
|
||||||
.string()
|
return pairs.reduce(
|
||||||
.optional()
|
(acc, pair) => {
|
||||||
.transform((val) => (val ? val.split(",") : undefined)),
|
const [key, value] = pair.split("=");
|
||||||
userAgentType: z.nativeEnum(UserAgentType).optional().describe(AUDIT_LOGS.EXPORT.userAgentType),
|
if (key && value) {
|
||||||
eventMetadata: z
|
acc[key] = value;
|
||||||
.string()
|
}
|
||||||
.optional()
|
return acc;
|
||||||
.transform((val) => {
|
},
|
||||||
if (!val) {
|
{} as Record<string, string>
|
||||||
return undefined;
|
);
|
||||||
|
})
|
||||||
|
.describe(AUDIT_LOGS.EXPORT.eventMetadata),
|
||||||
|
startDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.startDate),
|
||||||
|
endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate),
|
||||||
|
offset: z.coerce.number().default(0).describe(AUDIT_LOGS.EXPORT.offset),
|
||||||
|
limit: z.coerce.number().max(1000).default(20).describe(AUDIT_LOGS.EXPORT.limit),
|
||||||
|
actor: z.string().optional().describe(AUDIT_LOGS.EXPORT.actor)
|
||||||
|
})
|
||||||
|
.superRefine((el, ctx) => {
|
||||||
|
if (el.endDate && el.startDate) {
|
||||||
|
const startDate = new Date(el.startDate);
|
||||||
|
const endDate = new Date(el.endDate);
|
||||||
|
const maxAllowedDate = new Date(startDate);
|
||||||
|
maxAllowedDate.setMonth(maxAllowedDate.getMonth() + 3);
|
||||||
|
if (endDate < startDate) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["endDate"],
|
||||||
|
message: "End date cannot be before start date"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
if (endDate > maxAllowedDate) {
|
||||||
const pairs = val.split(",");
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
return pairs.reduce(
|
path: ["endDate"],
|
||||||
(acc, pair) => {
|
message: "Dates must be within 3 months"
|
||||||
const [key, value] = pair.split("=");
|
});
|
||||||
if (key && value) {
|
}
|
||||||
acc[key] = value;
|
}
|
||||||
}
|
}),
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as Record<string, string>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.describe(AUDIT_LOGS.EXPORT.eventMetadata),
|
|
||||||
startDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.startDate),
|
|
||||||
endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate),
|
|
||||||
offset: z.coerce.number().default(0).describe(AUDIT_LOGS.EXPORT.offset),
|
|
||||||
limit: z.coerce.number().default(20).describe(AUDIT_LOGS.EXPORT.limit),
|
|
||||||
actor: z.string().optional().describe(AUDIT_LOGS.EXPORT.actor)
|
|
||||||
}),
|
|
||||||
|
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
auditLogs: AuditLogsSchema.omit({
|
auditLogs: AuditLogsSchema.omit({
|
||||||
@@ -188,14 +209,13 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
const auditLogs = await server.services.auditLog.listAuditLogs({
|
const auditLogs = await server.services.auditLog.listAuditLogs({
|
||||||
filter: {
|
filter: {
|
||||||
...req.query,
|
...req.query,
|
||||||
endDate: req.query.endDate,
|
endDate: req.query.endDate || new Date().toISOString(),
|
||||||
projectId: req.query.projectId,
|
projectId: req.query.projectId,
|
||||||
startDate: req.query.startDate || getLastMidnightDateISO(),
|
startDate: req.query.startDate || getLastMidnightDateISO(),
|
||||||
auditLogActorId: req.query.actor,
|
auditLogActorId: req.query.actor,
|
||||||
actorType: req.query.actorType,
|
actorType: req.query.actorType,
|
||||||
eventType: req.query.eventType as EventType[] | undefined
|
eventType: req.query.eventType as EventType[] | undefined
|
||||||
},
|
},
|
||||||
|
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
|
|||||||
queueService.start(QueueName.DailyResourceCleanUp, async () => {
|
queueService.start(QueueName.DailyResourceCleanUp, async () => {
|
||||||
logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`);
|
logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`);
|
||||||
await secretDAL.pruneSecretReminders(queueService);
|
await secretDAL.pruneSecretReminders(queueService);
|
||||||
await auditLogDAL.pruneAuditLog();
|
|
||||||
await identityAccessTokenDAL.removeExpiredTokens();
|
await identityAccessTokenDAL.removeExpiredTokens();
|
||||||
await identityUniversalAuthClientSecretDAL.removeExpiredClientSecrets();
|
await identityUniversalAuthClientSecretDAL.removeExpiredClientSecrets();
|
||||||
await secretSharingDAL.pruneExpiredSharedSecrets();
|
await secretSharingDAL.pruneExpiredSharedSecrets();
|
||||||
@@ -58,6 +57,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
|
|||||||
await secretFolderVersionDAL.pruneExcessVersions();
|
await secretFolderVersionDAL.pruneExcessVersions();
|
||||||
await serviceTokenService.notifyExpiringTokens();
|
await serviceTokenService.notifyExpiringTokens();
|
||||||
await orgService.notifyInvitedUsers();
|
await orgService.notifyInvitedUsers();
|
||||||
|
await auditLogDAL.pruneAuditLog();
|
||||||
logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`);
|
logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ export const useGetAuditLogs = (
|
|||||||
params: {
|
params: {
|
||||||
...filters,
|
...filters,
|
||||||
offset: pageParam,
|
offset: pageParam,
|
||||||
startDate: filters?.startDate?.toISOString(),
|
startDate: filters.startDate.toISOString(),
|
||||||
endDate: filters?.endDate?.toISOString(),
|
endDate: filters.endDate.toISOString(),
|
||||||
...(filters.eventMetadata && Object.keys(filters.eventMetadata).length
|
...(filters.eventMetadata && Object.keys(filters.eventMetadata).length
|
||||||
? {
|
? {
|
||||||
eventMetadata: Object.entries(filters.eventMetadata)
|
eventMetadata: Object.entries(filters.eventMetadata)
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ export type TGetAuditLogsFilter = {
|
|||||||
actor?: string; // user ID format
|
actor?: string; // user ID format
|
||||||
secretPath?: string;
|
secretPath?: string;
|
||||||
secretKey?: string;
|
secretKey?: string;
|
||||||
startDate?: Date;
|
startDate: Date;
|
||||||
endDate?: Date;
|
endDate: Date;
|
||||||
limit: number;
|
limit: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faArrowRight, faCalendar, faChevronRight } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import ms from "ms";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DatePicker,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
SelectItem
|
||||||
|
} from "@app/components/v2";
|
||||||
|
|
||||||
|
import {
|
||||||
|
auditLogDateFilterFormSchema,
|
||||||
|
AuditLogDateFilterType,
|
||||||
|
TAuditLogDateFilterFormData
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
setFilter: (data: TAuditLogDateFilterFormData) => void;
|
||||||
|
filter: TAuditLogDateFilterFormData;
|
||||||
|
};
|
||||||
|
const RELATIVE_VALUES = ["5m", "30m", "1h", "3h", "12h"];
|
||||||
|
export const LogsDateFilter = ({ setFilter, filter }: Props) => {
|
||||||
|
const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
|
||||||
|
const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false);
|
||||||
|
const [isPopupOpen, setIsPopOpen] = useState(false);
|
||||||
|
|
||||||
|
const { control, watch, handleSubmit, formState } = useForm<TAuditLogDateFilterFormData>({
|
||||||
|
resolver: zodResolver(auditLogDateFilterFormSchema),
|
||||||
|
values: filter
|
||||||
|
});
|
||||||
|
const selectType = watch("type");
|
||||||
|
const isCustomRelative =
|
||||||
|
filter.type === AuditLogDateFilterType.Relative &&
|
||||||
|
!RELATIVE_VALUES.includes(filter.relativeModeValue || "");
|
||||||
|
|
||||||
|
const onSubmit = (data: TAuditLogDateFilterFormData) => {
|
||||||
|
const endDate = data.type === AuditLogDateFilterType.Relative ? new Date() : data.endDate;
|
||||||
|
const startDate =
|
||||||
|
data.type === AuditLogDateFilterType.Relative && data.relativeModeValue
|
||||||
|
? new Date(Number(new Date()) - ms(data.relativeModeValue))
|
||||||
|
: data.startDate;
|
||||||
|
setFilter({
|
||||||
|
...data,
|
||||||
|
startDate,
|
||||||
|
endDate
|
||||||
|
});
|
||||||
|
setIsPopOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu open={isPopupOpen} onOpenChange={(el) => setIsPopOpen(el)}>
|
||||||
|
<div className="mr-2 flex items-center">
|
||||||
|
{filter.type === AuditLogDateFilterType.Relative ? (
|
||||||
|
<>
|
||||||
|
{RELATIVE_VALUES.map((el) => (
|
||||||
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
className={twMerge(
|
||||||
|
"rounded-none px-3 py-2 first:rounded-l-md",
|
||||||
|
filter.type === AuditLogDateFilterType.Relative &&
|
||||||
|
filter.relativeModeValue === el &&
|
||||||
|
"border-primary/40 bg-primary/[0.1]"
|
||||||
|
)}
|
||||||
|
key={`${el}-relative`}
|
||||||
|
onClick={() =>
|
||||||
|
setFilter({
|
||||||
|
relativeModeValue: el,
|
||||||
|
type: AuditLogDateFilterType.Relative,
|
||||||
|
endDate: new Date(),
|
||||||
|
startDate: new Date(Number(new Date()) - ms(el))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{el}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="rounded-l-md border border-transparent bg-mineshaft-600 px-3 py-2 text-sm text-bunker-200">
|
||||||
|
{format(filter.startDate, "yyyy-MM-dd HH:mm")}
|
||||||
|
</div>
|
||||||
|
<div className="border border-transparent bg-mineshaft-600 px-3 py-2 text-sm text-bunker-200">
|
||||||
|
<FontAwesomeIcon icon={faChevronRight} />
|
||||||
|
</div>
|
||||||
|
<div className="border border-transparent bg-mineshaft-600 px-3 py-2 text-sm text-bunker-200">
|
||||||
|
{format(filter.endDate, "yyyy-MM-dd HH:mm")}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
className={twMerge(
|
||||||
|
"rounded-none rounded-r-md px-3 py-2",
|
||||||
|
(filter.type === AuditLogDateFilterType.Absolute || isCustomRelative) &&
|
||||||
|
"border-primary/40 bg-primary/[0.1]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faCalendar} />
|
||||||
|
{filter.type === AuditLogDateFilterType.Relative && isCustomRelative && (
|
||||||
|
<span className="ml-1">({filter.relativeModeValue})</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
</div>
|
||||||
|
<DropdownMenuContent className="min-w-80 p-4" align="center" alignOffset={16}>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="type"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormControl className="mb-2 w-full">
|
||||||
|
<Select
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={(el) => field.onChange(el)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<SelectItem value={AuditLogDateFilterType.Relative}>Relative</SelectItem>
|
||||||
|
<SelectItem value={AuditLogDateFilterType.Absolute}>Absolute</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{selectType === AuditLogDateFilterType.Relative && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="relativeModeValue"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormControl className="mb-0 w-full" helperText="Example: 1h, 1d, 2d">
|
||||||
|
<Input {...field} value={field.value || ""} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{selectType === AuditLogDateFilterType.Absolute && (
|
||||||
|
<div className="mb-2 flex h-10 w-full items-center justify-between gap-2">
|
||||||
|
<Controller
|
||||||
|
name="startDate"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
className="relative top-2"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<DatePicker
|
||||||
|
value={field.value || undefined}
|
||||||
|
onChange={onChange}
|
||||||
|
dateFormat="P"
|
||||||
|
popUpProps={{
|
||||||
|
open: isStartDatePickerOpen,
|
||||||
|
onOpenChange: setIsStartDatePickerOpen
|
||||||
|
}}
|
||||||
|
popUpContentProps={{}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center -space-x-3">
|
||||||
|
<div className="h-[2px] w-[20px] rounded-full bg-mineshaft-500" />
|
||||||
|
<FontAwesomeIcon icon={faArrowRight} className="text-mineshaft-500" />
|
||||||
|
</div>
|
||||||
|
<Controller
|
||||||
|
name="endDate"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
className="relative top-2"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<DatePicker
|
||||||
|
value={field.value || undefined}
|
||||||
|
onChange={onChange}
|
||||||
|
dateFormat="P"
|
||||||
|
popUpProps={{
|
||||||
|
open: isEndDatePickerOpen,
|
||||||
|
onOpenChange: setIsEndDatePickerOpen
|
||||||
|
}}
|
||||||
|
popUpContentProps={{}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="mt-4">
|
||||||
|
<Button size="xs" type="submit" isDisabled={!formState.isDirty}>
|
||||||
|
Apply
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,28 +1,15 @@
|
|||||||
/* eslint-disable no-nested-ternary */
|
/* eslint-disable no-nested-ternary */
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo } from "react";
|
||||||
import {
|
import { Controller, useForm } from "react-hook-form";
|
||||||
Control,
|
import { faCaretDown, faCheckCircle, faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
Controller,
|
|
||||||
UseFormGetFieldState,
|
|
||||||
UseFormReset,
|
|
||||||
UseFormResetField,
|
|
||||||
UseFormSetValue,
|
|
||||||
UseFormWatch
|
|
||||||
} from "react-hook-form";
|
|
||||||
import {
|
|
||||||
faArrowRight,
|
|
||||||
faCaretDown,
|
|
||||||
faCheckCircle,
|
|
||||||
faFilterCircleXmark
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
DatePicker,
|
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
@@ -44,7 +31,7 @@ import { EventType } from "@app/hooks/api/auditLogs/enums";
|
|||||||
import { UserAgentType } from "@app/hooks/api/auth/types";
|
import { UserAgentType } from "@app/hooks/api/auth/types";
|
||||||
|
|
||||||
import { LogFilterItem } from "./LogFilterItem";
|
import { LogFilterItem } from "./LogFilterItem";
|
||||||
import { AuditLogFilterFormData, Presets } from "./types";
|
import { auditLogFilterFormSchema, Presets, TAuditLogFilterFormData } from "./types";
|
||||||
|
|
||||||
const eventTypes = Object.entries(eventToNameMap).map(([value, label]) => ({ label, value }));
|
const eventTypes = Object.entries(eventToNameMap).map(([value, label]) => ({ label, value }));
|
||||||
const userAgentTypes = Object.entries(userAgentTypeToNameMap).map(([value, label]) => ({
|
const userAgentTypes = Object.entries(userAgentTypeToNameMap).map(([value, label]) => ({
|
||||||
@@ -54,78 +41,54 @@ const userAgentTypes = Object.entries(userAgentTypeToNameMap).map(([value, label
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
presets?: Presets;
|
presets?: Presets;
|
||||||
control: Control<AuditLogFilterFormData>;
|
setFilter: (data: TAuditLogFilterFormData) => void;
|
||||||
reset: UseFormReset<AuditLogFilterFormData>;
|
filter: TAuditLogFilterFormData;
|
||||||
resetField: UseFormResetField<AuditLogFilterFormData>;
|
|
||||||
watch: UseFormWatch<AuditLogFilterFormData>;
|
|
||||||
getFieldState: UseFormGetFieldState<AuditLogFilterFormData>;
|
|
||||||
setValue: UseFormSetValue<AuditLogFilterFormData>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getActiveFilterCount = (
|
const getActiveFilterCount = (filter: TAuditLogFilterFormData) => {
|
||||||
getFieldState: UseFormGetFieldState<AuditLogFilterFormData>,
|
|
||||||
watch: UseFormWatch<AuditLogFilterFormData>
|
|
||||||
) => {
|
|
||||||
const fields = [
|
const fields = [
|
||||||
"actor",
|
"actor",
|
||||||
"project",
|
"project",
|
||||||
"eventType",
|
"eventType",
|
||||||
"startDate",
|
|
||||||
"endDate",
|
|
||||||
"environment",
|
"environment",
|
||||||
"secretPath",
|
"secretPath",
|
||||||
"userAgentType",
|
"userAgentType",
|
||||||
"secretKey"
|
"secretKey"
|
||||||
] as Partial<keyof AuditLogFilterFormData>[];
|
] as Partial<keyof TAuditLogFilterFormData>[];
|
||||||
|
|
||||||
let filterCount = 0;
|
let filterCount = 0;
|
||||||
|
|
||||||
// either start or end date should only be counted as one filter
|
// either start or end date should only be counted as one filter
|
||||||
let dateProcessed = false;
|
|
||||||
|
|
||||||
fields.forEach((field) => {
|
fields.forEach((field) => {
|
||||||
const fieldState = getFieldState(field);
|
const value = filter?.[field];
|
||||||
|
if (Array.isArray(value) ? value.length : value) {
|
||||||
if (
|
|
||||||
field === "userAgentType" ||
|
|
||||||
field === "environment" ||
|
|
||||||
field === "secretKey" ||
|
|
||||||
field === "secretPath"
|
|
||||||
) {
|
|
||||||
const value = watch(field);
|
|
||||||
|
|
||||||
if (value !== undefined && value !== "") {
|
|
||||||
filterCount += 1;
|
|
||||||
}
|
|
||||||
} else if (fieldState.isDirty && !dateProcessed) {
|
|
||||||
filterCount += 1;
|
filterCount += 1;
|
||||||
|
|
||||||
if (field === "startDate" || field === "endDate") {
|
|
||||||
dateProcessed = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return filterCount;
|
return filterCount;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LogsFilter = ({
|
export const LogsFilter = ({ presets, setFilter, filter }: Props) => {
|
||||||
presets,
|
|
||||||
control,
|
|
||||||
reset,
|
|
||||||
resetField,
|
|
||||||
watch,
|
|
||||||
getFieldState,
|
|
||||||
setValue
|
|
||||||
}: Props) => {
|
|
||||||
const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
|
|
||||||
const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false);
|
|
||||||
|
|
||||||
const { data: workspaces = [] } = useGetUserWorkspaces();
|
const { data: workspaces = [] } = useGetUserWorkspaces();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
|
|
||||||
const workspacesInOrg = workspaces.filter((ws) => ws.orgId === currentOrg?.id);
|
const workspacesInOrg = workspaces.filter((ws) => ws.orgId === currentOrg?.id);
|
||||||
|
|
||||||
|
const { control, watch, resetField, setValue, handleSubmit, formState } =
|
||||||
|
useForm<TAuditLogFilterFormData>({
|
||||||
|
resolver: zodResolver(auditLogFilterFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
project: null,
|
||||||
|
environment: undefined,
|
||||||
|
secretKey: "",
|
||||||
|
secretPath: "",
|
||||||
|
actor: presets?.actorId,
|
||||||
|
eventType: filter?.eventType || [],
|
||||||
|
userAgentType: undefined
|
||||||
|
},
|
||||||
|
values: filter
|
||||||
|
});
|
||||||
const selectedEventTypes = watch("eventType") as EventType[] | undefined;
|
const selectedEventTypes = watch("eventType") as EventType[] | undefined;
|
||||||
const selectedProject = watch("project");
|
const selectedProject = watch("project");
|
||||||
|
|
||||||
@@ -140,408 +103,345 @@ export const LogsFilter = ({
|
|||||||
return workspacesInOrg.find((ws) => ws.id === selectedProject.id)?.environments ?? [];
|
return workspacesInOrg.find((ws) => ws.id === selectedProject.id)?.environments ?? [];
|
||||||
}, [selectedProject, workspacesInOrg]);
|
}, [selectedProject, workspacesInOrg]);
|
||||||
|
|
||||||
const activeFilterCount = getActiveFilterCount(getFieldState, watch);
|
const activeFilterCount = getActiveFilterCount(filter);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="outline_bg" colorSchema="primary">
|
<Button variant="outline_bg" colorSchema="primary" className="relative">
|
||||||
<FontAwesomeIcon icon={faFilterCircleXmark} className="mr-3 px-[0.1rem]" />
|
<FontAwesomeIcon icon={faFilterCircleXmark} />
|
||||||
Filters
|
|
||||||
{activeFilterCount > 0 && (
|
{activeFilterCount > 0 && (
|
||||||
<Badge className="ml-2 px-1.5 py-0.5" variant="primary">
|
<Badge className="absolute bottom-0 right-0" variant="primary">
|
||||||
{activeFilterCount}
|
{activeFilterCount}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="mt-4 py-4">
|
<DropdownMenuContent align="end" className="mt-4 py-4">
|
||||||
<div className="flex min-w-64 flex-col font-inter">
|
<form onSubmit={handleSubmit(setFilter)}>
|
||||||
<div className="mb-3 flex items-center border-b border-b-mineshaft-500 px-3 pb-2">
|
<div className="flex min-w-64 flex-col font-inter">
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="mb-3 flex items-center border-b border-b-mineshaft-500 px-3 pb-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex w-full items-center justify-between">
|
||||||
<span>Filters</span>
|
<div className="flex items-center gap-2">
|
||||||
<Badge className="px-1.5 py-0.5" variant="primary">
|
<span>Filters</span>
|
||||||
{activeFilterCount}
|
<Badge className="px-1.5 py-0.5" variant="primary">
|
||||||
</Badge>
|
{activeFilterCount}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setFilter({
|
||||||
|
eventType: presets?.eventType || [],
|
||||||
|
actor: presets?.actorId,
|
||||||
|
userAgentType: undefined,
|
||||||
|
project: null,
|
||||||
|
secretPath: undefined,
|
||||||
|
secretKey: undefined
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
variant="link"
|
||||||
|
className="text-mineshaft-400"
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
Clear filters
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
</div>
|
||||||
onClick={() => {
|
|
||||||
reset({
|
<div className="px-3">
|
||||||
eventType: presets?.eventType || [],
|
<LogFilterItem
|
||||||
actor: presets?.actorId,
|
label="Events"
|
||||||
userAgentType: undefined,
|
onClear={() => {
|
||||||
startDate: undefined,
|
resetField("eventType");
|
||||||
endDate: undefined,
|
|
||||||
project: null,
|
|
||||||
secretPath: undefined,
|
|
||||||
secretKey: undefined
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
variant="link"
|
|
||||||
className="text-mineshaft-400"
|
|
||||||
size="xs"
|
|
||||||
>
|
>
|
||||||
Clear filters
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="eventType"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormControl>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<div className="thin-scrollbar inline-flex w-full cursor-pointer items-center justify-between whitespace-nowrap rounded-md border border-mineshaft-500 bg-mineshaft-700 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
|
||||||
|
{selectedEventTypes?.length === 1
|
||||||
|
? eventTypes.find(
|
||||||
|
(eventType) => eventType.value === selectedEventTypes[0]
|
||||||
|
)?.label
|
||||||
|
: selectedEventTypes?.length === 0
|
||||||
|
? "All events"
|
||||||
|
: `${selectedEventTypes?.length} events selected`}
|
||||||
|
<FontAwesomeIcon icon={faCaretDown} className="ml-2 text-xs" />
|
||||||
|
</div>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
align="start"
|
||||||
|
className="thin-scrollbar z-[100] max-h-80 overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="max-h-80 overflow-y-auto">
|
||||||
|
{eventTypes && eventTypes.length > 0 ? (
|
||||||
|
eventTypes.map((eventType) => {
|
||||||
|
const isSelected = selectedEventTypes?.includes(
|
||||||
|
eventType.value as EventType
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={(event) =>
|
||||||
|
eventTypes.length > 1 && event.preventDefault()
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
if (
|
||||||
|
selectedEventTypes?.includes(eventType.value as EventType)
|
||||||
|
) {
|
||||||
|
field.onChange(
|
||||||
|
selectedEventTypes?.filter(
|
||||||
|
(e: string) => e !== eventType.value
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
field.onChange([
|
||||||
|
...(selectedEventTypes || []),
|
||||||
|
eventType.value
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
key={`event-type-${eventType.value}`}
|
||||||
|
icon={
|
||||||
|
isSelected ? (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faCheckCircle}
|
||||||
|
className="pr-0.5 text-primary"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="pl-[1.01rem]" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
iconPos="left"
|
||||||
|
className="w-[28.4rem] text-sm"
|
||||||
|
>
|
||||||
|
{eventType.label}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</LogFilterItem>
|
||||||
|
<LogFilterItem
|
||||||
|
label="Source"
|
||||||
|
onClear={() => {
|
||||||
|
resetField("userAgentType");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="userAgentType"
|
||||||
|
render={({ field: { onChange, value, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
value={value === undefined ? "all" : value}
|
||||||
|
onValueChange={(e) => {
|
||||||
|
if (e === "all") onChange(undefined);
|
||||||
|
else setValue("userAgentType", e as UserAgentType, { shouldDirty: true });
|
||||||
|
}}
|
||||||
|
className={twMerge("w-full border border-mineshaft-500 bg-mineshaft-700")}
|
||||||
|
>
|
||||||
|
<SelectItem value="all" key="all">
|
||||||
|
All sources
|
||||||
|
</SelectItem>
|
||||||
|
{userAgentTypes.map(({ label, value: userAgent }) => (
|
||||||
|
<SelectItem value={userAgent} key={label}>
|
||||||
|
{label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</LogFilterItem>
|
||||||
|
<LogFilterItem
|
||||||
|
label="Project"
|
||||||
|
onClear={() => {
|
||||||
|
resetField("project");
|
||||||
|
resetField("environment");
|
||||||
|
setValue("secretPath", "");
|
||||||
|
setValue("secretKey", "");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="project"
|
||||||
|
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
className="mb-0 w-full"
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
value={value}
|
||||||
|
isClearable
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e === null) {
|
||||||
|
setValue("secretPath", "");
|
||||||
|
setValue("secretKey", "");
|
||||||
|
}
|
||||||
|
resetField("environment");
|
||||||
|
onChange(e);
|
||||||
|
}}
|
||||||
|
placeholder="All projects"
|
||||||
|
options={workspacesInOrg.map(({ name, id, defaultProduct }) => ({
|
||||||
|
name,
|
||||||
|
id,
|
||||||
|
type: defaultProduct
|
||||||
|
}))}
|
||||||
|
getOptionValue={(option) => option.id}
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</LogFilterItem>
|
||||||
|
<AnimatePresence initial={false}>
|
||||||
|
{showSecretsSection && (
|
||||||
|
<motion.div
|
||||||
|
className="overflow-hidden"
|
||||||
|
initial={{ opacity: 0, height: 0 }}
|
||||||
|
animate={{ opacity: 1, height: "auto" }}
|
||||||
|
exit={{ opacity: 0, height: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
|
<div className="mb-3 mt-2">
|
||||||
|
<p className="text-xs opacity-60">Secrets</p>
|
||||||
|
<div className="h-[1px] w-full rounded-full bg-mineshaft-500" />
|
||||||
|
</div>
|
||||||
|
<LogFilterItem
|
||||||
|
label="Environment"
|
||||||
|
hoverTooltip={
|
||||||
|
!selectedProject
|
||||||
|
? "Select a project before filtering by environment."
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
className={twMerge(!selectedProject && "opacity-50")}
|
||||||
|
onClear={() => {
|
||||||
|
resetField("environment");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="environment"
|
||||||
|
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
value={value}
|
||||||
|
key={value?.name || "filter-environment"}
|
||||||
|
isClearable
|
||||||
|
isDisabled={!selectedProject}
|
||||||
|
onChange={(e) => onChange(e)}
|
||||||
|
placeholder="All environments"
|
||||||
|
options={availableEnvironments.map(({ name, slug }) => ({
|
||||||
|
name,
|
||||||
|
slug
|
||||||
|
}))}
|
||||||
|
getOptionValue={(option) => option.slug}
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</LogFilterItem>
|
||||||
|
<LogFilterItem
|
||||||
|
label="Secret Path"
|
||||||
|
hoverTooltip={
|
||||||
|
!selectedProject
|
||||||
|
? "Select a project before filtering by secret path."
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
className={twMerge(!selectedProject && "opacity-50")}
|
||||||
|
onClear={() => {
|
||||||
|
setValue("secretPath", "");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="secretPath"
|
||||||
|
render={({ field: { onChange, value, ...field } }) => (
|
||||||
|
<FormControl
|
||||||
|
tooltipText="Filter audit logs related to events that occurred on a specific secret path."
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder="Enter secret path"
|
||||||
|
className="disabled:cursor-not-allowed"
|
||||||
|
isDisabled={!selectedProject}
|
||||||
|
{...field}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</LogFilterItem>
|
||||||
|
|
||||||
|
<LogFilterItem
|
||||||
|
hoverTooltip={
|
||||||
|
!selectedProject
|
||||||
|
? "Select a project before filtering by secret key."
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
className={twMerge(!selectedProject && "opacity-50")}
|
||||||
|
label="Secret Key"
|
||||||
|
onClear={() => {
|
||||||
|
setValue("secretKey", "");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="secretKey"
|
||||||
|
render={({ field: { onChange, value, ...field } }) => (
|
||||||
|
<FormControl
|
||||||
|
tooltipText="Filter audit logs related to a specific secret."
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
isDisabled={!selectedProject}
|
||||||
|
{...field}
|
||||||
|
placeholder="Enter secret key"
|
||||||
|
className="disabled:cursor-not-allowed"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) =>
|
||||||
|
setValue("secretKey", e.target.value, { shouldDirty: true })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</LogFilterItem>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 px-3">
|
||||||
|
<Button size="xs" type="submit" isDisabled={!formState.isDirty}>
|
||||||
|
Apply
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
<div className="px-3">
|
|
||||||
<LogFilterItem
|
|
||||||
label="Events"
|
|
||||||
onClear={() => {
|
|
||||||
resetField("eventType");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="eventType"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormControl>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<div className="thin-scrollbar inline-flex w-full cursor-pointer items-center justify-between whitespace-nowrap rounded-md border border-mineshaft-500 bg-mineshaft-700 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
|
|
||||||
{selectedEventTypes?.length === 1
|
|
||||||
? eventTypes.find(
|
|
||||||
(eventType) => eventType.value === selectedEventTypes[0]
|
|
||||||
)?.label
|
|
||||||
: selectedEventTypes?.length === 0
|
|
||||||
? "All events"
|
|
||||||
: `${selectedEventTypes?.length} events selected`}
|
|
||||||
<FontAwesomeIcon icon={faCaretDown} className="ml-2 text-xs" />
|
|
||||||
</div>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent
|
|
||||||
align="start"
|
|
||||||
className="thin-scrollbar z-[100] max-h-80 overflow-hidden"
|
|
||||||
>
|
|
||||||
<div className="max-h-80 overflow-y-auto">
|
|
||||||
{eventTypes && eventTypes.length > 0 ? (
|
|
||||||
eventTypes.map((eventType) => {
|
|
||||||
const isSelected = selectedEventTypes?.includes(
|
|
||||||
eventType.value as EventType
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenuItem
|
|
||||||
onSelect={(event) =>
|
|
||||||
eventTypes.length > 1 && event.preventDefault()
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
if (
|
|
||||||
selectedEventTypes?.includes(eventType.value as EventType)
|
|
||||||
) {
|
|
||||||
field.onChange(
|
|
||||||
selectedEventTypes?.filter(
|
|
||||||
(e: string) => e !== eventType.value
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
field.onChange([
|
|
||||||
...(selectedEventTypes || []),
|
|
||||||
eventType.value
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
key={`event-type-${eventType.value}`}
|
|
||||||
icon={
|
|
||||||
isSelected ? (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faCheckCircle}
|
|
||||||
className="pr-0.5 text-primary"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="pl-[1.01rem]" />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
iconPos="left"
|
|
||||||
className="w-[28.4rem] text-sm"
|
|
||||||
>
|
|
||||||
{eventType.label}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<div />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</LogFilterItem>
|
|
||||||
<LogFilterItem
|
|
||||||
label="Source"
|
|
||||||
onClear={() => {
|
|
||||||
resetField("userAgentType");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="userAgentType"
|
|
||||||
render={({ field: { onChange, value, ...field }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
value={value === undefined ? "all" : value}
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => {
|
|
||||||
if (e === "all") onChange(undefined);
|
|
||||||
else setValue("userAgentType", e as UserAgentType, { shouldDirty: true });
|
|
||||||
}}
|
|
||||||
className={twMerge("w-full border border-mineshaft-500 bg-mineshaft-700")}
|
|
||||||
>
|
|
||||||
<SelectItem value="all" key="all">
|
|
||||||
All sources
|
|
||||||
</SelectItem>
|
|
||||||
{userAgentTypes.map(({ label, value: userAgent }) => (
|
|
||||||
<SelectItem value={userAgent} key={label}>
|
|
||||||
{label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</LogFilterItem>
|
|
||||||
|
|
||||||
<LogFilterItem
|
|
||||||
label="Date"
|
|
||||||
onClear={() => {
|
|
||||||
resetField("startDate");
|
|
||||||
resetField("endDate");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex h-10 w-full items-center justify-between gap-2">
|
|
||||||
<Controller
|
|
||||||
name="startDate"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
|
||||||
return (
|
|
||||||
<FormControl
|
|
||||||
className="relative top-2"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
>
|
|
||||||
<DatePicker
|
|
||||||
value={field.value || undefined}
|
|
||||||
onChange={onChange}
|
|
||||||
dateFormat="P"
|
|
||||||
popUpProps={{
|
|
||||||
open: isStartDatePickerOpen,
|
|
||||||
onOpenChange: setIsStartDatePickerOpen
|
|
||||||
}}
|
|
||||||
popUpContentProps={{}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex items-center -space-x-3">
|
|
||||||
<div className="h-[2px] w-[20px] rounded-full bg-mineshaft-500" />
|
|
||||||
<FontAwesomeIcon icon={faArrowRight} className="text-mineshaft-500" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
name="endDate"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
|
||||||
return (
|
|
||||||
<FormControl
|
|
||||||
className="relative top-2"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
>
|
|
||||||
<DatePicker
|
|
||||||
value={field.value || undefined}
|
|
||||||
onChange={onChange}
|
|
||||||
dateFormat="P"
|
|
||||||
popUpProps={{
|
|
||||||
open: isEndDatePickerOpen,
|
|
||||||
onOpenChange: setIsEndDatePickerOpen
|
|
||||||
}}
|
|
||||||
popUpContentProps={{}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</LogFilterItem>
|
|
||||||
<AnimatePresence initial={false}>
|
|
||||||
{showSecretsSection && (
|
|
||||||
<motion.div
|
|
||||||
className="mt-2 overflow-hidden"
|
|
||||||
initial={{ opacity: 0, height: 0 }}
|
|
||||||
animate={{ opacity: 1, height: "auto" }}
|
|
||||||
exit={{ opacity: 0, height: 0 }}
|
|
||||||
transition={{ duration: 0.3 }}
|
|
||||||
>
|
|
||||||
<div className="mb-3 mt-2">
|
|
||||||
<p className="text-xs opacity-60">Secrets</p>
|
|
||||||
<div className="h-[1px] w-full rounded-full bg-mineshaft-500" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<LogFilterItem
|
|
||||||
label="Project"
|
|
||||||
onClear={() => {
|
|
||||||
resetField("project");
|
|
||||||
resetField("environment");
|
|
||||||
setValue("secretPath", "");
|
|
||||||
setValue("secretKey", "");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="project"
|
|
||||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
<FilterableSelect
|
|
||||||
value={value}
|
|
||||||
isClearable
|
|
||||||
onChange={(e) => {
|
|
||||||
if (e === null) {
|
|
||||||
setValue("secretPath", "");
|
|
||||||
setValue("secretKey", "");
|
|
||||||
}
|
|
||||||
resetField("environment");
|
|
||||||
onChange(e);
|
|
||||||
}}
|
|
||||||
placeholder="All projects"
|
|
||||||
options={workspacesInOrg.map(({ name, id, defaultProduct }) => ({
|
|
||||||
name,
|
|
||||||
id,
|
|
||||||
type: defaultProduct
|
|
||||||
}))}
|
|
||||||
getOptionValue={(option) => option.id}
|
|
||||||
getOptionLabel={(option) => option.name}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</LogFilterItem>
|
|
||||||
|
|
||||||
<LogFilterItem
|
|
||||||
label="Environment"
|
|
||||||
hoverTooltip={
|
|
||||||
!selectedProject
|
|
||||||
? "Select a project before filtering by environment."
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
className={twMerge(!selectedProject && "opacity-50")}
|
|
||||||
onClear={() => {
|
|
||||||
resetField("environment");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="environment"
|
|
||||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
<FilterableSelect
|
|
||||||
value={value}
|
|
||||||
key={value?.name || "filter-environment"}
|
|
||||||
isClearable
|
|
||||||
isDisabled={!selectedProject}
|
|
||||||
onChange={(e) => onChange(e)}
|
|
||||||
placeholder="All environments"
|
|
||||||
options={availableEnvironments.map(({ name, slug }) => ({
|
|
||||||
name,
|
|
||||||
slug
|
|
||||||
}))}
|
|
||||||
getOptionValue={(option) => option.slug}
|
|
||||||
getOptionLabel={(option) => option.name}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</LogFilterItem>
|
|
||||||
<LogFilterItem
|
|
||||||
label="Secret Path"
|
|
||||||
hoverTooltip={
|
|
||||||
!selectedProject
|
|
||||||
? "Select a project before filtering by secret path."
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
className={twMerge(!selectedProject && "opacity-50")}
|
|
||||||
onClear={() => {
|
|
||||||
setValue("secretPath", "");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="secretPath"
|
|
||||||
render={({ field: { onChange, value, ...field } }) => (
|
|
||||||
<FormControl
|
|
||||||
tooltipText="Filter audit logs related to events that occurred on a specific secret path."
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
placeholder="Enter secret path"
|
|
||||||
className="disabled:cursor-not-allowed"
|
|
||||||
isDisabled={!selectedProject}
|
|
||||||
{...field}
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => onChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</LogFilterItem>
|
|
||||||
|
|
||||||
<LogFilterItem
|
|
||||||
hoverTooltip={
|
|
||||||
!selectedProject
|
|
||||||
? "Select a project before filtering by secret key."
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
className={twMerge(!selectedProject && "opacity-50")}
|
|
||||||
label="Secret Key"
|
|
||||||
onClear={() => {
|
|
||||||
setValue("secretKey", "");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="secretKey"
|
|
||||||
render={({ field: { onChange, value, ...field } }) => (
|
|
||||||
<FormControl
|
|
||||||
tooltipText="Filter audit logs related to a specific secret."
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
isDisabled={!selectedProject}
|
|
||||||
{...field}
|
|
||||||
placeholder="Enter secret key"
|
|
||||||
className="disabled:cursor-not-allowed"
|
|
||||||
value={value}
|
|
||||||
onChange={(e) =>
|
|
||||||
setValue("secretKey", e.target.value, { shouldDirty: true })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</LogFilterItem>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import ms from "ms";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
|
|
||||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
|
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
|
||||||
import { withPermission } from "@app/hoc";
|
import { withPermission } from "@app/hoc";
|
||||||
import { useDebounce } from "@app/hooks";
|
|
||||||
import { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
|
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
import { LogsDateFilter } from "./LogsDateFilter";
|
||||||
import { LogsFilter } from "./LogsFilter";
|
import { LogsFilter } from "./LogsFilter";
|
||||||
import { LogsTable } from "./LogsTable";
|
import { LogsTable } from "./LogsTable";
|
||||||
import { AuditLogFilterFormData, auditLogFilterFormSchema, Presets } from "./types";
|
import {
|
||||||
|
AuditLogDateFilterType,
|
||||||
|
Presets,
|
||||||
|
TAuditLogDateFilterFormData,
|
||||||
|
TAuditLogFilterFormData
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
presets?: Presets;
|
presets?: Presets;
|
||||||
@@ -24,74 +27,45 @@ export const LogsSection = withPermission(
|
|||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
|
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
|
||||||
|
const [logFilter, setLogFilter] = useState<TAuditLogFilterFormData>({
|
||||||
const { control, reset, watch, getFieldState, resetField, setValue } =
|
eventType: presets?.eventType || [],
|
||||||
useForm<AuditLogFilterFormData>({
|
actor: presets?.actorId
|
||||||
resolver: zodResolver(auditLogFilterFormSchema),
|
});
|
||||||
defaultValues: {
|
const [dateFilter, setDateFilter] = useState<TAuditLogDateFilterFormData>({
|
||||||
project: null,
|
startDate: new Date(Number(new Date()) - ms("1h")),
|
||||||
environment: undefined,
|
endDate: new Date(),
|
||||||
secretKey: "",
|
type: AuditLogDateFilterType.Relative,
|
||||||
secretPath: "",
|
relativeModeValue: "1h"
|
||||||
actor: presets?.actorId,
|
});
|
||||||
eventType: presets?.eventType || [],
|
|
||||||
userAgentType: undefined,
|
|
||||||
startDate: presets?.startDate ?? new Date(new Date().setDate(new Date().getDate() - 1)),
|
|
||||||
endDate: presets?.endDate ?? new Date(new Date(Date.now()).setHours(23, 59, 59, 999))
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (subscription && !subscription.auditLogs) {
|
if (subscription && !subscription.auditLogs) {
|
||||||
handlePopUpOpen("upgradePlan");
|
handlePopUpOpen("upgradePlan");
|
||||||
}
|
}
|
||||||
}, [subscription]);
|
}, [subscription]);
|
||||||
|
|
||||||
const eventType = watch("eventType") as EventType[] | undefined;
|
|
||||||
const userAgentType = watch("userAgentType") as UserAgentType | undefined;
|
|
||||||
const actor = watch("actor");
|
|
||||||
const projectId = watch("project")?.id;
|
|
||||||
const environment = watch("environment")?.slug;
|
|
||||||
const secretPath = watch("secretPath");
|
|
||||||
const secretKey = watch("secretKey");
|
|
||||||
|
|
||||||
const startDate = watch("startDate");
|
|
||||||
const endDate = watch("endDate");
|
|
||||||
|
|
||||||
const [debouncedSecretPath] = useDebounce<string>(secretPath!, 500);
|
|
||||||
const [debouncedSecretKey] = useDebounce<string>(secretKey!, 500);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex w-full justify-end">
|
<div className="flex w-full justify-end">
|
||||||
|
{showFilters && <LogsDateFilter filter={dateFilter} setFilter={setDateFilter} />}
|
||||||
{showFilters && (
|
{showFilters && (
|
||||||
<LogsFilter
|
<LogsFilter presets={presets} setFilter={setLogFilter} filter={logFilter} />
|
||||||
presets={presets}
|
|
||||||
control={control}
|
|
||||||
watch={watch}
|
|
||||||
reset={reset}
|
|
||||||
resetField={resetField}
|
|
||||||
getFieldState={getFieldState}
|
|
||||||
setValue={setValue}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LogsTable
|
<LogsTable
|
||||||
refetchInterval={refetchInterval}
|
refetchInterval={refetchInterval}
|
||||||
filter={{
|
filter={{
|
||||||
secretPath: debouncedSecretPath || undefined,
|
secretPath: logFilter.secretPath || undefined,
|
||||||
secretKey: debouncedSecretKey || undefined,
|
secretKey: logFilter.secretKey || undefined,
|
||||||
eventMetadata: presets?.eventMetadata,
|
eventMetadata: logFilter?.eventMetadata,
|
||||||
projectId,
|
projectId: logFilter?.project?.id,
|
||||||
actorType: presets?.actorType,
|
actorType: presets?.actorType,
|
||||||
limit: 15,
|
limit: 15,
|
||||||
eventType,
|
eventType: logFilter?.eventType,
|
||||||
userAgentType,
|
userAgentType: logFilter?.userAgentType,
|
||||||
startDate,
|
startDate: dateFilter?.startDate,
|
||||||
endDate,
|
endDate: dateFilter?.endDate,
|
||||||
environment,
|
environment: logFilter?.environment?.slug,
|
||||||
actor
|
actor: logFilter?.actor
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<UpgradePlanModal
|
<UpgradePlanModal
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ import { TGetAuditLogsFilter } from "@app/hooks/api/auditLogs/types";
|
|||||||
import { LogsTableRow } from "./LogsTableRow";
|
import { LogsTableRow } from "./LogsTableRow";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
filter?: TGetAuditLogsFilter;
|
filter: TGetAuditLogsFilter;
|
||||||
refetchInterval?: number;
|
refetchInterval?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AUDIT_LOG_LIMIT = 15;
|
const AUDIT_LOG_LIMIT = 30;
|
||||||
|
|
||||||
export const LogsTable = ({ filter, refetchInterval }: Props) => {
|
export const LogsTable = ({ filter, refetchInterval }: Props) => {
|
||||||
// Determine the project ID for filtering
|
// Determine the project ID for filtering
|
||||||
|
|||||||
@@ -3,23 +3,33 @@ import { z } from "zod";
|
|||||||
import { ActorType, EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
|
import { ActorType, EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
|
||||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
export const auditLogFilterFormSchema = z
|
export enum AuditLogDateFilterType {
|
||||||
|
Relative = "relative",
|
||||||
|
Absolute = "absolute"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const auditLogFilterFormSchema = z.object({
|
||||||
|
eventMetadata: z.object({}).optional(),
|
||||||
|
project: z
|
||||||
|
.object({ id: z.string(), name: z.string(), type: z.nativeEnum(ProjectType) })
|
||||||
|
.optional()
|
||||||
|
.nullable(),
|
||||||
|
environment: z.object({ name: z.string(), slug: z.string() }).optional().nullable(),
|
||||||
|
eventType: z.nativeEnum(EventType).array(),
|
||||||
|
actor: z.string().optional(),
|
||||||
|
userAgentType: z.nativeEnum(UserAgentType).optional(),
|
||||||
|
secretPath: z.string().optional(),
|
||||||
|
secretKey: z.string().optional(),
|
||||||
|
page: z.coerce.number().optional(),
|
||||||
|
perPage: z.coerce.number().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const auditLogDateFilterFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
eventMetadata: z.object({}).optional(),
|
type: z.nativeEnum(AuditLogDateFilterType),
|
||||||
project: z
|
relativeModeValue: z.string().optional(),
|
||||||
.object({ id: z.string(), name: z.string(), type: z.nativeEnum(ProjectType) })
|
startDate: z.date(),
|
||||||
.optional()
|
endDate: z.date()
|
||||||
.nullable(),
|
|
||||||
environment: z.object({ name: z.string(), slug: z.string() }).optional().nullable(),
|
|
||||||
eventType: z.nativeEnum(EventType).array(),
|
|
||||||
actor: z.string().optional(),
|
|
||||||
userAgentType: z.nativeEnum(UserAgentType),
|
|
||||||
secretPath: z.string().optional(),
|
|
||||||
secretKey: z.string().optional(),
|
|
||||||
startDate: z.date().optional(),
|
|
||||||
endDate: z.date().optional(),
|
|
||||||
page: z.coerce.number().optional(),
|
|
||||||
perPage: z.coerce.number().optional()
|
|
||||||
})
|
})
|
||||||
.superRefine((el, ctx) => {
|
.superRefine((el, ctx) => {
|
||||||
if (el.endDate && el.startDate && el.endDate < el.startDate) {
|
if (el.endDate && el.startDate && el.endDate < el.startDate) {
|
||||||
@@ -31,10 +41,11 @@ export const auditLogFilterFormSchema = z
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export type AuditLogFilterFormData = z.infer<typeof auditLogFilterFormSchema>;
|
export type TAuditLogFilterFormData = z.infer<typeof auditLogFilterFormSchema>;
|
||||||
|
export type TAuditLogDateFilterFormData = z.infer<typeof auditLogDateFilterFormSchema>;
|
||||||
|
|
||||||
export type SetValueType = (
|
export type SetValueType = (
|
||||||
name: keyof AuditLogFilterFormData,
|
name: keyof TAuditLogFilterFormData,
|
||||||
value: any,
|
value: any,
|
||||||
options?: {
|
options?: {
|
||||||
shouldValidate?: boolean;
|
shouldValidate?: boolean;
|
||||||
|
|||||||
@@ -1,41 +1,37 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
import { BreadcrumbTypes } from '@app/components/v2'
|
import { BreadcrumbTypes } from "@app/components/v2";
|
||||||
import { workspaceKeys } from '@app/hooks/api'
|
import { workspaceKeys } from "@app/hooks/api";
|
||||||
import {
|
import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries";
|
||||||
fetchUserProjectPermissions,
|
import { fetchWorkspaceById } from "@app/hooks/api/workspace/queries";
|
||||||
roleQueryKeys,
|
import { ProjectLayout } from "@app/layouts/ProjectLayout";
|
||||||
} from '@app/hooks/api/roles/queries'
|
import { ProjectSelect } from "@app/layouts/ProjectLayout/components/ProjectSelect";
|
||||||
import { fetchWorkspaceById } from '@app/hooks/api/workspace/queries'
|
|
||||||
import { ProjectLayout } from '@app/layouts/ProjectLayout'
|
|
||||||
import { ProjectSelect } from '@app/layouts/ProjectLayout/components/ProjectSelect'
|
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
export const Route = createFileRoute(
|
||||||
'/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout',
|
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout"
|
||||||
)({
|
)({
|
||||||
component: ProjectLayout,
|
component: ProjectLayout,
|
||||||
beforeLoad: async ({ params, context }) => {
|
beforeLoad: async ({ params, context }) => {
|
||||||
const project = await context.queryClient.ensureQueryData({
|
const project = await context.queryClient.ensureQueryData({
|
||||||
queryKey: workspaceKeys.getWorkspaceById(params.projectId),
|
queryKey: workspaceKeys.getWorkspaceById(params.projectId),
|
||||||
queryFn: () => fetchWorkspaceById(params.projectId),
|
queryFn: () => fetchWorkspaceById(params.projectId)
|
||||||
})
|
});
|
||||||
|
|
||||||
await context.queryClient.ensureQueryData({
|
await context.queryClient.ensureQueryData({
|
||||||
queryKey: roleQueryKeys.getUserProjectPermissions({
|
queryKey: roleQueryKeys.getUserProjectPermissions({
|
||||||
workspaceId: params.projectId,
|
workspaceId: params.projectId
|
||||||
}),
|
}),
|
||||||
queryFn: () =>
|
queryFn: () => fetchUserProjectPermissions({ workspaceId: params.projectId })
|
||||||
fetchUserProjectPermissions({ workspaceId: params.projectId }),
|
});
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
project,
|
project,
|
||||||
breadcrumbs: [
|
breadcrumbs: [
|
||||||
{
|
{
|
||||||
type: BreadcrumbTypes.Component,
|
type: BreadcrumbTypes.Component,
|
||||||
component: ProjectSelect,
|
component: ProjectSelect
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
}
|
};
|
||||||
},
|
}
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { createFileRoute, redirect } from '@tanstack/react-router'
|
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||||
|
|
||||||
// this is done as part of migration for multi product inside project
|
// this is done as part of migration for multi product inside project
|
||||||
export const Route = createFileRoute(
|
export const Route = createFileRoute(
|
||||||
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/approval',
|
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/approval"
|
||||||
)({
|
)({
|
||||||
beforeLoad: ({ params, search }) => {
|
beforeLoad: ({ params, search }) => {
|
||||||
throw redirect({
|
throw redirect({
|
||||||
to: '/projects/$projectId/secret-manager/approval',
|
to: "/projects/$projectId/secret-manager/approval",
|
||||||
params,
|
params,
|
||||||
search,
|
search
|
||||||
})
|
});
|
||||||
},
|
}
|
||||||
})
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user