Fix single date selection in organization audit logs (#34689)

* Fix single date selection in organization audit logs

* Update apps/studio/components/interfaces/Account/AuditLogs.tsx

Co-authored-by: Alaister Young <alaister@users.noreply.github.com>

---------

Co-authored-by: Alaister Young <alaister@users.noreply.github.com>
This commit is contained in:
Joshen LimandAlaister Young authored and GitHub committed 2025-04-03 16:14:11 +08:00
1 parent 06091157d9
commit 74723eccb6
5 files changed
+74 -80

No files matched your search

@@ -15,6 +15,7 @@ import { useProfileAuditLogsQuery } from 'data/profile/profile-audit-logs-query'
import { useProjectsQuery } from 'data/projects/projects-query'
import { Alert, Button } from 'ui'
import { TimestampInfo } from 'ui-patterns'
import { formatSelectedDateRange } from '../Organization/AuditLogs/AuditLogs.utils'
const AuditLogs = () => {
const currentTime = dayjs().utc().set('millisecond', 0)
@@ -70,6 +71,9 @@ const AuditLogs = () => {
}
})
const minDate = dayjs().subtract(retentionPeriod, 'days')
const maxDate = dayjs()
return (
<>
<div className="space-y-4 flex flex-col pb-8">
@@ -91,47 +95,12 @@ const AuditLogs = () => {
triggerButtonTitle=""
from={dateRange.from}
to={dateRange.to}
minDate={dayjs().subtract(retentionPeriod, 'days').toDate()}
maxDate={dayjs().toDate()}
minDate={minDate.toDate()}
maxDate={maxDate.toDate()}
onChange={(value) => {
if (value.from !== null && value.to !== null) {
const current = dayjs()
const from = dayjs(value.from)
.hour(current.hour())
.minute(current.minute())
.second(current.second())
const to = dayjs(value.to)
.hour(current.hour())
.minute(current.minute())
.second(current.second())
if (from.date() === to.date()) {
// [Joshen] If a single date is selected, we either set the "from" to start from 00:00
// or "to" to end at 23:59 depending on which date was selected
if (from.date() === current.date()) {
setDateRange({
from: from
.set('hour', 0)
.set('minute', 0)
.set('second', 0)
.utc()
.toISOString(),
to: to.utc().toISOString(),
})
} else {
setDateRange({
from: from.utc().toISOString(),
to: to
.set('hour', 23)
.set('minute', 59)
.set('second', 59)
.utc()
.toISOString(),
})
}
} else {
setDateRange({ from: from.utc().toISOString(), to: to.utc().toISOString() })
}
const { from, to } = formatSelectedDateRange(value)
setDateRange({ from, to })
}
}}
renderFooter={() => {
@@ -32,6 +32,7 @@ import {
WarningIcon,
} from 'ui'
import { Admonition } from 'ui-patterns'
import { formatSelectedDateRange } from './AuditLogs.utils'
// [Joshen considerations]
// - Maybe fix the height of the table to the remaining height of the viewport, so that the search input is always visible
@@ -79,23 +80,6 @@ const AuditLogs = () => {
}
)
// This feature depends on the subscription tier of the user. Free user can view logs up to 1 day
// in the past. The API limits the logs to maximum of 1 day and 5 minutes so when the page is
// viewed for more than 5 minutes, the call parameters needs to be updated. This also works with
// higher tiers (7 days of logs).The user will see a loading shimmer.
useEffect(() => {
const duration = dayjs(dateRange.from).diff(dayjs(dateRange.to))
const interval = setInterval(() => {
const currentTime = dayjs().utc().set('millisecond', 0)
setDateRange({
from: currentTime.add(duration).toISOString(),
to: currentTime.toISOString(),
})
}, 5 * 60000)
return () => clearInterval(interval)
}, [dateRange.from, dateRange.to])
const activeMembers = (members ?? []).filter((x) => !x.invited_at)
const roles = [...(rolesData?.org_scoped_roles ?? []), ...(rolesData?.project_scoped_roles ?? [])]
@@ -123,6 +107,25 @@ const AuditLogs = () => {
})
const currentOrganization = organizations?.find((o) => o.slug === slug)
const minDate = dayjs().subtract(retentionPeriod, 'days')
const maxDate = dayjs()
// This feature depends on the subscription tier of the user. Free user can view logs up to 1 day
// in the past. The API limits the logs to maximum of 1 day and 5 minutes so when the page is
// viewed for more than 5 minutes, the call parameters needs to be updated. This also works with
// higher tiers (7 days of logs).The user will see a loading shimmer.
useEffect(() => {
const duration = dayjs(dateRange.from).diff(dayjs(dateRange.to))
const interval = setInterval(() => {
const currentTime = dayjs().utc().set('millisecond', 0)
setDateRange({
from: currentTime.add(duration).toISOString(),
to: currentTime.toISOString(),
})
}, 5 * 60000)
return () => clearInterval(interval)
}, [dateRange.from, dateRange.to])
if (!canReadAuditLogs) {
return (
@@ -208,23 +211,11 @@ const AuditLogs = () => {
triggerButtonTitle=""
from={dateRange.from}
to={dateRange.to}
minDate={dayjs().subtract(retentionPeriod, 'days').toDate()}
maxDate={dayjs().toDate()}
minDate={minDate.toDate()}
maxDate={maxDate.toDate()}
onChange={(value) => {
if (value.from !== null && value.to !== null) {
const current = dayjs().utc()
const from = dayjs(value.from)
.utc()
.hour(current.hour())
.minute(current.minute())
.second(current.second())
.toISOString()
const to = dayjs(value.to)
.utc()
.hour(current.hour())
.minute(current.minute())
.second(current.second())
.toISOString()
const { from, to } = formatSelectedDateRange(value)
setDateRange({ from, to })
}
}}
@@ -0,0 +1,34 @@
import { DatePickerToFrom } from 'components/interfaces/Settings/Logs/Logs.types'
import dayjs from 'dayjs'
// [Joshen] Mainly to handle if a single date is selected - currently just for Audit Logs as
// i'm on the fence if this logic should be within the DatePicker component itself
// e.g for Logs.DatePicker which uses this component, the component itself has its own time selection UI
// JFYI currentDate is just a parameter so that I can run tests for this
export const formatSelectedDateRange = (value: DatePickerToFrom) => {
const current = dayjs()
const from = dayjs(value.from)
.hour(current.hour())
.minute(current.minute())
.second(current.second())
const to = dayjs(value.to).hour(current.hour()).minute(current.minute()).second(current.second())
if (from.date() === to.date()) {
// [Joshen] If a single date is selected, we either set the "from" to start from 00:00
// or "to" to end at 23:59 depending on which date was selected
if (from.date() === current.date()) {
return {
from: from.set('hour', 0).set('minute', 0).set('second', 0).utc().toISOString(),
to: to.utc().toISOString(),
}
} else {
return {
from: from.utc().toISOString(),
to: to.set('hour', 23).set('minute', 59).set('second', 59).utc().toISOString(),
}
}
} else {
return { from: from.utc().toISOString(), to: to.utc().toISOString() }
}
}
@@ -1,7 +1,8 @@
import { DatePicker } from 'components/ui/DatePicker'
import dayjs from 'dayjs'
import { useEffect, useState } from 'react'
import { Clock } from 'lucide-react'
import { ComponentProps, PropsWithChildren, useEffect, useState } from 'react'
import { DatePicker } from 'components/ui/DatePicker'
import {
Alert,
Button,
@@ -13,15 +14,14 @@ import {
} from 'ui'
import { LOGS_LARGE_DATE_RANGE_DAYS_THRESHOLD, getDefaultHelper } from './Logs.constants'
import type { DatetimeHelper } from './Logs.types'
import { Clock } from 'lucide-react'
interface Props {
to: string
from: string
onChange: React.ComponentProps<typeof DatePicker>['onChange']
onChange: ComponentProps<typeof DatePicker>['onChange']
helpers: DatetimeHelper[]
}
const DatePickers: React.FC<Props> = ({ to, from, onChange, helpers }) => {
const DatePickers = ({ to, from, onChange, helpers }: PropsWithChildren<Props>) => {
const defaultHelper = getDefaultHelper(helpers)
const [helperValue, setHelperValue] = useState<string>(to || from ? '' : defaultHelper.text)
@@ -1,5 +1,4 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import dayjs from 'dayjs'
import { get, handleError } from 'data/fetchers'
import type { ResponseError } from 'types'
@@ -68,11 +67,12 @@ export const useOrganizationAuditLogsQuery = <TData = OrganizationAuditLogsData>
}: UseQueryOptions<OrganizationAuditLogsData, OrganizationAuditLogsError, TData> = {}
) => {
const { slug, iso_timestamp_start, iso_timestamp_end } = vars
const date_start = dayjs(iso_timestamp_start).utc().format('YYYY-MM-DD')
const date_end = dayjs(iso_timestamp_end).utc().format('YYYY-MM-DD')
return useQuery<OrganizationAuditLogsData, OrganizationAuditLogsError, TData>(
organizationKeys.auditLogs(slug, { date_start, date_end }),
organizationKeys.auditLogs(slug, {
date_start: iso_timestamp_start,
date_end: iso_timestamp_end,
}),
({ signal }) => getOrganizationAuditLogs(vars, signal),
{
enabled: enabled && typeof slug !== 'undefined',