mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-07 11:00:31 +01:00
Compare commits
1 Commits
ns/alert-d
...
fix/multi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07d9a9ecb6 |
@@ -190,7 +190,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.122.0
|
||||
image: signoz/signoz:v0.121.1
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
# - "6060:6060" # pprof port
|
||||
|
||||
@@ -117,7 +117,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.122.0
|
||||
image: signoz/signoz:v0.121.1
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
volumes:
|
||||
|
||||
@@ -181,7 +181,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.122.0}
|
||||
image: signoz/signoz:${VERSION:-v0.121.1}
|
||||
container_name: signoz
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
|
||||
@@ -109,7 +109,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.122.0}
|
||||
image: signoz/signoz:${VERSION:-v0.121.1}
|
||||
container_name: signoz
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
|
||||
@@ -24,43 +24,6 @@
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.crossPanelSyncSectionHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.crossPanelSyncInfoIcon {
|
||||
cursor: help;
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
.crossPanelSyncTooltipContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.crossPanelSyncTooltipTitle {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.crossPanelSyncTooltipDescription {
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.crossPanelSyncTooltipDocLink {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--primary-background);
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.crossPanelSyncRow {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Col, Input, Radio, Select, Space, Tooltip, Typography } from 'antd';
|
||||
import { Col, Input, Radio, Select, Space, Typography } from 'antd';
|
||||
import AddTags from 'container/DashboardContainer/DashboardSettings/General/AddTags';
|
||||
import { useDashboardCursorSyncMode } from 'hooks/dashboard/useDashboardCursorSyncMode';
|
||||
import { useSyncTooltipFilterMode } from 'hooks/dashboard/useSyncTooltipFilterMode';
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
SyncTooltipFilterMode,
|
||||
} from 'lib/uPlotV2/plugins/TooltipPlugin/types';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { Check, ExternalLink, Info, X } from '@signozhq/icons';
|
||||
import { Check, X } from 'lucide-react';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
|
||||
import styles from './GeneralSettings.module.scss';
|
||||
@@ -173,36 +173,9 @@ function GeneralDashboardSettings(): JSX.Element {
|
||||
</Space>
|
||||
</Col>
|
||||
<Col className={`${styles.overviewSettings} ${styles.crossPanelSyncGroup}`}>
|
||||
<div className={styles.crossPanelSyncSectionHeader}>
|
||||
<Typography.Text className={styles.crossPanelSyncSectionTitle}>
|
||||
Cross-Panel Sync
|
||||
</Typography.Text>
|
||||
<Tooltip
|
||||
title={
|
||||
<div className={styles.crossPanelSyncTooltipContent}>
|
||||
<strong className={styles.crossPanelSyncTooltipTitle}>
|
||||
Cross-Panel Sync
|
||||
</strong>
|
||||
<span className={styles.crossPanelSyncTooltipDescription}>
|
||||
Sync crosshair and tooltip across all the dashboard panels
|
||||
</span>
|
||||
<a
|
||||
href="https://signoz.io/docs/dashboards/interactivity/#cross-panel-sync"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.crossPanelSyncTooltipDocLink}
|
||||
>
|
||||
Learn more
|
||||
<ExternalLink size={12} />
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
placement="top"
|
||||
mouseEnterDelay={0.5}
|
||||
>
|
||||
<Info size={14} className={styles.crossPanelSyncInfoIcon} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Typography.Text className={styles.crossPanelSyncSectionTitle}>
|
||||
Cross-Panel Sync
|
||||
</Typography.Text>
|
||||
<div className={styles.crossPanelSyncRow}>
|
||||
<div className={styles.crossPanelSyncInfo}>
|
||||
<Typography.Text className={styles.crossPanelSyncTitle}>
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
import { act, render } from '@testing-library/react';
|
||||
import { Modal } from 'antd';
|
||||
import { useDashboardBootstrap } from 'hooks/dashboard/useDashboardBootstrap';
|
||||
import { useTransformDashboardVariables } from 'hooks/dashboard/useTransformDashboardVariables';
|
||||
import useTabVisibility from 'hooks/useTabFocus';
|
||||
import { getMinMaxForSelectedTime } from 'lib/getMinMax';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { useDashboardQuery } from './useDashboardQuery';
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
const mockSetDashboardData = jest.fn();
|
||||
const mockSetLayouts = jest.fn();
|
||||
const mockSetPanelMap = jest.fn();
|
||||
const mockResetDashboardStore = jest.fn();
|
||||
const mockGetUrlVariables = jest.fn();
|
||||
const mockUpdateUrlVariable = jest.fn();
|
||||
const mockRefetch = jest.fn();
|
||||
|
||||
let mockGlobalTime = {
|
||||
selectedTime: 'custom',
|
||||
minTime: 1710000000000000000,
|
||||
maxTime: 1710000300000000000,
|
||||
isAutoRefreshDisabled: true,
|
||||
};
|
||||
|
||||
let currentQueryData: unknown;
|
||||
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: (): { t: (key: string) => string } => ({
|
||||
t: (key: string): string => key,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
useDispatch: jest.fn(() => mockDispatch),
|
||||
useSelector: jest.fn(
|
||||
(
|
||||
selectorFn: (state: { globalTime: typeof mockGlobalTime }) => unknown,
|
||||
): unknown => selectorFn({ globalTime: mockGlobalTime }),
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useTabFocus', () => jest.fn(() => true));
|
||||
jest.mock('hooks/dashboard/useDashboardVariablesSync', () => ({
|
||||
useDashboardVariablesSync: jest.fn(),
|
||||
}));
|
||||
jest.mock('./useDashboardQuery', () => ({
|
||||
useDashboardQuery: jest.fn(),
|
||||
}));
|
||||
jest.mock('hooks/dashboard/useTransformDashboardVariables', () => ({
|
||||
useTransformDashboardVariables: jest.fn(),
|
||||
}));
|
||||
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
useDashboardStore: jest.fn(),
|
||||
}));
|
||||
jest.mock('providers/Dashboard/initializeDefaultVariables', () => ({
|
||||
initializeDefaultVariables: jest.fn(),
|
||||
}));
|
||||
jest.mock('lib/dashboard/getUpdatedLayout', () => ({
|
||||
getUpdatedLayout: jest.fn(() => []),
|
||||
}));
|
||||
jest.mock('providers/Dashboard/util', () => ({
|
||||
sortLayout: jest.fn((layout) => layout),
|
||||
}));
|
||||
jest.mock('lib/getMinMax', () => ({
|
||||
getMinMaxForSelectedTime: jest.fn(),
|
||||
}));
|
||||
|
||||
function TestComponent({ confirm }: { confirm: typeof Modal.confirm }): null {
|
||||
useDashboardBootstrap('dashboard-1', { confirm });
|
||||
return null;
|
||||
}
|
||||
|
||||
describe('useDashboardBootstrap', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockGlobalTime = {
|
||||
selectedTime: 'custom',
|
||||
minTime: 1710000000000000000,
|
||||
maxTime: 1710000300000000000,
|
||||
isAutoRefreshDisabled: true,
|
||||
};
|
||||
|
||||
jest.mocked(useDashboardStore as unknown as jest.Mock).mockReturnValue({
|
||||
setDashboardData: mockSetDashboardData,
|
||||
setLayouts: mockSetLayouts,
|
||||
setPanelMap: mockSetPanelMap,
|
||||
resetDashboardStore: mockResetDashboardStore,
|
||||
});
|
||||
|
||||
jest
|
||||
.mocked(useTransformDashboardVariables as unknown as jest.Mock)
|
||||
.mockReturnValue({
|
||||
getUrlVariables: mockGetUrlVariables,
|
||||
updateUrlVariable: mockUpdateUrlVariable,
|
||||
transformDashboardVariables: <T,>(data: T): T => data,
|
||||
});
|
||||
|
||||
jest.mocked(useTabVisibility as unknown as jest.Mock).mockReturnValue(true);
|
||||
jest
|
||||
.mocked(useDashboardQuery as unknown as jest.Mock)
|
||||
.mockImplementation(() => ({
|
||||
data: currentQueryData,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
isFetching: false,
|
||||
error: null,
|
||||
refetch: mockRefetch,
|
||||
}));
|
||||
});
|
||||
|
||||
it('keeps minTime and maxTime unchanged for custom range on refresh confirm', () => {
|
||||
const initialDashboard = {
|
||||
id: 'dashboard-1',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
data: { layout: [], panelMap: {}, variables: {} },
|
||||
};
|
||||
|
||||
const updatedDashboard = {
|
||||
id: 'dashboard-1',
|
||||
updatedAt: '2024-01-01T01:00:00.000Z',
|
||||
data: { layout: [], panelMap: {}, variables: {} },
|
||||
};
|
||||
|
||||
const mockConfirm = jest.fn<
|
||||
ReturnType<typeof Modal.confirm>,
|
||||
Parameters<typeof Modal.confirm>
|
||||
>(() => ({ destroy: jest.fn(), update: jest.fn() }));
|
||||
|
||||
currentQueryData = { data: initialDashboard };
|
||||
const { rerender } = render(<TestComponent confirm={mockConfirm} />);
|
||||
|
||||
expect(mockConfirm).not.toHaveBeenCalled();
|
||||
|
||||
currentQueryData = { data: updatedDashboard };
|
||||
rerender(<TestComponent confirm={mockConfirm} />);
|
||||
|
||||
expect(mockConfirm).toHaveBeenCalledTimes(1);
|
||||
const firstCall = mockConfirm.mock.calls[0];
|
||||
expect(firstCall).toBeDefined();
|
||||
const [confirmProps] = firstCall as Parameters<typeof Modal.confirm>;
|
||||
|
||||
act(() => {
|
||||
confirmProps.onOk?.();
|
||||
});
|
||||
|
||||
expect(getMinMaxForSelectedTime).not.toHaveBeenCalled();
|
||||
expect(mockDispatch).toHaveBeenCalledWith({
|
||||
type: 'UPDATE_TIME_INTERVAL',
|
||||
payload: {
|
||||
selectedTime: 'custom',
|
||||
minTime: mockGlobalTime.minTime,
|
||||
maxTime: mockGlobalTime.maxTime,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -102,19 +102,11 @@ export function useDashboardBootstrap(
|
||||
onOk() {
|
||||
setDashboardData(updatedDashboardData);
|
||||
|
||||
const { maxTime, minTime } =
|
||||
globalTime.selectedTime === 'custom'
|
||||
? {
|
||||
// For custom ranges, min/max are already stored in nanoseconds.
|
||||
// Recomputing via getMinMaxForSelectedTime would multiply them again.
|
||||
maxTime: globalTime.maxTime,
|
||||
minTime: globalTime.minTime,
|
||||
}
|
||||
: getMinMaxForSelectedTime(
|
||||
globalTime.selectedTime,
|
||||
globalTime.minTime,
|
||||
globalTime.maxTime,
|
||||
);
|
||||
const { maxTime, minTime } = getMinMaxForSelectedTime(
|
||||
globalTime.selectedTime,
|
||||
globalTime.minTime,
|
||||
globalTime.maxTime,
|
||||
);
|
||||
dispatch({
|
||||
type: UPDATE_TIME_INTERVAL,
|
||||
payload: { maxTime, minTime, selectedTime: globalTime.selectedTime },
|
||||
|
||||
@@ -24,15 +24,10 @@ export default function Tooltip({
|
||||
);
|
||||
|
||||
const showHeader = showTooltipHeader || activeItem != null;
|
||||
// A single row collapses into the header when it's the active item, but
|
||||
// must stay in the list when there's no active item (e.g. sync-driven
|
||||
// tooltips with no focused series) — otherwise the row would vanish.
|
||||
const showList =
|
||||
tooltipContent.length > 1 ||
|
||||
(tooltipContent.length === 1 && activeItem == null);
|
||||
// The divider separates the active row in the header from the list; with
|
||||
// no active item it has nothing to separate.
|
||||
const showDivider = showList && showHeader && activeItem != null;
|
||||
// With a single series the active item is fully represented in the header —
|
||||
// hide the divider and list to avoid showing a duplicate row.
|
||||
const showList = tooltipContent.length > 1;
|
||||
const showDivider = showList && showHeader;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -4,6 +4,7 @@ import cx from 'classnames';
|
||||
import uPlot from 'uplot';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
|
||||
import { syncCursorRegistry } from './syncCursorRegistry';
|
||||
import { createSyncDisplayHook } from './syncDisplayHook';
|
||||
import {
|
||||
createInitialControllerState,
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
createSetSeriesHandler,
|
||||
getPlot,
|
||||
isScrollEventInPlot,
|
||||
shouldShowTooltipForSync,
|
||||
updatePlotVisibility,
|
||||
updateWindowSize,
|
||||
} from './tooltipController';
|
||||
@@ -300,49 +302,17 @@ export default function TooltipPlugin({
|
||||
}
|
||||
};
|
||||
|
||||
// Handles all tooltip-pin keyboard interactions:
|
||||
// Escape — always releases the tooltip when pinned (never steals Escape
|
||||
// from other handlers since we do not call stopPropagation).
|
||||
// pinKey — toggles: pins when hovering+unpinned, unpins when pinned.
|
||||
const handleKeyDown = (event: KeyboardEvent): void => {
|
||||
// Escape: release-only (never toggles on).
|
||||
if (event.key === 'Escape') {
|
||||
if (controller.pinned) {
|
||||
logEvent(Events.TOOLTIP_UNPINNED, {
|
||||
id: config.getId(),
|
||||
});
|
||||
dismissTooltip();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key.toLowerCase() !== pinKey.toLowerCase()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle off: P pressed while already pinned.
|
||||
if (controller.pinned) {
|
||||
logEvent(Events.TOOLTIP_UNPINNED, {
|
||||
id: config.getId(),
|
||||
});
|
||||
dismissTooltip();
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle on: P pressed while hovering.
|
||||
// Returns false when the cursor is offscreen so the caller can bail
|
||||
// without scheduling side effects.
|
||||
function pinAtCursor(): boolean {
|
||||
const plot = getPlot(controller);
|
||||
if (
|
||||
!plot ||
|
||||
!controller.hoverActive ||
|
||||
controller.focusedSeriesIndex == null
|
||||
) {
|
||||
return;
|
||||
if (!plot) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const cursorLeft = plot.cursor.left ?? -1;
|
||||
const cursorTop = plot.cursor.top ?? -1;
|
||||
if (cursorLeft < 0 || cursorTop < 0) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const plotRect = plot.over.getBoundingClientRect();
|
||||
@@ -360,8 +330,71 @@ export default function TooltipPlugin({
|
||||
id: config.getId(),
|
||||
});
|
||||
scheduleRender(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Escape always releases (never pins); pinKey toggles. Unpin fans out
|
||||
// naturally — every pinned panel's document listener sees the same key
|
||||
// event and dismisses itself — so only pinning needs a broadcast.
|
||||
const handleKeyDown = (event: KeyboardEvent): void => {
|
||||
if (event.key === 'Escape') {
|
||||
if (controller.pinned) {
|
||||
logEvent(Events.TOOLTIP_UNPINNED, {
|
||||
id: config.getId(),
|
||||
});
|
||||
dismissTooltip();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key.toLowerCase() !== pinKey.toLowerCase()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (controller.pinned) {
|
||||
logEvent(Events.TOOLTIP_UNPINNED, {
|
||||
id: config.getId(),
|
||||
});
|
||||
dismissTooltip();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!controller.hoverActive || controller.focusedSeriesIndex == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pinAtCursor()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Deferred to a macrotask so the same P keydown finishes dispatching
|
||||
// to every panel's listener first. A microtask is not enough — those
|
||||
// run between listener invocations, and once a receiver's `pinned`
|
||||
// flips, its own listener takes the unpin branch on the same event.
|
||||
if (syncTooltipWithDashboard) {
|
||||
setTimeout(() => {
|
||||
syncCursorRegistry.broadcastPin(syncKey);
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
// Skips the focusedSeriesIndex guard so single-series or no-overlap
|
||||
// receivers still pin at the synced timestamp.
|
||||
const handleSyncPinBroadcast = (): void => {
|
||||
if (controller.pinned) {
|
||||
return;
|
||||
}
|
||||
if (!shouldShowTooltipForSync(controller, syncTooltipWithDashboard)) {
|
||||
return;
|
||||
}
|
||||
pinAtCursor();
|
||||
};
|
||||
|
||||
const unsubscribeSyncPin =
|
||||
syncTooltipWithDashboard && canPinTooltip
|
||||
? syncCursorRegistry.subscribePin(syncKey, handleSyncPinBroadcast)
|
||||
: null;
|
||||
|
||||
// Forward overlay clicks to the consumer-provided onClick callback.
|
||||
const handleOverClick = (event: MouseEvent): void => {
|
||||
const plot = getPlot(controller);
|
||||
@@ -453,6 +486,7 @@ export default function TooltipPlugin({
|
||||
removeSetLegendHook();
|
||||
removeSetCursorHook();
|
||||
removeSyncDisplayHook?.();
|
||||
unsubscribeSyncPin?.();
|
||||
if (overClickHandler) {
|
||||
const plot = getPlot(controller);
|
||||
plot?.over.removeEventListener('click', overClickHandler);
|
||||
|
||||
@@ -17,6 +17,9 @@ const activeSeriesMetricBySyncKey = new Map<
|
||||
Record<string, string> | null
|
||||
>();
|
||||
|
||||
type SyncPinListener = () => void;
|
||||
const pinListenersBySyncKey = new Map<string, Set<SyncPinListener>>();
|
||||
|
||||
export const syncCursorRegistry = {
|
||||
setMetadata(syncKey: string, metadata: TooltipSyncMetadata | undefined): void {
|
||||
metadataBySyncKey.set(syncKey, metadata);
|
||||
@@ -36,4 +39,20 @@ export const syncCursorRegistry = {
|
||||
getActiveSeriesMetric(syncKey: string): Record<string, string> | null {
|
||||
return activeSeriesMetricBySyncKey.get(syncKey) ?? null;
|
||||
},
|
||||
|
||||
subscribePin(syncKey: string, listener: SyncPinListener): () => void {
|
||||
let listeners = pinListenersBySyncKey.get(syncKey);
|
||||
if (!listeners) {
|
||||
listeners = new Set();
|
||||
pinListenersBySyncKey.set(syncKey, listeners);
|
||||
}
|
||||
listeners.add(listener);
|
||||
return (): void => {
|
||||
pinListenersBySyncKey.get(syncKey)?.delete(listener);
|
||||
};
|
||||
},
|
||||
|
||||
broadcastPin(syncKey: string): void {
|
||||
pinListenersBySyncKey.get(syncKey)?.forEach((listener) => listener());
|
||||
},
|
||||
};
|
||||
|
||||
@@ -137,7 +137,7 @@ function applyReceiverSync({
|
||||
|
||||
if (commonKeys.length === 0) {
|
||||
uPlotInstance.setSeries(null, { focus: false });
|
||||
return noMatchResult;
|
||||
return [];
|
||||
}
|
||||
|
||||
if ((uPlotInstance.cursor.left ?? -1) < 0) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import afterLogin from 'AppRoutes/utils';
|
||||
import AuthError from 'components/AuthError/AuthError';
|
||||
import AuthPageContainer from 'components/AuthPageContainer';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { ArrowRight, CircleAlert } from 'lucide-react';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import tvUrl from '@/assets/svgs/tv.svg';
|
||||
@@ -28,8 +28,9 @@ type FormValues = {
|
||||
|
||||
function SignUp(): JSX.Element {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [confirmPasswordTouched, setConfirmPasswordTouched] = useState(false);
|
||||
|
||||
const [confirmPasswordError, setConfirmPasswordError] =
|
||||
useState<boolean>(false);
|
||||
const [formError, setFormError] = useState<APIError | null>();
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
@@ -83,10 +84,35 @@ function SignUp(): JSX.Element {
|
||||
})();
|
||||
};
|
||||
|
||||
const isPasswordMismatch =
|
||||
Boolean(confirmPassword) && password !== confirmPassword;
|
||||
const handleValuesChange: (changedValues: Partial<FormValues>) => void = (
|
||||
changedValues,
|
||||
) => {
|
||||
// Clear error if passwords match while typing (but don't set error until blur)
|
||||
if ('password' in changedValues || 'confirmPassword' in changedValues) {
|
||||
const { password, confirmPassword } = form.getFieldsValue();
|
||||
|
||||
const showPasswordMismatchError = confirmPasswordTouched && isPasswordMismatch;
|
||||
if (password && confirmPassword && password === confirmPassword) {
|
||||
setConfirmPasswordError(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handlePasswordBlur = (): void => {
|
||||
const { password, confirmPassword } = form.getFieldsValue();
|
||||
// Only validate if confirm password has a value
|
||||
if (confirmPassword) {
|
||||
const isSamePassword = password === confirmPassword;
|
||||
setConfirmPasswordError(!isSamePassword);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmPasswordBlur = (): void => {
|
||||
const { password, confirmPassword } = form.getFieldsValue();
|
||||
if (password && confirmPassword) {
|
||||
const isSamePassword = password === confirmPassword;
|
||||
setConfirmPasswordError(!isSamePassword);
|
||||
}
|
||||
};
|
||||
|
||||
const isValidForm = useMemo(
|
||||
(): boolean =>
|
||||
@@ -94,8 +120,8 @@ function SignUp(): JSX.Element {
|
||||
Boolean(email?.trim()) &&
|
||||
Boolean(password?.trim()) &&
|
||||
Boolean(confirmPassword?.trim()) &&
|
||||
password === confirmPassword,
|
||||
[loading, email, password, confirmPassword],
|
||||
!confirmPasswordError,
|
||||
[loading, email, password, confirmPassword, confirmPasswordError],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -114,7 +140,12 @@ function SignUp(): JSX.Element {
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
|
||||
<FormContainer onFinish={handleSubmit} form={form} className="signup-form">
|
||||
<FormContainer
|
||||
onFinish={handleSubmit}
|
||||
onValuesChange={handleValuesChange}
|
||||
form={form}
|
||||
className="signup-form"
|
||||
>
|
||||
<div className="signup-form-container">
|
||||
<div className="signup-form-fields">
|
||||
<div className="signup-field-container">
|
||||
@@ -144,6 +175,7 @@ function SignUp(): JSX.Element {
|
||||
placeholder="Enter new password"
|
||||
disabled={loading}
|
||||
className="signup-antd-input"
|
||||
onBlur={handlePasswordBlur}
|
||||
/>
|
||||
</FormContainer.Item>
|
||||
</div>
|
||||
@@ -153,12 +185,6 @@ function SignUp(): JSX.Element {
|
||||
<FormContainer.Item
|
||||
name="confirmPassword"
|
||||
validateTrigger="onBlur"
|
||||
validateStatus={showPasswordMismatchError ? 'error' : undefined}
|
||||
help={
|
||||
showPasswordMismatchError
|
||||
? "Passwords don't match. Please try again."
|
||||
: undefined
|
||||
}
|
||||
rules={[{ required: true, message: 'Please enter confirm password!' }]}
|
||||
>
|
||||
<AntdInput.Password
|
||||
@@ -167,7 +193,7 @@ function SignUp(): JSX.Element {
|
||||
placeholder="Confirm your new password"
|
||||
disabled={loading}
|
||||
className="signup-antd-input"
|
||||
onBlur={() => setConfirmPasswordTouched(true)}
|
||||
onBlur={handleConfirmPasswordBlur}
|
||||
/>
|
||||
</FormContainer.Item>
|
||||
</div>
|
||||
@@ -179,7 +205,19 @@ function SignUp(): JSX.Element {
|
||||
your admin for an invite link
|
||||
</Callout>
|
||||
|
||||
{formError && <AuthError error={formError} />}
|
||||
{confirmPasswordError && (
|
||||
<Callout
|
||||
type="error"
|
||||
size="small"
|
||||
showIcon
|
||||
icon={<CircleAlert size={12} />}
|
||||
className="signup-error-callout"
|
||||
>
|
||||
Passwords don't match. Please try again.
|
||||
</Callout>
|
||||
)}
|
||||
|
||||
{formError && !confirmPasswordError && <AuthError error={formError} />}
|
||||
|
||||
<div className="signup-form-actions">
|
||||
<Button
|
||||
|
||||
@@ -7,12 +7,7 @@ export const topTracesTableColumns = [
|
||||
dataIndex: 'trace_id',
|
||||
key: 'trace_id',
|
||||
render: (traceId: string): JSX.Element => (
|
||||
<Link
|
||||
to={`/trace/${traceId}`}
|
||||
className="trace-id-cell"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Link to={`/trace/${traceId}`} className="trace-id-cell">
|
||||
{traceId}
|
||||
</Link>
|
||||
),
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"github.com/SigNoz/govaluate"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
@@ -970,19 +969,11 @@ func (q *querier) prepareFillZeroArgsWithStep(functions []qbtypes.Function, req
|
||||
updatedFunctions := make([]qbtypes.Function, len(functions))
|
||||
copy(updatedFunctions, functions)
|
||||
|
||||
// funcFillZero expects start/end in milliseconds. req.Start/req.End may
|
||||
// arrive in s/ms/μs/ns depending on the caller; normalize via ToNanoSecs
|
||||
// (same pattern used elsewhere in the codebase, e.g. RecommendedStepInterval)
|
||||
// then convert to ms. Without this, an ns payload makes (end-start)/step
|
||||
// 10^6× too large and OOMs the process.
|
||||
startMs := querybuilder.ToNanoSecs(req.Start) / 1_000_000
|
||||
endMs := querybuilder.ToNanoSecs(req.End) / 1_000_000
|
||||
|
||||
for i, fn := range updatedFunctions {
|
||||
if fn.Name == qbtypes.FunctionNameFillZero && len(fn.Args) == 0 {
|
||||
fn.Args = []qbtypes.FunctionArg{
|
||||
{Value: float64(startMs)},
|
||||
{Value: float64(endMs)},
|
||||
{Value: float64(req.Start)},
|
||||
{Value: float64(req.End)},
|
||||
{Value: float64(step)},
|
||||
}
|
||||
updatedFunctions[i] = fn
|
||||
|
||||
@@ -952,7 +952,6 @@ func (m *Manager) PatchRule(ctx context.Context, ruleStr string, id valuer.UUID)
|
||||
m.logger.ErrorContext(ctx, "failed to unmarshal rule from db", slog.String("rule.id", id.StringValue()), errors.Attr(err))
|
||||
return nil, err
|
||||
}
|
||||
storedRule.NormalizeAlertType()
|
||||
|
||||
if err := json.Unmarshal([]byte(ruleStr), &storedRule); err != nil {
|
||||
m.logger.ErrorContext(ctx, "failed to unmarshal patched rule with given id", slog.String("rule.id", id.StringValue()), errors.Attr(err))
|
||||
|
||||
@@ -36,15 +36,6 @@ func (AlertType) Enum() []any {
|
||||
}
|
||||
}
|
||||
|
||||
// NormalizeAlertType corrects known legacy alert type values that were stored
|
||||
// before strict validation was introduced. The corrected value is persisted on
|
||||
// the next write, so the DB self-heals without a migration.
|
||||
func (r *PostableRule) NormalizeAlertType() {
|
||||
if r.AlertType == "LOG_BASED_ALERT" {
|
||||
r.AlertType = AlertTypeLogs
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
DefaultSchemaVersion = "v1"
|
||||
SchemaVersionV2Alpha1 = "v2alpha1"
|
||||
|
||||
Reference in New Issue
Block a user