diff --git a/frontend/src/components/v2/DatePicker/DatePicker.tsx b/frontend/src/components/v2/DatePicker/DatePicker.tsx index 88520f31a..a4af1afd9 100644 --- a/frontend/src/components/v2/DatePicker/DatePicker.tsx +++ b/frontend/src/components/v2/DatePicker/DatePicker.tsx @@ -1,11 +1,13 @@ import { ChangeEventHandler, useState } from "react"; -import { DayPicker, DayPickerProps, getDefaultClassNames, UI } from "react-day-picker"; +import { DayPicker, DayPickerProps, getDefaultClassNames, TZDate, UI } from "react-day-picker"; import { faCalendar } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { PopoverContentProps, PopoverProps } from "@radix-ui/react-popover"; import { format, setHours, setMinutes } from "date-fns"; import { twMerge } from "tailwind-merge"; +import { formatDateTime, Timezone } from "@app/helpers/datetime"; + import { Button } from "../Button"; import { Input } from "../Input"; import { Popover, PopoverContent, PopoverTrigger } from "../Popoverv2"; @@ -18,6 +20,32 @@ export type DatePickerProps = Omit & { popUpProps: PopoverProps; popUpContentProps: PopoverContentProps; dateFormat?: "PPP" | "PP" | "P"; // extend as needed + buttonClassName?: string; + timezone?: Timezone; +}; + +const localTimeToUTC = (timeString: string) => { + const today = new Date(); + const [hours, minutes] = timeString.split(":").map(Number); + + today.setHours(hours, minutes, 0, 0); + + const utcHours = today.getUTCHours().toString().padStart(2, "0"); + const utcMinutes = today.getUTCMinutes().toString().padStart(2, "0"); + + return `${utcHours}:${utcMinutes}`; +}; + +const utcTimeToLocal = (utcTimeString: string) => { + const today = new Date(); + const [hours, minutes] = utcTimeString.split(":").map(Number); + + today.setUTCHours(hours, minutes, 0, 0); + + const localHours = today.getHours().toString().padStart(2, "0"); + const localMinutes = today.getMinutes().toString().padStart(2, "0"); + + return `${localHours}:${localMinutes}`; }; // Doc: https://react-day-picker.js.org/ @@ -27,12 +55,16 @@ export const DatePicker = ({ popUpProps, popUpContentProps, dateFormat = "PPP", + buttonClassName, + timezone, ...props }: DatePickerProps) => { const [timeValue, setTimeValue] = useState(value ? format(value, "HH:mm") : "00:00"); + const displayUtc = timezone === Timezone.UTC; const handleTimeChange: ChangeEventHandler = (e) => { - const time = e.target.value; + const time = displayUtc ? utcTimeToLocal(e.target.value) : e.target.value; + if (time) { setTimeValue(time); if (value) { @@ -57,8 +89,14 @@ export const DatePicker = ({ return ( - handleDaySelect(date ? new TZDate(date, undefined) : undefined)} className="font-inter text-mineshaft-200" + timeZone={displayUtc ? "UTC" : undefined} classNames={{ today: "text-primary border-primary", selected: " text-mineshaft-100 bg-mineshaft-500", @@ -88,7 +127,7 @@ export const DatePicker = ({
diff --git a/frontend/src/components/v2/Select/Select.tsx b/frontend/src/components/v2/Select/Select.tsx index 91c376662..f114c0f02 100644 --- a/frontend/src/components/v2/Select/Select.tsx +++ b/frontend/src/components/v2/Select/Select.tsx @@ -19,6 +19,7 @@ type Props = { icon?: IconProp; isMulti?: boolean; iconClassName?: string; + dropdownContainerStyle?: React.CSSProperties; }; export type SelectProps = Omit & Props; @@ -35,6 +36,7 @@ export const Select = forwardRef( position, containerClassName, iconClassName, + dropdownContainerStyle, ...props }, ref @@ -82,7 +84,7 @@ export const Select = forwardRef( dropdownContainerClassName )} position={position} - style={{ width: "var(--radix-select-trigger-width)" }} + style={dropdownContainerStyle ?? { width: "var(--radix-select-trigger-width)" }} >
diff --git a/frontend/src/helpers/datetime.ts b/frontend/src/helpers/datetime.ts new file mode 100644 index 000000000..6e0fb8e48 --- /dev/null +++ b/frontend/src/helpers/datetime.ts @@ -0,0 +1,24 @@ +import { format } from "date-fns"; + +export enum Timezone { + Local = "local", + UTC = "UTC" +} + +export const formatDateTime = ({ + timezone, + timestamp, + dateFormat = "MMM do yyyy, hh:mm a" +}: { + timestamp: string | Date; + timezone?: Timezone; + dateFormat?: string; +}) => { + const date = new Date(timestamp); + + if (timezone === Timezone.UTC) { + const utcDate = new Date(date.getTime() + date.getTimezoneOffset() * 60000); + return `${format(utcDate, dateFormat)}`; + } + return format(date, dateFormat); +}; diff --git a/frontend/src/hooks/api/auditLogs/queries.tsx b/frontend/src/hooks/api/auditLogs/queries.tsx index 52a35cb85..b254b46d3 100644 --- a/frontend/src/hooks/api/auditLogs/queries.tsx +++ b/frontend/src/hooks/api/auditLogs/queries.tsx @@ -57,6 +57,7 @@ export const useGetAuditLogs = ( }, getNextPageParam: (lastPage, pages) => lastPage.length !== 0 ? pages.length * filters.limit : undefined, + placeholderData: (prev) => prev, ...options }); }; diff --git a/frontend/src/pages/organization/AuditLogsPage/AuditLogsPage.tsx b/frontend/src/pages/organization/AuditLogsPage/AuditLogsPage.tsx index bec3bfd77..25459719f 100644 --- a/frontend/src/pages/organization/AuditLogsPage/AuditLogsPage.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/AuditLogsPage.tsx @@ -18,7 +18,7 @@ export const AuditLogsPage = () => { title="Audit logs" description="Audit logs for security and compliance teams to monitor information access." /> - +
diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogsDateFilter.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogsDateFilter.tsx index 87957ad22..71d418c9d 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/LogsDateFilter.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/LogsDateFilter.tsx @@ -1,9 +1,8 @@ import { useState } from "react"; import { Controller, useForm } from "react-hook-form"; -import { faArrowRight, faCalendar, faChevronRight } from "@fortawesome/free-solid-svg-icons"; +import { faCalendar, faChevronRight } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; -import { format } from "date-fns"; import ms from "ms"; import { twMerge } from "tailwind-merge"; @@ -18,6 +17,7 @@ import { Select, SelectItem } from "@app/components/v2"; +import { formatDateTime, Timezone } from "@app/helpers/datetime"; import { auditLogDateFilterFormSchema, @@ -28,9 +28,19 @@ import { type Props = { setFilter: (data: TAuditLogDateFilterFormData) => void; filter: TAuditLogDateFilterFormData; + setTimezone: (timezone: Timezone) => void; + timezone: Timezone; }; const RELATIVE_VALUES = ["5m", "30m", "1h", "3h", "12h"]; -export const LogsDateFilter = ({ setFilter, filter }: Props) => { + +const RELATIVE_OPTIONS = [ + { label: "Minutes", unit: "m", values: [5, 10, 15, 30, 45] }, + { label: "Hours", unit: "h", values: [1, 2, 3, 6, 8, 12] }, + { label: "Days", unit: "d", values: [1, 2, 3, 4, 5, 6] }, + { label: "Weeks", unit: "w", values: [1, 2, 3, 4] } +]; + +export const LogsDateFilter = ({ setFilter, filter, timezone, setTimezone }: Props) => { const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false); const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false); const [isPopupOpen, setIsPopOpen] = useState(false); @@ -59,154 +69,258 @@ export const LogsDateFilter = ({ setFilter, filter }: Props) => { }; return ( - setIsPopOpen(el)}> -
- {filter.type === AuditLogDateFilterType.Relative ? ( - <> - {RELATIVE_VALUES.map((el) => ( - - ))} - - ) : ( - <> -
- {format(filter.startDate, "yyyy-MM-dd HH:mm")} -
-
- -
-
- {format(filter.endDate, "yyyy-MM-dd HH:mm")} -
- - )} - - - -
- -
- ( - - - - )} - /> - {selectType === AuditLogDateFilterType.Relative && ( + {el} + + ))} + + ) : ( +
+
+ {formatDateTime({ + timezone, + timestamp: filter.startDate, + dateFormat: "yyyy/MM/dd HH:mm" + })} +
+
+ +
+
+ {formatDateTime({ + timezone, + timestamp: filter.endDate, + dateFormat: "yyyy/MM/dd HH:mm" + })} +
+
+ )} + + + + + + ( - - - +
+ + +
)} /> - )} - {selectType === AuditLogDateFilterType.Absolute && ( -
+ {selectType === AuditLogDateFilterType.Relative && ( { + name="relativeModeValue" + render={({ field }) => { + const duration = field.value?.substring(0, field.value.length - 1); + const unitOfTime = field.value?.at(-1); return ( - - - +
+ {RELATIVE_OPTIONS.map(({ label, unit, values }) => ( +
+
{label}
+ {values.map((v) => { + const value = `${v}${unit}`; + return ( + + ); + })} +
+ ))} +
+ + field.onChange(`${val}${unitOfTime}`)} + max={60} + min={1} + /> + + + + +
+
); }} /> -
-
- + )} + {selectType === AuditLogDateFilterType.Absolute && ( +
+ { + return ( + + + + ); + }} + /> + + { + return ( + + + + ); + }} + />
- { - return ( - - - - ); - }} - /> + )} +
+
- )} -
- -
- - - + + + + + ); }; diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogsSection.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogsSection.tsx index 7f676121f..d4d74659f 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/LogsSection.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/LogsSection.tsx @@ -1,8 +1,11 @@ import { useEffect, useState } from "react"; +import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import ms from "ms"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context"; +import { Timezone } from "@app/helpers/datetime"; import { withPermission } from "@app/hoc"; import { usePopUp } from "@app/hooks/usePopUp"; @@ -20,10 +23,11 @@ type Props = { presets?: Presets; refetchInterval?: number; showFilters?: boolean; + pageView?: boolean; }; export const LogsSection = withPermission( - ({ presets, refetchInterval, showFilters = true }: Props) => { + ({ presets, refetchInterval, showFilters = true, pageView = false }: Props) => { const { subscription } = useSubscription(); const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const); @@ -31,6 +35,8 @@ export const LogsSection = withPermission( eventType: presets?.eventType || [], actor: presets?.actorId }); + const [timezone, setTimezone] = useState(Timezone.Local); + const [dateFilter, setDateFilter] = useState({ startDate: new Date(Number(new Date()) - ms("1h")), endDate: new Date(), @@ -43,10 +49,85 @@ export const LogsSection = withPermission( handlePopUpOpen("upgradePlan"); } }, [subscription]); + + if (pageView) + return ( +
+
+
+
+

