mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-16 19:28:35 +00:00
Moved audit logs to org-level entirely
This commit is contained in:
@@ -1,21 +0,0 @@
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
|
||||
import { LogsSection } from "./components";
|
||||
|
||||
export const AuditLogsPage = withProjectPermission(
|
||||
() => {
|
||||
return (
|
||||
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
||||
<div className="w-full max-w-7xl px-6">
|
||||
<div className="sticky top-0 z-10 bg-bunker-800 py-6">
|
||||
<p className="text-3xl font-semibold text-gray-200">Audit Logs</p>
|
||||
<div />
|
||||
</div>
|
||||
<LogsSection showFilters />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.AuditLogs }
|
||||
);
|
||||
@@ -1,338 +0,0 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { useState } from "react";
|
||||
import { Control, Controller, UseFormReset, UseFormWatch } from "react-hook-form";
|
||||
import {
|
||||
faCheckCircle,
|
||||
faChevronDown,
|
||||
faFilterCircleXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import {
|
||||
Button,
|
||||
DatePicker,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
FormControl,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useGetAuditLogActorFilterOpts } 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 { AuditLogFilterFormData } from "./types";
|
||||
|
||||
const eventTypes = Object.entries(eventToNameMap).map(([value, label]) => ({ label, value }));
|
||||
const userAgentTypes = Object.entries(userAgentTTypeoNameMap).map(([value, label]) => ({
|
||||
label,
|
||||
value
|
||||
}));
|
||||
|
||||
type Props = {
|
||||
presets?: {
|
||||
actorId?: string;
|
||||
eventType?: EventType[];
|
||||
};
|
||||
className?: string;
|
||||
isOrgAuditLogs?: boolean;
|
||||
control: Control<AuditLogFilterFormData>;
|
||||
reset: UseFormReset<AuditLogFilterFormData>;
|
||||
watch: UseFormWatch<AuditLogFilterFormData>;
|
||||
};
|
||||
|
||||
export const LogsFilter = ({
|
||||
presets,
|
||||
isOrgAuditLogs,
|
||||
className,
|
||||
control,
|
||||
reset,
|
||||
watch
|
||||
}: Props) => {
|
||||
const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
|
||||
const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false);
|
||||
|
||||
const { currentWorkspace, workspaces } = useWorkspace();
|
||||
const { data, isLoading } = useGetAuditLogActorFilterOpts(currentWorkspace?.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>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<SelectItem value="actor-none" key="actor-none">
|
||||
N/A
|
||||
</SelectItem>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const selectedEventTypes = watch("eventType") as EventType[] | undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"sticky top-20 z-10 flex items-center justify-between bg-bunker-800",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<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 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={faChevronDown} 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>
|
||||
)}
|
||||
/>
|
||||
|
||||
{!isLoading && 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, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Source"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="w-40"
|
||||
>
|
||||
<Select
|
||||
{...(field.value ? { value: field.value } : { placeholder: "All sources" })}
|
||||
{...field}
|
||||
onValueChange={(e) => {
|
||||
if (e === "all") onChange(undefined);
|
||||
else onChange(e);
|
||||
}}
|
||||
className={twMerge(
|
||||
"w-full border border-mineshaft-500 bg-mineshaft-700 text-mineshaft-100",
|
||||
(field.value === "all" || field.value === undefined) && "text-mineshaft-400"
|
||||
)}
|
||||
>
|
||||
<SelectItem value="all" key="all">
|
||||
All sources
|
||||
</SelectItem>
|
||||
{userAgentTypes.map(({ label, value }) => (
|
||||
<SelectItem value={String(value || "")} key={label}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
{isOrgAuditLogs && workspaces.length && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="projectId"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Project"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="w-40"
|
||||
>
|
||||
<Select
|
||||
{...(field.value ? { value: field.value } : { placeholder: "All projects" })}
|
||||
{...field}
|
||||
onValueChange={(e) => {
|
||||
if (e === "all") onChange(undefined);
|
||||
else onChange(e);
|
||||
}}
|
||||
className={twMerge(
|
||||
"w-full border border-mineshaft-500 bg-mineshaft-700 text-mineshaft-100",
|
||||
(field.value === "all" || field.value === undefined) && "text-mineshaft-400"
|
||||
)}
|
||||
>
|
||||
<SelectItem value="all" key="all">
|
||||
All projects
|
||||
</SelectItem>
|
||||
{workspaces.map((project) => (
|
||||
<SelectItem value={String(project.id || "")} key={project.id}>
|
||||
{project.name}
|
||||
</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={(date) => {
|
||||
onChange(date);
|
||||
setIsStartDatePickerOpen(false);
|
||||
}}
|
||||
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={(pickedDate) => {
|
||||
pickedDate?.setHours(23, 59, 59, 999); // we choose the end of today not the start of it (going off of aws cloud watch)
|
||||
onChange(pickedDate);
|
||||
setIsEndDatePickerOpen(false);
|
||||
}}
|
||||
popUpProps={{
|
||||
open: isEndDatePickerOpen,
|
||||
onOpenChange: setIsEndDatePickerOpen
|
||||
}}
|
||||
popUpContentProps={{}}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
isLoading={false}
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
className="mt-1.5"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faFilterCircleXmark} />}
|
||||
onClick={() =>
|
||||
reset({
|
||||
eventType: presets?.eventType || [],
|
||||
actor: presets?.actorId,
|
||||
userAgentType: undefined,
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
projectId: undefined
|
||||
})
|
||||
}
|
||||
>
|
||||
Clear filters
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,116 +0,0 @@
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useRouter } from "next/router";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/v2";
|
||||
import { useSubscription } from "@app/context";
|
||||
import { ActorType, EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { LogsFilter } from "./LogsFilter";
|
||||
import { LogsTable, TAuditLogTableHeader } from "./LogsTable";
|
||||
import { AuditLogFilterFormData, auditLogFilterFormSchema } from "./types";
|
||||
|
||||
type Props = {
|
||||
presets?: {
|
||||
actorId?: string;
|
||||
eventType?: EventType[];
|
||||
actorType?: ActorType;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
eventMetadata?: Record<string, string>;
|
||||
};
|
||||
|
||||
showFilters?: boolean;
|
||||
filterClassName?: string;
|
||||
isOrgAuditLogs?: boolean;
|
||||
showActorColumn?: boolean;
|
||||
remappedHeaders?: Partial<Record<TAuditLogTableHeader, string>>;
|
||||
refetchInterval?: number;
|
||||
};
|
||||
|
||||
export const LogsSection = ({
|
||||
presets,
|
||||
filterClassName,
|
||||
remappedHeaders,
|
||||
isOrgAuditLogs,
|
||||
showActorColumn,
|
||||
refetchInterval,
|
||||
showFilters
|
||||
}: Props) => {
|
||||
const { subscription } = useSubscription();
|
||||
const router = useRouter();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
|
||||
|
||||
const { control, reset, watch } = useForm<AuditLogFilterFormData>({
|
||||
resolver: yupResolver(auditLogFilterFormSchema),
|
||||
defaultValues: {
|
||||
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
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (subscription && !subscription.auditLogs) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}
|
||||
}, [subscription]);
|
||||
|
||||
const eventType = watch("eventType") as EventType[] | undefined;
|
||||
const userAgentType = watch("userAgentType") as UserAgentType | undefined;
|
||||
const actor = watch("actor");
|
||||
const projectId = watch("projectId");
|
||||
|
||||
const startDate = watch("startDate");
|
||||
const endDate = watch("endDate");
|
||||
|
||||
return (
|
||||
<div>
|
||||
{showFilters && (
|
||||
<LogsFilter
|
||||
isOrgAuditLogs
|
||||
className={filterClassName}
|
||||
presets={presets}
|
||||
control={control}
|
||||
watch={watch}
|
||||
reset={reset}
|
||||
/>
|
||||
)}
|
||||
<LogsTable
|
||||
refetchInterval={refetchInterval}
|
||||
remappedHeaders={remappedHeaders}
|
||||
isOrgAuditLogs={isOrgAuditLogs}
|
||||
showActorColumn={!!showActorColumn && !isOrgAuditLogs}
|
||||
filter={{
|
||||
eventMetadata: presets?.eventMetadata,
|
||||
projectId,
|
||||
actorType: presets?.actorType,
|
||||
limit: 15,
|
||||
eventType,
|
||||
userAgentType,
|
||||
startDate,
|
||||
endDate,
|
||||
actorId: actor
|
||||
}}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!isOpen) {
|
||||
router.back();
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpToggle("upgradePlan", isOpen);
|
||||
}}
|
||||
text="You can use audit logs if you switch to a paid Infisical plan."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,119 +0,0 @@
|
||||
import { Fragment } from "react";
|
||||
import { faFile } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import {
|
||||
Button,
|
||||
EmptyState,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useGetAuditLogs } from "@app/hooks/api";
|
||||
import { TGetAuditLogsFilter } from "@app/hooks/api/auditLogs/types";
|
||||
|
||||
import { LogsTableRow } from "./LogsTableRow";
|
||||
|
||||
type Props = {
|
||||
isOrgAuditLogs?: boolean;
|
||||
showActorColumn: boolean;
|
||||
filter?: TGetAuditLogsFilter;
|
||||
remappedHeaders?: Partial<Record<TAuditLogTableHeader, string>>;
|
||||
refetchInterval?: number;
|
||||
};
|
||||
|
||||
const AUDIT_LOG_LIMIT = 15;
|
||||
|
||||
const TABLE_HEADERS = ["Timestamp", "Event", "Project", "Actor", "Source", "Metadata"] as const;
|
||||
export type TAuditLogTableHeader = (typeof TABLE_HEADERS)[number];
|
||||
|
||||
export const LogsTable = ({
|
||||
showActorColumn,
|
||||
isOrgAuditLogs,
|
||||
filter,
|
||||
remappedHeaders,
|
||||
refetchInterval
|
||||
}: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const filterProjectId =
|
||||
filter?.projectId ?? (!isOrgAuditLogs ? currentWorkspace?.id ?? "" : null);
|
||||
|
||||
const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } = useGetAuditLogs(
|
||||
{
|
||||
...filter,
|
||||
limit: AUDIT_LOG_LIMIT
|
||||
},
|
||||
filterProjectId,
|
||||
{
|
||||
refetchInterval
|
||||
}
|
||||
);
|
||||
|
||||
const isEmpty = !isLoading && !data?.pages?.[0].length;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
{TABLE_HEADERS.map((header, idx) => {
|
||||
if (
|
||||
(header === "Project" && !isOrgAuditLogs) ||
|
||||
(header === "Actor" && !showActorColumn)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Th key={`table-header-${idx + 1}`}>{remappedHeaders?.[header] || header}</Th>
|
||||
);
|
||||
})}
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{!isLoading &&
|
||||
data?.pages?.map((group, i) => (
|
||||
<Fragment key={`audit-log-fragment-${i + 1}`}>
|
||||
{group.map((auditLog) => (
|
||||
<LogsTableRow
|
||||
showActorColumn={showActorColumn}
|
||||
isOrgAuditLogs={isOrgAuditLogs}
|
||||
auditLog={auditLog}
|
||||
key={`audit-log-${auditLog.id}`}
|
||||
/>
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
{isLoading && <TableSkeleton innerKey="logs-table" columns={5} key="logs" />}
|
||||
{isEmpty && (
|
||||
<Tr>
|
||||
<Td colSpan={5}>
|
||||
<EmptyState title="No audit logs on file" icon={faFile} />
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{!isEmpty && (
|
||||
<Button
|
||||
className="mt-4 mb-20 py-3 px-4 text-sm"
|
||||
isFullWidth
|
||||
variant="star"
|
||||
isLoading={isFetchingNextPage}
|
||||
isDisabled={isFetchingNextPage || !hasNextPage}
|
||||
onClick={() => fetchNextPage()}
|
||||
>
|
||||
{hasNextPage ? "Load More" : "End of logs"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,540 +0,0 @@
|
||||
import { Badge, Td, Tooltip, Tr } from "@app/components/v2";
|
||||
import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants";
|
||||
import { ActorType, EventType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { Actor, AuditLog, Event } from "@app/hooks/api/auditLogs/types";
|
||||
|
||||
type Props = {
|
||||
auditLog: AuditLog;
|
||||
isOrgAuditLogs?: boolean;
|
||||
showActorColumn: boolean;
|
||||
};
|
||||
|
||||
export const LogsTableRow = ({ auditLog, isOrgAuditLogs, showActorColumn }: Props) => {
|
||||
const renderActor = (actor: Actor) => {
|
||||
switch (actor.type) {
|
||||
case ActorType.USER:
|
||||
return (
|
||||
<Td>
|
||||
<p>{actor.metadata.email}</p>
|
||||
<p>User</p>
|
||||
</Td>
|
||||
);
|
||||
case ActorType.SERVICE:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`${actor.metadata.name}`}</p>
|
||||
<p>Service token</p>
|
||||
</Td>
|
||||
);
|
||||
case ActorType.IDENTITY:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`${actor.metadata.name}`}</p>
|
||||
<p>Machine Identity</p>
|
||||
</Td>
|
||||
);
|
||||
default:
|
||||
return <Td />;
|
||||
}
|
||||
};
|
||||
|
||||
const renderMetadata = (event: Event) => {
|
||||
switch (event.type) {
|
||||
case EventType.GET_SECRETS:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Path: ${event.metadata.secretPath}`}</p>
|
||||
<p>{`# Secrets: ${event.metadata.numberOfSecrets}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.GET_SECRET:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Path: ${event.metadata.secretPath}`}</p>
|
||||
<p>{`Secret: ${event.metadata.secretKey}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_SECRET:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Path: ${event.metadata.secretPath}`}</p>
|
||||
<p>{`Secret: ${event.metadata.secretKey}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UPDATE_SECRET:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Path: ${event.metadata.secretPath}`}</p>
|
||||
<p>{`Secret: ${event.metadata.secretKey}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.DELETE_SECRET:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Path: ${event.metadata.secretPath}`}</p>
|
||||
<p>{`Secret: ${event.metadata.secretKey}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.AUTHORIZE_INTEGRATION:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Integration: ${event.metadata.integration}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UNAUTHORIZE_INTEGRATION:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Integration: ${event.metadata.integration}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_INTEGRATION:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Integration: ${event.metadata.integration}`}</p>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Path: ${event.metadata.secretPath}`}</p>
|
||||
{event.metadata.app && <p>{`Target app: ${event.metadata.app}`}</p>}
|
||||
{event.metadata.appId && <p>{`Target app: ${event.metadata.appId}`}</p>}
|
||||
{event.metadata.targetEnvironment && (
|
||||
<p>{`Target environment: ${event.metadata.targetEnvironment}`}</p>
|
||||
)}
|
||||
{event.metadata.targetEnvironmentId && (
|
||||
<p>{`Target environment ID: ${event.metadata.targetEnvironmentId}`}</p>
|
||||
)}
|
||||
</Td>
|
||||
);
|
||||
case EventType.DELETE_INTEGRATION:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Integration: ${event.metadata.integration}`}</p>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Path: ${event.metadata.secretPath}`}</p>
|
||||
{event.metadata.app && <p>{`Target App: ${event.metadata.app}`}</p>}
|
||||
{event.metadata.appId && <p>{`Target app: ${event.metadata.appId}`}</p>}
|
||||
{event.metadata.targetEnvironment && (
|
||||
<p>{`Target environment: ${event.metadata.targetEnvironment}`}</p>
|
||||
)}
|
||||
{event.metadata.targetEnvironmentId && (
|
||||
<p>{`Target environment ID: ${event.metadata.targetEnvironmentId}`}</p>
|
||||
)}
|
||||
</Td>
|
||||
);
|
||||
case EventType.ADD_TRUSTED_IP:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`IP: ${event.metadata.ipAddress}${
|
||||
event.metadata.prefix !== undefined ? `/${event.metadata.prefix}` : ""
|
||||
}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UPDATE_TRUSTED_IP:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`IP: ${event.metadata.ipAddress}${
|
||||
event.metadata.prefix !== undefined ? `/${event.metadata.prefix}` : ""
|
||||
}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.DELETE_TRUSTED_IP:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`IP: ${event.metadata.ipAddress}${
|
||||
event.metadata.prefix !== undefined ? `/${event.metadata.prefix}` : ""
|
||||
}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_SERVICE_TOKEN:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Name: ${event.metadata.name}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.DELETE_SERVICE_TOKEN:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Name: ${event.metadata.name}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_IDENTITY:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`ID: ${event.metadata.identityId}`}</p>
|
||||
<p>{`Name: ${event.metadata.name}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UPDATE_IDENTITY:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`ID: ${event.metadata.identityId}`}</p>
|
||||
<p>{`Name: ${event.metadata.name}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.DELETE_IDENTITY:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`ID: ${event.metadata.identityId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_ENVIRONMENT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Name: ${event.metadata.name}`}</p>
|
||||
<p>{`Slug: ${event.metadata.slug}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UPDATE_ENVIRONMENT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Old name: ${event.metadata.oldName}`}</p>
|
||||
<p>{`New name: ${event.metadata.newName}`}</p>
|
||||
<p>{`Old slug: ${event.metadata.oldSlug}`}</p>
|
||||
<p>{`New slug: ${event.metadata.newSlug}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.DELETE_ENVIRONMENT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Name: ${event.metadata.name}`}</p>
|
||||
<p>{`Slug: ${event.metadata.slug}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.ADD_WORKSPACE_MEMBER:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Email: ${event.metadata.email}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.REMOVE_WORKSPACE_MEMBER:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Email: ${event.metadata.email}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_FOLDER:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Path: ${event.metadata.folderPath}`}</p>
|
||||
<p>{`Folder: ${event.metadata.folderName}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UPDATE_FOLDER:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Path: ${event.metadata.folderPath}`}</p>
|
||||
<p>{`Old folder: ${event.metadata.oldFolderName}`}</p>
|
||||
<p>{`New folder: ${event.metadata.newFolderName}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.DELETE_FOLDER:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Path: ${event.metadata.folderPath}`}</p>
|
||||
<p>{`Folder: ${event.metadata.folderName}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_WEBHOOK:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Secret path: ${event.metadata.secretPath}`}</p>
|
||||
<p>{`Disabled: ${event.metadata.isDisabled}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UPDATE_WEBHOOK_STATUS:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Secret path: ${event.metadata.secretPath}`}</p>
|
||||
<p>{`Disabled: ${event.metadata.isDisabled}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.DELETE_WEBHOOK:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`Secret path: ${event.metadata.secretPath}`}</p>
|
||||
<p>{`Disabled: ${event.metadata.isDisabled}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.GET_SECRET_IMPORTS:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Environment: ${event.metadata.environment}`}</p>
|
||||
<p>{`# Imported paths: ${event.metadata.numberOfImports}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_SECRET_IMPORT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Import from env: ${event.metadata.importFromEnvironment}`}</p>
|
||||
<p>{`Import from path: ${event.metadata.importFromSecretPath}`}</p>
|
||||
<p>{`Import to env: ${event.metadata.importToEnvironment}`}</p>
|
||||
<p>{`Import to path: ${event.metadata.importToSecretPath}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UPDATE_SECRET_IMPORT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Import to env: ${event.metadata.importToEnvironment}`}</p>
|
||||
<p>{`Import to path: ${event.metadata.importToSecretPath}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.DELETE_SECRET_IMPORT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Import from env: ${event.metadata.importFromEnvironment}`}</p>
|
||||
<p>{`Import from path: ${event.metadata.importFromSecretPath}`}</p>
|
||||
<p>{`Import to env: ${event.metadata.importToEnvironment}`}</p>
|
||||
<p>{`Import to path: ${event.metadata.importToSecretPath}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UPDATE_USER_WORKSPACE_ROLE:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Email: ${event.metadata.email}`}</p>
|
||||
<p>{`Old role: ${event.metadata.oldRole}`}</p>
|
||||
<p>{`New role: ${event.metadata.newRole}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Email: ${event.metadata.email}`}</p>
|
||||
{event.metadata.deniedPermissions.map((permission) => {
|
||||
return (
|
||||
<p
|
||||
key={`audit-log-denied-permission-${event.metadata.userId}-${permission.environmentSlug}-${permission.ability}`}
|
||||
>
|
||||
{`Denied env-ability: ${permission.environmentSlug}-${permission.ability}`}
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
</Td>
|
||||
);
|
||||
case EventType.ORG_ADMIN_ACCESS_PROJECT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Email: ${event.metadata.email}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_PKI_ALERT:
|
||||
case EventType.UPDATE_PKI_ALERT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Alert ID: ${event.metadata.pkiAlertId}`}</p>
|
||||
<p>{`Name: ${event.metadata.name}`}</p>
|
||||
<p>{`Alert Before Days: ${event.metadata.alertBeforeDays}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.GET_PKI_ALERT:
|
||||
case EventType.DELETE_PKI_ALERT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Alert ID: ${event.metadata.pkiAlertId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_PKI_COLLECTION:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Collection ID: ${event.metadata.pkiCollectionId}`}</p>
|
||||
<p>{`Name: ${event.metadata.name}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UPDATE_PKI_COLLECTION:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Collection ID: ${event.metadata.pkiCollectionId}`}</p>
|
||||
<p>{`Name: ${event.metadata.name}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.GET_PKI_COLLECTION:
|
||||
case EventType.DELETE_PKI_COLLECTION:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Collection ID: ${event.metadata.pkiCollectionId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.GET_PKI_COLLECTION_ITEMS:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Collection ID: ${event.metadata.pkiCollectionId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.ADD_PKI_COLLECTION_ITEM:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Collection ID: ${event.metadata.pkiCollectionId}`}</p>
|
||||
<p>{`Collection Item ID: ${event.metadata.pkiCollectionItemId}`}</p>
|
||||
<p>{`Type: ${event.metadata.type}`}</p>
|
||||
<p>{`Item ID: ${event.metadata.itemId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.DELETE_PKI_COLLECTION_ITEM:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Collection ID: ${event.metadata.pkiCollectionId}`}</p>
|
||||
<p>{`Collection Item ID: ${event.metadata.pkiCollectionItemId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_CA:
|
||||
case EventType.GET_CA:
|
||||
case EventType.UPDATE_CA:
|
||||
case EventType.DELETE_CA:
|
||||
case EventType.GET_CA_CSR:
|
||||
case EventType.GET_CA_CERT:
|
||||
case EventType.IMPORT_CA_CERT:
|
||||
case EventType.GET_CA_CRL:
|
||||
case EventType.SIGN_INTERMEDIATE:
|
||||
case EventType.ISSUE_CERT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`CA DN: ${event.metadata.dn}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.GET_CERT:
|
||||
case EventType.DELETE_CERT:
|
||||
case EventType.REVOKE_CERT:
|
||||
case EventType.GET_CERT_BODY:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Cert CN: ${event.metadata.cn}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_CERTIFICATE_TEMPLATE:
|
||||
case EventType.UPDATE_CERTIFICATE_TEMPLATE:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Certificate Template ID: ${event.metadata.certificateTemplateId}`}</p>
|
||||
<p>{`Certificate Authority ID: ${event.metadata.caId}`}</p>
|
||||
<p>{`Name: ${event.metadata.name}`}</p>
|
||||
<p>{`Common Name: ${event.metadata.commonName}`}</p>
|
||||
<p>{`Subject Alternative Name: ${event.metadata.subjectAlternativeName}`}</p>
|
||||
<p>{`TTL: ${event.metadata.ttl}`}</p>
|
||||
{event.metadata.pkiCollectionId && (
|
||||
<p>{`Collection ID: ${event.metadata.pkiCollectionId}`}</p>
|
||||
)}
|
||||
</Td>
|
||||
);
|
||||
case EventType.GET_CERTIFICATE_TEMPLATE:
|
||||
case EventType.DELETE_CERTIFICATE_TEMPLATE:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Certificate Template ID: ${event.metadata.certificateTemplateId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG:
|
||||
case EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Certificate Template ID: ${event.metadata.certificateTemplateId}`}</p>
|
||||
<p>{`Enabled: ${event.metadata.isEnabled}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.GET_CERTIFICATE_TEMPLATE_EST_CONFIG:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Certificate Template ID: ${event.metadata.certificateTemplateId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.GET_PROJECT_SLACK_CONFIG:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Project Slack Config ID: ${event.metadata.id}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.UPDATE_PROJECT_SLACK_CONFIG:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Project Slack Config ID: ${event.metadata.id}`}</p>
|
||||
<p>{`Slack integration ID: ${event.metadata.slackIntegrationId}`}</p>
|
||||
<p>{`Access Request Notification Status: ${event.metadata.isAccessRequestNotificationEnabled}`}</p>
|
||||
<p>{`Access Request Channels: ${event.metadata.accessRequestChannels}`}</p>
|
||||
<p>{`Secret Approval Request Notification Status: ${event.metadata.isSecretRequestNotificationEnabled}`}</p>
|
||||
<p>{`Secret Request Channels: ${event.metadata.secretRequestChannels}`}</p>
|
||||
</Td>
|
||||
);
|
||||
|
||||
case EventType.INTEGRATION_SYNCED:
|
||||
return (
|
||||
<Td>
|
||||
<Tooltip
|
||||
className="max-w-xs whitespace-normal break-words"
|
||||
content={event.metadata.syncMessage!}
|
||||
isDisabled={!event.metadata.syncMessage}
|
||||
>
|
||||
<Badge variant={event.metadata.isSynced ? "success" : "danger"}>
|
||||
<p className="text-center">{event.metadata.isSynced ? "Successful" : "Failed"}</p>
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
);
|
||||
default:
|
||||
return <Td />;
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateToFormat: string) => {
|
||||
const date = new Date(dateToFormat);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
|
||||
let hours = date.getHours();
|
||||
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||||
|
||||
// convert from 24h to 12h format
|
||||
const period = hours >= 12 ? "PM" : "AM";
|
||||
hours %= 12;
|
||||
hours = hours || 12; // the hour '0' should be '12'
|
||||
|
||||
const formattedDate = `${day}-${month}-${year} at ${hours}:${minutes} ${period}`;
|
||||
return formattedDate;
|
||||
};
|
||||
|
||||
const renderSource = () => {
|
||||
const { event, actor } = auditLog;
|
||||
|
||||
if (event.type === EventType.INTEGRATION_SYNCED) {
|
||||
if (actor.type === ActorType.USER) {
|
||||
return (
|
||||
<Td>
|
||||
<p>Manually triggered by {actor.metadata.email}</p>
|
||||
</Td>
|
||||
);
|
||||
}
|
||||
|
||||
// Platform / automatic syncs
|
||||
return (
|
||||
<Td>
|
||||
<p>Automatically synced by Infisical</p>
|
||||
</Td>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Td>
|
||||
<p>{userAgentTTypeoNameMap[auditLog.userAgentType]}</p>
|
||||
<p>{auditLog.ipAddress}</p>
|
||||
</Td>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Tr className={`log-${auditLog.id} h-10 border-x-0 border-b border-t-0`}>
|
||||
<Td>{formatDate(auditLog.createdAt)}</Td>
|
||||
<Td>{`${eventToNameMap[auditLog.event.type]}`}</Td>
|
||||
{isOrgAuditLogs && <Td>{auditLog.project.name}</Td>}
|
||||
{showActorColumn && renderActor(auditLog.actor)}
|
||||
{renderSource()}
|
||||
{renderMetadata(auditLog.event)}
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { LogsSection } from "./LogsSection";
|
||||
@@ -1,28 +0,0 @@
|
||||
import * as yup from "yup";
|
||||
|
||||
import { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
|
||||
|
||||
export const auditLogFilterFormSchema = yup
|
||||
.object({
|
||||
eventMetadata: yup.object({}).optional(),
|
||||
projectId: yup.string().optional(),
|
||||
eventType: yup.array(yup.string().oneOf(Object.values(EventType), "Invalid event type")),
|
||||
actor: yup.string(),
|
||||
userAgentType: yup.string().oneOf(Object.values(UserAgentType), "Invalid user agent type"),
|
||||
startDate: yup.date(),
|
||||
endDate: yup.date().min(yup.ref("startDate"), "End date cannot be before start date"),
|
||||
page: yup.number(),
|
||||
perPage: yup.number()
|
||||
})
|
||||
.required();
|
||||
|
||||
export type AuditLogFilterFormData = yup.InferType<typeof auditLogFilterFormSchema>;
|
||||
|
||||
export type SetValueType = (
|
||||
name: keyof AuditLogFilterFormData,
|
||||
value: any,
|
||||
options?: {
|
||||
shouldValidate?: boolean;
|
||||
shouldDirty?: boolean;
|
||||
}
|
||||
) => void;
|
||||
@@ -1 +0,0 @@
|
||||
export { AuditLogsPage } from "./AuditLogsPage";
|
||||
Reference in New Issue
Block a user