feat(dashboard): organization-level audit logs

This commit is contained in:
Daniel Hougaard
2024-09-20 23:07:52 +04:00
parent 5b9903a226
commit b5660c87a0
10 changed files with 156 additions and 26 deletions
@@ -8,6 +8,7 @@ export type TGetAuditLogsFilter = {
userAgentType?: UserAgentType;
eventMetadata?: Record<string, string>;
actorType?: ActorType;
projectId?: string;
actorId?: string; // user ID format
startDate?: Date;
endDate?: Date;
@@ -755,6 +755,16 @@ export const AppLayout = ({ children }: LayoutProps) => {
</a>
</Link>
)}
<Link href={`/org/${currentOrg?.id}/audit-logs`} passHref>
<a>
<MenuItem
isSelected={router.asPath === `/org/${currentOrg?.id}/audit-logs`}
icon="system-outline-168-view-headline"
>
Audit Logs
</MenuItem>
</a>
</Link>
<Link href={`/org/${currentOrg?.id}/settings`} passHref>
<a>
<MenuItem
@@ -0,0 +1,20 @@
import Head from "next/head";
import { AuditLogsPage } from "@app/views/Org/AuditLogsPage";
const Logs = () => {
return (
<div className="h-full bg-bunker-800">
<Head>
<title>Infisical | Audit Logs</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
</Head>
<AuditLogsPage />
</div>
);
};
export default Logs;
Logs.requireAuth = true;
@@ -0,0 +1,20 @@
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from "@app/hoc";
import { LogsSection } from "@app/views/Project/AuditLogsPage/components";
export const AuditLogsPage = withPermission(
() => {
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="bg-bunker-800 py-6">
<p className="text-3xl font-semibold text-gray-200">Audit Logs</p>
<div />
</div>
<LogsSection filterClassName="static p-2" showFilters isOrgAuditLogs />
</div>
</div>
);
},
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Member } // TODO(Daniel): Create a permission for org audit logs
);
@@ -0,0 +1 @@
export { AuditLogsPage } from "./AuditLogsPage";
@@ -40,16 +40,24 @@ type Props = {
eventType?: EventType[];
};
className?: string;
isOrgAuditLogs?: boolean;
control: Control<AuditLogFilterFormData>;
reset: UseFormReset<AuditLogFilterFormData>;
watch: UseFormWatch<AuditLogFilterFormData>;
};
export const LogsFilter = ({ presets, className, control, reset, watch }: Props) => {
export const LogsFilter = ({
presets,
isOrgAuditLogs,
className,
control,
reset,
watch
}: Props) => {
const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false);
const { currentWorkspace } = useWorkspace();
const { currentWorkspace, workspaces } = useWorkspace();
const { data, isLoading } = useGetAuditLogActorFilterOpts(currentWorkspace?.id ?? "");
const renderActorSelectItem = (actor: Actor) => {
@@ -112,7 +120,7 @@ export const LogsFilter = ({ presets, className, control, reset, watch }: Props)
? eventTypes.find((eventType) => eventType.value === selectedEventTypes[0])
?.label
: selectedEventTypes?.length === 0
? "Select event types"
? "All events"
: `${selectedEventTypes?.length} events selected`}
<FontAwesomeIcon icon={faChevronDown} className="ml-2 text-xs" />
</div>
@@ -199,11 +207,20 @@ export const LogsFilter = ({ presets, className, control, reset, watch }: Props)
className="w-40"
>
<Select
{...(field.value ? { value: field.value } : { placeholder: "Select" })}
{...(field.value ? { value: field.value } : { placeholder: "All sources" })}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full border border-mineshaft-500 bg-mineshaft-700 text-mineshaft-100"
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}
@@ -213,6 +230,43 @@ export const LogsFilter = ({ presets, className, control, reset, watch }: Props)
</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}
@@ -272,7 +326,8 @@ export const LogsFilter = ({ presets, className, control, reset, watch }: Props)
actor: presets?.actorId,
userAgentType: undefined,
startDate: undefined,
endDate: undefined
endDate: undefined,
projectId: undefined
})
}
>
@@ -65,6 +65,7 @@ export const LogsSection = ({
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");
@@ -73,6 +74,7 @@ export const LogsSection = ({
<div>
{showFilters && (
<LogsFilter
isOrgAuditLogs
className={filterClassName}
presets={presets}
control={control}
@@ -87,6 +89,7 @@ export const LogsSection = ({
showActorColumn={!!showActorColumn && !isOrgAuditLogs}
filter={{
eventMetadata: presets?.eventMetadata,
projectId,
actorType: presets?.actorType,
limit: 15,
eventType,
@@ -41,12 +41,15 @@ export const LogsTable = ({
}: Props) => {
const { currentWorkspace } = useWorkspace();
const filterProjectId =
filter?.projectId ?? (!isOrgAuditLogs ? currentWorkspace?.id ?? "" : null);
const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } = useGetAuditLogs(
{
...filter,
limit: AUDIT_LOG_LIMIT
},
!isOrgAuditLogs ? currentWorkspace?.id ?? "" : null,
filterProjectId,
{
refetchInterval
}
@@ -5,6 +5,7 @@ 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"),