diff --git a/redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/cards/ConfigurationCard.spec.tsx b/redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/cards/ConfigurationCard.spec.tsx index 387964297f..2493cc8e89 100644 --- a/redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/cards/ConfigurationCard.spec.tsx +++ b/redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/cards/ConfigurationCard.spec.tsx @@ -7,7 +7,20 @@ import ConfigurationCard, { ConfigurationCardProps } from './ConfigurationCard' const mockedProps = mock() +const mockUseConfigurationState = jest.fn() +jest.mock('./hooks/useConfigurationState', () => ({ + useConfigurationState: () => mockUseConfigurationState(), +})) + describe('ConfigurationCard', () => { + beforeEach(() => { + mockUseConfigurationState.mockReturnValue({ + hasChanges: false, + isValid: true, + configValidationErrors: [], + }) + }) + it('should render with correct title', () => { render() @@ -49,4 +62,98 @@ describe('ConfigurationCard', () => { expect(card).toHaveAttribute('tabIndex', '0') expect(card).toHaveAttribute('role', 'button') }) + + describe('Changes indicator', () => { + it('should not show changes indicator when no changes', () => { + mockUseConfigurationState.mockReturnValue({ + hasChanges: false, + isValid: true, + configValidationErrors: [], + }) + + render() + + expect( + screen.queryByTestId('updated-configuration-highlight'), + ).not.toBeInTheDocument() + }) + + it('should show changes indicator when config has changes', () => { + mockUseConfigurationState.mockReturnValue({ + hasChanges: true, + isValid: true, + configValidationErrors: [], + }) + + render() + + expect( + screen.getByTestId('updated-configuration-highlight'), + ).toBeInTheDocument() + }) + }) + + describe('Validation errors', () => { + it('should not show error icon when config is valid', () => { + mockUseConfigurationState.mockReturnValue({ + hasChanges: false, + isValid: true, + configValidationErrors: [], + }) + + render() + + expect( + screen.queryByTestId('rdi-pipeline-nav__error-configuration'), + ).not.toBeInTheDocument() + }) + + it('should show error icon when config has validation errors', () => { + mockUseConfigurationState.mockReturnValue({ + hasChanges: false, + isValid: false, + configValidationErrors: [ + 'Invalid configuration', + 'Missing required field', + ], + }) + + render() + + expect( + screen.getByTestId('rdi-pipeline-nav__error-configuration'), + ).toBeInTheDocument() + }) + + it('should handle single validation error', () => { + mockUseConfigurationState.mockReturnValue({ + hasChanges: false, + isValid: false, + configValidationErrors: ['Single error'], + }) + + render() + + expect( + screen.getByTestId('rdi-pipeline-nav__error-configuration'), + ).toBeInTheDocument() + }) + }) + + it('should show both changes indicator and error icon when config has changes and errors', () => { + mockUseConfigurationState.mockReturnValue({ + hasChanges: true, + isValid: false, + configValidationErrors: ['Invalid configuration'], + }) + + render() + + expect( + screen.getByTestId('updated-configuration-highlight'), + ).toBeInTheDocument() + expect( + screen.getByTestId('rdi-pipeline-nav__error-configuration'), + ).toBeInTheDocument() + }) }) diff --git a/redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/cards/ConfigurationCard.tsx b/redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/cards/ConfigurationCard.tsx index 0e8d623117..633c86548c 100644 --- a/redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/cards/ConfigurationCard.tsx +++ b/redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/cards/ConfigurationCard.tsx @@ -1,6 +1,15 @@ import React from 'react' import { RdiPipelineTabs } from 'uiSrc/slices/interfaces' + +import { RiTooltip } from 'uiSrc/components' +import { Indicator } from 'uiSrc/components/base/text/text.styles' +import { Row } from 'uiSrc/components/base/layout/flex' +import { Text } from 'uiSrc/components/base/text' +import { Icon, ToastNotificationIcon } from 'uiSrc/components/base/icons' +import { useConfigurationState } from './hooks' + import BaseCard, { BaseCardProps } from './BaseCard' +import ValidationErrorsList from '../../validation-errors-list/ValidationErrorsList' export type ConfigurationCardProps = Omit< BaseCardProps, @@ -13,6 +22,9 @@ const ConfigurationCard = ({ onSelect, isSelected, }: ConfigurationCardProps) => { + const { hasChanges, isValid, configValidationErrors } = + useConfigurationState() + const handleClick = () => { onSelect(RdiPipelineTabs.Config) } @@ -25,7 +37,39 @@ const ConfigurationCard = ({ onClick={handleClick} data-testid={`rdi-nav-btn-${RdiPipelineTabs.Config}`} > - Configuration file + + {!hasChanges && } + + {hasChanges && ( + + + + )} + + Configuration file + + {!isValid && ( + + } + > + + + )} + ) } diff --git a/redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/cards/hooks/index.ts b/redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/cards/hooks/index.ts new file mode 100644 index 0000000000..09fb01ee0f --- /dev/null +++ b/redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/cards/hooks/index.ts @@ -0,0 +1 @@ +export * from './useConfigurationState' diff --git a/redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/cards/hooks/useConfigurationState.spec.ts b/redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/cards/hooks/useConfigurationState.spec.ts new file mode 100644 index 0000000000..4e6ddafcbe --- /dev/null +++ b/redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/cards/hooks/useConfigurationState.spec.ts @@ -0,0 +1,165 @@ +import { renderHook } from 'uiSrc/utils/test-utils' +import { rdiPipelineSelector, initialState } from 'uiSrc/slices/rdi/pipeline' +import { IStateRdiPipeline, FileChangeType } from 'uiSrc/slices/interfaces' +import { useConfigurationState } from './useConfigurationState' + +jest.mock('uiSrc/slices/rdi/pipeline', () => ({ + ...jest.requireActual('uiSrc/slices/rdi/pipeline'), + rdiPipelineSelector: jest.fn(), +})) + +const mockRdiPipelineSelector = rdiPipelineSelector as jest.MockedFunction< + typeof rdiPipelineSelector +> + +const createMockState = ( + overrides: Partial = {}, +): IStateRdiPipeline => ({ + ...initialState, + ...overrides, +}) + +describe('useConfigurationState', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should return correct state when no changes and no validation errors', () => { + mockRdiPipelineSelector.mockReturnValue( + createMockState({ + changes: {}, + configValidationErrors: [], + }), + ) + + const { result } = renderHook(() => useConfigurationState()) + + expect(result.current).toEqual({ + hasChanges: false, + isValid: true, + configValidationErrors: [], + }) + }) + + it('should return hasChanges as true when config has changes', () => { + mockRdiPipelineSelector.mockReturnValue( + createMockState({ + changes: { config: FileChangeType.Modified }, + configValidationErrors: [], + }), + ) + + const { result } = renderHook(() => useConfigurationState()) + + expect(result.current).toEqual({ + hasChanges: true, + isValid: true, + configValidationErrors: [], + }) + }) + + it('should return isValid as false when config has validation errors', () => { + mockRdiPipelineSelector.mockReturnValue( + createMockState({ + changes: {}, + configValidationErrors: [ + 'Invalid configuration', + 'Missing required field', + ], + }), + ) + + const { result } = renderHook(() => useConfigurationState()) + + expect(result.current).toEqual({ + hasChanges: false, + isValid: false, + configValidationErrors: [ + 'Invalid configuration', + 'Missing required field', + ], + }) + }) + + it('should handle both changes and validation errors', () => { + mockRdiPipelineSelector.mockReturnValue( + createMockState({ + changes: { config: FileChangeType.Added }, + configValidationErrors: ['Configuration error'], + }), + ) + + const { result } = renderHook(() => useConfigurationState()) + + expect(result.current).toEqual({ + hasChanges: true, + isValid: false, + configValidationErrors: ['Configuration error'], + }) + }) + + it('should handle empty validation errors array', () => { + mockRdiPipelineSelector.mockReturnValue( + createMockState({ + changes: {}, + configValidationErrors: [], + }), + ) + + const { result } = renderHook(() => useConfigurationState()) + + expect(result.current.isValid).toBe(true) + }) + + it('should handle single validation error', () => { + mockRdiPipelineSelector.mockReturnValue( + createMockState({ + changes: {}, + configValidationErrors: ['Single error'], + }), + ) + + const { result } = renderHook(() => useConfigurationState()) + + expect(result.current).toEqual({ + hasChanges: false, + isValid: false, + configValidationErrors: ['Single error'], + }) + }) + + it('should handle multiple validation errors', () => { + const errors = ['Error 1', 'Error 2', 'Error 3'] + mockRdiPipelineSelector.mockReturnValue( + createMockState({ + changes: {}, + configValidationErrors: errors, + }), + ) + + const { result } = renderHook(() => useConfigurationState()) + + expect(result.current).toEqual({ + hasChanges: false, + isValid: false, + configValidationErrors: errors, + }) + }) + + it('should handle changes in other files without affecting config state', () => { + mockRdiPipelineSelector.mockReturnValue( + createMockState({ + changes: { + job1: FileChangeType.Modified, + job2: FileChangeType.Added, + // no config changes + }, + configValidationErrors: [], + }), + ) + + const { result } = renderHook(() => useConfigurationState()) + + expect(result.current.hasChanges).toBe(false) + }) +}) diff --git a/redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/cards/hooks/useConfigurationState.ts b/redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/cards/hooks/useConfigurationState.ts new file mode 100644 index 0000000000..eda3a3e541 --- /dev/null +++ b/redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/cards/hooks/useConfigurationState.ts @@ -0,0 +1,21 @@ +import { useSelector } from 'react-redux' +import { rdiPipelineSelector } from 'uiSrc/slices/rdi/pipeline' + +export interface ConfigurationState { + hasChanges: boolean + isValid: boolean + configValidationErrors: string[] +} + +export const useConfigurationState = (): ConfigurationState => { + const { changes, configValidationErrors } = useSelector(rdiPipelineSelector) + + const hasChanges = !!changes.config + const isValid = configValidationErrors.length === 0 + + return { + hasChanges, + isValid, + configValidationErrors, + } +}