From 2ac515894bf7a77b77650ae53a2ffce926761666 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Thu, 4 Jun 2026 08:47:23 +0200 Subject: [PATCH] feat(settings): add Logs as a dedicated tab in General settings (#21180) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What & why The audit-log viewer lived as a full-screen page reachable only via a "View Logs" button buried in the **Security** tab. This surfaces it as the **third tab in General settings** (`General | Security | Logs`), consistent with the other tabs. ## Changes - **Relocated** the event-logs module `pages/settings/security/event-logs/` → `modules/settings/event-logs/` and render it as tab content instead of a `FullScreenContainer` page. Dropped `SettingsPath.EventLogs`, its route, and the fullscreen handling in favor of the `general#logs` hash tab. - **Security tab:** removed the "View Logs" entry; kept the log-retention setting there. - **In-tab gating** (shown to users with the Security permission): Enterprise upgrade card when not entitled, a clear "ClickHouse not configured" placeholder otherwise (derived from client config), and the query is skipped when disabled. Replaces a bespoke error component that string-matched error messages with the shared `SettingsEmptyPlaceholder` / `SettingsEnterpriseFeatureGateCard`. - **Layout:** boxed content column with the table selector + filters grouped in a `Card` and the results table below, matching settings conventions. Kept the existing fixed filters (page/event name, member, period) rather than recreating the record-view filter chips (those are tightly coupled to record/view context). Frontend + `twenty-shared` only — no changes to the log query or data. ## Test plan - [x] `npx nx typecheck twenty-front` and `npx nx lint twenty-front` pass - [x] Settings → General shows three tabs; Logs is the third; breadcrumb stays "Workspace / General" - [x] With Enterprise + ClickHouse: table selector, filters, refresh, and the paginated table work - [x] Non-Enterprise: Enterprise upgrade card shown; no failing query fires - [ ] Enterprise without ClickHouse: shows the "ClickHouse not configured" placeholder - [ ] Security tab still shows the log-retention setting and the "View Logs" button is gone - [ ] A user without the Security permission sees neither the Security nor Logs tab --- .github/workflows/ci-test-docker-compose.yaml | 22 +- .../src/generated-metadata/graphql.ts | 8 + .../modules/app/components/SettingsRoutes.tsx | 9 - .../components/SettingsDatePickerInput.tsx | 15 +- .../components/EventLogDatePickerInput.tsx | 0 .../event-logs/components/EventLogFilters.tsx | 134 +++++----- .../components/EventLogJsonCell.tsx | 0 .../components/EventLogResultsTable.tsx | 4 +- .../components/EventLogTableSelector.tsx | 0 .../event-logs/components/SettingsLogs.tsx | 194 ++++++++++++++ .../graphql/queries/getEventLogs.ts | 0 .../event-logs/hooks/useQueryEventLogs.ts | 8 +- .../event-logs/types/EventLogFiltersState.ts | 10 + .../utils/getColumnsForEventLogTable.ts | 0 .../components/SettingsSecuritySettings.tsx | 71 ++--- .../fullscreen/hooks/useShowFullscreen.ts | 6 +- .../settings/general/SettingsGeneral.tsx | 15 +- .../security/event-logs/SettingsEventLogs.tsx | 246 ------------------ .../event-logs/event-logs.resolver.ts | 2 - .../event-logs/event-logs.service.ts | 20 +- .../twenty-shared/src/types/SettingsPath.ts | 2 +- 21 files changed, 358 insertions(+), 408 deletions(-) rename packages/twenty-front/src/{pages/settings/security => modules/settings}/event-logs/components/EventLogDatePickerInput.tsx (100%) rename packages/twenty-front/src/{pages/settings/security => modules/settings}/event-logs/components/EventLogFilters.tsx (57%) rename packages/twenty-front/src/{pages/settings/security => modules/settings}/event-logs/components/EventLogJsonCell.tsx (100%) rename packages/twenty-front/src/{pages/settings/security => modules/settings}/event-logs/components/EventLogResultsTable.tsx (98%) rename packages/twenty-front/src/{pages/settings/security => modules/settings}/event-logs/components/EventLogTableSelector.tsx (100%) create mode 100644 packages/twenty-front/src/modules/settings/event-logs/components/SettingsLogs.tsx rename packages/twenty-front/src/{pages/settings/security => modules/settings}/event-logs/graphql/queries/getEventLogs.ts (100%) rename packages/twenty-front/src/{pages/settings/security => modules/settings}/event-logs/hooks/useQueryEventLogs.ts (88%) create mode 100644 packages/twenty-front/src/modules/settings/event-logs/types/EventLogFiltersState.ts rename packages/twenty-front/src/{pages/settings/security => modules/settings}/event-logs/utils/getColumnsForEventLogTable.ts (100%) delete mode 100644 packages/twenty-front/src/pages/settings/security/event-logs/SettingsEventLogs.tsx diff --git a/.github/workflows/ci-test-docker-compose.yaml b/.github/workflows/ci-test-docker-compose.yaml index 705097814cf..28a63460499 100644 --- a/.github/workflows/ci-test-docker-compose.yaml +++ b/.github/workflows/ci-test-docker-compose.yaml @@ -27,11 +27,12 @@ jobs: steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - name: Login to Docker Hub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 - with: - username: ${{ vars.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} + # Pull base images through Google's Docker Hub mirror — avoids Docker Hub + # rate limits and needs no credentials (this repo is public). + - name: Configure Docker Hub mirror + run: | + echo '{"registry-mirrors":["https://mirror.gcr.io"]}' | sudo tee /etc/docker/daemon.json + sudo systemctl restart docker - name: Run compose run: | echo "Patching docker-compose.yml..." @@ -102,11 +103,12 @@ jobs: steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - name: Login to Docker Hub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 - with: - username: ${{ vars.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} + # Pull base images through Google's Docker Hub mirror — avoids Docker Hub + # rate limits and needs no credentials (this repo is public). + - name: Configure Docker Hub mirror + run: | + echo '{"registry-mirrors":["https://mirror.gcr.io"]}' | sudo tee /etc/docker/daemon.json + sudo systemctl restart docker - name: Create frontend placeholder run: | mkdir -p packages/twenty-front/build diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 98c9909fdf6..f01bc182b29 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -7348,6 +7348,13 @@ export type EnterpriseSubscriptionStatusQueryVariables = Exact<{ [key: string]: export type EnterpriseSubscriptionStatusQuery = { __typename?: 'Query', enterpriseSubscriptionStatus?: { __typename?: 'EnterpriseSubscriptionStatusDTO', status: string, licensee?: string | null, expiresAt?: string | null, cancelAt?: string | null, currentPeriodEnd?: string | null, isCancellationScheduled: boolean } | null }; +export type EventLogsQueryVariables = Exact<{ + input: EventLogQueryInput; +}>; + + +export type EventLogsQuery = { __typename?: 'Query', eventLogs: { __typename?: 'EventLogQueryResult', totalCount: number, records: Array<{ __typename?: 'EventLogRecord', event: string, timestamp: string, userId?: string | null, properties?: any | null, recordId?: string | null, objectMetadataId?: string | null, isCustom?: boolean | null }>, pageInfo: { __typename?: 'EventLogPageInfo', endCursor?: string | null, hasNextPage: boolean } } }; + export type UpdateLabPublicFeatureFlagMutationVariables = Exact<{ input: UpdateLabPublicFeatureFlagInput; }>; @@ -8197,6 +8204,7 @@ export const SetEnterpriseKeyDocument = {"kind":"Document","definitions":[{"kind export const EnterpriseCheckoutSessionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EnterpriseCheckoutSession"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"billingInterval"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"enterpriseCheckoutSession"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"billingInterval"},"value":{"kind":"Variable","name":{"kind":"Name","value":"billingInterval"}}}]}]}}]} as unknown as DocumentNode; export const EnterprisePortalSessionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EnterprisePortalSession"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"returnUrlPath"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"enterprisePortalSession"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"returnUrlPath"},"value":{"kind":"Variable","name":{"kind":"Name","value":"returnUrlPath"}}}]}]}}]} as unknown as DocumentNode; export const EnterpriseSubscriptionStatusDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EnterpriseSubscriptionStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"enterpriseSubscriptionStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"licensee"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}},{"kind":"Field","name":{"kind":"Name","value":"cancelAt"}},{"kind":"Field","name":{"kind":"Name","value":"currentPeriodEnd"}},{"kind":"Field","name":{"kind":"Name","value":"isCancellationScheduled"}}]}}]}}]} as unknown as DocumentNode; +export const EventLogsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EventLogs"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EventLogQueryInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"eventLogs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"records"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}},{"kind":"Field","name":{"kind":"Name","value":"properties"}},{"kind":"Field","name":{"kind":"Name","value":"recordId"}},{"kind":"Field","name":{"kind":"Name","value":"objectMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}}]}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"endCursor"}},{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}}]}}]}}]}}]} as unknown as DocumentNode; export const UpdateLabPublicFeatureFlagDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateLabPublicFeatureFlag"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateLabPublicFeatureFlagInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateLabPublicFeatureFlag"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}}]}}]}}]} as unknown as DocumentNode; export const UploadWorkspaceMemberProfilePictureDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UploadWorkspaceMemberProfilePicture"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"file"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Upload"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"uploadWorkspaceMemberProfilePicture"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"file"},"value":{"kind":"Variable","name":{"kind":"Name","value":"file"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}}]}}]} as unknown as DocumentNode; export const UpdateUserEmailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateUserEmail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"newEmail"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"verifyEmailRedirectPath"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateUserEmail"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"newEmail"},"value":{"kind":"Variable","name":{"kind":"Name","value":"newEmail"}}},{"kind":"Argument","name":{"kind":"Name","value":"verifyEmailRedirectPath"},"value":{"kind":"Variable","name":{"kind":"Name","value":"verifyEmailRedirectPath"}}}]}]}}]} as unknown as DocumentNode; diff --git a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx index 3bde2e353a2..04fb88db16e 100644 --- a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx +++ b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx @@ -435,14 +435,6 @@ const SettingsSecurityApprovedAccessDomain = lazy(() => ), ); -const SettingsEventLogs = lazy(() => - import('~/pages/settings/security/event-logs/SettingsEventLogs').then( - (module) => ({ - default: module.SettingsEventLogs, - }), - ), -); - const SettingsNewEmailingDomain = lazy(() => import('~/pages/settings/emailing-domains/SettingsNewEmailingDomain').then( (module) => ({ @@ -934,7 +926,6 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => ( path={SettingsPath.NewApprovedAccessDomain} element={} /> - } /> {isAdminPageEnabled && ( diff --git a/packages/twenty-front/src/modules/settings/components/SettingsDatePickerInput.tsx b/packages/twenty-front/src/modules/settings/components/SettingsDatePickerInput.tsx index fba6ea5e7b4..9fea877079b 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsDatePickerInput.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsDatePickerInput.tsx @@ -1,6 +1,6 @@ import { styled } from '@linaria/react'; import { useLingui } from '@lingui/react/macro'; -import { useRef, useState } from 'react'; +import { useId, useRef, useState } from 'react'; import { Temporal } from 'temporal-polyfill'; import { autoUpdate, @@ -67,7 +67,8 @@ const StyledFloatingContainer = styled.div` `; export type SettingsDatePickerInputProps = { - label: string; + label?: string; + instanceId?: string; value: Date | undefined; onChange: (date: Date | undefined) => void; placeholder?: string; @@ -75,6 +76,7 @@ export type SettingsDatePickerInputProps = { export const SettingsDatePickerInput = ({ label, + instanceId, value, onChange, placeholder, @@ -83,6 +85,9 @@ export const SettingsDatePickerInput = ({ const [isOpen, setIsOpen] = useState(false); const containerRef = useRef(null); const { userTimezone } = useUserTimezone(); + const generatedId = useId(); + + const pickerInstanceId = instanceId ?? label ?? generatedId; const { refs, floatingStyles } = useFloating({ open: isOpen, @@ -97,7 +102,7 @@ export const SettingsDatePickerInput = ({ useListenClickOutside({ refs: [containerRef], - listenerId: `settings-date-picker-${label}`, + listenerId: `settings-date-picker-${pickerInstanceId}`, callback: handleClose, enabled: isOpen, excludedClickOutsideIds: [ @@ -145,7 +150,7 @@ export const SettingsDatePickerInput = ({ return ( - {label} + {label && {label}} void; }; -const StyledFiltersContainer = styled.div` - align-items: flex-end; - display: flex; - flex-wrap: wrap; - gap: ${themeCssVariables.spacing[2]}; +const StyledFiltersGrid = styled.div` + display: grid; + gap: ${themeCssVariables.spacing[3]} ${themeCssVariables.spacing[4]}; + grid-template-columns: 1fr 1fr; `; -const StyledFilterItem = styled.div` - flex: 1; - max-width: 300px; - min-width: 200px; +const StyledFullWidthField = styled.div` + grid-column: 1 / -1; +`; + +const StyledPeriodRow = styled.div` + display: grid; + gap: ${themeCssVariables.spacing[4]}; + grid-template-columns: 1fr 1fr; `; export const EventLogFilters = ({ @@ -82,7 +86,7 @@ export const EventLogFilters = ({ }; const eventLabel = - table === EventLogTable.PAGEVIEW ? t`Page Name` : t`Event Type`; + table === EventLogTable.PAGEVIEW ? t`Page name` : t`Event type`; const userWorkspaceOptions: SelectOption[] = [ { label: t`All Members`, value: null, Icon: IconUser }, @@ -106,70 +110,64 @@ export const EventLogFilters = ({ ]; return ( - - - - + + - - - - - - - - - + + {t`Period`} + + + + + {table === EventLogTable.OBJECT_EVENT && ( <> - - - - - + )} - + ); }; diff --git a/packages/twenty-front/src/pages/settings/security/event-logs/components/EventLogJsonCell.tsx b/packages/twenty-front/src/modules/settings/event-logs/components/EventLogJsonCell.tsx similarity index 100% rename from packages/twenty-front/src/pages/settings/security/event-logs/components/EventLogJsonCell.tsx rename to packages/twenty-front/src/modules/settings/event-logs/components/EventLogJsonCell.tsx diff --git a/packages/twenty-front/src/pages/settings/security/event-logs/components/EventLogResultsTable.tsx b/packages/twenty-front/src/modules/settings/event-logs/components/EventLogResultsTable.tsx similarity index 98% rename from packages/twenty-front/src/pages/settings/security/event-logs/components/EventLogResultsTable.tsx rename to packages/twenty-front/src/modules/settings/event-logs/components/EventLogResultsTable.tsx index 0d47b12e3a8..653f31e35ba 100644 --- a/packages/twenty-front/src/pages/settings/security/event-logs/components/EventLogResultsTable.tsx +++ b/packages/twenty-front/src/modules/settings/event-logs/components/EventLogResultsTable.tsx @@ -22,8 +22,8 @@ import { import { type ColumnConfig, getColumnsForEventLogTable, -} from '~/pages/settings/security/event-logs/utils/getColumnsForEventLogTable'; -import { EventLogJsonCell } from '~/pages/settings/security/event-logs/components/EventLogJsonCell'; +} from '@/settings/event-logs/utils/getColumnsForEventLogTable'; +import { EventLogJsonCell } from '@/settings/event-logs/components/EventLogJsonCell'; type EventLogResultsTableProps = { records: EventLogRecord[]; diff --git a/packages/twenty-front/src/pages/settings/security/event-logs/components/EventLogTableSelector.tsx b/packages/twenty-front/src/modules/settings/event-logs/components/EventLogTableSelector.tsx similarity index 100% rename from packages/twenty-front/src/pages/settings/security/event-logs/components/EventLogTableSelector.tsx rename to packages/twenty-front/src/modules/settings/event-logs/components/EventLogTableSelector.tsx diff --git a/packages/twenty-front/src/modules/settings/event-logs/components/SettingsLogs.tsx b/packages/twenty-front/src/modules/settings/event-logs/components/SettingsLogs.tsx new file mode 100644 index 00000000000..25ca1fe5cf9 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/event-logs/components/SettingsLogs.tsx @@ -0,0 +1,194 @@ +import { styled } from '@linaria/react'; +import { useLingui } from '@lingui/react/macro'; +import { useState } from 'react'; + +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; +import { isClickHouseConfiguredState } from '@/client-config/states/isClickHouseConfiguredState'; +import { SettingsEmptyPlaceholder } from '@/settings/components/SettingsEmptyPlaceholder'; +import { SettingsEnterpriseFeatureGateCard } from '@/settings/components/SettingsEnterpriseFeatureGateCard'; +import { EventLogFilters } from '@/settings/event-logs/components/EventLogFilters'; +import { EventLogResultsTable } from '@/settings/event-logs/components/EventLogResultsTable'; +import { EventLogTableSelector } from '@/settings/event-logs/components/EventLogTableSelector'; +import { useEventLogs } from '@/settings/event-logs/hooks/useQueryEventLogs'; +import { type EventLogFiltersState } from '@/settings/event-logs/types/EventLogFiltersState'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { isDefined } from 'twenty-shared/utils'; +import { IconRefresh } from 'twenty-ui/display'; +import { IconButton } from 'twenty-ui/input'; +import { Card } from 'twenty-ui/layout'; +import { themeCssVariables } from 'twenty-ui/theme-constants'; + +import { EventLogTable } from '~/generated-metadata/graphql'; + +const StyledCardContent = styled.div` + display: flex; + flex-direction: column; + gap: ${themeCssVariables.spacing[4]}; + padding: ${themeCssVariables.spacing[4]}; +`; + +const StyledSelectorRow = styled.div` + align-items: flex-end; + display: flex; + gap: ${themeCssVariables.spacing[2]}; +`; + +const StyledSelectorGrow = styled.div` + flex: 1; + min-width: 0; +`; + +const StyledResults = styled.div` + display: flex; + flex-direction: column; + gap: ${themeCssVariables.spacing[2]}; +`; + +const StyledRecordCount = styled.span` + align-self: flex-end; + color: ${themeCssVariables.font.color.secondary}; + font-size: ${themeCssVariables.font.size.sm}; +`; + +// The results table scrolls internally and loads more as you reach the bottom, +// so it needs a bounded height. +const StyledTableWrapper = styled.div` + height: 480px; + overflow: hidden; +`; + +const RECORDS_PER_PAGE = 100; + +export const SettingsLogs = () => { + const { t } = useLingui(); + + const currentWorkspace = useAtomStateValue(currentWorkspaceState); + const isClickHouseConfigured = useAtomStateValue(isClickHouseConfiguredState); + + const hasEnterpriseAccess = + currentWorkspace?.hasValidSignedEnterpriseKey === true; + + const [selectedTable, setSelectedTable] = useState( + EventLogTable.PAGEVIEW, + ); + const [filters, setFilters] = useState({}); + + const isApplicationLog = selectedTable === EventLogTable.APPLICATION_LOG; + const canQuery = + isClickHouseConfigured && (isApplicationLog || hasEnterpriseAccess); + + const { + records, + totalCount, + hasNextPage, + loading, + error, + refetch, + loadMore, + } = useEventLogs( + { + table: selectedTable, + filters: { + eventType: filters.eventType, + userWorkspaceId: filters.userWorkspaceId, + dateRange: filters.dateRange + ? { + start: filters.dateRange.start?.toISOString(), + end: filters.dateRange.end?.toISOString(), + } + : undefined, + recordId: filters.recordId, + objectMetadataId: filters.objectMetadataId, + }, + first: RECORDS_PER_PAGE, + }, + { skip: !canQuery }, + ); + + const handleTableChange = (table: EventLogTable) => { + setSelectedTable(table); + setFilters({}); + }; + + const handleFiltersChange = (newFilters: EventLogFiltersState) => { + setFilters(newFilters); + }; + + const renderResults = () => { + if (!isApplicationLog && !hasEnterpriseAccess) { + return ( + + ); + } + + if (!isClickHouseConfigured) { + return ( + + {t`Audit logs require ClickHouse to be configured. Please contact your administrator.`} + + ); + } + + if (isDefined(error)) { + return ( + + {t`Something went wrong while loading audit logs. Please try again.`} + + ); + } + + return ( + + {t`${records.length} of ${totalCount}`} + + + + + ); + }; + + return ( + <> + + + + + + + { + if (canQuery) { + void refetch(); + } + }} + /> + + + + + + {renderResults()} + + ); +}; diff --git a/packages/twenty-front/src/pages/settings/security/event-logs/graphql/queries/getEventLogs.ts b/packages/twenty-front/src/modules/settings/event-logs/graphql/queries/getEventLogs.ts similarity index 100% rename from packages/twenty-front/src/pages/settings/security/event-logs/graphql/queries/getEventLogs.ts rename to packages/twenty-front/src/modules/settings/event-logs/graphql/queries/getEventLogs.ts diff --git a/packages/twenty-front/src/pages/settings/security/event-logs/hooks/useQueryEventLogs.ts b/packages/twenty-front/src/modules/settings/event-logs/hooks/useQueryEventLogs.ts similarity index 88% rename from packages/twenty-front/src/pages/settings/security/event-logs/hooks/useQueryEventLogs.ts rename to packages/twenty-front/src/modules/settings/event-logs/hooks/useQueryEventLogs.ts index 1d3b2b195d5..edf417da1d7 100644 --- a/packages/twenty-front/src/pages/settings/security/event-logs/hooks/useQueryEventLogs.ts +++ b/packages/twenty-front/src/modules/settings/event-logs/hooks/useQueryEventLogs.ts @@ -6,7 +6,7 @@ import { type EventLogQueryResult, type EventLogRecord, } from '~/generated-metadata/graphql'; -import { GET_EVENT_LOGS } from '~/pages/settings/security/event-logs/graphql/queries/getEventLogs'; +import { GET_EVENT_LOGS } from '@/settings/event-logs/graphql/queries/getEventLogs'; type EventLogsData = { eventLogs: EventLogQueryResult; @@ -16,13 +16,17 @@ type EventLogsVariables = { input: EventLogQueryInput; }; -export const useEventLogs = (input: EventLogQueryInput) => { +export const useEventLogs = ( + input: EventLogQueryInput, + options?: { skip?: boolean }, +) => { const { data, loading, error, refetch, fetchMore } = useQuery< EventLogsData, EventLogsVariables >(GET_EVENT_LOGS, { variables: { input }, fetchPolicy: 'network-only', + skip: options?.skip, }); const records = data?.eventLogs.records ?? ([] as EventLogRecord[]); diff --git a/packages/twenty-front/src/modules/settings/event-logs/types/EventLogFiltersState.ts b/packages/twenty-front/src/modules/settings/event-logs/types/EventLogFiltersState.ts new file mode 100644 index 00000000000..94c6a9bd49e --- /dev/null +++ b/packages/twenty-front/src/modules/settings/event-logs/types/EventLogFiltersState.ts @@ -0,0 +1,10 @@ +export type EventLogFiltersState = { + eventType?: string; + userWorkspaceId?: string; + dateRange?: { + start?: Date; + end?: Date; + }; + recordId?: string; + objectMetadataId?: string; +}; diff --git a/packages/twenty-front/src/pages/settings/security/event-logs/utils/getColumnsForEventLogTable.ts b/packages/twenty-front/src/modules/settings/event-logs/utils/getColumnsForEventLogTable.ts similarity index 100% rename from packages/twenty-front/src/pages/settings/security/event-logs/utils/getColumnsForEventLogTable.ts rename to packages/twenty-front/src/modules/settings/event-logs/utils/getColumnsForEventLogTable.ts diff --git a/packages/twenty-front/src/modules/settings/security/components/SettingsSecuritySettings.tsx b/packages/twenty-front/src/modules/settings/security/components/SettingsSecuritySettings.tsx index c403f70dcb4..1e029a072d5 100644 --- a/packages/twenty-front/src/modules/settings/security/components/SettingsSecuritySettings.tsx +++ b/packages/twenty-front/src/modules/settings/security/components/SettingsSecuritySettings.tsx @@ -1,6 +1,5 @@ import { styled } from '@linaria/react'; import { useLingui } from '@lingui/react/macro'; -import { Link } from 'react-router-dom'; import { useDebouncedCallback } from 'use-debounce'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; @@ -26,8 +25,6 @@ import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { CombinedGraphQLErrors } from '@apollo/client/errors'; import { useMutation } from '@apollo/client/react'; -import { SettingsPath } from 'twenty-shared/types'; -import { getSettingsPath } from 'twenty-shared/utils'; import { Tag } from 'twenty-ui/components'; import { H2Title, @@ -37,7 +34,6 @@ import { IconMail, IconTrash, } from 'twenty-ui/display'; -import { Button } from 'twenty-ui/input'; import { Card, Section } from 'twenty-ui/layout'; import { themeCssVariables } from 'twenty-ui/theme-constants'; import { UpdateWorkspaceDocument } from '~/generated-metadata/graphql'; @@ -57,16 +53,6 @@ const StyledSectionContainer = styled.div` flex-shrink: 0; `; -const StyledLinkContainer = styled.div` - > a { - text-decoration: none; - - &[data-disabled='true'] { - pointer-events: none; - } - } -`; - export const SettingsSecuritySettings = () => { const { t } = useLingui(); const { enqueueErrorSnackBar } = useSnackBar(); @@ -259,7 +245,7 @@ export const SettingsSecuritySettings = () => {
{ /> {hasEnterpriseAccess ? ( - - -