From 8d1c4491b76831c4e998d7d87c9787bcc25c2025 Mon Sep 17 00:00:00 2001 From: amlannandy Date: Wed, 4 Dec 2024 21:12:28 +0530 Subject: [PATCH 01/24] feat: add functionality to export dashboard as json from listing page --- .../ListOfDashboard/DashboardsList.tsx | 28 +++++++++++++++++++ .../DashboardDescription/index.tsx | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/frontend/src/container/ListOfDashboard/DashboardsList.tsx b/frontend/src/container/ListOfDashboard/DashboardsList.tsx index 0a5b3b5130..d45ddaebdd 100644 --- a/frontend/src/container/ListOfDashboard/DashboardsList.tsx +++ b/frontend/src/container/ListOfDashboard/DashboardsList.tsx @@ -27,6 +27,8 @@ import { AxiosError } from 'axios'; import cx from 'classnames'; import { ENTITY_VERSION_V4 } from 'constants/app'; import ROUTES from 'constants/routes'; +import { sanitizeDashboardData } from 'container/NewDashboard/DashboardDescription'; +import { downloadObjectAsJson } from 'container/NewDashboard/DashboardDescription/utils'; import { Base64Icons } from 'container/NewDashboard/DashboardSettings/General/utils'; import dayjs from 'dayjs'; import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard'; @@ -44,6 +46,7 @@ import { EllipsisVertical, Expand, ExternalLink, + FileJson, Github, HdmiPort, LayoutGrid, @@ -450,6 +453,23 @@ function DashboardsList(): JSX.Element { }); }; + const handleJsonExport = (event: React.MouseEvent): void => { + event.stopPropagation(); + event.preventDefault(); + const selectedDashboardData = dashboards?.find( + (d) => d.uuid === dashboard.id, + ); + if (selectedDashboardData) { + downloadObjectAsJson( + sanitizeDashboardData({ + ...selectedDashboardData.data, + uuid: selectedDashboardData.uuid, + }), + dashboard.name, + ); + } + }; + return (
@@ -523,6 +543,14 @@ function DashboardsList(): JSX.Element { > Copy Link +
{ if (!selectedData?.variables) { From 715f8a2363e69f24b8e57da6d9040a13ef10c2c3 Mon Sep 17 00:00:00 2001 From: amlannandy Date: Thu, 5 Dec 2024 11:19:25 +0530 Subject: [PATCH 02/24] feat: address comments --- .../container/ListOfDashboard/DashboardsList.tsx | 14 +------------- .../NewDashboard/DashboardDescription/index.tsx | 2 +- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/frontend/src/container/ListOfDashboard/DashboardsList.tsx b/frontend/src/container/ListOfDashboard/DashboardsList.tsx index d45ddaebdd..afaa597139 100644 --- a/frontend/src/container/ListOfDashboard/DashboardsList.tsx +++ b/frontend/src/container/ListOfDashboard/DashboardsList.tsx @@ -27,7 +27,6 @@ import { AxiosError } from 'axios'; import cx from 'classnames'; import { ENTITY_VERSION_V4 } from 'constants/app'; import ROUTES from 'constants/routes'; -import { sanitizeDashboardData } from 'container/NewDashboard/DashboardDescription'; import { downloadObjectAsJson } from 'container/NewDashboard/DashboardDescription/utils'; import { Base64Icons } from 'container/NewDashboard/DashboardSettings/General/utils'; import dayjs from 'dayjs'; @@ -456,18 +455,7 @@ function DashboardsList(): JSX.Element { const handleJsonExport = (event: React.MouseEvent): void => { event.stopPropagation(); event.preventDefault(); - const selectedDashboardData = dashboards?.find( - (d) => d.uuid === dashboard.id, - ); - if (selectedDashboardData) { - downloadObjectAsJson( - sanitizeDashboardData({ - ...selectedDashboardData.data, - uuid: selectedDashboardData.uuid, - }), - dashboard.name, - ); - } + downloadObjectAsJson(dashboard, dashboard.name); }; return ( diff --git a/frontend/src/container/NewDashboard/DashboardDescription/index.tsx b/frontend/src/container/NewDashboard/DashboardDescription/index.tsx index 7845cf818e..151fba7609 100644 --- a/frontend/src/container/NewDashboard/DashboardDescription/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardDescription/index.tsx @@ -65,7 +65,7 @@ interface DashboardDescriptionProps { handle: FullScreenHandle; } -export function sanitizeDashboardData( +function sanitizeDashboardData( selectedData: DashboardData, ): Omit { if (!selectedData?.variables) { From b35b9757986972e9d4408d3a0928c013b0bc44b2 Mon Sep 17 00:00:00 2001 From: amlannandy Date: Fri, 6 Dec 2024 10:05:45 +0530 Subject: [PATCH 03/24] chore: address comments --- .../ListOfDashboard/DashboardsList.tsx | 24 +++++++++++++++++-- .../DashboardDescription/index.tsx | 2 +- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/frontend/src/container/ListOfDashboard/DashboardsList.tsx b/frontend/src/container/ListOfDashboard/DashboardsList.tsx index afaa597139..14718a562b 100644 --- a/frontend/src/container/ListOfDashboard/DashboardsList.tsx +++ b/frontend/src/container/ListOfDashboard/DashboardsList.tsx @@ -27,6 +27,7 @@ import { AxiosError } from 'axios'; import cx from 'classnames'; import { ENTITY_VERSION_V4 } from 'constants/app'; import ROUTES from 'constants/routes'; +import { sanitizeDashboardData } from 'container/NewDashboard/DashboardDescription'; import { downloadObjectAsJson } from 'container/NewDashboard/DashboardDescription/utils'; import { Base64Icons } from 'container/NewDashboard/DashboardSettings/General/utils'; import dayjs from 'dayjs'; @@ -68,12 +69,18 @@ import { useRef, useState, } from 'react'; +import { Layout } from 'react-grid-layout'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import { generatePath, Link } from 'react-router-dom'; import { useCopyToClipboard } from 'react-use'; import { AppState } from 'store/reducers'; -import { Dashboard } from 'types/api/dashboard/getAll'; +import { + Dashboard, + IDashboardVariable, + WidgetRow, + Widgets, +} from 'types/api/dashboard/getAll'; import AppReducer from 'types/reducer/app'; import { isCloudUser } from 'utils/app'; @@ -262,6 +269,11 @@ function DashboardsList(): JSX.Element { isLocked: !!e.isLocked || false, lastUpdatedBy: e.updated_by, image: e.data.image || Base64Icons[0], + variables: e.data.variables, + widgets: e.data.widgets, + layout: e.data.layout, + panelMap: e.data.panelMap, + version: e.data.version, refetchDashboardList, })) || []; @@ -455,7 +467,10 @@ function DashboardsList(): JSX.Element { const handleJsonExport = (event: React.MouseEvent): void => { event.stopPropagation(); event.preventDefault(); - downloadObjectAsJson(dashboard, dashboard.name); + downloadObjectAsJson( + sanitizeDashboardData({ ...dashboard, title: dashboard.name }), + dashboard.name, + ); }; return ( @@ -1121,6 +1136,11 @@ export interface Data { isLocked: boolean; id: string; image?: string; + widgets?: Array; + layout?: Layout[]; + panelMap?: Record; + variables: Record; + version?: string; } export default DashboardsList; diff --git a/frontend/src/container/NewDashboard/DashboardDescription/index.tsx b/frontend/src/container/NewDashboard/DashboardDescription/index.tsx index 151fba7609..7845cf818e 100644 --- a/frontend/src/container/NewDashboard/DashboardDescription/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardDescription/index.tsx @@ -65,7 +65,7 @@ interface DashboardDescriptionProps { handle: FullScreenHandle; } -function sanitizeDashboardData( +export function sanitizeDashboardData( selectedData: DashboardData, ): Omit { if (!selectedData?.variables) { From b499b103337dc25c8dce41eedf3aa871132d9a3f Mon Sep 17 00:00:00 2001 From: amlannandy Date: Sat, 7 Dec 2024 10:40:14 +0530 Subject: [PATCH 04/24] feat: add unit test --- .../ListOfDashboard/DashboardsList.tsx | 116 +++++++++--------- .../__tests__/DashboardListPage.test.tsx | 25 +++- 2 files changed, 83 insertions(+), 58 deletions(-) diff --git a/frontend/src/container/ListOfDashboard/DashboardsList.tsx b/frontend/src/container/ListOfDashboard/DashboardsList.tsx index 14718a562b..254a12b195 100644 --- a/frontend/src/container/ListOfDashboard/DashboardsList.tsx +++ b/frontend/src/container/ListOfDashboard/DashboardsList.tsx @@ -521,63 +521,65 @@ function DashboardsList(): JSX.Element {
{action && ( - -
- - - -
-
- -
-
- } - placement="bottomRight" - arrow={false} - rootClassName="dashboard-actions" - > - { - e.stopPropagation(); - e.preventDefault(); - }} - /> - +
+ +
+ + + +
+
+ +
+
+ } + placement="bottomRight" + arrow={false} + rootClassName="dashboard-actions" + > + { + e.stopPropagation(); + e.preventDefault(); + }} + /> + + )}
diff --git a/frontend/src/pages/DashboardsListPage/__tests__/DashboardListPage.test.tsx b/frontend/src/pages/DashboardsListPage/__tests__/DashboardListPage.test.tsx index 98bd40ef62..bcb166eeb0 100644 --- a/frontend/src/pages/DashboardsListPage/__tests__/DashboardListPage.test.tsx +++ b/frontend/src/pages/DashboardsListPage/__tests__/DashboardListPage.test.tsx @@ -1,7 +1,10 @@ /* eslint-disable sonarjs/no-duplicate-string */ import ROUTES from 'constants/routes'; import DashboardsList from 'container/ListOfDashboard'; -import { dashboardEmptyState } from 'mocks-server/__mockdata__/dashboards'; +import { + dashboardEmptyState, + dashboardSuccessResponse, +} from 'mocks-server/__mockdata__/dashboards'; import { server } from 'mocks-server/server'; import { rest } from 'msw'; import { DashboardProvider } from 'providers/Dashboard/Dashboard'; @@ -204,4 +207,24 @@ describe('dashboard list page', () => { ), ); }); + + it('ensure that the popover actions on each list item renders list of options', async () => { + const { getByText, getAllByTestId } = render( + + + + + , + ); + + await waitFor(() => { + const popovers = getAllByTestId('dashboard-action-popover'); + expect(popovers).toHaveLength(dashboardSuccessResponse.data.length); + fireEvent.click([...popovers[0].children][0]); + }); + + expect(getByText('View')).toBeInTheDocument(); + expect(getByText('Copy Link')).toBeInTheDocument(); + expect(getByText('Export JSON')).toBeInTheDocument(); + }); }); From 1b8213653a833418793fa98c1cc6ac3b77b95df8 Mon Sep 17 00:00:00 2001 From: amlannandy Date: Sat, 7 Dec 2024 11:08:57 +0530 Subject: [PATCH 05/24] feat: update tests --- .../ListOfDashboard/DashboardsList.tsx | 117 +++++++++--------- .../__tests__/DashboardListPage.test.tsx | 2 +- 2 files changed, 59 insertions(+), 60 deletions(-) diff --git a/frontend/src/container/ListOfDashboard/DashboardsList.tsx b/frontend/src/container/ListOfDashboard/DashboardsList.tsx index 254a12b195..760fee457d 100644 --- a/frontend/src/container/ListOfDashboard/DashboardsList.tsx +++ b/frontend/src/container/ListOfDashboard/DashboardsList.tsx @@ -521,65 +521,64 @@ function DashboardsList(): JSX.Element {
{action && ( -
- -
- - - -
-
- -
-
- } - placement="bottomRight" - arrow={false} - rootClassName="dashboard-actions" - > - { - e.stopPropagation(); - e.preventDefault(); - }} - /> - - + +
+ + + +
+
+ +
+ + } + placement="bottomRight" + arrow={false} + rootClassName="dashboard-actions" + > + { + e.stopPropagation(); + e.preventDefault(); + }} + /> +
)}
diff --git a/frontend/src/pages/DashboardsListPage/__tests__/DashboardListPage.test.tsx b/frontend/src/pages/DashboardsListPage/__tests__/DashboardListPage.test.tsx index bcb166eeb0..0fcdea102b 100644 --- a/frontend/src/pages/DashboardsListPage/__tests__/DashboardListPage.test.tsx +++ b/frontend/src/pages/DashboardsListPage/__tests__/DashboardListPage.test.tsx @@ -218,7 +218,7 @@ describe('dashboard list page', () => { ); await waitFor(() => { - const popovers = getAllByTestId('dashboard-action-popover'); + const popovers = getAllByTestId('dashboard-action-icon'); expect(popovers).toHaveLength(dashboardSuccessResponse.data.length); fireEvent.click([...popovers[0].children][0]); }); From 2508e6f9f17c77ee460e5d555a459b66d449edb6 Mon Sep 17 00:00:00 2001 From: amlannandy Date: Sat, 7 Dec 2024 11:22:10 +0530 Subject: [PATCH 06/24] feat: update unit test --- .../__tests__/DashboardListPage.test.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/DashboardsListPage/__tests__/DashboardListPage.test.tsx b/frontend/src/pages/DashboardsListPage/__tests__/DashboardListPage.test.tsx index 0fcdea102b..e8ab48f9c3 100644 --- a/frontend/src/pages/DashboardsListPage/__tests__/DashboardListPage.test.tsx +++ b/frontend/src/pages/DashboardsListPage/__tests__/DashboardListPage.test.tsx @@ -1,6 +1,7 @@ /* eslint-disable sonarjs/no-duplicate-string */ import ROUTES from 'constants/routes'; import DashboardsList from 'container/ListOfDashboard'; +import * as dashboardUtils from 'container/NewDashboard/DashboardDescription'; import { dashboardEmptyState, dashboardSuccessResponse, @@ -11,6 +12,10 @@ import { DashboardProvider } from 'providers/Dashboard/Dashboard'; import { MemoryRouter, useLocation } from 'react-router-dom'; import { fireEvent, render, waitFor } from 'tests/test-utils'; +jest.mock('container/NewDashboard/DashboardDescription', () => ({ + sanitizeDashboardData: jest.fn(), +})); + jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useLocation: jest.fn(), @@ -208,7 +213,7 @@ describe('dashboard list page', () => { ); }); - it('ensure that the popover actions on each list item renders list of options', async () => { + it('ensure that the popover action renders list of options and export JSON works correctly', async () => { const { getByText, getAllByTestId } = render( @@ -226,5 +231,8 @@ describe('dashboard list page', () => { expect(getByText('View')).toBeInTheDocument(); expect(getByText('Copy Link')).toBeInTheDocument(); expect(getByText('Export JSON')).toBeInTheDocument(); + + fireEvent.click(getByText('Export JSON')); + expect(dashboardUtils.sanitizeDashboardData).toHaveBeenCalled(); }); }); From d09c4d947eddf74c75b8e3e6dd3bc9fe9e4e11d4 Mon Sep 17 00:00:00 2001 From: amlannandy Date: Sat, 7 Dec 2024 11:24:58 +0530 Subject: [PATCH 07/24] feat: update unit test --- .../__tests__/DashboardListPage.test.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/frontend/src/pages/DashboardsListPage/__tests__/DashboardListPage.test.tsx b/frontend/src/pages/DashboardsListPage/__tests__/DashboardListPage.test.tsx index e8ab48f9c3..21cfd2ae65 100644 --- a/frontend/src/pages/DashboardsListPage/__tests__/DashboardListPage.test.tsx +++ b/frontend/src/pages/DashboardsListPage/__tests__/DashboardListPage.test.tsx @@ -213,7 +213,7 @@ describe('dashboard list page', () => { ); }); - it('ensure that the popover action renders list of options and export JSON works correctly', async () => { + it('ensure that the export JSON popover action works correctly', async () => { const { getByText, getAllByTestId } = render( @@ -228,11 +228,9 @@ describe('dashboard list page', () => { fireEvent.click([...popovers[0].children][0]); }); - expect(getByText('View')).toBeInTheDocument(); - expect(getByText('Copy Link')).toBeInTheDocument(); - expect(getByText('Export JSON')).toBeInTheDocument(); - - fireEvent.click(getByText('Export JSON')); + const exportJsonBtn = getByText('Export JSON'); + expect(exportJsonBtn).toBeInTheDocument(); + fireEvent.click(exportJsonBtn); expect(dashboardUtils.sanitizeDashboardData).toHaveBeenCalled(); }); }); From 8a3319cdf585a0446bf3ea5d1bdde86208e4f8d1 Mon Sep 17 00:00:00 2001 From: amlannandy Date: Tue, 10 Dec 2024 18:17:16 +0530 Subject: [PATCH 08/24] chore: address comments --- .../__tests__/DashboardListPage.test.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/frontend/src/pages/DashboardsListPage/__tests__/DashboardListPage.test.tsx b/frontend/src/pages/DashboardsListPage/__tests__/DashboardListPage.test.tsx index 21cfd2ae65..d075316422 100644 --- a/frontend/src/pages/DashboardsListPage/__tests__/DashboardListPage.test.tsx +++ b/frontend/src/pages/DashboardsListPage/__tests__/DashboardListPage.test.tsx @@ -214,13 +214,7 @@ describe('dashboard list page', () => { }); it('ensure that the export JSON popover action works correctly', async () => { - const { getByText, getAllByTestId } = render( - - - - - , - ); + const { getByText, getAllByTestId } = render(); await waitFor(() => { const popovers = getAllByTestId('dashboard-action-icon'); @@ -231,6 +225,13 @@ describe('dashboard list page', () => { const exportJsonBtn = getByText('Export JSON'); expect(exportJsonBtn).toBeInTheDocument(); fireEvent.click(exportJsonBtn); - expect(dashboardUtils.sanitizeDashboardData).toHaveBeenCalled(); + const firstDashboardData = dashboardSuccessResponse.data[0]; + expect(dashboardUtils.sanitizeDashboardData).toHaveBeenCalledWith( + expect.objectContaining({ + id: firstDashboardData.uuid, + title: firstDashboardData.data.title, + createdAt: firstDashboardData.created_at, + }), + ); }); }); From b333aa37759978950f12c7081899e972cafb17be Mon Sep 17 00:00:00 2001 From: Shaheer Kochai Date: Mon, 16 Dec 2024 10:27:20 +0430 Subject: [PATCH 09/24] Feat: Timezone picker feature (#6474) * feat: time picker hint and timezone picker UI with basic functionality + helper to get timezones * feat: add support for esc keypress to close the timezone picker * chore: add the selected timezone as url param and close timezone picker on select * fix: overall improvement + add searchIndex to timezone * feat: timezone preferences UI * chore: improve timezone utils * chore: change timezone item from div to button * feat: display timezone in timepicker input * chore: fix the typo * fix: don't focus on time picker when timezone is clicked * fix: fix the issue of timezone breaking for browser and utc timezones * fix: display the timezone in timepicker hint 'You are at' * feat: timezone basic functionality (#6492) * chore: change div to fragment + change type to any as the ESLint complains otherwise * chore: manage etc timezone filtering with an arg * chore: update timezone wrapper class name * fix: add timezone support to downloaded logs * feat: add current timezone to dashboard list and configure metadata modal * fix: add pencil icon next to timezone hint + change the copy to Current timezone * fix: properly handle the escape button behavior for timezone picker * chore: replace @vvo/tzdb with native Intl API for timezones * feat: lightmode for timezone picker and timezone adaptation components * fix: use normald tz in browser timezone * fix: timezone picker lightmode fixes * feat: display selected time range in 12 hour format * chore: remove unnecessary optional chaining * fix: fix the typo in css variable * chore: add em dash and change icon for timezone hint in date/time picker * chore: move pen line icon to the right of timezone offset * fix: fix the failing tests * feat: handle switching off the timezone adaptation --- .../CustomTimePicker.styles.scss | 68 +++++- .../CustomTimePicker/CustomTimePicker.tsx | 66 +++++- .../CustomTimePickerPopoverContent.tsx | 149 +++++++++---- .../CustomTimePicker/RangePickerModal.tsx | 16 +- .../TimezonePicker.styles.scss | 166 +++++++++++++++ .../CustomTimePicker/TimezonePicker.tsx | 201 ++++++++++++++++++ .../CustomTimePicker/timezoneUtils.ts | 152 +++++++++++++ frontend/src/components/Graph/index.tsx | 34 +++ frontend/src/components/Graph/utils.ts | 4 +- .../src/components/Logs/ListLogView/index.tsx | 16 +- .../src/components/Logs/RawLogView/index.tsx | 24 ++- .../Logs/TableView/useTableView.tsx | 21 +- .../ResizeTable/TableComponent/Time.tsx | 10 +- frontend/src/constants/localStorage.ts | 1 + .../shortcuts/TimezonePickerShortcuts.ts | 3 + .../AlertHistory/Timeline/Graph/Graph.tsx | 14 +- .../AlertHistory/Timeline/Table/Table.tsx | 4 + .../Timeline/Table/useTimelineTable.tsx | 11 +- frontend/src/container/AllError/index.tsx | 23 +- .../AnomalyAlertEvaluationView.tsx | 7 +- .../tooltipPlugin.ts | 4 +- frontend/src/container/ErrorDetails/index.tsx | 13 +- .../FormAlertRules/ChartPreview/index.tsx | 8 + .../src/container/GantChart/Span/index.tsx | 7 +- .../MultiIngestionSettings.tsx | 31 ++- .../src/container/Licenses/ListLicenses.tsx | 14 ++ .../ListOfDashboard/DashboardList.styles.scss | 3 +- .../ListOfDashboard/DashboardsList.tsx | 55 +---- .../InfraMetrics/NodeMetrics.tsx | 8 + .../InfraMetrics/PodMetrics.tsx | 16 +- .../TableView/TableViewActions.tsx | 66 +++--- .../src/container/LogsExplorerViews/index.tsx | 13 +- .../tests/LogsExplorerViews.test.tsx | 25 ++- .../LogsPanelTable/LogsPanelComponent.tsx | 8 +- .../src/container/LogsPanelTable/utils.tsx | 14 ++ .../TimezoneAdaptation.styles.scss | 96 +++++++++ .../TimezoneAdaptation/TimezoneAdaptation.tsx | 82 +++++++ frontend/src/container/MySettings/index.tsx | 3 + .../PanelWrapper/UplotPanelWrapper.tsx | 8 + .../Layouts/ChangeHistory/DeploymentTime.tsx | 10 +- .../tests/ChangeHistory.test.tsx | 19 +- .../Preview/components/LogsList/index.tsx | 9 +- .../TableComponents/index.tsx | 17 +- .../tests/PipelineListsView.test.tsx | 73 ++++--- .../TimeSeriesView/TimeSeriesView.tsx | 7 + .../TopNav/CustomDateTimeModal/index.tsx | 5 +- .../TopNav/DateTimeSelectionV2/index.tsx | 11 +- frontend/src/container/TraceDetail/index.tsx | 7 +- .../TracesExplorer/ListView/index.tsx | 10 +- .../TracesExplorer/ListView/utils.tsx | 10 +- .../TracesTableComponent.tsx | 8 +- .../FilteredTable/ExapandableRow.tsx | 7 +- .../TriggeredAlerts/NoFilterTable.tsx | 19 +- .../useTimezoneFormatter.ts | 103 +++++++++ frontend/src/index.tsx | 19 +- .../src/lib/uPlotLib/getUplotChartOptions.ts | 6 + .../src/lib/uPlotLib/plugins/tooltipPlugin.ts | 12 +- .../__tests__/LogsExplorer.test.tsx | 17 +- frontend/src/pages/SaveView/index.tsx | 30 +-- frontend/src/providers/Timezone.tsx | 115 ++++++++++ frontend/src/tests/test-utils.tsx | 5 +- 61 files changed, 1683 insertions(+), 300 deletions(-) create mode 100644 frontend/src/components/CustomTimePicker/TimezonePicker.styles.scss create mode 100644 frontend/src/components/CustomTimePicker/TimezonePicker.tsx create mode 100644 frontend/src/components/CustomTimePicker/timezoneUtils.ts create mode 100644 frontend/src/constants/shortcuts/TimezonePickerShortcuts.ts create mode 100644 frontend/src/container/MySettings/TimezoneAdaptation/TimezoneAdaptation.styles.scss create mode 100644 frontend/src/container/MySettings/TimezoneAdaptation/TimezoneAdaptation.tsx create mode 100644 frontend/src/hooks/useTimezoneFormatter/useTimezoneFormatter.ts create mode 100644 frontend/src/providers/Timezone.tsx diff --git a/frontend/src/components/CustomTimePicker/CustomTimePicker.styles.scss b/frontend/src/components/CustomTimePicker/CustomTimePicker.styles.scss index 14f80a9b93..022a193761 100644 --- a/frontend/src/components/CustomTimePicker/CustomTimePicker.styles.scss +++ b/frontend/src/components/CustomTimePicker/CustomTimePicker.styles.scss @@ -40,7 +40,7 @@ &.custom-time { input:not(:focus) { - min-width: 240px; + min-width: 280px; } } @@ -119,3 +119,69 @@ color: var(--bg-slate-400) !important; } } + +.date-time-popover__footer { + border-top: 1px solid var(--bg-ink-200); + padding: 8px 14px; + .timezone-container { + &, + .timezone { + font-family: Inter; + font-size: 12px; + line-height: 16px; + letter-spacing: -0.06px; + } + display: flex; + align-items: center; + color: var(--bg-vanilla-400); + gap: 6px; + .timezone { + display: flex; + align-items: center; + gap: 4px; + border-radius: 2px; + background: rgba(171, 189, 255, 0.04); + cursor: pointer; + padding: 0px 4px; + color: var(--bg-vanilla-100); + border: none; + } + } +} +.timezone-badge { + display: flex; + align-items: center; + justify-content: center; + padding: 0 4px; + border-radius: 2px; + background: rgba(171, 189, 255, 0.04); + color: var(--bg-vanilla-100); + font-size: 12px; + font-weight: 400; + line-height: 16px; + letter-spacing: -0.06px; + cursor: pointer; +} + +.lightMode { + .date-time-popover__footer { + border-color: var(--bg-vanilla-400); + } + .timezone-container { + color: var(--bg-ink-400); + &__clock-icon { + stroke: var(--bg-ink-400); + } + .timezone { + color: var(--bg-ink-100); + background: rgb(179 179 179 / 15%); + &__icon { + stroke: var(--bg-ink-100); + } + } + } + .timezone-badge { + color: var(--bg-ink-100); + background: rgb(179 179 179 / 15%); + } +} diff --git a/frontend/src/components/CustomTimePicker/CustomTimePicker.tsx b/frontend/src/components/CustomTimePicker/CustomTimePicker.tsx index 47ee89c880..6064b64a09 100644 --- a/frontend/src/components/CustomTimePicker/CustomTimePicker.tsx +++ b/frontend/src/components/CustomTimePicker/CustomTimePicker.tsx @@ -15,11 +15,14 @@ import { isValidTimeFormat } from 'lib/getMinMax'; import { defaultTo, isFunction, noop } from 'lodash-es'; import debounce from 'lodash-es/debounce'; import { CheckCircle, ChevronDown, Clock } from 'lucide-react'; +import { useTimezone } from 'providers/Timezone'; import { ChangeEvent, Dispatch, SetStateAction, + useCallback, useEffect, + useMemo, useState, } from 'react'; import { useLocation } from 'react-router-dom'; @@ -28,6 +31,8 @@ import { popupContainer } from 'utils/selectPopupContainer'; import CustomTimePickerPopoverContent from './CustomTimePickerPopoverContent'; const maxAllowedMinTimeInMonths = 6; +type ViewType = 'datetime' | 'timezone'; +const DEFAULT_VIEW: ViewType = 'datetime'; interface CustomTimePickerProps { onSelect: (value: string) => void; @@ -81,11 +86,42 @@ function CustomTimePicker({ const location = useLocation(); const [isInputFocused, setIsInputFocused] = useState(false); + const [activeView, setActiveView] = useState(DEFAULT_VIEW); + + const { timezone, browserTimezone } = useTimezone(); + const activeTimezoneOffset = timezone.offset; + const isTimezoneOverridden = useMemo( + () => timezone.offset !== browserTimezone.offset, + [timezone, browserTimezone], + ); + + const handleViewChange = useCallback( + (newView: 'timezone' | 'datetime'): void => { + if (activeView !== newView) { + setActiveView(newView); + } + setOpen(true); + }, + [activeView, setOpen], + ); + + const [isOpenedFromFooter, setIsOpenedFromFooter] = useState(false); + const getSelectedTimeRangeLabel = ( selectedTime: string, selectedTimeValue: string, ): string => { if (selectedTime === 'custom') { + // Convert the date range string to 12-hour format + const dates = selectedTimeValue.split(' - '); + if (dates.length === 2) { + const startDate = dayjs(dates[0], 'DD/MM/YYYY HH:mm'); + const endDate = dayjs(dates[1], 'DD/MM/YYYY HH:mm'); + + return `${startDate.format('DD/MM/YYYY hh:mm A')} - ${endDate.format( + 'DD/MM/YYYY hh:mm A', + )}`; + } return selectedTimeValue; } @@ -131,6 +167,7 @@ function CustomTimePicker({ setOpen(newOpen); if (!newOpen) { setCustomDTPickerVisible?.(false); + setActiveView('datetime'); } }; @@ -244,6 +281,7 @@ function CustomTimePicker({ const handleFocus = (): void => { setIsInputFocused(true); + setActiveView('datetime'); }; const handleBlur = (): void => { @@ -280,6 +318,10 @@ function CustomTimePicker({ handleGoLive={defaultTo(handleGoLive, noop)} options={items} selectedTime={selectedTime} + activeView={activeView} + setActiveView={setActiveView} + setIsOpenedFromFooter={setIsOpenedFromFooter} + isOpenedFromFooter={isOpenedFromFooter} /> ) : ( content @@ -316,12 +358,24 @@ function CustomTimePicker({ ) } suffix={ - { - setOpen(!open); - }} - /> + <> + {!!isTimezoneOverridden && activeTimezoneOffset && ( +
{ + e.stopPropagation(); + handleViewChange('timezone'); + setIsOpenedFromFooter(false); + }} + > + {activeTimezoneOffset} +
+ )} + handleViewChange('datetime')} + /> + } /> diff --git a/frontend/src/components/CustomTimePicker/CustomTimePickerPopoverContent.tsx b/frontend/src/components/CustomTimePicker/CustomTimePickerPopoverContent.tsx index 4a41bec4f5..a42bb6b478 100644 --- a/frontend/src/components/CustomTimePicker/CustomTimePickerPopoverContent.tsx +++ b/frontend/src/components/CustomTimePicker/CustomTimePickerPopoverContent.tsx @@ -1,5 +1,6 @@ import './CustomTimePicker.styles.scss'; +import { Color } from '@signozhq/design-tokens'; import { Button } from 'antd'; import cx from 'classnames'; import ROUTES from 'constants/routes'; @@ -9,10 +10,13 @@ import { Option, RelativeDurationSuggestionOptions, } from 'container/TopNav/DateTimeSelectionV2/config'; +import { Clock, PenLine } from 'lucide-react'; +import { useTimezone } from 'providers/Timezone'; import { Dispatch, SetStateAction, useMemo } from 'react'; import { useLocation } from 'react-router-dom'; import RangePickerModal from './RangePickerModal'; +import TimezonePicker from './TimezonePicker'; interface CustomTimePickerPopoverContentProps { options: any[]; @@ -26,8 +30,13 @@ interface CustomTimePickerPopoverContentProps { onSelectHandler: (label: string, value: string) => void; handleGoLive: () => void; selectedTime: string; + activeView: 'datetime' | 'timezone'; + setActiveView: Dispatch>; + isOpenedFromFooter: boolean; + setIsOpenedFromFooter: Dispatch>; } +// eslint-disable-next-line sonarjs/cognitive-complexity function CustomTimePickerPopoverContent({ options, setIsOpen, @@ -37,12 +46,18 @@ function CustomTimePickerPopoverContent({ onSelectHandler, handleGoLive, selectedTime, + activeView, + setActiveView, + isOpenedFromFooter, + setIsOpenedFromFooter, }: CustomTimePickerPopoverContentProps): JSX.Element { const { pathname } = useLocation(); const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [ pathname, ]); + const { timezone } = useTimezone(); + const activeTimezoneOffset = timezone.offset; function getTimeChips(options: Option[]): JSX.Element { return ( @@ -63,55 +78,99 @@ function CustomTimePickerPopoverContent({ ); } + const handleTimezoneHintClick = (): void => { + setActiveView('timezone'); + setIsOpenedFromFooter(true); + }; + + if (activeView === 'timezone') { + return ( +
+ +
+ ); + } + return ( -
-
- {isLogsExplorerPage && ( - - )} - {options.map((option) => ( - - ))} + <> +
+
+ {isLogsExplorerPage && ( + + )} + {options.map((option) => ( + + ))} +
+
+ {selectedTime === 'custom' || customDateTimeVisible ? ( + + ) : ( +
+
RELATIVE TIMES
+
{getTimeChips(RelativeDurationSuggestionOptions)}
+
+ )} +
-
- {selectedTime === 'custom' || customDateTimeVisible ? ( - +
+ - ) : ( -
-
RELATIVE TIMES
-
{getTimeChips(RelativeDurationSuggestionOptions)}
-
- )} + Current timezone +
+ +
-
+ ); } diff --git a/frontend/src/components/CustomTimePicker/RangePickerModal.tsx b/frontend/src/components/CustomTimePicker/RangePickerModal.tsx index 24ba0e2b01..862d63e922 100644 --- a/frontend/src/components/CustomTimePicker/RangePickerModal.tsx +++ b/frontend/src/components/CustomTimePicker/RangePickerModal.tsx @@ -3,7 +3,8 @@ import './RangePickerModal.styles.scss'; import { DatePicker } from 'antd'; import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal'; import { LexicalContext } from 'container/TopNav/DateTimeSelectionV2/config'; -import dayjs, { Dayjs } from 'dayjs'; +import dayjs from 'dayjs'; +import { useTimezone } from 'providers/Timezone'; import { Dispatch, SetStateAction } from 'react'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; @@ -31,7 +32,10 @@ function RangePickerModal(props: RangePickerModalProps): JSX.Element { (state) => state.globalTime, ); - const disabledDate = (current: Dayjs): boolean => { + // Using any type here because antd's DatePicker expects its own internal Dayjs type + // which conflicts with our project's Dayjs type that has additional plugins (tz, utc etc). + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types + const disabledDate = (current: any): boolean => { const currentDay = dayjs(current); return currentDay.isAfter(dayjs()); }; @@ -49,16 +53,22 @@ function RangePickerModal(props: RangePickerModalProps): JSX.Element { } onCustomDateHandler(date_time, LexicalContext.CUSTOM_DATE_PICKER); }; + + const { timezone } = useTimezone(); return (
diff --git a/frontend/src/components/CustomTimePicker/TimezonePicker.styles.scss b/frontend/src/components/CustomTimePicker/TimezonePicker.styles.scss new file mode 100644 index 0000000000..50ee1502ba --- /dev/null +++ b/frontend/src/components/CustomTimePicker/TimezonePicker.styles.scss @@ -0,0 +1,166 @@ +// Variables +$font-family: 'Inter'; +$item-spacing: 8px; + +:root { + --border-color: var(--bg-slate-400); +} + +.lightMode { + --border-color: var(--bg-vanilla-400); +} + +// Mixins +@mixin text-style-base { + font-family: $font-family; + font-style: normal; + font-weight: 400; +} + +@mixin flex-center { + display: flex; + align-items: center; +} + +.timezone-picker { + width: 532px; + color: var(--bg-vanilla-400); + font-family: $font-family; + + &__search { + @include flex-center; + justify-content: space-between; + padding: 12px 14px; + border-bottom: 1px solid var(--border-color); + } + + &__input-container { + @include flex-center; + gap: 6px; + width: -webkit-fill-available; + } + + &__input { + @include text-style-base; + width: 100%; + background: transparent; + border: none; + outline: none; + color: var(--bg-vanilla-100); + font-size: 14px; + line-height: 20px; + letter-spacing: -0.07px; + padding: 0; + &.ant-input:focus { + box-shadow: none; + } + + &::placeholder { + color: var(--bg-vanilla-400); + } + } + + &__esc-key { + @include text-style-base; + font-size: 8px; + color: var(--bg-vanilla-400); + letter-spacing: -0.04px; + border-radius: 2.286px; + border: 1.143px solid var(--bg-ink-200); + border-bottom-width: 2.286px; + background: var(--bg-ink-400); + padding: 0 1px; + } + + &__list { + max-height: 310px; + overflow-y: auto; + } + + &__item { + @include flex-center; + justify-content: space-between; + padding: 7.5px 6px 7.5px $item-spacing; + margin: 4px $item-spacing; + cursor: pointer; + background: transparent; + border: none; + width: -webkit-fill-available; + color: var(--bg-vanilla-400); + font-family: $font-family; + + &:hover, + &.selected { + border-radius: 2px; + background: rgba(171, 189, 255, 0.04); + color: var(--bg-vanilla-100); + } + + &.has-divider { + position: relative; + &::after { + content: ''; + position: absolute; + bottom: -2px; + left: -$item-spacing; + right: -$item-spacing; + border-bottom: 1px solid var(--border-color); + } + } + } + + &__name { + @include text-style-base; + font-size: 14px; + line-height: 20px; + letter-spacing: -0.07px; + } + + &__offset { + color: var(--bg-vanilla-100); + font-size: 12px; + line-height: 16px; + letter-spacing: -0.06px; + } +} + +.timezone-name-wrapper { + @include flex-center; + gap: 6px; + + &__selected-icon { + height: 15px; + width: 15px; + } +} + +.lightMode { + .timezone-picker { + &__search { + .search-icon { + stroke: var(--bg-ink-400); + } + } + &__input { + color: var(--bg-ink-100); + } + &__esc-key { + background-color: var(--bg-vanilla-100); + border-color: var(--bg-vanilla-400); + color: var(--bg-ink-400); + } + &__item { + color: var(--bg-ink-400); + } + &__offset { + color: var(--bg-ink-100); + } + } + .timezone-name-wrapper { + &__selected-icon { + .check-icon { + stroke: var(--bg-ink-100); + } + } + } +} diff --git a/frontend/src/components/CustomTimePicker/TimezonePicker.tsx b/frontend/src/components/CustomTimePicker/TimezonePicker.tsx new file mode 100644 index 0000000000..2f4da45837 --- /dev/null +++ b/frontend/src/components/CustomTimePicker/TimezonePicker.tsx @@ -0,0 +1,201 @@ +import './TimezonePicker.styles.scss'; + +import { Color } from '@signozhq/design-tokens'; +import { Input } from 'antd'; +import cx from 'classnames'; +import { TimezonePickerShortcuts } from 'constants/shortcuts/TimezonePickerShortcuts'; +import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys'; +import { Check, Search } from 'lucide-react'; +import { useTimezone } from 'providers/Timezone'; +import { + Dispatch, + SetStateAction, + useCallback, + useEffect, + useState, +} from 'react'; + +import { Timezone, TIMEZONE_DATA } from './timezoneUtils'; + +interface SearchBarProps { + value: string; + onChange: (value: string) => void; + setIsOpen: Dispatch>; + setActiveView: Dispatch>; + isOpenedFromFooter: boolean; +} + +interface TimezoneItemProps { + timezone: Timezone; + isSelected?: boolean; + onClick?: () => void; +} + +const ICON_SIZE = 14; + +function SearchBar({ + value, + onChange, + setIsOpen, + setActiveView, + isOpenedFromFooter = false, +}: SearchBarProps): JSX.Element { + const handleKeyDown = useCallback( + (e: React.KeyboardEvent): void => { + if (e.key === 'Escape') { + if (isOpenedFromFooter) { + setActiveView('datetime'); + } else { + setIsOpen(false); + } + } + }, + [setActiveView, setIsOpen, isOpenedFromFooter], + ); + + return ( +
+
+ + onChange(e.target.value)} + onKeyDown={handleKeyDown} + tabIndex={0} + autoFocus + /> +
+ esc +
+ ); +} + +function TimezoneItem({ + timezone, + isSelected = false, + onClick, +}: TimezoneItemProps): JSX.Element { + return ( + + ); +} + +TimezoneItem.defaultProps = { + isSelected: false, + onClick: undefined, +}; + +interface TimezonePickerProps { + setActiveView: Dispatch>; + setIsOpen: Dispatch>; + isOpenedFromFooter: boolean; +} + +function TimezonePicker({ + setActiveView, + setIsOpen, + isOpenedFromFooter, +}: TimezonePickerProps): JSX.Element { + console.log({ isOpenedFromFooter }); + const [searchTerm, setSearchTerm] = useState(''); + const { timezone, updateTimezone } = useTimezone(); + const [selectedTimezone, setSelectedTimezone] = useState( + timezone.name ?? TIMEZONE_DATA[0].name, + ); + + const getFilteredTimezones = useCallback((searchTerm: string): Timezone[] => { + const normalizedSearch = searchTerm.toLowerCase(); + return TIMEZONE_DATA.filter( + (tz) => + tz.name.toLowerCase().includes(normalizedSearch) || + tz.offset.toLowerCase().includes(normalizedSearch) || + tz.searchIndex.toLowerCase().includes(normalizedSearch), + ); + }, []); + + const handleCloseTimezonePicker = useCallback(() => { + if (isOpenedFromFooter) { + setActiveView('datetime'); + } else { + setIsOpen(false); + } + }, [isOpenedFromFooter, setActiveView, setIsOpen]); + + const handleTimezoneSelect = useCallback( + (timezone: Timezone) => { + setSelectedTimezone(timezone.name); + updateTimezone(timezone); + handleCloseTimezonePicker(); + setIsOpen(false); + }, + [handleCloseTimezonePicker, setIsOpen, updateTimezone], + ); + + // Register keyboard shortcuts + const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys(); + + useEffect(() => { + registerShortcut( + TimezonePickerShortcuts.CloseTimezonePicker, + handleCloseTimezonePicker, + ); + + return (): void => { + deregisterShortcut(TimezonePickerShortcuts.CloseTimezonePicker); + }; + }, [deregisterShortcut, handleCloseTimezonePicker, registerShortcut]); + + return ( +
+ +
+ {getFilteredTimezones(searchTerm).map((timezone) => ( + handleTimezoneSelect(timezone)} + /> + ))} +
+
+ ); +} + +export default TimezonePicker; diff --git a/frontend/src/components/CustomTimePicker/timezoneUtils.ts b/frontend/src/components/CustomTimePicker/timezoneUtils.ts new file mode 100644 index 0000000000..92da405ba4 --- /dev/null +++ b/frontend/src/components/CustomTimePicker/timezoneUtils.ts @@ -0,0 +1,152 @@ +import dayjs from 'dayjs'; +import timezone from 'dayjs/plugin/timezone'; +import utc from 'dayjs/plugin/utc'; + +dayjs.extend(utc); +dayjs.extend(timezone); + +export interface Timezone { + name: string; + value: string; + offset: string; + searchIndex: string; + hasDivider?: boolean; +} + +const TIMEZONE_TYPES = { + BROWSER: 'BROWSER', + UTC: 'UTC', + STANDARD: 'STANDARD', +} as const; + +type TimezoneType = typeof TIMEZONE_TYPES[keyof typeof TIMEZONE_TYPES]; + +export const UTC_TIMEZONE: Timezone = { + name: 'Coordinated Universal Time — UTC, GMT', + value: 'UTC', + offset: 'UTC', + searchIndex: 'UTC', + hasDivider: true, +}; + +const normalizeTimezoneName = (timezone: string): string => { + // https://github.com/tc39/proposal-temporal/issues/1076 + if (timezone === 'Asia/Calcutta') { + return 'Asia/Kolkata'; + } + return timezone; +}; + +const formatOffset = (offsetMinutes: number): string => { + if (offsetMinutes === 0) return 'UTC'; + + const hours = Math.floor(Math.abs(offsetMinutes) / 60); + const minutes = Math.abs(offsetMinutes) % 60; + const sign = offsetMinutes > 0 ? '+' : '-'; + + return `UTC ${sign} ${hours}${ + minutes ? `:${minutes.toString().padStart(2, '0')}` : ':00' + }`; +}; + +const createTimezoneEntry = ( + name: string, + offsetMinutes: number, + type: TimezoneType = TIMEZONE_TYPES.STANDARD, + hasDivider = false, +): Timezone => { + const offset = formatOffset(offsetMinutes); + let value = name; + let displayName = name; + + switch (type) { + case TIMEZONE_TYPES.BROWSER: + displayName = `Browser time — ${name}`; + value = name; + break; + case TIMEZONE_TYPES.UTC: + displayName = 'Coordinated Universal Time — UTC, GMT'; + value = 'UTC'; + break; + case TIMEZONE_TYPES.STANDARD: + displayName = name; + value = name; + break; + default: + console.error(`Invalid timezone type: ${type}`); + } + + return { + name: displayName, + value, + offset, + searchIndex: offset.replace(/ /g, ''), + ...(hasDivider && { hasDivider }), + }; +}; + +const getOffsetByTimezone = (timezone: string): number => { + const dayjsTimezone = dayjs().tz(timezone); + return dayjsTimezone.utcOffset(); +}; + +export const getBrowserTimezone = (): Timezone => { + const browserTz = dayjs.tz.guess(); + const normalizedTz = normalizeTimezoneName(browserTz); + const browserOffset = getOffsetByTimezone(normalizedTz); + return createTimezoneEntry( + normalizedTz, + browserOffset, + TIMEZONE_TYPES.BROWSER, + ); +}; + +const filterAndSortTimezones = ( + allTimezones: string[], + browserTzName?: string, + includeEtcTimezones = false, +): Timezone[] => + allTimezones + .filter((tz) => { + const isNotBrowserTz = tz !== browserTzName; + const isNotEtcTz = includeEtcTimezones || !tz.startsWith('Etc/'); + return isNotBrowserTz && isNotEtcTz; + }) + .sort((a, b) => a.localeCompare(b)) + .map((tz) => { + const normalizedTz = normalizeTimezoneName(tz); + const offset = getOffsetByTimezone(normalizedTz); + return createTimezoneEntry(normalizedTz, offset); + }); + +const generateTimezoneData = (includeEtcTimezones = false): Timezone[] => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const allTimezones = (Intl as any).supportedValuesOf('timeZone'); + const timezones: Timezone[] = []; + + // Add browser timezone + const browserTzObject = getBrowserTimezone(); + timezones.push(browserTzObject); + + // Add UTC timezone with divider + timezones.push(UTC_TIMEZONE); + + timezones.push( + ...filterAndSortTimezones( + allTimezones, + browserTzObject.value, + includeEtcTimezones, + ), + ); + + return timezones; +}; + +export const getTimezoneObjectByTimezoneString = ( + timezone: string, +): Timezone => { + const utcOffset = getOffsetByTimezone(timezone); + return createTimezoneEntry(timezone, utcOffset); +}; + +export const TIMEZONE_DATA = generateTimezoneData(); diff --git a/frontend/src/components/Graph/index.tsx b/frontend/src/components/Graph/index.tsx index 0065f6b33c..6358ebfe7a 100644 --- a/frontend/src/components/Graph/index.tsx +++ b/frontend/src/components/Graph/index.tsx @@ -1,4 +1,5 @@ import { + _adapters, BarController, BarElement, CategoryScale, @@ -18,8 +19,10 @@ import { } from 'chart.js'; import annotationPlugin from 'chartjs-plugin-annotation'; import { generateGridTitle } from 'container/GridPanelSwitch/utils'; +import dayjs from 'dayjs'; import { useIsDarkMode } from 'hooks/useDarkMode'; import isEqual from 'lodash-es/isEqual'; +import { useTimezone } from 'providers/Timezone'; import { forwardRef, memo, @@ -62,6 +65,17 @@ Chart.register( Tooltip.positioners.custom = TooltipPositionHandler; +// Map of Chart.js time formats to dayjs format strings +const formatMap = { + 'HH:mm:ss': 'HH:mm:ss', + 'HH:mm': 'HH:mm', + 'MM/DD HH:mm': 'MM/DD HH:mm', + 'MM/dd HH:mm': 'MM/DD HH:mm', + 'MM/DD': 'MM/DD', + 'YY-MM': 'YY-MM', + YY: 'YY', +}; + const Graph = forwardRef( ( { @@ -80,11 +94,13 @@ const Graph = forwardRef( dragSelectColor, }, ref, + // eslint-disable-next-line sonarjs/cognitive-complexity ): JSX.Element => { const nearestDatasetIndex = useRef(null); const chartRef = useRef(null); const isDarkMode = useIsDarkMode(); const gridTitle = useMemo(() => generateGridTitle(title), [title]); + const { timezone } = useTimezone(); const currentTheme = isDarkMode ? 'dark' : 'light'; const xAxisTimeUnit = useXAxisTimeUnit(data); // Computes the relevant time unit for x axis by analyzing the time stamp data @@ -112,6 +128,22 @@ const Graph = forwardRef( return 'rgba(231,233,237,0.8)'; }, [currentTheme]); + // Override Chart.js date adapter to use dayjs with timezone support + useEffect(() => { + _adapters._date.override({ + format(time: number | Date, fmt: string) { + const dayjsTime = dayjs(time).tz(timezone.value); + const format = formatMap[fmt as keyof typeof formatMap]; + if (!format) { + console.warn(`Missing datetime format for ${fmt}`); + return dayjsTime.format('YYYY-MM-DD HH:mm:ss'); // fallback format + } + + return dayjsTime.format(format); + }, + }); + }, [timezone]); + const buildChart = useCallback(() => { if (lineChartRef.current !== undefined) { lineChartRef.current.destroy(); @@ -132,6 +164,7 @@ const Graph = forwardRef( isStacked, onClickHandler, data, + timezone, ); const chartHasData = hasData(data); @@ -166,6 +199,7 @@ const Graph = forwardRef( isStacked, onClickHandler, data, + timezone, name, type, ]); diff --git a/frontend/src/components/Graph/utils.ts b/frontend/src/components/Graph/utils.ts index f002d1402f..603f387566 100644 --- a/frontend/src/components/Graph/utils.ts +++ b/frontend/src/components/Graph/utils.ts @@ -1,5 +1,6 @@ import { Chart, ChartConfiguration, ChartData, Color } from 'chart.js'; import * as chartjsAdapter from 'chartjs-adapter-date-fns'; +import { Timezone } from 'components/CustomTimePicker/timezoneUtils'; import dayjs from 'dayjs'; import { MutableRefObject } from 'react'; @@ -50,6 +51,7 @@ export const getGraphOptions = ( isStacked: boolean | undefined, onClickHandler: GraphOnClickHandler | undefined, data: ChartData, + timezone: Timezone, // eslint-disable-next-line sonarjs/cognitive-complexity ): CustomChartOptions => ({ animation: { @@ -97,7 +99,7 @@ export const getGraphOptions = ( callbacks: { title(context): string | string[] { const date = dayjs(context[0].parsed.x); - return date.format('MMM DD, YYYY, HH:mm:ss'); + return date.tz(timezone.value).format('MMM DD, YYYY, HH:mm:ss'); }, label(context): string | string[] { let label = context.dataset.label || ''; diff --git a/frontend/src/components/Logs/ListLogView/index.tsx b/frontend/src/components/Logs/ListLogView/index.tsx index 8d5c0118cd..1fdd7414dd 100644 --- a/frontend/src/components/Logs/ListLogView/index.tsx +++ b/frontend/src/components/Logs/ListLogView/index.tsx @@ -8,13 +8,13 @@ import LogDetail from 'components/LogDetail'; import { VIEW_TYPES } from 'components/LogDetail/constants'; import { unescapeString } from 'container/LogDetailedView/utils'; import { FontSize } from 'container/OptionsMenu/types'; -import dayjs from 'dayjs'; import dompurify from 'dompurify'; import { useActiveLog } from 'hooks/logs/useActiveLog'; import { useCopyLogLink } from 'hooks/logs/useCopyLogLink'; import { useIsDarkMode } from 'hooks/useDarkMode'; // utils import { FlatLogData } from 'lib/logs/flatLogData'; +import { useTimezone } from 'providers/Timezone'; import { useCallback, useMemo, useState } from 'react'; // interfaces import { IField } from 'types/api/logs/fields'; @@ -174,12 +174,20 @@ function ListLogView({ [selectedFields], ); + const { formatTimezoneAdjustedTimestamp } = useTimezone(); + const timestampValue = useMemo( () => typeof flattenLogData.timestamp === 'string' - ? dayjs(flattenLogData.timestamp).format('YYYY-MM-DD HH:mm:ss.SSS') - : dayjs(flattenLogData.timestamp / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS'), - [flattenLogData.timestamp], + ? formatTimezoneAdjustedTimestamp( + flattenLogData.timestamp, + 'YYYY-MM-DD HH:mm:ss.SSS', + ) + : formatTimezoneAdjustedTimestamp( + flattenLogData.timestamp / 1e6, + 'YYYY-MM-DD HH:mm:ss.SSS', + ), + [flattenLogData.timestamp, formatTimezoneAdjustedTimestamp], ); const logType = getLogIndicatorType(logData); diff --git a/frontend/src/components/Logs/RawLogView/index.tsx b/frontend/src/components/Logs/RawLogView/index.tsx index 46e6a63aba..222931ee6d 100644 --- a/frontend/src/components/Logs/RawLogView/index.tsx +++ b/frontend/src/components/Logs/RawLogView/index.tsx @@ -6,7 +6,6 @@ import LogDetail from 'components/LogDetail'; import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants'; import { unescapeString } from 'container/LogDetailedView/utils'; import LogsExplorerContext from 'container/LogsExplorerContext'; -import dayjs from 'dayjs'; import dompurify from 'dompurify'; import { useActiveLog } from 'hooks/logs/useActiveLog'; import { useCopyLogLink } from 'hooks/logs/useCopyLogLink'; @@ -14,6 +13,7 @@ import { useCopyLogLink } from 'hooks/logs/useCopyLogLink'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { FlatLogData } from 'lib/logs/flatLogData'; import { isEmpty, isNumber, isUndefined } from 'lodash-es'; +import { useTimezone } from 'providers/Timezone'; import { KeyboardEvent, MouseEvent, @@ -89,16 +89,24 @@ function RawLogView({ attributesText += ' | '; } + const { formatTimezoneAdjustedTimestamp } = useTimezone(); + const text = useMemo(() => { const date = typeof data.timestamp === 'string' - ? dayjs(data.timestamp) - : dayjs(data.timestamp / 1e6); - - return `${date.format('YYYY-MM-DD HH:mm:ss.SSS')} | ${attributesText} ${ - data.body - }`; - }, [data.timestamp, data.body, attributesText]); + ? formatTimezoneAdjustedTimestamp(data.timestamp, 'YYYY-MM-DD HH:mm:ss.SSS') + : formatTimezoneAdjustedTimestamp( + data.timestamp / 1e6, + 'YYYY-MM-DD HH:mm:ss.SSS', + ); + + return `${date} | ${attributesText} ${data.body}`; + }, [ + data.timestamp, + data.body, + attributesText, + formatTimezoneAdjustedTimestamp, + ]); const handleClickExpand = useCallback(() => { if (activeContextLog || isReadOnly) return; diff --git a/frontend/src/components/Logs/TableView/useTableView.tsx b/frontend/src/components/Logs/TableView/useTableView.tsx index 662686e67d..890b9e0500 100644 --- a/frontend/src/components/Logs/TableView/useTableView.tsx +++ b/frontend/src/components/Logs/TableView/useTableView.tsx @@ -5,10 +5,10 @@ import { Typography } from 'antd'; import { ColumnsType } from 'antd/es/table'; import cx from 'classnames'; import { unescapeString } from 'container/LogDetailedView/utils'; -import dayjs from 'dayjs'; import dompurify from 'dompurify'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { FlatLogData } from 'lib/logs/flatLogData'; +import { useTimezone } from 'providers/Timezone'; import { useMemo } from 'react'; import { FORBID_DOM_PURIFY_TAGS } from 'utils/app'; @@ -44,6 +44,8 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => { logs, ]); + const { formatTimezoneAdjustedTimestamp } = useTimezone(); + const columns: ColumnsType> = useMemo(() => { const fieldColumns: ColumnsType> = fields .filter((e) => e.name !== 'id') @@ -81,8 +83,11 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => { render: (field, item): ColumnTypeRender> => { const date = typeof field === 'string' - ? dayjs(field).format('YYYY-MM-DD HH:mm:ss.SSS') - : dayjs(field / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS'); + ? formatTimezoneAdjustedTimestamp(field, 'YYYY-MM-DD HH:mm:ss.SSS') + : formatTimezoneAdjustedTimestamp( + field / 1e6, + 'YYYY-MM-DD HH:mm:ss.SSS', + ); return { children: (
@@ -125,7 +130,15 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => { }, ...(appendTo === 'end' ? fieldColumns : []), ]; - }, [fields, isListViewPanel, appendTo, isDarkMode, linesPerRow, fontSize]); + }, [ + fields, + isListViewPanel, + appendTo, + isDarkMode, + linesPerRow, + fontSize, + formatTimezoneAdjustedTimestamp, + ]); return { columns, dataSource: flattenLogData }; }; diff --git a/frontend/src/components/ResizeTable/TableComponent/Time.tsx b/frontend/src/components/ResizeTable/TableComponent/Time.tsx index e579dbdfda..062dafcc2e 100644 --- a/frontend/src/components/ResizeTable/TableComponent/Time.tsx +++ b/frontend/src/components/ResizeTable/TableComponent/Time.tsx @@ -1,11 +1,13 @@ import { Typography } from 'antd'; -import convertDateToAmAndPm from 'lib/convertDateToAmAndPm'; -import getFormattedDate from 'lib/getFormatedDate'; +import { useTimezone } from 'providers/Timezone'; function Time({ CreatedOrUpdateTime }: DateProps): JSX.Element { + const { formatTimezoneAdjustedTimestamp } = useTimezone(); const time = new Date(CreatedOrUpdateTime); - const date = getFormattedDate(time); - const timeString = `${date} ${convertDateToAmAndPm(time)}`; + const timeString = formatTimezoneAdjustedTimestamp( + time, + 'MM/DD/YYYY hh:mm:ss A (UTC Z)', + ); return {timeString}; } diff --git a/frontend/src/constants/localStorage.ts b/frontend/src/constants/localStorage.ts index 4e6859a2dd..5284fc92ad 100644 --- a/frontend/src/constants/localStorage.ts +++ b/frontend/src/constants/localStorage.ts @@ -21,4 +21,5 @@ export enum LOCALSTORAGE { THEME_ANALYTICS_V1 = 'THEME_ANALYTICS_V1', LAST_USED_SAVED_VIEWS = 'LAST_USED_SAVED_VIEWS', SHOW_LOGS_QUICK_FILTERS = 'SHOW_LOGS_QUICK_FILTERS', + PREFERRED_TIMEZONE = 'PREFERRED_TIMEZONE', } diff --git a/frontend/src/constants/shortcuts/TimezonePickerShortcuts.ts b/frontend/src/constants/shortcuts/TimezonePickerShortcuts.ts new file mode 100644 index 0000000000..b7627e6e3d --- /dev/null +++ b/frontend/src/constants/shortcuts/TimezonePickerShortcuts.ts @@ -0,0 +1,3 @@ +export const TimezonePickerShortcuts = { + CloseTimezonePicker: 'escape', +}; diff --git a/frontend/src/container/AlertHistory/Timeline/Graph/Graph.tsx b/frontend/src/container/AlertHistory/Timeline/Graph/Graph.tsx index b3eecda24c..6a09298fd3 100644 --- a/frontend/src/container/AlertHistory/Timeline/Graph/Graph.tsx +++ b/frontend/src/container/AlertHistory/Timeline/Graph/Graph.tsx @@ -7,6 +7,7 @@ import useUrlQuery from 'hooks/useUrlQuery'; import history from 'lib/history'; import heatmapPlugin from 'lib/uPlotLib/plugins/heatmapPlugin'; import timelinePlugin from 'lib/uPlotLib/plugins/timelinePlugin'; +import { useTimezone } from 'providers/Timezone'; import { useMemo, useRef } from 'react'; import { useDispatch } from 'react-redux'; import { UpdateTimeInterval } from 'store/actions'; @@ -48,6 +49,7 @@ function HorizontalTimelineGraph({ const urlQuery = useUrlQuery(); const dispatch = useDispatch(); + const { timezone } = useTimezone(); const options: uPlot.Options = useMemo( () => ({ @@ -116,8 +118,18 @@ function HorizontalTimelineGraph({ }), ] : [], + + tzDate: (timestamp: number): Date => + uPlot.tzDate(new Date(timestamp * 1e3), timezone.value), }), - [width, isDarkMode, transformedData.length, urlQuery, dispatch], + [ + width, + isDarkMode, + transformedData.length, + urlQuery, + dispatch, + timezone.value, + ], ); return ; } diff --git a/frontend/src/container/AlertHistory/Timeline/Table/Table.tsx b/frontend/src/container/AlertHistory/Timeline/Table/Table.tsx index dffcf3fb7a..d910950547 100644 --- a/frontend/src/container/AlertHistory/Timeline/Table/Table.tsx +++ b/frontend/src/container/AlertHistory/Timeline/Table/Table.tsx @@ -7,6 +7,7 @@ import { useGetAlertRuleDetailsTimelineTable, useTimelineTable, } from 'pages/AlertDetails/hooks'; +import { useTimezone } from 'providers/Timezone'; import { HTMLAttributes, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { AlertRuleTimelineTableResponse } from 'types/api/alerts/def'; @@ -41,6 +42,8 @@ function TimelineTable(): JSX.Element { const { t } = useTranslation('common'); + const { formatTimezoneAdjustedTimestamp } = useTimezone(); + if (isError || !isValidRuleId || !ruleId) { return
{t('something_went_wrong')}
; } @@ -64,6 +67,7 @@ function TimelineTable(): JSX.Element { filters, labels: labels ?? {}, setFilters, + formatTimezoneAdjustedTimestamp, })} onRow={handleRowClick} dataSource={timelineData} diff --git a/frontend/src/container/AlertHistory/Timeline/Table/useTimelineTable.tsx b/frontend/src/container/AlertHistory/Timeline/Table/useTimelineTable.tsx index 5c67caa984..adccbedfa3 100644 --- a/frontend/src/container/AlertHistory/Timeline/Table/useTimelineTable.tsx +++ b/frontend/src/container/AlertHistory/Timeline/Table/useTimelineTable.tsx @@ -8,6 +8,7 @@ import ClientSideQBSearch, { import { ConditionalAlertPopover } from 'container/AlertHistory/AlertPopover/AlertPopover'; import { transformKeyValuesToAttributeValuesMap } from 'container/QueryBuilder/filters/utils'; import { useIsDarkMode } from 'hooks/useDarkMode'; +import { TimestampInput } from 'hooks/useTimezoneFormatter/useTimezoneFormatter'; import { Search } from 'lucide-react'; import AlertLabels, { AlertLabelsProps, @@ -16,7 +17,6 @@ import AlertState from 'pages/AlertDetails/AlertHeader/AlertState/AlertState'; import { useMemo } from 'react'; import { AlertRuleTimelineTableResponse } from 'types/api/alerts/def'; import { TagFilter } from 'types/api/queryBuilder/queryBuilderData'; -import { formatEpochTimestamp } from 'utils/timeUtils'; const transformLabelsToQbKeys = ( labels: AlertRuleTimelineTableResponse['labels'], @@ -74,10 +74,15 @@ export const timelineTableColumns = ({ filters, labels, setFilters, + formatTimezoneAdjustedTimestamp, }: { filters: TagFilter; labels: AlertLabelsProps['labels']; setFilters: (filters: TagFilter) => void; + formatTimezoneAdjustedTimestamp: ( + input: TimestampInput, + format?: string, + ) => string; }): ColumnsType => [ { title: 'STATE', @@ -106,7 +111,9 @@ export const timelineTableColumns = ({ dataIndex: 'unixMilli', width: 200, render: (value): JSX.Element => ( -
{formatEpochTimestamp(value)}
+
+ {formatTimezoneAdjustedTimestamp(value, 'MMM D, YYYY ⎯ HH:mm:ss')} +
), }, { diff --git a/frontend/src/container/AllError/index.tsx b/frontend/src/container/AllError/index.tsx index 0dd46c0a64..ce238c76b5 100644 --- a/frontend/src/container/AllError/index.tsx +++ b/frontend/src/container/AllError/index.tsx @@ -17,14 +17,15 @@ import getAll from 'api/errors/getAll'; import getErrorCounts from 'api/errors/getErrorCounts'; import { ResizeTable } from 'components/ResizeTable'; import ROUTES from 'constants/routes'; -import dayjs from 'dayjs'; import { useNotifications } from 'hooks/useNotifications'; import useResourceAttribute from 'hooks/useResourceAttribute'; import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils'; +import { TimestampInput } from 'hooks/useTimezoneFormatter/useTimezoneFormatter'; import useUrlQuery from 'hooks/useUrlQuery'; import createQueryParams from 'lib/createQueryParams'; import history from 'lib/history'; import { isUndefined } from 'lodash-es'; +import { useTimezone } from 'providers/Timezone'; import { useCallback, useEffect, useMemo, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useQueries } from 'react-query'; @@ -155,8 +156,16 @@ function AllErrors(): JSX.Element { } }, [data?.error, data?.payload, t, notifications]); - const getDateValue = (value: string): JSX.Element => ( - {dayjs(value).format('DD/MM/YYYY HH:mm:ss A')} + const getDateValue = ( + value: string, + formatTimezoneAdjustedTimestamp: ( + input: TimestampInput, + format?: string, + ) => string, + ): JSX.Element => ( + + {formatTimezoneAdjustedTimestamp(value, 'DD/MM/YYYY hh:mm:ss A')} + ); const filterIcon = useCallback(() => , []); @@ -283,6 +292,8 @@ function AllErrors(): JSX.Element { [filterIcon, filterDropdownWrapper], ); + const { formatTimezoneAdjustedTimestamp } = useTimezone(); + const columns: ColumnsType = [ { title: 'Exception Type', @@ -342,7 +353,8 @@ function AllErrors(): JSX.Element { dataIndex: 'lastSeen', width: 80, key: 'lastSeen', - render: getDateValue, + render: (value): JSX.Element => + getDateValue(value, formatTimezoneAdjustedTimestamp), sorter: true, defaultSortOrder: getDefaultOrder( getUpdatedParams, @@ -355,7 +367,8 @@ function AllErrors(): JSX.Element { dataIndex: 'firstSeen', width: 80, key: 'firstSeen', - render: getDateValue, + render: (value): JSX.Element => + getDateValue(value, formatTimezoneAdjustedTimestamp), sorter: true, defaultSortOrder: getDefaultOrder( getUpdatedParams, diff --git a/frontend/src/container/AnomalyAlertEvaluationView/AnomalyAlertEvaluationView.tsx b/frontend/src/container/AnomalyAlertEvaluationView/AnomalyAlertEvaluationView.tsx index 88eff524c6..90ecdbed0f 100644 --- a/frontend/src/container/AnomalyAlertEvaluationView/AnomalyAlertEvaluationView.tsx +++ b/frontend/src/container/AnomalyAlertEvaluationView/AnomalyAlertEvaluationView.tsx @@ -10,6 +10,7 @@ import getAxes from 'lib/uPlotLib/utils/getAxes'; import { getUplotChartDataForAnomalyDetection } from 'lib/uPlotLib/utils/getUplotChartData'; import { getYAxisScaleForAnomalyDetection } from 'lib/uPlotLib/utils/getYAxisScale'; import { LineChart } from 'lucide-react'; +import { useTimezone } from 'providers/Timezone'; import { useEffect, useRef, useState } from 'react'; import uPlot from 'uplot'; @@ -148,10 +149,12 @@ function AnomalyAlertEvaluationView({ ] : []; + const { timezone } = useTimezone(); + const options = { width: dimensions.width, height: dimensions.height - 36, - plugins: [bandsPlugin, tooltipPlugin(isDarkMode)], + plugins: [bandsPlugin, tooltipPlugin(isDarkMode, timezone.value)], focus: { alpha: 0.3, }, @@ -256,6 +259,8 @@ function AnomalyAlertEvaluationView({ show: true, }, axes: getAxes(isDarkMode, yAxisUnit), + tzDate: (timestamp: number): Date => + uPlot.tzDate(new Date(timestamp * 1e3), timezone.value), }; const handleSearch = (searchText: string): void => { diff --git a/frontend/src/container/AnomalyAlertEvaluationView/tooltipPlugin.ts b/frontend/src/container/AnomalyAlertEvaluationView/tooltipPlugin.ts index 6d32dbee35..c98515e371 100644 --- a/frontend/src/container/AnomalyAlertEvaluationView/tooltipPlugin.ts +++ b/frontend/src/container/AnomalyAlertEvaluationView/tooltipPlugin.ts @@ -1,8 +1,10 @@ import { themeColors } from 'constants/theme'; +import dayjs from 'dayjs'; import { generateColor } from 'lib/uPlotLib/utils/generateColor'; const tooltipPlugin = ( isDarkMode: boolean, + timezone: string, ): { hooks: { init: (u: any) => void } } => { let tooltip: HTMLDivElement; const tooltipLeftOffset = 10; @@ -17,7 +19,7 @@ const tooltipPlugin = ( return value.toFixed(3); } if (value instanceof Date) { - return value.toLocaleString(); + return dayjs(value).tz(timezone).format('MM/DD/YYYY, h:mm:ss A'); } if (value == null) { return 'N/A'; diff --git a/frontend/src/container/ErrorDetails/index.tsx b/frontend/src/container/ErrorDetails/index.tsx index c6b0d5fa22..7dbc57e4a5 100644 --- a/frontend/src/container/ErrorDetails/index.tsx +++ b/frontend/src/container/ErrorDetails/index.tsx @@ -6,12 +6,12 @@ import getNextPrevId from 'api/errors/getNextPrevId'; import Editor from 'components/Editor'; import { ResizeTable } from 'components/ResizeTable'; import { getNanoSeconds } from 'container/AllError/utils'; -import dayjs from 'dayjs'; import { useNotifications } from 'hooks/useNotifications'; import createQueryParams from 'lib/createQueryParams'; import history from 'lib/history'; import { isUndefined } from 'lodash-es'; import { urlKey } from 'pages/ErrorDetails/utils'; +import { useTimezone } from 'providers/Timezone'; import { useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useQuery } from 'react-query'; @@ -103,8 +103,6 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element { } }; - const timeStamp = dayjs(errorDetail.timestamp); - const data: { key: string; value: string }[] = Object.keys(errorDetail) .filter((e) => !keyToExclude.includes(e)) .map((key) => ({ @@ -136,6 +134,8 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element { // eslint-disable-next-line react-hooks/exhaustive-deps }, [data]); + const { formatTimezoneAdjustedTimestamp } = useTimezone(); + return ( <> {errorDetail.exceptionType} @@ -145,7 +145,12 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
Event {errorDetail.errorId} - {timeStamp.format('MMM DD YYYY hh:mm:ss A')} + + {formatTimezoneAdjustedTimestamp( + errorDetail.timestamp, + 'DD/MM/YYYY hh:mm:ss A (UTC Z)', + )} +
diff --git a/frontend/src/container/FormAlertRules/ChartPreview/index.tsx b/frontend/src/container/FormAlertRules/ChartPreview/index.tsx index af6550dedf..54a25e6565 100644 --- a/frontend/src/container/FormAlertRules/ChartPreview/index.tsx +++ b/frontend/src/container/FormAlertRules/ChartPreview/index.tsx @@ -25,6 +25,7 @@ import getTimeString from 'lib/getTimeString'; import history from 'lib/history'; import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions'; import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData'; +import { useTimezone } from 'providers/Timezone'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; @@ -35,6 +36,7 @@ import { AlertDef } from 'types/api/alerts/def'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { EQueryType } from 'types/common/dashboard'; import { GlobalReducer } from 'types/reducer/globalTime'; +import uPlot from 'uplot'; import { getGraphType } from 'utils/getGraphType'; import { getSortedSeriesData } from 'utils/getSortedSeriesData'; import { getTimeRange } from 'utils/getTimeRange'; @@ -201,6 +203,8 @@ function ChartPreview({ [dispatch, location.pathname, urlQuery], ); + const { timezone } = useTimezone(); + const options = useMemo( () => getUPlotChartOptions({ @@ -236,6 +240,9 @@ function ChartPreview({ softMax: null, softMin: null, panelType: graphType, + tzDate: (timestamp: number) => + uPlot.tzDate(new Date(timestamp * 1e3), timezone.value), + timezone: timezone.value, }), [ yAxisUnit, @@ -250,6 +257,7 @@ function ChartPreview({ optionName, alertDef?.condition.targetUnit, graphType, + timezone.value, ], ); diff --git a/frontend/src/container/GantChart/Span/index.tsx b/frontend/src/container/GantChart/Span/index.tsx index 23fc000ac3..03b2b805e4 100644 --- a/frontend/src/container/GantChart/Span/index.tsx +++ b/frontend/src/container/GantChart/Span/index.tsx @@ -4,6 +4,7 @@ import { Popover, Typography } from 'antd'; import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils'; import dayjs from 'dayjs'; import { useIsDarkMode } from 'hooks/useDarkMode'; +import { useTimezone } from 'providers/Timezone'; import { useEffect } from 'react'; import { toFixed } from 'utils/toFixed'; @@ -32,13 +33,17 @@ function Span(props: SpanLengthProps): JSX.Element { const isDarkMode = useIsDarkMode(); const { time, timeUnitName } = convertTimeToRelevantUnit(inMsCount); + const { timezone } = useTimezone(); + useEffect(() => { document.documentElement.scrollTop = document.documentElement.clientHeight; document.documentElement.scrollLeft = document.documentElement.clientWidth; }, []); const getContent = (): JSX.Element => { - const timeStamp = dayjs(startTime).format('h:mm:ss:SSS A'); + const timeStamp = dayjs(startTime) + .tz(timezone.value) + .format('h:mm:ss:SSS A (UTC Z)'); const startTimeInMs = startTime - globalStart; return (
diff --git a/frontend/src/container/IngestionSettings/MultiIngestionSettings.tsx b/frontend/src/container/IngestionSettings/MultiIngestionSettings.tsx index b6e744ccaf..aceb1c477a 100644 --- a/frontend/src/container/IngestionSettings/MultiIngestionSettings.tsx +++ b/frontend/src/container/IngestionSettings/MultiIngestionSettings.tsx @@ -31,7 +31,7 @@ import { AxiosError } from 'axios'; import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig'; import Tags from 'components/Tags/Tags'; import { SOMETHING_WENT_WRONG } from 'constants/api'; -import dayjs, { Dayjs } from 'dayjs'; +import dayjs from 'dayjs'; import { useGetAllIngestionsKeys } from 'hooks/IngestionKeys/useGetAllIngestionKeys'; import useDebouncedFn from 'hooks/useDebouncedFunction'; import { useNotifications } from 'hooks/useNotifications'; @@ -51,6 +51,7 @@ import { Trash2, X, } from 'lucide-react'; +import { useTimezone } from 'providers/Timezone'; import { ChangeEvent, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useMutation } from 'react-query'; @@ -70,7 +71,10 @@ const { Option } = Select; const BYTES = 1073741824; -export const disabledDate = (current: Dayjs): boolean => +// Using any type here because antd's DatePicker expects its own internal Dayjs type +// which conflicts with our project's Dayjs type that has additional plugins (tz, utc etc). +// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types +export const disabledDate = (current: any): boolean => // Disable all dates before today current && current < dayjs().endOf('day'); @@ -393,8 +397,11 @@ function MultiIngestionSettings(): JSX.Element { const gbToBytes = (gb: number): number => Math.round(gb * 1024 ** 3); - const getFormattedTime = (date: string): string => - dayjs(date).format('MMM DD,YYYY, hh:mm a'); + const getFormattedTime = ( + date: string, + formatTimezoneAdjustedTimestamp: (date: string, format: string) => string, + ): string => + formatTimezoneAdjustedTimestamp(date, 'MMM DD,YYYY, hh:mm a (UTC Z)'); const showDeleteLimitModal = ( APIKey: IngestionKeyProps, @@ -544,17 +551,27 @@ function MultiIngestionSettings(): JSX.Element { } }; + const { formatTimezoneAdjustedTimestamp } = useTimezone(); + const columns: AntDTableProps['columns'] = [ { title: 'Ingestion Key', key: 'ingestion-key', // eslint-disable-next-line sonarjs/cognitive-complexity render: (APIKey: IngestionKeyProps): JSX.Element => { - const createdOn = getFormattedTime(APIKey.created_at); + const createdOn = getFormattedTime( + APIKey.created_at, + formatTimezoneAdjustedTimestamp, + ); const formattedDateAndTime = - APIKey && APIKey?.expires_at && getFormattedTime(APIKey?.expires_at); + APIKey && + APIKey?.expires_at && + getFormattedTime(APIKey?.expires_at, formatTimezoneAdjustedTimestamp); - const updatedOn = getFormattedTime(APIKey?.updated_at); + const updatedOn = getFormattedTime( + APIKey?.updated_at, + formatTimezoneAdjustedTimestamp, + ); const limits: { [key: string]: LimitProps } = {}; diff --git a/frontend/src/container/Licenses/ListLicenses.tsx b/frontend/src/container/Licenses/ListLicenses.tsx index 02d3abbb65..b166e2d659 100644 --- a/frontend/src/container/Licenses/ListLicenses.tsx +++ b/frontend/src/container/Licenses/ListLicenses.tsx @@ -1,8 +1,20 @@ +import { Typography } from 'antd'; import { ColumnsType } from 'antd/lib/table'; import { ResizeTable } from 'components/ResizeTable'; +import { useTimezone } from 'providers/Timezone'; import { useTranslation } from 'react-i18next'; import { License } from 'types/api/licenses/def'; +function ValidityColumn({ value }: { value: string }): JSX.Element { + const { formatTimezoneAdjustedTimestamp } = useTimezone(); + + return ( + + {formatTimezoneAdjustedTimestamp(value, 'YYYY-MM-DD HH:mm:ss (UTC Z)')} + + ); +} + function ListLicenses({ licenses }: ListLicensesProps): JSX.Element { const { t } = useTranslation(['licenses']); @@ -23,12 +35,14 @@ function ListLicenses({ licenses }: ListLicensesProps): JSX.Element { title: t('column_valid_from'), dataIndex: 'ValidFrom', key: 'valid from', + render: (value: string): JSX.Element => ValidityColumn({ value }), width: 80, }, { title: t('column_valid_until'), dataIndex: 'ValidUntil', key: 'valid until', + render: (value: string): JSX.Element => ValidityColumn({ value }), width: 80, }, ]; diff --git a/frontend/src/container/ListOfDashboard/DashboardList.styles.scss b/frontend/src/container/ListOfDashboard/DashboardList.styles.scss index 21d4a5e20d..08e62dd101 100644 --- a/frontend/src/container/ListOfDashboard/DashboardList.styles.scss +++ b/frontend/src/container/ListOfDashboard/DashboardList.styles.scss @@ -867,7 +867,7 @@ .configure-metadata-root { .ant-modal-content { - width: 400px; + width: 500px; flex-shrink: 0; border-radius: 4px; border: 1px solid var(--Slate-500, #161922); @@ -1039,7 +1039,6 @@ display: flex; justify-content: space-between; align-items: center; - width: 336px; padding: 0px 0px 0px 14.634px; .left { diff --git a/frontend/src/container/ListOfDashboard/DashboardsList.tsx b/frontend/src/container/ListOfDashboard/DashboardsList.tsx index 0a5b3b5130..885ed87e46 100644 --- a/frontend/src/container/ListOfDashboard/DashboardsList.tsx +++ b/frontend/src/container/ListOfDashboard/DashboardsList.tsx @@ -57,6 +57,7 @@ import { // see more: https://github.com/lucide-icons/lucide/issues/94 import { handleContactSupport } from 'pages/Integrations/utils'; import { useDashboard } from 'providers/Dashboard/Dashboard'; +import { useTimezone } from 'providers/Timezone'; import { ChangeEvent, Key, @@ -343,31 +344,13 @@ function DashboardsList(): JSX.Element { } }, [state.error, state.value, t, notifications]); - function getFormattedTime(dashboard: Dashboard, option: string): string { - const timeOptions: Intl.DateTimeFormatOptions = { - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - hour12: false, - }; - const formattedTime = new Date(get(dashboard, option, '')).toLocaleTimeString( - 'en-US', - timeOptions, - ); + const { formatTimezoneAdjustedTimestamp } = useTimezone(); - const dateOptions: Intl.DateTimeFormatOptions = { - month: 'short', - day: 'numeric', - year: 'numeric', - }; - - const formattedDate = new Date(get(dashboard, option, '')).toLocaleDateString( - 'en-US', - dateOptions, + function getFormattedTime(dashboard: Dashboard, option: string): string { + return formatTimezoneAdjustedTimestamp( + get(dashboard, option, ''), + 'MMM D, YYYY ⎯ hh:mm:ss A (UTC Z)', ); - - // Combine time and date - return `${formattedDate} ⎯ ${formattedTime}`; } const onLastUpdated = (time: string): string => { @@ -410,31 +393,11 @@ function DashboardsList(): JSX.Element { title: 'Dashboards', key: 'dashboard', render: (dashboard: Data, _, index): JSX.Element => { - const timeOptions: Intl.DateTimeFormatOptions = { - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - hour12: false, - }; - const formattedTime = new Date(dashboard.createdAt).toLocaleTimeString( - 'en-US', - timeOptions, - ); - - const dateOptions: Intl.DateTimeFormatOptions = { - month: 'short', - day: 'numeric', - year: 'numeric', - }; - - const formattedDate = new Date(dashboard.createdAt).toLocaleDateString( - 'en-US', - dateOptions, + const formattedDateAndTime = formatTimezoneAdjustedTimestamp( + dashboard.createdAt, + 'MMM D, YYYY ⎯ hh:mm:ss A (UTC Z)', ); - // Combine time and date - const formattedDateAndTime = `${formattedDate} ⎯ ${formattedTime}`; - const getLink = (): string => `${ROUTES.ALL_DASHBOARD}/${dashboard.id}`; const onClickHandler = (event: React.MouseEvent): void => { diff --git a/frontend/src/container/LogDetailedView/InfraMetrics/NodeMetrics.tsx b/frontend/src/container/LogDetailedView/InfraMetrics/NodeMetrics.tsx index 3c935c8b89..8c46d572fd 100644 --- a/frontend/src/container/LogDetailedView/InfraMetrics/NodeMetrics.tsx +++ b/frontend/src/container/LogDetailedView/InfraMetrics/NodeMetrics.tsx @@ -8,10 +8,12 @@ import { useResizeObserver } from 'hooks/useDimensions'; import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults'; import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions'; import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData'; +import { useTimezone } from 'providers/Timezone'; import { useMemo, useRef } from 'react'; import { useQueries, UseQueryResult } from 'react-query'; import { SuccessResponse } from 'types/api'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; +import uPlot from 'uplot'; import { getHostQueryPayload, @@ -73,6 +75,8 @@ function NodeMetrics({ [queries], ); + const { timezone } = useTimezone(); + const options = useMemo( () => queries.map(({ data }, idx) => @@ -86,6 +90,9 @@ function NodeMetrics({ minTimeScale: start, maxTimeScale: end, verticalLineTimestamp, + tzDate: (timestamp: number) => + uPlot.tzDate(new Date(timestamp * 1e3), timezone.value), + timezone: timezone.value, }), ), [ @@ -96,6 +103,7 @@ function NodeMetrics({ start, verticalLineTimestamp, end, + timezone.value, ], ); diff --git a/frontend/src/container/LogDetailedView/InfraMetrics/PodMetrics.tsx b/frontend/src/container/LogDetailedView/InfraMetrics/PodMetrics.tsx index 99391d65e0..bb6ff0b654 100644 --- a/frontend/src/container/LogDetailedView/InfraMetrics/PodMetrics.tsx +++ b/frontend/src/container/LogDetailedView/InfraMetrics/PodMetrics.tsx @@ -8,10 +8,12 @@ import { useResizeObserver } from 'hooks/useDimensions'; import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults'; import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions'; import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData'; +import { useTimezone } from 'providers/Timezone'; import { useMemo, useRef } from 'react'; import { useQueries, UseQueryResult } from 'react-query'; import { SuccessResponse } from 'types/api'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; +import uPlot from 'uplot'; import { getPodQueryPayload, podWidgetInfo } from './constants'; @@ -60,6 +62,7 @@ function PodMetrics({ () => queries.map(({ data }) => getUPlotChartData(data?.payload)), [queries], ); + const { timezone } = useTimezone(); const options = useMemo( () => @@ -74,9 +77,20 @@ function PodMetrics({ minTimeScale: start, maxTimeScale: end, verticalLineTimestamp, + tzDate: (timestamp: number) => + uPlot.tzDate(new Date(timestamp * 1e3), timezone.value), + timezone: timezone.value, }), ), - [queries, isDarkMode, dimensions, start, verticalLineTimestamp, end], + [ + queries, + isDarkMode, + dimensions, + start, + end, + verticalLineTimestamp, + timezone.value, + ], ); const renderCardContent = ( diff --git a/frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx b/frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx index 57ceea5072..501c99740d 100644 --- a/frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx +++ b/frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx @@ -11,7 +11,8 @@ import ROUTES from 'constants/routes'; import dompurify from 'dompurify'; import { isEmpty } from 'lodash-es'; import { ArrowDownToDot, ArrowUpFromDot, Ellipsis } from 'lucide-react'; -import { useMemo, useState } from 'react'; +import { useTimezone } from 'providers/Timezone'; +import React, { useMemo, useState } from 'react'; import { useLocation } from 'react-router-dom'; import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { FORBID_DOM_PURIFY_TAGS } from 'utils/app'; @@ -68,6 +69,8 @@ export function TableViewActions( const [isOpen, setIsOpen] = useState(false); + const { formatTimezoneAdjustedTimestamp } = useTimezone(); + if (record.field === 'body') { const parsedBody = recursiveParseJSON(fieldData.value); if (!isEmpty(parsedBody)) { @@ -100,33 +103,44 @@ export function TableViewActions( ); } - return ( -
- {record.field === 'body' ? ( - - - - ) : ( - - - {removeEscapeCharacters(fieldData.value)} + let cleanTimestamp: string; + if (record.field === 'timestamp') { + cleanTimestamp = fieldData.value.replace(/^["']|["']$/g, ''); + } + + const renderFieldContent = (): JSX.Element => { + const commonStyles: React.CSSProperties = { + color: Color.BG_SIENNA_400, + whiteSpace: 'pre-wrap', + tabSize: 4, + }; + + switch (record.field) { + case 'body': + return ; + + case 'timestamp': + return ( + + {formatTimezoneAdjustedTimestamp( + cleanTimestamp, + 'MM/DD/YYYY, HH:mm:ss.SSS (UTC Z)', + )} - - )} + ); + + default: + return ( + {removeEscapeCharacters(fieldData.value)} + ); + } + }; + return ( +
+ + {renderFieldContent()} + {!isListViewPanel && ( diff --git a/frontend/src/container/LogsExplorerViews/index.tsx b/frontend/src/container/LogsExplorerViews/index.tsx index 5ce5dbe2be..8d6b74a025 100644 --- a/frontend/src/container/LogsExplorerViews/index.tsx +++ b/frontend/src/container/LogsExplorerViews/index.tsx @@ -50,6 +50,7 @@ import { } from 'lodash-es'; import { Sliders } from 'lucide-react'; import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils'; +import { useTimezone } from 'providers/Timezone'; import { memo, MutableRefObject, @@ -669,13 +670,19 @@ function LogsExplorerViews({ setIsLoadingQueries, ]); + const { timezone } = useTimezone(); + const flattenLogData = useMemo( () => logs.map((log) => { const timestamp = typeof log.timestamp === 'string' - ? dayjs(log.timestamp).format('YYYY-MM-DD HH:mm:ss.SSS') - : dayjs(log.timestamp / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS'); + ? dayjs(log.timestamp) + .tz(timezone.value) + .format('YYYY-MM-DD HH:mm:ss.SSS') + : dayjs(log.timestamp / 1e6) + .tz(timezone.value) + .format('YYYY-MM-DD HH:mm:ss.SSS'); return FlatLogData({ timestamp, @@ -683,7 +690,7 @@ function LogsExplorerViews({ ...omit(log, 'timestamp', 'body'), }); }), - [logs], + [logs, timezone.value], ); return ( diff --git a/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx b/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx index 6271ff793e..d708684824 100644 --- a/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx +++ b/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx @@ -7,6 +7,7 @@ import { rest } from 'msw'; import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils'; import { QueryBuilderProvider } from 'providers/QueryBuilder'; import MockQueryClientProvider from 'providers/test/MockQueryClientProvider'; +import TimezoneProvider from 'providers/Timezone'; import { I18nextProvider } from 'react-i18next'; import { Provider } from 'react-redux'; import { MemoryRouter } from 'react-router-dom'; @@ -91,17 +92,19 @@ const renderer = (): RenderResult => - - {}} - listQueryKeyRef={{ current: {} }} - chartQueryKeyRef={{ current: {} }} - /> - + + + {}} + listQueryKeyRef={{ current: {} }} + chartQueryKeyRef={{ current: {} }} + /> + + diff --git a/frontend/src/container/LogsPanelTable/LogsPanelComponent.tsx b/frontend/src/container/LogsPanelTable/LogsPanelComponent.tsx index a7598b5c76..d8131b5403 100644 --- a/frontend/src/container/LogsPanelTable/LogsPanelComponent.tsx +++ b/frontend/src/container/LogsPanelTable/LogsPanelComponent.tsx @@ -15,6 +15,7 @@ import { useLogsData } from 'hooks/useLogsData'; import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults'; import { FlatLogData } from 'lib/logs/flatLogData'; import { RowData } from 'lib/query/createTableColumnsFromQuery'; +import { useTimezone } from 'providers/Timezone'; import { Dispatch, HTMLAttributes, @@ -76,7 +77,12 @@ function LogsPanelComponent({ }); }; - const columns = getLogPanelColumnsList(widget.selectedLogFields); + const { formatTimezoneAdjustedTimestamp } = useTimezone(); + + const columns = getLogPanelColumnsList( + widget.selectedLogFields, + formatTimezoneAdjustedTimestamp, + ); const dataLength = queryResponse.data?.payload?.data?.newResult?.data?.result[0]?.list?.length; diff --git a/frontend/src/container/LogsPanelTable/utils.tsx b/frontend/src/container/LogsPanelTable/utils.tsx index a95442b7cc..954526bde1 100644 --- a/frontend/src/container/LogsPanelTable/utils.tsx +++ b/frontend/src/container/LogsPanelTable/utils.tsx @@ -1,6 +1,7 @@ import { ColumnsType } from 'antd/es/table'; import { Typography } from 'antd/lib'; import { OPERATORS } from 'constants/queryBuilder'; +import { TimestampInput } from 'hooks/useTimezoneFormatter/useTimezoneFormatter'; // import Typography from 'antd/es/typography/Typography'; import { RowData } from 'lib/query/createTableColumnsFromQuery'; import { ReactNode } from 'react'; @@ -13,18 +14,31 @@ import { v4 as uuid } from 'uuid'; export const getLogPanelColumnsList = ( selectedLogFields: Widgets['selectedLogFields'], + formatTimezoneAdjustedTimestamp: ( + input: TimestampInput, + format?: string, + ) => string, ): ColumnsType => { const initialColumns: ColumnsType = []; const columns: ColumnsType = selectedLogFields?.map((field: IField) => { const { name } = field; + return { title: name, dataIndex: name, key: name, width: name === 'body' ? 350 : 100, render: (value: ReactNode): JSX.Element => { + if (name === 'timestamp') { + return ( + + {formatTimezoneAdjustedTimestamp(value as string)} + + ); + } + if (name === 'body') { return ( diff --git a/frontend/src/container/MySettings/TimezoneAdaptation/TimezoneAdaptation.styles.scss b/frontend/src/container/MySettings/TimezoneAdaptation/TimezoneAdaptation.styles.scss new file mode 100644 index 0000000000..d7b9813c50 --- /dev/null +++ b/frontend/src/container/MySettings/TimezoneAdaptation/TimezoneAdaptation.styles.scss @@ -0,0 +1,96 @@ +.timezone-adaption { + padding: 16px; + background: var(--bg-ink-400); + border: 1px solid var(--bg-ink-500); + border-radius: 4px; + + &__header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + } + + &__title { + color: var(--bg-vanilla-300); + font-size: 14px; + font-weight: 500; + margin: 0; + } + + &__description { + color: var(--bg-vanilla-400); + font-size: 14px; + line-height: 20px; + margin: 0 0 12px 0; + } + + &__note { + display: flex; + align-items: center; + justify-content: space-between; + padding: 7.5px 12px; + background: rgba(78, 116, 248, 0.1); + border: 1px solid rgba(78, 116, 248, 0.1); + border-radius: 4px; + } + + &__bullet { + color: var(--bg-robin-400); + font-size: 16px; + line-height: 20px; + } + + &__note-text-container { + display: flex; + align-items: center; + gap: 10px; + } + + &__note-text { + display: flex; + align-items: center; + gap: 4px; + color: var(--bg-robin-400); + font-size: 14px; + line-height: 20px; + } + &__note-text-overridden { + display: flex; + align-items: center; + padding: 0 2px; + background: rgba(171, 189, 255, 0.04); + border-radius: 2px; + font-size: 12px; + line-height: 16px; + color: var(--bg-vanilla-100); + } + &__clear-override { + display: flex; + align-items: center; + gap: 6px; + background: transparent; + border: none; + padding: 0; + color: var(--bg-robin-300); + font-size: 12px; + line-height: 16px; /* 133.333% */ + letter-spacing: 0.12px; + cursor: pointer; + } +} + +.lightMode { + .timezone-adaption { + background-color: var(--bg-vanilla-100); + border-color: var(--bg-vanilla-300); + + &__title { + color: var(--text-ink-300); + } + + &__description { + color: var(--text-ink-400); + } + } +} diff --git a/frontend/src/container/MySettings/TimezoneAdaptation/TimezoneAdaptation.tsx b/frontend/src/container/MySettings/TimezoneAdaptation/TimezoneAdaptation.tsx new file mode 100644 index 0000000000..a716d4196d --- /dev/null +++ b/frontend/src/container/MySettings/TimezoneAdaptation/TimezoneAdaptation.tsx @@ -0,0 +1,82 @@ +import './TimezoneAdaptation.styles.scss'; + +import { Color } from '@signozhq/design-tokens'; +import { Switch } from 'antd'; +import { Delete } from 'lucide-react'; +import { useTimezone } from 'providers/Timezone'; +import { useMemo } from 'react'; + +function TimezoneAdaptation(): JSX.Element { + const { + timezone, + browserTimezone, + updateTimezone, + isAdaptationEnabled, + setIsAdaptationEnabled, + } = useTimezone(); + + const isTimezoneOverridden = useMemo( + () => timezone.offset !== browserTimezone.offset, + [timezone, browserTimezone], + ); + + const getSwitchStyles = (): React.CSSProperties => ({ + backgroundColor: + isAdaptationEnabled && isTimezoneOverridden ? Color.BG_AMBER_400 : undefined, + }); + + const handleOverrideClear = (): void => { + updateTimezone(browserTimezone); + }; + + return ( +
+
+

Adapt to my timezone

+ +
+ +

+ Adapt the timestamps shown in the SigNoz console to my active timezone. +

+ +
+
+ + + {isTimezoneOverridden ? ( + <> + Your current timezone is overridden to + + {timezone.offset} + + + ) : ( + <> + You can override the timezone adaption for any view with the time + picker. + + )} + +
+ + {!!isTimezoneOverridden && ( + + )} +
+
+ ); +} + +export default TimezoneAdaptation; diff --git a/frontend/src/container/MySettings/index.tsx b/frontend/src/container/MySettings/index.tsx index de6c12eb53..59abd0cf73 100644 --- a/frontend/src/container/MySettings/index.tsx +++ b/frontend/src/container/MySettings/index.tsx @@ -7,6 +7,7 @@ import { LogOut, Moon, Sun } from 'lucide-react'; import { useState } from 'react'; import Password from './Password'; +import TimezoneAdaptation from './TimezoneAdaptation/TimezoneAdaptation'; import UserInfo from './UserInfo'; function MySettings(): JSX.Element { @@ -78,6 +79,8 @@ function MySettings(): JSX.Element {
+ +