Compare commits

..

8 Commits

Author SHA1 Message Date
ahmadshaheer
733d6ce78c chore: overall improvements to general tests 2024-07-18 10:59:58 +04:30
ahmadshaheer
d7d78d6ab4 chore: overall improvements to general tests 2024-07-18 10:50:18 +04:30
ahmadshaheer
fb73f23dce feat: write tests for cloud user general settings 2024-07-17 19:32:17 +04:30
ahmadshaheer
cb790ea3e5 chore: overall improvements to general settings tests 2024-07-17 19:31:09 +04:30
ahmadshaheer
e48784c09c feat: general settings tests 2024-07-17 15:21:39 +04:30
Vikrant Gupta
a6e68c6519 fix: issue with table sorting when column contains both string and numbers (#5458) 2024-07-15 21:15:37 +05:30
Vikrant Gupta
c7e3e6dc4e fix: retain legends while changing panel types (#5447) 2024-07-15 21:04:49 +05:30
Srikanth Chekuri
9194ab08b6 fix: incorrect response for promql value type panels (#5497) 2024-07-15 18:06:39 +05:30
20 changed files with 1672 additions and 174 deletions

View File

@@ -524,7 +524,13 @@ function GeneralSettings({
) {
return (
<Fragment key={category.name}>
<Col xs={22} xl={11} key={category.name} style={{ margin: '0.5rem' }}>
<Col
xs={22}
xl={11}
key={category.name}
style={{ margin: '0.5rem' }}
data-testid={`${category.name.toLowerCase()}-card`}
>
<Card style={{ height: '100%' }}>
<Typography.Title style={{ margin: 0 }} level={3}>
{category.name}
@@ -554,6 +560,7 @@ function GeneralSettings({
type="primary"
onClick={category.save.modalOpen}
disabled={category.save.isDisabled}
data-testid="retention-submit-button"
>
{category.save.saveButtonText}
</Button>
@@ -574,6 +581,7 @@ function GeneralSettings({
centered
open={category.save.modal}
confirmLoading={category.save.apiLoading}
data-testid={`${category.name.toLowerCase()}-modal`}
>
<Typography>
{t('retention_confirmation_description', {
@@ -597,14 +605,16 @@ function GeneralSettings({
<Col xs={24} md={22} xl={20} xxl={18} style={{ margin: 'auto' }}>
<ErrorTextContainer>
{!isCloudUserVal && (
<TextToolTip
{...{
text: `More details on how to set retention period`,
url: 'https://signoz.io/docs/userguide/retention-period/',
}}
/>
<div data-testid="help-icon">
<TextToolTip
{...{
text: `More details on how to set retention period`,
url: 'https://signoz.io/docs/userguide/retention-period/',
}}
/>
</div>
)}
{errorText && <ErrorText>{errorText}</ErrorText>}
{errorText && <ErrorText data-testid="error-text">{errorText}</ErrorText>}
</ErrorTextContainer>
<Row justify="start">{renderConfig}</Row>
@@ -615,7 +625,7 @@ function GeneralSettings({
);
}
interface GeneralSettingsProps {
export interface GeneralSettingsProps {
getAvailableDiskPayload: GetDisksPayload;
metricsTtlValuesPayload: GetRetentionPeriodMetricsPayload;
tracesTtlValuesPayload: GetRetentionPeriodTracesPayload;

View File

@@ -95,9 +95,11 @@ function Retention({
return (
<RetentionContainer>
<Row justify="space-between">
<Row justify="space-between" aria-label={text}>
<Col span={12} style={{ display: 'flex' }}>
<RetentionFieldLabel>{text}</RetentionFieldLabel>
<RetentionFieldLabel data-testid="retention-field-label">
{text}
</RetentionFieldLabel>
</Col>
<Row justify="end">
<RetentionFieldInputContainer>
@@ -106,12 +108,14 @@ function Retention({
disabled={isCloudUserVal}
onChange={(e): void => onChangeHandler(e, setSelectedValue)}
style={{ width: 75 }}
data-testid="retention-field-input"
/>
<Select
value={selectedTimeUnit}
onChange={currentSelectedOption}
disabled={isCloudUserVal}
style={{ width: 100 }}
data-testid="retention-field-dropdown"
>
{menuItems}
</Select>

View File

@@ -60,6 +60,7 @@ function StatusMessage({
style={{
color: messageColor,
}}
data-testid="status-message"
>
{statusMessage}
</Col>

View File

@@ -2,6 +2,7 @@ import { Typography } from 'antd';
import getDisks from 'api/disks/getDisks';
import getRetentionPeriodApi from 'api/settings/getRetention';
import Spinner from 'components/Spinner';
import GeneralSettingsContainer from 'container/GeneralSettings/GeneralSettings';
import { useTranslation } from 'react-i18next';
import { useQueries } from 'react-query';
import { useSelector } from 'react-redux';
@@ -11,8 +12,6 @@ import { TTTLType } from 'types/api/settings/common';
import { PayloadProps as GetRetentionPeriodAPIPayloadProps } from 'types/api/settings/getRetention';
import AppReducer from 'types/reducer/app';
import GeneralSettingsContainer from './GeneralSettings';
type TRetentionAPIReturn<T extends TTTLType> = Promise<
SuccessResponse<GetRetentionPeriodAPIPayloadProps<T>> | ErrorResponse
>;

View File

@@ -7,7 +7,7 @@ export default function GeneralSettingsCloud(): JSX.Element {
return (
<Card className="general-settings-container">
<Info size={16} />
<Typography.Text>
<Typography.Text data-testid="cloud-user-info-card">
Please <a href="mailto:cloud-support@signoz.io"> email us </a> or connect
with us via intercom support to change the retention period.
</Typography.Text>

View File

@@ -1,6 +1,10 @@
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { createColumnsAndDataSource, getQueryLegend } from '../utils';
import {
createColumnsAndDataSource,
getQueryLegend,
sortFunction,
} from '../utils';
import {
expectedOutputWithLegends,
tableDataMultipleQueriesSuccessResponse,
@@ -39,4 +43,88 @@ describe('Table Panel utils', () => {
// should return undefined when legend not present
expect(getQueryLegend(query, 'B')).toBe(undefined);
});
it('sorter function for table sorting', () => {
let rowA: {
A: string | number;
timestamp: number;
key: string;
} = {
A: 22.4,
timestamp: 111111,
key: '1111',
};
let rowB: {
A: string | number;
timestamp: number;
key: string;
} = {
A: 'n/a',
timestamp: 111112,
key: '1112',
};
const item = {
isValueColumn: true,
name: 'A',
queryName: 'A',
};
// A has value and value is considered bigger than n/a hence 1
expect(sortFunction(rowA, rowB, item)).toBe(1);
rowA = {
A: 'n/a',
timestamp: 111111,
key: '1111',
};
rowB = {
A: 22.4,
timestamp: 111112,
key: '1112',
};
// B has value and value is considered bigger than n/a hence -1
expect(sortFunction(rowA, rowB, item)).toBe(-1);
rowA = {
A: 11,
timestamp: 111111,
key: '1111',
};
rowB = {
A: 22,
timestamp: 111112,
key: '1112',
};
// A and B has value , since B > A hence A-B
expect(sortFunction(rowA, rowB, item)).toBe(-11);
rowA = {
A: 'read',
timestamp: 111111,
key: '1111',
};
rowB = {
A: 'write',
timestamp: 111112,
key: '1112',
};
// A and B are strings so A is smaller than B because r comes before w hence -1
expect(sortFunction(rowA, rowB, item)).toBe(-1);
rowA = {
A: 'n/a',
timestamp: 111111,
key: '1111',
};
rowB = {
A: 'n/a',
timestamp: 111112,
key: '1112',
};
// A and B are strings n/a , since both of them are same hence 0
expect(sortFunction(rowA, rowB, item)).toBe(0);
});
});

View File

@@ -1,3 +1,4 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { ColumnsType, ColumnType } from 'antd/es/table';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
import { QUERY_TABLE_CONFIG } from 'container/QueryTable/config';
@@ -105,6 +106,39 @@ export function getQueryLegend(
return legend;
}
export function sortFunction(
a: RowData,
b: RowData,
item: {
name: string;
queryName: string;
isValueColumn: boolean;
},
): number {
// assumption :- number values is bigger than 'n/a'
const valueA = Number(a[`${item.name}_without_unit`] ?? a[item.name]);
const valueB = Number(b[`${item.name}_without_unit`] ?? b[item.name]);
// if both the values are numbers then return the difference here
if (!isNaN(valueA) && !isNaN(valueB)) {
return valueA - valueB;
}
// if valueB is a number then make it bigger value
if (isNaN(valueA) && !isNaN(valueB)) {
return -1;
}
// if valueA is number make it the bigger value
if (!isNaN(valueA) && isNaN(valueB)) {
return 1;
}
// if both of them are strings do the localecompare
return ((a[item.name] as string) || '').localeCompare(
(b[item.name] as string) || '',
);
}
export function createColumnsAndDataSource(
data: TableData,
currentQuery: Query,
@@ -123,18 +157,7 @@ export function createColumnsAndDataSource(
title: !isEmpty(legend) ? legend : item.name,
width: QUERY_TABLE_CONFIG.width,
render: renderColumnCell && renderColumnCell[item.name],
sorter: (a: RowData, b: RowData): number => {
const valueA = Number(a[`${item.name}_without_unit`] ?? a[item.name]);
const valueB = Number(b[`${item.name}_without_unit`] ?? b[item.name]);
if (!isNaN(valueA) && !isNaN(valueB)) {
return valueA - valueB;
}
return ((a[item.name] as string) || '').localeCompare(
(b[item.name] as string) || '',
);
},
sorter: (a: RowData, b: RowData): number => sortFunction(a, b, item),
};
return [...acc, column];

View File

@@ -54,6 +54,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'disabled',
'legend',
],
},
},
@@ -72,6 +73,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'disabled',
'legend',
],
},
},
@@ -88,6 +90,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'disabled',
'legend',
],
},
},
@@ -107,6 +110,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'disabled',
'legend',
],
},
},
@@ -125,6 +129,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'disabled',
'legend',
],
},
},
@@ -141,6 +146,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'disabled',
'legend',
],
},
},
@@ -157,6 +163,8 @@ export const panelTypeDataSourceFormValuesMap: Record<
'having',
'orderBy',
'functions',
'disabled',
'legend',
],
},
},
@@ -172,6 +180,8 @@ export const panelTypeDataSourceFormValuesMap: Record<
'orderBy',
'functions',
'spaceAggregation',
'disabled',
'legend',
],
},
},
@@ -185,6 +195,8 @@ export const panelTypeDataSourceFormValuesMap: Record<
'limit',
'having',
'orderBy',
'disabled',
'legend',
],
},
},
@@ -204,6 +216,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'disabled',
'legend',
],
},
},
@@ -222,6 +235,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'disabled',
'legend',
],
},
},
@@ -238,6 +252,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'disabled',
'legend',
],
},
},
@@ -257,6 +272,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'disabled',
'legend',
],
},
},
@@ -275,6 +291,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'disabled',
'legend',
],
},
},
@@ -291,6 +308,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'disabled',
'legend',
],
},
},
@@ -325,6 +343,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'disabled',
'legend',
],
},
},
@@ -341,6 +360,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'disabled',
'legend',
],
},
},
@@ -357,6 +377,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'disabled',
'legend',
],
},
},

