Skip to content

Commit a4353a6

Browse files
Merge pull request #3537 from RedisInsight/fe/bugfix/RI-5856_incorrect_yaml
#RI-5856 - handle yaml error
2 parents 5dffe0c + b7409f0 commit a4353a6

File tree

11 files changed

+244
-25
lines changed

11 files changed

+244
-25
lines changed

redisinsight/ui/src/pages/rdi/instance/InstancePage.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import React, { useEffect, useRef, useState } from 'react'
22
import { useDispatch, useSelector } from 'react-redux'
33
import { useHistory, useLocation, useParams } from 'react-router-dom'
44
import { Formik, FormikProps } from 'formik'
5+
import { EuiText } from '@elastic/eui'
6+
import { upperFirst } from 'lodash'
57

68
import {
79
appContextSelector,
@@ -22,7 +24,8 @@ import {
2224
setPipelineInitialState,
2325
} from 'uiSrc/slices/rdi/pipeline'
2426
import { IPipeline } from 'uiSrc/slices/interfaces'
25-
import { Nullable, pipelineToJson } from 'uiSrc/utils'
27+
import { createAxiosError, Nullable, pipelineToJson } from 'uiSrc/utils'
28+
import { addErrorNotification } from 'uiSrc/slices/app/notifications'
2629

2730
import InstancePageRouter from './InstancePageRouter'
2831
import { ConfirmLeavePagePopup } from './components'
@@ -88,7 +91,19 @@ const RdiInstancePage = ({ routes = [] }: Props) => {
8891
}, [])
8992

9093
const onSubmit = (values: IPipeline) => {
91-
const JSONValues = pipelineToJson(values)
94+
const JSONValues = pipelineToJson(values, (errors) => {
95+
dispatch(addErrorNotification(createAxiosError({
96+
message: (
97+
<>
98+
<EuiText>{`${upperFirst(errors[0].filename)} has an invalid structure.`}</EuiText>
99+
<EuiText>{errors[0].msg}</EuiText>
100+
</>
101+
)
102+
})))
103+
})
104+
if (!JSONValues) {
105+
return
106+
}
92107
dispatch(deployPipelineAction(rdiInstanceId, JSONValues))
93108
}
94109

redisinsight/ui/src/pages/rdi/pipeline-management/components/jobs-panel/Panel.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {
1414
import { useDispatch, useSelector } from 'react-redux'
1515
import { useParams } from 'react-router-dom'
1616
import { isArray } from 'lodash'
17-
import yaml from 'js-yaml'
1817

1918
import { PipelineJobsTabs } from 'uiSrc/slices/interfaces/rdi'
2019
import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'
@@ -27,7 +26,7 @@ import { formatLongName } from 'uiSrc/utils'
2726
import styles from './styles.module.scss'
2827

2928
export interface Props {
30-
job: string
29+
job?: object
3130
onClose: () => void
3231
}
3332

@@ -111,7 +110,7 @@ const DryRunJobPanel = (props: Props) => {
111110
id: rdiInstanceId,
112111
},
113112
})
114-
dispatch(rdiDryRunJob(rdiInstanceId, JSON.parse(input), yaml.load(job)))
113+
dispatch(rdiDryRunJob(rdiInstanceId, JSON.parse(input), job))
115114
}
116115

117116
const isSelectAvailable = selectedTab === PipelineJobsTabs.Output

redisinsight/ui/src/pages/rdi/pipeline-management/pages/config/Config.spec.tsx

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import React from 'react'
22
import { useFormikContext } from 'formik'
33
import { cloneDeep } from 'lodash'
4+
import { EuiText } from '@elastic/eui'
5+
import { AxiosError } from 'axios'
46
import { rdiPipelineSelector, setChangedFile, deleteChangedFile } from 'uiSrc/slices/rdi/pipeline'
57
import { rdiTestConnectionsSelector } from 'uiSrc/slices/rdi/testConnections'
68
import { act, cleanup, fireEvent, mockedStore, render, screen } from 'uiSrc/utils/test-utils'
79

