mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-26 20:00:33 +01:00
Compare commits
16 Commits
config-alt
...
feat/field
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5d3adb212 | ||
|
|
d543fbc660 | ||
|
|
f2a18e8b6c | ||
|
|
c482e89dac | ||
|
|
4da5673e12 | ||
|
|
c3db819d8e | ||
|
|
c83578f211 | ||
|
|
04a4d3fe32 | ||
|
|
27dc996fd8 | ||
|
|
83b25f3e9a | ||
|
|
67e4c4611c | ||
|
|
7274421895 | ||
|
|
9c6656d6b9 | ||
|
|
5c54a2537c | ||
|
|
d28b2fa025 | ||
|
|
af5b925023 |
7
.github/CODEOWNERS
vendored
7
.github/CODEOWNERS
vendored
@@ -118,6 +118,9 @@ go.mod @therealpandey
|
||||
|
||||
/tests/integration/ @therealpandey
|
||||
|
||||
# e2e tests
|
||||
/tests/e2e/ @AshwinBhatkal
|
||||
|
||||
# Flagger Owners
|
||||
|
||||
/pkg/flagger/ @therealpandey
|
||||
@@ -162,3 +165,7 @@ go.mod @therealpandey
|
||||
/frontend/src/lib/dashboard/ @SigNoz/pulse-frontend
|
||||
/frontend/src/lib/dashboardVariables/ @SigNoz/pulse-frontend
|
||||
/frontend/src/components/NewSelect/ @SigNoz/pulse-frontend
|
||||
|
||||
## Dashboard V2
|
||||
/frontend/src/pages/DashboardPageV2/ @SigNoz/pulse-frontend
|
||||
/frontend/src/pages/DashboardsListPageV2/ @SigNoz/pulse-frontend
|
||||
|
||||
@@ -2689,7 +2689,6 @@ components:
|
||||
records:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesClusterRecord'
|
||||
nullable: true
|
||||
type: array
|
||||
requiredMetricsCheck:
|
||||
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
|
||||
@@ -2759,7 +2758,6 @@ components:
|
||||
records:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesDaemonSetRecord'
|
||||
nullable: true
|
||||
type: array
|
||||
requiredMetricsCheck:
|
||||
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
|
||||
@@ -2829,7 +2827,6 @@ components:
|
||||
records:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesDeploymentRecord'
|
||||
nullable: true
|
||||
type: array
|
||||
requiredMetricsCheck:
|
||||
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
|
||||
@@ -2908,7 +2905,6 @@ components:
|
||||
records:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesHostRecord'
|
||||
nullable: true
|
||||
type: array
|
||||
requiredMetricsCheck:
|
||||
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
|
||||
@@ -2984,7 +2980,6 @@ components:
|
||||
records:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesJobRecord'
|
||||
nullable: true
|
||||
type: array
|
||||
requiredMetricsCheck:
|
||||
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
|
||||
@@ -3032,7 +3027,6 @@ components:
|
||||
records:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesNamespaceRecord'
|
||||
nullable: true
|
||||
type: array
|
||||
requiredMetricsCheck:
|
||||
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
|
||||
@@ -3110,7 +3104,6 @@ components:
|
||||
records:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesNodeRecord'
|
||||
nullable: true
|
||||
type: array
|
||||
requiredMetricsCheck:
|
||||
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
|
||||
@@ -3209,7 +3202,6 @@ components:
|
||||
records:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodRecord'
|
||||
nullable: true
|
||||
type: array
|
||||
requiredMetricsCheck:
|
||||
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
|
||||
@@ -3554,7 +3546,6 @@ components:
|
||||
records:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesStatefulSetRecord'
|
||||
nullable: true
|
||||
type: array
|
||||
requiredMetricsCheck:
|
||||
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
|
||||
@@ -3615,7 +3606,6 @@ components:
|
||||
records:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesVolumeRecord'
|
||||
nullable: true
|
||||
type: array
|
||||
requiredMetricsCheck:
|
||||
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
"@signozhq/design-tokens": "2.1.4",
|
||||
"@signozhq/icons": "0.4.0",
|
||||
"@signozhq/resizable": "0.0.2",
|
||||
"@signozhq/ui": "0.0.19",
|
||||
"@signozhq/ui": "0.0.21",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"@tanstack/react-virtual": "3.13.22",
|
||||
"@uiw/codemirror-theme-copilot": "4.23.11",
|
||||
|
||||
@@ -14,8 +14,11 @@
|
||||
*/
|
||||
|
||||
const BANNED_COMPONENTS = {
|
||||
Typography: 'Use @signozhq/ui Typography instead of antd Typography.',
|
||||
Typography:
|
||||
'Use @signozhq/ui/typography Typography instead of antd Typography.',
|
||||
Switch: 'Use @signozhq/ui/switch Switch instead of antd Switch.',
|
||||
Badge: 'Use @signozhq/ui/badge instead of antd Badge.',
|
||||
Progress: 'Use @signozhq/ui/progress instead of antd Progress.',
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
31
frontend/pnpm-lock.yaml
generated
31
frontend/pnpm-lock.yaml
generated
@@ -77,8 +77,8 @@ importers:
|
||||
specifier: 0.0.2
|
||||
version: 0.0.2(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@signozhq/ui':
|
||||
specifier: 0.0.19
|
||||
version: 0.0.19(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)
|
||||
specifier: 0.0.21
|
||||
version: 0.0.21(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)
|
||||
'@tanstack/react-table':
|
||||
specifier: 8.21.3
|
||||
version: 8.21.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -3269,8 +3269,8 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^18.2.0
|
||||
|
||||
'@signozhq/ui@0.0.19':
|
||||
resolution: {integrity: sha512-2q6aRxN/PR4PlR2xJZAREEuvLPiDFggfFKzCW2Z5vHVVbrgnvZHWD1jPUuwszfEg0ceH3UvkwqceO7wN4uRJAA==}
|
||||
'@signozhq/ui@0.0.21':
|
||||
resolution: {integrity: sha512-uLM3Vqwxlk2USXbwtb3qRLpjZR9b9QSHFQq/jtcfYNMDmIE/sNjSj0nRkEhX4RqqRgsLRt2PVA33aeWxDOLO3g==}
|
||||
peerDependencies:
|
||||
'@signozhq/icons': 0.3.0
|
||||
react: ^18.2.0
|
||||
@@ -3851,27 +3851,6 @@ packages:
|
||||
peerDependencies:
|
||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||
|
||||
'@webassemblyjs/ast@1.14.1':
|
||||
resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==}
|
||||
|
||||
'@webassemblyjs/floating-point-hex-parser@1.13.2':
|
||||
resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==}
|
||||
|
||||
'@webassemblyjs/helper-api-error@1.13.2':
|
||||
resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==}
|
||||
|
||||
'@webassemblyjs/helper-buffer@1.14.1':
|
||||
resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==}
|
||||
|
||||
'@webassemblyjs/helper-numbers@1.13.2':
|
||||
resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==}
|
||||
|
||||
'@webassemblyjs/helper-wasm-bytecode@1.13.2':
|
||||
resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==}
|
||||
|
||||
'@webassemblyjs/helper-wasm-section@1.14.1':
|
||||
resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==}
|
||||
|
||||
'@xmldom/xmldom@0.8.13':
|
||||
resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
@@ -12034,7 +12013,7 @@ snapshots:
|
||||
- react-dom
|
||||
- tailwindcss
|
||||
|
||||
'@signozhq/ui@0.0.19(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)':
|
||||
'@signozhq/ui@0.0.21(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@chenglou/pretext': 0.0.5
|
||||
'@radix-ui/react-checkbox': 1.3.3(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
|
||||
@@ -3488,9 +3488,9 @@ export interface InframonitoringtypesClustersDTO {
|
||||
*/
|
||||
endTimeBeforeRetention: boolean;
|
||||
/**
|
||||
* @type array,null
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesClusterRecordDTO[] | null;
|
||||
records: InframonitoringtypesClusterRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
@@ -3566,9 +3566,9 @@ export interface InframonitoringtypesDaemonSetsDTO {
|
||||
*/
|
||||
endTimeBeforeRetention: boolean;
|
||||
/**
|
||||
* @type array,null
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesDaemonSetRecordDTO[] | null;
|
||||
records: InframonitoringtypesDaemonSetRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
@@ -3644,9 +3644,9 @@ export interface InframonitoringtypesDeploymentsDTO {
|
||||
*/
|
||||
endTimeBeforeRetention: boolean;
|
||||
/**
|
||||
* @type array,null
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesDeploymentRecordDTO[] | null;
|
||||
records: InframonitoringtypesDeploymentRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
@@ -3730,9 +3730,9 @@ export interface InframonitoringtypesHostsDTO {
|
||||
*/
|
||||
endTimeBeforeRetention: boolean;
|
||||
/**
|
||||
* @type array,null
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesHostRecordDTO[] | null;
|
||||
records: InframonitoringtypesHostRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
@@ -3816,9 +3816,9 @@ export interface InframonitoringtypesJobsDTO {
|
||||
*/
|
||||
endTimeBeforeRetention: boolean;
|
||||
/**
|
||||
* @type array,null
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesJobRecordDTO[] | null;
|
||||
records: InframonitoringtypesJobRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
@@ -3866,9 +3866,9 @@ export interface InframonitoringtypesNamespacesDTO {
|
||||
*/
|
||||
endTimeBeforeRetention: boolean;
|
||||
/**
|
||||
* @type array,null
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesNamespaceRecordDTO[] | null;
|
||||
records: InframonitoringtypesNamespaceRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
@@ -3933,9 +3933,9 @@ export interface InframonitoringtypesNodesDTO {
|
||||
*/
|
||||
endTimeBeforeRetention: boolean;
|
||||
/**
|
||||
* @type array,null
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesNodeRecordDTO[] | null;
|
||||
records: InframonitoringtypesNodeRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
@@ -4017,9 +4017,9 @@ export interface InframonitoringtypesPodsDTO {
|
||||
*/
|
||||
endTimeBeforeRetention: boolean;
|
||||
/**
|
||||
* @type array,null
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesPodRecordDTO[] | null;
|
||||
records: InframonitoringtypesPodRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
@@ -4437,9 +4437,9 @@ export interface InframonitoringtypesStatefulSetsDTO {
|
||||
*/
|
||||
endTimeBeforeRetention: boolean;
|
||||
/**
|
||||
* @type array,null
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesStatefulSetRecordDTO[] | null;
|
||||
records: InframonitoringtypesStatefulSetRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
@@ -4506,9 +4506,9 @@ export interface InframonitoringtypesVolumesDTO {
|
||||
*/
|
||||
endTimeBeforeRetention: boolean;
|
||||
/**
|
||||
* @type array,null
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesVolumeRecordDTO[] | null;
|
||||
records: InframonitoringtypesVolumeRecordDTO[];
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
|
||||
@@ -51,13 +51,6 @@
|
||||
background: var(--l1-background);
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
.ant-progress-bg {
|
||||
height: 8px !important;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr:hover > td {
|
||||
background: color-mix(in srgb, var(--l1-foreground) 4%, transparent);
|
||||
}
|
||||
|
||||
@@ -9,13 +9,13 @@ import {
|
||||
Flex,
|
||||
Input,
|
||||
InputRef,
|
||||
Progress,
|
||||
Space,
|
||||
Spin,
|
||||
TableColumnsType,
|
||||
TableColumnType,
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
import { Progress } from '@signozhq/ui/progress';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import type { FilterDropdownProps } from 'antd/lib/table/interface';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
@@ -59,7 +59,7 @@ function ProgressRender(item: string | number): JSX.Element {
|
||||
<Progress
|
||||
percent={percent}
|
||||
strokeLinecap="butt"
|
||||
size="small"
|
||||
showInfo
|
||||
strokeColor={((): string => {
|
||||
const cpuPercent = percent;
|
||||
if (cpuPercent >= 90) {
|
||||
|
||||
@@ -137,7 +137,6 @@ function CreateServiceAccountModal(): JSX.Element {
|
||||
<AuthZTooltip checks={[SACreatePermission]}>
|
||||
<Button
|
||||
type="submit"
|
||||
// @ts-expect-error -- form prop not in @signozhq/ui Button type - TODO: Fix this - @SagarRajput
|
||||
form="create-sa-form"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
|
||||
@@ -11,9 +11,6 @@ import { GuardAuthZ } from './GuardAuthZ';
|
||||
describe('GuardAuthZ', () => {
|
||||
const TestChild = (): ReactElement => <div>Protected Content</div>;
|
||||
const LoadingFallback = (): ReactElement => <div>Loading...</div>;
|
||||
const ErrorFallback = (error: Error): ReactElement => (
|
||||
<div>Error occurred: {error.message}</div>
|
||||
);
|
||||
const NoPermissionFallback = (_response: {
|
||||
requiredPermissionName: BrandedPermission;
|
||||
}): ReactElement => <div>Access denied</div>;
|
||||
@@ -90,40 +87,28 @@ describe('GuardAuthZ', () => {
|
||||
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render fallbackOnError when API error occurs', async () => {
|
||||
const errorMessage = 'Internal Server Error';
|
||||
|
||||
it('should render children when API error occurs and no fallbackOnError provided (fail open)', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
|
||||
return res(ctx.status(500), ctx.json({ error: errorMessage }));
|
||||
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
|
||||
}),
|
||||
);
|
||||
|
||||
render(
|
||||
<GuardAuthZ relation="read" object="role:*" fallbackOnError={ErrorFallback}>
|
||||
<GuardAuthZ relation="read" object="role:*">
|
||||
<TestChild />
|
||||
</GuardAuthZ>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Error occurred:/)).toBeInTheDocument();
|
||||
expect(screen.getByText('Protected Content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should pass error object to fallbackOnError function', async () => {
|
||||
const errorMessage = 'Network request failed';
|
||||
let receivedError: Error | null = null;
|
||||
|
||||
const errorFallbackWithCapture = (error: Error): ReactElement => {
|
||||
receivedError = error;
|
||||
return <div>Captured error: {error.message}</div>;
|
||||
};
|
||||
|
||||
it('should render fallbackOnError when API error occurs and fallbackOnError is provided', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
|
||||
return res(ctx.status(500), ctx.json({ error: errorMessage }));
|
||||
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -131,35 +116,14 @@ describe('GuardAuthZ', () => {
|
||||
<GuardAuthZ
|
||||
relation="read"
|
||||
object="role:*"
|
||||
fallbackOnError={errorFallbackWithCapture}
|
||||
fallbackOnError={<div>Custom error fallback</div>}
|
||||
>
|
||||
<TestChild />
|
||||
</GuardAuthZ>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(receivedError).not.toBeNull();
|
||||
});
|
||||
|
||||
expect(receivedError).toBeInstanceOf(Error);
|
||||
expect(screen.getByText(/Captured error:/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render null when error occurs and no fallbackOnError provided', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
|
||||
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
|
||||
}),
|
||||
);
|
||||
|
||||
const { container } = render(
|
||||
<GuardAuthZ relation="read" object="role:*">
|
||||
<TestChild />
|
||||
</GuardAuthZ>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.firstChild).toBeNull();
|
||||
expect(screen.getByText('Custom error fallback')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
|
||||
@@ -12,7 +12,7 @@ export type GuardAuthZProps<R extends AuthZRelation> = {
|
||||
relation: R;
|
||||
object: AuthZObject<R>;
|
||||
fallbackOnLoading?: JSX.Element;
|
||||
fallbackOnError?: (error: Error) => JSX.Element;
|
||||
fallbackOnError?: JSX.Element;
|
||||
fallbackOnNoPermissions?: (response: {
|
||||
requiredPermissionName: BrandedPermission;
|
||||
}) => JSX.Element;
|
||||
@@ -35,7 +35,7 @@ export function GuardAuthZ<R extends AuthZRelation>({
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return fallbackOnError?.(error) ?? null;
|
||||
return fallbackOnError ?? children;
|
||||
}
|
||||
|
||||
if (!permissions?.[permission]?.isGranted) {
|
||||
|
||||
@@ -4,7 +4,8 @@ import { useSelector } from 'react-redux';
|
||||
import { matchPath, useLocation } from 'react-router-dom';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Switch } from 'antd';
|
||||
import { Button } from 'antd';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { QueryParams } from 'constants/query';
|
||||
@@ -125,9 +126,8 @@ function ShareURLModal(): JSX.Element {
|
||||
<Info size={14} color={Color.BG_AMBER_600} />
|
||||
)}
|
||||
<Switch
|
||||
checked={enableAbsoluteTime}
|
||||
value={enableAbsoluteTime}
|
||||
disabled={!isValidateRelativeTime}
|
||||
size="small"
|
||||
onChange={(): void => {
|
||||
setEnableAbsoluteTime((prev) => !prev);
|
||||
}}
|
||||
|
||||
@@ -69,6 +69,8 @@ export function useLogsTableColumns({
|
||||
id: 'timestamp',
|
||||
header: 'Timestamp',
|
||||
accessorFn: (log): unknown => log.timestamp,
|
||||
canBeHidden: false,
|
||||
enableRemove: false,
|
||||
width: { default: 170, min: 170 },
|
||||
cell: ({ value }): ReactElement => {
|
||||
const ts = value as string | number;
|
||||
@@ -92,6 +94,7 @@ export function useLogsTableColumns({
|
||||
header: 'Body',
|
||||
accessorFn: (log): string => getBodyDisplayString(log.body),
|
||||
canBeHidden: false,
|
||||
enableRemove: false,
|
||||
width: { default: '100%', min: 300 },
|
||||
cell: ({ value, isActive }): ReactElement => (
|
||||
<TanStackTable.Text
|
||||
|
||||
@@ -62,6 +62,7 @@ describe('LogsFormatOptionsMenu (unit)', () => {
|
||||
onSearch: jest.fn(),
|
||||
onSelect: jest.fn(),
|
||||
onRemove: jest.fn(),
|
||||
onReorder: jest.fn(),
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
|
||||
@@ -13,7 +13,7 @@ export function NoAuthBanner(): JSX.Element {
|
||||
Impersonation mode: authentication is disabled. Anyone with access to this
|
||||
instance has admin privileges.{' '}
|
||||
<a
|
||||
href="https://signoz.io/docs/manage/administrator-guide/configuration/no-auth-mode/"
|
||||
href="https://signoz.io/docs/manage/administrator-guide/configuration/impersonation-mode/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
|
||||
@@ -14,7 +14,8 @@ import {
|
||||
ComboboxList,
|
||||
ComboboxTrigger,
|
||||
} from '@signozhq/ui/combobox';
|
||||
import { Skeleton, Switch, Tooltip } from 'antd';
|
||||
import { Skeleton, Tooltip } from 'antd';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import setLocalStorageKey from 'api/browser/localstorage/set';
|
||||
@@ -281,9 +282,8 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
<div className="api-quick-filters-header">
|
||||
<Typography.Text>Show IP addresses</Typography.Text>
|
||||
<Switch
|
||||
size="small"
|
||||
style={{ marginLeft: 'auto' }}
|
||||
checked={showIP ?? true}
|
||||
value={showIP ?? true}
|
||||
onChange={(checked): void => {
|
||||
logEvent('API Monitoring: Show IP addresses clicked', {
|
||||
showIP: checked,
|
||||
|
||||
@@ -4,7 +4,8 @@ import type {
|
||||
TableColumnsType as ColumnsType,
|
||||
TableColumnType as ColumnType,
|
||||
} from 'antd';
|
||||
import { Button, Dropdown, Flex, MenuProps, Switch } from 'antd';
|
||||
import { Button, Dropdown, Flex, MenuProps } from 'antd';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
@@ -60,9 +61,7 @@ function DynamicColumnTable({
|
||||
|
||||
const onToggleHandler =
|
||||
(index: number, column: ColumnGroupType<any> | ColumnType<any>) =>
|
||||
(checked: boolean, event: React.MouseEvent<HTMLButtonElement>): void => {
|
||||
event.stopPropagation();
|
||||
|
||||
(checked: boolean): void => {
|
||||
if (shouldSendAlertsLogEvent) {
|
||||
logEvent('Alert: Column toggled', {
|
||||
column: column?.title,
|
||||
@@ -88,10 +87,14 @@ function DynamicColumnTable({
|
||||
const items: MenuProps['items'] =
|
||||
dynamicColumns?.map((column, index) => ({
|
||||
label: (
|
||||
<div className="dynamicColumnsTable-items">
|
||||
<div
|
||||
className="dynamicColumnsTable-items"
|
||||
onClick={(e): void => e.stopPropagation()}
|
||||
role="presentation"
|
||||
>
|
||||
<div>{column.title?.toString()}</div>
|
||||
<Switch
|
||||
checked={columnsData?.findIndex((c) => c.key === column.key) !== -1}
|
||||
value={columnsData?.findIndex((c) => c.key === column.key) !== -1}
|
||||
onChange={onToggleHandler(index, column)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -127,7 +127,6 @@ function KeyFormPhase({
|
||||
>
|
||||
<Button
|
||||
type="submit"
|
||||
// @ts-expect-error -- form prop not in @signozhq/ui Button type - TODO: Fix this - @SagarRajput
|
||||
form={FORM_ID}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
|
||||
@@ -190,7 +190,6 @@ function EditKeyForm({
|
||||
>
|
||||
<Button
|
||||
type="submit"
|
||||
// @ts-expect-error -- form prop not in @signozhq/ui Button type - TODO: Fix this - @SagarRajput
|
||||
form={FORM_ID}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
|
||||
@@ -308,9 +308,7 @@ function TanStackTableInner<TData>(
|
||||
});
|
||||
|
||||
const hasSingleColumn = useMemo(
|
||||
() =>
|
||||
effectiveColumns.filter((c) => !c.pin && c.enableRemove !== false).length <=
|
||||
1,
|
||||
() => effectiveColumns.filter((c) => !c.pin).length <= 1,
|
||||
[effectiveColumns],
|
||||
);
|
||||
|
||||
|
||||
@@ -204,7 +204,7 @@ describe('createGuardedRoute', () => {
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render error fallback when API error occurs', async () => {
|
||||
it('should render the component when API error occurs (fail open)', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
|
||||
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
|
||||
@@ -230,12 +230,8 @@ describe('createGuardedRoute', () => {
|
||||
render(<GuardedComponent {...props} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Something went wrong/i)).toBeInTheDocument();
|
||||
expect(screen.getByText('Test Component: test-value')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.queryByText('Test Component: test-value'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render no permissions fallback when permission is denied', async () => {
|
||||
@@ -9,14 +9,11 @@ import { parsePermission } from 'hooks/useAuthZ/utils';
|
||||
|
||||
import noDataUrl from '@/assets/Icons/no-data.svg';
|
||||
|
||||
import ErrorBoundaryFallback from '../../pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import AppLoading from '../AppLoading/AppLoading';
|
||||
import { GuardAuthZ } from '../GuardAuthZ/GuardAuthZ';
|
||||
|
||||
import './createGuardedRoute.styles.scss';
|
||||
|
||||
const onErrorFallback = (): JSX.Element => <ErrorBoundaryFallback />;
|
||||
|
||||
function OnNoPermissionsFallback(response: {
|
||||
requiredPermissionName: BrandedPermission;
|
||||
}): ReactElement {
|
||||
@@ -63,7 +60,6 @@ export function createGuardedRoute<P extends object, R extends AuthZRelation>(
|
||||
relation={relation}
|
||||
object={resolvedObject}
|
||||
fallbackOnLoading={<AppLoading />}
|
||||
fallbackOnError={onErrorFallback}
|
||||
fallbackOnNoPermissions={(response): ReactElement => (
|
||||
<OnNoPermissionsFallback {...response} />
|
||||
)}
|
||||
|
||||
@@ -45,6 +45,10 @@
|
||||
.contributors-row {
|
||||
height: 80px;
|
||||
}
|
||||
.top-contributors-progress {
|
||||
--progress-background: transparent;
|
||||
}
|
||||
|
||||
&__content {
|
||||
.ant-table {
|
||||
&-cell {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { HTMLAttributes } from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Progress, Table, TableColumnsType as ColumnsType } from 'antd';
|
||||
import { Table, TableColumnsType as ColumnsType } from 'antd';
|
||||
import { Progress } from '@signozhq/ui/progress';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ConditionalAlertPopover } from 'container/AlertHistory/AlertPopover/AlertPopover';
|
||||
import AlertLabels from 'pages/AlertDetails/AlertHeader/AlertLabels/AlertLabels';
|
||||
@@ -51,8 +52,8 @@ function TopContributorsRows({
|
||||
<Progress
|
||||
percent={(count / totalCurrentTriggers) * 100}
|
||||
showInfo={false}
|
||||
trailColor="rgba(255, 255, 255, 0)"
|
||||
strokeColor={Color.BG_ROBIN_500}
|
||||
className="top-contributors-progress"
|
||||
/>
|
||||
</ConditionalAlertPopover>
|
||||
),
|
||||
|
||||
@@ -141,12 +141,9 @@
|
||||
|
||||
.progress-container {
|
||||
width: 158px;
|
||||
.ant-progress {
|
||||
margin: 0;
|
||||
|
||||
.ant-progress-text {
|
||||
font-weight: 600;
|
||||
}
|
||||
span {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { QueryFunctionContext, useQueries, useQuery } from 'react-query';
|
||||
import { Spin, Switch, Table, Tooltip } from 'antd';
|
||||
import { Spin, Table, Tooltip } from 'antd';
|
||||
import { Info, Loader } from '@signozhq/icons';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { getQueryRangeV5 } from 'api/v5/queryRange/getQueryRange';
|
||||
import { MetricRangePayloadV5, ScalarData } from 'api/v5/v5';
|
||||
@@ -170,11 +171,7 @@ function TopErrors({
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<Switch
|
||||
checked={showStatusCodeErrors}
|
||||
onChange={setShowStatusCodeErrors}
|
||||
size="small"
|
||||
/>
|
||||
<Switch value={showStatusCodeErrors} onChange={setShowStatusCodeErrors} />
|
||||
<span style={{ color: 'white', fontSize: '14px' }}>
|
||||
Status Message Exists
|
||||
</span>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useQueries } from 'react-query';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Progress, Skeleton, Tooltip } from 'antd';
|
||||
import { Skeleton, Tooltip } from 'antd';
|
||||
import { Progress } from '@signozhq/ui/progress';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
@@ -136,12 +137,11 @@ function DomainMetrics({
|
||||
<Tooltip title={formattedDomainMetricsData.errorRate}>
|
||||
{formattedDomainMetricsData.errorRate !== '-' ? (
|
||||
<Progress
|
||||
status="active"
|
||||
percent={Number(
|
||||
Number(formattedDomainMetricsData.errorRate).toFixed(2),
|
||||
)}
|
||||
strokeLinecap="butt"
|
||||
size="small"
|
||||
showInfo
|
||||
strokeColor={((): string => {
|
||||
const errorRatePercent = Number(
|
||||
Number(formattedDomainMetricsData.errorRate).toFixed(2),
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useMemo } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Progress, Skeleton, Tooltip } from 'antd';
|
||||
import { Skeleton, Tooltip } from 'antd';
|
||||
import { Progress } from '@signozhq/ui/progress';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import {
|
||||
getDisplayValue,
|
||||
@@ -80,10 +81,9 @@ function EndPointMetrics({
|
||||
<Tooltip title={metricsData?.errorRate}>
|
||||
{metricsData?.errorRate !== '-' ? (
|
||||
<Progress
|
||||
status="active"
|
||||
percent={Number(Number(metricsData?.errorRate ?? 0).toFixed(2))}
|
||||
strokeLinecap="butt"
|
||||
size="small"
|
||||
showInfo
|
||||
strokeColor={((): string => {
|
||||
const errorRatePercent = Number(
|
||||
Number(metricsData?.errorRate ?? 0).toFixed(2),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Progress, TableColumnType as ColumnType, Tag, Tooltip } from 'antd';
|
||||
import { TableColumnType as ColumnType, Tag, Tooltip } from 'antd';
|
||||
import { Progress } from '@signozhq/ui/progress';
|
||||
import { convertFiltersToExpressionWithExistingQuery } from 'components/QueryBuilderV2/utils';
|
||||
import {
|
||||
FiltersType,
|
||||
@@ -257,10 +258,9 @@ export const columnsConfig: ColumnType<APIDomainsRowData>[] = [
|
||||
errorRate === 'n/a' || errorRate === '-' ? 0 : errorRate;
|
||||
return (
|
||||
<Progress
|
||||
status="active"
|
||||
percent={Number((errorRateValue as number).toFixed(2))}
|
||||
strokeLinecap="butt"
|
||||
size="small"
|
||||
showInfo
|
||||
strokeColor={((): string => {
|
||||
const errorRatePercent = Number((errorRateValue as number).toFixed(2));
|
||||
if (errorRatePercent >= 90) {
|
||||
@@ -1022,14 +1022,13 @@ export const getEndPointsColumnsConfig = (
|
||||
className: `column`,
|
||||
render: (errorRate: number | string): React.ReactNode => (
|
||||
<Progress
|
||||
status="active"
|
||||
percent={Number(
|
||||
(
|
||||
(errorRate === 'n/a' || errorRate === '-' ? 0 : errorRate) as number
|
||||
).toFixed(1),
|
||||
)}
|
||||
strokeLinecap="butt"
|
||||
size="small"
|
||||
showInfo
|
||||
strokeColor={((): string => {
|
||||
const errorRatePercent = Number((errorRate as number).toFixed(1));
|
||||
if (errorRatePercent >= 90) {
|
||||
@@ -2514,10 +2513,9 @@ export const dependentServicesColumns: ColumnType<DependentServicesData>[] = [
|
||||
render: (errorPercentage: number | string): React.ReactNode =>
|
||||
errorPercentage !== '-' ? (
|
||||
<Progress
|
||||
status="active"
|
||||
percent={Number((errorPercentage as number).toFixed(2))}
|
||||
strokeLinecap="butt"
|
||||
size="small"
|
||||
showInfo
|
||||
strokeColor={((): string => {
|
||||
const errorPercentagePercent = Number(
|
||||
(errorPercentage as number).toFixed(2),
|
||||
@@ -3022,14 +3020,13 @@ export const getAllEndpointsWidgetData = (
|
||||
),
|
||||
F1: (errorRate: any): ReactNode => (
|
||||
<Progress
|
||||
status="active"
|
||||
percent={Number(
|
||||
(
|
||||
(errorRate === 'n/a' || errorRate === '-' ? 0 : errorRate) as number
|
||||
).toFixed(2),
|
||||
)}
|
||||
strokeLinecap="butt"
|
||||
size="small"
|
||||
showInfo
|
||||
strokeColor={((): string => {
|
||||
const errorRatePercent = Number(
|
||||
(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Button, Flex, SelectProps, Switch } from 'antd';
|
||||
import { Button, Flex, SelectProps } from 'antd';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import type { BaseOptionType, DefaultOptionType } from 'antd/es/select';
|
||||
import { getInvolvedQueriesInTraceOperator } from 'components/QueryBuilderV2/QueryV2/TraceOperator/utils/utils';
|
||||
@@ -419,8 +420,8 @@ export function RoutingPolicyBanner({
|
||||
</Typography.Text>
|
||||
<div className="routing-policies-info-banner-right">
|
||||
<Switch
|
||||
checked={notificationSettings.routingPolicies}
|
||||
data-testid="routing-policies-switch"
|
||||
value={notificationSettings.routingPolicies}
|
||||
testId="routing-policies-switch"
|
||||
onChange={(value): void => {
|
||||
setNotificationSettings({
|
||||
type: 'SET_ROUTING_POLICIES',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Switch, Tooltip } from 'antd';
|
||||
import { Tooltip } from 'antd';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Info } from '@signozhq/icons';
|
||||
|
||||
@@ -49,7 +50,7 @@ function AdvancedOptionItem({
|
||||
>
|
||||
{input}
|
||||
</div>
|
||||
<Switch onChange={handleOnToggle} checked={showInput} />
|
||||
<Switch onChange={handleOnToggle} value={showInput} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,8 @@ import { useQuery } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { orange } from '@ant-design/colors';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Collapse, Input, Select, Switch, Tag } from 'antd';
|
||||
import { Button, Collapse, Input, Select, Tag } from 'antd';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
|
||||
import cx from 'classnames';
|
||||
@@ -763,7 +764,7 @@ function VariableItem({
|
||||
</Typography>
|
||||
</LabelContainer>
|
||||
<Switch
|
||||
checked={variableMultiSelect}
|
||||
value={variableMultiSelect}
|
||||
onChange={(e): void => {
|
||||
setVariableMultiSelect(e);
|
||||
if (!e) {
|
||||
@@ -780,7 +781,7 @@ function VariableItem({
|
||||
</Typography>
|
||||
</LabelContainer>
|
||||
<Switch
|
||||
checked={variableShowALLOption}
|
||||
value={variableShowALLOption}
|
||||
onChange={(e): void => setVariableShowALLOption(e)}
|
||||
/>
|
||||
</VariableItemRow>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Dispatch, ReactElement, SetStateAction } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, FormInstance, Input, Select, Switch } from 'antd';
|
||||
import { Form, FormInstance, Input, Select } from 'antd';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import type { Store } from 'antd/lib/form/interface';
|
||||
import ROUTES from 'constants/routes';
|
||||
@@ -82,8 +83,8 @@ function FormAlertChannels({
|
||||
name="send_resolved"
|
||||
>
|
||||
<Switch
|
||||
defaultChecked={initialValue?.send_resolved}
|
||||
data-testid="field-send-resolved-checkbox"
|
||||
defaultValue={initialValue?.send_resolved}
|
||||
testId="field-send-resolved-checkbox"
|
||||
onChange={(value): void => {
|
||||
setSelectedConfig((state) => ({
|
||||
...state,
|
||||
|
||||
@@ -2,7 +2,8 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import { Plus } from '@signozhq/icons';
|
||||
import { Button, Flex, Form, Select, Switch, Tooltip } from 'antd';
|
||||
import { Button, Flex, Form, Select, Tooltip } from 'antd';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import getAll from 'api/channels/getAll';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
||||
@@ -198,10 +199,10 @@ function BasicInfo({
|
||||
placement="right"
|
||||
>
|
||||
<Switch
|
||||
checked={shouldBroadCastToAllChannels}
|
||||
value={shouldBroadCastToAllChannels}
|
||||
onChange={handleBroadcastToAllChannels}
|
||||
disabled={noChannels || !!isLoading}
|
||||
data-testid="alert-broadcast-to-all-channels"
|
||||
testId="alert-broadcast-to-all-channels"
|
||||
/>
|
||||
</Tooltip>
|
||||
</FormItemMedium>
|
||||
|
||||
@@ -39,7 +39,5 @@
|
||||
|
||||
width: 100% !important;
|
||||
|
||||
.ant-progress-steps-outer {
|
||||
width: 100% !important;
|
||||
}
|
||||
--progress-width: 100%;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Progress } from 'antd';
|
||||
import { Progress } from '@signozhq/ui/progress';
|
||||
|
||||
import { ChecklistItem } from '../HomeChecklist/HomeChecklist';
|
||||
|
||||
@@ -15,9 +15,7 @@ function StepsProgress({
|
||||
|
||||
const totalChecklistItems = checklistItems.length;
|
||||
|
||||
const progress = Math.round(
|
||||
(completedChecklistItems.length / totalChecklistItems) * 100,
|
||||
);
|
||||
const progress = (completedChecklistItems.length / totalChecklistItems) * 100;
|
||||
|
||||
return (
|
||||
<div className="steps-progress-container">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Progress, Tag } from 'antd';
|
||||
import { Tag } from 'antd';
|
||||
import { Progress } from '@signozhq/ui/progress';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import {
|
||||
getHostLists,
|
||||
@@ -79,8 +80,8 @@ export const hostDetailsMetadataConfig: K8sDetailsMetadataConfig<HostData>[] = [
|
||||
render: (value): React.ReactNode => (
|
||||
<Progress
|
||||
percent={Number(Number(value).toFixed(1))}
|
||||
size="small"
|
||||
strokeColor={getProgressColor(Number(value))}
|
||||
showInfo
|
||||
/>
|
||||
),
|
||||
},
|
||||
@@ -90,8 +91,8 @@ export const hostDetailsMetadataConfig: K8sDetailsMetadataConfig<HostData>[] = [
|
||||
render: (value): React.ReactNode => (
|
||||
<Progress
|
||||
percent={Number(Number(value).toFixed(1))}
|
||||
size="small"
|
||||
strokeColor={getMemoryProgressColor(Number(value))}
|
||||
showInfo
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -60,11 +60,6 @@
|
||||
& > div {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:global(.ant-progress-bg) {
|
||||
height: 8px !important;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
|
||||
@@ -103,12 +103,8 @@
|
||||
.progress-container {
|
||||
width: 158px;
|
||||
|
||||
.ant-progress {
|
||||
margin: 0;
|
||||
|
||||
.ant-progress-text {
|
||||
font-weight: 600;
|
||||
}
|
||||
span {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,10 +288,6 @@
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
.ant-progress-bg {
|
||||
height: 8px !important;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr:hover > td {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Progress } from 'antd';
|
||||
import { Progress } from '@signozhq/ui/progress';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import {
|
||||
getMemoryProgressColor,
|
||||
@@ -53,7 +53,6 @@ export function EntityProgressBar({
|
||||
<Progress
|
||||
percent={percentage}
|
||||
strokeLinecap="butt"
|
||||
size="small"
|
||||
status="normal"
|
||||
strokeColor={getStrokeColor(type, value)}
|
||||
className={styles.progressBar}
|
||||
|
||||
@@ -15,13 +15,13 @@ import {
|
||||
Modal,
|
||||
Row,
|
||||
Select,
|
||||
Switch,
|
||||
Table,
|
||||
TablePaginationConfig,
|
||||
TableProps as AntDTableProps,
|
||||
Tag,
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import type { NotificationInstance } from 'antd/es/notification/interface';
|
||||
import type { CollapseProps } from 'antd/lib';
|
||||
@@ -1180,8 +1180,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
<div className="limit-enable-disable-toggle">
|
||||
<Form.Item name="enableDailyLimit">
|
||||
<Switch
|
||||
size="small"
|
||||
checked={activeSignal?.config?.day?.enabled}
|
||||
value={activeSignal?.config?.day?.enabled}
|
||||
onChange={(value): void => {
|
||||
setActiveSignal((prev) =>
|
||||
prev
|
||||
@@ -1270,8 +1269,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
<div className="limit-enable-disable-toggle">
|
||||
<Form.Item name="enableSecondLimit">
|
||||
<Switch
|
||||
size="small"
|
||||
checked={activeSignal?.config?.second?.enabled}
|
||||
value={activeSignal?.config?.second?.enabled}
|
||||
onChange={(value): void => {
|
||||
setActiveSignal((prev) =>
|
||||
prev
|
||||
|
||||
@@ -21,11 +21,11 @@ import {
|
||||
Modal,
|
||||
Popover,
|
||||
Skeleton,
|
||||
Switch,
|
||||
Table,
|
||||
Tag,
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import type { TableProps } from 'antd/lib';
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
@@ -965,8 +965,7 @@ function DashboardsList(): JSX.Element {
|
||||
<div className="connection-line" />
|
||||
<div className="right">
|
||||
<Switch
|
||||
size="small"
|
||||
checked
|
||||
value
|
||||
disabled
|
||||
onChange={(check): void =>
|
||||
setVisibleColumns((prev) => ({
|
||||
@@ -985,9 +984,8 @@ function DashboardsList(): JSX.Element {
|
||||
<div className="connection-line" />
|
||||
<div className="right">
|
||||
<Switch
|
||||
size="small"
|
||||
disabled
|
||||
checked
|
||||
value
|
||||
onChange={(check): void =>
|
||||
setVisibleColumns((prev) => ({
|
||||
...prev,
|
||||
@@ -1005,8 +1003,7 @@ function DashboardsList(): JSX.Element {
|
||||
<div className="connection-line" />
|
||||
<div className="right">
|
||||
<Switch
|
||||
size="small"
|
||||
checked={visibleColumns.updatedAt}
|
||||
value={visibleColumns.updatedAt}
|
||||
onChange={(check): void =>
|
||||
setVisibleColumns((prev) => ({
|
||||
...prev,
|
||||
@@ -1024,8 +1021,7 @@ function DashboardsList(): JSX.Element {
|
||||
<div className="connection-line" />
|
||||
<div className="right">
|
||||
<Switch
|
||||
size="small"
|
||||
checked={visibleColumns.updatedBy}
|
||||
value={visibleColumns.updatedBy}
|
||||
onChange={(check): void =>
|
||||
setVisibleColumns((prev) => ({
|
||||
...prev,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Switch } from 'antd';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
|
||||
import { MAX_LOGS_LIST_SIZE } from 'constants/liveTail';
|
||||
@@ -228,9 +228,8 @@ function LiveLogsContainer({
|
||||
<div className="live-logs-frequency-chart-view-controller">
|
||||
<Typography>Frequency chart</Typography>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={showLiveLogsFrequencyChart}
|
||||
defaultChecked
|
||||
value={showLiveLogsFrequencyChart}
|
||||
defaultValue
|
||||
onChange={handleToggleFrequencyChart}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,6 @@ import { useLogsTableColumns } from 'components/Logs/TableView/useLogsTableColum
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import type { TanStackTableHandle } from 'components/TanStackTableView';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { useHiddenColumnIds } from 'components/TanStackTableView/useColumnStore';
|
||||
import { CARD_BODY_STYLE } from 'constants/card';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { OptionFormatTypes } from 'constants/optionsFormatTypes';
|
||||
@@ -24,13 +23,11 @@ import { QueryParams } from 'constants/query';
|
||||
import { InfinityWrapperStyled } from 'container/LogsExplorerList/styles';
|
||||
import { convertKeysToColumnFields } from 'container/LogsExplorerList/utils';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { defaultLogsSelectedColumns } from 'container/OptionsMenu/constants';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import useLogDetailHandlers from 'hooks/logs/useLogDetailHandlers';
|
||||
import useScrollToLog from 'hooks/logs/useScrollToLog';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useEventSource } from 'providers/EventSource';
|
||||
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
// interfaces
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
@@ -54,9 +51,6 @@ function LiveLogsList({
|
||||
const { isConnectionLoading } = useEventSource();
|
||||
|
||||
const { activeLogId } = useCopyLogLink();
|
||||
const { logs: logsPreferences } = usePreferenceContext();
|
||||
const hiddenColumnIds = useHiddenColumnIds(LOCALSTORAGE.LOGS_LIST_COLUMNS);
|
||||
const hasReconciledHiddenColumnsRef = useRef(false);
|
||||
|
||||
const {
|
||||
activeLog,
|
||||
@@ -72,7 +66,7 @@ function LiveLogsList({
|
||||
[logs],
|
||||
);
|
||||
|
||||
const { options } = useOptionsMenu({
|
||||
const { options, config } = useOptionsMenu({
|
||||
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
||||
dataSource: DataSource.LOGS,
|
||||
aggregateOperator: StringOperators.NOOP,
|
||||
@@ -83,16 +77,7 @@ function LiveLogsList({
|
||||
[formattedLogs, activeLogId],
|
||||
);
|
||||
|
||||
const selectedFields = convertKeysToColumnFields([
|
||||
...defaultLogsSelectedColumns,
|
||||
...options.selectColumns,
|
||||
]);
|
||||
|
||||
const syncedSelectedColumns = useMemo(
|
||||
() =>
|
||||
options.selectColumns.filter(({ name }) => !hiddenColumnIds.includes(name)),
|
||||
[options.selectColumns, hiddenColumnIds],
|
||||
);
|
||||
const selectedFields = convertKeysToColumnFields(options.selectColumns);
|
||||
|
||||
const logsColumns = useLogsTableColumns({
|
||||
fields: selectedFields,
|
||||
@@ -100,30 +85,6 @@ function LiveLogsList({
|
||||
appendTo: 'end',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (hasReconciledHiddenColumnsRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
hasReconciledHiddenColumnsRef.current = true;
|
||||
|
||||
if (syncedSelectedColumns.length === options.selectColumns.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
logsPreferences.updateColumns(syncedSelectedColumns);
|
||||
}, [logsPreferences, options.selectColumns.length, syncedSelectedColumns]);
|
||||
|
||||
const handleColumnRemove = useCallback(
|
||||
(columnId: string) => {
|
||||
const updatedColumns = options.selectColumns.filter(
|
||||
({ name }) => name !== columnId,
|
||||
);
|
||||
logsPreferences.updateColumns(updatedColumns);
|
||||
},
|
||||
[options.selectColumns, logsPreferences],
|
||||
);
|
||||
|
||||
const makeOnLogCopy = useCallback(
|
||||
(log: ILog) =>
|
||||
(event: MouseEvent<HTMLElement>): void => {
|
||||
@@ -237,7 +198,7 @@ function LiveLogsList({
|
||||
ref={ref as React.Ref<TanStackTableHandle>}
|
||||
columns={logsColumns}
|
||||
columnStorageKey={LOCALSTORAGE.LOGS_LIST_COLUMNS}
|
||||
onColumnRemove={handleColumnRemove}
|
||||
onColumnRemove={config?.addColumn?.onRemove}
|
||||
plainTextCellLineClamp={options.maxLines}
|
||||
cellTypographySize={options.fontSize}
|
||||
data={formattedLogs}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import MEditor, { EditorProps, Monaco } from '@monaco-editor/react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Switch } from 'antd';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
|
||||
@@ -81,7 +81,7 @@ function JSONView({ logData }: JSONViewProps): JSX.Element {
|
||||
<div className="log-switch">
|
||||
<div className="wrap-word-switch">
|
||||
<Typography.Text>Wrap text</Typography.Text>
|
||||
<Switch checked={isWrapWord} onChange={handleWrapWord} size="small" />
|
||||
<Switch value={isWrapWord} onChange={handleWrapWord} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,8 @@ import { ReactNode, useState } from 'react';
|
||||
import MEditor, { EditorProps, Monaco } from '@monaco-editor/react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Collapse, Divider, Input, Switch, Tag } from 'antd';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import { Collapse, Divider, Input, Tag } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
|
||||
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
@@ -136,7 +137,7 @@ function Overview({
|
||||
<div className="log-switch">
|
||||
<div className="wrap-word-switch">
|
||||
<Typography.Text>Wrap text</Typography.Text>
|
||||
<Switch checked={isWrapWord} onChange={handleWrapWord} size="small" />
|
||||
<Switch value={isWrapWord} onChange={handleWrapWord} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,21 +18,19 @@ import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import Spinner from 'components/Spinner';
|
||||
import type { TanStackTableHandle } from 'components/TanStackTableView';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { useHiddenColumnIds } from 'components/TanStackTableView/useColumnStore';
|
||||
import type { TableColumnDef } from 'components/TanStackTableView/types';
|
||||
import { CARD_BODY_STYLE } from 'constants/card';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch';
|
||||
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { defaultLogsSelectedColumns } from 'container/OptionsMenu/constants';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import useLogDetailHandlers from 'hooks/logs/useLogDetailHandlers';
|
||||
import useScrollToLog from 'hooks/logs/useScrollToLog';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import APIError from 'types/api/error';
|
||||
// interfaces
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
@@ -69,10 +67,6 @@ function LogsExplorerList({
|
||||
const [, setCopy] = useCopyToClipboard();
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const { activeLogId } = useCopyLogLink();
|
||||
const { logs: logsPreferences } = usePreferenceContext();
|
||||
const hiddenColumnIds = useHiddenColumnIds(LOCALSTORAGE.LOGS_LIST_COLUMNS);
|
||||
const hasReconciledHiddenColumnsRef = useRef(false);
|
||||
|
||||
const {
|
||||
activeLog,
|
||||
onAddToQuery,
|
||||
@@ -81,7 +75,7 @@ function LogsExplorerList({
|
||||
handleCloseLogDetail,
|
||||
} = useLogDetailHandlers();
|
||||
|
||||
const { options } = useOptionsMenu({
|
||||
const { options, config } = useOptionsMenu({
|
||||
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
||||
dataSource: DataSource.LOGS,
|
||||
aggregateOperator:
|
||||
@@ -97,28 +91,15 @@ function LogsExplorerList({
|
||||
);
|
||||
|
||||
const selectedFields = useMemo(
|
||||
() =>
|
||||
convertKeysToColumnFields([
|
||||
...defaultLogsSelectedColumns,
|
||||
...options.selectColumns,
|
||||
]),
|
||||
[options],
|
||||
() => convertKeysToColumnFields(options.selectColumns),
|
||||
[options.selectColumns],
|
||||
);
|
||||
|
||||
const syncedSelectedColumns = useMemo(
|
||||
() =>
|
||||
options.selectColumns.filter(({ name }) => !hiddenColumnIds.includes(name)),
|
||||
[options.selectColumns, hiddenColumnIds],
|
||||
);
|
||||
|
||||
const handleColumnRemove = useCallback(
|
||||
(columnId: string) => {
|
||||
const updatedColumns = options.selectColumns.filter(
|
||||
({ name }) => name !== columnId,
|
||||
);
|
||||
logsPreferences.updateColumns(updatedColumns);
|
||||
const handleColumnOrderChange = useCallback(
|
||||
(cols: TableColumnDef<ILog>[]): void => {
|
||||
config?.addColumn?.onReorder(cols.map((c) => c.id));
|
||||
},
|
||||
[options.selectColumns, logsPreferences],
|
||||
[config],
|
||||
);
|
||||
|
||||
const logsColumns = useLogsTableColumns({
|
||||
@@ -161,20 +142,6 @@ function LogsExplorerList({
|
||||
}
|
||||
}, [isLoading, isFetching, isError, logs.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasReconciledHiddenColumnsRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
hasReconciledHiddenColumnsRef.current = true;
|
||||
|
||||
if (syncedSelectedColumns.length === options.selectColumns.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
logsPreferences.updateColumns(syncedSelectedColumns);
|
||||
}, [logsPreferences, options.selectColumns.length, syncedSelectedColumns]);
|
||||
|
||||
const getItemContent = useCallback(
|
||||
(_: number, log: ILog): JSX.Element => {
|
||||
if (options.format === 'raw') {
|
||||
@@ -237,7 +204,8 @@ function LogsExplorerList({
|
||||
ref={ref as React.Ref<TanStackTableHandle>}
|
||||
columns={logsColumns}
|
||||
columnStorageKey={LOCALSTORAGE.LOGS_LIST_COLUMNS}
|
||||
onColumnRemove={handleColumnRemove}
|
||||
onColumnRemove={config?.addColumn?.onRemove}
|
||||
onColumnOrderChange={handleColumnOrderChange}
|
||||
plainTextCellLineClamp={options.maxLines}
|
||||
cellTypographySize={options.fontSize}
|
||||
data={logs}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Switch } from 'antd';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import DownloadOptionsMenu from 'components/DownloadOptionsMenu/DownloadOptionsMenu';
|
||||
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
|
||||
@@ -69,9 +69,8 @@ function LogsActionsContainer({
|
||||
<div className="frequency-chart-view-controller">
|
||||
<Typography>Frequency chart</Typography>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={showFrequencyChart}
|
||||
defaultChecked
|
||||
value={showFrequencyChart}
|
||||
defaultValue
|
||||
onChange={handleToggleFrequencyChart}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -5,11 +5,11 @@ import { useParams } from 'react-router-dom';
|
||||
import { Search } from '@signozhq/icons';
|
||||
import {
|
||||
InputRef,
|
||||
Switch,
|
||||
TableColumnsType as ColumnsType,
|
||||
TableColumnType as ColumnType,
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
@@ -212,11 +212,7 @@ function TopOperationsTable({
|
||||
/>
|
||||
</div>
|
||||
<div className="top-operation__entry-point">
|
||||
<Switch
|
||||
checked={isEntryPoint}
|
||||
onChange={onEntryPointToggle}
|
||||
size="small"
|
||||
/>
|
||||
<Switch value={isEntryPoint} onChange={onEntryPointToggle} />
|
||||
<span className="top-operation__entry-point-label">Entrypoint Spans</span>
|
||||
<TextToolTip
|
||||
text={entryPointSpanInfo.text}
|
||||
|
||||
@@ -2,7 +2,8 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Switch, Tooltip } from 'antd';
|
||||
import { Tooltip } from 'antd';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||
import WarningPopover from 'components/WarningPopover/WarningPopover';
|
||||
@@ -358,10 +359,9 @@ function Explorer(): JSX.Element {
|
||||
title={oneChartPerQueryDisabledTooltip}
|
||||
>
|
||||
<Switch
|
||||
checked={showOneChartPerQuery}
|
||||
value={showOneChartPerQuery}
|
||||
onChange={handleToggleShowOneChartPerQuery}
|
||||
disabled={disableOneChartPerQuery || splitedQueries.length <= 1}
|
||||
size="small"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,8 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Skeleton, Switch } from 'antd';
|
||||
import { Button, Skeleton } from 'antd';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import Uplot from 'components/Uplot';
|
||||
@@ -206,7 +207,7 @@ function GraphView({
|
||||
</Button.Group>
|
||||
<div className="view-toggle-button">
|
||||
<Switch
|
||||
checked={viewType === 'graph'}
|
||||
value={viewType === 'graph'}
|
||||
onChange={(checked): void => {
|
||||
const newViewType = checked ? 'graph' : 'table';
|
||||
setViewType(newViewType);
|
||||
|
||||
@@ -142,13 +142,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
.ant-progress-bg {
|
||||
height: 8px !important;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr:hover > td {
|
||||
background: color-mix(in srgb, var(--l1-foreground) 4%, transparent);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Switch } from 'antd';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { Delete } from '@signozhq/icons';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
@@ -49,10 +49,10 @@ function TimezoneAdaptation(): JSX.Element {
|
||||
<div className="timezone-adaption__header">
|
||||
<h2 className="timezone-adaption__title">Adapt to my timezone</h2>
|
||||
<Switch
|
||||
checked={isAdaptationEnabled}
|
||||
value={isAdaptationEnabled}
|
||||
onChange={handleSwitchChange}
|
||||
style={getSwitchStyles()}
|
||||
data-testid="timezone-adaptation-switch"
|
||||
testId="timezone-adaptation-switch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -127,6 +127,12 @@
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding-bottom: 16px;
|
||||
|
||||
.password-error-text {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--bg-cherry-400);
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-color-picker-trigger {
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Input, Modal } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import {
|
||||
updateMyPassword,
|
||||
useUpdateMyUserV2,
|
||||
} from 'api/generated/services/users';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { Check, FileTerminal, Mail, User } from '@signozhq/icons';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import APIError from 'types/api/error';
|
||||
import { ErrorV2Resp } from 'types/api';
|
||||
import { AxiosError } from 'axios';
|
||||
|
||||
import '../MySettings.styles.scss';
|
||||
import './UserInfo.styles.scss';
|
||||
|
||||
function UserInfo(): JSX.Element {
|
||||
const { user, org, updateUser } = useAppContext();
|
||||
const { t } = useTranslation(['routes', 'settings', 'common']);
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
const { showErrorModal } = useErrorModal();
|
||||
const { mutateAsync: updateMyUser } = useUpdateMyUserV2();
|
||||
|
||||
const [currentPassword, setCurrentPassword] = useState<string>('');
|
||||
@@ -47,6 +49,8 @@ function UserInfo(): JSX.Element {
|
||||
|
||||
const hideResetPasswordModal = (): void => {
|
||||
setIsResetPasswordModalOpen(false);
|
||||
setCurrentPassword('');
|
||||
setUpdatePassword('');
|
||||
};
|
||||
|
||||
const onChangePasswordClickHandler = async (): Promise<void> => {
|
||||
@@ -57,27 +61,29 @@ function UserInfo(): JSX.Element {
|
||||
newPassword: updatePassword,
|
||||
oldPassword: currentPassword,
|
||||
});
|
||||
notifications.success({
|
||||
message: t('success', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
toast.success('Password updated successfully');
|
||||
hideResetPasswordModal();
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
notifications.error({
|
||||
message: (error as APIError).error.error.code,
|
||||
description: (error as APIError).error.error.message,
|
||||
});
|
||||
try {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
} catch (apiError) {
|
||||
showErrorModal(apiError as APIError);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const passwordsMatch =
|
||||
currentPassword.length > 0 &&
|
||||
updatePassword.length > 0 &&
|
||||
currentPassword === updatePassword;
|
||||
|
||||
const isResetPasswordDisabled =
|
||||
isLoading ||
|
||||
currentPassword.length === 0 ||
|
||||
updatePassword.length === 0 ||
|
||||
currentPassword === updatePassword;
|
||||
passwordsMatch;
|
||||
|
||||
const onSaveHandler = async (): Promise<void> => {
|
||||
void logEvent('Account Settings: Name Updated', {
|
||||
@@ -94,11 +100,7 @@ function UserInfo(): JSX.Element {
|
||||
setIsLoading(true);
|
||||
await updateMyUser({ data: { displayName: changedName } });
|
||||
|
||||
notifications.success({
|
||||
message: t('success', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
toast.success('Name updated successfully');
|
||||
updateUser({
|
||||
...user,
|
||||
displayName: changedName,
|
||||
@@ -106,10 +108,11 @@ function UserInfo(): JSX.Element {
|
||||
setIsLoading(false);
|
||||
hideUpdateNameModal();
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
message: (error as APIError).getErrorCode(),
|
||||
description: (error as APIError).getErrorMessage(),
|
||||
});
|
||||
try {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
} catch (apiError) {
|
||||
showErrorModal(apiError as APIError);
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
@@ -166,7 +169,7 @@ function UserInfo(): JSX.Element {
|
||||
type="primary"
|
||||
icon={<Check size={16} />}
|
||||
onClick={onSaveHandler}
|
||||
disabled={isLoading}
|
||||
loading={isLoading}
|
||||
data-testid="update-name-btn"
|
||||
>
|
||||
Update name
|
||||
@@ -178,7 +181,11 @@ function UserInfo(): JSX.Element {
|
||||
<Input
|
||||
placeholder="e.g. John Doe"
|
||||
value={changedName}
|
||||
disabled={isLoading}
|
||||
onChange={(e): void => setChangedName(e.target.value)}
|
||||
onPressEnter={(): void => {
|
||||
void onSaveHandler();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -188,6 +195,7 @@ function UserInfo(): JSX.Element {
|
||||
title={<span className="title">Reset password</span>}
|
||||
open={isResetPasswordModalOpen}
|
||||
closable
|
||||
destroyOnClose
|
||||
onCancel={hideResetPasswordModal}
|
||||
footer={[
|
||||
<Button
|
||||
@@ -197,7 +205,8 @@ function UserInfo(): JSX.Element {
|
||||
}`}
|
||||
icon={<Check size={16} />}
|
||||
onClick={onChangePasswordClickHandler}
|
||||
disabled={isLoading || isResetPasswordDisabled}
|
||||
loading={isLoading}
|
||||
disabled={isResetPasswordDisabled}
|
||||
data-testid="reset-password-btn"
|
||||
>
|
||||
Reset password
|
||||
@@ -218,6 +227,11 @@ function UserInfo(): JSX.Element {
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
visibilityToggle
|
||||
onPressEnter={(): void => {
|
||||
if (!isResetPasswordDisabled) {
|
||||
void onChangePasswordClickHandler();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -235,7 +249,18 @@ function UserInfo(): JSX.Element {
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
visibilityToggle={false}
|
||||
status={passwordsMatch ? 'error' : ''}
|
||||
onPressEnter={(): void => {
|
||||
if (!isResetPasswordDisabled) {
|
||||
void onChangePasswordClickHandler();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{passwordsMatch && (
|
||||
<span className="password-error-text">
|
||||
New password must be different from current password
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -8,11 +8,23 @@ import {
|
||||
waitFor,
|
||||
within,
|
||||
} from 'tests/test-utils';
|
||||
import APIError from 'types/api/error';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
|
||||
const toggleThemeFunction = jest.fn();
|
||||
const logEventFunction = jest.fn();
|
||||
const copyToClipboardFn = jest.fn();
|
||||
const editUserFn = jest.fn();
|
||||
const updateMyPasswordFn = jest.fn();
|
||||
const showErrorModalFn = jest.fn();
|
||||
|
||||
jest.mock('@signozhq/ui/sonner', () => ({
|
||||
...jest.requireActual('@signozhq/ui/sonner'),
|
||||
toast: {
|
||||
success: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('react-use', () => ({
|
||||
__esModule: true,
|
||||
@@ -24,12 +36,21 @@ jest.mock('react-use', () => ({
|
||||
|
||||
jest.mock('api/generated/services/users', () => ({
|
||||
...jest.requireActual('api/generated/services/users'),
|
||||
updateMyPassword: (...args: unknown[]): Promise<unknown> =>
|
||||
updateMyPasswordFn(...args),
|
||||
useUpdateMyUserV2: jest.fn(() => ({
|
||||
mutateAsync: (...args: unknown[]): Promise<unknown> => editUserFn(...args),
|
||||
isLoading: false,
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('providers/ErrorModalProvider', () => ({
|
||||
...jest.requireActual('providers/ErrorModalProvider'),
|
||||
useErrorModal: jest.fn(() => ({
|
||||
showErrorModal: showErrorModalFn,
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useDarkMode', () => ({
|
||||
__esModule: true,
|
||||
useIsDarkMode: jest.fn(() => true),
|
||||
@@ -65,12 +86,12 @@ const NEW_PASSWORD_TEST_ID = 'new-password-textbox';
|
||||
const UPDATE_NAME_BUTTON_TEST_ID = 'update-name-btn';
|
||||
const RESET_PASSWORD_BUTTON_TEST_ID = 'reset-password-btn';
|
||||
const UPDATE_NAME_BUTTON_TEXT = 'Update name';
|
||||
const PASSWORD_VALIDATION_MESSAGE_TEST_ID = 'password-validation-message';
|
||||
|
||||
describe('MySettings Flows', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
editUserFn.mockResolvedValue({});
|
||||
updateMyPasswordFn.mockResolvedValue({});
|
||||
render(<MySettingsContainer />);
|
||||
});
|
||||
|
||||
@@ -152,9 +173,7 @@ describe('MySettings Flows', () => {
|
||||
fireEvent.click(modalUpdateNameButton);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(successNotification).toHaveBeenCalledWith({
|
||||
message: 'success',
|
||||
}),
|
||||
expect(toast.success).toHaveBeenCalledWith('Name updated successfully'),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -181,22 +200,131 @@ describe('MySettings Flows', () => {
|
||||
expect(screen.getByTestId(NEW_PASSWORD_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should display validation error if password is less than 8 characters', async () => {
|
||||
it('Should show inline error when new password matches current password', async () => {
|
||||
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
|
||||
fireEvent.click(resetPasswordButtons[0]);
|
||||
|
||||
const currentPasswordTextbox = screen.getByTestId(CURRENT_PASSWORD_TEST_ID);
|
||||
act(() => {
|
||||
fireEvent.change(currentPasswordTextbox, { target: { value: '123' } });
|
||||
fireEvent.change(screen.getByTestId(CURRENT_PASSWORD_TEST_ID), {
|
||||
target: { value: 'samePassword1' },
|
||||
});
|
||||
fireEvent.change(screen.getByTestId(NEW_PASSWORD_TEST_ID), {
|
||||
target: { value: 'samePassword1' },
|
||||
});
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText('New password must be different from current password'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId(RESET_PASSWORD_BUTTON_TEST_ID)).toBeDisabled();
|
||||
});
|
||||
|
||||
it('Should hide inline error when passwords are changed to be different', async () => {
|
||||
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
|
||||
fireEvent.click(resetPasswordButtons[0]);
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(screen.getByTestId(CURRENT_PASSWORD_TEST_ID), {
|
||||
target: { value: 'samePassword1' },
|
||||
});
|
||||
fireEvent.change(screen.getByTestId(NEW_PASSWORD_TEST_ID), {
|
||||
target: { value: 'samePassword1' },
|
||||
});
|
||||
});
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(screen.getByTestId(NEW_PASSWORD_TEST_ID), {
|
||||
target: { value: 'differentPassword1' },
|
||||
});
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.queryByText('New password must be different from current password'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should show error modal when password reset API returns an error', async () => {
|
||||
updateMyPasswordFn.mockRejectedValue(
|
||||
new Error('Current password is incorrect'),
|
||||
);
|
||||
|
||||
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
|
||||
fireEvent.click(resetPasswordButtons[0]);
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(screen.getByTestId(CURRENT_PASSWORD_TEST_ID), {
|
||||
target: { value: 'oldPassword1' },
|
||||
});
|
||||
fireEvent.change(screen.getByTestId(NEW_PASSWORD_TEST_ID), {
|
||||
target: { value: 'newPassword1' },
|
||||
});
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTestId(RESET_PASSWORD_BUTTON_TEST_ID));
|
||||
|
||||
await waitFor(() => {
|
||||
// Use getByTestId for the validation message (if present in your modal/component)
|
||||
if (screen.queryByTestId(PASSWORD_VALIDATION_MESSAGE_TEST_ID)) {
|
||||
expect(
|
||||
screen.getByTestId(PASSWORD_VALIDATION_MESSAGE_TEST_ID),
|
||||
).toBeInTheDocument();
|
||||
}
|
||||
expect(showErrorModalFn).toHaveBeenCalledWith(expect.any(APIError));
|
||||
});
|
||||
});
|
||||
|
||||
it('Should show success toast and close modal on successful password reset', async () => {
|
||||
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
|
||||
fireEvent.click(resetPasswordButtons[0]);
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(screen.getByTestId(CURRENT_PASSWORD_TEST_ID), {
|
||||
target: { value: 'oldPassword1' },
|
||||
});
|
||||
fireEvent.change(screen.getByTestId(NEW_PASSWORD_TEST_ID), {
|
||||
target: { value: 'newPassword1' },
|
||||
});
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTestId(RESET_PASSWORD_BUTTON_TEST_ID));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.success).toHaveBeenCalledWith('Password updated successfully');
|
||||
expect(
|
||||
screen.queryByTestId(CURRENT_PASSWORD_TEST_ID),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('Should clear password fields when modal is cancelled', async () => {
|
||||
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
|
||||
fireEvent.click(resetPasswordButtons[0]);
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(screen.getByTestId(CURRENT_PASSWORD_TEST_ID), {
|
||||
target: { value: 'somePassword' },
|
||||
});
|
||||
fireEvent.change(screen.getByTestId(NEW_PASSWORD_TEST_ID), {
|
||||
target: { value: 'otherPassword' },
|
||||
});
|
||||
});
|
||||
|
||||
expect(screen.getByTestId(CURRENT_PASSWORD_TEST_ID)).toHaveValue(
|
||||
'somePassword',
|
||||
);
|
||||
|
||||
// Close the modal
|
||||
const closeButton = document.querySelector(
|
||||
'.reset-password-modal .ant-modal-close',
|
||||
) as HTMLElement;
|
||||
fireEvent.click(closeButton);
|
||||
|
||||
// Reopen the modal
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByTestId(CURRENT_PASSWORD_TEST_ID),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT)[0]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId(CURRENT_PASSWORD_TEST_ID)).toHaveValue('');
|
||||
expect(screen.getByTestId(NEW_PASSWORD_TEST_ID)).toHaveValue('');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
import { Radio, RadioChangeEvent, Switch, Tag } from 'antd';
|
||||
import { Radio, RadioChangeEvent, Tag } from 'antd';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import updateUserPreference from 'api/v1/user/preferences/name/update';
|
||||
@@ -218,10 +219,10 @@ function MySettings(): JSX.Element {
|
||||
<div className="user-preference-section-content-item-title-action">
|
||||
Keep the primary sidebar always open{' '}
|
||||
<Switch
|
||||
checked={sideNavPinned}
|
||||
value={sideNavPinned}
|
||||
onChange={handleSideNavPinnedChange}
|
||||
loading={isUpdatingUserPreference}
|
||||
data-testid="side-nav-pinned-switch"
|
||||
disabled={isUpdatingUserPreference}
|
||||
testId="side-nav-pinned-switch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { Switch } from 'antd';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import {
|
||||
FillMode,
|
||||
@@ -73,7 +73,7 @@ export default function ChartAppearanceSection({
|
||||
Display individual data points on the chart
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Switch size="small" checked={showPoints} onChange={setShowPoints} />
|
||||
<Switch value={showPoints} onChange={setShowPoints} />
|
||||
</section>
|
||||
)}
|
||||
{allowSpanGaps && (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { InputNumber, Switch } from 'antd';
|
||||
import { InputNumber } from 'antd';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import SettingsSection from '../../components/SettingsSection/SettingsSection';
|
||||
@@ -61,8 +62,7 @@ export default function HistogramBucketsSection({
|
||||
</span>
|
||||
</Typography.Text>
|
||||
<Switch
|
||||
checked={combineHistogram}
|
||||
size="small"
|
||||
value={combineHistogram}
|
||||
onChange={(checked): void => setCombineHistogram(checked)}
|
||||
/>
|
||||
</section>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||
import { Select, Switch } from 'antd';
|
||||
import { Select } from 'antd';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import TimePreference from 'components/TimePreferenceDropDown';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
@@ -104,8 +105,7 @@ export default function VisualizationSettingsSection({
|
||||
<section className="stack-chart control-container">
|
||||
<Typography.Text className="section-heading">Stack series</Typography.Text>
|
||||
<Switch
|
||||
checked={stackedBarChart}
|
||||
size="small"
|
||||
value={stackedBarChart}
|
||||
onChange={(checked): void => setStackedBarChart(checked)}
|
||||
/>
|
||||
</section>
|
||||
@@ -120,8 +120,7 @@ export default function VisualizationSettingsSection({
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={isFillSpans}
|
||||
size="small"
|
||||
value={isFillSpans}
|
||||
onChange={(checked): void => setIsFillSpans(checked)}
|
||||
/>
|
||||
</section>
|
||||
|
||||
@@ -341,10 +341,10 @@ describe('Stacking bar in new panel', () => {
|
||||
const section = container.querySelector('.stack-chart');
|
||||
expect(section).toBeInTheDocument();
|
||||
|
||||
// Verify switch is present and enabled (ant-switch-checked)
|
||||
const switchBtn = section?.querySelector('.ant-switch');
|
||||
// Verify switch is present and enabled
|
||||
const switchBtn = section?.querySelector('[role="switch"]');
|
||||
expect(switchBtn).toBeInTheDocument();
|
||||
expect(switchBtn).toHaveClass('ant-switch-checked');
|
||||
expect(switchBtn).toHaveAttribute('data-state', 'checked');
|
||||
|
||||
// Check that stack series is present and checked
|
||||
checkStackSeriesState(container, true);
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
|
||||
import {
|
||||
defaultLogsSelectedColumns,
|
||||
ensureLogsRequiredColumns,
|
||||
} from '../constants';
|
||||
|
||||
const TIMESTAMP = defaultLogsSelectedColumns.find(
|
||||
(c) => c.name === 'timestamp',
|
||||
);
|
||||
const BODY = defaultLogsSelectedColumns.find((c) => c.name === 'body');
|
||||
|
||||
if (!TIMESTAMP || !BODY) {
|
||||
throw new Error('defaults missing timestamp/body — test fixture invalid');
|
||||
}
|
||||
|
||||
const ATTR_A: TelemetryFieldKey = {
|
||||
name: 'service.name',
|
||||
signal: 'logs',
|
||||
fieldContext: 'resource',
|
||||
fieldDataType: 'string',
|
||||
};
|
||||
const ATTR_B: TelemetryFieldKey = {
|
||||
name: 'severity_text',
|
||||
signal: 'logs',
|
||||
fieldContext: 'log',
|
||||
fieldDataType: 'string',
|
||||
};
|
||||
|
||||
describe('ensureLogsRequiredColumns', () => {
|
||||
it('prepends both timestamp + body to an empty list', () => {
|
||||
expect(ensureLogsRequiredColumns([])).toStrictEqual([TIMESTAMP, BODY]);
|
||||
});
|
||||
|
||||
it('prepends only `body` when `timestamp` is already present', () => {
|
||||
expect(ensureLogsRequiredColumns([TIMESTAMP, ATTR_A])).toStrictEqual([
|
||||
BODY,
|
||||
TIMESTAMP,
|
||||
ATTR_A,
|
||||
]);
|
||||
});
|
||||
|
||||
it('prepends only `timestamp` when `body` is already present', () => {
|
||||
expect(ensureLogsRequiredColumns([BODY, ATTR_A])).toStrictEqual([
|
||||
TIMESTAMP,
|
||||
BODY,
|
||||
ATTR_A,
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns the same array when both are present (no duplicates, original order preserved)', () => {
|
||||
const input = [TIMESTAMP, BODY, ATTR_A, ATTR_B];
|
||||
expect(ensureLogsRequiredColumns(input)).toBe(input);
|
||||
});
|
||||
|
||||
it('preserves a non-default order when both are present', () => {
|
||||
const input = [ATTR_A, BODY, ATTR_B, TIMESTAMP];
|
||||
expect(ensureLogsRequiredColumns(input)).toStrictEqual(input);
|
||||
});
|
||||
|
||||
it('prepends both when neither is present in a list of user attributes', () => {
|
||||
expect(ensureLogsRequiredColumns([ATTR_A, ATTR_B])).toStrictEqual([
|
||||
TIMESTAMP,
|
||||
BODY,
|
||||
ATTR_A,
|
||||
ATTR_B,
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not duplicate if a required column appears twice in the input', () => {
|
||||
// Tolerant of malformed input — invariant only adds *missing* required
|
||||
// columns; it does not deduplicate existing entries (that's a separate
|
||||
// concern, not its job).
|
||||
const input = [BODY, BODY, ATTR_A];
|
||||
const result = ensureLogsRequiredColumns(input);
|
||||
expect(result.filter((c) => c.name === 'timestamp')).toHaveLength(1);
|
||||
expect(result[0]).toStrictEqual(TIMESTAMP);
|
||||
});
|
||||
});
|
||||
@@ -35,6 +35,32 @@ export const defaultLogsSelectedColumns: TelemetryFieldKey[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const LOGS_REQUIRED_COLUMNS = ['timestamp', 'body'] as const;
|
||||
|
||||
/**
|
||||
* Always-on invariant: every logs selectColumns array must contain `body` and
|
||||
* `timestamp`. Applied at both loader and writer boundaries so the picker, the
|
||||
* table, and persisted state can never diverge into a "missing required
|
||||
* column" state.
|
||||
*/
|
||||
export function ensureLogsRequiredColumns(
|
||||
columns: TelemetryFieldKey[],
|
||||
): TelemetryFieldKey[] {
|
||||
const missing = LOGS_REQUIRED_COLUMNS.filter(
|
||||
(name) => !columns.some((c) => c.name === name),
|
||||
);
|
||||
if (missing.length === 0) {
|
||||
return columns;
|
||||
}
|
||||
const defaultsByName = new Map(
|
||||
defaultLogsSelectedColumns.map((c) => [c.name, c]),
|
||||
);
|
||||
const prepended = missing
|
||||
.map((name) => defaultsByName.get(name))
|
||||
.filter((c): c is TelemetryFieldKey => c !== undefined);
|
||||
return [...prepended, ...columns];
|
||||
}
|
||||
|
||||
export const defaultTraceSelectedColumns: TelemetryFieldKey[] = [
|
||||
{
|
||||
name: 'service.name',
|
||||
|
||||
@@ -40,5 +40,6 @@ export type OptionsMenuConfig = {
|
||||
isFetching: boolean;
|
||||
value: TelemetryFieldKey[];
|
||||
onRemove: (key: string) => void;
|
||||
onReorder: (orderedIds: string[]) => void;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -187,30 +187,6 @@ const useOptionsMenu = ({
|
||||
searchedAttributesDataV5?.data.data.keys || {},
|
||||
).flat();
|
||||
if (searchedAttributesDataList.length) {
|
||||
if (dataSource === DataSource.LOGS) {
|
||||
const logsSelectedColumns: TelemetryFieldKey[] =
|
||||
defaultLogsSelectedColumns.map((e) => ({
|
||||
...e,
|
||||
name: e.name,
|
||||
signal: e.signal as SignalType,
|
||||
fieldContext: e.fieldContext as FieldContext,
|
||||
fieldDataType: e.fieldDataType as FieldDataType,
|
||||
}));
|
||||
return [
|
||||
...logsSelectedColumns,
|
||||
...searchedAttributesDataList
|
||||
.filter((attribute) => attribute.name !== 'body')
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
.map((e) => ({
|
||||
...e,
|
||||
name: e.name,
|
||||
signal: e.signal as SignalType,
|
||||
fieldContext: e.fieldContext as FieldContext,
|
||||
fieldDataType: e.fieldDataType as FieldDataType,
|
||||
})),
|
||||
];
|
||||
}
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
return searchedAttributesDataList.map((e) => ({
|
||||
...e,
|
||||
name: e.name,
|
||||
@@ -297,24 +273,9 @@ const useOptionsMenu = ({
|
||||
return [...acc, column];
|
||||
}, [] as TelemetryFieldKey[]);
|
||||
|
||||
const optionsData: OptionsQuery = {
|
||||
...defaultOptionsQuery,
|
||||
selectColumns: newSelectedColumns,
|
||||
format: preferences?.formatting?.format || defaultOptionsQuery.format,
|
||||
maxLines: preferences?.formatting?.maxLines || defaultOptionsQuery.maxLines,
|
||||
fontSize: preferences?.formatting?.fontSize || defaultOptionsQuery.fontSize,
|
||||
};
|
||||
|
||||
updateColumns(newSelectedColumns);
|
||||
handleRedirectWithOptionsData(optionsData);
|
||||
},
|
||||
[
|
||||
searchedAttributeKeys,
|
||||
selectedColumnKeys,
|
||||
preferences,
|
||||
handleRedirectWithOptionsData,
|
||||
updateColumns,
|
||||
],
|
||||
[searchedAttributeKeys, selectedColumnKeys, preferences, updateColumns],
|
||||
);
|
||||
|
||||
const handleRemoveSelectedColumn = useCallback(
|
||||
@@ -327,27 +288,12 @@ const useOptionsMenu = ({
|
||||
notifications.error({
|
||||
message: 'There must be at least one selected column',
|
||||
});
|
||||
} else {
|
||||
const optionsData: OptionsQuery = {
|
||||
...defaultOptionsQuery,
|
||||
selectColumns: newSelectedColumns || [],
|
||||
format: preferences?.formatting?.format || defaultOptionsQuery.format,
|
||||
maxLines:
|
||||
preferences?.formatting?.maxLines || defaultOptionsQuery.maxLines,
|
||||
fontSize:
|
||||
preferences?.formatting?.fontSize || defaultOptionsQuery.fontSize,
|
||||
};
|
||||
updateColumns(newSelectedColumns || []);
|
||||
handleRedirectWithOptionsData(optionsData);
|
||||
return;
|
||||
}
|
||||
|
||||
updateColumns(newSelectedColumns || []);
|
||||
},
|
||||
[
|
||||
dataSource,
|
||||
notifications,
|
||||
preferences,
|
||||
handleRedirectWithOptionsData,
|
||||
updateColumns,
|
||||
],
|
||||
[dataSource, notifications, preferences, updateColumns],
|
||||
);
|
||||
|
||||
const handleFormatChange = useCallback(
|
||||
@@ -414,6 +360,18 @@ const useOptionsMenu = ({
|
||||
setSearchText(value);
|
||||
}, []);
|
||||
|
||||
const reorderSelectColumns = useCallback(
|
||||
(orderedIds: string[]): void => {
|
||||
const current = preferences?.columns ?? [];
|
||||
const byName = new Map(current.map((f) => [f.name, f]));
|
||||
const reordered = orderedIds
|
||||
.map((id) => byName.get(id))
|
||||
.filter((f): f is TelemetryFieldKey => f !== undefined);
|
||||
updateColumns(reordered);
|
||||
},
|
||||
[preferences, updateColumns],
|
||||
);
|
||||
|
||||
const handleFocus = (): void => {
|
||||
setIsFocused(true);
|
||||
};
|
||||
@@ -436,6 +394,7 @@ const useOptionsMenu = ({
|
||||
onSelect: handleSelectColumns,
|
||||
onRemove: handleRemoveSelectedColumn,
|
||||
onSearch: handleSearchAttribute,
|
||||
onReorder: reorderSelectColumns,
|
||||
},
|
||||
format: {
|
||||
value: preferences?.formatting?.format || defaultOptionsQuery.format,
|
||||
@@ -457,6 +416,7 @@ const useOptionsMenu = ({
|
||||
handleSelectColumns,
|
||||
handleRemoveSelectedColumn,
|
||||
handleSearchAttribute,
|
||||
reorderSelectColumns,
|
||||
handleFormatChange,
|
||||
handleMaxLinesChange,
|
||||
handleFontSizeChange,
|
||||
|
||||
@@ -58,6 +58,26 @@ describe('AuthDomain', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('reflects ssoEnabled state from nested config in each row toggle', async () => {
|
||||
server.use(
|
||||
rest.get(AUTH_DOMAINS_LIST_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockDomainsListResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
render(<AuthDomain />);
|
||||
|
||||
// mockDomainsListResponse rows:
|
||||
// [0] signoz.io → config.ssoEnabled: true
|
||||
// [1] example.com → config.ssoEnabled: false
|
||||
// [2] corp.io → config.ssoEnabled: true
|
||||
const switches = await screen.findAllByRole('switch');
|
||||
expect(switches).toHaveLength(3);
|
||||
expect(switches[0]).toBeChecked();
|
||||
expect(switches[1]).not.toBeChecked();
|
||||
expect(switches[2]).toBeChecked();
|
||||
});
|
||||
|
||||
it('renders empty state when no domains exist', async () => {
|
||||
server.use(
|
||||
rest.get(AUTH_DOMAINS_LIST_ENDPOINT, (_, res, ctx) =>
|
||||
|
||||
@@ -121,14 +121,14 @@ function AuthDomain(): JSX.Element {
|
||||
},
|
||||
{
|
||||
title: 'Enforce SSO',
|
||||
dataIndex: 'ssoEnabled',
|
||||
dataIndex: ['config', 'ssoEnabled'],
|
||||
key: 'ssoEnabled',
|
||||
width: 80,
|
||||
render: (
|
||||
value: boolean,
|
||||
record: AuthtypesGettableAuthDomainDTO,
|
||||
): JSX.Element => (
|
||||
<SSOEnforcementToggle isDefaultChecked={value} record={record} />
|
||||
<SSOEnforcementToggle isDefaultChecked={!!value} record={record} />
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -87,12 +87,7 @@
|
||||
|
||||
.service-progress-indicator {
|
||||
width: fit-content;
|
||||
margin-inline-end: 0px !important;
|
||||
margin-bottom: 0px !important;
|
||||
|
||||
.ant-progress-inner {
|
||||
width: 30px;
|
||||
}
|
||||
--progress-width: 30px;
|
||||
}
|
||||
|
||||
.percent-value {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Progress, Skeleton, Tooltip } from 'antd';
|
||||
import { Skeleton, Tooltip } from 'antd';
|
||||
import { Progress } from '@signozhq/ui/progress';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { AxiosError } from 'axios';
|
||||
import Spinner from 'components/Spinner';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Info } from '@signozhq/icons';
|
||||
import { Flex, Form, Input, Space, Switch, Tooltip } from 'antd';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import { Flex, Form, Input, Space, Tooltip } from 'antd';
|
||||
import { ProcessorData } from 'types/api/pipeline/def';
|
||||
|
||||
import { PREDEFINED_MAPPING } from '../config';
|
||||
@@ -58,11 +59,7 @@ function JsonFlattening({
|
||||
initialValue={isAdd ? true : selectedProcessorData?.enable_paths}
|
||||
>
|
||||
<Space>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={enablePaths}
|
||||
onChange={handleEnablePathsChange}
|
||||
/>
|
||||
<Switch value={enablePaths} onChange={handleEnablePathsChange} />
|
||||
Enable Paths
|
||||
</Space>
|
||||
</Form.Item>
|
||||
@@ -79,11 +76,7 @@ function JsonFlattening({
|
||||
|
||||
<Form.Item className="json-flattening-form__item">
|
||||
<Space>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={enableMapping}
|
||||
onChange={handleEnableMappingChange}
|
||||
/>
|
||||
<Switch value={enableMapping} onChange={handleEnableMappingChange} />
|
||||
<Flex gap="8px" align="center">
|
||||
Enable Mapping
|
||||
<Tooltip title="The order of filled keys will determine the priority of keys i.e. earlier keys have higher precedence">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Input, Select, Space, Switch } from 'antd';
|
||||
import { Form, Input, Select, Space } from 'antd';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import { ModalFooterTitle } from 'container/PipelinePage/styles';
|
||||
import { ProcessorData } from 'types/api/pipeline/def';
|
||||
|
||||
@@ -92,8 +93,7 @@ function ProcessorFieldInput({
|
||||
>
|
||||
<Space>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={form.getFieldValue('enable_flattening')}
|
||||
value={form.getFieldValue('enable_flattening')}
|
||||
onChange={(checked: boolean): void => {
|
||||
form.setFieldValue('enable_flattening', checked);
|
||||
}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { GripVertical } from '@signozhq/icons';
|
||||
import { Switch } from 'antd';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
|
||||
import { holdIconStyle } from '../config';
|
||||
import { LastActionColumn } from '../styles';
|
||||
@@ -7,7 +7,7 @@ import { LastActionColumn } from '../styles';
|
||||
function DragAction({ isEnabled, onChange }: DragActionProps): JSX.Element {
|
||||
return (
|
||||
<LastActionColumn>
|
||||
<Switch defaultChecked={isEnabled} onChange={onChange} />
|
||||
<Switch defaultValue={isEnabled} onChange={onChange} />
|
||||
<GripVertical size="lg" style={holdIconStyle} />
|
||||
</LastActionColumn>
|
||||
);
|
||||
|
||||
@@ -191,7 +191,7 @@ describe('PipelinePage container test', () => {
|
||||
await fireEvent.click(expandIcon[0]);
|
||||
|
||||
const switchToggle = document.querySelector(
|
||||
'.ant-table-expanded-row .ant-switch',
|
||||
'.ant-table-expanded-row [role="switch"]',
|
||||
);
|
||||
|
||||
expect(switchToggle).toBeChecked();
|
||||
@@ -244,7 +244,7 @@ describe('PipelinePage container test', () => {
|
||||
const addNewPipelineBtn = getByText('add_new_pipeline');
|
||||
expect(addNewPipelineBtn).toBeInTheDocument();
|
||||
|
||||
const switchToggle = document.querySelectorAll('.ant-switch');
|
||||
const switchToggle = document.querySelectorAll('[role="switch"]');
|
||||
|
||||
expect(switchToggle[0]).not.toBeChecked();
|
||||
await fireEvent.click(switchToggle[0] as HTMLElement);
|
||||
|
||||
@@ -21,26 +21,25 @@ exports[`PipelinePage container test should render DragAction section 1`] = `
|
||||
<div
|
||||
class="c0"
|
||||
>
|
||||
<button
|
||||
aria-checked="true"
|
||||
class="ant-switch css-dev-only-do-not-override-2i2tap ant-switch-checked"
|
||||
role="switch"
|
||||
type="button"
|
||||
<div
|
||||
class="_switch-wrapper_jbsv7_1"
|
||||
>
|
||||
<div
|
||||
class="ant-switch-handle"
|
||||
/>
|
||||
<span
|
||||
class="ant-switch-inner"
|
||||
<button
|
||||
aria-checked="true"
|
||||
class="_switch_jbsv7_1"
|
||||
data-color="robin"
|
||||
data-state="checked"
|
||||
id=":r0:"
|
||||
role="switch"
|
||||
type="button"
|
||||
value="on"
|
||||
>
|
||||
<span
|
||||
class="ant-switch-inner-checked"
|
||||
class="_switch__thumb_jbsv7_59"
|
||||
data-state="checked"
|
||||
/>
|
||||
<span
|
||||
class="ant-switch-inner-unchecked"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</button>
|
||||
</div>
|
||||
<svg
|
||||
style="font-size: 1rem; cursor: move;"
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
|
||||
import DateTimeSelection from '../index';
|
||||
import {
|
||||
__resetSearchParamsGetter,
|
||||
__setSearchParamsGetterForTest,
|
||||
} from '../utils/getUnstableCurrentSearchParams';
|
||||
import { queryClient, TestWrapper } from './testUtils';
|
||||
|
||||
const mockSafeNavigate = jest.fn();
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: (): { safeNavigate: jest.Mock } => ({
|
||||
safeNavigate: mockSafeNavigate,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('container/NewExplorerCTA', () => ({
|
||||
__esModule: true,
|
||||
default: (): null => null,
|
||||
}));
|
||||
|
||||
jest.mock('components/CustomTimePicker/CustomTimePicker', () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
onSelect,
|
||||
}: {
|
||||
onSelect: (value: string) => void;
|
||||
}): JSX.Element => (
|
||||
<div data-testid="custom-time-picker">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="select-15m"
|
||||
onClick={(): void => onSelect('15m')}
|
||||
>
|
||||
15m
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="select-1h"
|
||||
onClick={(): void => onSelect('1h')}
|
||||
>
|
||||
1h
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="select-6h"
|
||||
onClick={(): void => onSelect('6h')}
|
||||
>
|
||||
6h
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="select-custom"
|
||||
onClick={(): void => onSelect('custom')}
|
||||
>
|
||||
Custom
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe('DateTimeSelectionV2 - Edge Cases', () => {
|
||||
let currentSearchParams: URLSearchParams;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockSafeNavigate.mockClear();
|
||||
queryClient.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
__resetSearchParamsGetter();
|
||||
});
|
||||
|
||||
describe('Fresh Params at Navigation Time (Core Fix)', () => {
|
||||
it('should read params at navigation time, not render time', async () => {
|
||||
currentSearchParams = new URLSearchParams('relativeTime=30m');
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
render(
|
||||
<TestWrapper
|
||||
initialSearchParams="relativeTime=30m"
|
||||
onUrlUpdate={(event): void => {
|
||||
currentSearchParams = event.searchParams;
|
||||
}}
|
||||
>
|
||||
<DateTimeSelection showAutoRefresh />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
currentSearchParams = new URLSearchParams(
|
||||
'relativeTime=30m&externalParam=addedLater',
|
||||
);
|
||||
|
||||
mockSafeNavigate.mockClear();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId('select-1h'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const navigatedUrl = mockSafeNavigate.mock.calls[
|
||||
mockSafeNavigate.mock.calls.length - 1
|
||||
][0] as string;
|
||||
|
||||
expect(navigatedUrl).toContain('relativeTime=1h');
|
||||
expect(navigatedUrl).toContain('externalParam=addedLater');
|
||||
});
|
||||
|
||||
it('should preserve multiple externally added params', async () => {
|
||||
currentSearchParams = new URLSearchParams('relativeTime=30m');
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
render(
|
||||
<TestWrapper initialSearchParams="relativeTime=30m">
|
||||
<DateTimeSelection showAutoRefresh />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
currentSearchParams = new URLSearchParams(
|
||||
'relativeTime=30m&yAxisUnit=bytes&groupBy=host&view=table',
|
||||
);
|
||||
|
||||
mockSafeNavigate.mockClear();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId('select-6h'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const navigatedUrl = mockSafeNavigate.mock.calls[
|
||||
mockSafeNavigate.mock.calls.length - 1
|
||||
][0] as string;
|
||||
|
||||
expect(navigatedUrl).toContain('relativeTime=6h');
|
||||
expect(navigatedUrl).toContain('yAxisUnit=bytes');
|
||||
expect(navigatedUrl).toContain('groupBy=host');
|
||||
expect(navigatedUrl).toContain('view=table');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Empty and Special Values', () => {
|
||||
it('should handle empty URL params gracefully', async () => {
|
||||
currentSearchParams = new URLSearchParams('');
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
render(
|
||||
<TestWrapper initialSearchParams="">
|
||||
<DateTimeSelection showAutoRefresh />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
mockSafeNavigate.mockClear();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId('select-15m'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const navigatedUrl = mockSafeNavigate.mock.calls[
|
||||
mockSafeNavigate.mock.calls.length - 1
|
||||
][0] as string;
|
||||
|
||||
expect(navigatedUrl).toContain('relativeTime=15m');
|
||||
});
|
||||
|
||||
it('should handle special characters in preserved params', async () => {
|
||||
currentSearchParams = new URLSearchParams(
|
||||
'relativeTime=30m&filter=name%3D%22test%22',
|
||||
);
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
render(
|
||||
<TestWrapper initialSearchParams="relativeTime=30m&filter=name%3D%22test%22">
|
||||
<DateTimeSelection showAutoRefresh />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
mockSafeNavigate.mockClear();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId('select-1h'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const navigatedUrl = mockSafeNavigate.mock.calls[
|
||||
mockSafeNavigate.mock.calls.length - 1
|
||||
][0] as string;
|
||||
|
||||
expect(navigatedUrl).toContain('relativeTime=1h');
|
||||
expect(navigatedUrl).toContain('filter=');
|
||||
});
|
||||
|
||||
it('should not navigate when selecting custom (opens picker instead)', async () => {
|
||||
currentSearchParams = new URLSearchParams('relativeTime=30m');
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
render(
|
||||
<TestWrapper initialSearchParams="relativeTime=30m">
|
||||
<DateTimeSelection showAutoRefresh />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
mockSafeNavigate.mockClear();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId('select-custom'));
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const customNavigationCalls = mockSafeNavigate.mock.calls.filter((call) => {
|
||||
const url = call[0] as string;
|
||||
return url.includes('startTime=') || url.includes('endTime=');
|
||||
});
|
||||
|
||||
expect(customNavigationCalls).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,180 @@
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
|
||||
import DateTimeSelection from '../index';
|
||||
import {
|
||||
__resetSearchParamsGetter,
|
||||
__setSearchParamsGetterForTest,
|
||||
} from '../utils/getUnstableCurrentSearchParams';
|
||||
import { queryClient, TestWrapper } from './testUtils';
|
||||
|
||||
const mockSafeNavigate = jest.fn();
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: (): { safeNavigate: jest.Mock } => ({
|
||||
safeNavigate: mockSafeNavigate,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('container/NewExplorerCTA', () => ({
|
||||
__esModule: true,
|
||||
default: (): null => null,
|
||||
}));
|
||||
|
||||
let mockOnCustomDateHandler: ((range: [unknown, unknown]) => void) | null =
|
||||
null;
|
||||
let mockOnValidCustomDateChange: ((data: { timeStr: string }) => void) | null =
|
||||
null;
|
||||
|
||||
jest.mock('components/CustomTimePicker/CustomTimePicker', () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
onSelect,
|
||||
onCustomDateHandler,
|
||||
onValidCustomDateChange,
|
||||
}: {
|
||||
onSelect: (value: string) => void;
|
||||
onCustomDateHandler?: (range: [unknown, unknown]) => void;
|
||||
onValidCustomDateChange?: (data: { timeStr: string }) => void;
|
||||
}): JSX.Element => {
|
||||
mockOnCustomDateHandler = onCustomDateHandler || null;
|
||||
mockOnValidCustomDateChange = onValidCustomDateChange || null;
|
||||
|
||||
return (
|
||||
<div data-testid="custom-time-picker">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="select-1h"
|
||||
onClick={(): void => onSelect('1h')}
|
||||
>
|
||||
1h
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
describe('DateTimeSelectionV2 - Modal Mode', () => {
|
||||
let currentSearchParams: URLSearchParams;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockSafeNavigate.mockClear();
|
||||
queryClient.clear();
|
||||
mockOnCustomDateHandler = null;
|
||||
mockOnValidCustomDateChange = null;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
__resetSearchParamsGetter();
|
||||
});
|
||||
|
||||
it('should call onTimeChange instead of navigating for relative time', async () => {
|
||||
currentSearchParams = new URLSearchParams('relativeTime=30m');
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
const mockOnTimeChange = jest.fn();
|
||||
|
||||
render(
|
||||
<TestWrapper initialSearchParams="relativeTime=30m">
|
||||
<DateTimeSelection
|
||||
showAutoRefresh
|
||||
isModalTimeSelection
|
||||
onTimeChange={mockOnTimeChange}
|
||||
/>
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
mockSafeNavigate.mockClear();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId('select-1h'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnTimeChange).toHaveBeenCalledWith('1h');
|
||||
});
|
||||
|
||||
expect(mockSafeNavigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call onTimeChange with custom and timestamps for custom date', async () => {
|
||||
currentSearchParams = new URLSearchParams('relativeTime=30m');
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
const mockOnTimeChange = jest.fn();
|
||||
|
||||
render(
|
||||
<TestWrapper initialSearchParams="relativeTime=30m">
|
||||
<DateTimeSelection
|
||||
showAutoRefresh
|
||||
isModalTimeSelection
|
||||
onTimeChange={mockOnTimeChange}
|
||||
/>
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const startMoment = { toDate: (): Date => new Date(1700000000000) };
|
||||
const endMoment = { toDate: (): Date => new Date(1700003600000) };
|
||||
|
||||
mockSafeNavigate.mockClear();
|
||||
|
||||
act(() => {
|
||||
mockOnCustomDateHandler?.([startMoment, endMoment]);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnTimeChange).toHaveBeenCalledWith(
|
||||
'custom',
|
||||
[1700000000000, 1700003600000],
|
||||
);
|
||||
});
|
||||
|
||||
expect(mockSafeNavigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call onTimeChange for valid custom date string in modal', async () => {
|
||||
currentSearchParams = new URLSearchParams('relativeTime=30m');
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
const mockOnTimeChange = jest.fn();
|
||||
|
||||
render(
|
||||
<TestWrapper initialSearchParams="relativeTime=30m">
|
||||
<DateTimeSelection
|
||||
showAutoRefresh
|
||||
isModalTimeSelection
|
||||
onTimeChange={mockOnTimeChange}
|
||||
/>
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
mockSafeNavigate.mockClear();
|
||||
|
||||
act(() => {
|
||||
mockOnValidCustomDateChange?.({ timeStr: '4h' });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnTimeChange).toHaveBeenCalledWith('4h');
|
||||
});
|
||||
|
||||
expect(mockSafeNavigate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,207 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter, Route } from 'react-router-dom';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { parseAsString, useQueryState } from 'nuqs';
|
||||
import { AppContext } from 'providers/App/App';
|
||||
import TimezoneProvider from 'providers/Timezone';
|
||||
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import store from 'store';
|
||||
import { getAppContextMock } from 'tests/test-utils';
|
||||
import { CompatRouter } from 'react-router-dom-v5-compat';
|
||||
|
||||
import DateTimeSelection from '../index';
|
||||
import {
|
||||
__resetSearchParamsGetter,
|
||||
__setSearchParamsGetterForTest,
|
||||
} from '../utils/getUnstableCurrentSearchParams';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { refetchOnWindowFocus: false, retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
const mockStore = configureStore([thunk]);
|
||||
|
||||
const mockSafeNavigate = jest.fn();
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: (): { safeNavigate: jest.Mock } => ({
|
||||
safeNavigate: mockSafeNavigate,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('components/CustomTimePicker/CustomTimePicker', () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
onSelect,
|
||||
}: {
|
||||
onSelect: (value: string) => void;
|
||||
}): JSX.Element => (
|
||||
<div data-testid="custom-time-picker">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="select-1h"
|
||||
onClick={(): void => onSelect('1h')}
|
||||
>
|
||||
1h
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('container/NewExplorerCTA', () => ({
|
||||
__esModule: true,
|
||||
default: (): null => null,
|
||||
}));
|
||||
|
||||
function NuqsParamSetter({ paramValue }: { paramValue: string }): JSX.Element {
|
||||
const [, setYAxisUnit] = useQueryState(
|
||||
'yAxisUnit',
|
||||
parseAsString.withDefault(''),
|
||||
);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="set-nuqs-param"
|
||||
onClick={(): void => {
|
||||
setYAxisUnit(paramValue);
|
||||
}}
|
||||
>
|
||||
Set yAxisUnit
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
interface WrapperProps {
|
||||
children: React.ReactNode;
|
||||
initialSearchParams?: string;
|
||||
initialPath?: string;
|
||||
onUrlUpdate?: (event: { searchParams: URLSearchParams }) => void;
|
||||
}
|
||||
|
||||
function TestWrapper({
|
||||
children,
|
||||
initialSearchParams = '',
|
||||
initialPath = '/services',
|
||||
onUrlUpdate,
|
||||
}: WrapperProps): JSX.Element {
|
||||
const initialEntry = initialSearchParams
|
||||
? `${initialPath}?${initialSearchParams}`
|
||||
: initialPath;
|
||||
|
||||
const mockedStore = mockStore({
|
||||
...store.getState(),
|
||||
app: {
|
||||
...store.getState().app,
|
||||
role: 'ADMIN',
|
||||
user: {
|
||||
userId: 'test-user-id',
|
||||
email: 'test@signoz.io',
|
||||
name: 'TestUser',
|
||||
profilePictureURL: '',
|
||||
accessJwt: '',
|
||||
refreshJwt: '',
|
||||
},
|
||||
isLoggedIn: true,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<MemoryRouter initialEntries={[initialEntry]}>
|
||||
<CompatRouter>
|
||||
<NuqsTestingAdapter
|
||||
searchParams={initialSearchParams}
|
||||
onUrlUpdate={onUrlUpdate}
|
||||
>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Provider store={mockedStore}>
|
||||
<AppContext.Provider value={getAppContextMock('ADMIN')}>
|
||||
<TimezoneProvider>
|
||||
<QueryBuilderProvider>
|
||||
<Route path="*">{children}</Route>
|
||||
</QueryBuilderProvider>
|
||||
</TimezoneProvider>
|
||||
</AppContext.Provider>
|
||||
</Provider>
|
||||
</QueryClientProvider>
|
||||
</NuqsTestingAdapter>
|
||||
</CompatRouter>
|
||||
</MemoryRouter>
|
||||
);
|
||||
}
|
||||
|
||||
describe('REGRESSION: DateTimeSelectionV2 preserves nuqs query params on time change', () => {
|
||||
let currentSearchParams: URLSearchParams;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockSafeNavigate.mockClear();
|
||||
queryClient.clear();
|
||||
|
||||
// Initialize with test's initial search params
|
||||
currentSearchParams = new URLSearchParams('relativeTime=30m');
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
__resetSearchParamsGetter();
|
||||
});
|
||||
|
||||
it('should preserve yAxisUnit param set via nuqs when changing time selection', async () => {
|
||||
render(
|
||||
<TestWrapper
|
||||
initialSearchParams="relativeTime=30m"
|
||||
onUrlUpdate={(event): void => {
|
||||
// Sync nuqs URL updates to our mock getter
|
||||
// This simulates how window.location.search would be updated in real browser
|
||||
currentSearchParams = event.searchParams;
|
||||
}}
|
||||
>
|
||||
<NuqsParamSetter paramValue="bytes" />
|
||||
<DateTimeSelection showAutoRefresh />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId('set-nuqs-param'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(currentSearchParams.get('yAxisUnit')).toBe('bytes');
|
||||
});
|
||||
|
||||
mockSafeNavigate.mockClear();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId('select-1h'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const navigatedUrl = mockSafeNavigate.mock.calls[
|
||||
mockSafeNavigate.mock.calls.length - 1
|
||||
][0] as string;
|
||||
|
||||
expect(navigatedUrl).toContain('relativeTime=1h');
|
||||
expect(navigatedUrl).toContain('yAxisUnit=bytes');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import ROUTES from 'constants/routes';
|
||||
|
||||
import DateTimeSelection from '../index';
|
||||
import {
|
||||
__resetSearchParamsGetter,
|
||||
__setSearchParamsGetterForTest,
|
||||
} from '../utils/getUnstableCurrentSearchParams';
|
||||
import { queryClient, TestWrapper } from './testUtils';
|
||||
|
||||
const mockSafeNavigate = jest.fn();
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: (): { safeNavigate: jest.Mock } => ({
|
||||
safeNavigate: mockSafeNavigate,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('container/NewExplorerCTA', () => ({
|
||||
__esModule: true,
|
||||
default: (): null => null,
|
||||
}));
|
||||
|
||||
jest.mock('components/CustomTimePicker/CustomTimePicker', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => <div data-testid="custom-time-picker" />,
|
||||
}));
|
||||
|
||||
describe('DateTimeSelectionV2 - Route-Specific Behavior', () => {
|
||||
let currentSearchParams: URLSearchParams;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockSafeNavigate.mockClear();
|
||||
queryClient.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
__resetSearchParamsGetter();
|
||||
});
|
||||
|
||||
describe('Alert Pages', () => {
|
||||
it('should set default time for alert overview when no time params', async () => {
|
||||
currentSearchParams = new URLSearchParams('otherParam=value');
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
render(
|
||||
<TestWrapper
|
||||
initialSearchParams="otherParam=value"
|
||||
initialPath={ROUTES.ALERT_OVERVIEW}
|
||||
>
|
||||
<DateTimeSelection showAutoRefresh defaultRelativeTime="6h" />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const navigatedUrl = mockSafeNavigate.mock.calls[0][0] as string;
|
||||
expect(navigatedUrl).toContain('relativeTime=6h');
|
||||
expect(navigatedUrl).toContain('otherParam=value');
|
||||
});
|
||||
|
||||
it('should set default time for alert history when no time params', async () => {
|
||||
currentSearchParams = new URLSearchParams('filter=active');
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
render(
|
||||
<TestWrapper
|
||||
initialSearchParams="filter=active"
|
||||
initialPath={ROUTES.ALERT_HISTORY}
|
||||
>
|
||||
<DateTimeSelection showAutoRefresh defaultRelativeTime="6h" />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const navigatedUrl = mockSafeNavigate.mock.calls[0][0] as string;
|
||||
expect(navigatedUrl).toContain('relativeTime=6h');
|
||||
});
|
||||
|
||||
it('should NOT override existing time params on alert pages', async () => {
|
||||
currentSearchParams = new URLSearchParams('relativeTime=1h&filter=active');
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
render(
|
||||
<TestWrapper
|
||||
initialSearchParams="relativeTime=1h&filter=active"
|
||||
initialPath={ROUTES.ALERT_OVERVIEW}
|
||||
>
|
||||
<DateTimeSelection showAutoRefresh defaultRelativeTime="6h" />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const calls = mockSafeNavigate.mock.calls;
|
||||
if (calls.length > 0) {
|
||||
const lastUrl = calls[calls.length - 1][0] as string;
|
||||
expect(lastUrl).toContain('relativeTime=1h');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('disableUrlSync Behavior', () => {
|
||||
it('should not sync URL on mount when disableUrlSync is true', async () => {
|
||||
currentSearchParams = new URLSearchParams('');
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
render(
|
||||
<TestWrapper initialSearchParams="" initialPath="/services">
|
||||
<DateTimeSelection showAutoRefresh disableUrlSync />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const syncCalls = mockSafeNavigate.mock.calls.filter((call) => {
|
||||
const url = call[0] as string;
|
||||
return url.includes('relativeTime=') && !url.includes('services?');
|
||||
});
|
||||
|
||||
expect(syncCalls).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,353 @@
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
|
||||
import DateTimeSelection from '../index';
|
||||
import {
|
||||
__resetSearchParamsGetter,
|
||||
__setSearchParamsGetterForTest,
|
||||
} from '../utils/getUnstableCurrentSearchParams';
|
||||
import { queryClient, TestWrapper, createMockMoment } from './testUtils';
|
||||
|
||||
const mockSafeNavigate = jest.fn();
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: (): { safeNavigate: jest.Mock } => ({
|
||||
safeNavigate: mockSafeNavigate,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('container/NewExplorerCTA', () => ({
|
||||
__esModule: true,
|
||||
default: (): null => null,
|
||||
}));
|
||||
|
||||
let mockOnCustomDateHandler: ((range: [unknown, unknown]) => void) | null =
|
||||
null;
|
||||
let mockOnValidCustomDateChange: ((data: { timeStr: string }) => void) | null =
|
||||
null;
|
||||
|
||||
jest.mock('components/CustomTimePicker/CustomTimePicker', () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
onSelect,
|
||||
onCustomDateHandler,
|
||||
onValidCustomDateChange,
|
||||
}: {
|
||||
onSelect: (value: string) => void;
|
||||
onCustomDateHandler?: (range: [unknown, unknown]) => void;
|
||||
onValidCustomDateChange?: (data: { timeStr: string }) => void;
|
||||
}): JSX.Element => {
|
||||
mockOnCustomDateHandler = onCustomDateHandler || null;
|
||||
mockOnValidCustomDateChange = onValidCustomDateChange || null;
|
||||
|
||||
return (
|
||||
<div data-testid="custom-time-picker">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="select-15m"
|
||||
onClick={(): void => onSelect('15m')}
|
||||
>
|
||||
15m
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="select-1h"
|
||||
onClick={(): void => onSelect('1h')}
|
||||
>
|
||||
1h
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="select-6h"
|
||||
onClick={(): void => onSelect('6h')}
|
||||
>
|
||||
6h
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
describe('DateTimeSelectionV2 - Time Selection', () => {
|
||||
let currentSearchParams: URLSearchParams;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockSafeNavigate.mockClear();
|
||||
queryClient.clear();
|
||||
mockOnCustomDateHandler = null;
|
||||
mockOnValidCustomDateChange = null;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
__resetSearchParamsGetter();
|
||||
});
|
||||
|
||||
describe('Relative Time', () => {
|
||||
it('should update relativeTime and remove startTime/endTime', async () => {
|
||||
currentSearchParams = new URLSearchParams(
|
||||
'startTime=1000&endTime=2000&otherParam=keep',
|
||||
);
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
render(
|
||||
<TestWrapper initialSearchParams="startTime=1000&endTime=2000&otherParam=keep">
|
||||
<DateTimeSelection showAutoRefresh />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
mockSafeNavigate.mockClear();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId('select-15m'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const navigatedUrl = mockSafeNavigate.mock.calls[
|
||||
mockSafeNavigate.mock.calls.length - 1
|
||||
][0] as string;
|
||||
|
||||
expect(navigatedUrl).toContain('relativeTime=15m');
|
||||
expect(navigatedUrl).not.toContain('startTime=');
|
||||
expect(navigatedUrl).not.toContain('endTime=');
|
||||
expect(navigatedUrl).toContain('otherParam=keep');
|
||||
});
|
||||
|
||||
it('should remove activeLogId param on time change', async () => {
|
||||
currentSearchParams = new URLSearchParams(
|
||||
'relativeTime=30m&activeLogId=log123',
|
||||
);
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
render(
|
||||
<TestWrapper initialSearchParams="relativeTime=30m&activeLogId=log123">
|
||||
<DateTimeSelection showAutoRefresh />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
mockSafeNavigate.mockClear();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId('select-1h'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const navigatedUrl = mockSafeNavigate.mock.calls[
|
||||
mockSafeNavigate.mock.calls.length - 1
|
||||
][0] as string;
|
||||
|
||||
expect(navigatedUrl).toContain('relativeTime=1h');
|
||||
expect(navigatedUrl).not.toContain('activeLogId');
|
||||
});
|
||||
|
||||
it('should update compositeQuery with new ID when present', async () => {
|
||||
const compositeQuery = encodeURIComponent(
|
||||
JSON.stringify({ id: 'old-id', builder: { queryData: [] } }),
|
||||
);
|
||||
currentSearchParams = new URLSearchParams(
|
||||
`relativeTime=30m&compositeQuery=${compositeQuery}`,
|
||||
);
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
render(
|
||||
<TestWrapper
|
||||
initialSearchParams={`relativeTime=30m&compositeQuery=${compositeQuery}`}
|
||||
>
|
||||
<DateTimeSelection showAutoRefresh />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
mockSafeNavigate.mockClear();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId('select-6h'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const navigatedUrl = mockSafeNavigate.mock.calls[
|
||||
mockSafeNavigate.mock.calls.length - 1
|
||||
][0] as string;
|
||||
|
||||
expect(navigatedUrl).toContain('compositeQuery=');
|
||||
expect(navigatedUrl).not.toContain('old-id');
|
||||
});
|
||||
|
||||
it('should preserve all non-time URL params', async () => {
|
||||
currentSearchParams = new URLSearchParams(
|
||||
'relativeTime=30m¶m1=a¶m2=b¶m3=c',
|
||||
);
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
render(
|
||||
<TestWrapper initialSearchParams="relativeTime=30m¶m1=a¶m2=b¶m3=c">
|
||||
<DateTimeSelection showAutoRefresh />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
mockSafeNavigate.mockClear();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId('select-1h'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const navigatedUrl = mockSafeNavigate.mock.calls[
|
||||
mockSafeNavigate.mock.calls.length - 1
|
||||
][0] as string;
|
||||
|
||||
expect(navigatedUrl).toContain('relativeTime=1h');
|
||||
expect(navigatedUrl).toContain('param1=a');
|
||||
expect(navigatedUrl).toContain('param2=b');
|
||||
expect(navigatedUrl).toContain('param3=c');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom Date Range', () => {
|
||||
it('should set startTime/endTime and remove relativeTime', async () => {
|
||||
currentSearchParams = new URLSearchParams('relativeTime=30m&keepThis=yes');
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
render(
|
||||
<TestWrapper initialSearchParams="relativeTime=30m&keepThis=yes">
|
||||
<DateTimeSelection showAutoRefresh />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const startMoment = createMockMoment(1700000000000);
|
||||
const endMoment = createMockMoment(1700003600000);
|
||||
|
||||
mockSafeNavigate.mockClear();
|
||||
|
||||
act(() => {
|
||||
mockOnCustomDateHandler?.([startMoment, endMoment]);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const navigatedUrl = mockSafeNavigate.mock.calls[
|
||||
mockSafeNavigate.mock.calls.length - 1
|
||||
][0] as string;
|
||||
|
||||
expect(navigatedUrl).toContain('startTime=1700000000000');
|
||||
expect(navigatedUrl).toContain('endTime=1700003600000');
|
||||
expect(navigatedUrl).not.toContain('relativeTime=');
|
||||
expect(navigatedUrl).toContain('keepThis=yes');
|
||||
});
|
||||
|
||||
it('should update compositeQuery when present for custom date', async () => {
|
||||
const compositeQuery = encodeURIComponent(
|
||||
JSON.stringify({ id: 'old-id', builder: { queryData: [] } }),
|
||||
);
|
||||
currentSearchParams = new URLSearchParams(
|
||||
`relativeTime=30m&compositeQuery=${compositeQuery}`,
|
||||
);
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
render(
|
||||
<TestWrapper
|
||||
initialSearchParams={`relativeTime=30m&compositeQuery=${compositeQuery}`}
|
||||
>
|
||||
<DateTimeSelection showAutoRefresh />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const startMoment = createMockMoment(1700000000000);
|
||||
const endMoment = createMockMoment(1700003600000);
|
||||
|
||||
mockSafeNavigate.mockClear();
|
||||
|
||||
act(() => {
|
||||
mockOnCustomDateHandler?.([startMoment, endMoment]);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const navigatedUrl = mockSafeNavigate.mock.calls[
|
||||
mockSafeNavigate.mock.calls.length - 1
|
||||
][0] as string;
|
||||
|
||||
expect(navigatedUrl).toContain('compositeQuery=');
|
||||
expect(navigatedUrl).not.toContain('old-id');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Valid Custom Date String', () => {
|
||||
it('should handle shorthand date format and preserve params', async () => {
|
||||
currentSearchParams = new URLSearchParams('relativeTime=30m&filter=active');
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
render(
|
||||
<TestWrapper initialSearchParams="relativeTime=30m&filter=active">
|
||||
<DateTimeSelection showAutoRefresh />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
mockSafeNavigate.mockClear();
|
||||
|
||||
act(() => {
|
||||
mockOnValidCustomDateChange?.({ timeStr: '2h' });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const navigatedUrl = mockSafeNavigate.mock.calls[
|
||||
mockSafeNavigate.mock.calls.length - 1
|
||||
][0] as string;
|
||||
|
||||
expect(navigatedUrl).toContain('relativeTime=2h');
|
||||
expect(navigatedUrl).toContain('filter=active');
|
||||
expect(navigatedUrl).not.toContain('startTime=');
|
||||
expect(navigatedUrl).not.toContain('endTime=');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter, Route } from 'react-router-dom';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { AppContext } from 'providers/App/App';
|
||||
import TimezoneProvider from 'providers/Timezone';
|
||||
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import store from 'store';
|
||||
import { getAppContextMock } from 'tests/test-utils';
|
||||
import { CompatRouter } from 'react-router-dom-v5-compat';
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { refetchOnWindowFocus: false, retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
export const mockStore = configureStore([thunk]);
|
||||
|
||||
interface WrapperProps {
|
||||
children: React.ReactNode;
|
||||
initialSearchParams?: string;
|
||||
initialPath?: string;
|
||||
onUrlUpdate?: (event: { searchParams: URLSearchParams }) => void;
|
||||
}
|
||||
|
||||
export function TestWrapper({
|
||||
children,
|
||||
initialSearchParams = '',
|
||||
initialPath = '/services',
|
||||
onUrlUpdate,
|
||||
}: WrapperProps): JSX.Element {
|
||||
const initialEntry = initialSearchParams
|
||||
? `${initialPath}?${initialSearchParams}`
|
||||
: initialPath;
|
||||
|
||||
const mockedStore = mockStore({
|
||||
...store.getState(),
|
||||
app: {
|
||||
...store.getState().app,
|
||||
role: 'ADMIN',
|
||||
user: {
|
||||
userId: 'test-user-id',
|
||||
email: 'test@signoz.io',
|
||||
name: 'TestUser',
|
||||
profilePictureURL: '',
|
||||
accessJwt: '',
|
||||
refreshJwt: '',
|
||||
},
|
||||
isLoggedIn: true,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<MemoryRouter initialEntries={[initialEntry]}>
|
||||
<CompatRouter>
|
||||
<NuqsTestingAdapter
|
||||
searchParams={initialSearchParams}
|
||||
onUrlUpdate={onUrlUpdate}
|
||||
>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Provider store={mockedStore}>
|
||||
<AppContext.Provider value={getAppContextMock('ADMIN')}>
|
||||
<TimezoneProvider>
|
||||
<QueryBuilderProvider>
|
||||
<Route path="*">{children}</Route>
|
||||
</QueryBuilderProvider>
|
||||
</TimezoneProvider>
|
||||
</AppContext.Provider>
|
||||
</Provider>
|
||||
</QueryClientProvider>
|
||||
</NuqsTestingAdapter>
|
||||
</CompatRouter>
|
||||
</MemoryRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export function createMockMoment(timestamp: number): {
|
||||
toDate: () => Date;
|
||||
toISOString: () => string;
|
||||
format: () => string;
|
||||
toString: () => string;
|
||||
} {
|
||||
const date = new Date(timestamp);
|
||||
return {
|
||||
toDate: (): Date => date,
|
||||
toISOString: (): string => date.toISOString(),
|
||||
format: (): string => date.toISOString(),
|
||||
toString: (): string => date.toString(),
|
||||
};
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { connect, useDispatch, useSelector } from 'react-redux';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
import { useNavigationType, useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { useNavigationType } from 'react-router-dom-v5-compat';
|
||||
import { RefreshCw, Undo } from '@signozhq/icons';
|
||||
import { Button } from 'antd';
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
} from 'store/globalTime';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { isValidShortHandDateTimeFormat } from 'lib/getMinMax';
|
||||
import getTimeString from 'lib/getTimeString';
|
||||
import { cloneDeep, isObject } from 'lodash-es';
|
||||
@@ -54,6 +53,7 @@ import {
|
||||
Time,
|
||||
TimeRange,
|
||||
} from './types';
|
||||
import { getUnstableCurrentSearchParams } from './utils/getUnstableCurrentSearchParams';
|
||||
|
||||
import './DateTimeSelectionV2.styles.scss';
|
||||
|
||||
@@ -90,10 +90,12 @@ function DateTimeSelection({
|
||||
const [hasSelectedTimeError, setHasSelectedTimeError] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
|
||||
const urlQuery = useUrlQuery();
|
||||
const searchStartTime = urlQuery.get('startTime');
|
||||
const searchEndTime = urlQuery.get('endTime');
|
||||
const relativeTimeFromUrl = urlQuery.get(QueryParams.relativeTime);
|
||||
const currentSearchParams = getUnstableCurrentSearchParams();
|
||||
const searchStartTime = currentSearchParams.get(QueryParams.startTime);
|
||||
const searchEndTime = currentSearchParams.get(QueryParams.endTime);
|
||||
const relativeTimeFromUrl = currentSearchParams.get(QueryParams.relativeTime);
|
||||
const hasTimeParamsInUrl =
|
||||
(searchStartTime && searchEndTime) || relativeTimeFromUrl;
|
||||
|
||||
// Prioritize props for initial modal time, fallback to URL params
|
||||
let initialModalStartTime = 0;
|
||||
@@ -115,8 +117,6 @@ function DateTimeSelection({
|
||||
);
|
||||
const [modalEndTime, setModalEndTime] = useState<number>(initialModalEndTime);
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
// Effect to update modal time state when props change
|
||||
useEffect(() => {
|
||||
if (modalInitialStartTime !== undefined) {
|
||||
@@ -323,14 +323,15 @@ function DateTimeSelection({
|
||||
return;
|
||||
}
|
||||
|
||||
urlQuery.delete('startTime');
|
||||
urlQuery.delete('endTime');
|
||||
const urlQuery = getUnstableCurrentSearchParams();
|
||||
urlQuery.delete(QueryParams.startTime);
|
||||
urlQuery.delete(QueryParams.endTime);
|
||||
|
||||
urlQuery.set(QueryParams.relativeTime, value);
|
||||
// Remove Hidden Filters from URL query parameters on time change
|
||||
urlQuery.delete(QueryParams.activeLogId);
|
||||
|
||||
if (searchParams.has(QueryParams.compositeQuery)) {
|
||||
if (urlQuery.has(QueryParams.compositeQuery)) {
|
||||
const updatedCompositeQuery = getUpdatedCompositeQuery();
|
||||
urlQuery.set(QueryParams.compositeQuery, updatedCompositeQuery);
|
||||
}
|
||||
@@ -349,8 +350,6 @@ function DateTimeSelection({
|
||||
getUpdatedCompositeQuery,
|
||||
updateLocalStorageForRoutes,
|
||||
updateTimeInterval,
|
||||
urlQuery,
|
||||
searchParams,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -414,6 +413,7 @@ function DateTimeSelection({
|
||||
|
||||
updateLocalStorageForRoutes(JSON.stringify({ startTime, endTime }));
|
||||
|
||||
const urlQuery = getUnstableCurrentSearchParams();
|
||||
urlQuery.set(
|
||||
QueryParams.startTime,
|
||||
startTime?.toDate().getTime().toString(),
|
||||
@@ -421,7 +421,7 @@ function DateTimeSelection({
|
||||
urlQuery.set(QueryParams.endTime, endTime?.toDate().getTime().toString());
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
|
||||
if (searchParams.has(QueryParams.compositeQuery)) {
|
||||
if (urlQuery.has(QueryParams.compositeQuery)) {
|
||||
const updatedCompositeQuery = getUpdatedCompositeQuery();
|
||||
urlQuery.set(QueryParams.compositeQuery, updatedCompositeQuery);
|
||||
}
|
||||
@@ -441,8 +441,9 @@ function DateTimeSelection({
|
||||
updateTimeInterval(dateTimeStr);
|
||||
updateLocalStorageForRoutes(dateTimeStr);
|
||||
|
||||
urlQuery.delete('startTime');
|
||||
urlQuery.delete('endTime');
|
||||
const urlQuery = getUnstableCurrentSearchParams();
|
||||
urlQuery.delete(QueryParams.startTime);
|
||||
urlQuery.delete(QueryParams.endTime);
|
||||
|
||||
urlQuery.set(QueryParams.relativeTime, dateTimeStr);
|
||||
|
||||
@@ -595,13 +596,12 @@ function DateTimeSelection({
|
||||
|
||||
// set the default relative time for alert history and overview pages if relative time is not specified
|
||||
if (
|
||||
(!urlQuery.has(QueryParams.startTime) ||
|
||||
!urlQuery.has(QueryParams.endTime)) &&
|
||||
!urlQuery.has(QueryParams.relativeTime) &&
|
||||
!hasTimeParamsInUrl &&
|
||||
(currentRoute === ROUTES.ALERT_OVERVIEW ||
|
||||
currentRoute === ROUTES.ALERT_HISTORY)
|
||||
) {
|
||||
updateTimeInterval(defaultRelativeTime);
|
||||
const urlQuery = getUnstableCurrentSearchParams();
|
||||
urlQuery.set(QueryParams.relativeTime, defaultRelativeTime);
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
safeNavigate(generatedUrl);
|
||||
@@ -625,9 +625,10 @@ function DateTimeSelection({
|
||||
updateTimeInterval(updatedTime, [preStartTime, preEndTime]);
|
||||
}
|
||||
|
||||
const urlQuery = getUnstableCurrentSearchParams();
|
||||
if (updatedTime !== 'custom') {
|
||||
urlQuery.delete('startTime');
|
||||
urlQuery.delete('endTime');
|
||||
urlQuery.delete(QueryParams.startTime);
|
||||
urlQuery.delete(QueryParams.endTime);
|
||||
urlQuery.set(QueryParams.relativeTime, updatedTime);
|
||||
} else {
|
||||
const startTime = preStartTime.toString();
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* This was introduced to fix a sync bug between Nuqs and react-router-dom
|
||||
*
|
||||
* We are using the wrong adapter for nuqs because the correct one only supports v6/v7,
|
||||
* and we are at version v5. This causes the nuqs/react-router-dom to be out of sync.
|
||||
*
|
||||
* We can revert this commit once we migrate react-router-dom to v6, or once we migrate
|
||||
* to DateTimeSelectionV3
|
||||
*/
|
||||
|
||||
/**
|
||||
* This was created to help testing the regression introduced between nuqs/react-router-dom
|
||||
*/
|
||||
type SearchParamsGetter = () => URLSearchParams;
|
||||
let getter: SearchParamsGetter = (): URLSearchParams =>
|
||||
new URLSearchParams(window.location.search);
|
||||
|
||||
/**
|
||||
* This function will return a fresh instance of URLSearchParams every time it's called.
|
||||
*
|
||||
* DO NOT USE IT FOR useEffect/useCallback dependencies, use Nuqs instead.
|
||||
*/
|
||||
export function getUnstableCurrentSearchParams(): URLSearchParams {
|
||||
return getter();
|
||||
}
|
||||
|
||||
// Testing helpers
|
||||
export function __setSearchParamsGetterForTest(fn: SearchParamsGetter): void {
|
||||
getter = fn;
|
||||
}
|
||||
|
||||
export function __resetSearchParamsGetter(): void {
|
||||
getter = (): URLSearchParams => new URLSearchParams(window.location.search);
|
||||
}
|
||||
@@ -30,10 +30,7 @@ import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { Pagination } from 'hooks/queryPagination';
|
||||
import { getDefaultPaginationConfig } from 'hooks/queryPagination/utils';
|
||||
import useDragColumns from 'hooks/useDragColumns';
|
||||
import { getDraggedColumns } from 'hooks/useDragColumns/utils';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { ArrowUp10, Minus } from '@signozhq/icons';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -85,10 +82,6 @@ function ListView({
|
||||
},
|
||||
});
|
||||
|
||||
const { draggedColumns, onDragColumns } = useDragColumns<RowData>(
|
||||
LOCALSTORAGE.TRACES_LIST_COLUMNS,
|
||||
);
|
||||
|
||||
const { queryData: paginationQueryData } = useUrlQueryData<Pagination>(
|
||||
QueryParams.pagination,
|
||||
);
|
||||
@@ -100,6 +93,19 @@ function ListView({
|
||||
[stagedQuery, orderBy],
|
||||
);
|
||||
|
||||
// TEMP — remove after traces moves to TanStack table.
|
||||
// - Drag updates selectColumns; raw queryKey would churn on reorder.
|
||||
// - Trace API fetches only listed columns → add/remove must refetch.
|
||||
// - Sorted-name signature: stable on reorder, changes on add/remove.
|
||||
const selectColumnsSignature = useMemo(
|
||||
() =>
|
||||
(options?.selectColumns ?? [])
|
||||
.map((c) => c.name)
|
||||
.sort()
|
||||
.join(','),
|
||||
[options?.selectColumns],
|
||||
);
|
||||
|
||||
const queryKey = useMemo(
|
||||
() => [
|
||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
@@ -109,7 +115,7 @@ function ListView({
|
||||
stagedQuery,
|
||||
panelType,
|
||||
paginationConfig,
|
||||
options?.selectColumns,
|
||||
selectColumnsSignature,
|
||||
orderBy,
|
||||
],
|
||||
[
|
||||
@@ -117,7 +123,7 @@ function ListView({
|
||||
panelType,
|
||||
globalSelectedTime,
|
||||
paginationConfig,
|
||||
options?.selectColumns,
|
||||
selectColumnsSignature,
|
||||
maxTime,
|
||||
minTime,
|
||||
orderBy,
|
||||
@@ -182,13 +188,14 @@ function ListView({
|
||||
|
||||
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||
|
||||
const columns = useMemo(() => {
|
||||
const updatedColumns = getListColumns(
|
||||
options?.selectColumns || [],
|
||||
formatTimezoneAdjustedTimestamp,
|
||||
);
|
||||
return getDraggedColumns(updatedColumns, draggedColumns);
|
||||
}, [options?.selectColumns, formatTimezoneAdjustedTimestamp, draggedColumns]);
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
getListColumns(
|
||||
options?.selectColumns || [],
|
||||
formatTimezoneAdjustedTimestamp,
|
||||
),
|
||||
[options?.selectColumns, formatTimezoneAdjustedTimestamp],
|
||||
);
|
||||
|
||||
const transformedQueryTableData = useMemo(
|
||||
() => transformDataWithDate(queryTableData) || [],
|
||||
@@ -196,9 +203,16 @@ function ListView({
|
||||
);
|
||||
|
||||
const handleDragColumn = useCallback(
|
||||
(fromIndex: number, toIndex: number) =>
|
||||
onDragColumns(columns, fromIndex, toIndex),
|
||||
[columns, onDragColumns],
|
||||
(fromIndex: number, toIndex: number): void => {
|
||||
const reordered = [...columns];
|
||||
const [moved] = reordered.splice(fromIndex, 1);
|
||||
reordered.splice(toIndex, 0, moved);
|
||||
const orderedIds = reordered
|
||||
.map((c) => String(('dataIndex' in c && c.dataIndex) || c.key || ''))
|
||||
.filter(Boolean);
|
||||
config?.addColumn?.onReorder(orderedIds);
|
||||
},
|
||||
[columns, config],
|
||||
);
|
||||
|
||||
const handleOrderChange = useCallback((value: string) => {
|
||||
|
||||
@@ -48,11 +48,10 @@ describe('useAuthZ', () => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.error).toBeNull();
|
||||
expect(result.current.permissions).toStrictEqual(expectedResponse);
|
||||
});
|
||||
|
||||
it('should handle API errors', async () => {
|
||||
it('should return error and null permissions when API errors', async () => {
|
||||
const permission = buildPermission('read', 'role:*');
|
||||
|
||||
server.use(
|
||||
@@ -191,7 +190,6 @@ describe('useAuthZ', () => {
|
||||
});
|
||||
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.error).toBeNull();
|
||||
expect(result.current.permissions).toStrictEqual({});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
import { createBrowserHistory } from 'history';
|
||||
import { getBasePath } from 'utils/basePath';
|
||||
|
||||
export default createBrowserHistory({ basename: getBasePath() });
|
||||
const history = createBrowserHistory({ basename: getBasePath() });
|
||||
|
||||
let inAppPushCount = 0;
|
||||
history.listen((_, action) => {
|
||||
if (action === 'PUSH') {
|
||||
inAppPushCount += 1;
|
||||
}
|
||||
});
|
||||
|
||||
export const hasInAppHistory = (): boolean => inAppPushCount > 0;
|
||||
|
||||
export default history;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Divider, Dropdown, MenuProps, Switch, Tooltip } from 'antd';
|
||||
import { Divider, Dropdown, MenuProps, Tooltip } from 'antd';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { Copy, Ellipsis, PenLine, Trash2 } from '@signozhq/icons';
|
||||
import {
|
||||
@@ -132,11 +133,7 @@ function AlertActionButtons({
|
||||
<div className="alert-action-buttons">
|
||||
<Tooltip title={isAlertRuleDisabled ? 'Enable alert' : 'Disable alert'}>
|
||||
{isAlertRuleDisabled !== undefined && (
|
||||
<Switch
|
||||
size="small"
|
||||
onChange={toggleAlertRule}
|
||||
checked={!isAlertRuleDisabled}
|
||||
/>
|
||||
<Switch onChange={toggleAlertRule} value={!isAlertRuleDisabled} />
|
||||
)}
|
||||
</Tooltip>
|
||||
<CopyToClipboard textToCopy={window.location.href} />
|
||||
|
||||
8
frontend/src/pages/DashboardPageV2/index.tsx
Normal file
8
frontend/src/pages/DashboardPageV2/index.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
function DashboardPageV2(): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<h1>Dashboard Page V2</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default DashboardPageV2;
|
||||
9
frontend/src/pages/DashboardsListPageV2/index.tsx
Normal file
9
frontend/src/pages/DashboardsListPageV2/index.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
function DashboardsListPageV2(): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<h1>Dashboards List Page V2</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardsListPageV2;
|
||||
@@ -3,7 +3,6 @@ import { useQueryClient } from 'react-query';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import cx from 'classnames';
|
||||
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
|
||||
import QueryCancelledPlaceholder from 'components/QueryCancelledPlaceholder';
|
||||
@@ -15,12 +14,6 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { usePageActions } from 'container/AIAssistant/pageActions/usePageActions';
|
||||
import LogExplorerQuerySection from 'container/LogExplorerQuerySection';
|
||||
import LogsExplorerViewsContainer from 'container/LogsExplorerViews';
|
||||
import {
|
||||
defaultLogsSelectedColumns,
|
||||
defaultOptionsQuery,
|
||||
URL_OPTIONS,
|
||||
} from 'container/OptionsMenu/constants';
|
||||
import { OptionsQuery } from 'container/OptionsMenu/types';
|
||||
import LeftToolbarActions from 'container/QueryBuilder/components/ToolbarActions/LeftToolbarActions';
|
||||
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
|
||||
import Toolbar from 'container/Toolbar/Toolbar';
|
||||
@@ -31,11 +24,9 @@ import {
|
||||
useHandleExplorerTabChange,
|
||||
} from 'hooks/useHandleExplorerTabChange';
|
||||
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { defaultTo, isEmpty, isEqual, isNull } from 'lodash-es';
|
||||
import { defaultTo, isEmpty, isNull } from 'lodash-es';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { EventSourceProvider } from 'providers/EventSource';
|
||||
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import { Warning } from 'types/api';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import {
|
||||
@@ -62,8 +53,6 @@ function LogsExplorer(): JSX.Element {
|
||||
const [selectedView, setSelectedView] = useState<ExplorerViews>(
|
||||
() => panelTypeToExplorerView[panelTypesFromUrl],
|
||||
);
|
||||
const { logs } = usePreferenceContext();
|
||||
const { preferences } = logs;
|
||||
|
||||
const [showFilters, setShowFilters] = useState<boolean>(() => {
|
||||
const localStorageValue = getLocalStorageKey(
|
||||
@@ -182,116 +171,6 @@ function LogsExplorer(): JSX.Element {
|
||||
setShowFilters((prev) => !prev);
|
||||
};
|
||||
|
||||
const { redirectWithQuery: redirectWithOptionsData } =
|
||||
useUrlQueryData<OptionsQuery>(URL_OPTIONS, defaultOptionsQuery);
|
||||
|
||||
// Get and parse stored columns from localStorage
|
||||
const logListOptionsFromLocalStorage = useMemo(() => {
|
||||
const data = getLocalStorageKey(LOCALSTORAGE.LOGS_LIST_OPTIONS);
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Check if the columns have the required columns (timestamp, body)
|
||||
const hasRequiredColumns = useCallback(
|
||||
(columns?: TelemetryFieldKey[] | null): boolean => {
|
||||
if (!columns?.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasTimestamp = columns.some((col) => col.name === 'timestamp');
|
||||
const hasBody = columns.some((col) => col.name === 'body');
|
||||
|
||||
return hasTimestamp && hasBody;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// Merge the columns with the required columns (timestamp, body) if missing
|
||||
const mergeWithRequiredColumns = useCallback(
|
||||
(columns: TelemetryFieldKey[]): TelemetryFieldKey[] => [
|
||||
// Add required columns (timestamp, body) if missing
|
||||
...(!hasRequiredColumns(columns) ? defaultLogsSelectedColumns : []),
|
||||
...columns,
|
||||
],
|
||||
[hasRequiredColumns],
|
||||
);
|
||||
|
||||
// Migrate the options query to the new format
|
||||
const migrateOptionsQuery = useCallback(
|
||||
(query: OptionsQuery): OptionsQuery => {
|
||||
// Skip if already migrated
|
||||
if (query.version) {
|
||||
return query;
|
||||
}
|
||||
|
||||
if (logListOptionsFromLocalStorage?.version) {
|
||||
return logListOptionsFromLocalStorage;
|
||||
}
|
||||
|
||||
// Case 1: we have localStorage columns
|
||||
if (logListOptionsFromLocalStorage?.selectColumns?.length > 0) {
|
||||
return {
|
||||
...query,
|
||||
version: 1,
|
||||
selectColumns: mergeWithRequiredColumns(
|
||||
logListOptionsFromLocalStorage.selectColumns,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// Case 2: No query columns in localStorage in but query has columns
|
||||
if (query.selectColumns.length > 0) {
|
||||
return {
|
||||
...query,
|
||||
version: 1,
|
||||
selectColumns: mergeWithRequiredColumns(query.selectColumns),
|
||||
};
|
||||
}
|
||||
|
||||
// Case 3: No columns anywhere, use defaults
|
||||
return {
|
||||
...query,
|
||||
version: 1,
|
||||
selectColumns: defaultLogsSelectedColumns,
|
||||
};
|
||||
},
|
||||
[mergeWithRequiredColumns, logListOptionsFromLocalStorage],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!preferences) {
|
||||
return;
|
||||
}
|
||||
const migratedQuery = migrateOptionsQuery({
|
||||
selectColumns: preferences.columns || defaultLogsSelectedColumns,
|
||||
maxLines: preferences.formatting?.maxLines || defaultOptionsQuery.maxLines,
|
||||
format: preferences.formatting?.format || defaultOptionsQuery.format,
|
||||
fontSize: preferences.formatting?.fontSize || defaultOptionsQuery.fontSize,
|
||||
version: preferences.formatting?.version,
|
||||
});
|
||||
// Only redirect if the query was actually modified
|
||||
if (
|
||||
!isEqual(migratedQuery, {
|
||||
selectColumns: preferences?.columns,
|
||||
maxLines: preferences?.formatting?.maxLines,
|
||||
format: preferences?.formatting?.format,
|
||||
fontSize: preferences?.formatting?.fontSize,
|
||||
version: preferences?.formatting?.version,
|
||||
})
|
||||
) {
|
||||
redirectWithOptionsData(migratedQuery);
|
||||
}
|
||||
}, [migrateOptionsQuery, preferences, redirectWithOptionsData]);
|
||||
|
||||
const toolbarViews = useMemo(
|
||||
() => ({
|
||||
list: {
|
||||
|
||||
@@ -16,7 +16,7 @@ import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
|
||||
import dayjs from 'dayjs';
|
||||
import history from 'lib/history';
|
||||
import history, { hasInAppHistory } from 'lib/history';
|
||||
import {
|
||||
ArrowLeft,
|
||||
CalendarClock,
|
||||
@@ -96,13 +96,7 @@ function TraceDetailsHeader({
|
||||
}, [traceID]);
|
||||
|
||||
const handlePreviousBtnClick = useCallback((): void => {
|
||||
const isSpaNavigate =
|
||||
document.referrer &&
|
||||
// oxlint-disable-next-line signoz/no-raw-absolute-path
|
||||
new URL(document.referrer).origin === window.location.origin;
|
||||
const hasBackHistory = window.history.length > 1;
|
||||
|
||||
if (isSpaNavigate && hasBackHistory) {
|
||||
if (hasInAppHistory()) {
|
||||
history.goBack();
|
||||
} else {
|
||||
history.push(ROUTES.TRACES_EXPLORER);
|
||||
@@ -130,6 +124,7 @@ function TraceDetailsHeader({
|
||||
size="md"
|
||||
className={styles.backBtn}
|
||||
onClick={handlePreviousBtnClick}
|
||||
aria-label="Back"
|
||||
>
|
||||
<ArrowLeft size={14} />
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { render } from 'tests/test-utils';
|
||||
|
||||
import TraceDetailsHeader from '../TraceDetailsHeader';
|
||||
|
||||
const mockGoBack = jest.fn();
|
||||
const mockPush = jest.fn();
|
||||
const mockHasInAppHistory = jest.fn();
|
||||
|
||||
jest.mock('lib/history', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
goBack: (): void => mockGoBack(),
|
||||
push: (path: string): void => mockPush(path),
|
||||
replace: jest.fn(),
|
||||
location: { pathname: '/', search: '' },
|
||||
listen: (): (() => void) => (): void => undefined,
|
||||
},
|
||||
hasInAppHistory: (): boolean => mockHasInAppHistory(),
|
||||
}));
|
||||
|
||||
const baseProps = {
|
||||
filterMetadata: {
|
||||
startTime: 0,
|
||||
endTime: 1,
|
||||
traceId: 'trace-123',
|
||||
},
|
||||
onFilteredSpansChange: jest.fn(),
|
||||
isDataLoaded: false,
|
||||
};
|
||||
|
||||
describe('TraceDetailsHeader – back button', () => {
|
||||
beforeEach(() => {
|
||||
mockGoBack.mockClear();
|
||||
mockPush.mockClear();
|
||||
mockHasInAppHistory.mockReset();
|
||||
});
|
||||
|
||||
it('calls history.goBack() when there is in-app SPA history', () => {
|
||||
mockHasInAppHistory.mockReturnValue(true);
|
||||
render(<TraceDetailsHeader {...baseProps} />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /back/i }));
|
||||
|
||||
expect(mockGoBack).toHaveBeenCalledTimes(1);
|
||||
expect(mockPush).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('pushes to the traces explorer route when there is no in-app SPA history', () => {
|
||||
mockHasInAppHistory.mockReturnValue(false);
|
||||
render(<TraceDetailsHeader {...baseProps} />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /back/i }));
|
||||
|
||||
expect(mockPush).toHaveBeenCalledTimes(1);
|
||||
expect(mockPush).toHaveBeenCalledWith(ROUTES.TRACES_EXPLORER);
|
||||
expect(mockGoBack).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -6,9 +6,9 @@ import {
|
||||
Form,
|
||||
MenuProps,
|
||||
Space,
|
||||
Switch,
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import cx from 'classnames';
|
||||
import { FilterSelect } from 'components/CeleryOverview/CeleryOverviewConfigOptions/CeleryOverviewConfigOptions';
|
||||
import { QueryParams } from 'constants/query';
|
||||
@@ -201,8 +201,7 @@ function FunnelStep({
|
||||
<div className="error">
|
||||
<Switch
|
||||
className="error__switch"
|
||||
size="small"
|
||||
checked={stepData.has_errors}
|
||||
value={stepData.has_errors}
|
||||
disabled={!hasEditPermission}
|
||||
onChange={(): void =>
|
||||
onStepChange(index, { has_errors: !stepData.has_errors })
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import MEditor, { EditorProps, Monaco } from '@monaco-editor/react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Switch } from 'antd';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
|
||||
@@ -72,9 +72,8 @@ function JsonView({ data, height = '575px' }: JsonViewProps): JSX.Element {
|
||||
<div className="json-view__wrap-toggle">
|
||||
<Typography.Text>Wrap text</Typography.Text>
|
||||
<Switch
|
||||
checked={isWrapWord}
|
||||
value={isWrapWord}
|
||||
onChange={(checked): void => setIsWrapWord(checked)}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -227,10 +227,8 @@ function PrettyView({
|
||||
menu={{ items: menuItems }}
|
||||
align="start"
|
||||
className="pretty-view-actions-dropdown"
|
||||
// onClick on the dropdown content is forwarded to the underlying div via ...props
|
||||
// but is not in the public type. Stop click bubbling so item clicks don't reach
|
||||
// clickable ancestors of the trigger through the React tree.
|
||||
// @ts-expect-error see comment above
|
||||
// Stop click bubbling so item clicks don't reach clickable ancestors
|
||||
// of the trigger through the React tree.
|
||||
onClick={(e: React.MouseEvent): void => e.stopPropagation()}
|
||||
>
|
||||
<span
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user