diff --git a/common/constants.ts b/common/constants.ts
index d1efcbe8..765265a4 100644
--- a/common/constants.ts
+++ b/common/constants.ts
@@ -18,10 +18,7 @@ export const GROUP_BY = 'Group by';
export const AVERAGE_LATENCY = 'Average Latency';
export const AVERAGE_CPU_TIME = 'Average CPU Time';
export const AVERAGE_MEMORY_USAGE = 'Average Memory Usage';
-/*
- * Copyright OpenSearch Contributors
- * SPDX-License-Identifier: Apache-2.0
- */
+
export const MetricType = {
LATENCY: 'latency',
CPU: 'cpu',
@@ -69,3 +66,18 @@ export const DEFAULT_TIME_UNIT = TIME_UNIT.MINUTES;
export const DEFAULT_GROUP_BY = 'none';
export const DEFAULT_EXPORTER_TYPE = EXPORTER_TYPE.localIndex;
export const DEFAULT_DELETE_AFTER_DAYS = '7';
+// Validation constants
+export const VALIDATION_LIMITS = {
+ TOP_N_SIZE: {
+ MIN: 1,
+ MAX: 100,
+ },
+ WINDOW_SIZE_HOURS: {
+ MIN: 1,
+ MAX: 24,
+ },
+ DELETE_AFTER_DAYS: {
+ MIN: 1,
+ MAX: 180,
+ },
+};
diff --git a/public/pages/Configuration/Configuration.test.tsx b/public/pages/Configuration/Configuration.test.tsx
index d795e8f9..59337d46 100644
--- a/public/pages/Configuration/Configuration.test.tsx
+++ b/public/pages/Configuration/Configuration.test.tsx
@@ -9,6 +9,13 @@ import '@testing-library/jest-dom/extend-expect';
import { MemoryRouter } from 'react-router-dom';
import Configuration from './Configuration';
import { DataSourceContext } from '../TopNQueries/TopNQueries';
+import { TIME_UNITS_TEXT, EXPORTER_TYPE } from '../../../common/constants';
+import {
+ validateTopNSize,
+ validateWindowSize,
+ validateDeleteAfterDays,
+ validateConfiguration,
+} from './configurationValidation';
const mockConfigInfo = jest.fn();
const mockCoreStart = {
@@ -145,4 +152,183 @@ describe('Configuration Component', () => {
fireEvent.click(screen.getByText('Cancel'));
expect(getTopNSizeConfiguration()[0]).toHaveValue(5); // Resets to initial value
});
+
+ describe('Validation Logic Tests', () => {
+ describe('TopN Size Validation', () => {
+ it('should hide Save button when topN size is less than 1', () => {
+ renderConfiguration();
+ fireEvent.change(getTopNSizeConfiguration()[0], { target: { value: '0' } });
+ expect(screen.queryByText('Save')).not.toBeInTheDocument();
+ });
+
+ it('should hide Save button when topN size is greater than 100', () => {
+ renderConfiguration();
+ fireEvent.change(getTopNSizeConfiguration()[0], { target: { value: '101' } });
+ expect(screen.queryByText('Save')).not.toBeInTheDocument();
+ });
+
+ it('should show Save button when topN size is within valid range (1-100)', () => {
+ renderConfiguration();
+ fireEvent.change(getTopNSizeConfiguration()[0], { target: { value: '50' } });
+ expect(screen.getByText('Save')).toBeInTheDocument();
+ });
+ });
+
+ describe('Window Size Validation - Minutes', () => {
+ it('should hide Save button when window size is empty for minutes', () => {
+ renderConfiguration();
+ fireEvent.change(getTopNSizeConfiguration()[0], { target: { value: '6' } });
+ fireEvent.change(getWindowSizeConfigurations()[2], { target: { value: 'MINUTES' } });
+ fireEvent.change(getWindowSizeConfigurations()[1], { target: { value: '' } });
+ expect(screen.queryByText('Save')).not.toBeInTheDocument();
+ });
+
+ it('should hide Save button when window size is NaN for minutes', () => {
+ renderConfiguration();
+ fireEvent.change(getTopNSizeConfiguration()[0], { target: { value: '6' } });
+ fireEvent.change(getWindowSizeConfigurations()[2], { target: { value: 'MINUTES' } });
+ fireEvent.change(getWindowSizeConfigurations()[1], { target: { value: 'invalid' } });
+ expect(screen.queryByText('Save')).not.toBeInTheDocument();
+ });
+
+ it('should show Save button when window size is valid for minutes', () => {
+ renderConfiguration();
+ fireEvent.change(getTopNSizeConfiguration()[0], { target: { value: '6' } });
+ fireEvent.change(getWindowSizeConfigurations()[2], { target: { value: 'MINUTES' } });
+ fireEvent.change(getWindowSizeConfigurations()[1], { target: { value: '5' } });
+ expect(screen.getByText('Save')).toBeInTheDocument();
+ });
+ });
+
+ describe('Window Size Validation - Hours', () => {
+ it('should show Save button when window size is within valid range (1-24) for hours', () => {
+ renderConfiguration();
+ fireEvent.change(getTopNSizeConfiguration()[0], { target: { value: '6' } });
+ fireEvent.change(getWindowSizeConfigurations()[2], { target: { value: 'HOURS' } });
+ fireEvent.change(getWindowSizeConfigurations()[1], { target: { value: '12' } });
+ expect(screen.getByText('Save')).toBeInTheDocument();
+ });
+ });
+
+ describe('Delete After Days Validation', () => {
+ it('should hide Save button when delete after days is less than 1 for local index', () => {
+ renderConfiguration();
+ fireEvent.change(getTopNSizeConfiguration()[0], { target: { value: '6' } });
+ const deleteAfterField = getTopNSizeConfiguration()[1];
+ fireEvent.change(deleteAfterField, { target: { value: '0' } });
+ expect(screen.queryByText('Save')).not.toBeInTheDocument();
+ });
+
+ it('should hide Save button when delete after days is greater than 180 for local index', () => {
+ renderConfiguration();
+ fireEvent.change(getTopNSizeConfiguration()[0], { target: { value: '6' } });
+ const deleteAfterField = getTopNSizeConfiguration()[1];
+ fireEvent.change(deleteAfterField, { target: { value: '181' } });
+ expect(screen.queryByText('Save')).not.toBeInTheDocument();
+ });
+
+ it('should show Save button when delete after days is within valid range (1-180) for local index', () => {
+ renderConfiguration();
+ fireEvent.change(getTopNSizeConfiguration()[0], { target: { value: '6' } });
+ const deleteAfterField = getTopNSizeConfiguration()[1];
+ fireEvent.change(deleteAfterField, { target: { value: '90' } });
+ expect(screen.getByText('Save')).toBeInTheDocument();
+ });
+
+ it('should show Save button when exporter is changed to none', () => {
+ renderConfiguration();
+ fireEvent.change(getTopNSizeConfiguration()[0], { target: { value: '6' } });
+ fireEvent.change(screen.getByDisplayValue('Local Index'), { target: { value: 'none' } });
+ expect(screen.getByText('Save')).toBeInTheDocument();
+ });
+ });
+
+ describe('Combined Validation Scenarios', () => {
+ it('should hide Save button when multiple validation rules fail', () => {
+ renderConfiguration();
+ fireEvent.change(getTopNSizeConfiguration()[0], { target: { value: '101' } });
+ expect(screen.queryByText('Save')).not.toBeInTheDocument();
+ });
+
+ it('should show Save button when all validation rules pass', () => {
+ renderConfiguration();
+ fireEvent.change(getTopNSizeConfiguration()[0], { target: { value: '25' } });
+ expect(screen.getByText('Save')).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('Validation Utility Functions', () => {
+ describe('validateTopNSize', () => {
+ it('should return false for values less than 1', () => {
+ expect(validateTopNSize('0')).toBe(false);
+ expect(validateTopNSize('-1')).toBe(false);
+ });
+
+ it('should return false for values greater than 100', () => {
+ expect(validateTopNSize('101')).toBe(false);
+ expect(validateTopNSize('200')).toBe(false);
+ });
+
+ it('should return true for valid values (1-100)', () => {
+ expect(validateTopNSize('1')).toBe(true);
+ expect(validateTopNSize('50')).toBe(true);
+ expect(validateTopNSize('100')).toBe(true);
+ });
+
+ it('should return false for non-numeric strings', () => {
+ expect(validateTopNSize('abc')).toBe(false);
+ expect(validateTopNSize('')).toBe(false);
+ expect(validateTopNSize('10.5')).toBe(false);
+ });
+ });
+
+ describe('validateWindowSize', () => {
+ it('should validate minutes correctly', () => {
+ const minutesUnit = TIME_UNITS_TEXT[0].value;
+ expect(validateWindowSize('', minutesUnit)).toBe(false);
+ expect(validateWindowSize('abc', minutesUnit)).toBe(false);
+ expect(validateWindowSize('1', minutesUnit)).toBe(true);
+ expect(validateWindowSize('30', minutesUnit)).toBe(true);
+ });
+
+ it('should validate hours correctly', () => {
+ const hoursUnit = TIME_UNITS_TEXT[1].value;
+ expect(validateWindowSize('0', hoursUnit)).toBe(false);
+ expect(validateWindowSize('25', hoursUnit)).toBe(false);
+ expect(validateWindowSize('1', hoursUnit)).toBe(true);
+ expect(validateWindowSize('24', hoursUnit)).toBe(true);
+ });
+ });
+
+ describe('validateDeleteAfterDays', () => {
+ it('should validate local index correctly', () => {
+ const localIndexType = EXPORTER_TYPE.localIndex;
+ expect(validateDeleteAfterDays('0', localIndexType)).toBe(false);
+ expect(validateDeleteAfterDays('181', localIndexType)).toBe(false);
+ expect(validateDeleteAfterDays('1', localIndexType)).toBe(true);
+ expect(validateDeleteAfterDays('180', localIndexType)).toBe(true);
+ });
+
+ it('should return true for non-local index', () => {
+ const noneType = EXPORTER_TYPE.none;
+ expect(validateDeleteAfterDays('0', noneType)).toBe(true);
+ expect(validateDeleteAfterDays('abc', noneType)).toBe(true);
+ });
+ });
+
+ describe('validateConfiguration', () => {
+ it('should return false when any validation fails', () => {
+ expect(validateConfiguration('101', '5', 'MINUTES', '30', EXPORTER_TYPE.localIndex)).toBe(
+ false
+ );
+ expect(validateConfiguration('50', '', 'MINUTES', '30', EXPORTER_TYPE.localIndex)).toBe(
+ false
+ );
+ expect(validateConfiguration('50', '5', 'MINUTES', '181', EXPORTER_TYPE.localIndex)).toBe(
+ false
+ );
+ });
+ });
+ });
});
diff --git a/public/pages/Configuration/Configuration.tsx b/public/pages/Configuration/Configuration.tsx
index 79f9079e..108b827c 100644
--- a/public/pages/Configuration/Configuration.tsx
+++ b/public/pages/Configuration/Configuration.tsx
@@ -43,6 +43,7 @@ import {
} from '../../../common/constants';
import { QueryInsightsDataSourceMenu } from '../../components/DataSourcePicker';
import { QueryInsightsDashboardsPluginStartDependencies } from '../../types';
+import { validateConfiguration } from './configurationValidation';
const Configuration = ({
latencySettings,
@@ -198,6 +199,9 @@ const Configuration = ({
);
const WindowChoice = time === TIME_UNITS_TEXT[0].value ? MinutesBox : HoursBox;
+ const isLocalIndex = exporterType === EXPORTER_TYPE.localIndex;
+ const parsedDeleteAfter = parseInt(deleteAfterDays, 10);
+ const isDeleteAfterValid = !isLocalIndex || (parsedDeleteAfter >= 1 && parsedDeleteAfter <= 180);
const isChanged =
isEnabled !== metricSettingsMap[metric].isEnabled ||
@@ -208,13 +212,7 @@ const Configuration = ({
exporterType !== dataRetentionSettingMap.dataRetention.exporterType ||
deleteAfterDays !== dataRetentionSettingMap.dataRetention.deleteAfterDays;
- const isValid = (() => {
- const nVal = parseInt(topNSize, 10);
- if (nVal < 1 || nVal > 100) return false;
- if (time === TIME_UNITS_TEXT[0].value) return true;
- const windowVal = parseInt(windowSize, 10);
- return windowVal >= 1 && windowVal <= 24;
- })();
+ const isValid = validateConfiguration(topNSize, windowSize, time, deleteAfterDays, exporterType);
const formRowPadding = { padding: '0px 0px 20px' };
const enabledSymb =