810
import { sendPageViewTelemetry, TelemetryPageView } from 'uiSrc/telemetry'
911
import { MOCK_RDI_PIPELINE_DATA } from 'uiSrc/mocks/data/rdi'
1012
import { FileChangeType } from 'uiSrc/slices/interfaces'
13+
import { addErrorNotification } from 'uiSrc/slices/app/notifications'
1114
import Config from './Config'
1215

1316
jest.mock('uiSrc/telemetry', () => ({
@@ -149,7 +152,7 @@ describe('Config', () => {
149152
expect(queryByTestId('test-connection-panel')).toBeInTheDocument()
150153
})
151154

152-
it('should open right panel', async () => {
155+
it('should close right panel', async () => {
153156
const { queryByTestId } = render(<Config />)
154157

155158
expect(queryByTestId('test-connection-panel')).not.toBeInTheDocument()
@@ -167,6 +170,40 @@ describe('Config', () => {
167170
expect(queryByTestId('test-connection-panel')).not.toBeInTheDocument()
168171
})
169172

173+
it('should render error notification', async () => {
174+
const mockUseFormikContext = {
175+
setFieldValue: mockSetFieldValue,
176+
values: { config: 'sources:incorrect\n target:' },
177+
};
178+
(useFormikContext as jest.Mock).mockReturnValue(mockUseFormikContext)
179+
180+
const { queryByTestId } = render(<Config />)
181+
182+
expect(queryByTestId('test-connection-panel')).not.toBeInTheDocument()
183+
184+
await act(async () => {
185+
fireEvent.click(screen.getByTestId('rdi-test-connection-btn'))
186+
})
187+
188+
const expectedActions = [
189+
addErrorNotification({
190+
response: {
191+
data: {
192+
message: (
193+
<>
194+
<EuiText>Config has an invalid structure.</EuiText>
195+
<EuiText>end of the stream or a document separator is expected</EuiText>
196+
</>
197+
)
198+
}
199+
}
200+
} as AxiosError)
201+
]
202+
203+
expect(store.getActions().slice(0, expectedActions.length)).toEqual(expectedActions)
204+
expect(queryByTestId('test-connection-panel')).not.toBeInTheDocument()
205+
})
206+
170207
it('should render loading spinner', () => {
171208
const rdiPipelineSelectorMock = jest.fn().mockReturnValue({
172209
loading: true,

redisinsight/ui/src/pages/rdi/pipeline-management/pages/config/Config.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import cx from 'classnames'
66
import { useParams } from 'react-router-dom'
77
import { get, throttle } from 'lodash'
88

9-
import yaml from 'js-yaml'
109
import { sendPageViewTelemetry, sendEventTelemetry, TelemetryPageView, TelemetryEvent } from 'uiSrc/telemetry'
1110
import { EXTERNAL_LINKS, UTM_MEDIUMS } from 'uiSrc/constants/links'
1211
import { getUtmExternalLink } from 'uiSrc/utils/links'
@@ -17,8 +16,9 @@ import TestConnectionsPanel from 'uiSrc/pages/rdi/pipeline-management/components
1716
import TemplatePopover from 'uiSrc/pages/rdi/pipeline-management/components/template-popover'
1817
import { testConnectionsAction, rdiTestConnectionsSelector, testConnectionsController } from 'uiSrc/slices/rdi/testConnections'
1918
import { appContextPipelineManagement } from 'uiSrc/slices/app/context'
20-
import { isEqualPipelineFile } from 'uiSrc/utils'
19+
import { createAxiosError, isEqualPipelineFile, yamlToJson } from 'uiSrc/utils'
2120

21+
import { addErrorNotification } from 'uiSrc/slices/app/notifications'
2222
import styles from './styles.module.scss'
2323

2424
const Config = () => {
@@ -57,8 +57,21 @@ const Config = () => {
5757
}, [isOpenDialog, config, pipelineLoading])
5858

5959
const testConnections = () => {
60+
const JSONValue = yamlToJson(config, (msg) => {
61+
dispatch(addErrorNotification(createAxiosError({
62+
message: (
63+
<>
64+
<EuiText>Config has an invalid structure.</EuiText>
65+
<EuiText>{msg}</EuiText>
66+
</>
67+
)
68+
})))
69+
})
70+
if (!JSONValue) {
71+
return
72+
}
6073
setIsPanelOpen(true)
61-
dispatch(testConnectionsAction(rdiInstanceId, yaml.load(config)))
74+
dispatch(testConnectionsAction(rdiInstanceId, JSONValue))
6275
sendEventTelemetry({
6376
event: TelemetryEvent.RDI_TEST_TARGET_CONNECTIONS_CLICKED,
6477
eventData: {

redisinsight/ui/src/pages/rdi/pipeline-management/pages/job/Job.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'
22
import { useDispatch, useSelector } from 'react-redux'
33
import { EuiText, EuiLink, EuiButton, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui'
44
import { useFormikContext } from 'formik'
5-
import { get, throttle } from 'lodash'
5+
import { upperFirst, get, throttle } from 'lodash'
66
import cx from 'classnames'
77
import { monaco as monacoEditor } from 'react-monaco-editor'
88

@@ -14,10 +14,11 @@ import MonacoYaml from 'uiSrc/components/monaco-editor/components/monaco-yaml'
1414
import DryRunJobPanel from 'uiSrc/pages/rdi/pipeline-management/components/jobs-panel'
1515
import { DSL, KEYBOARD_SHORTCUTS } from 'uiSrc/constants'
1616
import TemplatePopover from 'uiSrc/pages/rdi/pipeline-management/components/template-popover'
17-
import { isEqualPipelineFile, Maybe } from 'uiSrc/utils'
17+
import { createAxiosError, isEqualPipelineFile, Maybe, yamlToJson } from 'uiSrc/utils'
1818
import { getUtmExternalLink } from 'uiSrc/utils/links'
1919
import { KeyboardShortcut } from 'uiSrc/components'
2020

21+
import { addErrorNotification } from 'uiSrc/slices/app/notifications'
2122
import styles from './styles.module.scss'
2223

2324
export interface Props {
@@ -33,6 +34,7 @@ const Job = (props: Props) => {
3334

3435
const [isPanelOpen, setIsPanelOpen] = useState<boolean>(false)
3536
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false)
37+
const [JSONJob, setJSONJob] = useState<object>()
3638
const [shouldOpenDedicatedEditor, setShouldOpenDedicatedEditor] = useState<boolean>(false)
3739

3840
const dispatch = useDispatch()
@@ -66,7 +68,21 @@ const Job = (props: Props) => {
6668
}, [name])
6769

6870
const handleDryRunJob = () => {
71+
const JSONValue = yamlToJson(value, (msg) => {
72+
dispatch(addErrorNotification(createAxiosError({
73+
message: (
74+
<>
75+
<EuiText>{`${upperFirst(name)} has an invalid structure.`}</EuiText>
76+
<EuiText>{msg}</EuiText>
77+
</>
78+
)
79+
})))
80+
})
81+
if (!JSONValue) {
82+
return
83+
}
6984
setIsPanelOpen(true)
85+
setJSONJob(JSONValue)
7086
sendEventTelemetry({
7187
event: TelemetryEvent.RDI_TEST_JOB_OPENED,
7288
eventData: {
@@ -234,7 +250,7 @@ const Job = (props: Props) => {
234250
{isPanelOpen && (
235251
<DryRunJobPanel
236252
onClose={() => setIsPanelOpen(false)}
237-
job={value}
253+
job={JSONJob}
238254
/>
239255
)}
240256
</>

redisinsight/ui/src/pages/rdi/pipeline-management/pages/job/JobsWrapper.spec.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import { sendPageViewTelemetry, TelemetryPageView, sendEventTelemetry, Telemetry
99
import { MOCK_RDI_PIPELINE_CONFIG, MOCK_RDI_PIPELINE_DATA, MOCK_RDI_PIPELINE_JOB2 } from 'uiSrc/mocks/data/rdi'
1010
import { FileChangeType } from 'uiSrc/slices/interfaces'
1111
import JobWrapper from './JobWrapper'
12+
import {addErrorNotification} from "uiSrc/slices/app/notifications";
13+
import {EuiText} from "@elastic/eui";
14+
import {AxiosError} from "axios";
1215

1316
jest.mock('uiSrc/telemetry', () => ({
1417
...jest.requireActual('uiSrc/telemetry'),
@@ -179,4 +182,42 @@ describe('JobWrapper', () => {
179182

180183
expect(store.getActions()).toEqual(expectedActions)
181184
})
185+
186+
it('should render error notification', () => {
187+
const rdiPipelineSelectorMock = jest.fn().mockReturnValue({
188+
loading: false,
189+
schema: { jobs: { test: {} } },
190+
data: { jobs: [{ name: 'jobName', value: 'sources:incorrect\n target:' }] }
191+
});
192+
(rdiPipelineSelector as jest.Mock).mockImplementation(rdiPipelineSelectorMock)
193+
194+
const mockUseFormikContext = {
195+
setFieldValue: jest.fn,
196+
values: { config: MOCK_RDI_PIPELINE_CONFIG, jobs: [{ name: 'jobName', value: 'sources:incorrect\n target:' }] },
197+
};
198+
(useFormikContext as jest.Mock).mockReturnValue(mockUseFormikContext)
199+
200+
const { queryByTestId } = render(<JobWrapper />)
201+
202+
fireEvent.click(screen.getByTestId('rdi-job-dry-run'))
203+
204+
const expectedActions = [
205+
addErrorNotification({
206+
response: {
207+
data: {
208+
message: (
209+
<>
210+
<EuiText>JobName has an invalid structure.</EuiText>
211+
<EuiText>end of the stream or a document separator is expected</EuiText>
212+
</>
213+
)
214+
}
215+
}
216+
} as AxiosError)
217+
]
218+
219+
expect(store.getActions().slice(0 - expectedActions.length)).toEqual(expectedActions)
220+
221+
expect(queryByTestId('dry-run-panel')).not.toBeInTheDocument()
222+
})
182223
})

redisinsight/ui/src/slices/interfaces/app.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ export interface CustomError {
1414
resourceId?: string
1515
}
1616

17+
export interface ErrorOptions {
18+
message: string | JSX.Element
19+
code?: string
20+
config?: object
21+
request?: object
22+
response?: object
23+
}
24+
1725
export interface EnhancedAxiosError extends AxiosError<CustomError> {
1826
}
1927

redisinsight/ui/src/slices/interfaces/rdi.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,3 +233,8 @@ export interface IStateRdiTestConnections {
233233
export type TJMESPathFunctions = {
234234
[key: string]: Pick<ICommand, 'summary'> & Required<Pick<ICommand, 'arguments'>>
235235
}
236+
237+
export interface IYamlFormatError {
238+
filename: string
239+
msg: string
240+
}

redisinsight/ui/src/utils/apiResponse.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { AxiosError } from 'axios'
22
import { first, isArray, get } from 'lodash'
3-
import { AddRedisDatabaseStatus, EnhancedAxiosError, IBulkOperationResult } from 'uiSrc/slices/interfaces'
3+
import { AddRedisDatabaseStatus, EnhancedAxiosError, ErrorOptions, IBulkOperationResult } from 'uiSrc/slices/interfaces'
44
import { parseCustomError } from 'uiSrc/utils'
55

66
export const DEFAULT_ERROR_MESSAGE = 'Something was wrong!'
@@ -12,9 +12,16 @@ export const getAxiosError = (error: EnhancedAxiosError): AxiosError => {
1212
return error
1313
}
1414

15+
export const createAxiosError = (options: ErrorOptions): AxiosError => ({
16+
response: {
17+
data: options,
18+
},
19+
}) as AxiosError
20+
1521
export const getApiErrorCode = (error: AxiosError) => error?.response?.status
1622

1723
export function getApiErrorMessage(error: AxiosError): string {
24+
// @ts-ignore
1825
const errorMessage = error?.response?.data?.message
1926
if (!error || !error.response) {
2027
return DEFAULT_ERROR_MESSAGE

0 commit comments

Comments
 (0)