View File

@@ -97,4 +97,12 @@ export const handlers = [
rest.post('http://localhost/api/v1/invite', (_, res, ctx) =>
res(ctx.status(200), ctx.json(inviteUser)),
),
rest.post('http://localhost/api/v1/settings/ttl', (_, res, ctx) =>
res(
ctx.status(200),
ctx.json({
message: 'move ttl has been successfully set up',
}),
),
),
];

View File

@@ -0,0 +1,180 @@
/* eslint-disable sonarjs/no-duplicate-string */
import GeneralSettingsContainer from 'container/GeneralSettings/GeneralSettings';
import {
act,
fireEvent,
render,
screen,
waitFor,
within,
} from 'tests/test-utils';
import { generalSettingsProps } from './mock';
const tooltipText = /More details on how to set retention period/;
const types = [
{
testId: 'metrics-card',
header: 'Metrics',
modalTestId: 'metrics-modal',
},
{
testId: 'traces-card',
header: 'Traces',
modalTestId: 'traces-modal',
},
{
testId: 'logs-card',
header: 'Logs',
modalTestId: 'logs-modal',
},
];
describe('General Settings Page', () => {
beforeEach(() => {
render(
<GeneralSettingsContainer
metricsTtlValuesPayload={generalSettingsProps.metricsTtlValuesPayload}
tracesTtlValuesPayload={generalSettingsProps.tracesTtlValuesPayload}
logsTtlValuesPayload={generalSettingsProps.logsTtlValuesPayload}
getAvailableDiskPayload={generalSettingsProps.getAvailableDiskPayload}
metricsTtlValuesRefetch={generalSettingsProps.metricsTtlValuesRefetch}
tracesTtlValuesRefetch={generalSettingsProps.tracesTtlValuesRefetch}
logsTtlValuesRefetch={generalSettingsProps.logsTtlValuesRefetch}
/>,
);
});
it('should properly display the help icon', async () => {
const helpIcon = screen.getByLabelText('question-circle');
fireEvent.mouseOver(helpIcon);
await waitFor(() => {
const tooltip = screen.getByText(tooltipText);
expect(tooltip).toBeInTheDocument();
});
});
types.forEach(({ testId, header, modalTestId }) => {
describe(`${header} Card`, () => {
it(`should be able to find "${header}" as the header `, () => {
expect(
screen.getByRole('heading', {
name: header,
}),
).toBeInTheDocument();
});
it(`should check if ${header} body is properly displayed`, () => {
const sectionCard = screen.getByTestId(testId);
const retentionFieldLabel = within(sectionCard).getByTestId(
'retention-field-label',
);
expect(retentionFieldLabel).toBeInTheDocument();
const retentionFieldInput = within(sectionCard).getByTestId(
'retention-field-input',
);
expect(retentionFieldInput).toBeInTheDocument();
const retentionFieldDropdown = within(sectionCard).getByTestId(
'retention-field-dropdown',
);
expect(retentionFieldDropdown).toBeInTheDocument();
const retentionSubmitButton = within(sectionCard).getByTestId(
'retention-submit-button',
);
expect(retentionSubmitButton).toBeInTheDocument();
});
it('Should check if save button is disabled by default', () => {
const sectionCard = screen.getByTestId(testId);
const retentionSubmitButton = within(sectionCard).getByTestId(
'retention-submit-button',
);
expect(retentionSubmitButton).toBeDisabled();
});
it('Should check if changing the value of the textbox enables the save button ', () => {
const sectionCard = screen.getByTestId(testId);
const retentionFieldInput = within(sectionCard).getByTestId(
'retention-field-input',
);
const retentionSubmitButton = within(sectionCard).getByTestId(
'retention-submit-button',
);
expect(retentionSubmitButton).toBeDisabled();
act(() => {
fireEvent.change(retentionFieldInput, { target: { value: '2' } });
});
expect(retentionSubmitButton).toBeEnabled();
});
it('Should check if "retention_null_value_error" is displayed if the value is not set ', async () => {
const sectionCard = screen.getByTestId(testId);
const retentionFieldInput = within(sectionCard).getByTestId(
'retention-field-input',
);
act(() => {
fireEvent.change(retentionFieldInput, { target: { value: 0 } });
});
expect(
await screen.findByText('retention_null_value_error'),
).toBeInTheDocument();
});
it('should display the modal when a value is provided and save is clicked', async () => {
const sectionCard = screen.getByTestId(testId);
const retentionFieldInput = within(sectionCard).getByTestId(
'retention-field-input',
);
const retentionSubmitButton = within(sectionCard).getByTestId(
'retention-submit-button',
);
act(() => {
fireEvent.change(retentionFieldInput, { target: { value: 1 } });
fireEvent.click(retentionSubmitButton);
});
const sectionModal = screen.getByTestId(modalTestId);
expect(
await within(sectionModal).findByText('retention_confirmation'),
).toBeInTheDocument();
});
describe(`${header} Modal`, () => {
let sectionModal: HTMLElement;
beforeEach(() => {
const sectionCard = screen.getByTestId(testId);
const retentionFieldInput = within(sectionCard).getByTestId(
'retention-field-input',
);
const retentionSubmitButton = within(sectionCard).getByTestId(
'retention-submit-button',
);
act(() => {
fireEvent.change(retentionFieldInput, { target: { value: 1 } });
fireEvent.click(retentionSubmitButton);
});
sectionModal = screen.getByTestId(modalTestId);
});
it('Should check if the modal is properly displayed', async () => {
expect(
within(sectionModal).getByText('retention_confirmation'),
).toBeInTheDocument();
expect(
within(sectionModal).getByText('retention_confirmation_description'),
).toBeInTheDocument();
});
});
});
});
});