Audit History

+ +
+ + Docs + +
+
+
+
+
+ {showFilters && ( + + )} + {showFilters && ( + + )} +
+
+
+ + { + handlePopUpToggle("upgradePlan", isOpen); + }} + text="You can use audit logs if you switch to a paid Infisical plan." + /> +
+
+ ); + return (
- {showFilters && } + {showFilters && ( + + )} {showFilters && ( )} @@ -67,6 +148,7 @@ export const LogsSection = withPermission( environment: logFilter?.environment?.slug, actor: logFilter?.actor }} + timezone={timezone} /> { +export const LogsTable = ({ filter, refetchInterval, timezone }: Props) => { // Determine the project ID for filtering const filterProjectId = // Use the projectId from the filter if it exists @@ -57,16 +57,7 @@ export const LogsTable = ({ filter, refetchInterval }: Props) => { - - Timestamp - - - - + Timestamp Event @@ -79,6 +70,7 @@ export const LogsTable = ({ filter, refetchInterval }: Props) => { rowNumber={index + i * AUDIT_LOG_LIMIT + 1} auditLog={auditLog} key={`audit-log-${auditLog.id}`} + timezone={timezone} /> ))} @@ -96,7 +88,7 @@ export const LogsTable = ({ filter, refetchInterval }: Props) => { {!isEmpty && (