Skip to content

Commit a284591

Browse files
authored
RI-7728: Show different statuses for RDI configuration (#5207)
* extract logic in hook
1 parent 8fb805d commit a284591

File tree

5 files changed

+339
-1
lines changed

5 files changed

+339
-1
lines changed

redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/cards/ConfigurationCard.spec.tsx

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,20 @@ import ConfigurationCard, { ConfigurationCardProps } from './ConfigurationCard'
77

88
const mockedProps = mock<ConfigurationCardProps>()
99

10+
const mockUseConfigurationState = jest.fn()
11+
jest.mock('./hooks/useConfigurationState', () => ({
12+
useConfigurationState: () => mockUseConfigurationState(),
13+
}))
14+
1015
describe('ConfigurationCard', () => {
16+
beforeEach(() => {
17+
mockUseConfigurationState.mockReturnValue({
18+
hasChanges: false,
19+
isValid: true,
20+
configValidationErrors: [],
21+
})
22+
})
23+
1124
it('should render with correct title', () => {
1225
render(<ConfigurationCard {...instance(mockedProps)} />)
1326

@@ -49,4 +62,98 @@ describe('ConfigurationCard', () => {
4962
expect(card).toHaveAttribute('tabIndex', '0')
5063
expect(card).toHaveAttribute('role', 'button')
5164
})
65+
66+
describe('Changes indicator', () => {
67+
it('should not show changes indicator when no changes', () => {
68+
mockUseConfigurationState.mockReturnValue({
69+
hasChanges: false,
70+
isValid: true,
71+
configValidationErrors: [],
72+
})
73+
74+
render(<ConfigurationCard {...instance(mockedProps)} />)
75+
76+
expect(
77+
screen.queryByTestId('updated-configuration-highlight'),
78+
).not.toBeInTheDocument()
79+
})
80+
81+
it('should show changes indicator when config has changes', () => {
82+
mockUseConfigurationState.mockReturnValue({
83+
hasChanges: true,
84+
isValid: true,
85+
configValidationErrors: [],
86+
})
87+
88+
render(<ConfigurationCard {...instance(mockedProps)} />)
89+
90+
expect(
91+
screen.getByTestId('updated-configuration-highlight'),
92+
).toBeInTheDocument()
93+
})
94+
})
95+
96+
describe('Validation errors', () => {
97+
it('should not show error icon when config is valid', () => {
98+
mockUseConfigurationState.mockReturnValue({
99+
hasChanges: false,
100+
isValid: true,
101+
configValidationErrors: [],
102+
})
103+
104+
render(<ConfigurationCard {...instance(mockedProps)} />)
105+
106+
expect(
107+
screen.queryByTestId('rdi-pipeline-nav__error-configuration'),
108+
).not.toBeInTheDocument()
109+
})
110+
111+
it('should show error icon when config has validation errors', () => {
112+
mockUseConfigurationState.mockReturnValue({
113+
hasChanges: false,
114+
isValid: false,
115+
configValidationErrors: [
116+
'Invalid configuration',
117+
'Missing required field',
118+
],
119+
})
120+
121+
render(<ConfigurationCard {...instance(mockedProps)} />)
122+
123+
expect(
124+
screen.getByTestId('rdi-pipeline-nav__error-configuration'),
125+
).toBeInTheDocument()
126+
})
127+
128+
it('should handle single validation error', () => {
129+
mockUseConfigurationState.mockReturnValue({
130+
hasChanges: false,
131+
isValid: false,
132+
configValidationErrors: ['Single error'],
133+
})
134+
135+
render(<ConfigurationCard {...instance(mockedProps)} />)
136+
137+
expect(
138+
screen.getByTestId('rdi-pipeline-nav__error-configuration'),
139+
).toBeInTheDocument()
140+
})
141+
})
142+
143+
it('should show both changes indicator and error icon when config has changes and errors', () => {
144+
mockUseConfigurationState.mockReturnValue({
145+
hasChanges: true,
146+
isValid: false,
147+
configValidationErrors: ['Invalid configuration'],
148+
})
149+
150+
render(<ConfigurationCard {...instance(mockedProps)} />)
151+
152+
expect(
153+
screen.getByTestId('updated-configuration-highlight'),
154+
).toBeInTheDocument()
155+
expect(
156+
screen.getByTestId('rdi-pipeline-nav__error-configuration'),
157+
).toBeInTheDocument()
158+
})
52159
})

redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/cards/ConfigurationCard.tsx

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
import React from 'react'
22
import { RdiPipelineTabs } from 'uiSrc/slices/interfaces'
3+
4+
import { RiTooltip } from 'uiSrc/components'
5+
import { Indicator } from 'uiSrc/components/base/text/text.styles'
6+
import { Row } from 'uiSrc/components/base/layout/flex'
7+
import { Text } from 'uiSrc/components/base/text'
8+
import { Icon, ToastNotificationIcon } from 'uiSrc/components/base/icons'
9+
import { useConfigurationState } from './hooks'
10+
311
import BaseCard, { BaseCardProps } from './BaseCard'
12+
import ValidationErrorsList from '../../validation-errors-list/ValidationErrorsList'
413

514
export type ConfigurationCardProps = Omit<
615
BaseCardProps,
@@ -13,6 +22,9 @@ const ConfigurationCard = ({
1322
onSelect,
1423
isSelected,
1524
}: ConfigurationCardProps) => {
25+
const { hasChanges, isValid, configValidationErrors } =
26+
useConfigurationState()
27+
1628
const handleClick = () => {
1729
onSelect(RdiPipelineTabs.Config)
1830
}
@@ -25,7 +37,39 @@ const ConfigurationCard = ({
2537
onClick={handleClick}
2638
data-testid={`rdi-nav-btn-${RdiPipelineTabs.Config}`}
2739
>
28-
Configuration file
40+
<Row gap="s" align="center">
41+
{!hasChanges && <Indicator $color="transparent" />}
42+
43+
{hasChanges && (
44+
<RiTooltip
45+
content="This file contains undeployed changes."
46+
position="top"
47+
>
48+
<Indicator
49+
$color="informative"
50+
data-testid={`updated-configuration-highlight`}
51+
/>
52+
</RiTooltip>
53+
)}
54+
55+
<Text>Configuration file</Text>
56+
57+
{!isValid && (
58+
<RiTooltip
59+
position="right"
60+
content={
61+
<ValidationErrorsList validationErrors={configValidationErrors} />
62+
}
63+
>
64+
<Icon
65+
icon={ToastNotificationIcon}
66+
color="danger500"
67+
size="M"
68+
data-testid={`rdi-pipeline-nav__error-configuration`}
69+
/>
70+
</RiTooltip>
71+
)}
72+
</Row>
2973
</BaseCard>
3074
)
3175
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './useConfigurationState'
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { renderHook } from 'uiSrc/utils/test-utils'
2+
import { rdiPipelineSelector, initialState } from 'uiSrc/slices/rdi/pipeline'
3+
import { IStateRdiPipeline, FileChangeType } from 'uiSrc/slices/interfaces'
4+
import { useConfigurationState } from './useConfigurationState'
5+
6+
jest.mock('uiSrc/slices/rdi/pipeline', () => ({
7+
...jest.requireActual('uiSrc/slices/rdi/pipeline'),
8+
rdiPipelineSelector: jest.fn(),
9+
}))
10+
11+
const mockRdiPipelineSelector = rdiPipelineSelector as jest.MockedFunction<
12+
typeof rdiPipelineSelector
13+
>
14+
15+
const createMockState = (
16+
overrides: Partial<IStateRdiPipeline> = {},
17+
): IStateRdiPipeline => ({
18+
...initialState,
19+
...overrides,
20+
})
21+
22+
describe('useConfigurationState', () => {
23+
beforeEach(() => {
24+
jest.clearAllMocks()
25+
})
26+
27+
it('should return correct state when no changes and no validation errors', () => {
28+
mockRdiPipelineSelector.mockReturnValue(
29+
createMockState({
30+
changes: {},
31+
configValidationErrors: [],
32+
}),
33+
)
34+
35+
const { result } = renderHook(() => useConfigurationState())
36+
37+
expect(result.current).toEqual({
38+
hasChanges: false,
39+
isValid: true,
40+
configValidationErrors: [],
41+
})
42+
})
43+
44+
it('should return hasChanges as true when config has changes', () => {
45+
mockRdiPipelineSelector.mockReturnValue(
46+
createMockState({
47+
changes: { config: FileChangeType.Modified },
48+
configValidationErrors: [],
49+
}),
50+
)
51+
52+
const { result } = renderHook(() => useConfigurationState())
53+
54+
expect(result.current).toEqual({
55+
hasChanges: true,
56+
isValid: true,
57+
configValidationErrors: [],
58+
})
59+
})
60+
61+
it('should return isValid as false when config has validation errors', () => {
62+
mockRdiPipelineSelector.mockReturnValue(
63+
createMockState({
64+
changes: {},
65+
configValidationErrors: [
66+
'Invalid configuration',
67+
'Missing required field',
68+
],
69+
}),
70+
)
71+
72+
const { result } = renderHook(() => useConfigurationState())
73+
74+
expect(result.current).toEqual({
75+
hasChanges: false,
76+
isValid: false,
77+
configValidationErrors: [
78+
'Invalid configuration',
79+
'Missing required field',
80+
],
81+
})
82+
})
83+
84+
it('should handle both changes and validation errors', () => {
85+
mockRdiPipelineSelector.mockReturnValue(
86+
createMockState({
87+
changes: { config: FileChangeType.Added },
88+
configValidationErrors: ['Configuration error'],
89+
}),
90+
)
91+
92+
const { result } = renderHook(() => useConfigurationState())
93+
94+
expect(result.current).toEqual({
95+
hasChanges: true,
96+
isValid: false,
97+
configValidationErrors: ['Configuration error'],
98+
})
99+
})
100+
101+
it('should handle empty validation errors array', () => {
102+
mockRdiPipelineSelector.mockReturnValue(
103+
createMockState({
104+
changes: {},
105+
configValidationErrors: [],
106+
}),
107+
)
108+
109+
const { result } = renderHook(() => useConfigurationState())
110+
111+
expect(result.current.isValid).toBe(true)
112+
})
113+
114+
it('should handle single validation error', () => {
115+
mockRdiPipelineSelector.mockReturnValue(
116+
createMockState({
117+
changes: {},
118+
configValidationErrors: ['Single error'],
119+
}),
120+
)
121+
122+
const { result } = renderHook(() => useConfigurationState())
123+
124+
expect(result.current).toEqual({
125+
hasChanges: false,
126+
isValid: false,
127+
configValidationErrors: ['Single error'],
128+
})
129+
})
130+
131+
it('should handle multiple validation errors', () => {
132+
const errors = ['Error 1', 'Error 2', 'Error 3']
133+
mockRdiPipelineSelector.mockReturnValue(
134+
createMockState({
135+
changes: {},
136+
configValidationErrors: errors,
137+
}),
138+
)
139+
140+
const { result } = renderHook(() => useConfigurationState())
141+
142+
expect(result.current).toEqual({
143+
hasChanges: false,
144+
isValid: false,
145+
configValidationErrors: errors,
146+
})
147+
})
148+
149+
it('should handle changes in other files without affecting config state', () => {
150+
mockRdiPipelineSelector.mockReturnValue(
151+
createMockState({
152+
changes: {
153+
job1: FileChangeType.Modified,
154+
job2: FileChangeType.Added,
155+
// no config changes
156+
},
157+
configValidationErrors: [],
158+
}),
159+
)
160+
161+
const { result } = renderHook(() => useConfigurationState())
162+
163+
expect(result.current.hasChanges).toBe(false)
164+
})
165+
})
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { useSelector } from 'react-redux'
2+
import { rdiPipelineSelector } from 'uiSrc/slices/rdi/pipeline'
3+
4+
export interface ConfigurationState {
5+
hasChanges: boolean
6+
isValid: boolean
7+
configValidationErrors: string[]
8+
}
9+
10+
export const useConfigurationState = (): ConfigurationState => {
11+
const { changes, configValidationErrors } = useSelector(rdiPipelineSelector)
12+
13+
const hasChanges = !!changes.config
14+
const isValid = configValidationErrors.length === 0
15+
16+
return {
17+
hasChanges,
18+
isValid,
19+
configValidationErrors,
20+
}
21+
}

0 commit comments

Comments
 (0)