From 2f4b8f6f80c461dc5af8de840ec81b44c2d0eb40 Mon Sep 17 00:00:00 2001 From: Yunus M Date: Wed, 24 Sep 2025 11:52:37 +0530 Subject: [PATCH 1/4] feat: standardise header to include share and feedback sections (#9037) * feat: standardise header to include share and feedback sections * feat: add unit test cases * feat: handle click outside to close open modals * fix: handle click outside to close modals * chore: update event name and placeholder * fix: test cases * feat: show success / failure message on feedback submit, fix test cases * feat: add test cases to check if toast messages are shown on feedback submit * feat: address review comments * feat: update test cases --------- Co-authored-by: makeavish --- .gitignore | 4 +- .../HeaderRightSection/AnnouncementsModal.tsx | 15 + .../HeaderRightSection/FeedbackModal.tsx | 160 ++++++++++ .../HeaderRightSection.styles.scss | 253 +++++++++++++++ .../HeaderRightSection/HeaderRightSection.tsx | 137 +++++++++ .../HeaderRightSection/ShareURLModal.tsx | 171 +++++++++++ .../__tests__/AnnouncementsModal.test.tsx | 29 ++ .../__tests__/FeedbackModal.test.tsx | 274 +++++++++++++++++ .../__tests__/HeaderRightSection.test.tsx | 192 ++++++++++++ .../__tests__/ShareURLModal.test.tsx | 289 ++++++++++++++++++ frontend/src/components/RouteTab/index.tsx | 12 + frontend/src/components/RouteTab/types.ts | 1 + .../src/container/LogsError/LogsError.tsx | 1 + .../Description.styles.scss | 4 +- .../DashboardDescription/index.tsx | 7 + .../DateTimeSelectionV2.styles.scss | 55 +--- .../TopNav/DateTimeSelectionV2/index.tsx | 135 +------- .../src/container/TopNav/TopNav.styles.scss | 17 +- frontend/src/container/TopNav/index.tsx | 15 +- .../src/pages/AlertList/AlertList.styles.scss | 4 +- frontend/src/pages/AlertList/index.tsx | 14 +- .../src/pages/AllErrors/AllErrors.styles.scss | 7 +- frontend/src/pages/AllErrors/index.tsx | 19 +- .../DashboardsListPage.styles.scss | 9 +- .../DashboardsListPage/DashboardsListPage.tsx | 13 +- .../src/pages/Settings/Settings.styles.scss | 1 - .../src/pages/Support/Support.styles.scss | 3 +- frontend/src/pages/Support/Support.tsx | 3 +- 28 files changed, 1629 insertions(+), 215 deletions(-) create mode 100644 frontend/src/components/HeaderRightSection/AnnouncementsModal.tsx create mode 100644 frontend/src/components/HeaderRightSection/FeedbackModal.tsx create mode 100644 frontend/src/components/HeaderRightSection/HeaderRightSection.styles.scss create mode 100644 frontend/src/components/HeaderRightSection/HeaderRightSection.tsx create mode 100644 frontend/src/components/HeaderRightSection/ShareURLModal.tsx create mode 100644 frontend/src/components/HeaderRightSection/__tests__/AnnouncementsModal.test.tsx create mode 100644 frontend/src/components/HeaderRightSection/__tests__/FeedbackModal.test.tsx create mode 100644 frontend/src/components/HeaderRightSection/__tests__/HeaderRightSection.test.tsx create mode 100644 frontend/src/components/HeaderRightSection/__tests__/ShareURLModal.test.tsx diff --git a/.gitignore b/.gitignore index ff71a25bc909..014c7c2800bc 100644 --- a/.gitignore +++ b/.gitignore @@ -230,4 +230,6 @@ poetry.toml # LSP config files pyrightconfig.json -# End of https://www.toptal.com/developers/gitignore/api/python \ No newline at end of file +# End of https://www.toptal.com/developers/gitignore/api/python + +frontend/.cursor/rules/ \ No newline at end of file diff --git a/frontend/src/components/HeaderRightSection/AnnouncementsModal.tsx b/frontend/src/components/HeaderRightSection/AnnouncementsModal.tsx new file mode 100644 index 000000000000..fdd054de110b --- /dev/null +++ b/frontend/src/components/HeaderRightSection/AnnouncementsModal.tsx @@ -0,0 +1,15 @@ +import { Typography } from 'antd'; + +function AnnouncementsModal(): JSX.Element { + return ( +
+
+ + Announcements + +
+
+ ); +} + +export default AnnouncementsModal; diff --git a/frontend/src/components/HeaderRightSection/FeedbackModal.tsx b/frontend/src/components/HeaderRightSection/FeedbackModal.tsx new file mode 100644 index 000000000000..f0de75f3447c --- /dev/null +++ b/frontend/src/components/HeaderRightSection/FeedbackModal.tsx @@ -0,0 +1,160 @@ +import { toast } from '@signozhq/sonner'; +import { Button, Input, Radio, RadioChangeEvent, Typography } from 'antd'; +import logEvent from 'api/common/logEvent'; +import { useGetTenantLicense } from 'hooks/useGetTenantLicense'; +import { handleContactSupport } from 'pages/Integrations/utils'; +import { useCallback, useEffect, useState } from 'react'; +import { useLocation } from 'react-router-dom'; + +function FeedbackModal({ onClose }: { onClose: () => void }): JSX.Element { + const [activeTab, setActiveTab] = useState('feedback'); + const [feedback, setFeedback] = useState(''); + const location = useLocation(); + const { isCloudUser: isCloudUserVal } = useGetTenantLicense(); + const [isLoading, setIsLoading] = useState(false); + + const handleSubmit = async (): Promise => { + setIsLoading(true); + + let entityName = 'Feedback'; + if (activeTab === 'reportBug') { + entityName = 'Bug report'; + } else if (activeTab === 'featureRequest') { + entityName = 'Feature request'; + } + + logEvent('Feedback: Submitted', { + data: feedback, + type: activeTab, + page: location.pathname, + }) + .then(() => { + onClose(); + + toast.success(`${entityName} submitted successfully`, { + position: 'top-right', + }); + }) + .catch(() => { + console.error(`Failed to submit ${entityName}`); + toast.error(`Failed to submit ${entityName}`, { + position: 'top-right', + }); + }) + .finally(() => { + setIsLoading(false); + }); + }; + + useEffect( + () => (): void => { + setFeedback(''); + setActiveTab('feedback'); + }, + [], + ); + + const items = [ + { + label: ( +
+
+ Feedback +
+ ), + key: 'feedback', + value: 'feedback', + }, + { + label: ( +
+
+ Report a bug +
+ ), + key: 'reportBug', + value: 'reportBug', + }, + { + label: ( +
+
+ Feature request +
+ ), + key: 'featureRequest', + value: 'featureRequest', + }, + ]; + + const handleFeedbackChange = ( + e: React.ChangeEvent, + ): void => { + setFeedback(e.target.value); + }; + + const handleContactSupportClick = useCallback((): void => { + handleContactSupport(isCloudUserVal); + }, [isCloudUserVal]); + + return ( +
+
+ setActiveTab(e.target.value)} + /> +
+
+
+ +
+
+ +
+ +
+ + Have a specific issue?{' '} + + Contact Support{' '} + + or{' '} + + Read our docs + + +
+
+
+ ); +} + +export default FeedbackModal; diff --git a/frontend/src/components/HeaderRightSection/HeaderRightSection.styles.scss b/frontend/src/components/HeaderRightSection/HeaderRightSection.styles.scss new file mode 100644 index 000000000000..910200e98868 --- /dev/null +++ b/frontend/src/components/HeaderRightSection/HeaderRightSection.styles.scss @@ -0,0 +1,253 @@ +.header-right-section-container { + display: flex; + align-items: center; + gap: 8px; +} + +.share-modal-content, +.feedback-modal-container { + display: flex; + flex-direction: column; + gap: 16px; + padding: 12px; + width: 460px; + + border-radius: 4px; + box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(20px); + + .absolute-relative-time-toggler-container { + display: flex; + gap: 8px; + align-items: center; + justify-content: space-between; + + .absolute-relative-time-toggler-label { + color: var(--bg-vanilla-100); + font-size: 13px; + font-style: normal; + font-weight: 500; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + } + + .absolute-relative-time-toggler { + display: flex; + gap: 4px; + align-items: center; + } + + .absolute-relative-time-error { + font-size: 12px; + color: var(--bg-amber-600); + } + + .share-link { + .url-share-container { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + + .url-share-container-header { + display: flex; + flex-direction: column; + gap: 4px; + + .url-share-title, + .url-share-sub-title { + color: var(--bg-vanilla-100); + font-size: 13px; + font-style: normal; + font-weight: 500; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + + .url-share-sub-title { + font-size: 12px; + color: var(--bg-vanilla-300); + font-weight: 400; + line-height: 18px; + letter-spacing: -0.06px; + } + } + } + } +} + +.feedback-modal-container { + .feedback-modal-tabs { + width: 100%; + display: flex; + + .ant-radio-button-wrapper { + flex: 1; + margin: 0px !important; + + border: 1px solid var(--bg-slate-400); + + &:before { + display: none; + } + + .ant-radio-button-checked { + background-color: var(--bg-slate-400); + } + } + + .feedback-modal-tab-label { + display: flex; + align-items: center; + gap: 8px; + + .tab-icon { + width: 6px; + height: 6px; + } + + .feedback-tab { + background-color: var(--bg-sakura-500); + } + + .bug-tab { + background-color: var(--bg-amber-500); + } + + .feature-tab { + background-color: var(--bg-robin-500); + } + } + + .ant-tabs-nav-list { + .ant-tabs-tab { + padding: 6px 16px; + + border-radius: 2px; + background: var(--bg-ink-400); + box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.1); + border: 1px solid var(--bg-slate-400); + + margin: 0 !important; + + .ant-tabs-tab-btn { + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 166.667% */ + letter-spacing: -0.06px; + } + + &-active { + background: var(--bg-slate-400); + color: var(--bg-vanilla-100); + + border-bottom: none !important; + + .ant-tabs-tab-btn { + color: var(--bg-vanilla-100); + } + } + } + } + } + + .feedback-modal-content { + display: flex; + flex-direction: column; + gap: 16px; + + .feedback-input { + resize: none; + + text-area { + resize: none; + } + } + + .feedback-content-include-console-logs { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + } + } + + .feedback-modal-content-footer { + display: flex; + flex-direction: column; + gap: 16px; + + .feedback-modal-content-footer-info-text { + font-size: 12px; + color: var(--bg-vanilla-400, #c0c1c3); + text-align: center; + + /* button/ small */ + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 24px; /* 200% */ + + .contact-support-link, + .read-docs-link { + color: var(--bg-robin-400); + font-weight: 500; + font-size: 12px; + } + } + } +} + +.lightMode { + .share-modal-content, + .feedback-modal-container { + .absolute-relative-time-toggler-container { + .absolute-relative-time-toggler-label { + color: var(--bg-ink-400); + } + } + + .share-link { + .url-share-container { + .url-share-container-header { + .url-share-title, + .url-share-sub-title { + color: var(--bg-ink-400); + } + + .url-share-sub-title { + color: var(--bg-ink-300); + } + } + } + } + } + + .feedback-modal-container { + .feedback-modal-tabs { + .ant-radio-button-wrapper { + flex: 1; + margin: 0px !important; + + border: 1px solid var(--bg-vanilla-300); + + &:before { + display: none; + } + + .ant-radio-button-checked { + background-color: var(--bg-vanilla-300); + } + } + } + + .feedback-modal-content-footer { + .feedback-modal-content-footer-info-text { + color: var(--bg-slate-400); + } + } + } +} diff --git a/frontend/src/components/HeaderRightSection/HeaderRightSection.tsx b/frontend/src/components/HeaderRightSection/HeaderRightSection.tsx new file mode 100644 index 000000000000..19b00391bd7e --- /dev/null +++ b/frontend/src/components/HeaderRightSection/HeaderRightSection.tsx @@ -0,0 +1,137 @@ +import './HeaderRightSection.styles.scss'; + +import { Button, Popover } from 'antd'; +import logEvent from 'api/common/logEvent'; +import { Globe, Inbox, SquarePen } from 'lucide-react'; +import { useCallback, useState } from 'react'; +import { useLocation } from 'react-router-dom'; + +import AnnouncementsModal from './AnnouncementsModal'; +import FeedbackModal from './FeedbackModal'; +import ShareURLModal from './ShareURLModal'; + +interface HeaderRightSectionProps { + enableAnnouncements: boolean; + enableShare: boolean; + enableFeedback: boolean; +} + +function HeaderRightSection({ + enableAnnouncements, + enableShare, + enableFeedback, +}: HeaderRightSectionProps): JSX.Element { + const location = useLocation(); + + const [openFeedbackModal, setOpenFeedbackModal] = useState(false); + const [openShareURLModal, setOpenShareURLModal] = useState(false); + const [openAnnouncementsModal, setOpenAnnouncementsModal] = useState(false); + + const handleOpenFeedbackModal = useCallback((): void => { + logEvent('Feedback: Clicked', { + page: location.pathname, + }); + + setOpenFeedbackModal(true); + setOpenShareURLModal(false); + setOpenAnnouncementsModal(false); + }, [location.pathname]); + + const handleOpenShareURLModal = useCallback((): void => { + logEvent('Share: Clicked', { + page: location.pathname, + }); + + setOpenShareURLModal(true); + setOpenFeedbackModal(false); + setOpenAnnouncementsModal(false); + }, [location.pathname]); + + const handleCloseFeedbackModal = (): void => { + setOpenFeedbackModal(false); + }; + + const handleOpenFeedbackModalChange = (open: boolean): void => { + setOpenFeedbackModal(open); + }; + + const handleOpenAnnouncementsModalChange = (open: boolean): void => { + setOpenAnnouncementsModal(open); + }; + + const handleOpenShareURLModalChange = (open: boolean): void => { + setOpenShareURLModal(open); + }; + + return ( +
+ {enableFeedback && ( + } + destroyTooltipOnHide + arrow={false} + trigger="click" + open={openFeedbackModal} + onOpenChange={handleOpenFeedbackModalChange} + > + + + )} +
+ ); +} + +export default HeaderRightSection; diff --git a/frontend/src/components/HeaderRightSection/ShareURLModal.tsx b/frontend/src/components/HeaderRightSection/ShareURLModal.tsx new file mode 100644 index 000000000000..c15c0d3c6ed4 --- /dev/null +++ b/frontend/src/components/HeaderRightSection/ShareURLModal.tsx @@ -0,0 +1,171 @@ +import { Color } from '@signozhq/design-tokens'; +import { Button, Switch, Typography } from 'antd'; +import logEvent from 'api/common/logEvent'; +import { QueryParams } from 'constants/query'; +import ROUTES from 'constants/routes'; +import useUrlQuery from 'hooks/useUrlQuery'; +import GetMinMax from 'lib/getMinMax'; +import { Check, Info, Link2 } from 'lucide-react'; +import { useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { matchPath, useLocation } from 'react-router-dom'; +import { useCopyToClipboard } from 'react-use'; +import { AppState } from 'store/reducers'; +import { GlobalReducer } from 'types/reducer/globalTime'; + +const routesToBeSharedWithTime = [ + ROUTES.LOGS_EXPLORER, + ROUTES.TRACES_EXPLORER, + ROUTES.METRICS_EXPLORER_EXPLORER, + ROUTES.METER_EXPLORER, +]; + +function ShareURLModal(): JSX.Element { + const urlQuery = useUrlQuery(); + const location = useLocation(); + const { selectedTime } = useSelector( + (state) => state.globalTime, + ); + + const [enableAbsoluteTime, setEnableAbsoluteTime] = useState( + selectedTime !== 'custom', + ); + + const startTime = urlQuery.get(QueryParams.startTime); + const endTime = urlQuery.get(QueryParams.endTime); + const relativeTime = urlQuery.get(QueryParams.relativeTime); + + const [isURLCopied, setIsURLCopied] = useState(false); + const [, handleCopyToClipboard] = useCopyToClipboard(); + + const isValidateRelativeTime = useMemo( + () => + selectedTime !== 'custom' || + (startTime && endTime && selectedTime === 'custom'), + [startTime, endTime, selectedTime], + ); + + const shareURLWithTime = useMemo( + () => relativeTime || (startTime && endTime), + [relativeTime, startTime, endTime], + ); + + const isRouteToBeSharedWithTime = useMemo( + () => + routesToBeSharedWithTime.some((route) => + matchPath(location.pathname, { path: route, exact: true }), + ), + [location.pathname], + ); + + // eslint-disable-next-line sonarjs/cognitive-complexity + const processURL = (): string => { + let currentUrl = window.location.href; + const isCustomTime = !!(startTime && endTime && selectedTime === 'custom'); + + if (shareURLWithTime || isRouteToBeSharedWithTime) { + if (enableAbsoluteTime || isCustomTime) { + if (selectedTime === 'custom') { + if (startTime && endTime) { + urlQuery.set(QueryParams.startTime, startTime.toString()); + urlQuery.set(QueryParams.endTime, endTime.toString()); + } + } else { + const { minTime, maxTime } = GetMinMax(selectedTime); + + urlQuery.set(QueryParams.startTime, minTime.toString()); + urlQuery.set(QueryParams.endTime, maxTime.toString()); + } + + urlQuery.delete(QueryParams.relativeTime); + + currentUrl = `${window.location.origin}${ + location.pathname + }?${urlQuery.toString()}`; + } else { + urlQuery.delete(QueryParams.startTime); + urlQuery.delete(QueryParams.endTime); + + urlQuery.set(QueryParams.relativeTime, selectedTime); + currentUrl = `${window.location.origin}${ + location.pathname + }?${urlQuery.toString()}`; + } + } + + return currentUrl; + }; + + const handleCopyURL = (): void => { + const URL = processURL(); + + handleCopyToClipboard(URL); + setIsURLCopied(true); + + logEvent('Share: Copy link clicked', { + page: location.pathname, + URL, + }); + + setTimeout(() => { + setIsURLCopied(false); + }, 1000); + }; + + return ( +
+ {(shareURLWithTime || isRouteToBeSharedWithTime) && ( + <> +
+ + Enable absolute time + + +
+ {!isValidateRelativeTime && ( + + )} + { + setEnableAbsoluteTime((prev) => !prev); + }} + /> +
+
+ + {!isValidateRelativeTime && ( +
+ Please select / enter valid relative time to toggle. +
+ )} + + )} + +
+
+
+ + Share page link + + + Share the current page link with your team member + +
+ + +
+
+
+ ); +} + +export default ShareURLModal; diff --git a/frontend/src/components/HeaderRightSection/__tests__/AnnouncementsModal.test.tsx b/frontend/src/components/HeaderRightSection/__tests__/AnnouncementsModal.test.tsx new file mode 100644 index 000000000000..39d01def7d93 --- /dev/null +++ b/frontend/src/components/HeaderRightSection/__tests__/AnnouncementsModal.test.tsx @@ -0,0 +1,29 @@ +import { render, screen } from '@testing-library/react'; + +import AnnouncementsModal from '../AnnouncementsModal'; + +describe('AnnouncementsModal', () => { + it('should render announcements modal with title', () => { + render(); + + expect(screen.getByText('Announcements')).toBeInTheDocument(); + }); + + it('should have proper structure and classes', () => { + render(); + + const container = screen + .getByText('Announcements') + .closest('.announcements-modal-container'); + expect(container).toBeInTheDocument(); + + const headerContainer = screen + .getByText('Announcements') + .closest('.announcements-modal-container-header'); + expect(headerContainer).toBeInTheDocument(); + }); + + it('should render without any errors', () => { + expect(() => render()).not.toThrow(); + }); +}); diff --git a/frontend/src/components/HeaderRightSection/__tests__/FeedbackModal.test.tsx b/frontend/src/components/HeaderRightSection/__tests__/FeedbackModal.test.tsx new file mode 100644 index 000000000000..2b4be0ff4634 --- /dev/null +++ b/frontend/src/components/HeaderRightSection/__tests__/FeedbackModal.test.tsx @@ -0,0 +1,274 @@ +/* eslint-disable sonarjs/no-duplicate-string */ +// Mock dependencies before imports +import { toast } from '@signozhq/sonner'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import logEvent from 'api/common/logEvent'; +import { useGetTenantLicense } from 'hooks/useGetTenantLicense'; +import { handleContactSupport } from 'pages/Integrations/utils'; +import { useLocation } from 'react-router-dom'; + +import FeedbackModal from '../FeedbackModal'; + +jest.mock('api/common/logEvent', () => ({ + __esModule: true, + default: jest.fn(() => Promise.resolve()), +})); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: jest.fn(), +})); + +jest.mock('@signozhq/sonner', () => ({ + toast: { + success: jest.fn(), + error: jest.fn(), + }, +})); + +jest.mock('hooks/useGetTenantLicense', () => ({ + useGetTenantLicense: jest.fn(), +})); + +jest.mock('pages/Integrations/utils', () => ({ + handleContactSupport: jest.fn(), +})); + +const mockLogEvent = logEvent as jest.MockedFunction; +const mockUseLocation = useLocation as jest.Mock; +const mockUseGetTenantLicense = useGetTenantLicense as jest.Mock; +const mockHandleContactSupport = handleContactSupport as jest.Mock; +const mockToast = toast as jest.Mocked; + +const mockOnClose = jest.fn(); + +const mockLocation = { + pathname: '/test-path', +}; + +describe('FeedbackModal', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockUseLocation.mockReturnValue(mockLocation); + mockUseGetTenantLicense.mockReturnValue({ + isCloudUser: false, + }); + mockToast.success.mockClear(); + mockToast.error.mockClear(); + }); + + it('should render feedback modal with all tabs', () => { + render(); + + expect(screen.getByText('Feedback')).toBeInTheDocument(); + expect(screen.getByText('Report a bug')).toBeInTheDocument(); + expect(screen.getByText('Feature request')).toBeInTheDocument(); + expect( + screen.getByPlaceholderText('Write your feedback here...'), + ).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /submit/i })).toBeInTheDocument(); + }); + + it('should switch between tabs when clicked', async () => { + const user = userEvent.setup(); + render(); + + // Initially, feedback radio should be active + const feedbackRadio = screen.getByRole('radio', { name: 'Feedback' }); + expect(feedbackRadio).toBeChecked(); + + const bugTab = screen.getByText('Report a bug'); + await user.click(bugTab); + + // Bug radio should now be active + const bugRadio = screen.getByRole('radio', { name: 'Report a bug' }); + expect(bugRadio).toBeChecked(); + + const featureTab = screen.getByText('Feature request'); + await user.click(featureTab); + + // Feature radio should now be active + const featureRadio = screen.getByRole('radio', { name: 'Feature request' }); + expect(featureRadio).toBeChecked(); + }); + + it('should update feedback text when typing in textarea', async () => { + const user = userEvent.setup(); + render(); + + const textarea = screen.getByPlaceholderText('Write your feedback here...'); + const testFeedback = 'This is my feedback'; + + await user.type(textarea, testFeedback); + + expect(textarea).toHaveValue(testFeedback); + }); + + it('should submit feedback and log event when submit button is clicked', async () => { + const user = userEvent.setup(); + render(); + + const textarea = screen.getByPlaceholderText('Write your feedback here...'); + const submitButton = screen.getByRole('button', { name: /submit/i }); + const testFeedback = 'Test feedback content'; + + await user.type(textarea, testFeedback); + await user.click(submitButton); + + expect(mockLogEvent).toHaveBeenCalledWith('Feedback: Submitted', { + data: testFeedback, + type: 'feedback', + page: mockLocation.pathname, + }); + expect(mockOnClose).toHaveBeenCalled(); + expect(mockToast.success).toHaveBeenCalledWith( + 'Feedback submitted successfully', + { + position: 'top-right', + }, + ); + }); + + it('should submit bug report with correct type', async () => { + const user = userEvent.setup(); + render(); + + // Switch to bug report tab + const bugTab = screen.getByText('Report a bug'); + await user.click(bugTab); + + // Verify bug report radio is now active + const bugRadio = screen.getByRole('radio', { name: 'Report a bug' }); + expect(bugRadio).toBeChecked(); + + const textarea = screen.getByPlaceholderText('Write your feedback here...'); + const submitButton = screen.getByRole('button', { name: /submit/i }); + const testFeedback = 'This is a bug report'; + + await user.type(textarea, testFeedback); + await user.click(submitButton); + + expect(mockLogEvent).toHaveBeenCalledWith('Feedback: Submitted', { + data: testFeedback, + type: 'reportBug', + page: mockLocation.pathname, + }); + expect(mockOnClose).toHaveBeenCalled(); + expect(mockToast.success).toHaveBeenCalledWith( + 'Bug report submitted successfully', + { + position: 'top-right', + }, + ); + }); + + it('should submit feature request with correct type', async () => { + const user = userEvent.setup(); + render(); + + // Switch to feature request tab + const featureTab = screen.getByText('Feature request'); + await user.click(featureTab); + + // Verify feature request radio is now active + const featureRadio = screen.getByRole('radio', { name: 'Feature request' }); + expect(featureRadio).toBeChecked(); + + const textarea = screen.getByPlaceholderText('Write your feedback here...'); + const submitButton = screen.getByRole('button', { name: /submit/i }); + const testFeedback = 'This is a feature request'; + + await user.type(textarea, testFeedback); + await user.click(submitButton); + + expect(mockLogEvent).toHaveBeenCalledWith('Feedback: Submitted', { + data: testFeedback, + type: 'featureRequest', + page: mockLocation.pathname, + }); + expect(mockOnClose).toHaveBeenCalled(); + expect(mockToast.success).toHaveBeenCalledWith( + 'Feature request submitted successfully', + { + position: 'top-right', + }, + ); + }); + + it('should call handleContactSupport when contact support link is clicked', async () => { + const user = userEvent.setup(); + const isCloudUser = true; + mockUseGetTenantLicense.mockReturnValue({ + isCloudUser, + }); + + render(); + + const contactSupportLink = screen.getByText('Contact Support'); + await user.click(contactSupportLink); + + expect(mockHandleContactSupport).toHaveBeenCalledWith(isCloudUser); + }); + + it('should handle non-cloud user for contact support', async () => { + const user = userEvent.setup(); + const isCloudUser = false; + mockUseGetTenantLicense.mockReturnValue({ + isCloudUser, + }); + + render(); + + const contactSupportLink = screen.getByText('Contact Support'); + await user.click(contactSupportLink); + + expect(mockHandleContactSupport).toHaveBeenCalledWith(isCloudUser); + }); + + it('should render docs link with correct attributes', () => { + render(); + + const docsLink = screen.getByText('Read our docs'); + expect(docsLink).toHaveAttribute( + 'href', + 'https://signoz.io/docs/introduction/', + ); + expect(docsLink).toHaveAttribute('target', '_blank'); + expect(docsLink).toHaveAttribute('rel', 'noreferrer'); + }); + + it('should reset form state when component unmounts', async () => { + const user = userEvent.setup(); + + // Render component + const { unmount } = render(); + + // Change the form state first + const textArea = screen.getByPlaceholderText('Write your feedback here...'); + await user.type(textArea, 'Some feedback text'); + + // Change the active tab + const bugTab = screen.getByText('Report a bug'); + await user.click(bugTab); + + // Verify state has changed + expect(textArea).toHaveValue('Some feedback text'); + + // Unmount the component - this should trigger cleanup + unmount(); + + // Re-render the component to verify state was reset + render(); + + // Verify form state is reset + const newTextArea = screen.getByPlaceholderText( + 'Write your feedback here...', + ); + expect(newTextArea).toHaveValue(''); // Should be empty + + // Verify active radio is reset to default (Feedback radio) + const feedbackRadio = screen.getByRole('radio', { name: 'Feedback' }); + expect(feedbackRadio).toBeChecked(); + }); +}); diff --git a/frontend/src/components/HeaderRightSection/__tests__/HeaderRightSection.test.tsx b/frontend/src/components/HeaderRightSection/__tests__/HeaderRightSection.test.tsx new file mode 100644 index 000000000000..68e367f31c14 --- /dev/null +++ b/frontend/src/components/HeaderRightSection/__tests__/HeaderRightSection.test.tsx @@ -0,0 +1,192 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable sonarjs/no-duplicate-string */ +/* eslint-disable react/jsx-props-no-spreading */ +// Mock dependencies before imports +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import logEvent from 'api/common/logEvent'; +import { useLocation } from 'react-router-dom'; + +import HeaderRightSection from '../HeaderRightSection'; + +jest.mock('api/common/logEvent', () => ({ + __esModule: true, + default: jest.fn(), +})); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: jest.fn(), +})); + +jest.mock('../FeedbackModal', () => ({ + __esModule: true, + default: ({ onClose }: { onClose: () => void }): JSX.Element => ( +
+ +
+ ), +})); + +jest.mock('../ShareURLModal', () => ({ + __esModule: true, + default: (): JSX.Element => ( +
Share URL Modal
+ ), +})); + +jest.mock('../AnnouncementsModal', () => ({ + __esModule: true, + default: (): JSX.Element => ( +
Announcements Modal
+ ), +})); + +const mockLogEvent = logEvent as jest.Mock; +const mockUseLocation = useLocation as jest.Mock; + +const defaultProps = { + enableAnnouncements: true, + enableShare: true, + enableFeedback: true, +}; + +const mockLocation = { + pathname: '/test-path', +}; + +describe('HeaderRightSection', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockUseLocation.mockReturnValue(mockLocation); + }); + + it('should render all buttons when all features are enabled', () => { + render(); + + const buttons = screen.getAllByRole('button'); + expect(buttons).toHaveLength(3); + expect(screen.getByRole('button', { name: /share/i })).toBeInTheDocument(); + + // Check for feedback button by class + const feedbackButton = document.querySelector( + '.share-feedback-btn[class*="share-feedback-btn"]', + ); + expect(feedbackButton).toBeInTheDocument(); + + // Check for announcements button by finding the inbox icon + const inboxIcon = document.querySelector('.lucide-inbox'); + expect(inboxIcon).toBeInTheDocument(); + }); + + it('should render only enabled features', () => { + render( + , + ); + + const buttons = screen.getAllByRole('button'); + expect(buttons).toHaveLength(1); + expect( + screen.queryByRole('button', { name: /share/i }), + ).not.toBeInTheDocument(); + + // Check that inbox icon is not present + const inboxIcon = document.querySelector('.lucide-inbox'); + expect(inboxIcon).not.toBeInTheDocument(); + + // Check that feedback button is present + const squarePenIcon = document.querySelector('.lucide-square-pen'); + expect(squarePenIcon).toBeInTheDocument(); + }); + + it('should open feedback modal and log event when feedback button is clicked', async () => { + const user = userEvent.setup(); + render(); + + const feedbackButton = document + .querySelector('.lucide-square-pen') + ?.closest('button'); + expect(feedbackButton).toBeInTheDocument(); + + await user.click(feedbackButton!); + + expect(mockLogEvent).toHaveBeenCalledWith('Feedback: Clicked', { + page: mockLocation.pathname, + }); + expect(screen.getByTestId('feedback-modal')).toBeInTheDocument(); + }); + + it('should open share modal and log event when share button is clicked', async () => { + const user = userEvent.setup(); + render(); + + const shareButton = screen.getByRole('button', { name: /share/i }); + await user.click(shareButton); + + expect(mockLogEvent).toHaveBeenCalledWith('Share: Clicked', { + page: mockLocation.pathname, + }); + expect(screen.getByTestId('share-modal')).toBeInTheDocument(); + }); + + it('should log event when announcements button is clicked', async () => { + const user = userEvent.setup(); + render(); + + const announcementsButton = document + .querySelector('.lucide-inbox') + ?.closest('button'); + expect(announcementsButton).toBeInTheDocument(); + + await user.click(announcementsButton!); + + expect(mockLogEvent).toHaveBeenCalledWith('Announcements: Clicked', { + page: mockLocation.pathname, + }); + }); + + it('should close feedback modal when onClose is called', async () => { + const user = userEvent.setup(); + render(); + + // Open feedback modal + const feedbackButton = document + .querySelector('.lucide-square-pen') + ?.closest('button'); + expect(feedbackButton).toBeInTheDocument(); + + await user.click(feedbackButton!); + expect(screen.getByTestId('feedback-modal')).toBeInTheDocument(); + + // Close feedback modal + const closeFeedbackButton = screen.getByText('Close Feedback'); + await user.click(closeFeedbackButton); + expect(screen.queryByTestId('feedback-modal')).not.toBeInTheDocument(); + }); + + it('should close other modals when opening feedback modal', async () => { + const user = userEvent.setup(); + render(); + + // Open share modal first + const shareButton = screen.getByRole('button', { name: /share/i }); + await user.click(shareButton); + expect(screen.getByTestId('share-modal')).toBeInTheDocument(); + + // Open feedback modal - should close share modal + const feedbackButton = document + .querySelector('.lucide-square-pen') + ?.closest('button'); + expect(feedbackButton).toBeInTheDocument(); + + await user.click(feedbackButton!); + expect(screen.getByTestId('feedback-modal')).toBeInTheDocument(); + expect(screen.queryByTestId('share-modal')).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/src/components/HeaderRightSection/__tests__/ShareURLModal.test.tsx b/frontend/src/components/HeaderRightSection/__tests__/ShareURLModal.test.tsx new file mode 100644 index 000000000000..bf634f57daf9 --- /dev/null +++ b/frontend/src/components/HeaderRightSection/__tests__/ShareURLModal.test.tsx @@ -0,0 +1,289 @@ +// Mock dependencies before imports +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import logEvent from 'api/common/logEvent'; +import ROUTES from 'constants/routes'; +import useUrlQuery from 'hooks/useUrlQuery'; +import GetMinMax from 'lib/getMinMax'; +import { useSelector } from 'react-redux'; +import { matchPath, useLocation } from 'react-router-dom'; +import { useCopyToClipboard } from 'react-use'; + +import ShareURLModal from '../ShareURLModal'; + +jest.mock('api/common/logEvent', () => ({ + __esModule: true, + default: jest.fn(), +})); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: jest.fn(), + matchPath: jest.fn(), +})); + +jest.mock('hooks/useUrlQuery', () => ({ + __esModule: true, + default: jest.fn(), +})); + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: jest.fn(), +})); + +jest.mock('lib/getMinMax', () => ({ + __esModule: true, + default: jest.fn(), +})); + +jest.mock('react-use', () => ({ + ...jest.requireActual('react-use'), + useCopyToClipboard: jest.fn(), +})); + +// Mock window.location +const mockLocation = { + href: 'https://example.com/test-path?param=value', + origin: 'https://example.com', +}; +Object.defineProperty(window, 'location', { + value: mockLocation, + writable: true, +}); + +const mockLogEvent = logEvent as jest.Mock; +const mockUseLocation = useLocation as jest.Mock; +const mockUseUrlQuery = useUrlQuery as jest.Mock; +const mockUseSelector = useSelector as jest.Mock; +const mockGetMinMax = GetMinMax as jest.Mock; +const mockUseCopyToClipboard = useCopyToClipboard as jest.Mock; +const mockMatchPath = matchPath as jest.Mock; + +const mockUrlQuery = { + get: jest.fn(), + set: jest.fn(), + delete: jest.fn(), + toString: jest.fn(() => 'param=value'), +}; + +const mockHandleCopyToClipboard = jest.fn(); + +const TEST_PATH = '/test-path'; +const ENABLE_ABSOLUTE_TIME_TEXT = 'Enable absolute time'; + +describe('ShareURLModal', () => { + beforeEach(() => { + jest.clearAllMocks(); + + mockUseLocation.mockReturnValue({ + pathname: TEST_PATH, + }); + + mockUseUrlQuery.mockReturnValue(mockUrlQuery); + + mockUseSelector.mockReturnValue({ + selectedTime: '5min', + }); + + mockGetMinMax.mockReturnValue({ + minTime: 1000000, + maxTime: 2000000, + }); + + mockUseCopyToClipboard.mockReturnValue([null, mockHandleCopyToClipboard]); + + mockMatchPath.mockReturnValue(false); + + // Reset URL query mocks - all return null by default + mockUrlQuery.get.mockReturnValue(null); + + // Reset mock functions + mockUrlQuery.set.mockClear(); + mockUrlQuery.delete.mockClear(); + mockUrlQuery.toString.mockReturnValue('param=value'); + }); + + it('should render share modal with copy button', () => { + render(); + + expect(screen.getByText('Share page link')).toBeInTheDocument(); + expect( + screen.getByText('Share the current page link with your team member'), + ).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: /copy page link/i }), + ).toBeInTheDocument(); + }); + + it('should copy URL and log event when copy button is clicked', async () => { + const user = userEvent.setup(); + render(); + + const copyButton = screen.getByRole('button', { name: /copy page link/i }); + await user.click(copyButton); + + expect(mockHandleCopyToClipboard).toHaveBeenCalled(); + expect(mockLogEvent).toHaveBeenCalledWith('Share: Copy link clicked', { + page: TEST_PATH, + URL: expect.any(String), + }); + }); + + it('should show absolute time toggle when on time-enabled route', () => { + mockMatchPath.mockReturnValue(true); // Simulate being on a route that supports time + + render(); + + expect(screen.getByText(ENABLE_ABSOLUTE_TIME_TEXT)).toBeInTheDocument(); + expect(screen.getByRole('switch')).toBeInTheDocument(); + }); + + it('should show absolute time toggle when URL has time parameters', () => { + mockUrlQuery.get.mockImplementation((key: string) => + key === 'relativeTime' ? '5min' : null, + ); + + render(); + + expect(screen.getByText(ENABLE_ABSOLUTE_TIME_TEXT)).toBeInTheDocument(); + }); + + it('should toggle absolute time switch', async () => { + const user = userEvent.setup(); + mockMatchPath.mockReturnValue(true); + mockUseSelector.mockReturnValue({ + selectedTime: '5min', // Non-custom time should enable absolute time by default + }); + + render(); + + const toggleSwitch = screen.getByRole('switch'); + // Should be checked by default for non-custom time + expect(toggleSwitch).toBeChecked(); + + await user.click(toggleSwitch); + expect(toggleSwitch).not.toBeChecked(); + }); + + it('should disable toggle when relative time is invalid', () => { + mockUseSelector.mockReturnValue({ + selectedTime: 'custom', + }); + + // Invalid - missing start and end time for custom + mockUrlQuery.get.mockReturnValue(null); + + mockMatchPath.mockReturnValue(true); + + render(); + + expect( + screen.getByText('Please select / enter valid relative time to toggle.'), + ).toBeInTheDocument(); + expect(screen.getByRole('switch')).toBeDisabled(); + }); + + it('should process URL with absolute time for non-custom time', async () => { + const user = userEvent.setup(); + mockMatchPath.mockReturnValue(true); + mockUseSelector.mockReturnValue({ + selectedTime: '5min', + }); + + render(); + + // Absolute time should be enabled by default for non-custom time + // Click copy button directly + const copyButton = screen.getByRole('button', { name: /copy page link/i }); + await user.click(copyButton); + + expect(mockUrlQuery.set).toHaveBeenCalledWith('startTime', '1000000'); + expect(mockUrlQuery.set).toHaveBeenCalledWith('endTime', '2000000'); + expect(mockUrlQuery.delete).toHaveBeenCalledWith('relativeTime'); + }); + + it('should process URL with custom time parameters', async () => { + const user = userEvent.setup(); + mockMatchPath.mockReturnValue(true); + mockUseSelector.mockReturnValue({ + selectedTime: 'custom', + }); + + mockUrlQuery.get.mockImplementation((key: string) => { + switch (key) { + case 'startTime': + return '1500000'; + case 'endTime': + return '1600000'; + default: + return null; + } + }); + + render(); + + // Should be enabled by default for custom time + const copyButton = screen.getByRole('button', { name: /copy page link/i }); + await user.click(copyButton); + + expect(mockUrlQuery.set).toHaveBeenCalledWith('startTime', '1500000'); + expect(mockUrlQuery.set).toHaveBeenCalledWith('endTime', '1600000'); + }); + + it('should process URL with relative time when absolute time is disabled', async () => { + const user = userEvent.setup(); + mockMatchPath.mockReturnValue(true); + mockUseSelector.mockReturnValue({ + selectedTime: '5min', + }); + + render(); + + // Disable absolute time first (it's enabled by default for non-custom time) + const toggleSwitch = screen.getByRole('switch'); + await user.click(toggleSwitch); + + const copyButton = screen.getByRole('button', { name: /copy page link/i }); + await user.click(copyButton); + + expect(mockUrlQuery.delete).toHaveBeenCalledWith('startTime'); + expect(mockUrlQuery.delete).toHaveBeenCalledWith('endTime'); + expect(mockUrlQuery.set).toHaveBeenCalledWith('relativeTime', '5min'); + }); + + it('should handle routes that should be shared with time', async () => { + const user = userEvent.setup(); + mockUseLocation.mockReturnValue({ + pathname: ROUTES.LOGS_EXPLORER, + }); + + mockMatchPath.mockImplementation( + (pathname: string, options: any) => options.path === ROUTES.LOGS_EXPLORER, + ); + + render(); + + expect(screen.getByText(ENABLE_ABSOLUTE_TIME_TEXT)).toBeInTheDocument(); + expect(screen.getByRole('switch')).toBeChecked(); + + // on clicking copy page link, the copied url should have startTime and endTime + const copyButton = screen.getByRole('button', { name: /copy page link/i }); + + await user.click(copyButton); + + expect(mockUrlQuery.set).toHaveBeenCalledWith('startTime', '1000000'); + expect(mockUrlQuery.set).toHaveBeenCalledWith('endTime', '2000000'); + expect(mockUrlQuery.delete).toHaveBeenCalledWith('relativeTime'); + + // toggle the switch to share url with relative time + const toggleSwitch = screen.getByRole('switch'); + await user.click(toggleSwitch); + + await user.click(copyButton); + + expect(mockUrlQuery.delete).toHaveBeenCalledWith('startTime'); + expect(mockUrlQuery.delete).toHaveBeenCalledWith('endTime'); + expect(mockUrlQuery.set).toHaveBeenCalledWith('relativeTime', '5min'); + }); +}); diff --git a/frontend/src/components/RouteTab/index.tsx b/frontend/src/components/RouteTab/index.tsx index 392677a33ee9..43d652b2e4ab 100644 --- a/frontend/src/components/RouteTab/index.tsx +++ b/frontend/src/components/RouteTab/index.tsx @@ -1,4 +1,5 @@ import { Tabs, TabsProps } from 'antd'; +import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection'; import { generatePath, matchPath, @@ -17,6 +18,7 @@ function RouteTab({ activeKey, onChangeHandler, history, + showRightSection, ...rest }: RouteTabProps & TabsProps): JSX.Element { const params = useParams(); @@ -61,12 +63,22 @@ function RouteTab({ items={items} // eslint-disable-next-line react/jsx-props-no-spreading {...rest} + tabBarExtraContent={ + showRightSection && ( + + ) + } /> ); } RouteTab.defaultProps = { onChangeHandler: undefined, + showRightSection: true, }; export default RouteTab; diff --git a/frontend/src/components/RouteTab/types.ts b/frontend/src/components/RouteTab/types.ts index a4dada0daf4d..6339a1c7496b 100644 --- a/frontend/src/components/RouteTab/types.ts +++ b/frontend/src/components/RouteTab/types.ts @@ -13,4 +13,5 @@ export interface RouteTabProps { activeKey: TabsProps['activeKey']; onChangeHandler?: (key: string) => void; history: History; + showRightSection: boolean; } diff --git a/frontend/src/container/LogsError/LogsError.tsx b/frontend/src/container/LogsError/LogsError.tsx index d0f058cc3e0c..d122228d4195 100644 --- a/frontend/src/container/LogsError/LogsError.tsx +++ b/frontend/src/container/LogsError/LogsError.tsx @@ -17,6 +17,7 @@ export default function LogsError(): JSX.Element { window.open('https://signoz.io/slack', '_blank'); } }; + return (
diff --git a/frontend/src/container/NewDashboard/DashboardDescription/Description.styles.scss b/frontend/src/container/NewDashboard/DashboardDescription/Description.styles.scss index 0f4b2dcc9565..f2b20f0d37cc 100644 --- a/frontend/src/container/NewDashboard/DashboardDescription/Description.styles.scss +++ b/frontend/src/container/NewDashboard/DashboardDescription/Description.styles.scss @@ -57,14 +57,14 @@ border-bottom: 1px solid var(--bg-slate-400); display: flex; justify-content: space-between; + gap: 16px; align-items: center; - margin-right: 16px; + padding: 0 8px; box-sizing: border-box; .dashboard-breadcrumbs { width: 100%; height: 48px; - padding: 16px; display: flex; gap: 6px; align-items: center; diff --git a/frontend/src/container/NewDashboard/DashboardDescription/index.tsx b/frontend/src/container/NewDashboard/DashboardDescription/index.tsx index 287cf1219ca8..154c63674962 100644 --- a/frontend/src/container/NewDashboard/DashboardDescription/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardDescription/index.tsx @@ -12,6 +12,7 @@ import { Typography, } from 'antd'; import logEvent from 'api/common/logEvent'; +import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection'; import { QueryParams } from 'constants/query'; import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder'; import ROUTES from 'constants/routes'; @@ -321,6 +322,12 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element { {title} + +
diff --git a/frontend/src/container/TopNav/DateTimeSelectionV2/DateTimeSelectionV2.styles.scss b/frontend/src/container/TopNav/DateTimeSelectionV2/DateTimeSelectionV2.styles.scss index a2ff345dad2c..70806fcf4133 100644 --- a/frontend/src/container/TopNav/DateTimeSelectionV2/DateTimeSelectionV2.styles.scss +++ b/frontend/src/container/TopNav/DateTimeSelectionV2/DateTimeSelectionV2.styles.scss @@ -76,51 +76,8 @@ } } -.share-modal-content { - display: flex; - flex-direction: column; - gap: 8px; - padding: 16px; - width: 420px; - - .absolute-relative-time-toggler-container { - display: flex; - gap: 8px; - align-items: center; - } - - .absolute-relative-time-toggler { - display: flex; - gap: 4px; - align-items: center; - } - - .absolute-relative-time-error { - font-size: 12px; - color: var(--bg-amber-600); - } - - .share-link { - display: flex; - align-items: center; - - .share-url { - flex: 1; - border: 1px solid var(--bg-slate-400); - border-radius: 2px; - background: var(--bg-ink-300); - height: 32px; - padding: 6px 8px; - } - - .copy-url-btn { - width: 32px; - } - } -} - .date-time-root, -.shareable-link-popover-root { +.header-section-popover-root { .ant-popover-inner { border-radius: 4px !important; border: 1px solid var(--bg-slate-400); @@ -359,7 +316,7 @@ } .date-time-root, - .shareable-link-popover-root { + .header-section-popover-root { .ant-popover-inner { border: 1px solid var(--bg-vanilla-400); background: var(--bg-vanilla-100) !important; @@ -471,14 +428,6 @@ } } - .share-modal-content { - .share-link { - .share-url { - border: 1px solid var(--bg-vanilla-300); - background: var(--bg-vanilla-100); - } - } - } .reset-button { background: var(--bg-vanilla-100); border-color: var(--bg-vanilla-300); diff --git a/frontend/src/container/TopNav/DateTimeSelectionV2/index.tsx b/frontend/src/container/TopNav/DateTimeSelectionV2/index.tsx index e4dbc0e1b553..12ea0ab63ab7 100644 --- a/frontend/src/container/TopNav/DateTimeSelectionV2/index.tsx +++ b/frontend/src/container/TopNav/DateTimeSelectionV2/index.tsx @@ -1,8 +1,7 @@ import './DateTimeSelectionV2.styles.scss'; import { SyncOutlined } from '@ant-design/icons'; -import { Color } from '@signozhq/design-tokens'; -import { Button, Popover, Switch, Typography } from 'antd'; +import { Button } from 'antd'; import getLocalStorageKey from 'api/browser/localstorage/get'; import setLocalStorageKey from 'api/browser/localstorage/set'; import CustomTimePicker from 'components/CustomTimePicker/CustomTimePicker'; @@ -15,16 +14,15 @@ import dayjs, { Dayjs } from 'dayjs'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useSafeNavigate } from 'hooks/useSafeNavigate'; import useUrlQuery from 'hooks/useUrlQuery'; -import GetMinMax, { isValidTimeFormat } from 'lib/getMinMax'; +import { isValidTimeFormat } from 'lib/getMinMax'; import getTimeString from 'lib/getTimeString'; import { cloneDeep, isObject } from 'lodash-es'; -import { Check, Copy, Info, Send, Undo } from 'lucide-react'; +import { Undo } from 'lucide-react'; import { useTimezone } from 'providers/Timezone'; import { useCallback, useEffect, useState } from 'react'; import { connect, useDispatch, useSelector } from 'react-redux'; import { RouteComponentProps, withRouter } from 'react-router-dom'; import { useNavigationType, useSearchParams } from 'react-router-dom-v5-compat'; -import { useCopyToClipboard } from 'react-use'; import { bindActionCreators, Dispatch } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; import { GlobalTimeLoading, UpdateTimeInterval } from 'store/actions'; @@ -53,7 +51,6 @@ import { Form, FormContainer, FormItem } from './styles'; function DateTimeSelection({ showAutoRefresh, showRefreshText = true, - hideShareModal = false, location, updateTimeInterval, globalTimeLoading, @@ -81,10 +78,6 @@ function DateTimeSelection({ const searchStartTime = urlQuery.get('startTime'); const searchEndTime = urlQuery.get('endTime'); const relativeTimeFromUrl = urlQuery.get(QueryParams.relativeTime); - const [enableAbsoluteTime, setEnableAbsoluteTime] = useState(false); - const [isValidteRelativeTime, setIsValidteRelativeTime] = useState(false); - const [, handleCopyToClipboard] = useCopyToClipboard(); - const [isURLCopied, setIsURLCopied] = useState(false); // Prioritize props for initial modal time, fallback to URL params let initialModalStartTime = 0; @@ -324,7 +317,6 @@ function DateTimeSelection({ if (isModalTimeSelection) { if (value === 'custom') { setCustomDTPickerVisible(true); - setIsValidteRelativeTime(false); return; } onTimeChange?.(value); @@ -334,15 +326,12 @@ function DateTimeSelection({ setIsOpen(false); updateTimeInterval(value); updateLocalStorageForRoutes(value); - setIsValidteRelativeTime(true); if (refreshButtonHidden) { setRefreshButtonHidden(false); } } else { setRefreshButtonHidden(true); setCustomDTPickerVisible(true); - setIsValidteRelativeTime(false); - setEnableAbsoluteTime(false); return; } @@ -458,11 +447,6 @@ function DateTimeSelection({ urlQuery.delete('startTime'); urlQuery.delete('endTime'); - setIsValidteRelativeTime(true); - - urlQuery.delete('startTime'); - urlQuery.delete('endTime'); - urlQuery.set(QueryParams.relativeTime, dateTimeStr); const generatedUrl = `${location.pathname}?${urlQuery.toString()}`; @@ -542,7 +526,6 @@ function DateTimeSelection({ const handleRelativeTimeSync = useCallback( (relativeTime: string): void => { updateTimeInterval(relativeTime as Time); - setIsValidteRelativeTime(true); setRefreshButtonHidden(false); }, [updateTimeInterval], @@ -625,8 +608,6 @@ function DateTimeSelection({ const updatedTime = getCustomOrIntervalTime(time, currentRoute); - setIsValidteRelativeTime(updatedTime !== 'custom'); - const [preStartTime = 0, preEndTime = 0] = getTime() || []; setRefreshButtonHidden(updatedTime === 'custom'); @@ -654,95 +635,6 @@ function DateTimeSelection({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [location.pathname, updateTimeInterval, globalTimeLoading]); - // eslint-disable-next-line sonarjs/cognitive-complexity - const shareModalContent = (): JSX.Element => { - let currentUrl = window.location.href; - - const startTime = urlQuery.get(QueryParams.startTime); - const endTime = urlQuery.get(QueryParams.endTime); - const isCustomTime = !!(startTime && endTime && selectedTime === 'custom'); - - if (enableAbsoluteTime || isCustomTime) { - if (selectedTime === 'custom') { - if (searchStartTime && searchEndTime) { - urlQuery.set(QueryParams.startTime, searchStartTime.toString()); - urlQuery.set(QueryParams.endTime, searchEndTime.toString()); - } - } else { - const { minTime, maxTime } = GetMinMax(selectedTime); - - urlQuery.set(QueryParams.startTime, minTime.toString()); - urlQuery.set(QueryParams.endTime, maxTime.toString()); - } - - urlQuery.delete(QueryParams.relativeTime); - - currentUrl = `${window.location.origin}${ - location.pathname - }?${urlQuery.toString()}`; - } else { - urlQuery.delete(QueryParams.startTime); - urlQuery.delete(QueryParams.endTime); - - urlQuery.set(QueryParams.relativeTime, selectedTime); - currentUrl = `${window.location.origin}${ - location.pathname - }?${urlQuery.toString()}`; - } - - return ( -
-
-
- {(selectedTime === 'custom' || !isValidteRelativeTime) && ( - - )} - { - setEnableAbsoluteTime(!enableAbsoluteTime); - }} - /> -
- - Enable Absolute Time -
- - {(selectedTime === 'custom' || !isValidteRelativeTime) && ( -
- Please select / enter valid relative time to toggle. -
- )} - -
- - {currentUrl} - - -
-
- ); - }; - const { timezone } = useTimezone(); const getSelectedValue = (): string => { @@ -814,9 +706,6 @@ function DateTimeSelection({ onValidCustomDateChange={(dateTime): void => { onValidCustomDateHandler(dateTime.timeStr as CustomTimeType); }} - onCustomTimeStatusUpdate={(isValid: boolean): void => { - setIsValidteRelativeTime(isValid); - }} selectedValue={getSelectedValue()} data-testid="dropDown" items={options} @@ -843,24 +732,6 @@ function DateTimeSelection({
)} - - {!hideShareModal && ( - - - - )}
diff --git a/frontend/src/container/TopNav/TopNav.styles.scss b/frontend/src/container/TopNav/TopNav.styles.scss index e2fd2d2f37e6..125d900048e4 100644 --- a/frontend/src/container/TopNav/TopNav.styles.scss +++ b/frontend/src/container/TopNav/TopNav.styles.scss @@ -1,4 +1,17 @@ .top-nav-container { - padding: 0px 8px; - margin-bottom: 16px; + padding: 8px; + border-bottom: 1px solid var(--bg-slate-500); + + display: flex; + align-items: center; + justify-content: end; + gap: 16px; + + margin-bottom: 8px; +} + +.lightMode { + .top-nav-container { + border-bottom: 1px solid var(--bg-vanilla-300); + } } diff --git a/frontend/src/container/TopNav/index.tsx b/frontend/src/container/TopNav/index.tsx index 4aeeadd9f0c7..50a046366990 100644 --- a/frontend/src/container/TopNav/index.tsx +++ b/frontend/src/container/TopNav/index.tsx @@ -1,6 +1,6 @@ import './TopNav.styles.scss'; -import { Col, Row, Space } from 'antd'; +import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection'; import ROUTES from 'constants/routes'; import { useMemo } from 'react'; import { matchPath, useHistory } from 'react-router-dom'; @@ -46,16 +46,9 @@ function TopNav(): JSX.Element | null { return !isRouteToSkip ? (
- - - - -
- -
-
-
- + + +
) : null; } diff --git a/frontend/src/pages/AlertList/AlertList.styles.scss b/frontend/src/pages/AlertList/AlertList.styles.scss index 0d1c58f36b28..e91325c37b6a 100644 --- a/frontend/src/pages/AlertList/AlertList.styles.scss +++ b/frontend/src/pages/AlertList/AlertList.styles.scss @@ -1,5 +1,5 @@ .alerts-container { - .ant-tabs-nav-wrap:first-of-type { - padding-left: 16px; + .ant-tabs-nav { + padding: 0 8px; } } diff --git a/frontend/src/pages/AlertList/index.tsx b/frontend/src/pages/AlertList/index.tsx index 8df082d995b1..c2d54f8cd698 100644 --- a/frontend/src/pages/AlertList/index.tsx +++ b/frontend/src/pages/AlertList/index.tsx @@ -3,6 +3,7 @@ import './AlertList.styles.scss'; import { Tabs } from 'antd'; import { TabsProps } from 'antd/lib'; import ConfigureIcon from 'assets/AlertHistory/ConfigureIcon'; +import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection'; import ROUTES from 'constants/routes'; import AllAlertRules from 'container/ListAlertRules'; import { PlannedDowntime } from 'container/PlannedDowntime/PlannedDowntime'; @@ -28,7 +29,7 @@ function AllAlertList(): JSX.Element { { label: (
- + Triggered Alerts
), @@ -38,7 +39,7 @@ function AllAlertList(): JSX.Element { { label: (
- + Alert Rules
), @@ -52,7 +53,7 @@ function AllAlertList(): JSX.Element { { label: (
- + Configuration
), @@ -82,6 +83,13 @@ function AllAlertList(): JSX.Element { className={`alerts-container ${ isAlertHistory || isAlertOverview ? 'alert-details-tabs' : '' }`} + tabBarExtraContent={ + + } /> ); } diff --git a/frontend/src/pages/AllErrors/AllErrors.styles.scss b/frontend/src/pages/AllErrors/AllErrors.styles.scss index 07e953a2a7d4..ebc8ad2f8091 100644 --- a/frontend/src/pages/AllErrors/AllErrors.styles.scss +++ b/frontend/src/pages/AllErrors/AllErrors.styles.scss @@ -7,7 +7,12 @@ } .all-errors-right-section { - padding: 0 10px; + .right-toolbar-actions-container { + display: flex; + gap: 8px; + align-items: center; + justify-content: flex-end; + } } .ant-tabs { diff --git a/frontend/src/pages/AllErrors/index.tsx b/frontend/src/pages/AllErrors/index.tsx index d2f048c4a66e..6b4e0749501b 100644 --- a/frontend/src/pages/AllErrors/index.tsx +++ b/frontend/src/pages/AllErrors/index.tsx @@ -5,6 +5,7 @@ import { Button, Tooltip } from 'antd'; import getLocalStorageKey from 'api/browser/localstorage/get'; import setLocalStorageApi from 'api/browser/localstorage/set'; import cx from 'classnames'; +import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection'; import QuickFilters from 'components/QuickFilters/QuickFilters'; import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types'; import RouteTab from 'components/RouteTab'; @@ -74,10 +75,24 @@ function AllErrors(): JSX.Element { ) : undefined } - rightActions={} + rightActions={ +
+ + +
+ } /> - + diff --git a/frontend/src/pages/DashboardsListPage/DashboardsListPage.styles.scss b/frontend/src/pages/DashboardsListPage/DashboardsListPage.styles.scss index af36aadaac2d..a2b7ccde5828 100644 --- a/frontend/src/pages/DashboardsListPage/DashboardsListPage.styles.scss +++ b/frontend/src/pages/DashboardsListPage/DashboardsListPage.styles.scss @@ -2,11 +2,18 @@ .dashboard-header { display: flex; align-items: center; - padding: 16px; + justify-content: space-between; + padding: 0 8px; gap: 8px; height: 48px; border-bottom: 1px solid var(--bg-slate-500); + .dashboard-header-left { + display: flex; + align-items: center; + gap: 8px; + } + .icon { color: var(--bg-vanilla-400); } diff --git a/frontend/src/pages/DashboardsListPage/DashboardsListPage.tsx b/frontend/src/pages/DashboardsListPage/DashboardsListPage.tsx index 6779fd945ae8..f66da0abc2a8 100644 --- a/frontend/src/pages/DashboardsListPage/DashboardsListPage.tsx +++ b/frontend/src/pages/DashboardsListPage/DashboardsListPage.tsx @@ -1,6 +1,7 @@ import './DashboardsListPage.styles.scss'; import { Space, Typography } from 'antd'; +import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection'; import ListOfAllDashboard from 'container/ListOfDashboard'; import { LayoutGrid } from 'lucide-react'; @@ -13,8 +14,16 @@ function DashboardsListPage(): JSX.Element { className="dashboard-list-page" >
- - Dashboards +
+ + Dashboards +
+ +
diff --git a/frontend/src/pages/Settings/Settings.styles.scss b/frontend/src/pages/Settings/Settings.styles.scss index ae31384c0e6b..936ff20c1c19 100644 --- a/frontend/src/pages/Settings/Settings.styles.scss +++ b/frontend/src/pages/Settings/Settings.styles.scss @@ -33,7 +33,6 @@ height: calc(100vh - 48px); border-right: 1px solid var(--Slate-500, #161922); background: var(--Ink-500, #0b0c0e); - padding: 10px 8px; } .settings-page-content { diff --git a/frontend/src/pages/Support/Support.styles.scss b/frontend/src/pages/Support/Support.styles.scss index e298f74d8a54..4d63414a9089 100644 --- a/frontend/src/pages/Support/Support.styles.scss +++ b/frontend/src/pages/Support/Support.styles.scss @@ -4,7 +4,7 @@ padding-right: 48px; max-width: 1400px; - margin: 0 auto; + margin: 64px auto; } .support-channels { @@ -19,6 +19,7 @@ flex: 0 0 calc(33.333% - 32px); min-height: 200px; position: relative; + border: none !important; .support-channel-title { width: 100%; diff --git a/frontend/src/pages/Support/Support.tsx b/frontend/src/pages/Support/Support.tsx index 7e8415bde637..0b3dad39fff1 100644 --- a/frontend/src/pages/Support/Support.tsx +++ b/frontend/src/pages/Support/Support.tsx @@ -205,6 +205,7 @@ export default function Support(): JSX.Element {
, From 4d8d0223e7917ceb96f53d56efc9073587d0db6c Mon Sep 17 00:00:00 2001 From: Vikrant Gupta Date: Wed, 24 Sep 2025 14:54:43 +0530 Subject: [PATCH 2/4] fix(integration): fix tests (#9168) * fix(integration): fix tests * fix(integration): fix tests --- .devenv/docker/signoz-otel-collector/compose.yaml | 2 +- .github/workflows/integrationci.yaml | 3 +-- docs/contributing/go/integration.md | 2 +- tests/integration/conftest.py | 4 ++-- tests/integration/fixtures/logs.py | 7 +++++-- tests/integration/fixtures/traces.py | 2 ++ 6 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.devenv/docker/signoz-otel-collector/compose.yaml b/.devenv/docker/signoz-otel-collector/compose.yaml index 62a931b38de5..eac598f6de69 100644 --- a/.devenv/docker/signoz-otel-collector/compose.yaml +++ b/.devenv/docker/signoz-otel-collector/compose.yaml @@ -1,6 +1,6 @@ services: signoz-otel-collector: - image: signoz/signoz-otel-collector:v0.128.2 + image: signoz/signoz-otel-collector:v0.129.6 container_name: signoz-otel-collector-dev command: - --config=/etc/otel-collector-config.yaml diff --git a/.github/workflows/integrationci.yaml b/.github/workflows/integrationci.yaml index d2683a65375b..35efa27f6e36 100644 --- a/.github/workflows/integrationci.yaml +++ b/.github/workflows/integrationci.yaml @@ -21,10 +21,9 @@ jobs: - postgres - sqlite clickhouse-version: - - 24.1.2-alpine - 25.5.6 schema-migrator-version: - - v0.128.1 + - v0.129.6 postgres-version: - 15 if: | diff --git a/docs/contributing/go/integration.md b/docs/contributing/go/integration.md index 6e1074e6126b..980f4f23ec29 100644 --- a/docs/contributing/go/integration.md +++ b/docs/contributing/go/integration.md @@ -192,7 +192,7 @@ Tests can be configured using pytest options: - `--sqlstore-provider` - Choose database provider (default: postgres) - `--postgres-version` - PostgreSQL version (default: 15) -- `--clickhouse-version` - ClickHouse version (default: 24.1.2-alpine) +- `--clickhouse-version` - ClickHouse version (default: 25.5.6) - `--zookeeper-version` - Zookeeper version (default: 3.7.1) Example: diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 9f6de991b01f..c4c04fbd599c 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -45,7 +45,7 @@ def pytest_addoption(parser: pytest.Parser): parser.addoption( "--clickhouse-version", action="store", - default="24.1.2-alpine", + default="25.5.6", help="clickhouse version", ) parser.addoption( @@ -57,6 +57,6 @@ def pytest_addoption(parser: pytest.Parser): parser.addoption( "--schema-migrator-version", action="store", - default="v0.128.2", + default="v0.129.6", help="schema migrator version", ) diff --git a/tests/integration/fixtures/logs.py b/tests/integration/fixtures/logs.py index 479189401dd7..c12567d54a51 100644 --- a/tests/integration/fixtures/logs.py +++ b/tests/integration/fixtures/logs.py @@ -29,7 +29,7 @@ class LogsResource(ABC): self.seen_at_ts_bucket_start = seen_at_ts_bucket_start def np_arr(self) -> np.array: - return np.array([self.labels, self.fingerprint, self.seen_at_ts_bucket_start]) + return np.array([self.labels, self.fingerprint, self.seen_at_ts_bucket_start, np.uint64(10),np.uint64(15)]) class LogsResourceOrAttributeKeys(ABC): @@ -317,6 +317,9 @@ class Logs(ABC): self.scope_name, self.scope_version, self.scope_string, + np.uint64(10), + np.uint64(15), + self.resources_string, ] ) @@ -378,7 +381,7 @@ def insert_logs( table="distributed_logs_resource_keys", data=[resource_key.np_arr() for resource_key in resource_keys], ) - + clickhouse.conn.insert( database="signoz_logs", table="distributed_logs_v2", diff --git a/tests/integration/fixtures/traces.py b/tests/integration/fixtures/traces.py index 4d0f025bd4a2..62fa209a6074 100644 --- a/tests/integration/fixtures/traces.py +++ b/tests/integration/fixtures/traces.py @@ -593,6 +593,7 @@ class Traces(ABC): self.db_operation, self.has_error, self.is_remote, + self.resources_string, ], dtype=object, ) @@ -681,6 +682,7 @@ def insert_traces( "db_operation", "has_error", "is_remote", + "resource", ], data=[trace.np_arr() for trace in traces], ) From c68096152d645ba0380aa4864f99e9dcc15dfa37 Mon Sep 17 00:00:00 2001 From: Vikrant Gupta Date: Wed, 24 Sep 2025 15:10:29 +0530 Subject: [PATCH 3/4] chore(clickhouse): bump ch-go (#9169) * fix(integration): fix tests * fix(integration): fix tests * chore(clickhouse): bump ch-go --------- Co-authored-by: Nityananda Gohain --- go.mod | 14 +++++++------- go.sum | 32 ++++++++++++++++---------------- pkg/instrumentation/sdk.go | 2 +- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/go.mod b/go.mod index 042661cbbf63..9dac960add84 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.24.0 require ( dario.cat/mergo v1.0.1 github.com/AfterShip/clickhouse-sql-parser v0.4.11 - github.com/ClickHouse/clickhouse-go/v2 v2.36.0 + github.com/ClickHouse/clickhouse-go/v2 v2.40.1 github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd github.com/SigNoz/signoz-otel-collector v0.129.4 @@ -66,16 +66,16 @@ require ( go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 go.opentelemetry.io/otel v1.37.0 go.opentelemetry.io/otel/metric v1.37.0 - go.opentelemetry.io/otel/sdk v1.36.0 + go.opentelemetry.io/otel/sdk v1.37.0 go.opentelemetry.io/otel/trace v1.37.0 go.uber.org/multierr v1.11.0 go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.39.0 + golang.org/x/crypto v0.40.0 golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b - golang.org/x/net v0.41.0 + golang.org/x/net v0.42.0 golang.org/x/oauth2 v0.30.0 golang.org/x/sync v0.16.0 - golang.org/x/text v0.26.0 + golang.org/x/text v0.27.0 google.golang.org/protobuf v1.36.6 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 @@ -91,11 +91,11 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect - github.com/ClickHouse/ch-go v0.66.0 // indirect + github.com/ClickHouse/ch-go v0.67.0 // indirect github.com/Masterminds/squirrel v1.5.4 // indirect github.com/Yiling-J/theine-go v0.6.1 // indirect github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect - github.com/andybalholm/brotli v1.1.1 // indirect + github.com/andybalholm/brotli v1.2.0 // indirect github.com/armon/go-metrics v0.4.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/aws/aws-sdk-go v1.55.7 // indirect diff --git a/go.sum b/go.sum index 812cb8ce2cf7..383be578088d 100644 --- a/go.sum +++ b/go.sum @@ -87,10 +87,10 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJ github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/ClickHouse/ch-go v0.66.0 h1:hLslxxAVb2PHpbHr4n0d6aP8CEIpUYGMVT1Yj/Q5Img= -github.com/ClickHouse/ch-go v0.66.0/go.mod h1:noiHWyLMJAZ5wYuq3R/K0TcRhrNA8h7o1AqHX0klEhM= -github.com/ClickHouse/clickhouse-go/v2 v2.36.0 h1:FJ03h8VdmBUhvR9nQEu5jRLdfG0c/HSxUjiNdOxRQww= -github.com/ClickHouse/clickhouse-go/v2 v2.36.0/go.mod h1:aijX64fKD1hAWu/zqWEmiGk7wRE8ZnpN0M3UvjsZG3I= +github.com/ClickHouse/ch-go v0.67.0 h1:18MQF6vZHj+4/hTRaK7JbS/TIzn4I55wC+QzO24uiqc= +github.com/ClickHouse/ch-go v0.67.0/go.mod h1:2MSAeyVmgt+9a2k2SQPPG1b4qbTPzdGDpf1+bcHh+18= +github.com/ClickHouse/clickhouse-go/v2 v2.40.1 h1:PbwsHBgqXRydU7jKULD1C8CHmifczffvQqmFvltM2W4= +github.com/ClickHouse/clickhouse-go/v2 v2.40.1/go.mod h1:GDzSBLVhladVm8V01aEB36IoBOVLLICfyeuiIp/8Ezc= github.com/Code-Hex/go-generics-cache v1.5.1 h1:6vhZGc5M7Y/YD8cIUcY8kcuQLB4cHR7U+0KMqAA0KcU= github.com/Code-Hex/go-generics-cache v1.5.1/go.mod h1:qxcC9kRVrct9rHeiYpFWSoW1vxyillCVzX13KZG8dl4= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= @@ -116,8 +116,8 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= -github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= -github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= @@ -1201,8 +1201,8 @@ go.opentelemetry.io/otel/log/logtest v0.0.0-20250526142609-aa5bd0e64989 h1:4JF7o go.opentelemetry.io/otel/log/logtest v0.0.0-20250526142609-aa5bd0e64989/go.mod h1:NToOxLDCS1tXDSB2dIj44H9xGPOpKr0csIN+gnuihv4= go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= -go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= go.opentelemetry.io/otel/sdk/log v0.12.2 h1:yNoETvTByVKi7wHvYS6HMcZrN5hFLD7I++1xIZ/k6W0= go.opentelemetry.io/otel/sdk/log v0.12.2/go.mod h1:DcpdmUXHJgSqN/dh+XMWa7Vf89u9ap0/AAk/XGLnEzY= go.opentelemetry.io/otel/sdk/log/logtest v0.0.0-20250521073539-a85ae98dcedc h1:uqxdywfHqqCl6LmZzI3pUnXT1RGFYyUgxj0AkWPFxi0= @@ -1245,8 +1245,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= -golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1337,8 +1337,8 @@ golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1468,8 +1468,8 @@ golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= +golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1480,8 +1480,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/pkg/instrumentation/sdk.go b/pkg/instrumentation/sdk.go index 89e03fb27a8c..1228136d473a 100644 --- a/pkg/instrumentation/sdk.go +++ b/pkg/instrumentation/sdk.go @@ -11,7 +11,7 @@ import ( contribsdkconfig "go.opentelemetry.io/contrib/config" sdkmetric "go.opentelemetry.io/otel/metric" sdkresource "go.opentelemetry.io/otel/sdk/resource" - semconv "go.opentelemetry.io/otel/semconv/v1.26.0" + semconv "go.opentelemetry.io/otel/semconv/v1.34.0" sdktrace "go.opentelemetry.io/otel/trace" ) From 9114b44c0e1c11fca0636179e0a1081d4e561c5d Mon Sep 17 00:00:00 2001 From: SagarRajput-7 <162284829+SagarRajput-7@users.noreply.github.com> Date: Wed, 24 Sep 2025 22:37:31 +0530 Subject: [PATCH 4/4] fix: correctly set and unset the stackbarchart value across panel types (#9158) --- .../NewWidget/RightContainer/index.tsx | 1 + .../NewWidget/__test__/NewWidget.test.tsx | 114 ++++++++++++++- .../__test__/__mocks__/uplotChartData.ts | 134 ++++++++++++++++++ .../__test__/getUplotChartData.test.ts | 50 +++++++ frontend/src/container/NewWidget/index.tsx | 7 + frontend/src/container/NewWidget/utils.ts | 2 +- .../PanelWrapper/UplotPanelWrapper.tsx | 21 ++- 7 files changed, 318 insertions(+), 11 deletions(-) create mode 100644 frontend/src/container/NewWidget/__test__/__mocks__/uplotChartData.ts create mode 100644 frontend/src/container/NewWidget/__test__/getUplotChartData.test.ts diff --git a/frontend/src/container/NewWidget/RightContainer/index.tsx b/frontend/src/container/NewWidget/RightContainer/index.tsx index 4a374d977d07..8e1d3586f5fa 100644 --- a/frontend/src/container/NewWidget/RightContainer/index.tsx +++ b/frontend/src/container/NewWidget/RightContainer/index.tsx @@ -300,6 +300,7 @@ function RightContainer({ style={{ width: '100%' }} className="panel-type-select" data-testid="panel-change-select" + data-stacking-state={stackedBarChart ? 'true' : 'false'} > {graphTypes.map((item) => (