From 8cadcdf577f505c107de31dc73a8d25bf21f8274 Mon Sep 17 00:00:00 2001
From: Ana Sofia Marin Alexandre <61988046+anamarn@users.noreply.github.com>
Date: Fri, 18 Oct 2024 11:00:21 +0200
Subject: [PATCH] add dynamic dates for webhookGraphDataUsage (#7720)
**Before:**
Only last 5 days where displayed on Developers Settings Webhook Usage
Graph.

**Now**
Added component where you can select the time range where you want to
view the webhook usage. To do better the styling and content depassing .
**In order to test**
1. Set ANALYTICS_ENABLED to true
2. Set TINYBIRD_TOKEN to your token from the workspace
twenty_analytics_playground
3. Write your client tinybird token in
SettingsDeveloppersWebhookDetail.tsx in line 93
4. Create a Webhook in twenty and set wich events it needs to track
5. Run twenty-worker in order to make the webhooks work.
6. Do your tasks in order to populate the data
7. Enter to settings> webhook>your webhook and the statistics section
should be displayed.
8. Select the desired time range in the dropdown
**To do list**
- Tooltip is truncated when accessing values at the right end of the
graph
- DateTicks needs to follow a more clear standard
- Update this PR with more representative images
---
.../twenty-front/src/generated/graphql.tsx | 4 +-
.../src/modules/auth/hooks/useAuth.ts | 6 +-
.../components/ClientConfigProviderEffect.tsx | 9 +-
.../graphql/queries/getClientConfig.ts | 1 +
.../states/isAnalyticsEnabledState.ts | 6 +
.../constants/DateFormatWithoutYear.ts | 11 ++
.../utils/__tests__/detectDateFormat.test.ts | 17 +--
.../utils/__tests__/detectTimeFormat.test.ts | 9 +-
...tDateISOStringToDateTimeSimplified.test.js | 90 +++++++++++
.../localization/utils/detectDateFormat.ts | 10 +-
.../localization/utils/detectTimeFormat.ts | 6 +-
...formatDateISOStringToDateTimeSimplified.ts | 18 +++
.../getDateFormatFromWorkspaceDateFormat.ts | 2 +-
.../getTimeFormatFromWorkspaceTimeFormat.ts | 2 +-
.../SettingsDevelopersWebhookTooltip.tsx | 89 +++++++++++
.../SettingsDevelopersWebhookUsageGraph.tsx | 142 ++++++++++++++++--
...tingsDevelopersWebhookUsageGraphEffect.tsx | 89 +----------
.../constants/WebhookGraphApiOptionsMap.ts | 6 +
.../developers/webhook/hooks/useGraphData.tsx | 23 +++
.../__tests__/fetchGraphDataOrThrow.test.js | 115 ++++++++++++++
.../webhook/utils/fetchGraphDataOrThrow.ts | 80 ++++++++++
.../users/components/UserProviderEffect.tsx | 6 +-
.../SettingsDevelopersWebhookDetail.tsx | 10 +-
.../components/DateTimeSettings.tsx | 4 +-
.../DateTimeSettingsDateFormatSelect.tsx | 2 +-
.../DateTimeSettingsTimeFormatSelect.tsx | 2 +-
.../client-config/client-config.entity.ts | 3 +
.../client-config/client-config.resolver.ts | 1 +
28 files changed, 631 insertions(+), 132 deletions(-)
create mode 100644 packages/twenty-front/src/modules/client-config/states/isAnalyticsEnabledState.ts
create mode 100644 packages/twenty-front/src/modules/localization/constants/DateFormatWithoutYear.ts
create mode 100644 packages/twenty-front/src/modules/localization/utils/__tests__/formatDateISOStringToDateTimeSimplified.test.js
create mode 100644 packages/twenty-front/src/modules/localization/utils/formatDateISOStringToDateTimeSimplified.ts
create mode 100644 packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip.tsx
create mode 100644 packages/twenty-front/src/modules/settings/developers/webhook/constants/WebhookGraphApiOptionsMap.ts
create mode 100644 packages/twenty-front/src/modules/settings/developers/webhook/hooks/useGraphData.tsx
create mode 100644 packages/twenty-front/src/modules/settings/developers/webhook/utils/__tests__/fetchGraphDataOrThrow.test.js
create mode 100644 packages/twenty-front/src/modules/settings/developers/webhook/utils/fetchGraphDataOrThrow.ts
diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx
index 5950c81d5..7f053fc6b 100644
--- a/packages/twenty-front/src/generated/graphql.tsx
+++ b/packages/twenty-front/src/generated/graphql.tsx
@@ -141,6 +141,7 @@ export enum CaptchaDriverType {
export type ClientConfig = {
__typename?: 'ClientConfig';
+ analyticsEnabled: Scalars['Boolean'];
api: ApiConfig;
authProviders: AuthProviders;
billing: Billing;
@@ -1599,7 +1600,7 @@ export type UpdateBillingSubscriptionMutation = { __typename?: 'Mutation', updat
export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>;
-export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, signUpDisabled: boolean, debugMode: boolean, chromeExtensionId?: string | null, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number } } };
+export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, signUpDisabled: boolean, debugMode: boolean, analyticsEnabled: boolean, chromeExtensionId?: string | null, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number } } };
export type SkipSyncEmailOnboardingStepMutationVariables = Exact<{ [key: string]: never; }>;
@@ -2765,6 +2766,7 @@ export const GetClientConfigDocument = gql`
signInPrefilled
signUpDisabled
debugMode
+ analyticsEnabled
support {
supportDriver
supportFrontChatId
diff --git a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts
index 7a7de0807..ae13d831f 100644
--- a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts
+++ b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts
@@ -32,6 +32,8 @@ import {
import { isDefined } from '~/utils/isDefined';
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates';
+import { DateFormat } from '@/localization/constants/DateFormat';
+import { TimeFormat } from '@/localization/constants/TimeFormat';
import { dateTimeFormatState } from '@/localization/states/dateTimeFormatState';
import { detectDateFormat } from '@/localization/utils/detectDateFormat';
import { detectTimeFormat } from '@/localization/utils/detectTimeFormat';
@@ -143,12 +145,12 @@ export const useAuth = () => {
? getDateFormatFromWorkspaceDateFormat(
user.workspaceMember.dateFormat,
)
- : detectDateFormat(),
+ : DateFormat[detectDateFormat()],
timeFormat: isDefined(user.workspaceMember.timeFormat)
? getTimeFormatFromWorkspaceTimeFormat(
user.workspaceMember.timeFormat,
)
- : detectTimeFormat(),
+ : TimeFormat[detectTimeFormat()],
});
}
diff --git a/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx b/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx
index 9eccbeb98..ed06d3f0e 100644
--- a/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx
+++ b/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx
@@ -1,23 +1,24 @@
-import { useEffect } from 'react';
-import { useRecoilState, useSetRecoilState } from 'recoil';
-
import { apiConfigState } from '@/client-config/states/apiConfigState';
import { authProvidersState } from '@/client-config/states/authProvidersState';
import { billingState } from '@/client-config/states/billingState';
import { captchaProviderState } from '@/client-config/states/captchaProviderState';
import { chromeExtensionIdState } from '@/client-config/states/chromeExtensionIdState';
+import { isAnalyticsEnabledState } from '@/client-config/states/isAnalyticsEnabledState';
import { isClientConfigLoadedState } from '@/client-config/states/isClientConfigLoadedState';
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState';
import { isSignUpDisabledState } from '@/client-config/states/isSignUpDisabledState';
import { sentryConfigState } from '@/client-config/states/sentryConfigState';
import { supportChatState } from '@/client-config/states/supportChatState';
+import { useEffect } from 'react';
+import { useRecoilState, useSetRecoilState } from 'recoil';
import { useGetClientConfigQuery } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
export const ClientConfigProviderEffect = () => {
const setAuthProviders = useSetRecoilState(authProvidersState);
const setIsDebugMode = useSetRecoilState(isDebugModeState);
+ const setIsAnalyticsEnabled = useSetRecoilState(isAnalyticsEnabledState);
const setIsSignInPrefilled = useSetRecoilState(isSignInPrefilledState);
const setIsSignUpDisabled = useSetRecoilState(isSignUpDisabledState);
@@ -50,6 +51,7 @@ export const ClientConfigProviderEffect = () => {
magicLink: false,
});
setIsDebugMode(data?.clientConfig.debugMode);
+ setIsAnalyticsEnabled(data?.clientConfig.analyticsEnabled);
setIsSignInPrefilled(data?.clientConfig.signInPrefilled);
setIsSignUpDisabled(data?.clientConfig.signUpDisabled);
@@ -84,6 +86,7 @@ export const ClientConfigProviderEffect = () => {
setCaptchaProvider,
setChromeExtensionId,
setApiConfig,
+ setIsAnalyticsEnabled,
]);
return <>>;
diff --git a/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts b/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts
index e702acefa..9a060b0d7 100644
--- a/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts
+++ b/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts
@@ -16,6 +16,7 @@ export const GET_CLIENT_CONFIG = gql`
signInPrefilled
signUpDisabled
debugMode
+ analyticsEnabled
support {
supportDriver
supportFrontChatId
diff --git a/packages/twenty-front/src/modules/client-config/states/isAnalyticsEnabledState.ts b/packages/twenty-front/src/modules/client-config/states/isAnalyticsEnabledState.ts
new file mode 100644
index 000000000..50c0f5c89
--- /dev/null
+++ b/packages/twenty-front/src/modules/client-config/states/isAnalyticsEnabledState.ts
@@ -0,0 +1,6 @@
+import { createState } from 'twenty-ui';
+
+export const isAnalyticsEnabledState = createState({
+ key: 'isAnalyticsEnabled',
+ defaultValue: false,
+});
diff --git a/packages/twenty-front/src/modules/localization/constants/DateFormatWithoutYear.ts b/packages/twenty-front/src/modules/localization/constants/DateFormatWithoutYear.ts
new file mode 100644
index 000000000..a1c7f2af3
--- /dev/null
+++ b/packages/twenty-front/src/modules/localization/constants/DateFormatWithoutYear.ts
@@ -0,0 +1,11 @@
+import { DateFormat } from '@/localization/constants/DateFormat';
+
+type DateFormatWithoutYear = {
+ [K in keyof typeof DateFormat]: string;
+};
+export const DATE_FORMAT_WITHOUT_YEAR: DateFormatWithoutYear = {
+ SYSTEM: 'SYSTEM',
+ MONTH_FIRST: 'MMM d',
+ DAY_FIRST: 'd MMM',
+ YEAR_FIRST: 'MMM d',
+};
diff --git a/packages/twenty-front/src/modules/localization/utils/__tests__/detectDateFormat.test.ts b/packages/twenty-front/src/modules/localization/utils/__tests__/detectDateFormat.test.ts
index 2b641f302..b267622bf 100644
--- a/packages/twenty-front/src/modules/localization/utils/__tests__/detectDateFormat.test.ts
+++ b/packages/twenty-front/src/modules/localization/utils/__tests__/detectDateFormat.test.ts
@@ -1,8 +1,7 @@
-import { DateFormat } from '@/localization/constants/DateFormat';
import { detectDateFormat } from '@/localization/utils/detectDateFormat';
describe('detectDateFormat', () => {
- it('should return DateFormat.MONTH_FIRST if the detected format starts with month', () => {
+ it('should return MONTH_FIRST if the detected format starts with month', () => {
// Mock the Intl.DateTimeFormat to return a specific format
const mockDateTimeFormat = jest.fn().mockReturnValue({
formatToParts: () => [
@@ -16,10 +15,10 @@ describe('detectDateFormat', () => {
const result = detectDateFormat();
- expect(result).toBe(DateFormat.MONTH_FIRST);
+ expect(result).toBe('MONTH_FIRST');
});
- it('should return DateFormat.DAY_FIRST if the detected format starts with day', () => {
+ it('should return DAY_FIRST if the detected format starts with day', () => {
// Mock the Intl.DateTimeFormat to return a specific format
const mockDateTimeFormat = jest.fn().mockReturnValue({
formatToParts: () => [
@@ -32,10 +31,10 @@ describe('detectDateFormat', () => {
const result = detectDateFormat();
- expect(result).toBe(DateFormat.DAY_FIRST);
+ expect(result).toBe('DAY_FIRST');
});
- it('should return DateFormat.YEAR_FIRST if the detected format starts with year', () => {
+ it('should return YEAR_FIRST if the detected format starts with year', () => {
// Mock the Intl.DateTimeFormat to return a specific format
const mockDateTimeFormat = jest.fn().mockReturnValue({
formatToParts: () => [
@@ -48,10 +47,10 @@ describe('detectDateFormat', () => {
const result = detectDateFormat();
- expect(result).toBe(DateFormat.YEAR_FIRST);
+ expect(result).toBe('YEAR_FIRST');
});
- it('should return DateFormat.MONTH_FIRST by default if the detected format does not match any specific order', () => {
+ it('should return MONTH_FIRST by default if the detected format does not match any specific order', () => {
// Mock the Intl.DateTimeFormat to return a specific format
const mockDateTimeFormat = jest.fn().mockReturnValue({
formatToParts: () => [
@@ -64,6 +63,6 @@ describe('detectDateFormat', () => {
const result = detectDateFormat();
- expect(result).toBe(DateFormat.MONTH_FIRST);
+ expect(result).toBe('MONTH_FIRST');
});
});
diff --git a/packages/twenty-front/src/modules/localization/utils/__tests__/detectTimeFormat.test.ts b/packages/twenty-front/src/modules/localization/utils/__tests__/detectTimeFormat.test.ts
index 643349578..9445068a5 100644
--- a/packages/twenty-front/src/modules/localization/utils/__tests__/detectTimeFormat.test.ts
+++ b/packages/twenty-front/src/modules/localization/utils/__tests__/detectTimeFormat.test.ts
@@ -1,8 +1,7 @@
-import { TimeFormat } from '@/localization/constants/TimeFormat';
import { detectTimeFormat } from '@/localization/utils/detectTimeFormat';
describe('detectTimeFormat', () => {
- it('should return TimeFormat.HOUR_12 if the hour format is 12-hour', () => {
+ it('should return HOUR_12 if the hour format is 12-hour', () => {
// Mock the resolvedOptions method to return hour12 as true
const mockResolvedOptions = jest.fn(() => ({ hour12: true }));
Intl.DateTimeFormat = jest.fn().mockImplementation(() => ({
@@ -11,11 +10,11 @@ describe('detectTimeFormat', () => {
const result = detectTimeFormat();
- expect(result).toBe(TimeFormat.HOUR_12);
+ expect(result).toBe('HOUR_12');
expect(mockResolvedOptions).toHaveBeenCalled();
});
- it('should return TimeFormat.HOUR_24 if the hour format is 24-hour', () => {
+ it('should return HOUR_24 if the hour format is 24-hour', () => {
// Mock the resolvedOptions method to return hour12 as false
const mockResolvedOptions = jest.fn(() => ({ hour12: false }));
Intl.DateTimeFormat = jest.fn().mockImplementation(() => ({
@@ -24,7 +23,7 @@ describe('detectTimeFormat', () => {
const result = detectTimeFormat();
- expect(result).toBe(TimeFormat.HOUR_24);
+ expect(result).toBe('HOUR_24');
expect(mockResolvedOptions).toHaveBeenCalled();
});
});
diff --git a/packages/twenty-front/src/modules/localization/utils/__tests__/formatDateISOStringToDateTimeSimplified.test.js b/packages/twenty-front/src/modules/localization/utils/__tests__/formatDateISOStringToDateTimeSimplified.test.js
new file mode 100644
index 000000000..4caee3aed
--- /dev/null
+++ b/packages/twenty-front/src/modules/localization/utils/__tests__/formatDateISOStringToDateTimeSimplified.test.js
@@ -0,0 +1,90 @@
+import { detectDateFormat } from '@/localization/utils/detectDateFormat';
+import { formatDateISOStringToDateTimeSimplified } from '@/localization/utils/formatDateISOStringToDateTimeSimplified';
+import { formatInTimeZone } from 'date-fns-tz';
+// Mock the imported modules
+jest.mock('@/localization/utils/detectDateFormat');
+jest.mock('date-fns-tz');
+
+describe('formatDateISOStringToDateTimeSimplified', () => {
+ const mockDate = new Date('2023-08-15T10:30:00Z');
+ const mockTimeZone = 'America/New_York';
+ const mockTimeFormat = 'HH:mm';
+
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it('should format the date correctly when DATE_FORMAT is MONTH_FIRST', () => {
+ detectDateFormat.mockReturnValue('MONTH_FIRST');
+ formatInTimeZone.mockReturnValue('Oct 15 · 06:30');
+
+ const result = formatDateISOStringToDateTimeSimplified(
+ mockDate,
+ mockTimeZone,
+ mockTimeFormat,
+ );
+
+ expect(detectDateFormat).toHaveBeenCalled();
+ expect(formatInTimeZone).toHaveBeenCalledWith(
+ mockDate,
+ mockTimeZone,
+ 'MMM d · HH:mm',
+ );
+ expect(result).toBe('Oct 15 · 06:30');
+ });
+
+ it('should format the date correctly when DATE_FORMAT is DAY_FIRST', () => {
+ detectDateFormat.mockReturnValue('DAY_FIRST');
+ formatInTimeZone.mockReturnValue('15 Oct · 06:30');
+
+ const result = formatDateISOStringToDateTimeSimplified(
+ mockDate,
+ mockTimeZone,
+ mockTimeFormat,
+ );
+
+ expect(detectDateFormat).toHaveBeenCalled();
+ expect(formatInTimeZone).toHaveBeenCalledWith(
+ mockDate,
+ mockTimeZone,
+ 'd MMM · HH:mm',
+ );
+ expect(result).toBe('15 Oct · 06:30');
+ });
+
+ it('should use the provided time format', () => {
+ detectDateFormat.mockReturnValue('MONTH_FIRST');
+ formatInTimeZone.mockReturnValue('Oct 15 · 6:30 AM');
+
+ const result = formatDateISOStringToDateTimeSimplified(
+ mockDate,
+ mockTimeZone,
+ 'h:mm aa',
+ );
+
+ expect(formatInTimeZone).toHaveBeenCalledWith(
+ mockDate,
+ mockTimeZone,
+ 'MMM d · h:mm aa',
+ );
+ expect(result).toBe('Oct 15 · 6:30 AM');
+ });
+
+ it('should handle different time zones', () => {
+ detectDateFormat.mockReturnValue('MONTH_FIRST');
+ formatInTimeZone.mockReturnValue('Oct 16 · 02:30');
+
+ const result = formatDateISOStringToDateTimeSimplified(
+ mockDate,
+ 'Asia/Tokyo',
+ mockTimeFormat,
+ );
+
+ expect(formatInTimeZone).toHaveBeenCalledWith(
+ mockDate,
+ 'Asia/Tokyo',
+ 'MMM d · HH:mm',
+ );
+ expect(result).toBe('Oct 16 · 02:30');
+ });
+});
diff --git a/packages/twenty-front/src/modules/localization/utils/detectDateFormat.ts b/packages/twenty-front/src/modules/localization/utils/detectDateFormat.ts
index b503ef826..e38b018df 100644
--- a/packages/twenty-front/src/modules/localization/utils/detectDateFormat.ts
+++ b/packages/twenty-front/src/modules/localization/utils/detectDateFormat.ts
@@ -1,6 +1,6 @@
import { DateFormat } from '@/localization/constants/DateFormat';
-export const detectDateFormat = (): DateFormat => {
+export const detectDateFormat = (): keyof typeof DateFormat => {
const date = new Date();
const formatter = new Intl.DateTimeFormat(navigator.language);
const parts = formatter.formatToParts(date);
@@ -9,9 +9,9 @@ export const detectDateFormat = (): DateFormat => {
.filter((part) => ['year', 'month', 'day'].includes(part.type))
.map((part) => part.type);
- if (partOrder[0] === 'month') return DateFormat.MONTH_FIRST;
- if (partOrder[0] === 'day') return DateFormat.DAY_FIRST;
- if (partOrder[0] === 'year') return DateFormat.YEAR_FIRST;
+ if (partOrder[0] === 'month') return 'MONTH_FIRST';
+ if (partOrder[0] === 'day') return 'DAY_FIRST';
+ if (partOrder[0] === 'year') return 'YEAR_FIRST';
- return DateFormat.MONTH_FIRST;
+ return 'MONTH_FIRST';
};
diff --git a/packages/twenty-front/src/modules/localization/utils/detectTimeFormat.ts b/packages/twenty-front/src/modules/localization/utils/detectTimeFormat.ts
index 01bad1716..d6d914d83 100644
--- a/packages/twenty-front/src/modules/localization/utils/detectTimeFormat.ts
+++ b/packages/twenty-front/src/modules/localization/utils/detectTimeFormat.ts
@@ -1,14 +1,14 @@
import { TimeFormat } from '@/localization/constants/TimeFormat';
import { isDefined } from '~/utils/isDefined';
-export const detectTimeFormat = () => {
+export const detectTimeFormat = (): keyof typeof TimeFormat => {
const isHour12 = Intl.DateTimeFormat(navigator.language, {
hour: 'numeric',
}).resolvedOptions().hour12;
if (isDefined(isHour12) && isHour12) {
- return TimeFormat.HOUR_12;
+ return 'HOUR_12';
}
- return TimeFormat.HOUR_24;
+ return 'HOUR_24';
};
diff --git a/packages/twenty-front/src/modules/localization/utils/formatDateISOStringToDateTimeSimplified.ts b/packages/twenty-front/src/modules/localization/utils/formatDateISOStringToDateTimeSimplified.ts
new file mode 100644
index 000000000..c96d9f2f8
--- /dev/null
+++ b/packages/twenty-front/src/modules/localization/utils/formatDateISOStringToDateTimeSimplified.ts
@@ -0,0 +1,18 @@
+import { DATE_FORMAT_WITHOUT_YEAR } from '@/localization/constants/DateFormatWithoutYear';
+import { TimeFormat } from '@/localization/constants/TimeFormat';
+import { detectDateFormat } from '@/localization/utils/detectDateFormat';
+import { formatInTimeZone } from 'date-fns-tz';
+
+export const formatDateISOStringToDateTimeSimplified = (
+ date: Date,
+ timeZone: string,
+ timeFormat: TimeFormat,
+) => {
+ const simplifiedDateFormat = DATE_FORMAT_WITHOUT_YEAR[detectDateFormat()];
+
+ return formatInTimeZone(
+ date,
+ timeZone,
+ `${simplifiedDateFormat} · ${timeFormat}`,
+ );
+};
diff --git a/packages/twenty-front/src/modules/localization/utils/getDateFormatFromWorkspaceDateFormat.ts b/packages/twenty-front/src/modules/localization/utils/getDateFormatFromWorkspaceDateFormat.ts
index f32bdbb93..09293fbb8 100644
--- a/packages/twenty-front/src/modules/localization/utils/getDateFormatFromWorkspaceDateFormat.ts
+++ b/packages/twenty-front/src/modules/localization/utils/getDateFormatFromWorkspaceDateFormat.ts
@@ -7,7 +7,7 @@ export const getDateFormatFromWorkspaceDateFormat = (
) => {
switch (workspaceDateFormat) {
case WorkspaceMemberDateFormatEnum.System:
- return detectDateFormat();
+ return DateFormat[detectDateFormat()];
case WorkspaceMemberDateFormatEnum.MonthFirst:
return DateFormat.MONTH_FIRST;
case WorkspaceMemberDateFormatEnum.DayFirst:
diff --git a/packages/twenty-front/src/modules/localization/utils/getTimeFormatFromWorkspaceTimeFormat.ts b/packages/twenty-front/src/modules/localization/utils/getTimeFormatFromWorkspaceTimeFormat.ts
index f6aebb437..7519d0cb4 100644
--- a/packages/twenty-front/src/modules/localization/utils/getTimeFormatFromWorkspaceTimeFormat.ts
+++ b/packages/twenty-front/src/modules/localization/utils/getTimeFormatFromWorkspaceTimeFormat.ts
@@ -7,7 +7,7 @@ export const getTimeFormatFromWorkspaceTimeFormat = (
) => {
switch (workspaceTimeFormat) {
case WorkspaceMemberTimeFormatEnum.System:
- return detectTimeFormat();
+ return TimeFormat[detectTimeFormat()];
case WorkspaceMemberTimeFormatEnum.Hour_24:
return TimeFormat.HOUR_24;
case WorkspaceMemberTimeFormatEnum.Hour_12:
diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip.tsx b/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip.tsx
new file mode 100644
index 000000000..40925c5d3
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip.tsx
@@ -0,0 +1,89 @@
+import { formatDateISOStringToDateTimeSimplified } from '@/localization/utils/formatDateISOStringToDateTimeSimplified';
+import { UserContext } from '@/users/contexts/UserContext';
+import styled from '@emotion/styled';
+import { Point } from '@nivo/line';
+import { ReactElement, useContext } from 'react';
+
+const StyledTooltipContainer = styled.div`
+ align-items: center;
+ border-radius: ${({ theme }) => theme.border.radius.md};
+ border: 1px solid ${({ theme }) => theme.border.color.medium};
+ display: flex;
+ width: 128px;
+ flex-direction: column;
+ justify-content: center;
+ background: ${({ theme }) => theme.background.transparent.secondary};
+ box-shadow: ${({ theme }) => theme.boxShadow.light};
+ backdrop-filter: ${({ theme }) => theme.blur.medium};
+`;
+
+const StyledTooltipDateContainer = styled.div`
+ align-items: flex-start;
+ align-self: stretch;
+ display: flex;
+ justify-content: center;
+ font-weight: ${({ theme }) => theme.font.weight.medium};
+ font-family: ${({ theme }) => theme.font.family};
+ gap: ${({ theme }) => theme.spacing(2)};
+ color: ${({ theme }) => theme.font.color.secondary};
+ padding: ${({ theme }) => theme.spacing(2)};
+`;
+
+const StyledTooltipDataRow = styled.div`
+ align-items: flex-start;
+ align-self: stretch;
+ display: flex;
+ justify-content: space-between;
+ color: ${({ theme }) => theme.font.color.tertiary};
+ padding: ${({ theme }) => theme.spacing(2)};
+`;
+
+const StyledLine = styled.div`
+ background-color: ${({ theme }) => theme.border.color.medium};
+ height: 1px;
+ width: 100%;
+`;
+const StyledColorPoint = styled.div<{ color: string }>`
+ background-color: ${({ color }) => color};
+ border-radius: 50%;
+ height: 8px;
+ width: 8px;
+ display: inline-block;
+`;
+const StyledDataDefinition = styled.div`
+ display: flex;
+ align-items: center;
+ gap: ${({ theme }) => theme.spacing(2)};
+`;
+const StyledSpan = styled.span`
+ color: ${({ theme }) => theme.font.color.primary};
+`;
+type SettingsDevelopersWebhookTooltipProps = {
+ point: Point;
+};
+export const SettingsDevelopersWebhookTooltip = ({
+ point,
+}: SettingsDevelopersWebhookTooltipProps): ReactElement => {
+ const { timeFormat, timeZone } = useContext(UserContext);
+ const windowInterval = new Date(point.data.x);
+ const windowIntervalDate = formatDateISOStringToDateTimeSimplified(
+ windowInterval,
+ timeZone,
+ timeFormat,
+ );
+ return (
+
+
+ {windowIntervalDate}
+
+
+
+
+
+ {String(point.serieId)}
+
+ {String(point.data.y)}
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph.tsx b/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph.tsx
index eb2e359ff..9626c6712 100644
--- a/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph.tsx
+++ b/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph.tsx
@@ -1,8 +1,13 @@
+import { SettingsDevelopersWebhookTooltip } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip';
+import { useGraphData } from '@/settings/developers/webhook/hooks/useGraphData';
import { webhookGraphDataState } from '@/settings/developers/webhook/states/webhookGraphDataState';
+import { Select } from '@/ui/input/components/Select';
+import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { ResponsiveLine } from '@nivo/line';
import { Section } from '@react-email/components';
-import { useRecoilValue } from 'recoil';
+import { useState } from 'react';
+import { useRecoilValue, useSetRecoilState } from 'recoil';
import { H2Title } from 'twenty-ui';
export type NivoLineInput = {
@@ -14,22 +19,102 @@ export type NivoLineInput = {
}>;
};
const StyledGraphContainer = styled.div`
- height: 200px;
- width: 100%;
+ background-color: ${({ theme }) => theme.background.secondary};
+ border: 1px solid ${({ theme }) => theme.border.color.medium};
+ border-radius: ${({ theme }) => theme.border.radius.md};
+ height: 199px;
+
+ padding: ${({ theme }) => theme.spacing(4, 2, 2, 2)};
+ width: 496px;
`;
-export const SettingsDeveloppersWebhookUsageGraph = () => {
+const StyledTitleContainer = styled.div`
+ align-items: flex-start;
+ display: flex;
+ justify-content: space-between;
+`;
+
+type SettingsDevelopersWebhookUsageGraphProps = {
+ webhookId: string;
+};
+
+export const SettingsDevelopersWebhookUsageGraph = ({
+ webhookId,
+}: SettingsDevelopersWebhookUsageGraphProps) => {
const webhookGraphData = useRecoilValue(webhookGraphDataState);
+ const setWebhookGraphData = useSetRecoilState(webhookGraphDataState);
+ const theme = useTheme();
+
+ const [windowLengthGraphOption, setWindowLengthGraphOption] = useState<
+ '7D' | '1D' | '12H' | '4H'
+ >('7D');
+
+ const { fetchGraphData } = useGraphData(webhookId);
return (
<>
{webhookGraphData.length ? (
-
+
+
+
+
d.color}
- margin={{ top: 0, right: 0, bottom: 50, left: 60 }}
+ theme={{
+ text: {
+ fill: theme.font.color.light,
+ fontSize: theme.font.size.sm,
+ fontFamily: theme.font.family,
+ },
+ axis: {
+ domain: {
+ line: {
+ stroke: theme.border.color.light,
+ },
+ },
+ ticks: {
+ line: {
+ stroke: theme.border.color.light,
+ },
+ },
+ },
+ grid: {
+ line: {
+ stroke: theme.border.color.light,
+ },
+ },
+
+ crosshair: {
+ line: {
+ stroke: theme.font.color.light,
+ strokeDasharray: '2 2',
+ },
+ },
+ }}
+ margin={{ top: 20, right: 0, bottom: 30, left: 30 }}
xFormat="time:%Y-%m-%d %H:%M%"
xScale={{
type: 'time',
@@ -37,17 +122,54 @@ export const SettingsDeveloppersWebhookUsageGraph = () => {
format: '%Y-%m-%d %H:%M:%S',
precision: 'hour',
}}
+ defs={[
+ {
+ colors: [
+ {
+ color: 'inherit',
+ offset: 0,
+ },
+ {
+ color: 'inherit',
+ offset: 100,
+ opacity: 0,
+ },
+ ],
+ id: 'gradientGraph',
+ type: 'linearGradient',
+ },
+ ]}
+ fill={[
+ {
+ id: 'gradientGraph',
+ match: '*',
+ },
+ ]}
yScale={{
type: 'linear',
}}
axisBottom={{
- tickValues: 'every day',
- format: '%b %d',
+ format: '%b %d, %I:%M %p',
+ tickValues: 2,
+ tickPadding: 5,
+ tickSize: 6,
+ }}
+ axisLeft={{
+ tickPadding: 5,
+ tickSize: 6,
+ tickValues: 4,
}}
- enableTouchCrosshair={true}
- enableGridY={false}
enableGridX={false}
+ lineWidth={1}
+ gridYValues={4}
enablePoints={false}
+ isInteractive={true}
+ useMesh={true}
+ enableSlices={false}
+ enableCrosshair={false}
+ tooltip={({ point }) => (
+
+ )}
/>
diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraphEffect.tsx b/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraphEffect.tsx
index 0c2635024..6d4fd06dc 100644
--- a/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraphEffect.tsx
+++ b/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraphEffect.tsx
@@ -1,7 +1,5 @@
-import { NivoLineInput } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph';
+import { useGraphData } from '@/settings/developers/webhook/hooks/useGraphData';
import { webhookGraphDataState } from '@/settings/developers/webhook/states/webhookGraphDataState';
-import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
-import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useEffect } from 'react';
import { useSetRecoilState } from 'recoil';
@@ -14,88 +12,13 @@ export const SettingsDevelopersWebhookUsageGraphEffect = ({
}: SettingsDevelopersWebhookUsageGraphEffectProps) => {
const setWebhookGraphData = useSetRecoilState(webhookGraphDataState);
- const { enqueueSnackBar } = useSnackBar();
+ const { fetchGraphData } = useGraphData(webhookId);
useEffect(() => {
- const fetchData = async () => {
- try {
- const queryString = new URLSearchParams({
- webhookIdRequest: webhookId,
- }).toString();
- const token = 'REPLACE_ME';
- const response = await fetch(
- `https://api.eu-central-1.aws.tinybird.co/v0/pipes/getWebhooksAnalytics.json?${queryString}`,
- {
- headers: {
- Authorization: 'Bearer ' + token,
- },
- },
- );
- const result = await response.json();
+ fetchGraphData('7D').then((graphInput) => {
+ setWebhookGraphData(graphInput);
+ });
+ }, [fetchGraphData, setWebhookGraphData, webhookId]);
- if (!response.ok) {
- enqueueSnackBar('Something went wrong while fetching webhook usage', {
- variant: SnackBarVariant.Error,
- });
- return;
- }
-
- const graphInput = result.data
- .flatMap(
- (dataRow: {
- start_interval: string;
- failure_count: number;
- success_count: number;
- }) => [
- {
- x: dataRow.start_interval,
- y: dataRow.failure_count,
- id: 'failure_count',
- color: 'red',
- },
- {
- x: dataRow.start_interval,
- y: dataRow.success_count,
- id: 'success_count',
- color: 'green',
- },
- ],
- )
- .reduce(
- (
- acc: NivoLineInput[],
- {
- id,
- x,
- y,
- color,
- }: { id: string; x: string; y: number; color: string },
- ) => {
- const existingGroupIndex = acc.findIndex(
- (group) => group.id === id,
- );
- const isExistingGroup = existingGroupIndex !== -1;
-
- if (isExistingGroup) {
- return acc.map((group, index) =>
- index === existingGroupIndex
- ? { ...group, data: [...group.data, { x, y }] }
- : group,
- );
- } else {
- return [...acc, { id, color, data: [{ x, y }] }];
- }
- },
- [],
- );
- setWebhookGraphData(graphInput);
- } catch (error) {
- enqueueSnackBar('Something went wrong while fetching webhook usage', {
- variant: SnackBarVariant.Error,
- });
- }
- };
- fetchData();
- }, [enqueueSnackBar, setWebhookGraphData, webhookId]);
return <>>;
};
diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/constants/WebhookGraphApiOptionsMap.ts b/packages/twenty-front/src/modules/settings/developers/webhook/constants/WebhookGraphApiOptionsMap.ts
new file mode 100644
index 000000000..39ef67347
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/developers/webhook/constants/WebhookGraphApiOptionsMap.ts
@@ -0,0 +1,6 @@
+export const WEBHOOK_GRAPH_API_OPTIONS_MAP = {
+ '7D': { windowInHours: '168', tickIntervalInMinutes: '420' },
+ '1D': { windowInHours: '24', tickIntervalInMinutes: '60' },
+ '12H': { windowInHours: '12', tickIntervalInMinutes: '30' },
+ '4H': { windowInHours: '4', tickIntervalInMinutes: '10' },
+};
diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/hooks/useGraphData.tsx b/packages/twenty-front/src/modules/settings/developers/webhook/hooks/useGraphData.tsx
new file mode 100644
index 000000000..62fc6d4ad
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/developers/webhook/hooks/useGraphData.tsx
@@ -0,0 +1,23 @@
+import { fetchGraphDataOrThrow } from '@/settings/developers/webhook/utils/fetchGraphDataOrThrow';
+import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
+import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
+
+export const useGraphData = (webhookId: string) => {
+ const { enqueueSnackBar } = useSnackBar();
+ const fetchGraphData = async (
+ windowLengthGraphOption: '7D' | '1D' | '12H' | '4H',
+ ) => {
+ try {
+ return await fetchGraphDataOrThrow({
+ webhookId,
+ windowLength: windowLengthGraphOption,
+ });
+ } catch (error) {
+ enqueueSnackBar('Something went wrong while fetching webhook usage', {
+ variant: SnackBarVariant.Error,
+ });
+ return [];
+ }
+ };
+ return { fetchGraphData };
+};
diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/utils/__tests__/fetchGraphDataOrThrow.test.js b/packages/twenty-front/src/modules/settings/developers/webhook/utils/__tests__/fetchGraphDataOrThrow.test.js
new file mode 100644
index 000000000..365c964d2
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/developers/webhook/utils/__tests__/fetchGraphDataOrThrow.test.js
@@ -0,0 +1,115 @@
+import { WEBHOOK_GRAPH_API_OPTIONS_MAP } from '@/settings/developers/webhook/constants/WebhookGraphApiOptionsMap';
+import { fetchGraphDataOrThrow } from '@/settings/developers/webhook/utils/fetchGraphDataOrThrow';
+
+// Mock the global fetch function
+global.fetch = jest.fn();
+
+describe('fetchGraphDataOrThrow', () => {
+ const mockWebhookId = 'test-webhook-id';
+ const mockWindowLength = '7D';
+
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it('should fetch and transform data successfully', async () => {
+ const mockResponse = {
+ ok: true,
+ json: jest.fn().mockResolvedValue({
+ data: [
+ { start_interval: '2023-05-01', failure_count: 2, success_count: 8 },
+ { start_interval: '2023-05-02', failure_count: 1, success_count: 9 },
+ ],
+ }),
+ };
+ global.fetch.mockResolvedValue(mockResponse);
+
+ const result = await fetchGraphDataOrThrow({
+ webhookId: mockWebhookId,
+ windowLength: mockWindowLength,
+ });
+
+ expect(global.fetch).toHaveBeenCalledWith(
+ expect.stringContaining(
+ `https://api.eu-central-1.aws.tinybird.co/v0/pipes/getWebhooksAnalyticsV2.json?`,
+ ),
+ expect.objectContaining({
+ headers: {
+ Authorization: expect.stringContaining('Bearer '),
+ },
+ }),
+ );
+
+ expect(result).toEqual([
+ {
+ id: 'Failed',
+ color: 'red',
+ data: [
+ { x: '2023-05-01', y: 2 },
+ { x: '2023-05-02', y: 1 },
+ ],
+ },
+ {
+ id: 'Succeeded',
+ color: 'blue',
+ data: [
+ { x: '2023-05-01', y: 8 },
+ { x: '2023-05-02', y: 9 },
+ ],
+ },
+ ]);
+ });
+
+ it('should throw an error when the response is not ok', async () => {
+ const mockResponse = {
+ ok: false,
+ json: jest.fn().mockResolvedValue({ error: 'Some error' }),
+ };
+ global.fetch.mockResolvedValue(mockResponse);
+
+ await expect(
+ fetchGraphDataOrThrow({
+ webhookId: mockWebhookId,
+ windowLength: mockWindowLength,
+ }),
+ ).rejects.toThrow('Something went wrong while fetching webhook usage');
+ });
+
+ it('should use correct query parameters based on window length', async () => {
+ const mockResponse = {
+ ok: true,
+ json: jest.fn().mockResolvedValue({ data: [] }),
+ };
+ global.fetch.mockResolvedValue(mockResponse);
+
+ await fetchGraphDataOrThrow({
+ webhookId: mockWebhookId,
+ windowLength: '1D',
+ });
+
+ expect(global.fetch).toHaveBeenCalledWith(
+ expect.stringContaining(
+ new URLSearchParams({
+ ...WEBHOOK_GRAPH_API_OPTIONS_MAP['1D'],
+ webhookIdRequest: mockWebhookId,
+ }).toString(),
+ ),
+ expect.any(Object),
+ );
+ });
+
+ it('should handle empty response data', async () => {
+ const mockResponse = {
+ ok: true,
+ json: jest.fn().mockResolvedValue({ data: [] }),
+ };
+ global.fetch.mockResolvedValue(mockResponse);
+
+ const result = await fetchGraphDataOrThrow({
+ webhookId: mockWebhookId,
+ windowLength: mockWindowLength,
+ });
+
+ expect(result).toEqual([]);
+ });
+});
diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/utils/fetchGraphDataOrThrow.ts b/packages/twenty-front/src/modules/settings/developers/webhook/utils/fetchGraphDataOrThrow.ts
new file mode 100644
index 000000000..b7123f712
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/developers/webhook/utils/fetchGraphDataOrThrow.ts
@@ -0,0 +1,80 @@
+import { NivoLineInput } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph';
+import { WEBHOOK_GRAPH_API_OPTIONS_MAP } from '@/settings/developers/webhook/constants/WebhookGraphApiOptionsMap';
+
+type fetchGraphDataOrThrowProps = {
+ webhookId: string;
+ windowLength: '7D' | '1D' | '12H' | '4H';
+};
+
+export const fetchGraphDataOrThrow = async ({
+ webhookId,
+ windowLength,
+}: fetchGraphDataOrThrowProps) => {
+ const queryString = new URLSearchParams({
+ ...WEBHOOK_GRAPH_API_OPTIONS_MAP[windowLength],
+ webhookIdRequest: webhookId,
+ }).toString();
+ const token = 'REPLACE_ME';
+ const response = await fetch(
+ `https://api.eu-central-1.aws.tinybird.co/v0/pipes/getWebhooksAnalyticsV2.json?${queryString}`,
+ {
+ headers: {
+ Authorization: 'Bearer ' + token,
+ },
+ },
+ );
+ const result = await response.json();
+
+ if (!response.ok) {
+ throw new Error('Something went wrong while fetching webhook usage');
+ }
+ // Next steps: separate the map logic to a different component (response.data, {id:str, color:str}[])=>NivoLineInput[]
+
+ const graphInput = result.data
+ .flatMap(
+ (dataRow: {
+ start_interval: string;
+ failure_count: number;
+ success_count: number;
+ }) => [
+ {
+ x: dataRow.start_interval,
+ y: dataRow.failure_count,
+ id: 'Failed',
+ color: 'red', // need to refacto this
+ },
+ {
+ x: dataRow.start_interval,
+ y: dataRow.success_count,
+ id: 'Succeeded',
+ color: 'blue',
+ },
+ ],
+ )
+ .reduce(
+ (
+ acc: NivoLineInput[],
+ {
+ id,
+ x,
+ y,
+ color,
+ }: { id: string; x: string; y: number; color: string },
+ ) => {
+ const existingGroupIndex = acc.findIndex((group) => group.id === id);
+ const isExistingGroup = existingGroupIndex !== -1;
+
+ if (isExistingGroup) {
+ return acc.map((group, index) =>
+ index === existingGroupIndex
+ ? { ...group, data: [...group.data, { x, y }] }
+ : group,
+ );
+ } else {
+ return [...acc, { id, color, data: [{ x, y }] }];
+ }
+ },
+ [],
+ );
+ return graphInput;
+};
diff --git a/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx b/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx
index 4c51e6da4..c41b7ff7e 100644
--- a/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx
+++ b/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx
@@ -7,6 +7,8 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMembe
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadingState';
import { workspacesState } from '@/auth/states/workspaces';
+import { DateFormat } from '@/localization/constants/DateFormat';
+import { TimeFormat } from '@/localization/constants/TimeFormat';
import { dateTimeFormatState } from '@/localization/states/dateTimeFormatState';
import { detectDateFormat } from '@/localization/utils/detectDateFormat';
import { detectTimeFormat } from '@/localization/utils/detectTimeFormat';
@@ -81,10 +83,10 @@ export const UserProviderEffect = () => {
: detectTimeZone(),
dateFormat: isDefined(workspaceMember.dateFormat)
? getDateFormatFromWorkspaceDateFormat(workspaceMember.dateFormat)
- : detectDateFormat(),
+ : DateFormat[detectDateFormat()],
timeFormat: isDefined(workspaceMember.timeFormat)
? getTimeFormatFromWorkspaceTimeFormat(workspaceMember.timeFormat)
- : detectTimeFormat(),
+ : TimeFormat[detectTimeFormat()],
});
}
diff --git a/packages/twenty-front/src/pages/settings/developers/webhooks/components/SettingsDevelopersWebhookDetail.tsx b/packages/twenty-front/src/pages/settings/developers/webhooks/components/SettingsDevelopersWebhookDetail.tsx
index f88ba926c..006a4db6a 100644
--- a/packages/twenty-front/src/pages/settings/developers/webhooks/components/SettingsDevelopersWebhookDetail.tsx
+++ b/packages/twenty-front/src/pages/settings/developers/webhooks/components/SettingsDevelopersWebhookDetail.tsx
@@ -3,6 +3,7 @@ import { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { H2Title, IconTrash } from 'twenty-ui';
+import { isAnalyticsEnabledState } from '@/client-config/states/isAnalyticsEnabledState';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
@@ -11,7 +12,7 @@ import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { Webhook } from '@/settings/developers/types/webhook/Webhook';
-import { SettingsDeveloppersWebhookUsageGraph } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph';
+import { SettingsDevelopersWebhookUsageGraph } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph';
import { SettingsDevelopersWebhookUsageGraphEffect } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraphEffect';
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
import { SettingsPath } from '@/types/SettingsPath';
@@ -23,6 +24,7 @@ import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModa
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
import { Section } from '@/ui/layout/section/components/Section';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
+import { useRecoilValue } from 'recoil';
const StyledFilterRow = styled.div`
display: flex;
@@ -32,6 +34,8 @@ const StyledFilterRow = styled.div`
export const SettingsDevelopersWebhooksDetail = () => {
const { objectMetadataItems } = useObjectMetadataItems();
+ const isAnalyticsEnabled = useRecoilValue(isAnalyticsEnabledState);
+
const navigate = useNavigate();
const { webhookId = '' } = useParams();
@@ -178,10 +182,10 @@ export const SettingsDevelopersWebhooksDetail = () => {
/>
- {isAnalyticsV2Enabled ? (
+ {isAnalyticsEnabled && isAnalyticsV2Enabled ? (
<>
-
+
>
) : (
<>>
diff --git a/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettings.tsx b/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettings.tsx
index 6a2e58e52..c1c20f324 100644
--- a/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettings.tsx
+++ b/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettings.tsx
@@ -77,7 +77,7 @@ export const DateTimeSettings = () => {
);
dateTime[settingName] =
(value as DateFormat) === DateFormat.SYSTEM
- ? detectDateFormat()
+ ? DateFormat[detectDateFormat()]
: (value as DateFormat);
break;
}
@@ -87,7 +87,7 @@ export const DateTimeSettings = () => {
);
dateTime[settingName] =
(value as TimeFormat) === TimeFormat.SYSTEM
- ? detectTimeFormat()
+ ? TimeFormat[detectTimeFormat()]
: (value as TimeFormat);
break;
}
diff --git a/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsDateFormatSelect.tsx b/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsDateFormatSelect.tsx
index dcadd5ba9..295705187 100644
--- a/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsDateFormatSelect.tsx
+++ b/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsDateFormatSelect.tsx
@@ -20,7 +20,7 @@ export const DateTimeSettingsDateFormatSelect = ({
const usedTimeZone = timeZone === 'system' ? systemTimeZone : timeZone;
- const systemDateFormat = detectDateFormat();
+ const systemDateFormat = DateFormat[detectDateFormat()];
return (