mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-17 15:36:48 +00:00
Merge branch 'main' into ux-changes
This commit is contained in:
commit
35192eecd8
@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
signoz-otel-collector:
|
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
|
container_name: signoz-otel-collector-dev
|
||||||
command:
|
command:
|
||||||
- --config=/etc/otel-collector-config.yaml
|
- --config=/etc/otel-collector-config.yaml
|
||||||
|
|||||||
3
.github/workflows/integrationci.yaml
vendored
3
.github/workflows/integrationci.yaml
vendored
@ -21,10 +21,9 @@ jobs:
|
|||||||
- postgres
|
- postgres
|
||||||
- sqlite
|
- sqlite
|
||||||
clickhouse-version:
|
clickhouse-version:
|
||||||
- 24.1.2-alpine
|
|
||||||
- 25.5.6
|
- 25.5.6
|
||||||
schema-migrator-version:
|
schema-migrator-version:
|
||||||
- v0.128.1
|
- v0.129.6
|
||||||
postgres-version:
|
postgres-version:
|
||||||
- 15
|
- 15
|
||||||
if: |
|
if: |
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -230,4 +230,6 @@ poetry.toml
|
|||||||
# LSP config files
|
# LSP config files
|
||||||
pyrightconfig.json
|
pyrightconfig.json
|
||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/python
|
# End of https://www.toptal.com/developers/gitignore/api/python
|
||||||
|
|
||||||
|
frontend/.cursor/rules/
|
||||||
@ -192,7 +192,7 @@ Tests can be configured using pytest options:
|
|||||||
|
|
||||||
- `--sqlstore-provider` - Choose database provider (default: postgres)
|
- `--sqlstore-provider` - Choose database provider (default: postgres)
|
||||||
- `--postgres-version` - PostgreSQL version (default: 15)
|
- `--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)
|
- `--zookeeper-version` - Zookeeper version (default: 3.7.1)
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|||||||
@ -0,0 +1,15 @@
|
|||||||
|
import { Typography } from 'antd';
|
||||||
|
|
||||||
|
function AnnouncementsModal(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="announcements-modal-container">
|
||||||
|
<div className="announcements-modal-container-header">
|
||||||
|
<Typography.Text className="announcements-modal-title">
|
||||||
|
Announcements
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AnnouncementsModal;
|
||||||
160
frontend/src/components/HeaderRightSection/FeedbackModal.tsx
Normal file
160
frontend/src/components/HeaderRightSection/FeedbackModal.tsx
Normal file
@ -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<void> => {
|
||||||
|
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: (
|
||||||
|
<div className="feedback-modal-tab-label">
|
||||||
|
<div className="tab-icon dot feedback-tab" />
|
||||||
|
Feedback
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
key: 'feedback',
|
||||||
|
value: 'feedback',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<div className="feedback-modal-tab-label">
|
||||||
|
<div className="tab-icon dot bug-tab" />
|
||||||
|
Report a bug
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
key: 'reportBug',
|
||||||
|
value: 'reportBug',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<div className="feedback-modal-tab-label">
|
||||||
|
<div className="tab-icon dot feature-tab" />
|
||||||
|
Feature request
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
key: 'featureRequest',
|
||||||
|
value: 'featureRequest',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleFeedbackChange = (
|
||||||
|
e: React.ChangeEvent<HTMLTextAreaElement>,
|
||||||
|
): void => {
|
||||||
|
setFeedback(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleContactSupportClick = useCallback((): void => {
|
||||||
|
handleContactSupport(isCloudUserVal);
|
||||||
|
}, [isCloudUserVal]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="feedback-modal-container">
|
||||||
|
<div className="feedback-modal-header">
|
||||||
|
<Radio.Group
|
||||||
|
value={activeTab}
|
||||||
|
defaultValue={activeTab}
|
||||||
|
optionType="button"
|
||||||
|
className="feedback-modal-tabs"
|
||||||
|
options={items}
|
||||||
|
onChange={(e: RadioChangeEvent): void => setActiveTab(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="feedback-modal-content">
|
||||||
|
<div className="feedback-modal-content-header">
|
||||||
|
<Input.TextArea
|
||||||
|
placeholder="Write your feedback here..."
|
||||||
|
rows={6}
|
||||||
|
required
|
||||||
|
className="feedback-input"
|
||||||
|
value={feedback}
|
||||||
|
onChange={handleFeedbackChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="feedback-modal-content-footer">
|
||||||
|
<Button
|
||||||
|
className="periscope-btn primary"
|
||||||
|
type="primary"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
loading={isLoading}
|
||||||
|
disabled={feedback.length === 0}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
<div className="feedback-modal-content-footer-info-text">
|
||||||
|
<Typography.Text>
|
||||||
|
Have a specific issue?{' '}
|
||||||
|
<Typography.Link
|
||||||
|
className="contact-support-link"
|
||||||
|
onClick={handleContactSupportClick}
|
||||||
|
>
|
||||||
|
Contact Support{' '}
|
||||||
|
</Typography.Link>
|
||||||
|
or{' '}
|
||||||
|
<a
|
||||||
|
href="https://signoz.io/docs/introduction/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="read-docs-link"
|
||||||
|
>
|
||||||
|
Read our docs
|
||||||
|
</a>
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FeedbackModal;
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 (
|
||||||
|
<div className="header-right-section-container">
|
||||||
|
{enableFeedback && (
|
||||||
|
<Popover
|
||||||
|
rootClassName="header-section-popover-root"
|
||||||
|
className="shareable-link-popover"
|
||||||
|
placement="bottomRight"
|
||||||
|
content={<FeedbackModal onClose={handleCloseFeedbackModal} />}
|
||||||
|
destroyTooltipOnHide
|
||||||
|
arrow={false}
|
||||||
|
trigger="click"
|
||||||
|
open={openFeedbackModal}
|
||||||
|
onOpenChange={handleOpenFeedbackModalChange}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
className="share-feedback-btn periscope-btn ghost"
|
||||||
|
icon={<SquarePen size={14} />}
|
||||||
|
onClick={handleOpenFeedbackModal}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{enableAnnouncements && (
|
||||||
|
<Popover
|
||||||
|
rootClassName="header-section-popover-root"
|
||||||
|
className="shareable-link-popover"
|
||||||
|
placement="bottomRight"
|
||||||
|
content={<AnnouncementsModal />}
|
||||||
|
arrow={false}
|
||||||
|
destroyTooltipOnHide
|
||||||
|
trigger="click"
|
||||||
|
open={openAnnouncementsModal}
|
||||||
|
onOpenChange={handleOpenAnnouncementsModalChange}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
icon={<Inbox size={14} />}
|
||||||
|
className="periscope-btn ghost announcements-btn"
|
||||||
|
onClick={(): void => {
|
||||||
|
logEvent('Announcements: Clicked', {
|
||||||
|
page: location.pathname,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{enableShare && (
|
||||||
|
<Popover
|
||||||
|
rootClassName="header-section-popover-root"
|
||||||
|
className="shareable-link-popover"
|
||||||
|
placement="bottomRight"
|
||||||
|
content={<ShareURLModal />}
|
||||||
|
open={openShareURLModal}
|
||||||
|
destroyTooltipOnHide
|
||||||
|
arrow={false}
|
||||||
|
trigger="click"
|
||||||
|
onOpenChange={handleOpenShareURLModalChange}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
className="share-link-btn periscope-btn ghost"
|
||||||
|
icon={<Globe size={14} />}
|
||||||
|
onClick={handleOpenShareURLModal}
|
||||||
|
>
|
||||||
|
Share
|
||||||
|
</Button>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HeaderRightSection;
|
||||||
171
frontend/src/components/HeaderRightSection/ShareURLModal.tsx
Normal file
171
frontend/src/components/HeaderRightSection/ShareURLModal.tsx
Normal file
@ -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<AppState, GlobalReducer>(
|
||||||
|
(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 (
|
||||||
|
<div className="share-modal-content">
|
||||||
|
{(shareURLWithTime || isRouteToBeSharedWithTime) && (
|
||||||
|
<>
|
||||||
|
<div className="absolute-relative-time-toggler-container">
|
||||||
|
<Typography.Text className="absolute-relative-time-toggler-label">
|
||||||
|
Enable absolute time
|
||||||
|
</Typography.Text>
|
||||||
|
|
||||||
|
<div className="absolute-relative-time-toggler">
|
||||||
|
{!isValidateRelativeTime && (
|
||||||
|
<Info size={14} color={Color.BG_AMBER_600} />
|
||||||
|
)}
|
||||||
|
<Switch
|
||||||
|
checked={enableAbsoluteTime}
|
||||||
|
disabled={!isValidateRelativeTime}
|
||||||
|
size="small"
|
||||||
|
onChange={(): void => {
|
||||||
|
setEnableAbsoluteTime((prev) => !prev);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isValidateRelativeTime && (
|
||||||
|
<div className="absolute-relative-time-error">
|
||||||
|
Please select / enter valid relative time to toggle.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="share-link">
|
||||||
|
<div className="url-share-container">
|
||||||
|
<div className="url-share-container-header">
|
||||||
|
<Typography.Text className="url-share-title">
|
||||||
|
Share page link
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text className="url-share-sub-title">
|
||||||
|
Share the current page link with your team member
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="periscope-btn secondary"
|
||||||
|
onClick={handleCopyURL}
|
||||||
|
icon={isURLCopied ? <Check size={14} /> : <Link2 size={14} />}
|
||||||
|
>
|
||||||
|
Copy page link
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ShareURLModal;
|
||||||
@ -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(<AnnouncementsModal />);
|
||||||
|
|
||||||
|
expect(screen.getByText('Announcements')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have proper structure and classes', () => {
|
||||||
|
render(<AnnouncementsModal />);
|
||||||
|
|
||||||
|
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(<AnnouncementsModal />)).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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<typeof logEvent>;
|
||||||
|
const mockUseLocation = useLocation as jest.Mock;
|
||||||
|
const mockUseGetTenantLicense = useGetTenantLicense as jest.Mock;
|
||||||
|
const mockHandleContactSupport = handleContactSupport as jest.Mock;
|
||||||
|
const mockToast = toast as jest.Mocked<typeof toast>;
|
||||||
|
|
||||||
|
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(<FeedbackModal onClose={mockOnClose} />);
|
||||||
|
|
||||||
|
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(<FeedbackModal onClose={mockOnClose} />);
|
||||||
|
|
||||||
|
// 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(<FeedbackModal onClose={mockOnClose} />);
|
||||||
|
|
||||||
|
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(<FeedbackModal onClose={mockOnClose} />);
|
||||||
|
|
||||||
|
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(<FeedbackModal onClose={mockOnClose} />);
|
||||||
|
|
||||||
|
// 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(<FeedbackModal onClose={mockOnClose} />);
|
||||||
|
|
||||||
|
// 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(<FeedbackModal onClose={mockOnClose} />);
|
||||||
|
|
||||||
|
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(<FeedbackModal onClose={mockOnClose} />);
|
||||||
|
|
||||||
|
const contactSupportLink = screen.getByText('Contact Support');
|
||||||
|
await user.click(contactSupportLink);
|
||||||
|
|
||||||
|
expect(mockHandleContactSupport).toHaveBeenCalledWith(isCloudUser);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render docs link with correct attributes', () => {
|
||||||
|
render(<FeedbackModal onClose={mockOnClose} />);
|
||||||
|
|
||||||
|
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(<FeedbackModal onClose={mockOnClose} />);
|
||||||
|
|
||||||
|
// 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(<FeedbackModal onClose={mockOnClose} />);
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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 => (
|
||||||
|
<div data-testid="feedback-modal">
|
||||||
|
<button onClick={onClose} type="button">
|
||||||
|
Close Feedback
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../ShareURLModal', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: (): JSX.Element => (
|
||||||
|
<div data-testid="share-modal">Share URL Modal</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../AnnouncementsModal', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: (): JSX.Element => (
|
||||||
|
<div data-testid="announcements-modal">Announcements Modal</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
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(<HeaderRightSection {...defaultProps} />);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<HeaderRightSection
|
||||||
|
enableAnnouncements={false}
|
||||||
|
enableShare={false}
|
||||||
|
enableFeedback
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
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(<HeaderRightSection {...defaultProps} />);
|
||||||
|
|
||||||
|
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(<HeaderRightSection {...defaultProps} />);
|
||||||
|
|
||||||
|
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(<HeaderRightSection {...defaultProps} />);
|
||||||
|
|
||||||
|
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(<HeaderRightSection {...defaultProps} />);
|
||||||
|
|
||||||
|
// 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(<HeaderRightSection {...defaultProps} />);
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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(<ShareURLModal />);
|
||||||
|
|
||||||
|
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(<ShareURLModal />);
|
||||||
|
|
||||||
|
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(<ShareURLModal />);
|
||||||
|
|
||||||
|
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(<ShareURLModal />);
|
||||||
|
|
||||||
|
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(<ShareURLModal />);
|
||||||
|
|
||||||
|
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(<ShareURLModal />);
|
||||||
|
|
||||||
|
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(<ShareURLModal />);
|
||||||
|
|
||||||
|
// 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(<ShareURLModal />);
|
||||||
|
|
||||||
|
// 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(<ShareURLModal />);
|
||||||
|
|
||||||
|
// 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(<ShareURLModal />);
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { Tabs, TabsProps } from 'antd';
|
import { Tabs, TabsProps } from 'antd';
|
||||||
|
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
|
||||||
import {
|
import {
|
||||||
generatePath,
|
generatePath,
|
||||||
matchPath,
|
matchPath,
|
||||||
@ -17,6 +18,7 @@ function RouteTab({
|
|||||||
activeKey,
|
activeKey,
|
||||||
onChangeHandler,
|
onChangeHandler,
|
||||||
history,
|
history,
|
||||||
|
showRightSection,
|
||||||
...rest
|
...rest
|
||||||
}: RouteTabProps & TabsProps): JSX.Element {
|
}: RouteTabProps & TabsProps): JSX.Element {
|
||||||
const params = useParams<Params>();
|
const params = useParams<Params>();
|
||||||
@ -61,12 +63,22 @@ function RouteTab({
|
|||||||
items={items}
|
items={items}
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
{...rest}
|
{...rest}
|
||||||
|
tabBarExtraContent={
|
||||||
|
showRightSection && (
|
||||||
|
<HeaderRightSection
|
||||||
|
enableAnnouncements={false}
|
||||||
|
enableShare
|
||||||
|
enableFeedback
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
RouteTab.defaultProps = {
|
RouteTab.defaultProps = {
|
||||||
onChangeHandler: undefined,
|
onChangeHandler: undefined,
|
||||||
|
showRightSection: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RouteTab;
|
export default RouteTab;
|
||||||
|
|||||||
@ -13,4 +13,5 @@ export interface RouteTabProps {
|
|||||||
activeKey: TabsProps['activeKey'];
|
activeKey: TabsProps['activeKey'];
|
||||||
onChangeHandler?: (key: string) => void;
|
onChangeHandler?: (key: string) => void;
|
||||||
history: History<unknown>;
|
history: History<unknown>;
|
||||||
|
showRightSection: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,7 @@ export default function LogsError(): JSX.Element {
|
|||||||
window.open('https://signoz.io/slack', '_blank');
|
window.open('https://signoz.io/slack', '_blank');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="logs-error-container">
|
<div className="logs-error-container">
|
||||||
<div className="logs-error-content">
|
<div className="logs-error-content">
|
||||||
|
|||||||
@ -57,14 +57,14 @@
|
|||||||
border-bottom: 1px solid var(--bg-slate-400);
|
border-bottom: 1px solid var(--bg-slate-400);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-right: 16px;
|
padding: 0 8px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
.dashboard-breadcrumbs {
|
.dashboard-breadcrumbs {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
padding: 16px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
|
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
@ -321,6 +322,12 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
|||||||
{title}
|
{title}
|
||||||
</Button>
|
</Button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<HeaderRightSection
|
||||||
|
enableAnnouncements={false}
|
||||||
|
enableShare
|
||||||
|
enableFeedback
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<section className="dashboard-details">
|
<section className="dashboard-details">
|
||||||
<div className="left-section">
|
<div className="left-section">
|
||||||
|
|||||||
@ -300,6 +300,7 @@ function RightContainer({
|
|||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
className="panel-type-select"
|
className="panel-type-select"
|
||||||
data-testid="panel-change-select"
|
data-testid="panel-change-select"
|
||||||
|
data-stacking-state={stackedBarChart ? 'true' : 'false'}
|
||||||
>
|
>
|
||||||
{graphTypes.map((item) => (
|
{graphTypes.map((item) => (
|
||||||
<Option key={item.name} value={item.name}>
|
<Option key={item.name} value={item.name}>
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
// This test suite covers several important scenarios:
|
// This test suite covers several important scenarios:
|
||||||
// - Empty layout - widget should be placed at origin (0,0)
|
// - Empty layout - widget should be placed at origin (0,0)
|
||||||
// - Empty layout with custom dimensions
|
// - Empty layout with custom dimensions
|
||||||
@ -6,13 +7,20 @@
|
|||||||
// - Handling multiple rows correctly
|
// - Handling multiple rows correctly
|
||||||
// - Handling widgets with different heights
|
// - Handling widgets with different heights
|
||||||
|
|
||||||
|
import { screen } from '@testing-library/react';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
||||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||||
import { I18nextProvider } from 'react-i18next';
|
import { I18nextProvider } from 'react-i18next';
|
||||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||||
import i18n from 'ReactI18';
|
import i18n from 'ReactI18';
|
||||||
import { render } from 'tests/test-utils';
|
import {
|
||||||
|
fireEvent,
|
||||||
|
getByText as getByTextUtil,
|
||||||
|
render,
|
||||||
|
userEvent,
|
||||||
|
within,
|
||||||
|
} from 'tests/test-utils';
|
||||||
|
|
||||||
import NewWidget from '..';
|
import NewWidget from '..';
|
||||||
import {
|
import {
|
||||||
@ -21,6 +29,28 @@ import {
|
|||||||
placeWidgetBetweenRows,
|
placeWidgetBetweenRows,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
|
|
||||||
|
// Helper function to check stack series state
|
||||||
|
const checkStackSeriesState = (
|
||||||
|
container: HTMLElement,
|
||||||
|
expectedChecked: boolean,
|
||||||
|
): HTMLElement => {
|
||||||
|
expect(getByTextUtil(container, 'Stack series')).toBeInTheDocument();
|
||||||
|
|
||||||
|
const stackSeriesSection = container.querySelector(
|
||||||
|
'section > .stack-chart',
|
||||||
|
) as HTMLElement;
|
||||||
|
expect(stackSeriesSection).toBeInTheDocument();
|
||||||
|
|
||||||
|
const switchElement = within(stackSeriesSection).getByRole('switch');
|
||||||
|
if (expectedChecked) {
|
||||||
|
expect(switchElement).toBeChecked();
|
||||||
|
} else {
|
||||||
|
expect(switchElement).not.toBeChecked();
|
||||||
|
}
|
||||||
|
|
||||||
|
return switchElement;
|
||||||
|
};
|
||||||
|
|
||||||
const MOCK_SEARCH_PARAMS =
|
const MOCK_SEARCH_PARAMS =
|
||||||
'?graphType=bar&widgetId=b473eef0-8eb5-4dd3-8089-c1817734084f&compositeQuery=%7B"id"%3A"f026c678-9abf-42af-a3dc-f73dc8cbb810"%2C"builder"%3A%7B"queryData"%3A%5B%7B"dataSource"%3A"metrics"%2C"queryName"%3A"A"%2C"aggregateOperator"%3A"count"%2C"aggregateAttribute"%3A%7B"id"%3A"----"%2C"dataType"%3A""%2C"key"%3A""%2C"type"%3A""%7D%2C"timeAggregation"%3A"rate"%2C"spaceAggregation"%3A"sum"%2C"filter"%3A%7B"expression"%3A""%7D%2C"aggregations"%3A%5B%7B"metricName"%3A""%2C"temporality"%3A""%2C"timeAggregation"%3A"count"%2C"spaceAggregation"%3A"sum"%2C"reduceTo"%3A"avg"%7D%5D%2C"functions"%3A%5B%5D%2C"filters"%3A%7B"items"%3A%5B%5D%2C"op"%3A"AND"%7D%2C"expression"%3A"A"%2C"disabled"%3Afalse%2C"stepInterval"%3Anull%2C"having"%3A%5B%5D%2C"limit"%3Anull%2C"orderBy"%3A%5B%5D%2C"groupBy"%3A%5B%5D%2C"legend"%3A""%2C"reduceTo"%3A"avg"%2C"source"%3A""%7D%5D%2C"queryFormulas"%3A%5B%5D%2C"queryTraceOperator"%3A%5B%5D%7D%2C"clickhouse_sql"%3A%5B%7B"name"%3A"A"%2C"legend"%3A""%2C"disabled"%3Afalse%2C"query"%3A""%7D%5D%2C"promql"%3A%5B%7B"name"%3A"A"%2C"query"%3A""%2C"legend"%3A""%2C"disabled"%3Afalse%7D%5D%2C"queryType"%3A"builder"%7D&relativeTime=30m';
|
'?graphType=bar&widgetId=b473eef0-8eb5-4dd3-8089-c1817734084f&compositeQuery=%7B"id"%3A"f026c678-9abf-42af-a3dc-f73dc8cbb810"%2C"builder"%3A%7B"queryData"%3A%5B%7B"dataSource"%3A"metrics"%2C"queryName"%3A"A"%2C"aggregateOperator"%3A"count"%2C"aggregateAttribute"%3A%7B"id"%3A"----"%2C"dataType"%3A""%2C"key"%3A""%2C"type"%3A""%7D%2C"timeAggregation"%3A"rate"%2C"spaceAggregation"%3A"sum"%2C"filter"%3A%7B"expression"%3A""%7D%2C"aggregations"%3A%5B%7B"metricName"%3A""%2C"temporality"%3A""%2C"timeAggregation"%3A"count"%2C"spaceAggregation"%3A"sum"%2C"reduceTo"%3A"avg"%7D%5D%2C"functions"%3A%5B%5D%2C"filters"%3A%7B"items"%3A%5B%5D%2C"op"%3A"AND"%7D%2C"expression"%3A"A"%2C"disabled"%3Afalse%2C"stepInterval"%3Anull%2C"having"%3A%5B%5D%2C"limit"%3Anull%2C"orderBy"%3A%5B%5D%2C"groupBy"%3A%5B%5D%2C"legend"%3A""%2C"reduceTo"%3A"avg"%2C"source"%3A""%7D%5D%2C"queryFormulas"%3A%5B%5D%2C"queryTraceOperator"%3A%5B%5D%7D%2C"clickhouse_sql"%3A%5B%7B"name"%3A"A"%2C"legend"%3A""%2C"disabled"%3Afalse%2C"query"%3A""%7D%5D%2C"promql"%3A%5B%7B"name"%3A"A"%2C"query"%3A""%2C"legend"%3A""%2C"disabled"%3Afalse%7D%5D%2C"queryType"%3A"builder"%7D&relativeTime=30m';
|
||||||
// Mocks
|
// Mocks
|
||||||
@ -279,7 +309,7 @@ describe('Stacking bar in new panel', () => {
|
|||||||
jest.fn(),
|
jest.fn(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { container, getByText, getByRole } = render(
|
const { container, getByText } = render(
|
||||||
<I18nextProvider i18n={i18n}>
|
<I18nextProvider i18n={i18n}>
|
||||||
<DashboardProvider>
|
<DashboardProvider>
|
||||||
<PreferenceContextProvider>
|
<PreferenceContextProvider>
|
||||||
@ -305,7 +335,83 @@ describe('Stacking bar in new panel', () => {
|
|||||||
expect(switchBtn).toBeInTheDocument();
|
expect(switchBtn).toBeInTheDocument();
|
||||||
expect(switchBtn).toHaveClass('ant-switch-checked');
|
expect(switchBtn).toHaveClass('ant-switch-checked');
|
||||||
|
|
||||||
// (Optional) More semantic: verify by role
|
// Check that stack series is present and checked
|
||||||
expect(getByRole('switch')).toBeChecked();
|
checkStackSeriesState(container, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const STACKING_STATE_ATTR = 'data-stacking-state';
|
||||||
|
|
||||||
|
describe('when switching to BAR panel type', () => {
|
||||||
|
jest.setTimeout(10000);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
// Mock useSearchParams to return the expected values
|
||||||
|
(useSearchParams as jest.Mock).mockReturnValue([
|
||||||
|
new URLSearchParams(MOCK_SEARCH_PARAMS),
|
||||||
|
jest.fn(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve saved stacking value of true', async () => {
|
||||||
|
const { getByTestId, getByText, container } = render(
|
||||||
|
<DashboardProvider>
|
||||||
|
<NewWidget
|
||||||
|
selectedGraph={PANEL_TYPES.BAR}
|
||||||
|
fillSpans={undefined}
|
||||||
|
yAxisUnit={undefined}
|
||||||
|
/>
|
||||||
|
</DashboardProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getByTestId('panel-change-select')).toHaveAttribute(
|
||||||
|
STACKING_STATE_ATTR,
|
||||||
|
'true',
|
||||||
|
);
|
||||||
|
|
||||||
|
await userEvent.click(getByText('Bar')); // Panel Type Selected
|
||||||
|
|
||||||
|
// find dropdown with - .ant-select-dropdown
|
||||||
|
const panelDropdown = document.querySelector(
|
||||||
|
'.ant-select-dropdown',
|
||||||
|
) as HTMLElement;
|
||||||
|
expect(panelDropdown).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Select TimeSeries from dropdown
|
||||||
|
const option = within(panelDropdown).getByText('Time Series');
|
||||||
|
fireEvent.click(option);
|
||||||
|
|
||||||
|
expect(getByTestId('panel-change-select')).toHaveAttribute(
|
||||||
|
STACKING_STATE_ATTR,
|
||||||
|
'false',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Since we are on timeseries panel, stack series should be false
|
||||||
|
expect(screen.queryByText('Stack series')).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
// switch back to Bar panel
|
||||||
|
const panelTypeDropdown2 = getByTestId('panel-change-select') as HTMLElement;
|
||||||
|
expect(panelTypeDropdown2).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(getByTextUtil(panelTypeDropdown2, 'Time Series')).toBeInTheDocument();
|
||||||
|
fireEvent.click(getByTextUtil(panelTypeDropdown2, 'Time Series'));
|
||||||
|
|
||||||
|
// find dropdown with - .ant-select-dropdown
|
||||||
|
const panelDropdown2 = document.querySelector(
|
||||||
|
'.ant-select-dropdown',
|
||||||
|
) as HTMLElement;
|
||||||
|
// // Select BAR from dropdown
|
||||||
|
const BarOption = within(panelDropdown2).getByText('Bar');
|
||||||
|
fireEvent.click(BarOption);
|
||||||
|
|
||||||
|
// Stack series should be true
|
||||||
|
checkStackSeriesState(container, true);
|
||||||
|
|
||||||
|
expect(getByTestId('panel-change-select')).toHaveAttribute(
|
||||||
|
STACKING_STATE_ATTR,
|
||||||
|
'true',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -0,0 +1,134 @@
|
|||||||
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
|
|
||||||
|
export const BarNonStackedChartData = {
|
||||||
|
apiResponse: {
|
||||||
|
data: {
|
||||||
|
result: [
|
||||||
|
{
|
||||||
|
metric: {
|
||||||
|
'service.name': 'recommendationservice',
|
||||||
|
},
|
||||||
|
values: [
|
||||||
|
[1758713940, '33.933'],
|
||||||
|
[1758715020, '31.767'],
|
||||||
|
],
|
||||||
|
queryName: 'A',
|
||||||
|
metaData: {
|
||||||
|
alias: '__result_0',
|
||||||
|
index: 0,
|
||||||
|
queryName: 'A',
|
||||||
|
},
|
||||||
|
legend: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metric: {
|
||||||
|
'service.name': 'frontend',
|
||||||
|
},
|
||||||
|
values: [
|
||||||
|
[1758713940, '20.0'],
|
||||||
|
[1758715020, '25.0'],
|
||||||
|
],
|
||||||
|
queryName: 'B',
|
||||||
|
metaData: {
|
||||||
|
alias: '__result_1',
|
||||||
|
index: 1,
|
||||||
|
queryName: 'B',
|
||||||
|
},
|
||||||
|
legend: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
resultType: 'time_series',
|
||||||
|
newResult: {
|
||||||
|
data: {
|
||||||
|
resultType: 'time_series',
|
||||||
|
result: [
|
||||||
|
{
|
||||||
|
queryName: 'A',
|
||||||
|
legend: '',
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
labels: {
|
||||||
|
'service.name': 'recommendationservice',
|
||||||
|
},
|
||||||
|
labelsArray: [
|
||||||
|
{
|
||||||
|
'service.name': 'recommendationservice',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
timestamp: 1758713940000,
|
||||||
|
value: '33.933',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamp: 1758715020000,
|
||||||
|
value: '31.767',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
metaData: {
|
||||||
|
alias: '__result_0',
|
||||||
|
index: 0,
|
||||||
|
queryName: 'A',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
predictedSeries: [],
|
||||||
|
upperBoundSeries: [],
|
||||||
|
lowerBoundSeries: [],
|
||||||
|
anomalyScores: [],
|
||||||
|
list: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
queryName: 'B',
|
||||||
|
legend: '',
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
labels: {
|
||||||
|
'service.name': 'frontend',
|
||||||
|
},
|
||||||
|
labelsArray: [
|
||||||
|
{
|
||||||
|
'service.name': 'frontend',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
timestamp: 1758713940000,
|
||||||
|
value: '20.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamp: 1758715020000,
|
||||||
|
value: '25.0',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
metaData: {
|
||||||
|
alias: '__result_1',
|
||||||
|
index: 1,
|
||||||
|
queryName: 'B',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
predictedSeries: [],
|
||||||
|
upperBoundSeries: [],
|
||||||
|
lowerBoundSeries: [],
|
||||||
|
anomalyScores: [],
|
||||||
|
list: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as MetricRangePayloadProps,
|
||||||
|
fillSpans: false,
|
||||||
|
stackedBarChart: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BarStackedChartData = {
|
||||||
|
...BarNonStackedChartData,
|
||||||
|
stackedBarChart: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TimeSeriesChartData = {
|
||||||
|
...BarNonStackedChartData,
|
||||||
|
stackedBarChart: false,
|
||||||
|
};
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
import { getUPlotChartData } from '../../../lib/uPlotLib/utils/getUplotChartData';
|
||||||
|
import {
|
||||||
|
BarNonStackedChartData,
|
||||||
|
BarStackedChartData,
|
||||||
|
TimeSeriesChartData,
|
||||||
|
} from './__mocks__/uplotChartData';
|
||||||
|
|
||||||
|
describe('getUplotChartData', () => {
|
||||||
|
it('should return the correct chart data for non-stacked bar chart', () => {
|
||||||
|
const result = getUPlotChartData(
|
||||||
|
BarNonStackedChartData.apiResponse,
|
||||||
|
BarNonStackedChartData.fillSpans,
|
||||||
|
BarNonStackedChartData.stackedBarChart,
|
||||||
|
);
|
||||||
|
expect(result).toEqual([
|
||||||
|
[1758713940, 1758715020],
|
||||||
|
[33.933, 31.767],
|
||||||
|
[20.0, 25.0],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the correct chart data for stacked bar chart', () => {
|
||||||
|
const result = getUPlotChartData(
|
||||||
|
BarStackedChartData.apiResponse,
|
||||||
|
BarStackedChartData.fillSpans,
|
||||||
|
BarStackedChartData.stackedBarChart,
|
||||||
|
);
|
||||||
|
// For stacked charts, the values should be cumulative
|
||||||
|
// First series: [33.933, 31.767] + [20.0, 25.0] = [53.933, 56.767]
|
||||||
|
// Second series: [20.0, 25.0] (unchanged)
|
||||||
|
expect(result).toHaveLength(3);
|
||||||
|
expect(result[0]).toEqual([1758713940, 1758715020]);
|
||||||
|
expect(result[1][0]).toBeCloseTo(53.933, 3);
|
||||||
|
expect(result[1][1]).toBeCloseTo(56.767, 3);
|
||||||
|
expect(result[2]).toEqual([20.0, 25.0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the correct chart data for time series chart', () => {
|
||||||
|
const result = getUPlotChartData(
|
||||||
|
TimeSeriesChartData.apiResponse,
|
||||||
|
TimeSeriesChartData.fillSpans,
|
||||||
|
TimeSeriesChartData.stackedBarChart,
|
||||||
|
);
|
||||||
|
expect(result).toEqual([
|
||||||
|
[1758713940, 1758715020],
|
||||||
|
[33.933, 31.767],
|
||||||
|
[20.0, 25.0],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -595,6 +595,13 @@ function NewWidget({
|
|||||||
selectedGraph,
|
selectedGraph,
|
||||||
);
|
);
|
||||||
setGraphType(type);
|
setGraphType(type);
|
||||||
|
|
||||||
|
// with a single source of truth for stacking, we can use the saved stacking value as a default value
|
||||||
|
const savedStackingValue = getWidget()?.stackedBarChart;
|
||||||
|
setStackedBarChart(
|
||||||
|
type === PANEL_TYPES.BAR ? savedStackingValue || false : false,
|
||||||
|
);
|
||||||
|
|
||||||
redirectWithQueryBuilderData(
|
redirectWithQueryBuilderData(
|
||||||
updatedQuery,
|
updatedQuery,
|
||||||
{ [QueryParams.graphType]: type },
|
{ [QueryParams.graphType]: type },
|
||||||
|
|||||||
@ -553,7 +553,7 @@ export const getDefaultWidgetData = (
|
|||||||
timePreferance: 'GLOBAL_TIME',
|
timePreferance: 'GLOBAL_TIME',
|
||||||
softMax: null,
|
softMax: null,
|
||||||
softMin: null,
|
softMin: null,
|
||||||
stackedBarChart: true,
|
stackedBarChart: name === PANEL_TYPES.BAR,
|
||||||
selectedLogFields: defaultLogsSelectedColumns.map((field) => ({
|
selectedLogFields: defaultLogsSelectedColumns.map((field) => ({
|
||||||
...field,
|
...field,
|
||||||
type: field.fieldContext ?? '',
|
type: field.fieldContext ?? '',
|
||||||
|
|||||||
@ -124,15 +124,23 @@ function UplotPanelWrapper({
|
|||||||
queryResponse.data.payload.data.result = sortedSeriesData;
|
queryResponse.data.payload.data.result = sortedSeriesData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stackedBarChart = useMemo(
|
||||||
|
() =>
|
||||||
|
(selectedGraph
|
||||||
|
? selectedGraph === PANEL_TYPES.BAR
|
||||||
|
: widget?.panelTypes === PANEL_TYPES.BAR) && widget?.stackedBarChart,
|
||||||
|
[selectedGraph, widget?.panelTypes, widget?.stackedBarChart],
|
||||||
|
);
|
||||||
|
|
||||||
const chartData = getUPlotChartData(
|
const chartData = getUPlotChartData(
|
||||||
queryResponse?.data?.payload,
|
queryResponse?.data?.payload,
|
||||||
widget.fillSpans,
|
widget.fillSpans,
|
||||||
widget?.stackedBarChart,
|
stackedBarChart,
|
||||||
hiddenGraph,
|
hiddenGraph,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (widget.panelTypes === PANEL_TYPES.BAR && widget?.stackedBarChart) {
|
if (widget.panelTypes === PANEL_TYPES.BAR && stackedBarChart) {
|
||||||
const graphV = cloneDeep(graphVisibility)?.slice(1);
|
const graphV = cloneDeep(graphVisibility)?.slice(1);
|
||||||
const isSomeSelectedLegend = graphV?.some((v) => v === false);
|
const isSomeSelectedLegend = graphV?.some((v) => v === false);
|
||||||
if (isSomeSelectedLegend) {
|
if (isSomeSelectedLegend) {
|
||||||
@ -145,7 +153,7 @@ function UplotPanelWrapper({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [graphVisibility, hiddenGraph, widget.panelTypes, widget?.stackedBarChart]);
|
}, [graphVisibility, hiddenGraph, widget.panelTypes, stackedBarChart]);
|
||||||
|
|
||||||
const { timezone } = useTimezone();
|
const { timezone } = useTimezone();
|
||||||
|
|
||||||
@ -221,7 +229,7 @@ function UplotPanelWrapper({
|
|||||||
setGraphsVisibilityStates: setGraphVisibility,
|
setGraphsVisibilityStates: setGraphVisibility,
|
||||||
panelType: selectedGraph || widget.panelTypes,
|
panelType: selectedGraph || widget.panelTypes,
|
||||||
currentQuery,
|
currentQuery,
|
||||||
stackBarChart: widget?.stackedBarChart,
|
stackBarChart: stackedBarChart,
|
||||||
hiddenGraph,
|
hiddenGraph,
|
||||||
setHiddenGraph,
|
setHiddenGraph,
|
||||||
customTooltipElement,
|
customTooltipElement,
|
||||||
@ -261,6 +269,7 @@ function UplotPanelWrapper({
|
|||||||
enableDrillDown,
|
enableDrillDown,
|
||||||
onClickHandler,
|
onClickHandler,
|
||||||
widget,
|
widget,
|
||||||
|
stackedBarChart,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -274,14 +283,14 @@ function UplotPanelWrapper({
|
|||||||
items={menuItemsConfig.items}
|
items={menuItemsConfig.items}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
{widget?.stackedBarChart && isFullViewMode && (
|
{stackedBarChart && isFullViewMode && (
|
||||||
<Alert
|
<Alert
|
||||||
message="Selecting multiple legends is currently not supported in case of stacked bar charts"
|
message="Selecting multiple legends is currently not supported in case of stacked bar charts"
|
||||||
type="info"
|
type="info"
|
||||||
className="info-text"
|
className="info-text"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isFullViewMode && setGraphVisibility && !widget?.stackedBarChart && (
|
{isFullViewMode && setGraphVisibility && !stackedBarChart && (
|
||||||
<GraphManager
|
<GraphManager
|
||||||
data={getUPlotChartData(queryResponse?.data?.payload, widget.fillSpans)}
|
data={getUPlotChartData(queryResponse?.data?.payload, widget.fillSpans)}
|
||||||
name={widget.id}
|
name={widget.id}
|
||||||
|
|||||||
@ -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,
|
.date-time-root,
|
||||||
.shareable-link-popover-root {
|
.header-section-popover-root {
|
||||||
.ant-popover-inner {
|
.ant-popover-inner {
|
||||||
border-radius: 4px !important;
|
border-radius: 4px !important;
|
||||||
border: 1px solid var(--bg-slate-400);
|
border: 1px solid var(--bg-slate-400);
|
||||||
@ -359,7 +316,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.date-time-root,
|
.date-time-root,
|
||||||
.shareable-link-popover-root {
|
.header-section-popover-root {
|
||||||
.ant-popover-inner {
|
.ant-popover-inner {
|
||||||
border: 1px solid var(--bg-vanilla-400);
|
border: 1px solid var(--bg-vanilla-400);
|
||||||
background: var(--bg-vanilla-100) !important;
|
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 {
|
.reset-button {
|
||||||
background: var(--bg-vanilla-100);
|
background: var(--bg-vanilla-100);
|
||||||
border-color: var(--bg-vanilla-300);
|
border-color: var(--bg-vanilla-300);
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import './DateTimeSelectionV2.styles.scss';
|
import './DateTimeSelectionV2.styles.scss';
|
||||||
|
|
||||||
import { SyncOutlined } from '@ant-design/icons';
|
import { SyncOutlined } from '@ant-design/icons';
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Button } from 'antd';
|
||||||
import { Button, Popover, Switch, Typography } from 'antd';
|
|
||||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||||
import setLocalStorageKey from 'api/browser/localstorage/set';
|
import setLocalStorageKey from 'api/browser/localstorage/set';
|
||||||
import CustomTimePicker from 'components/CustomTimePicker/CustomTimePicker';
|
import CustomTimePicker from 'components/CustomTimePicker/CustomTimePicker';
|
||||||
@ -15,16 +14,15 @@ import dayjs, { Dayjs } from 'dayjs';
|
|||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import GetMinMax, { isValidTimeFormat } from 'lib/getMinMax';
|
import { isValidTimeFormat } from 'lib/getMinMax';
|
||||||
import getTimeString from 'lib/getTimeString';
|
import getTimeString from 'lib/getTimeString';
|
||||||
import { cloneDeep, isObject } from 'lodash-es';
|
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 { useTimezone } from 'providers/Timezone';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { connect, useDispatch, useSelector } from 'react-redux';
|
import { connect, useDispatch, useSelector } from 'react-redux';
|
||||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||||
import { useNavigationType, useSearchParams } from 'react-router-dom-v5-compat';
|
import { useNavigationType, useSearchParams } from 'react-router-dom-v5-compat';
|
||||||
import { useCopyToClipboard } from 'react-use';
|
|
||||||
import { bindActionCreators, Dispatch } from 'redux';
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
import { ThunkDispatch } from 'redux-thunk';
|
import { ThunkDispatch } from 'redux-thunk';
|
||||||
import { GlobalTimeLoading, UpdateTimeInterval } from 'store/actions';
|
import { GlobalTimeLoading, UpdateTimeInterval } from 'store/actions';
|
||||||
@ -53,7 +51,6 @@ import { Form, FormContainer, FormItem } from './styles';
|
|||||||
function DateTimeSelection({
|
function DateTimeSelection({
|
||||||
showAutoRefresh,
|
showAutoRefresh,
|
||||||
showRefreshText = true,
|
showRefreshText = true,
|
||||||
hideShareModal = false,
|
|
||||||
location,
|
location,
|
||||||
updateTimeInterval,
|
updateTimeInterval,
|
||||||
globalTimeLoading,
|
globalTimeLoading,
|
||||||
@ -81,10 +78,6 @@ function DateTimeSelection({
|
|||||||
const searchStartTime = urlQuery.get('startTime');
|
const searchStartTime = urlQuery.get('startTime');
|
||||||
const searchEndTime = urlQuery.get('endTime');
|
const searchEndTime = urlQuery.get('endTime');
|
||||||
const relativeTimeFromUrl = urlQuery.get(QueryParams.relativeTime);
|
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
|
// Prioritize props for initial modal time, fallback to URL params
|
||||||
let initialModalStartTime = 0;
|
let initialModalStartTime = 0;
|
||||||
@ -324,7 +317,6 @@ function DateTimeSelection({
|
|||||||
if (isModalTimeSelection) {
|
if (isModalTimeSelection) {
|
||||||
if (value === 'custom') {
|
if (value === 'custom') {
|
||||||
setCustomDTPickerVisible(true);
|
setCustomDTPickerVisible(true);
|
||||||
setIsValidteRelativeTime(false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onTimeChange?.(value);
|
onTimeChange?.(value);
|
||||||
@ -334,15 +326,12 @@ function DateTimeSelection({
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
updateTimeInterval(value);
|
updateTimeInterval(value);
|
||||||
updateLocalStorageForRoutes(value);
|
updateLocalStorageForRoutes(value);
|
||||||
setIsValidteRelativeTime(true);
|
|
||||||
if (refreshButtonHidden) {
|
if (refreshButtonHidden) {
|
||||||
setRefreshButtonHidden(false);
|
setRefreshButtonHidden(false);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setRefreshButtonHidden(true);
|
setRefreshButtonHidden(true);
|
||||||
setCustomDTPickerVisible(true);
|
setCustomDTPickerVisible(true);
|
||||||
setIsValidteRelativeTime(false);
|
|
||||||
setEnableAbsoluteTime(false);
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -458,11 +447,6 @@ function DateTimeSelection({
|
|||||||
urlQuery.delete('startTime');
|
urlQuery.delete('startTime');
|
||||||
urlQuery.delete('endTime');
|
urlQuery.delete('endTime');
|
||||||
|
|
||||||
setIsValidteRelativeTime(true);
|
|
||||||
|
|
||||||
urlQuery.delete('startTime');
|
|
||||||
urlQuery.delete('endTime');
|
|
||||||
|
|
||||||
urlQuery.set(QueryParams.relativeTime, dateTimeStr);
|
urlQuery.set(QueryParams.relativeTime, dateTimeStr);
|
||||||
|
|
||||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||||
@ -542,7 +526,6 @@ function DateTimeSelection({
|
|||||||
const handleRelativeTimeSync = useCallback(
|
const handleRelativeTimeSync = useCallback(
|
||||||
(relativeTime: string): void => {
|
(relativeTime: string): void => {
|
||||||
updateTimeInterval(relativeTime as Time);
|
updateTimeInterval(relativeTime as Time);
|
||||||
setIsValidteRelativeTime(true);
|
|
||||||
setRefreshButtonHidden(false);
|
setRefreshButtonHidden(false);
|
||||||
},
|
},
|
||||||
[updateTimeInterval],
|
[updateTimeInterval],
|
||||||
@ -625,8 +608,6 @@ function DateTimeSelection({
|
|||||||
|
|
||||||
const updatedTime = getCustomOrIntervalTime(time, currentRoute);
|
const updatedTime = getCustomOrIntervalTime(time, currentRoute);
|
||||||
|
|
||||||
setIsValidteRelativeTime(updatedTime !== 'custom');
|
|
||||||
|
|
||||||
const [preStartTime = 0, preEndTime = 0] = getTime() || [];
|
const [preStartTime = 0, preEndTime = 0] = getTime() || [];
|
||||||
|
|
||||||
setRefreshButtonHidden(updatedTime === 'custom');
|
setRefreshButtonHidden(updatedTime === 'custom');
|
||||||
@ -654,95 +635,6 @@ function DateTimeSelection({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [location.pathname, updateTimeInterval, globalTimeLoading]);
|
}, [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 (
|
|
||||||
<div className="share-modal-content">
|
|
||||||
<div className="absolute-relative-time-toggler-container">
|
|
||||||
<div className="absolute-relative-time-toggler">
|
|
||||||
{(selectedTime === 'custom' || !isValidteRelativeTime) && (
|
|
||||||
<Info size={14} color={Color.BG_AMBER_600} />
|
|
||||||
)}
|
|
||||||
<Switch
|
|
||||||
checked={enableAbsoluteTime || isCustomTime}
|
|
||||||
disabled={selectedTime === 'custom' || !isValidteRelativeTime}
|
|
||||||
size="small"
|
|
||||||
onChange={(): void => {
|
|
||||||
setEnableAbsoluteTime(!enableAbsoluteTime);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Typography.Text>Enable Absolute Time</Typography.Text>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{(selectedTime === 'custom' || !isValidteRelativeTime) && (
|
|
||||||
<div className="absolute-relative-time-error">
|
|
||||||
Please select / enter valid relative time to toggle.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="share-link">
|
|
||||||
<Typography.Text ellipsis className="share-url">
|
|
||||||
{currentUrl}
|
|
||||||
</Typography.Text>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
className="periscope-btn copy-url-btn"
|
|
||||||
onClick={(): void => {
|
|
||||||
handleCopyToClipboard(currentUrl);
|
|
||||||
setIsURLCopied(true);
|
|
||||||
setTimeout(() => {
|
|
||||||
setIsURLCopied(false);
|
|
||||||
}, 1000);
|
|
||||||
}}
|
|
||||||
icon={
|
|
||||||
isURLCopied ? (
|
|
||||||
<Check size={14} color={Color.BG_FOREST_500} />
|
|
||||||
) : (
|
|
||||||
<Copy size={14} color={Color.BG_ROBIN_500} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { timezone } = useTimezone();
|
const { timezone } = useTimezone();
|
||||||
|
|
||||||
const getSelectedValue = (): string => {
|
const getSelectedValue = (): string => {
|
||||||
@ -814,9 +706,6 @@ function DateTimeSelection({
|
|||||||
onValidCustomDateChange={(dateTime): void => {
|
onValidCustomDateChange={(dateTime): void => {
|
||||||
onValidCustomDateHandler(dateTime.timeStr as CustomTimeType);
|
onValidCustomDateHandler(dateTime.timeStr as CustomTimeType);
|
||||||
}}
|
}}
|
||||||
onCustomTimeStatusUpdate={(isValid: boolean): void => {
|
|
||||||
setIsValidteRelativeTime(isValid);
|
|
||||||
}}
|
|
||||||
selectedValue={getSelectedValue()}
|
selectedValue={getSelectedValue()}
|
||||||
data-testid="dropDown"
|
data-testid="dropDown"
|
||||||
items={options}
|
items={options}
|
||||||
@ -843,24 +732,6 @@ function DateTimeSelection({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!hideShareModal && (
|
|
||||||
<Popover
|
|
||||||
rootClassName="shareable-link-popover-root"
|
|
||||||
className="shareable-link-popover"
|
|
||||||
placement="bottomRight"
|
|
||||||
content={shareModalContent}
|
|
||||||
arrow={false}
|
|
||||||
trigger={['hover']}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
className="share-link-btn periscope-btn"
|
|
||||||
icon={<Send size={14} />}
|
|
||||||
>
|
|
||||||
Share
|
|
||||||
</Button>
|
|
||||||
</Popover>
|
|
||||||
)}
|
|
||||||
</FormContainer>
|
</FormContainer>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,4 +1,17 @@
|
|||||||
.top-nav-container {
|
.top-nav-container {
|
||||||
padding: 0px 8px;
|
padding: 8px;
|
||||||
margin-bottom: 16px;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import './TopNav.styles.scss';
|
import './TopNav.styles.scss';
|
||||||
|
|
||||||
import { Col, Row, Space } from 'antd';
|
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { matchPath, useHistory } from 'react-router-dom';
|
import { matchPath, useHistory } from 'react-router-dom';
|
||||||
@ -46,16 +46,9 @@ function TopNav(): JSX.Element | null {
|
|||||||
|
|
||||||
return !isRouteToSkip ? (
|
return !isRouteToSkip ? (
|
||||||
<div className="top-nav-container">
|
<div className="top-nav-container">
|
||||||
<Col span={24} style={{ marginTop: '1rem' }}>
|
<NewExplorerCTA />
|
||||||
<Row justify="end">
|
<DateTimeSelector showAutoRefresh />
|
||||||
<Space align="center" size={16} direction="horizontal">
|
<HeaderRightSection enableShare enableFeedback enableAnnouncements={false} />
|
||||||
<NewExplorerCTA />
|
|
||||||
<div>
|
|
||||||
<DateTimeSelector showAutoRefresh />
|
|
||||||
</div>
|
|
||||||
</Space>
|
|
||||||
</Row>
|
|
||||||
</Col>
|
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
.alerts-container {
|
.alerts-container {
|
||||||
.ant-tabs-nav-wrap:first-of-type {
|
.ant-tabs-nav {
|
||||||
padding-left: 16px;
|
padding: 0 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import './AlertList.styles.scss';
|
|||||||
import { Tabs } from 'antd';
|
import { Tabs } from 'antd';
|
||||||
import { TabsProps } from 'antd/lib';
|
import { TabsProps } from 'antd/lib';
|
||||||
import ConfigureIcon from 'assets/AlertHistory/ConfigureIcon';
|
import ConfigureIcon from 'assets/AlertHistory/ConfigureIcon';
|
||||||
|
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import AllAlertRules from 'container/ListAlertRules';
|
import AllAlertRules from 'container/ListAlertRules';
|
||||||
import { PlannedDowntime } from 'container/PlannedDowntime/PlannedDowntime';
|
import { PlannedDowntime } from 'container/PlannedDowntime/PlannedDowntime';
|
||||||
@ -28,7 +29,7 @@ function AllAlertList(): JSX.Element {
|
|||||||
{
|
{
|
||||||
label: (
|
label: (
|
||||||
<div className="periscope-tab top-level-tab">
|
<div className="periscope-tab top-level-tab">
|
||||||
<GalleryVerticalEnd size={16} />
|
<GalleryVerticalEnd size={14} />
|
||||||
Triggered Alerts
|
Triggered Alerts
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@ -38,7 +39,7 @@ function AllAlertList(): JSX.Element {
|
|||||||
{
|
{
|
||||||
label: (
|
label: (
|
||||||
<div className="periscope-tab top-level-tab">
|
<div className="periscope-tab top-level-tab">
|
||||||
<Pyramid size={16} />
|
<Pyramid size={14} />
|
||||||
Alert Rules
|
Alert Rules
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@ -52,7 +53,7 @@ function AllAlertList(): JSX.Element {
|
|||||||
{
|
{
|
||||||
label: (
|
label: (
|
||||||
<div className="periscope-tab top-level-tab">
|
<div className="periscope-tab top-level-tab">
|
||||||
<ConfigureIcon />
|
<ConfigureIcon width={14} height={14} />
|
||||||
Configuration
|
Configuration
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@ -82,6 +83,13 @@ function AllAlertList(): JSX.Element {
|
|||||||
className={`alerts-container ${
|
className={`alerts-container ${
|
||||||
isAlertHistory || isAlertOverview ? 'alert-details-tabs' : ''
|
isAlertHistory || isAlertOverview ? 'alert-details-tabs' : ''
|
||||||
}`}
|
}`}
|
||||||
|
tabBarExtraContent={
|
||||||
|
<HeaderRightSection
|
||||||
|
enableAnnouncements={false}
|
||||||
|
enableShare
|
||||||
|
enableFeedback
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.all-errors-right-section {
|
.all-errors-right-section {
|
||||||
padding: 0 10px;
|
.right-toolbar-actions-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-tabs {
|
.ant-tabs {
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { Button, Tooltip } from 'antd';
|
|||||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
|
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
|
||||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||||
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
||||||
import RouteTab from 'components/RouteTab';
|
import RouteTab from 'components/RouteTab';
|
||||||
@ -74,10 +75,24 @@ function AllErrors(): JSX.Element {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
rightActions={<RightToolbarActions onStageRunQuery={handleRunQuery} />}
|
rightActions={
|
||||||
|
<div className="right-toolbar-actions-container">
|
||||||
|
<RightToolbarActions onStageRunQuery={handleRunQuery} />
|
||||||
|
<HeaderRightSection
|
||||||
|
enableAnnouncements={false}
|
||||||
|
enableShare
|
||||||
|
enableFeedback
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<ResourceAttributesFilterV2 />
|
<ResourceAttributesFilterV2 />
|
||||||
<RouteTab routes={routes} activeKey={pathname} history={history} />
|
<RouteTab
|
||||||
|
routes={routes}
|
||||||
|
activeKey={pathname}
|
||||||
|
history={history}
|
||||||
|
showRightSection={false}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
</TypicalOverlayScrollbar>
|
</TypicalOverlayScrollbar>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@ -2,11 +2,18 @@
|
|||||||
.dashboard-header {
|
.dashboard-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 16px;
|
justify-content: space-between;
|
||||||
|
padding: 0 8px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
border-bottom: 1px solid var(--bg-slate-500);
|
border-bottom: 1px solid var(--bg-slate-500);
|
||||||
|
|
||||||
|
.dashboard-header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
color: var(--bg-vanilla-400);
|
color: var(--bg-vanilla-400);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import './DashboardsListPage.styles.scss';
|
import './DashboardsListPage.styles.scss';
|
||||||
|
|
||||||
import { Space, Typography } from 'antd';
|
import { Space, Typography } from 'antd';
|
||||||
|
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
|
||||||
import ListOfAllDashboard from 'container/ListOfDashboard';
|
import ListOfAllDashboard from 'container/ListOfDashboard';
|
||||||
import { LayoutGrid } from 'lucide-react';
|
import { LayoutGrid } from 'lucide-react';
|
||||||
|
|
||||||
@ -13,8 +14,16 @@ function DashboardsListPage(): JSX.Element {
|
|||||||
className="dashboard-list-page"
|
className="dashboard-list-page"
|
||||||
>
|
>
|
||||||
<div className="dashboard-header">
|
<div className="dashboard-header">
|
||||||
<LayoutGrid size={14} className="icon" />
|
<div className="dashboard-header-left">
|
||||||
<Typography.Text className="text">Dashboards</Typography.Text>
|
<LayoutGrid size={14} className="icon" />
|
||||||
|
<Typography.Text className="text">Dashboards</Typography.Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<HeaderRightSection
|
||||||
|
enableAnnouncements={false}
|
||||||
|
enableShare
|
||||||
|
enableFeedback
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ListOfAllDashboard />
|
<ListOfAllDashboard />
|
||||||
</Space>
|
</Space>
|
||||||
|
|||||||
@ -33,7 +33,6 @@
|
|||||||
height: calc(100vh - 48px);
|
height: calc(100vh - 48px);
|
||||||
border-right: 1px solid var(--Slate-500, #161922);
|
border-right: 1px solid var(--Slate-500, #161922);
|
||||||
background: var(--Ink-500, #0b0c0e);
|
background: var(--Ink-500, #0b0c0e);
|
||||||
padding: 10px 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-page-content {
|
.settings-page-content {
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
padding-right: 48px;
|
padding-right: 48px;
|
||||||
|
|
||||||
max-width: 1400px;
|
max-width: 1400px;
|
||||||
margin: 0 auto;
|
margin: 64px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.support-channels {
|
.support-channels {
|
||||||
@ -19,6 +19,7 @@
|
|||||||
flex: 0 0 calc(33.333% - 32px);
|
flex: 0 0 calc(33.333% - 32px);
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
border: none !important;
|
||||||
|
|
||||||
.support-channel-title {
|
.support-channel-title {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@ -205,6 +205,7 @@ export default function Support(): JSX.Element {
|
|||||||
<div className="support-channel-action">
|
<div className="support-channel-action">
|
||||||
<Button
|
<Button
|
||||||
type="default"
|
type="default"
|
||||||
|
className="periscope-btn secondary"
|
||||||
onClick={(): void => handleChannelClick(channel)}
|
onClick={(): void => handleChannelClick(channel)}
|
||||||
>
|
>
|
||||||
<Text ellipsis>{channel.btnText} </Text>
|
<Text ellipsis>{channel.btnText} </Text>
|
||||||
@ -240,7 +241,7 @@ export default function Support(): JSX.Element {
|
|||||||
loading={isLoadingBilling}
|
loading={isLoadingBilling}
|
||||||
disabled={isLoadingBilling}
|
disabled={isLoadingBilling}
|
||||||
onClick={handleAddCreditCard}
|
onClick={handleAddCreditCard}
|
||||||
className="add-credit-card-btn"
|
className="add-credit-card-btn periscope-btn primary"
|
||||||
>
|
>
|
||||||
Add Credit Card
|
Add Credit Card
|
||||||
</Button>,
|
</Button>,
|
||||||
|
|||||||
14
go.mod
14
go.mod
@ -5,7 +5,7 @@ go 1.24.0
|
|||||||
require (
|
require (
|
||||||
dario.cat/mergo v1.0.1
|
dario.cat/mergo v1.0.1
|
||||||
github.com/AfterShip/clickhouse-sql-parser v0.4.11
|
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/DATA-DOG/go-sqlmock v1.5.2
|
||||||
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd
|
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd
|
||||||
github.com/SigNoz/signoz-otel-collector v0.129.4
|
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/contrib/instrumentation/net/http/otelhttp v0.61.0
|
||||||
go.opentelemetry.io/otel v1.37.0
|
go.opentelemetry.io/otel v1.37.0
|
||||||
go.opentelemetry.io/otel/metric 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.opentelemetry.io/otel/trace v1.37.0
|
||||||
go.uber.org/multierr v1.11.0
|
go.uber.org/multierr v1.11.0
|
||||||
go.uber.org/zap v1.27.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/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/oauth2 v0.30.0
|
||||||
golang.org/x/sync v0.16.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
|
google.golang.org/protobuf v1.36.6
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
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/azidentity v1.10.0 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // 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/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/Masterminds/squirrel v1.5.4 // indirect
|
||||||
github.com/Yiling-J/theine-go v0.6.1 // indirect
|
github.com/Yiling-J/theine-go v0.6.1 // indirect
|
||||||
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // 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/armon/go-metrics v0.4.1 // indirect
|
||||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||||
github.com/aws/aws-sdk-go v1.55.7 // indirect
|
github.com/aws/aws-sdk-go v1.55.7 // indirect
|
||||||
|
|||||||
32
go.sum
32
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/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/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/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.67.0 h1:18MQF6vZHj+4/hTRaK7JbS/TIzn4I55wC+QzO24uiqc=
|
||||||
github.com/ClickHouse/ch-go v0.66.0/go.mod h1:noiHWyLMJAZ5wYuq3R/K0TcRhrNA8h7o1AqHX0klEhM=
|
github.com/ClickHouse/ch-go v0.67.0/go.mod h1:2MSAeyVmgt+9a2k2SQPPG1b4qbTPzdGDpf1+bcHh+18=
|
||||||
github.com/ClickHouse/clickhouse-go/v2 v2.36.0 h1:FJ03h8VdmBUhvR9nQEu5jRLdfG0c/HSxUjiNdOxRQww=
|
github.com/ClickHouse/clickhouse-go/v2 v2.40.1 h1:PbwsHBgqXRydU7jKULD1C8CHmifczffvQqmFvltM2W4=
|
||||||
github.com/ClickHouse/clickhouse-go/v2 v2.36.0/go.mod h1:aijX64fKD1hAWu/zqWEmiGk7wRE8ZnpN0M3UvjsZG3I=
|
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 h1:6vhZGc5M7Y/YD8cIUcY8kcuQLB4cHR7U+0KMqAA0KcU=
|
||||||
github.com/Code-Hex/go-generics-cache v1.5.1/go.mod h1:qxcC9kRVrct9rHeiYpFWSoW1vxyillCVzX13KZG8dl4=
|
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=
|
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-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 h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0=
|
||||||
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=
|
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.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
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/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 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
|
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/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 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
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.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||||
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
|
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 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 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=
|
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-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-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.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.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
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-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-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
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-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-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/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.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
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-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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/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/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-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.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.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
|
||||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
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.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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/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.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.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
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-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-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import (
|
|||||||
contribsdkconfig "go.opentelemetry.io/contrib/config"
|
contribsdkconfig "go.opentelemetry.io/contrib/config"
|
||||||
sdkmetric "go.opentelemetry.io/otel/metric"
|
sdkmetric "go.opentelemetry.io/otel/metric"
|
||||||
sdkresource "go.opentelemetry.io/otel/sdk/resource"
|
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"
|
sdktrace "go.opentelemetry.io/otel/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -45,7 +45,7 @@ def pytest_addoption(parser: pytest.Parser):
|
|||||||
parser.addoption(
|
parser.addoption(
|
||||||
"--clickhouse-version",
|
"--clickhouse-version",
|
||||||
action="store",
|
action="store",
|
||||||
default="24.1.2-alpine",
|
default="25.5.6",
|
||||||
help="clickhouse version",
|
help="clickhouse version",
|
||||||
)
|
)
|
||||||
parser.addoption(
|
parser.addoption(
|
||||||
@ -57,6 +57,6 @@ def pytest_addoption(parser: pytest.Parser):
|
|||||||
parser.addoption(
|
parser.addoption(
|
||||||
"--schema-migrator-version",
|
"--schema-migrator-version",
|
||||||
action="store",
|
action="store",
|
||||||
default="v0.128.2",
|
default="v0.129.6",
|
||||||
help="schema migrator version",
|
help="schema migrator version",
|
||||||
)
|
)
|
||||||
|
|||||||
@ -29,7 +29,7 @@ class LogsResource(ABC):
|
|||||||
self.seen_at_ts_bucket_start = seen_at_ts_bucket_start
|
self.seen_at_ts_bucket_start = seen_at_ts_bucket_start
|
||||||
|
|
||||||
def np_arr(self) -> np.array:
|
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):
|
class LogsResourceOrAttributeKeys(ABC):
|
||||||
@ -317,6 +317,9 @@ class Logs(ABC):
|
|||||||
self.scope_name,
|
self.scope_name,
|
||||||
self.scope_version,
|
self.scope_version,
|
||||||
self.scope_string,
|
self.scope_string,
|
||||||
|
np.uint64(10),
|
||||||
|
np.uint64(15),
|
||||||
|
self.resources_string,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -378,7 +381,7 @@ def insert_logs(
|
|||||||
table="distributed_logs_resource_keys",
|
table="distributed_logs_resource_keys",
|
||||||
data=[resource_key.np_arr() for resource_key in resource_keys],
|
data=[resource_key.np_arr() for resource_key in resource_keys],
|
||||||
)
|
)
|
||||||
|
|
||||||
clickhouse.conn.insert(
|
clickhouse.conn.insert(
|
||||||
database="signoz_logs",
|
database="signoz_logs",
|
||||||
table="distributed_logs_v2",
|
table="distributed_logs_v2",
|
||||||
|
|||||||
@ -593,6 +593,7 @@ class Traces(ABC):
|
|||||||
self.db_operation,
|
self.db_operation,
|
||||||
self.has_error,
|
self.has_error,
|
||||||
self.is_remote,
|
self.is_remote,
|
||||||
|
self.resources_string,
|
||||||
],
|
],
|
||||||
dtype=object,
|
dtype=object,
|
||||||
)
|
)
|
||||||
@ -681,6 +682,7 @@ def insert_traces(
|
|||||||
"db_operation",
|
"db_operation",
|
||||||
"has_error",
|
"has_error",
|
||||||
"is_remote",
|
"is_remote",
|
||||||
|
"resource",
|
||||||
],
|
],
|
||||||
data=[trace.np_arr() for trace in traces],
|
data=[trace.np_arr() for trace in traces],
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user