mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 05:27:48 +00:00
feat(audit-logs): filtering revamp
This commit is contained in:
@@ -9,13 +9,14 @@ import { logger } from "@app/lib/logger";
|
|||||||
import { QueueName } from "@app/queue";
|
import { QueueName } from "@app/queue";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
import { EventType } from "./audit-log-types";
|
import { EventType, filterableSecretEvents } from "./audit-log-types";
|
||||||
|
|
||||||
export type TAuditLogDALFactory = ReturnType<typeof auditLogDALFactory>;
|
export type TAuditLogDALFactory = ReturnType<typeof auditLogDALFactory>;
|
||||||
|
|
||||||
type TFindQuery = {
|
type TFindQuery = {
|
||||||
actor?: string;
|
actor?: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
|
environment?: string;
|
||||||
orgId?: string;
|
orgId?: string;
|
||||||
eventType?: string;
|
eventType?: string;
|
||||||
startDate?: string;
|
startDate?: string;
|
||||||
@@ -32,6 +33,7 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
|||||||
{
|
{
|
||||||
orgId,
|
orgId,
|
||||||
projectId,
|
projectId,
|
||||||
|
environment,
|
||||||
userAgentType,
|
userAgentType,
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
@@ -92,7 +94,13 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (projectId) {
|
const eventIsSecretType = !eventType?.length || eventType.some((event) => filterableSecretEvents.includes(event));
|
||||||
|
// We only want to filter for environment/secretPath/secretKey if the user is either checking for all event types
|
||||||
|
if (projectId && eventIsSecretType) {
|
||||||
|
if (environment) {
|
||||||
|
void sqlQuery.whereRaw(`"eventMetadata"->>'environment' = ?`, [environment]);
|
||||||
|
}
|
||||||
|
|
||||||
if (secretPath) {
|
if (secretPath) {
|
||||||
void sqlQuery.whereRaw(`"eventMetadata"->>'secretPath' = ?`, [secretPath]);
|
void sqlQuery.whereRaw(`"eventMetadata"->>'secretPath' = ?`, [secretPath]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export const auditLogServiceFactory = ({
|
|||||||
eventMetadata: filter.eventMetadata,
|
eventMetadata: filter.eventMetadata,
|
||||||
secretPath: filter.secretPath,
|
secretPath: filter.secretPath,
|
||||||
secretKey: filter.secretKey,
|
secretKey: filter.secretKey,
|
||||||
|
environment: filter.environment,
|
||||||
...(filter.projectId ? { projectId: filter.projectId } : { orgId: actorOrgId })
|
...(filter.projectId ? { projectId: filter.projectId } : { orgId: actorOrgId })
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -287,6 +287,16 @@ export enum EventType {
|
|||||||
KMIP_OPERATION_REGISTER = "kmip-operation-register"
|
KMIP_OPERATION_REGISTER = "kmip-operation-register"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const filterableSecretEvents: EventType[] = [
|
||||||
|
EventType.GET_SECRET,
|
||||||
|
EventType.DELETE_SECRETS,
|
||||||
|
EventType.CREATE_SECRETS,
|
||||||
|
EventType.UPDATE_SECRETS,
|
||||||
|
EventType.CREATE_SECRET,
|
||||||
|
EventType.UPDATE_SECRET,
|
||||||
|
EventType.DELETE_SECRET
|
||||||
|
];
|
||||||
|
|
||||||
interface UserActorMetadata {
|
interface UserActorMetadata {
|
||||||
userId: string;
|
userId: string;
|
||||||
email?: string | null;
|
email?: string | null;
|
||||||
|
|||||||
@@ -840,6 +840,8 @@ export const AUDIT_LOGS = {
|
|||||||
EXPORT: {
|
EXPORT: {
|
||||||
projectId:
|
projectId:
|
||||||
"Optionally filter logs by project ID. If not provided, logs from the entire organization will be returned.",
|
"Optionally filter logs by project ID. If not provided, logs from the entire organization will be returned.",
|
||||||
|
environment:
|
||||||
|
"The environment to filter logs by. If not provided, logs from all environments will be returned. Note that the projectId parameter must also be provided.",
|
||||||
eventType: "The type of the event to export.",
|
eventType: "The type of the event to export.",
|
||||||
secretPath:
|
secretPath:
|
||||||
"The path of the secret to query audit logs for. Note that the projectId parameter must also be provided.",
|
"The path of the secret to query audit logs for. Note that the projectId parameter must also be provided.",
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
description: "Get all audit logs for an organization",
|
description: "Get all audit logs for an organization",
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
projectId: z.string().optional().describe(AUDIT_LOGS.EXPORT.projectId),
|
projectId: z.string().optional().describe(AUDIT_LOGS.EXPORT.projectId),
|
||||||
|
environment: z.string().optional().describe(AUDIT_LOGS.EXPORT.environment),
|
||||||
actorType: z.nativeEnum(ActorType).optional(),
|
actorType: z.nativeEnum(ActorType).optional(),
|
||||||
secretPath: z
|
secretPath: z
|
||||||
.string()
|
.string()
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
import { EventType, UserAgentType } from "./enums";
|
import { EventType, UserAgentType } from "./enums";
|
||||||
|
|
||||||
|
export const secretEvents: EventType[] = [
|
||||||
|
EventType.GET_SECRETS,
|
||||||
|
EventType.GET_SECRET,
|
||||||
|
EventType.DELETE_SECRETS,
|
||||||
|
EventType.CREATE_SECRETS,
|
||||||
|
EventType.UPDATE_SECRETS,
|
||||||
|
EventType.CREATE_SECRET,
|
||||||
|
EventType.UPDATE_SECRET,
|
||||||
|
EventType.DELETE_SECRET
|
||||||
|
];
|
||||||
|
|
||||||
export const eventToNameMap: { [K in EventType]: string } = {
|
export const eventToNameMap: { [K in EventType]: string } = {
|
||||||
[EventType.GET_SECRETS]: "List secrets",
|
[EventType.GET_SECRETS]: "List secrets",
|
||||||
[EventType.GET_SECRET]: "Read secret",
|
[EventType.GET_SECRET]: "Read secret",
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ export enum UserAgentType {
|
|||||||
CLI = "cli",
|
CLI = "cli",
|
||||||
K8_OPERATOR = "k8-operator",
|
K8_OPERATOR = "k8-operator",
|
||||||
TERRAFORM = "terraform",
|
TERRAFORM = "terraform",
|
||||||
NODE_SDK = "node-sdk",
|
NODE_SDK = "InfisicalNodeSDK",
|
||||||
PYTHON_SDK = "python-sdk",
|
PYTHON_SDK = "InfisicalPythonSDK",
|
||||||
OTHER = "other"
|
OTHER = "other"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export type TGetAuditLogsFilter = {
|
|||||||
eventMetadata?: Record<string, string>;
|
eventMetadata?: Record<string, string>;
|
||||||
actorType?: ActorType;
|
actorType?: ActorType;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
|
environment?: string;
|
||||||
actor?: string; // user ID format
|
actor?: string; // user ID format
|
||||||
secretPath?: string;
|
secretPath?: string;
|
||||||
secretKey?: string;
|
secretKey?: string;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const AuditLogsPage = () => {
|
|||||||
title="Audit logs"
|
title="Audit logs"
|
||||||
description="Audit logs for security and compliance teams to monitor information access."
|
description="Audit logs for security and compliance teams to monitor information access."
|
||||||
/>
|
/>
|
||||||
<LogsSection filterClassName="static py-2" showFilters />
|
<LogsSection />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { Button, Tooltip } from "@app/components/v2";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
hoverTooltip?: string;
|
||||||
|
className?: string;
|
||||||
|
label: string;
|
||||||
|
onClear: () => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LogFilterItem = ({ label, onClear, hoverTooltip, children, className }: Props) => {
|
||||||
|
return (
|
||||||
|
<Tooltip className="relative top-4" content={hoverTooltip} isDisabled={!hoverTooltip}>
|
||||||
|
<div className={twMerge("flex flex-col justify-between", className)}>
|
||||||
|
<div className="flex items-center justify-between pr-1">
|
||||||
|
<p className="text-xs opacity-60">{label}</p>
|
||||||
|
<Button
|
||||||
|
onClick={() => onClear()}
|
||||||
|
variant="link"
|
||||||
|
className="font-normal text-mineshaft-400 transition-all duration-75 hover:text-mineshaft-300"
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,10 +1,26 @@
|
|||||||
import { useState } from "react";
|
/* eslint-disable no-nested-ternary */
|
||||||
import { Control, Controller, UseFormReset, UseFormSetValue, UseFormWatch } from "react-hook-form";
|
import { useMemo, useState } from "react";
|
||||||
import { faCaretDown, faCheckCircle, faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons";
|
import {
|
||||||
|
Control,
|
||||||
|
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 { AnimatePresence, motion } from "framer-motion";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -18,13 +34,17 @@ import {
|
|||||||
SelectItem
|
SelectItem
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization } from "@app/context";
|
import { useOrganization } from "@app/context";
|
||||||
import { useGetAuditLogActorFilterOpts, useGetUserWorkspaces } from "@app/hooks/api";
|
import { useGetUserWorkspaces } from "@app/hooks/api";
|
||||||
import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants";
|
import {
|
||||||
import { ActorType, EventType } from "@app/hooks/api/auditLogs/enums";
|
eventToNameMap,
|
||||||
import { Actor } from "@app/hooks/api/auditLogs/types";
|
secretEvents,
|
||||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
userAgentTTypeoNameMap
|
||||||
|
} from "@app/hooks/api/auditLogs/constants";
|
||||||
|
import { EventType } from "@app/hooks/api/auditLogs/enums";
|
||||||
|
import { UserAgentType } from "@app/hooks/api/auth/types";
|
||||||
|
|
||||||
import { AuditLogFilterFormData } from "./types";
|
import { LogFilterItem } from "./LogFilterItem";
|
||||||
|
import { AuditLogFilterFormData, Presets } 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(userAgentTTypeoNameMap).map(([value, label]) => ({
|
const userAgentTypes = Object.entries(userAgentTTypeoNameMap).map(([value, label]) => ({
|
||||||
@@ -33,26 +53,70 @@ const userAgentTypes = Object.entries(userAgentTTypeoNameMap).map(([value, label
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
presets?: {
|
presets?: Presets;
|
||||||
actorId?: string;
|
|
||||||
eventType?: EventType[];
|
|
||||||
};
|
|
||||||
className?: string;
|
|
||||||
isOrgAuditLogs?: boolean;
|
|
||||||
setValue: UseFormSetValue<AuditLogFilterFormData>;
|
|
||||||
control: Control<AuditLogFilterFormData>;
|
control: Control<AuditLogFilterFormData>;
|
||||||
reset: UseFormReset<AuditLogFilterFormData>;
|
reset: UseFormReset<AuditLogFilterFormData>;
|
||||||
|
resetField: UseFormResetField<AuditLogFilterFormData>;
|
||||||
watch: UseFormWatch<AuditLogFilterFormData>;
|
watch: UseFormWatch<AuditLogFilterFormData>;
|
||||||
|
getFieldState: UseFormGetFieldState<AuditLogFilterFormData>;
|
||||||
|
setValue: UseFormSetValue<AuditLogFilterFormData>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getActiveFilterCount = (
|
||||||
|
getFieldState: UseFormGetFieldState<AuditLogFilterFormData>,
|
||||||
|
watch: UseFormWatch<AuditLogFilterFormData>
|
||||||
|
) => {
|
||||||
|
const fields = [
|
||||||
|
"actor",
|
||||||
|
"project",
|
||||||
|
"eventType",
|
||||||
|
"startDate",
|
||||||
|
"endDate",
|
||||||
|
"environment",
|
||||||
|
"secretPath",
|
||||||
|
"userAgentType",
|
||||||
|
"secretKey"
|
||||||
|
] as Partial<keyof AuditLogFilterFormData>[];
|
||||||
|
|
||||||
|
let filterCount = 0;
|
||||||
|
|
||||||
|
// either start or end date should only be counted as one filter
|
||||||
|
let dateProcessed = false;
|
||||||
|
|
||||||
|
fields.forEach((field) => {
|
||||||
|
const fieldState = getFieldState(field);
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (field === "startDate" || field === "endDate") {
|
||||||
|
dateProcessed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return filterCount;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LogsFilter = ({
|
export const LogsFilter = ({
|
||||||
presets,
|
presets,
|
||||||
isOrgAuditLogs,
|
|
||||||
className,
|
|
||||||
control,
|
control,
|
||||||
reset,
|
reset,
|
||||||
setValue,
|
resetField,
|
||||||
watch
|
watch,
|
||||||
|
getFieldState,
|
||||||
|
setValue
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
|
const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
|
||||||
const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false);
|
const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false);
|
||||||
@@ -62,317 +126,423 @@ export const LogsFilter = ({
|
|||||||
|
|
||||||
const workspacesInOrg = workspaces.filter((ws) => ws.orgId === currentOrg?.id);
|
const workspacesInOrg = workspaces.filter((ws) => ws.orgId === currentOrg?.id);
|
||||||
|
|
||||||
const { data, isPending } = useGetAuditLogActorFilterOpts(workspaces?.[0]?.id ?? "");
|
|
||||||
|
|
||||||
const renderActorSelectItem = (actor: Actor) => {
|
|
||||||
switch (actor.type) {
|
|
||||||
case ActorType.USER:
|
|
||||||
return (
|
|
||||||
<SelectItem
|
|
||||||
value={`${actor.type}-${actor.metadata.userId}`}
|
|
||||||
key={`user-actor-filter-${actor.metadata.userId}`}
|
|
||||||
>
|
|
||||||
{actor.metadata.email}
|
|
||||||
</SelectItem>
|
|
||||||
);
|
|
||||||
case ActorType.SERVICE:
|
|
||||||
return (
|
|
||||||
<SelectItem
|
|
||||||
value={`${actor.type}-${actor.metadata.serviceId}`}
|
|
||||||
key={`service-actor-filter-${actor.metadata.serviceId}`}
|
|
||||||
>
|
|
||||||
{actor.metadata.name}
|
|
||||||
</SelectItem>
|
|
||||||
);
|
|
||||||
case ActorType.IDENTITY:
|
|
||||||
return (
|
|
||||||
<SelectItem
|
|
||||||
value={`${actor.type}-${actor.metadata.identityId}`}
|
|
||||||
key={`identity-filter-${actor.metadata.identityId}`}
|
|
||||||
>
|
|
||||||
{actor.metadata.name}
|
|
||||||
</SelectItem>
|
|
||||||
);
|
|
||||||
case ActorType.KMIP_CLIENT:
|
|
||||||
return (
|
|
||||||
<SelectItem
|
|
||||||
value={`${actor.type}-${actor.metadata.clientId}`}
|
|
||||||
key={`kmip-client-filter-${actor.metadata.clientId}`}
|
|
||||||
>
|
|
||||||
{actor.metadata.name}
|
|
||||||
</SelectItem>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<SelectItem value="actor-none" key="actor-none">
|
|
||||||
N/A
|
|
||||||
</SelectItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectedEventTypes = watch("eventType") as EventType[] | undefined;
|
const selectedEventTypes = watch("eventType") as EventType[] | undefined;
|
||||||
const selectedProject = watch("project");
|
const selectedProject = watch("project");
|
||||||
|
|
||||||
|
const showSecretsSection =
|
||||||
|
selectedEventTypes?.some(
|
||||||
|
(eventType) => secretEvents.includes(eventType) && eventType !== EventType.GET_SECRETS
|
||||||
|
) || selectedEventTypes?.length === 0;
|
||||||
|
|
||||||
|
const availableEnvironments = useMemo(() => {
|
||||||
|
if (!selectedProject) return [];
|
||||||
|
|
||||||
|
return workspacesInOrg.find((ws) => ws.id === selectedProject.id)?.environments ?? [];
|
||||||
|
}, [selectedProject, workspacesInOrg]);
|
||||||
|
|
||||||
|
const activeFilterCount = getActiveFilterCount(getFieldState, watch);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={twMerge("sticky top-20 z-10 flex flex-col bg-bunker-800", className)}>
|
<DropdownMenu>
|
||||||
<div className="flex items-center gap-4">
|
<DropdownMenuTrigger asChild>
|
||||||
{isOrgAuditLogs && workspacesInOrg.length > 0 && (
|
<Button variant="outline_bg" colorSchema="primary">
|
||||||
<Controller
|
<FontAwesomeIcon icon={faFilterCircleXmark} className="mr-3 px-[0.1rem]" />
|
||||||
control={control}
|
Filters
|
||||||
name="project"
|
{activeFilterCount > 0 && (
|
||||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
<Badge className="ml-2 px-1.5 py-0.5" variant="primary">
|
||||||
<FormControl
|
{activeFilterCount}
|
||||||
label="Project"
|
</Badge>
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
className="w-64"
|
|
||||||
>
|
|
||||||
<FilterableSelect
|
|
||||||
value={value}
|
|
||||||
isClearable
|
|
||||||
onChange={(e) => {
|
|
||||||
if (e === null) {
|
|
||||||
setValue("secretPath", "");
|
|
||||||
}
|
|
||||||
onChange(e);
|
|
||||||
}}
|
|
||||||
placeholder="Select a project..."
|
|
||||||
options={workspacesInOrg.map(({ name, id, type }) => ({ name, id, type }))}
|
|
||||||
getOptionValue={(option) => option.id}
|
|
||||||
getOptionLabel={(option) => option.name}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{selectedProject?.type === ProjectType.SecretManager && (
|
|
||||||
<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."
|
|
||||||
label="Secret Path"
|
|
||||||
className="w-40"
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
placeholder="/folder"
|
|
||||||
{...field}
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => onChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedProject?.type === ProjectType.SecretManager && (
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="secretKey"
|
|
||||||
render={({ field: { onChange, value, ...field } }) => (
|
|
||||||
<FormControl
|
|
||||||
tooltipText="Filter audit logs related to a specific secret."
|
|
||||||
label="Secret Name"
|
|
||||||
className="w-40"
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
placeholder="API_KEY"
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => onChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="mt-1 flex w-full justify-start">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="eventType"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormControl label="Events">
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<div className="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="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>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{!isPending && data && data.length > 0 && !presets?.actorId && (
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="actor"
|
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Actor"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
className="w-40"
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
{...(field.value ? { value: field.value } : { placeholder: "Select" })}
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => onChange(e)}
|
|
||||||
className="w-full border border-mineshaft-500 bg-mineshaft-700 text-mineshaft-100"
|
|
||||||
>
|
|
||||||
{data.map((actor) => renderActorSelectItem(actor))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<Controller
|
</Button>
|
||||||
control={control}
|
</DropdownMenuTrigger>
|
||||||
name="userAgentType"
|
<DropdownMenuContent align="end" className="mt-4 py-4">
|
||||||
render={({ field: { onChange, value, ...field }, fieldState: { error } }) => (
|
<div className="flex min-w-64 flex-col font-inter">
|
||||||
<FormControl
|
<div className="mb-3 flex items-center border-b border-b-mineshaft-500 px-3 pb-2">
|
||||||
label="Source"
|
<div className="flex w-full items-center justify-between">
|
||||||
errorText={error?.message}
|
<div className="flex items-center gap-2">
|
||||||
isError={Boolean(error)}
|
<span>Filters</span>
|
||||||
className="w-40"
|
<Badge className="px-1.5 py-0.5" variant="primary">
|
||||||
|
{activeFilterCount}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
reset({
|
||||||
|
eventType: presets?.eventType || [],
|
||||||
|
actor: presets?.actorId,
|
||||||
|
userAgentType: undefined,
|
||||||
|
startDate: undefined,
|
||||||
|
endDate: undefined,
|
||||||
|
project: null,
|
||||||
|
secretPath: undefined,
|
||||||
|
secretKey: undefined
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
variant="link"
|
||||||
|
className="text-mineshaft-400"
|
||||||
|
size="xs"
|
||||||
>
|
>
|
||||||
<Select
|
Clear filters
|
||||||
value={value === undefined ? "all" : value}
|
</Button>
|
||||||
{...field}
|
</div>
|
||||||
onValueChange={(e) => {
|
</div>
|
||||||
if (e === "all") onChange(undefined);
|
|
||||||
else onChange(e);
|
<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>
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
className={twMerge("w-full border border-mineshaft-500 bg-mineshaft-700")}
|
/>
|
||||||
|
|
||||||
|
<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 }}
|
||||||
>
|
>
|
||||||
<SelectItem value="all" key="all">
|
<div className="mb-3 mt-2">
|
||||||
All sources
|
<p className="text-xs opacity-60">Secrets</p>
|
||||||
</SelectItem>
|
<div className="h-[1px] w-full rounded-full bg-mineshaft-500" />
|
||||||
{userAgentTypes.map(({ label, value: userAgent }) => (
|
</div>
|
||||||
<SelectItem value={userAgent} key={label}>
|
|
||||||
{label}
|
<LogFilterItem
|
||||||
</SelectItem>
|
label="Project"
|
||||||
))}
|
onClear={() => {
|
||||||
</Select>
|
resetField("project");
|
||||||
</FormControl>
|
resetField("environment");
|
||||||
)}
|
setValue("secretPath", "");
|
||||||
/>
|
setValue("secretKey", "");
|
||||||
<Controller
|
|
||||||
name="startDate"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
|
||||||
return (
|
|
||||||
<FormControl label="Start Date" errorText={error?.message} isError={Boolean(error)}>
|
|
||||||
<DatePicker
|
|
||||||
value={field.value || undefined}
|
|
||||||
onChange={onChange}
|
|
||||||
dateFormat="P"
|
|
||||||
popUpProps={{
|
|
||||||
open: isStartDatePickerOpen,
|
|
||||||
onOpenChange: setIsStartDatePickerOpen
|
|
||||||
}}
|
}}
|
||||||
popUpContentProps={{}}
|
>
|
||||||
/>
|
<Controller
|
||||||
</FormControl>
|
control={control}
|
||||||
);
|
name="project"
|
||||||
}}
|
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||||
/>
|
<FormControl
|
||||||
<Controller
|
errorText={error?.message}
|
||||||
name="endDate"
|
isError={Boolean(error)}
|
||||||
control={control}
|
className="w-full"
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
>
|
||||||
return (
|
<FilterableSelect
|
||||||
<FormControl label="End Date" errorText={error?.message} isError={Boolean(error)}>
|
value={value}
|
||||||
<DatePicker
|
isClearable
|
||||||
value={field.value || undefined}
|
onChange={(e) => {
|
||||||
onChange={onChange}
|
if (e === null) {
|
||||||
dateFormat="P"
|
setValue("secretPath", "");
|
||||||
popUpProps={{
|
setValue("secretKey", "");
|
||||||
open: isEndDatePickerOpen,
|
}
|
||||||
onOpenChange: setIsEndDatePickerOpen
|
resetField("environment");
|
||||||
|
onChange(e);
|
||||||
|
}}
|
||||||
|
placeholder="All projects"
|
||||||
|
options={workspacesInOrg.map(({ name, id, type }) => ({
|
||||||
|
name,
|
||||||
|
id,
|
||||||
|
type
|
||||||
|
}))}
|
||||||
|
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");
|
||||||
}}
|
}}
|
||||||
popUpContentProps={{}}
|
>
|
||||||
/>
|
<Controller
|
||||||
</FormControl>
|
control={control}
|
||||||
);
|
name="environment"
|
||||||
}}
|
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||||
/>
|
<FormControl
|
||||||
<Button
|
errorText={error?.message}
|
||||||
isLoading={false}
|
isError={Boolean(error)}
|
||||||
colorSchema="primary"
|
className="w-full"
|
||||||
variant="outline_bg"
|
>
|
||||||
className="mt-[0.45rem]"
|
<FilterableSelect
|
||||||
type="submit"
|
value={value}
|
||||||
leftIcon={<FontAwesomeIcon icon={faFilterCircleXmark} />}
|
key={value?.name || "filter-environment"}
|
||||||
onClick={() =>
|
isClearable
|
||||||
reset({
|
isDisabled={!selectedProject}
|
||||||
eventType: presets?.eventType || [],
|
onChange={(e) => onChange(e)}
|
||||||
actor: presets?.actorId,
|
placeholder="All environments"
|
||||||
userAgentType: undefined,
|
options={availableEnvironments.map(({ name, slug }) => ({
|
||||||
startDate: undefined,
|
name,
|
||||||
endDate: undefined,
|
slug
|
||||||
project: null,
|
}))}
|
||||||
secretPath: undefined,
|
getOptionValue={(option) => option.slug}
|
||||||
secretKey: undefined
|
getOptionLabel={(option) => option.name}
|
||||||
})
|
/>
|
||||||
}
|
</FormControl>
|
||||||
>
|
)}
|
||||||
Clear filters
|
/>
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
</div>
|
</DropdownMenuContent>
|
||||||
</div>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,46 +6,40 @@ 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 { useDebounce } from "@app/hooks";
|
||||||
import { ActorType, EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
|
import { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { LogsFilter } from "./LogsFilter";
|
import { LogsFilter } from "./LogsFilter";
|
||||||
import { LogsTable } from "./LogsTable";
|
import { LogsTable } from "./LogsTable";
|
||||||
import { AuditLogFilterFormData, auditLogFilterFormSchema } from "./types";
|
import { AuditLogFilterFormData, auditLogFilterFormSchema, Presets } from "./types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
presets?: {
|
presets?: Presets;
|
||||||
actorId?: string;
|
|
||||||
eventType?: EventType[];
|
|
||||||
actorType?: ActorType;
|
|
||||||
startDate?: Date;
|
|
||||||
endDate?: Date;
|
|
||||||
eventMetadata?: Record<string, string>;
|
|
||||||
};
|
|
||||||
|
|
||||||
showFilters?: boolean;
|
|
||||||
filterClassName?: string;
|
|
||||||
refetchInterval?: number;
|
refetchInterval?: number;
|
||||||
|
showFilters?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LogsSection = withPermission(
|
export const LogsSection = withPermission(
|
||||||
({ presets, filterClassName, refetchInterval, showFilters }: Props) => {
|
({ presets, refetchInterval, showFilters = true }: Props) => {
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
|
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
|
||||||
|
|
||||||
const { control, reset, watch, setValue } = useForm<AuditLogFilterFormData>({
|
const { control, reset, watch, getFieldState, resetField, setValue } =
|
||||||
resolver: zodResolver(auditLogFilterFormSchema),
|
useForm<AuditLogFilterFormData>({
|
||||||
defaultValues: {
|
resolver: zodResolver(auditLogFilterFormSchema),
|
||||||
project: null,
|
defaultValues: {
|
||||||
actor: presets?.actorId,
|
project: null,
|
||||||
eventType: presets?.eventType || [],
|
environment: undefined,
|
||||||
page: 1,
|
secretKey: "",
|
||||||
perPage: 10,
|
secretPath: "",
|
||||||
startDate: presets?.startDate ?? new Date(new Date().setDate(new Date().getDate() - 1)), // day before today
|
actor: presets?.actorId,
|
||||||
endDate: presets?.endDate ?? new Date(new Date(Date.now()).setHours(23, 59, 59, 999)) // end of today
|
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) {
|
||||||
@@ -57,6 +51,7 @@ export const LogsSection = withPermission(
|
|||||||
const userAgentType = watch("userAgentType") as UserAgentType | undefined;
|
const userAgentType = watch("userAgentType") as UserAgentType | undefined;
|
||||||
const actor = watch("actor");
|
const actor = watch("actor");
|
||||||
const projectId = watch("project")?.id;
|
const projectId = watch("project")?.id;
|
||||||
|
const environment = watch("environment")?.slug;
|
||||||
const secretPath = watch("secretPath");
|
const secretPath = watch("secretPath");
|
||||||
const secretKey = watch("secretKey");
|
const secretKey = watch("secretKey");
|
||||||
|
|
||||||
@@ -67,18 +62,21 @@ export const LogsSection = withPermission(
|
|||||||
const [debouncedSecretKey] = useDebounce<string>(secretKey!, 500);
|
const [debouncedSecretKey] = useDebounce<string>(secretKey!, 500);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
{showFilters && (
|
<div className="flex w-full justify-end">
|
||||||
<LogsFilter
|
{showFilters && (
|
||||||
isOrgAuditLogs
|
<LogsFilter
|
||||||
className={filterClassName}
|
presets={presets}
|
||||||
presets={presets}
|
control={control}
|
||||||
control={control}
|
watch={watch}
|
||||||
setValue={setValue}
|
reset={reset}
|
||||||
watch={watch}
|
resetField={resetField}
|
||||||
reset={reset}
|
getFieldState={getFieldState}
|
||||||
/>
|
setValue={setValue}
|
||||||
)}
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<LogsTable
|
<LogsTable
|
||||||
refetchInterval={refetchInterval}
|
refetchInterval={refetchInterval}
|
||||||
filter={{
|
filter={{
|
||||||
@@ -92,6 +90,7 @@ export const LogsSection = withPermission(
|
|||||||
userAgentType,
|
userAgentType,
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
|
environment,
|
||||||
actor
|
actor
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { faFile, faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
import { faFile, faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
|
Spinner,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
@@ -52,7 +54,9 @@ export const LogsTable = ({ filter, refetchInterval }: Props) => {
|
|||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th className="w-24" />
|
<Th className="w-24">
|
||||||
|
<Spinner size="xs" className={twMerge(isPending ? "opacity-100" : "opacity-0")} />
|
||||||
|
</Th>
|
||||||
<Th className="w-64">
|
<Th className="w-64">
|
||||||
Timestamp
|
Timestamp
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { 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 const auditLogFilterFormSchema = z
|
||||||
@@ -10,6 +10,7 @@ export const auditLogFilterFormSchema = z
|
|||||||
.object({ id: z.string(), name: z.string(), type: z.nativeEnum(ProjectType) })
|
.object({ id: z.string(), name: z.string(), type: z.nativeEnum(ProjectType) })
|
||||||
.optional()
|
.optional()
|
||||||
.nullable(),
|
.nullable(),
|
||||||
|
environment: z.object({ name: z.string(), slug: z.string() }).optional().nullable(),
|
||||||
eventType: z.nativeEnum(EventType).array(),
|
eventType: z.nativeEnum(EventType).array(),
|
||||||
actor: z.string().optional(),
|
actor: z.string().optional(),
|
||||||
userAgentType: z.nativeEnum(UserAgentType),
|
userAgentType: z.nativeEnum(UserAgentType),
|
||||||
@@ -40,3 +41,12 @@ export type SetValueType = (
|
|||||||
shouldDirty?: boolean;
|
shouldDirty?: boolean;
|
||||||
}
|
}
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
|
export type Presets = {
|
||||||
|
actorId?: string;
|
||||||
|
eventType?: EventType[];
|
||||||
|
actorType?: ActorType;
|
||||||
|
startDate?: Date;
|
||||||
|
endDate?: Date;
|
||||||
|
eventMetadata?: Record<string, string>;
|
||||||
|
};
|
||||||
|
|||||||
-23
@@ -1,8 +1,3 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { faFilter } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import { IconButton, Tooltip } from "@app/components/v2";
|
|
||||||
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 { OrgUser } from "@app/hooks/api/types";
|
import { OrgUser } from "@app/hooks/api/types";
|
||||||
@@ -14,7 +9,6 @@ type Props = {
|
|||||||
|
|
||||||
export const UserAuditLogsSection = withPermission(
|
export const UserAuditLogsSection = withPermission(
|
||||||
({ orgMembership }: Props) => {
|
({ orgMembership }: Props) => {
|
||||||
const [showFilter, setShowFilter] = useState(false);
|
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
|
|
||||||
// eslint-disable-next-line no-nested-ternary
|
// eslint-disable-next-line no-nested-ternary
|
||||||
@@ -23,25 +17,8 @@ export const UserAuditLogsSection = withPermission(
|
|||||||
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="mb-4 flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
<div className="mb-4 flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||||
<p className="text-lg font-semibold text-gray-200">Audit Logs</p>
|
<p className="text-lg font-semibold text-gray-200">Audit Logs</p>
|
||||||
|
|
||||||
<Tooltip content="Show audit log filters">
|
|
||||||
<IconButton
|
|
||||||
colorSchema="primary"
|
|
||||||
ariaLabel="copy icon"
|
|
||||||
variant="plain"
|
|
||||||
className="group relative"
|
|
||||||
onClick={() => setShowFilter(!showFilter)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<p>Filter</p>
|
|
||||||
<FontAwesomeIcon icon={faFilter} />
|
|
||||||
</div>
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
<LogsSection
|
<LogsSection
|
||||||
showFilters={showFilter}
|
|
||||||
filterClassName="bg-mineshaft-900 static"
|
|
||||||
presets={{
|
presets={{
|
||||||
actorId: orgMembership.user.id
|
actorId: orgMembership.user.id
|
||||||
}}
|
}}
|
||||||
|
|||||||
-1
@@ -35,7 +35,6 @@ export const IntegrationAuditLogsSection = ({ integration }: Props) => {
|
|||||||
startDate: new Date(new Date().setDate(new Date().getDate() - auditLogsRetentionDays)),
|
startDate: new Date(new Date().setDate(new Date().getDate() - auditLogsRetentionDays)),
|
||||||
eventType: INTEGRATION_EVENTS
|
eventType: INTEGRATION_EVENTS
|
||||||
}}
|
}}
|
||||||
filterClassName="bg-mineshaft-900 static"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
-1
@@ -41,7 +41,6 @@ export const SecretSyncAuditLogsSection = ({ secretSync }: Props) => {
|
|||||||
startDate: new Date(new Date().setDate(new Date().getDate() - auditLogsRetentionDays)),
|
startDate: new Date(new Date().setDate(new Date().getDate() - auditLogsRetentionDays)),
|
||||||
eventType: INTEGRATION_EVENTS
|
eventType: INTEGRATION_EVENTS
|
||||||
}}
|
}}
|
||||||
filterClassName="bg-mineshaft-900 static"
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-full items-center justify-center rounded-lg bg-mineshaft-800 text-sm text-mineshaft-200">
|
<div className="flex h-full items-center justify-center rounded-lg bg-mineshaft-800 text-sm text-mineshaft-200">
|
||||||
|
|||||||
Reference in New Issue
Block a user