Merge pull request #3311 from Infisical/daniel/audit-log-secretname

feat(audit-logs): filter audit logs by secret key
This commit is contained in:
Daniel Hougaard
2025-04-01 17:16:34 +04:00
committed by GitHub
17 changed files with 649 additions and 369 deletions

View File

@@ -1,5 +1,16 @@
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 } = {
[EventType.GET_SECRETS]: "List secrets",
[EventType.GET_SECRET]: "Read secret",

View File

@@ -12,8 +12,8 @@ export enum UserAgentType {
CLI = "cli",
K8_OPERATOR = "k8-operator",
TERRAFORM = "terraform",
NODE_SDK = "node-sdk",
PYTHON_SDK = "python-sdk",
NODE_SDK = "InfisicalNodeSDK",
PYTHON_SDK = "InfisicalPythonSDK",
OTHER = "other"
}

View File

@@ -9,8 +9,10 @@ export type TGetAuditLogsFilter = {
eventMetadata?: Record<string, string>;
actorType?: ActorType;
projectId?: string;
environment?: string;
actor?: string; // user ID format
secretPath?: string;
secretKey?: string;
startDate?: Date;
endDate?: Date;
limit: number;

View File

@@ -18,7 +18,7 @@ export const AuditLogsPage = () => {
title="Audit logs"
description="Audit logs for security and compliance teams to monitor information access."
/>
<LogsSection filterClassName="static py-2" showFilters />
<LogsSection />
</div>
</div>
</div>

View File

@@ -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>
);
};

View File

@@ -1,11 +1,26 @@
/* eslint-disable no-nested-ternary */
import { useState } from "react";
import { Control, Controller, UseFormReset, UseFormSetValue, UseFormWatch } from "react-hook-form";
import { faCaretDown, faCheckCircle, faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons";
import { useMemo, useState } from "react";
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 { AnimatePresence, motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import {
Badge,
Button,
DatePicker,
DropdownMenu,
@@ -19,13 +34,17 @@ import {
SelectItem
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import { useGetAuditLogActorFilterOpts, useGetUserWorkspaces } from "@app/hooks/api";
import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants";
import { ActorType, EventType } from "@app/hooks/api/auditLogs/enums";
import { Actor } from "@app/hooks/api/auditLogs/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { useGetUserWorkspaces } from "@app/hooks/api";
import {
eventToNameMap,
secretEvents,
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 userAgentTypes = Object.entries(userAgentTTypeoNameMap).map(([value, label]) => ({
@@ -34,26 +53,70 @@ const userAgentTypes = Object.entries(userAgentTTypeoNameMap).map(([value, label
}));
type Props = {
presets?: {
actorId?: string;
eventType?: EventType[];
};
className?: string;
isOrgAuditLogs?: boolean;
setValue: UseFormSetValue<AuditLogFilterFormData>;
presets?: Presets;
control: Control<AuditLogFilterFormData>;
reset: UseFormReset<AuditLogFilterFormData>;
resetField: UseFormResetField<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 = ({
presets,
isOrgAuditLogs,
className,
control,
reset,
setValue,
watch
resetField,
watch,
getFieldState,
setValue
}: Props) => {
const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false);
@@ -63,288 +126,423 @@ export const LogsFilter = ({
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 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 (
<div
className={twMerge(
"sticky top-20 z-10 flex flex-wrap items-center justify-between bg-bunker-800",
className
)}
>
<div className="flex items-center gap-4">
{isOrgAuditLogs && workspacesInOrg.length > 0 && (
<Controller
control={control}
name="project"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl
label="Project"
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 label="Secret path" className="w-40">
<Input {...field} value={value} onChange={(e) => onChange(e.target.value)} />
</FormControl>
)}
/>
)}
</div>
<div className="mt-1 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>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline_bg" colorSchema="primary">
<FontAwesomeIcon icon={faFilterCircleXmark} className="mr-3 px-[0.1rem]" />
Filters
{activeFilterCount > 0 && (
<Badge className="ml-2 px-1.5 py-0.5" variant="primary">
{activeFilterCount}
</Badge>
)}
/>
{!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
control={control}
name="userAgentType"
render={({ field: { onChange, value, ...field }, fieldState: { error } }) => (
<FormControl
label="Source"
errorText={error?.message}
isError={Boolean(error)}
className="w-40"
>
<Select
value={value === undefined ? "all" : value}
{...field}
onValueChange={(e) => {
if (e === "all") onChange(undefined);
else onChange(e);
}}
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>
)}
/>
<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={{}}
/>
</FormControl>
);
}}
/>
<Controller
name="endDate"
control={control}
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl label="End date" errorText={error?.message} isError={Boolean(error)}>
<DatePicker
value={field.value || undefined}
onChange={onChange}
dateFormat="P"
popUpProps={{
open: isEndDatePickerOpen,
onOpenChange: setIsEndDatePickerOpen
}}
popUpContentProps={{}}
/>
</FormControl>
);
}}
/>
<Button
isLoading={false}
colorSchema="primary"
variant="outline_bg"
className="mt-[0.45rem]"
type="submit"
leftIcon={<FontAwesomeIcon icon={faFilterCircleXmark} />}
onClick={() =>
reset({
eventType: presets?.eventType || [],
actor: presets?.actorId,
userAgentType: undefined,
startDate: undefined,
endDate: undefined,
project: null
})
}
>
Clear filters
</Button>
</div>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="mt-4 py-4">
<div className="flex min-w-64 flex-col font-inter">
<div className="mb-3 flex items-center border-b border-b-mineshaft-500 px-3 pb-2">
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-2">
<span>Filters</span>
<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"
>
Clear filters
</Button>
</div>
</div>
<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, 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");
}}
>
<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>
</DropdownMenu>
);
};

View File

@@ -6,46 +6,40 @@ import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
import { withPermission } from "@app/hoc";
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 { LogsFilter } from "./LogsFilter";
import { LogsTable } from "./LogsTable";
import { AuditLogFilterFormData, auditLogFilterFormSchema } from "./types";
import { AuditLogFilterFormData, auditLogFilterFormSchema, Presets } from "./types";
type Props = {
presets?: {
actorId?: string;
eventType?: EventType[];
actorType?: ActorType;
startDate?: Date;
endDate?: Date;
eventMetadata?: Record<string, string>;
};
showFilters?: boolean;
filterClassName?: string;
presets?: Presets;
refetchInterval?: number;
showFilters?: boolean;
};
export const LogsSection = withPermission(
({ presets, filterClassName, refetchInterval, showFilters }: Props) => {
({ presets, refetchInterval, showFilters = true }: Props) => {
const { subscription } = useSubscription();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
const { control, reset, watch, setValue } = useForm<AuditLogFilterFormData>({
resolver: zodResolver(auditLogFilterFormSchema),
defaultValues: {
project: null,
actor: presets?.actorId,
eventType: presets?.eventType || [],
page: 1,
perPage: 10,
startDate: presets?.startDate ?? new Date(new Date().setDate(new Date().getDate() - 1)), // day before today
endDate: presets?.endDate ?? new Date(new Date(Date.now()).setHours(23, 59, 59, 999)) // end of today
}
});
const { control, reset, watch, getFieldState, resetField, setValue } =
useForm<AuditLogFilterFormData>({
resolver: zodResolver(auditLogFilterFormSchema),
defaultValues: {
project: null,
environment: undefined,
secretKey: "",
secretPath: "",
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(() => {
if (subscription && !subscription.auditLogs) {
@@ -57,30 +51,37 @@ export const LogsSection = withPermission(
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 (
<div>
{showFilters && (
<LogsFilter
isOrgAuditLogs
className={filterClassName}
presets={presets}
control={control}
setValue={setValue}
watch={watch}
reset={reset}
/>
)}
<div className="space-y-2">
<div className="flex w-full justify-end">
{showFilters && (
<LogsFilter
presets={presets}
control={control}
watch={watch}
reset={reset}
resetField={resetField}
getFieldState={getFieldState}
setValue={setValue}
/>
)}
</div>
<LogsTable
refetchInterval={refetchInterval}
filter={{
secretPath: debouncedSecretPath || undefined,
secretKey: debouncedSecretKey || undefined,
eventMetadata: presets?.eventMetadata,
projectId,
actorType: presets?.actorType,
@@ -89,6 +90,7 @@ export const LogsSection = withPermission(
userAgentType,
startDate,
endDate,
environment,
actor
}}
/>

View File

@@ -1,10 +1,12 @@
import { Fragment } from "react";
import { faFile, faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import {
Button,
EmptyState,
Spinner,
Table,
TableContainer,
TableSkeleton,
@@ -52,7 +54,9 @@ export const LogsTable = ({ filter, refetchInterval }: Props) => {
<Table>
<THead>
<Tr>
<Th className="w-24" />
<Th className="w-24">
<Spinner size="xs" className={twMerge(isPending ? "opacity-100" : "opacity-0")} />
</Th>
<Th className="w-64">
Timestamp
<Tooltip
@@ -94,7 +98,7 @@ export const LogsTable = ({ filter, refetchInterval }: Props) => {
<Button
className="mb-20 mt-4 px-4 py-3 text-sm"
isFullWidth
variant="star"
variant="outline_bg"
isLoading={isFetchingNextPage}
isDisabled={isFetchingNextPage || !hasNextPage}
onClick={() => fetchNextPage()}

View File

@@ -1,6 +1,6 @@
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";
export const auditLogFilterFormSchema = z
@@ -10,10 +10,12 @@ export const auditLogFilterFormSchema = 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),
secretPath: z.string().optional(),
secretKey: z.string().optional(),
startDate: z.date().optional(),
endDate: z.date().optional(),
page: z.coerce.number().optional(),
@@ -39,3 +41,12 @@ export type SetValueType = (
shouldDirty?: boolean;
}
) => void;
export type Presets = {
actorId?: string;
eventType?: EventType[];
actorType?: ActorType;
startDate?: Date;
endDate?: Date;
eventMetadata?: Record<string, string>;
};

View File

@@ -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 { withPermission } from "@app/hoc";
import { OrgUser } from "@app/hooks/api/types";
@@ -14,7 +9,6 @@ type Props = {
export const UserAuditLogsSection = withPermission(
({ orgMembership }: Props) => {
const [showFilter, setShowFilter] = useState(false);
const { subscription } = useSubscription();
// 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="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>
<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>
<LogsSection
showFilters={showFilter}
filterClassName="bg-mineshaft-900 static"
presets={{
actorId: orgMembership.user.id
}}

View File

@@ -35,7 +35,6 @@ export const IntegrationAuditLogsSection = ({ integration }: Props) => {
startDate: new Date(new Date().setDate(new Date().getDate() - auditLogsRetentionDays)),
eventType: INTEGRATION_EVENTS
}}
filterClassName="bg-mineshaft-900 static"
/>
</div>
) : (

View File

@@ -41,7 +41,6 @@ export const SecretSyncAuditLogsSection = ({ secretSync }: Props) => {
startDate: new Date(new Date().setDate(new Date().getDate() - auditLogsRetentionDays)),
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">