View File

@@ -0,0 +1,62 @@
import GeneralSettingsContainer from 'container/GeneralSettings/GeneralSettings';
import { render, screen, within } from 'tests/test-utils';
import { generalSettingsProps } from './mock';
jest.mock('utils/app', () => {
const app = jest.requireActual('utils/app');
return {
...app,
isCloudUser: jest.fn(() => true),
};
});
const types = [
{
testId: 'metrics-card',
header: 'Metrics',
},
{
testId: 'traces-card',
header: 'Traces',
},
{
testId: 'logs-card',
header: 'Logs',
},
];
describe('Cloud User General Settings', () => {
beforeEach(() => {
render(
<GeneralSettingsContainer
metricsTtlValuesPayload={generalSettingsProps.metricsTtlValuesPayload}
tracesTtlValuesPayload={generalSettingsProps.tracesTtlValuesPayload}
logsTtlValuesPayload={generalSettingsProps.logsTtlValuesPayload}
getAvailableDiskPayload={generalSettingsProps.getAvailableDiskPayload}
metricsTtlValuesRefetch={generalSettingsProps.metricsTtlValuesRefetch}
tracesTtlValuesRefetch={generalSettingsProps.tracesTtlValuesRefetch}
logsTtlValuesRefetch={generalSettingsProps.logsTtlValuesRefetch}
/>,
);
});
it('should display the cloud user info card', () => {
const cloudUserCard = screen.getByTestId('cloud-user-info-card');
expect(cloudUserCard).toBeInTheDocument();
});
types.forEach(({ testId, header }) => {
it(`should check if value textbox and duration dropdown are disabled in the body of ${header}`, () => {
const sectionCard = screen.getByTestId(testId);
const retentionFieldInput = within(sectionCard).getByTestId(
'retention-field-input',
);
expect(retentionFieldInput).toBeDisabled();
const retentionFieldDropdown = within(sectionCard).getByTestId(
'retention-field-dropdown',
);
expect(retentionFieldDropdown).toHaveClass('ant-select-disabled');
});
});
});

