Compare commits
9 Commits
main
...
fix/extern
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fe2811b05 | ||
|
|
720eff1aae | ||
|
|
36f8bf64f9 | ||
|
|
61ca5fad72 | ||
|
|
5557c44a3d | ||
|
|
f546b2217f | ||
|
|
226c0a435b | ||
|
|
c1efc80a1e | ||
|
|
5c5ba65acd |
@@ -422,30 +422,28 @@
|
||||
gap: 8px;
|
||||
.endpoint-meta-data-pill {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-slate-300);
|
||||
width: fit-content;
|
||||
overflow: hidden;
|
||||
box-sizing: content-box;
|
||||
.endpoint-meta-data-label {
|
||||
display: flex;
|
||||
padding: 6px 8px;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border-right: 1px solid var(--bg-slate-300);
|
||||
color: var(--text-vanilla-100);
|
||||
font-size: 14px;
|
||||
line-height: 18px; /* 128.571% */
|
||||
letter-spacing: -0.07px;
|
||||
padding: 6px 8px;
|
||||
background: var(--bg-slate-500);
|
||||
height: calc(100% - 12px);
|
||||
}
|
||||
|
||||
.endpoint-meta-data-value {
|
||||
display: flex;
|
||||
padding: 6px 8px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: var(--text-vanilla-400);
|
||||
background: var(--bg-slate-400);
|
||||
height: calc(100% - 12px);
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -453,9 +451,23 @@
|
||||
.endpoint-details-filters-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
height: 36px;
|
||||
box-sizing: content-box;
|
||||
.ant-select-selector {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.endpoint-details-filters-container-dropdown {
|
||||
width: 120px;
|
||||
border-right: 1px solid var(--bg-slate-500);
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.ant-select-single {
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.endpoint-details-filters-container-search {
|
||||
@@ -996,7 +1008,6 @@
|
||||
|
||||
.lightMode {
|
||||
.ant-drawer-header {
|
||||
border-bottom: 1px solid var(--bg-vanilla-400);
|
||||
background: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
@@ -1007,6 +1018,25 @@
|
||||
}
|
||||
|
||||
.domain-detail-drawer {
|
||||
.endpoint-details-card,
|
||||
.status-code-table-container,
|
||||
.endpoint-details-filters-container,
|
||||
.endpoint-details-filters-container-dropdown,
|
||||
.ant-radio-button-wrapper,
|
||||
.views-tabs-container,
|
||||
.ant-btn-default.tab,
|
||||
.tab::before,
|
||||
.endpoint-meta-data-pill,
|
||||
.endpoint-meta-data-label,
|
||||
.endpoints-table-container,
|
||||
.group-by-label,
|
||||
.ant-select-selector,
|
||||
.ant-drawer-header {
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
}
|
||||
.views-tabs .tab::before {
|
||||
background: var(--bg-vanilla-300);
|
||||
}
|
||||
.title {
|
||||
color: var(--text-ink-300);
|
||||
}
|
||||
@@ -1031,7 +1061,6 @@
|
||||
|
||||
.selected_view {
|
||||
background: var(--bg-vanilla-300);
|
||||
border: 1px solid var(--bg-slate-300);
|
||||
color: var(--text-ink-400);
|
||||
}
|
||||
|
||||
@@ -1160,7 +1189,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.top-services-content {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
.dependent-services-container {
|
||||
border: none;
|
||||
padding: 10px 12px;
|
||||
.top-services-item {
|
||||
display: flex;
|
||||
@@ -1187,11 +1220,31 @@
|
||||
}
|
||||
|
||||
.top-services-item-progress-bar {
|
||||
background-color: var(--bg-vanilla-300);
|
||||
border: 1px solid var(--bg-slate-300);
|
||||
background-color: var(--bg-vanilla-200);
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-table {
|
||||
.ant-table-thead > tr > th {
|
||||
color: var(--text-ink-300);
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
&,
|
||||
&:has(.top-services-item-latency) {
|
||||
background: var(--bg-vanilla-100);
|
||||
}
|
||||
color: var(--text-ink-300);
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr:hover > td {
|
||||
background: var(--bg-vanilla-200);
|
||||
}
|
||||
.table-row-dark {
|
||||
background: var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
|
||||
.top-services-item-percentage {
|
||||
color: var(--text-ink-300);
|
||||
|
||||
@@ -352,6 +352,7 @@ describe('API Monitoring Utils', () => {
|
||||
metric: {
|
||||
[SPAN_ATTRIBUTES.URL_PATH]: '/api/test',
|
||||
[SPAN_ATTRIBUTES.RESPONSE_STATUS_CODE]: '500',
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
status_message: 'Internal Server Error',
|
||||
},
|
||||
values: [[1000000100, '10']],
|
||||
@@ -385,6 +386,51 @@ describe('API Monitoring Utils', () => {
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should filter out rows with undefined metric', () => {
|
||||
// Arrange
|
||||
const inputData = [
|
||||
{
|
||||
metric: {
|
||||
[SPAN_ATTRIBUTES.URL_PATH]: '/api/valid',
|
||||
[SPAN_ATTRIBUTES.RESPONSE_STATUS_CODE]: '500',
|
||||
status_message: 'Internal Server Error',
|
||||
},
|
||||
values: [[1000000100, '10']],
|
||||
queryName: 'A',
|
||||
legend: 'Valid Row',
|
||||
},
|
||||
{
|
||||
metric: undefined,
|
||||
values: [[1000000200, '5']],
|
||||
queryName: 'B',
|
||||
legend: 'Invalid Row',
|
||||
},
|
||||
{
|
||||
metric: {
|
||||
[SPAN_ATTRIBUTES.URL_PATH]: '/api/another',
|
||||
[SPAN_ATTRIBUTES.RESPONSE_STATUS_CODE]: '404',
|
||||
status_message: 'Not Found',
|
||||
},
|
||||
values: [[1000000300, '3']],
|
||||
queryName: 'C',
|
||||
legend: 'Another Valid Row',
|
||||
},
|
||||
];
|
||||
|
||||
// Act
|
||||
const result = formatTopErrorsDataForTable(
|
||||
inputData as TopErrorsResponseRow[],
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeDefined();
|
||||
expect(result.length).toBe(2); // Should only include 2 valid rows
|
||||
expect(result[0].endpointName).toBe('/api/valid');
|
||||
expect(result[0].statusCode).toBe('500');
|
||||
expect(result[1].endpointName).toBe('/api/another');
|
||||
expect(result[1].statusCode).toBe('404');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTopErrorsColumnsConfig', () => {
|
||||
|
||||
@@ -1266,31 +1266,33 @@ export const formatTopErrorsDataForTable = (
|
||||
): TopErrorsTableRowData[] => {
|
||||
if (!data) return [];
|
||||
|
||||
return data.map((row) => ({
|
||||
key: v4(),
|
||||
endpointName:
|
||||
row.metric[SPAN_ATTRIBUTES.URL_PATH] === 'n/a' ||
|
||||
row.metric[SPAN_ATTRIBUTES.URL_PATH] === undefined
|
||||
? '-'
|
||||
: row.metric[SPAN_ATTRIBUTES.URL_PATH],
|
||||
statusCode:
|
||||
row.metric[SPAN_ATTRIBUTES.RESPONSE_STATUS_CODE] === 'n/a' ||
|
||||
row.metric[SPAN_ATTRIBUTES.RESPONSE_STATUS_CODE] === undefined
|
||||
? '-'
|
||||
: row.metric[SPAN_ATTRIBUTES.RESPONSE_STATUS_CODE],
|
||||
statusMessage:
|
||||
row.metric.status_message === 'n/a' ||
|
||||
row.metric.status_message === undefined
|
||||
? '-'
|
||||
: row.metric.status_message,
|
||||
count:
|
||||
row.values &&
|
||||
row.values[0] &&
|
||||
row.values[0][1] !== undefined &&
|
||||
row.values[0][1] !== 'n/a'
|
||||
? row.values[0][1]
|
||||
: '-',
|
||||
}));
|
||||
return data
|
||||
.filter((row) => row.metric)
|
||||
.map((row) => ({
|
||||
key: v4(),
|
||||
endpointName:
|
||||
row.metric[SPAN_ATTRIBUTES.URL_PATH] === 'n/a' ||
|
||||
row.metric[SPAN_ATTRIBUTES.URL_PATH] === undefined
|
||||
? '-'
|
||||
: row.metric[SPAN_ATTRIBUTES.URL_PATH],
|
||||
statusCode:
|
||||
row.metric[SPAN_ATTRIBUTES.RESPONSE_STATUS_CODE] === 'n/a' ||
|
||||
row.metric[SPAN_ATTRIBUTES.RESPONSE_STATUS_CODE] === undefined
|
||||
? '-'
|
||||
: row.metric[SPAN_ATTRIBUTES.RESPONSE_STATUS_CODE],
|
||||
statusMessage:
|
||||
row.metric.status_message === 'n/a' ||
|
||||
row.metric.status_message === undefined
|
||||
? '-'
|
||||
: row.metric.status_message,
|
||||
count:
|
||||
row.values &&
|
||||
row.values[0] &&
|
||||
row.values[0][1] !== undefined &&
|
||||
row.values[0][1] !== 'n/a'
|
||||
? row.values[0][1]
|
||||
: '-',
|
||||
}));
|
||||
};
|
||||
|
||||
export const getTopErrorsCoRelationQueryFilters = (
|
||||
|
||||
@@ -198,7 +198,12 @@ function GeneralSettings({
|
||||
);
|
||||
|
||||
const s3Enabled = useMemo(
|
||||
() => !!find(availableDisks, (disks: IDiskType) => disks?.type === 's3'),
|
||||
() =>
|
||||
!!find(
|
||||
availableDisks,
|
||||
(disks: IDiskType) =>
|
||||
disks?.type === 's3' || disks?.type === 'ObjectStorage',
|
||||
),
|
||||
[availableDisks],
|
||||
);
|
||||
|
||||
@@ -348,11 +353,17 @@ function GeneralSettings({
|
||||
|
||||
try {
|
||||
if (type === 'logs') {
|
||||
// Only send S3 values if user has specified a duration
|
||||
const s3RetentionDays =
|
||||
apiCallS3Retention && apiCallS3Retention > 0
|
||||
? apiCallS3Retention / 24
|
||||
: 0;
|
||||
|
||||
await setRetentionApiV2({
|
||||
type,
|
||||
defaultTTLDays: apiCallTotalRetention ? apiCallTotalRetention / 24 : -1, // convert Hours to days
|
||||
coldStorageVolume: '',
|
||||
coldStorageDuration: 0,
|
||||
coldStorageVolume: s3RetentionDays > 0 ? 's3' : '',
|
||||
coldStorageDuration: s3RetentionDays,
|
||||
ttlConditions: [],
|
||||
});
|
||||
} else {
|
||||
@@ -524,6 +535,7 @@ function GeneralSettings({
|
||||
value: logsS3RetentionPeriod,
|
||||
setValue: setLogsS3RetentionPeriod,
|
||||
hide: !s3Enabled,
|
||||
isS3Field: true,
|
||||
},
|
||||
],
|
||||
save: {
|
||||
@@ -577,6 +589,7 @@ function GeneralSettings({
|
||||
retentionValue={retentionField.value}
|
||||
setRetentionValue={retentionField.setValue}
|
||||
hide={!!retentionField.hide}
|
||||
isS3Field={'isS3Field' in retentionField && retentionField.isS3Field}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
@@ -32,11 +33,31 @@ function Retention({
|
||||
setRetentionValue,
|
||||
text,
|
||||
hide,
|
||||
isS3Field = false,
|
||||
}: RetentionProps): JSX.Element | null {
|
||||
// Filter available units based on type and field
|
||||
const availableUnits = useMemo(
|
||||
() =>
|
||||
TimeUnits.filter((option) => {
|
||||
if (type === 'logs') {
|
||||
// For S3 cold storage fields: only allow Days
|
||||
if (isS3Field) {
|
||||
return option.value === TimeUnitsValues.day;
|
||||
}
|
||||
// For total retention: allow Days and Months (not Hours)
|
||||
return option.value !== TimeUnitsValues.hr;
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
[type, isS3Field],
|
||||
);
|
||||
|
||||
// Convert the hours value using only the available units
|
||||
const {
|
||||
value: initialValue,
|
||||
timeUnitValue: initialTimeUnitValue,
|
||||
} = convertHoursValueToRelevantUnit(Number(retentionValue));
|
||||
} = convertHoursValueToRelevantUnit(Number(retentionValue), availableUnits);
|
||||
|
||||
const [selectedTimeUnit, setSelectTimeUnit] = useState(initialTimeUnitValue);
|
||||
const [selectedValue, setSelectedValue] = useState<number | null>(
|
||||
initialValue,
|
||||
@@ -53,29 +74,27 @@ function Retention({
|
||||
if (!interacted.current) setSelectTimeUnit(initialTimeUnitValue);
|
||||
}, [initialTimeUnitValue]);
|
||||
|
||||
const menuItems = TimeUnits.filter((option) =>
|
||||
type === 'logs' ? option.value !== TimeUnitsValues.hr : true,
|
||||
).map((option) => (
|
||||
const menuItems = availableUnits.map((option) => (
|
||||
<Option key={option.value} value={option.value}>
|
||||
{option.key}
|
||||
</Option>
|
||||
));
|
||||
|
||||
const currentSelectedOption = (option: SettingPeriod): void => {
|
||||
const selectedValue = find(TimeUnits, (e) => e.value === option)?.value;
|
||||
const selectedValue = find(availableUnits, (e) => e.value === option)?.value;
|
||||
if (selectedValue) setSelectTimeUnit(selectedValue);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const inverseMultiplier = find(
|
||||
TimeUnits,
|
||||
availableUnits,
|
||||
(timeUnit) => timeUnit.value === selectedTimeUnit,
|
||||
)?.multiplier;
|
||||
if (!selectedValue) setRetentionValue(null);
|
||||
if (selectedValue && inverseMultiplier) {
|
||||
setRetentionValue(selectedValue * (1 / inverseMultiplier));
|
||||
}
|
||||
}, [selectedTimeUnit, selectedValue, setRetentionValue]);
|
||||
}, [selectedTimeUnit, selectedValue, setRetentionValue, availableUnits]);
|
||||
|
||||
const onChangeHandler = (
|
||||
e: ChangeEvent<HTMLInputElement>,
|
||||
@@ -134,6 +153,10 @@ interface RetentionProps {
|
||||
text: string;
|
||||
setRetentionValue: Dispatch<SetStateAction<number | null>>;
|
||||
hide: boolean;
|
||||
isS3Field?: boolean;
|
||||
}
|
||||
|
||||
Retention.defaultProps = {
|
||||
isS3Field: false,
|
||||
};
|
||||
export default Retention;
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
import setRetentionApiV2 from 'api/settings/setRetentionV2';
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
userEvent,
|
||||
waitFor,
|
||||
} from 'tests/test-utils';
|
||||
import { IDiskType } from 'types/api/disks/getDisks';
|
||||
import {
|
||||
PayloadPropsLogs,
|
||||
PayloadPropsMetrics,
|
||||
PayloadPropsTraces,
|
||||
} from 'types/api/settings/getRetention';
|
||||
|
||||
import GeneralSettings from '../GeneralSettings';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('api/settings/setRetentionV2');
|
||||
|
||||
const mockNotifications = {
|
||||
error: jest.fn(),
|
||||
success: jest.fn(),
|
||||
};
|
||||
|
||||
jest.mock('hooks/useNotifications', () => ({
|
||||
useNotifications: (): { notifications: typeof mockNotifications } => ({
|
||||
notifications: mockNotifications,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useComponentPermission', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(() => [true]),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useGetTenantLicense', () => ({
|
||||
useGetTenantLicense: (): { isCloudUser: boolean } => ({
|
||||
isCloudUser: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('container/GeneralSettingsCloud', () => ({
|
||||
__esModule: true,
|
||||
default: (): null => null,
|
||||
}));
|
||||
|
||||
// Mock data
|
||||
const mockMetricsRetention: PayloadPropsMetrics = {
|
||||
metrics_ttl_duration_hrs: 168,
|
||||
metrics_move_ttl_duration_hrs: -1,
|
||||
status: '',
|
||||
};
|
||||
|
||||
const mockTracesRetention: PayloadPropsTraces = {
|
||||
traces_ttl_duration_hrs: 168,
|
||||
traces_move_ttl_duration_hrs: -1,
|
||||
status: '',
|
||||
};
|
||||
|
||||
const mockLogsRetentionWithS3: PayloadPropsLogs = {
|
||||
version: 'v2',
|
||||
default_ttl_days: 30,
|
||||
logs_ttl_duration_hrs: 720, // 30 days in hours
|
||||
logs_move_ttl_duration_hrs: 240, // 10 days in hours
|
||||
status: '',
|
||||
};
|
||||
|
||||
const mockLogsRetentionWithoutS3: PayloadPropsLogs = {
|
||||
version: 'v2',
|
||||
default_ttl_days: 30,
|
||||
logs_ttl_duration_hrs: 720,
|
||||
logs_move_ttl_duration_hrs: -1,
|
||||
status: '',
|
||||
};
|
||||
|
||||
const mockDisksWithS3: IDiskType[] = [
|
||||
{
|
||||
name: 'default',
|
||||
type: 's3',
|
||||
},
|
||||
];
|
||||
|
||||
const mockDisksWithObjectStorage: IDiskType[] = [
|
||||
{
|
||||
name: 'default',
|
||||
type: 'ObjectStorage',
|
||||
},
|
||||
];
|
||||
|
||||
const mockDisksWithoutS3: IDiskType[] = [
|
||||
{
|
||||
name: 'default',
|
||||
type: 'local',
|
||||
},
|
||||
];
|
||||
|
||||
describe('GeneralSettings - S3 Logs Retention', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(setRetentionApiV2 as jest.Mock).mockResolvedValue({
|
||||
httpStatusCode: 200,
|
||||
data: { message: 'success' },
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test 1: S3 Enabled - Only Days in Dropdown', () => {
|
||||
it('should show only Days option for S3 retention and send correct API payload', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(
|
||||
<GeneralSettings
|
||||
metricsTtlValuesPayload={mockMetricsRetention}
|
||||
tracesTtlValuesPayload={mockTracesRetention}
|
||||
logsTtlValuesPayload={mockLogsRetentionWithS3}
|
||||
getAvailableDiskPayload={mockDisksWithS3}
|
||||
metricsTtlValuesRefetch={jest.fn()}
|
||||
tracesTtlValuesRefetch={jest.fn()}
|
||||
logsTtlValuesRefetch={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Find the Logs card
|
||||
const logsCard = screen.getByText('Logs').closest('.ant-card');
|
||||
expect(logsCard).toBeInTheDocument();
|
||||
|
||||
// Find all inputs in the Logs card - there should be 2 (total retention + S3)
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
const inputs = logsCard?.querySelectorAll('input[type="text"]');
|
||||
expect(inputs).toHaveLength(2);
|
||||
|
||||
// The second input is the S3 retention field
|
||||
const s3Input = inputs?.[1] as HTMLInputElement;
|
||||
|
||||
// Find the S3 dropdown (next sibling of the S3 input)
|
||||
const s3Dropdown = s3Input?.nextElementSibling?.querySelector(
|
||||
'.ant-select-selector',
|
||||
) as HTMLElement;
|
||||
expect(s3Dropdown).toBeInTheDocument();
|
||||
|
||||
// Click the S3 dropdown to open it
|
||||
fireEvent.mouseDown(s3Dropdown);
|
||||
|
||||
// Wait for dropdown options to appear and verify only "Days" is available
|
||||
await waitFor(() => {
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
const dropdownOptions = document.querySelectorAll('.ant-select-item');
|
||||
expect(dropdownOptions).toHaveLength(1);
|
||||
expect(dropdownOptions[0]).toHaveTextContent('Days');
|
||||
});
|
||||
|
||||
// Close dropdown
|
||||
fireEvent.click(document.body);
|
||||
|
||||
// Change S3 retention value to 5 days
|
||||
await user.clear(s3Input);
|
||||
await user.type(s3Input, '5');
|
||||
|
||||
// Find the save button in the Logs card
|
||||
const buttons = logsCard?.querySelectorAll('button[type="button"]');
|
||||
// The primary button should be the save button
|
||||
const saveButton = Array.from(buttons || []).find((btn) =>
|
||||
btn.className.includes('ant-btn-primary'),
|
||||
) as HTMLButtonElement;
|
||||
|
||||
expect(saveButton).toBeInTheDocument();
|
||||
|
||||
// Wait for button to be enabled (it should enable after value changes)
|
||||
await waitFor(() => {
|
||||
expect(saveButton).not.toBeDisabled();
|
||||
});
|
||||
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
// Wait for modal to appear
|
||||
const modal = await screen.findByRole('dialog');
|
||||
expect(modal).toBeInTheDocument();
|
||||
|
||||
// Click OK button
|
||||
const okButton = await screen.findByRole('button', { name: /ok/i });
|
||||
fireEvent.click(okButton);
|
||||
|
||||
// Verify API was called with correct payload
|
||||
await waitFor(() => {
|
||||
expect(setRetentionApiV2).toHaveBeenCalledWith({
|
||||
type: 'logs',
|
||||
defaultTTLDays: 30,
|
||||
coldStorageVolume: 's3',
|
||||
coldStorageDuration: 5,
|
||||
ttlConditions: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should recognize ObjectStorage disk type as S3 enabled', async () => {
|
||||
render(
|
||||
<GeneralSettings
|
||||
metricsTtlValuesPayload={mockMetricsRetention}
|
||||
tracesTtlValuesPayload={mockTracesRetention}
|
||||
logsTtlValuesPayload={mockLogsRetentionWithS3}
|
||||
getAvailableDiskPayload={mockDisksWithObjectStorage}
|
||||
metricsTtlValuesRefetch={jest.fn()}
|
||||
tracesTtlValuesRefetch={jest.fn()}
|
||||
logsTtlValuesRefetch={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Verify S3 field is visible
|
||||
const logsCard = screen.getByText('Logs').closest('.ant-card');
|
||||
const inputs = logsCard?.querySelectorAll('input[type="text"]');
|
||||
expect(inputs).toHaveLength(2); // Total + S3
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test 2: S3 Disabled - Field Hidden', () => {
|
||||
it('should hide S3 retention field and send empty S3 values to API', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(
|
||||
<GeneralSettings
|
||||
metricsTtlValuesPayload={mockMetricsRetention}
|
||||
tracesTtlValuesPayload={mockTracesRetention}
|
||||
logsTtlValuesPayload={mockLogsRetentionWithoutS3}
|
||||
getAvailableDiskPayload={mockDisksWithoutS3}
|
||||
metricsTtlValuesRefetch={jest.fn()}
|
||||
tracesTtlValuesRefetch={jest.fn()}
|
||||
logsTtlValuesRefetch={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Find the Logs card
|
||||
const logsCard = screen.getByText('Logs').closest('.ant-card');
|
||||
expect(logsCard).toBeInTheDocument();
|
||||
|
||||
// Only 1 input should be visible (total retention, no S3)
|
||||
const inputs = logsCard?.querySelectorAll('input[type="text"]');
|
||||
expect(inputs).toHaveLength(1);
|
||||
|
||||
// Change total retention value
|
||||
const totalInput = inputs?.[0] as HTMLInputElement;
|
||||
|
||||
// First, change the dropdown to Days (it defaults to Months)
|
||||
const totalDropdown = totalInput?.nextElementSibling?.querySelector(
|
||||
'.ant-select-selector',
|
||||
) as HTMLElement;
|
||||
await user.click(totalDropdown);
|
||||
|
||||
// Wait for dropdown options to appear
|
||||
await waitFor(() => {
|
||||
const options = document.querySelectorAll('.ant-select-item');
|
||||
expect(options.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// Find and click the Days option
|
||||
const options = document.querySelectorAll('.ant-select-item');
|
||||
const daysOption = Array.from(options).find((opt) =>
|
||||
opt.textContent?.includes('Days'),
|
||||
);
|
||||
expect(daysOption).toBeInTheDocument();
|
||||
await user.click(daysOption as HTMLElement);
|
||||
|
||||
// Now change the value
|
||||
await user.clear(totalInput);
|
||||
await user.type(totalInput, '60');
|
||||
|
||||
// Find the save button
|
||||
const buttons = logsCard?.querySelectorAll('button[type="button"]');
|
||||
const saveButton = Array.from(buttons || []).find((btn) =>
|
||||
btn.className.includes('ant-btn-primary'),
|
||||
) as HTMLButtonElement;
|
||||
|
||||
expect(saveButton).toBeInTheDocument();
|
||||
|
||||
// Wait for button to be enabled (ensures all state updates have settled)
|
||||
await waitFor(() => {
|
||||
expect(saveButton).not.toBeDisabled();
|
||||
});
|
||||
|
||||
// Click save button
|
||||
await user.click(saveButton);
|
||||
|
||||
// Wait for modal to appear
|
||||
const okButton = await screen.findByRole('button', { name: /ok/i });
|
||||
expect(okButton).toBeInTheDocument();
|
||||
|
||||
// Click OK button
|
||||
await user.click(okButton);
|
||||
|
||||
// Verify API was called with empty S3 values (60 days)
|
||||
await waitFor(() => {
|
||||
expect(setRetentionApiV2).toHaveBeenCalledWith({
|
||||
type: 'logs',
|
||||
defaultTTLDays: 60,
|
||||
coldStorageVolume: '',
|
||||
coldStorageDuration: 0,
|
||||
ttlConditions: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test 3: Save & Reload - Correct Display', () => {
|
||||
it('should display retention values correctly after converting from hours', () => {
|
||||
render(
|
||||
<GeneralSettings
|
||||
metricsTtlValuesPayload={mockMetricsRetention}
|
||||
tracesTtlValuesPayload={mockTracesRetention}
|
||||
logsTtlValuesPayload={mockLogsRetentionWithS3}
|
||||
getAvailableDiskPayload={mockDisksWithS3}
|
||||
metricsTtlValuesRefetch={jest.fn()}
|
||||
tracesTtlValuesRefetch={jest.fn()}
|
||||
logsTtlValuesRefetch={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Find the Logs card
|
||||
const logsCard = screen.getByText('Logs').closest('.ant-card');
|
||||
const inputs = logsCard?.querySelectorAll('input[type="text"]');
|
||||
|
||||
// Total retention: 720 hours = 30 days = 1 month (displays as 1 Month)
|
||||
const totalInput = inputs?.[0] as HTMLInputElement;
|
||||
expect(totalInput.value).toBe('1');
|
||||
|
||||
// S3 retention: 240 hours = 10 days (displays as 10 Days - only Days allowed)
|
||||
const s3Input = inputs?.[1] as HTMLInputElement;
|
||||
expect(s3Input.value).toBe('10');
|
||||
|
||||
// Verify dropdowns: total shows Months, S3 shows Days
|
||||
const dropdowns = logsCard?.querySelectorAll('.ant-select-selection-item');
|
||||
expect(dropdowns?.[0]).toHaveTextContent('Months');
|
||||
expect(dropdowns?.[1]).toHaveTextContent('Days');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -34,12 +34,22 @@ interface ITimeUnitConversion {
|
||||
value: number;
|
||||
timeUnitValue: SettingPeriod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts hours value to the most relevant unit from the available units.
|
||||
* @param value - The value in hours
|
||||
* @param availableUnits - Optional array of available time units to consider. If not provided, all units are considered.
|
||||
* @returns The converted value and the selected time unit
|
||||
*/
|
||||
export const convertHoursValueToRelevantUnit = (
|
||||
value: number,
|
||||
availableUnits?: ITimeUnit[],
|
||||
): ITimeUnitConversion => {
|
||||
if (value)
|
||||
for (let idx = TimeUnits.length - 1; idx >= 0; idx -= 1) {
|
||||
const timeUnit = TimeUnits[idx];
|
||||
const unitsToConsider = availableUnits?.length ? availableUnits : TimeUnits;
|
||||
|
||||
if (value) {
|
||||
for (let idx = unitsToConsider.length - 1; idx >= 0; idx -= 1) {
|
||||
const timeUnit = unitsToConsider[idx];
|
||||
const convertedValue = timeUnit.multiplier * value;
|
||||
|
||||
if (
|
||||
@@ -49,7 +59,10 @@ export const convertHoursValueToRelevantUnit = (
|
||||
return { value: convertedValue, timeUnitValue: timeUnit.value };
|
||||
}
|
||||
}
|
||||
return { value, timeUnitValue: TimeUnits[0].value };
|
||||
}
|
||||
|
||||
// Fallback to the first available unit
|
||||
return { value, timeUnitValue: unitsToConsider[0].value };
|
||||
};
|
||||
|
||||
export const convertHoursValueToRelevantUnitString = (
|
||||
|
||||
Reference in New Issue
Block a user