Skip to content

Commit

Permalink
feat(audit-logs): add last entry placeholder (#1508)
Browse files Browse the repository at this point in the history
* feat(audit-logs): add last entry placeholder
  • Loading branch information
RemiBonnet authored Dec 10, 2024
1 parent 93a2a86 commit 151d991
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 83 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { mockUseQueryResult } from '__tests__/utils/mock-use-query-result'
import { type OrganizationEventResponseList } from 'qovery-typescript-axios'
import { IntercomProvider } from 'react-use-intercom'
import { type EventQueryParams } from '@qovery/domains/event'
import { eventsFactoryMock } from '@qovery/shared/factories'
import { renderWithProviders, screen } from '@qovery/shared/util-tests'
import { renderWithProviders, screen, waitFor } from '@qovery/shared/util-tests'
import PageGeneralFeature from './page-general-feature'

jest.mock('react-router-dom', () => ({
Expand Down Expand Up @@ -31,49 +32,76 @@ describe('PageGeneralFeature', () => {
})

it('should render successfully', () => {
const { baseElement } = renderWithProviders(<PageGeneralFeature />)
const { baseElement } = renderWithProviders(
<IntercomProvider appId="__test__app__id__">
<PageGeneralFeature />
</IntercomProvider>
)
expect(baseElement).toBeTruthy()
})

it('should fetch the event with correct payload', () => {
renderWithProviders(<PageGeneralFeature />)
renderWithProviders(
<IntercomProvider appId="__test__app__id__">
<PageGeneralFeature />
</IntercomProvider>
)
expect(mockUseFetchEvents).toHaveBeenCalledWith('0', { pageSize: 30 })
})

it('should change query params on click on next', async () => {
const { userEvent } = renderWithProviders(<PageGeneralFeature />)

const button = screen.getByTestId('button-next-page')
const { userEvent } = renderWithProviders(
<IntercomProvider appId="__test__app__id__">
<PageGeneralFeature />
</IntercomProvider>
)

await userEvent.click(button)
// `waitFor` is necessary because `IntercomProvider` provides somes rendering
waitFor(async () => {
const button = screen.getByTestId('button-next-page')
await userEvent.click(button)

expect(mockUseFetchEvents).toHaveBeenCalledWith('0', {
pageSize: 30,
continueToken: '1683211879216566000',
expect(mockUseFetchEvents).toHaveBeenCalledWith('0', {
pageSize: 30,
continueToken: '1683211879216566000',
})
})
})

it('should change query params on click on previous', async () => {
const { userEvent } = renderWithProviders(<PageGeneralFeature />)
const button = screen.getByTestId('button-previous-page')
const { userEvent } = renderWithProviders(
<IntercomProvider appId="__test__app__id__">
<PageGeneralFeature />
</IntercomProvider>
)

await userEvent.click(button)
// `waitFor` is necessary because `IntercomProvider` provides somes rendering
waitFor(async () => {
const button = screen.getByTestId('button-previous-page')
await userEvent.click(button)

expect(mockUseFetchEvents).toHaveBeenCalledWith('0', {
pageSize: 30,
stepBackToken: '1683211879216566001',
expect(mockUseFetchEvents).toHaveBeenCalledWith('0', {
pageSize: 30,
stepBackToken: '1683211879216566001',
})
})
})

it('should change query params on click on pageSize', async () => {
const { userEvent } = renderWithProviders(<PageGeneralFeature />)

const select = screen.getByTestId('select-page-size')
const { userEvent } = renderWithProviders(
<IntercomProvider appId="__test__app__id__">
<PageGeneralFeature />
</IntercomProvider>
)

await userEvent.selectOptions(select, '50')
// `waitFor` is necessary because `IntercomProvider` provides somes rendering
waitFor(async () => {
const select = screen.getByTestId('select-page-size')
await userEvent.selectOptions(select, '50')

expect(mockUseFetchEvents).toHaveBeenCalledWith('0', {
pageSize: 50,
expect(mockUseFetchEvents).toHaveBeenCalledWith('0', {
pageSize: 50,
})
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from 'qovery-typescript-axios'
import { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
import { useIntercom } from 'react-use-intercom'
import { createEnumParam } from 'serialize-query-params'
import { NumberParam, StringParam, useQueryParams, withDefault } from 'use-query-params'
import { type EventQueryParams, useFetchEvents } from '@qovery/domains/event'
Expand Down Expand Up @@ -48,6 +49,7 @@ export function PageGeneralFeature() {
const [filter, setFilter] = useState<TableFilterProps[]>([])
const { data: eventsData, isLoading } = useFetchEvents(organizationId, queryParams)
const { data: organization } = useOrganization({ organizationId, enabled: !!organizationId })
const { show: showIntercom } = useIntercom()

// Sync queryParams -> table filters
useEffect(() => {
Expand Down Expand Up @@ -136,6 +138,7 @@ export function PageGeneralFeature() {
filter={filter}
setFilter={setFilter}
organization={organization}
showIntercom={showIntercom}
/>
)
}
Expand Down
164 changes: 109 additions & 55 deletions libs/pages/events/src/lib/ui/page-general/page-general.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,21 @@ import {
OrganizationEventType,
} from 'qovery-typescript-axios'
import { type Dispatch, type SetStateAction } from 'react'
import { Icon, Pagination, Section, Table, type TableFilterProps, type TableHeadProps } from '@qovery/shared/ui'
import {
Icon,
Pagination,
Section,
Skeleton,
Table,
type TableFilterProps,
type TableHeadProps,
} from '@qovery/shared/ui'
import CustomFilterFeature from '../../feature/custom-filter-feature/custom-filter-feature'
import RowEventFeature from '../../feature/row-event-feature/row-event-feature'

export interface PageGeneralProps {
isLoading: boolean
showIntercom: () => void
handleClearFilter: () => void
events?: OrganizationEventResponse[]
placeholderEvents?: OrganizationEventResponse[]
Expand All @@ -25,6 +34,56 @@ export interface PageGeneralProps {
organization?: Organization
}

const dataHead: TableHeadProps<OrganizationEventResponse>[] = [
{
title: 'Timestamp',
className: 'pl-9',
},
{
title: 'Event',
filter: [
{
title: 'Filter by event',
key: 'event_type',
itemsCustom: Object.keys(OrganizationEventType).map((item) => item),
hideFilterNumber: true,
},
],
},
{
title: 'Target type',
},
{
title: 'Target',
},
{
title: 'Change',
},
{
title: 'User',
filter: [
{
title: 'Filter by user',
key: 'triggered_by',
hideFilterNumber: true,
},
],
},
{
title: 'Source',
filter: [
{
title: 'Filter by source',
key: 'origin',
itemsCustom: Object.keys(OrganizationEventOrigin).map((item) => item),
hideFilterNumber: true,
},
],
},
]

const columnsWidth = '14% 14% 12% 15% 10% 22% 11%'

export function PageGeneral({
isLoading,
events,
Expand All @@ -39,56 +98,19 @@ export function PageGeneral({
filter,
handleClearFilter,
organization,
showIntercom,
}: PageGeneralProps) {
const dataHead: TableHeadProps<OrganizationEventResponse>[] = [
{
title: 'Timestamp',
className: 'pl-9',
},
{
title: 'Event',
filter: [
{
title: 'Filter by event',
key: 'event_type',
itemsCustom: Object.keys(OrganizationEventType).map((item) => item),
hideFilterNumber: true,
},
],
},
{
title: 'Target type',
},
{
title: 'Target',
},
{
title: 'Change',
},
{
title: 'User',
filter: [
{
title: 'Filter by user',
key: 'triggered_by',
hideFilterNumber: true,
},
],
},
{
title: 'Source',
filter: [
{
title: 'Filter by source',
key: 'origin',
itemsCustom: Object.keys(OrganizationEventOrigin).map((item) => item),
hideFilterNumber: true,
},
],
},
]
const auditLogsRetentionInDays = organization?.organization_plan?.audit_logs_retention_in_days ?? 30
const currentDate = new Date().getTime()
const retentionLimitDate = currentDate - auditLogsRetentionInDays * 24 * 60 * 60 * 1000

const filteredEvents =
events?.filter((event) => {
if (!event.timestamp) return null
return new Date(event.timestamp).getTime() >= retentionLimitDate
}) || []

const columnsWidth = '14% 14% 12% 15% 10% 22% 11%'
const checkIfEventsAreFiltered = filteredEvents.length !== events?.length

return (
<Section className="grow p-8">
Expand All @@ -110,24 +132,56 @@ export function PageGeneral({
placeholderEvents?.map((event) => (
<RowEventFeature key={event.timestamp} event={event} columnsWidth={columnsWidth} isPlaceholder />
))
) : events && events.length === 0 ? (
) : !checkIfEventsAreFiltered && events.length === 0 ? (
<div className="flex h-[30vh] items-center justify-center px-5 py-4 text-center">
<div>
<Icon iconName="wave-pulse" className="text-neutral-350" />
<p className="mt-1 text-xs font-medium text-neutral-350" data-testid="empty-result">
No events found, we retain logs for a maximum of{' '}
{organization?.organization_plan?.audit_logs_retention_in_days ?? 30} days <br /> Try to change your
filters.
No events found, we retain logs for a maximum of {auditLogsRetentionInDays} days <br /> Try to change
your filters.
</p>
</div>
</div>
) : checkIfEventsAreFiltered ? (
<div>
{filteredEvents?.map((event) => (
<RowEventFeature key={event.timestamp} event={event} columnsWidth={columnsWidth} />
))}
<div className="flex h-14 items-center justify-center border-b border-neutral-200">
<p className="flex gap-1 text-sm text-neutral-400">
{auditLogsRetentionInDays} days limit reached.
{/* TODO: add a real button */}
<span
className="cursor-pointer font-medium text-sky-500 transition-colors hover:text-sky-600"
onClick={() => showIntercom()}
>
Upgrade your plan to see more
</span>
</p>
</div>
{[...Array(3)].map((_, index) => (
<div
key={index}
className="flex h-14 items-center justify-between border-b border-neutral-200 px-5 last:border-0"
>
{[...Array(6)].map((_, index) => (
<div key={index} className="flex items-center gap-4">
{index === 0 ? <Icon iconName="lock-keyhole" className="text-sm text-neutral-350" /> : null}
<Skeleton key={index} height={10} width={116} />
</div>
))}
</div>
))}
</div>
) : (
events?.map((event) => <RowEventFeature key={event.timestamp} event={event} columnsWidth={columnsWidth} />)
filteredEvents?.map((event) => (
<RowEventFeature key={event.timestamp} event={event} columnsWidth={columnsWidth} />
))
)}
</div>
</Table>
<Pagination
className="pb-7 pt-4"
className="pb-20 pt-4"
onPrevious={onPrevious}
onNext={onNext}
nextDisabled={nextDisabled}
Expand Down
12 changes: 6 additions & 6 deletions libs/pages/events/src/lib/ui/row-event/row-event.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ export function RowEvent(props: RowEventProps) {
onClick={() => setExpanded(!expanded)}
>
<div className="flex gap-3 px-4">
<Skeleton height={16} width={120} show={isPlaceholder}>
<Skeleton height={10} width={120} show={isPlaceholder}>
<div className="flex gap-3">
<Icon
name={IconAwesomeEnum.ANGLE_DOWN}
Expand All @@ -202,17 +202,17 @@ export function RowEvent(props: RowEventProps) {
</Skeleton>
</div>
<div className="px-4" data-testid="tag">
<Skeleton height={16} width={80} show={isPlaceholder}>
<Skeleton height={10} width={80} show={isPlaceholder}>
{badge}
</Skeleton>
</div>
<div className="px-4">
<Skeleton height={16} width={80} show={isPlaceholder}>
<Skeleton height={10} width={80} show={isPlaceholder}>
<>{upperCaseFirstLetter(event.target_type)}</>
</Skeleton>
</div>
<div className="px-4">
<Skeleton height={16} width={80} show={isPlaceholder}>
<Skeleton height={10} width={80} show={isPlaceholder}>
<Tooltip
content={
<div>
Expand All @@ -235,12 +235,12 @@ export function RowEvent(props: RowEventProps) {
</Skeleton>
</div>
<div className="px-4">
<Skeleton height={16} width={80} show={isPlaceholder}>
<Skeleton height={10} width={80} show={isPlaceholder}>
<span className="truncate">{upperCaseFirstLetter(event.sub_target_type ?? '')?.replace('_', ' ')}</span>
</Skeleton>
</div>
<div className="px-4">
<Skeleton height={16} width={80} show={isPlaceholder}>
<Skeleton height={10} width={80} show={isPlaceholder}>
<Tooltip content={event.triggered_by || ''}>
<span className="truncate">{event.triggered_by}</span>
</Tooltip>
Expand Down

0 comments on commit 151d991

Please sign in to comment.