View File

@@ -0,0 +1,63 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { QueryObserverResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
PayloadPropsLogs,
PayloadPropsMetrics,
PayloadPropsTraces,
TStatus,
} from 'types/api/settings/getRetention';
export const generalSettingsProps = {
metricsTtlValuesPayload: {
metrics_ttl_duration_hrs: 720,
metrics_move_ttl_duration_hrs: -1,
expected_metrics_ttl_duration_hrs: 720,
expected_metrics_move_ttl_duration_hrs: -1,
status: 'success' as TStatus,
},
tracesTtlValuesPayload: {
traces_ttl_duration_hrs: 360,
traces_move_ttl_duration_hrs: -1,
expected_traces_ttl_duration_hrs: 24,
expected_traces_move_ttl_duration_hrs: -1,
status: '' as TStatus,
},
logsTtlValuesPayload: {
logs_ttl_duration_hrs: 360,
logs_move_ttl_duration_hrs: -1,
expected_logs_ttl_duration_hrs: 360,
expected_logs_move_ttl_duration_hrs: -1,
status: 'success' as TStatus,
},
getAvailableDiskPayload: [
{
name: 'default',
type: 'local',
},
],
metricsTtlValuesRefetch(): Promise<
QueryObserverResult<
ErrorResponse | SuccessResponse<PayloadPropsMetrics, unknown>,
unknown
>
> {
throw new Error('Function not implemented.');
},
tracesTtlValuesRefetch(): Promise<
QueryObserverResult<
ErrorResponse | SuccessResponse<PayloadPropsTraces, unknown>,
unknown
>
> {
throw new Error('Function not implemented.');
},
logsTtlValuesRefetch(): Promise<
QueryObserverResult<
ErrorResponse | SuccessResponse<PayloadPropsLogs, unknown>,
unknown
>
> {
throw new Error('Function not implemented.');
},
};

View File

@@ -1,135 +0,0 @@
import { Calendar } from 'antd';
import { Book, Cable, Github, MessageSquare, Slack } from 'lucide-react';
import { fireEvent, render } from 'tests/test-utils';
import Support from './Support';
const launchChat = 'Launch chat';
const useAnalyticsMock = jest.fn();
const useHistoryMock = jest.fn();
jest.mock('hooks/analytics/useAnalytics', () => ({
__esModule: true,
default: jest.fn(() => ({ trackEvent: useAnalyticsMock })),
}));
jest.mock('react-router-dom', () => ({
useHistory: useHistoryMock,
}));
const supportChannels = [
{
key: 'documentation',
name: 'Documentation',
icon: <Book />,
title: 'Find answers in the documentation.',
url: 'https://signoz.io/docs/',
btnText: 'Visit docs',
},
{
key: 'github',
name: 'Github',
icon: <Github />,
title: 'Create an issue on GitHub to report bugs or request new features.',
url: 'https://github.com/SigNoz/signoz/issues',
btnText: 'Create issue',
},
{
key: 'slack_community',
name: 'Slack Community',
icon: <Slack />,
title: 'Get support from the SigNoz community on Slack.',
url: 'https://signoz.io/slack',
btnText: 'Join Slack',
},
{
key: 'chat',
name: 'Chat',
icon: <MessageSquare />,
title: 'Get quick support directly from the team.',
url: '',
btnText: launchChat,
},
{
key: 'schedule_call',
name: 'Schedule a call',
icon: <Calendar />,
title: 'Schedule a call with the founders.',
url: 'https://calendly.com/pranay-signoz/signoz-intro-calls',
btnText: 'Schedule call',
},
{
key: 'slack_connect',
name: 'Slack Connect',
icon: <Cable />,
title: 'Get a dedicated support channel for your team.',
url: '',
btnText: 'Request Slack connect',
},
];
describe('Help and Support renders correctly', () => {
it('should render the support page with all support channels', () => {
const { getByText } = render(<Support />);
expect(getByText('Support')).toBeInTheDocument();
expect(
getByText(
'We are here to help in case of questions or issues. Pick the channel that is most convenient for you.',
),
).toBeInTheDocument();
supportChannels.forEach((channel) => {
expect(getByText(channel.name)).toBeInTheDocument();
expect(getByText(channel.title)).toBeInTheDocument();
expect(getByText(channel.btnText)).toBeInTheDocument();
});
});
it('should trigger correct handler function when channel button is clicked', () => {
const { getByText } = render(<Support />);
const button = getByText('Visit docs');
const windowOpenMock = jest.spyOn(window, 'open').mockImplementation();
fireEvent.click(button);
expect(windowOpenMock).toHaveBeenCalledWith(
'https://signoz.io/docs/',
'_blank',
);
});
it('should open Intercom chat widget when Chat channel is clicked', () => {
window.Intercom = jest.fn();
const { getByText } = render(<Support />);
const button = getByText(launchChat);
fireEvent.click(button);
expect(window.Intercom).toHaveBeenCalledWith('show');
});
});
describe('Handle channels with null or undefined properties', () => {
it('should handle window.Intercom being undefined or null', () => {
window.Intercom = null;
const { getByText } = render(<Support />);
const button = getByText(launchChat);
fireEvent.click(button);
expect(window.Intercom).toBeNull();
});
it('should handle missing or undefined history.location.state', () => {
const trackEvent = jest.fn();
useHistoryMock.mockReturnValue({ location: {} });
render(<Support />);
expect(trackEvent).not.toHaveBeenCalled();
});
it('should handle missing or undefined channel.url', () => {
const openSpy = jest.spyOn(window, 'open').mockImplementation();
const { getByText } = render(<Support />);
fireEvent.click(getByText(launchChat));
expect(openSpy).not.toHaveBeenCalled();
openSpy.mockRestore();
});
it('should handle missing or undefined channel.name', () => {
const trackEvent = jest.fn();
const handleChannelClick = jest.fn();
const channelWithoutName = { key: 'chat', url: '' };
render(<Support />);
handleChannelClick(channelWithoutName);
expect(trackEvent).not.toHaveBeenCalledWith();
});
});

View File

@@ -18,7 +18,7 @@ import (
"go.uber.org/zap"
)
func prepareLogsQuery(ctx context.Context,
func prepareLogsQuery(_ context.Context,
start,
end int64,
builderQuery *v3.BuilderQuery,

View File

@@ -50,8 +50,10 @@ type querier struct {
// TODO(srikanthccv): remove this once we have a proper mock
testingMode bool
queriesExecuted []string
returnedSeries []*v3.Series
returnedErr error
// tuple of start and end time in milliseconds
timeRanges [][]int
returnedSeries []*v3.Series
returnedErr error
}
type QuerierOptions struct {
@@ -117,6 +119,7 @@ func (q *querier) execClickHouseQuery(ctx context.Context, query string) ([]*v3.
func (q *querier) execPromQuery(ctx context.Context, params *model.QueryRangeParams) ([]*v3.Series, error) {
q.queriesExecuted = append(q.queriesExecuted, params.Query)
if q.testingMode && q.reader == nil {
q.timeRanges = append(q.timeRanges, []int{int(params.Start.UnixMilli()), int(params.End.UnixMilli())})
return q.returnedSeries, q.returnedErr
}
promResult, _, err := q.reader.GetQueryRangeResult(ctx, params)
@@ -342,10 +345,10 @@ func (q *querier) runPromQueries(ctx context.Context, params *v3.QueryRangeParam
wg.Add(1)
go func(queryName string, promQuery *v3.PromQuery) {
defer wg.Done()
cacheKey := cacheKeys[queryName]
cacheKey, ok := cacheKeys[queryName]
var cachedData []byte
// Ensure NoCache is not set and cache is not nil
if !params.NoCache && q.cache != nil {
if !params.NoCache && q.cache != nil && ok {
data, retrieveStatus, err := q.cache.Retrieve(cacheKey, true)
zap.L().Info("cache retrieve status", zap.String("status", retrieveStatus.String()))
if err == nil {
@@ -373,7 +376,7 @@ func (q *querier) runPromQueries(ctx context.Context, params *v3.QueryRangeParam
channelResults <- channelResult{Err: nil, Name: queryName, Query: promQuery.Query, Series: mergedSeries}
// Cache the seriesList for future queries
if len(missedSeries) > 0 && !params.NoCache && q.cache != nil {
if len(missedSeries) > 0 && !params.NoCache && q.cache != nil && ok {
mergedSeriesData, err := json.Marshal(mergedSeries)
if err != nil {
zap.L().Error("error marshalling merged series", zap.Error(err))
@@ -546,3 +549,7 @@ func (q *querier) QueryRange(ctx context.Context, params *v3.QueryRangeParamsV3,
func (q *querier) QueriesExecuted() []string {
return q.queriesExecuted
}
func (q *querier) TimeRanges() [][]int {
return q.timeRanges
}

View File

@@ -951,3 +951,102 @@ func TestQueryRangeTimeShiftWithLimitAndCache(t *testing.T) {
}
}
}
func TestQueryRangeValueTypePromQL(t *testing.T) {
// There shouldn't be any caching for value panel type
params := []*v3.QueryRangeParamsV3{
{
Start: 1675115596722,
End: 1675115596722 + 120*60*1000,
Step: 5 * time.Minute.Milliseconds(),
CompositeQuery: &v3.CompositeQuery{
QueryType: v3.QueryTypePromQL,
PanelType: v3.PanelTypeValue,
PromQueries: map[string]*v3.PromQuery{
"A": {
Query: "signoz_calls_total",
},
},
},
},
{
Start: 1675115596722 + 60*60*1000,
End: 1675115596722 + 180*60*1000,
Step: 5 * time.Minute.Milliseconds(),
CompositeQuery: &v3.CompositeQuery{
QueryType: v3.QueryTypePromQL,
PanelType: v3.PanelTypeValue,
PromQueries: map[string]*v3.PromQuery{
"A": {
Query: "signoz_latency_bucket",
},
},
},
},
}
cache := inmemory.New(&inmemory.Options{TTL: 60 * time.Minute, CleanupInterval: 10 * time.Minute})
opts := QuerierOptions{
Cache: cache,
Reader: nil,
FluxInterval: 5 * time.Minute,
KeyGenerator: queryBuilder.NewKeyGenerator(),
TestingMode: true,
ReturnedSeries: []*v3.Series{
{
Labels: map[string]string{
"method": "GET",
"service_name": "test",
"__name__": "doesn't matter",
},
Points: []v3.Point{
{Timestamp: 1675115596722, Value: 1},
{Timestamp: 1675115596722 + 60*60*1000, Value: 2},
{Timestamp: 1675115596722 + 120*60*1000, Value: 3},
},
},
},
}
q := NewQuerier(opts)
expectedQueryAndTimeRanges := []struct {
query string
ranges []missInterval
}{
{
query: "signoz_calls_total",
ranges: []missInterval{
{start: 1675115596722, end: 1675115596722 + 120*60*1000},
},
},
{
query: "signoz_latency_bucket",
ranges: []missInterval{
{start: 1675115596722 + 60*60*1000, end: 1675115596722 + 180*60*1000},
},
},
}
for i, param := range params {
_, errByName, err := q.QueryRange(context.Background(), param, nil)
if err != nil {
t.Errorf("expected no error, got %s", err)
}
if len(errByName) > 0 {
t.Errorf("expected no error, got %v", errByName)
}
if !strings.Contains(q.QueriesExecuted()[i], expectedQueryAndTimeRanges[i].query) {
t.Errorf("expected query to contain %s, got %s", expectedQueryAndTimeRanges[i].query, q.QueriesExecuted()[i])
}
if len(q.TimeRanges()[i]) != 2 {
t.Errorf("expected time ranges to be %v, got %v", expectedQueryAndTimeRanges[i].ranges, q.TimeRanges()[i])
}
if q.TimeRanges()[i][0] != int(expectedQueryAndTimeRanges[i].ranges[0].start) {
t.Errorf("expected time ranges to be %v, got %v", expectedQueryAndTimeRanges[i].ranges, q.TimeRanges()[i])
}
if q.TimeRanges()[i][1] != int(expectedQueryAndTimeRanges[i].ranges[0].end) {
t.Errorf("expected time ranges to be %v, got %v", expectedQueryAndTimeRanges[i].ranges, q.TimeRanges()[i])
}
}
}

View File

@@ -18,7 +18,7 @@ import (
"go.uber.org/zap"
)
func prepareLogsQuery(ctx context.Context,
func prepareLogsQuery(_ context.Context,
start,
end int64,
builderQuery *v3.BuilderQuery,

View File

@@ -50,8 +50,10 @@ type querier struct {
// TODO(srikanthccv): remove this once we have a proper mock
testingMode bool
queriesExecuted []string
returnedSeries []*v3.Series
returnedErr error
// tuple of start and end time in milliseconds
timeRanges [][]int
returnedSeries []*v3.Series
returnedErr error
}
type QuerierOptions struct {
@@ -117,6 +119,7 @@ func (q *querier) execClickHouseQuery(ctx context.Context, query string) ([]*v3.
func (q *querier) execPromQuery(ctx context.Context, params *model.QueryRangeParams) ([]*v3.Series, error) {
q.queriesExecuted = append(q.queriesExecuted, params.Query)
if q.testingMode && q.reader == nil {
q.timeRanges = append(q.timeRanges, []int{int(params.Start.UnixMilli()), int(params.End.UnixMilli())})
return q.returnedSeries, q.returnedErr
}
promResult, _, err := q.reader.GetQueryRangeResult(ctx, params)
@@ -335,10 +338,10 @@ func (q *querier) runPromQueries(ctx context.Context, params *v3.QueryRangeParam
wg.Add(1)
go func(queryName string, promQuery *v3.PromQuery) {
defer wg.Done()
cacheKey := cacheKeys[queryName]
cacheKey, ok := cacheKeys[queryName]
var cachedData []byte
// Ensure NoCache is not set and cache is not nil
if !params.NoCache && q.cache != nil {
if !params.NoCache && q.cache != nil && ok {
data, retrieveStatus, err := q.cache.Retrieve(cacheKey, true)
zap.L().Info("cache retrieve status", zap.String("status", retrieveStatus.String()))
if err == nil {
@@ -366,7 +369,7 @@ func (q *querier) runPromQueries(ctx context.Context, params *v3.QueryRangeParam
channelResults <- channelResult{Err: nil, Name: queryName, Query: promQuery.Query, Series: mergedSeries}
// Cache the seriesList for future queries
if len(missedSeries) > 0 && !params.NoCache && q.cache != nil {
if len(missedSeries) > 0 && !params.NoCache && q.cache != nil && ok {
mergedSeriesData, err := json.Marshal(mergedSeries)
if err != nil {
zap.L().Error("error marshalling merged series", zap.Error(err))
@@ -539,3 +542,7 @@ func (q *querier) QueryRange(ctx context.Context, params *v3.QueryRangeParamsV3,
func (q *querier) QueriesExecuted() []string {
return q.queriesExecuted
}
func (q *querier) TimeRanges() [][]int {
return q.timeRanges
}

File diff suppressed because it is too large Load Diff

View File

@@ -110,4 +110,5 @@ type Querier interface {
// test helpers
QueriesExecuted() []string
TimeRanges() [][]int
}