Skip to content

Commit 51cd240

Browse files
authored
Merge pull request #1500 from RedisInsight/fe/feature/RI-3903_db-import-results
#RI-3903 - add imports results
2 parents c09f6d9 + ddb6ebd commit 51cd240

File tree

20 files changed

+564
-46
lines changed

20 files changed

+564
-46
lines changed

redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.spec.tsx

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -92,31 +92,25 @@ describe('ImportDatabasesDialog', () => {
9292
expect(screen.getByTestId('file-loading-indicator')).toBeInTheDocument()
9393
})
9494

95-
it('should render success message when at least 1 database added', () => {
95+
it('should not render error message without error', () => {
9696
(importInstancesSelector as jest.Mock).mockImplementation(() => ({
9797
loading: false,
98-
data: {
99-
success: 1,
100-
total: 2
101-
}
98+
data: {}
10299
}))
103100

104101
render(<ImportDatabasesDialog onClose={jest.fn()} />)
105-
expect(screen.getByTestId('result-success')).toBeInTheDocument()
106102
expect(screen.queryByTestId('result-failed')).not.toBeInTheDocument()
107103
})
108104

109105
it('should render error message when 0 success databases added', () => {
110106
(importInstancesSelector as jest.Mock).mockImplementation(() => ({
111107
loading: false,
112-
data: {
113-
success: 0,
114-
total: 2
115-
}
108+
data: null,
109+
error: 'Error message'
116110
}))
117111

118112
render(<ImportDatabasesDialog onClose={jest.fn()} />)
119113
expect(screen.getByTestId('result-failed')).toBeInTheDocument()
120-
expect(screen.queryByTestId('result-success')).not.toBeInTheDocument()
114+
expect(screen.getByTestId('result-failed')).toHaveTextContent('Error message')
121115
})
122116
})

redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.tsx

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
} from 'uiSrc/slices/instances/instances'
2525
import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'
2626
import { Nullable } from 'uiSrc/utils'
27+
import ResultsLog from './components/ResultsLog'
2728

2829
import styles from './styles.module.scss'
2930

@@ -49,9 +50,11 @@ const ImportDatabasesDialog = ({ onClose }: Props) => {
4950
}
5051

5152
const handleOnClose = () => {
52-
dispatch(resetImportInstances())
53-
data?.success && dispatch(fetchInstancesAction())
53+
if (data?.success?.length || data?.partial?.length) {
54+
dispatch(fetchInstancesAction())
55+
}
5456
onClose(!data)
57+
dispatch(resetImportInstances())
5558
}
5659

5760
const onSubmit = () => {
@@ -72,17 +75,17 @@ const ImportDatabasesDialog = ({ onClose }: Props) => {
7275
<EuiModal onClose={handleOnClose} className={styles.modal} data-testid="import-dbs-dialog">
7376
<EuiModalHeader>
7477
<EuiModalHeaderTitle>
75-
<EuiTitle size="xs">
76-
<span>Import Database Connections</span>
78+
<EuiTitle size="xs" data-testid="import-dbs-dialog-title">
79+
<span>{(!data && !error) ? 'Import Database Connections' : 'Import Results'}</span>
7780
</EuiTitle>
7881
</EuiModalHeaderTitle>
7982
</EuiModalHeader>
8083

8184
<EuiModalBody>
8285
<EuiFlexGroup justifyContent="center" gutterSize="none" responsive={false}>
83-
<EuiFlexItem grow={false}>
86+
<EuiFlexItem grow={!!data} style={{ maxWidth: '100%' }}>
8487
{isShowForm && (
85-
<>
88+
<EuiFlexItem>
8689
<EuiFilePicker
8790
id="import-databases-input-file"
8891
initialPromptText="Select or drag and drop a file"
@@ -98,7 +101,7 @@ const ImportDatabasesDialog = ({ onClose }: Props) => {
98101
File should not exceed {MAX_MB_FILE} MB
99102
</EuiTextColor>
100103
)}
101-
</>
104+
</EuiFlexItem>
102105
)}
103106

104107
{loading && (
@@ -107,29 +110,21 @@ const ImportDatabasesDialog = ({ onClose }: Props) => {
107110
<EuiText color="subdued" style={{ marginTop: 12 }}>Uploading...</EuiText>
108111
</div>
109112
)}
110-
111-
{data && data.success !== 0 && (
112-
<div className={styles.result} data-testid="result-success">
113-
<EuiIcon type="checkInCircleFilled" size="xxl" color="success" />
114-
<EuiText color="subdued" style={{ marginTop: 12 }}>
115-
Successfully added {data.success} of {data.total} database connections
116-
</EuiText>
117-
</div>
118-
)}
119-
120-
{(data?.success === 0 || error) && (
113+
{data && (<ResultsLog data={data} />)}
114+
{error && (
121115
<div className={styles.result} data-testid="result-failed">
122116
<EuiIcon type="crossInACircleFilled" size="xxl" color="danger" />
123-
<EuiText color="subdued" style={{ marginTop: 12 }}>
117+
<EuiText color="subdued" style={{ marginTop: 16 }}>
124118
Failed to add database connections
125119
</EuiText>
120+
<EuiText color="subdued">{error}</EuiText>
126121
</div>
127122
)}
128123
</EuiFlexItem>
129124
</EuiFlexGroup>
130125
</EuiModalBody>
131126

132-
{data && data.success !== 0 && (
127+
{data && (
133128
<EuiModalFooter>
134129
<EuiButton
135130
color="secondary"
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import React from 'react'
2+
import { render, screen, fireEvent, within } from 'uiSrc/utils/test-utils'
3+
import { ImportDatabasesData } from 'uiSrc/slices/interfaces'
4+
import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'
5+
import ResultsLog from './ResultsLog'
6+
7+
jest.mock('uiSrc/telemetry', () => ({
8+
...jest.requireActual('uiSrc/telemetry'),
9+
sendEventTelemetry: jest.fn(),
10+
}))
11+
12+
const mockedError = { statusCode: 400, message: 'message', error: 'error' }
13+
describe('ResultsLog', () => {
14+
it('should render', () => {
15+
const mockedData = { total: 0, fail: [], partial: [], success: [] }
16+
render(<ResultsLog data={mockedData} />)
17+
})
18+
19+
it('should be all collapsed nav groups', () => {
20+
const mockedData: ImportDatabasesData = {
21+
total: 3,
22+
fail: [{ index: 0, status: 'fail', errors: [mockedError] }],
23+
partial: [{ index: 2, status: 'fail', errors: [mockedError] }],
24+
success: [{ index: 1, status: 'success', port: 1233, host: 'localhost' }]
25+
}
26+
render(<ResultsLog data={mockedData} />)
27+
28+
expect(screen.getByTestId('success-results-closed')).toBeInTheDocument()
29+
expect(screen.getByTestId('partial-results-closed')).toBeInTheDocument()
30+
expect(screen.getByTestId('failed-results-closed')).toBeInTheDocument()
31+
})
32+
33+
it('should open and collapse other groups', () => {
34+
const mockedData: ImportDatabasesData = {
35+
total: 3,
36+
fail: [{ index: 0, status: 'fail', errors: [mockedError] }],
37+
partial: [{ index: 2, status: 'fail', errors: [mockedError] }],
38+
success: [{ index: 1, status: 'success', port: 1233, host: 'localhost' }]
39+
}
40+
render(<ResultsLog data={mockedData} />)
41+
42+
fireEvent.click(
43+
within(screen.getByTestId('success-results-closed')).getByRole('button')
44+
)
45+
expect(screen.getByTestId('success-results-open')).toBeInTheDocument()
46+
47+
expect(screen.getByTestId('partial-results-closed')).toBeInTheDocument()
48+
expect(screen.getByTestId('failed-results-closed')).toBeInTheDocument()
49+
50+
fireEvent.click(
51+
within(screen.getByTestId('failed-results-closed')).getByRole('button')
52+
)
53+
expect(screen.getByTestId('failed-results-open')).toBeInTheDocument()
54+
55+
expect(screen.getByTestId('partial-results-closed')).toBeInTheDocument()
56+
expect(screen.getByTestId('success-results-closed')).toBeInTheDocument()
57+
58+
fireEvent.click(
59+
within(screen.getByTestId('partial-results-closed')).getByRole('button')
60+
)
61+
expect(screen.getByTestId('partial-results-open')).toBeInTheDocument()
62+
63+
expect(screen.getByTestId('failed-results-closed')).toBeInTheDocument()
64+
expect(screen.getByTestId('success-results-closed')).toBeInTheDocument()
65+
})
66+
67+
it('should show proper items length', () => {
68+
const mockedData: ImportDatabasesData = {
69+
total: 4,
70+
fail: [{ index: 0, status: 'fail', errors: [mockedError] }],
71+
partial: [{ index: 2, status: 'fail', errors: [mockedError] }],
72+
success: [
73+
{ index: 1, status: 'success', port: 1233, host: 'localhost' },
74+
{ index: 3, status: 'success', port: 1233, host: 'localhost' }
75+
]
76+
}
77+
render(<ResultsLog data={mockedData} />)
78+
79+
expect(
80+
within(screen.getByTestId('success-results-closed')).getByTestId('number-of-dbs')
81+
).toHaveTextContent('2')
82+
expect(
83+
within(screen.getByTestId('partial-results-closed')).getByTestId('number-of-dbs')
84+
).toHaveTextContent('1')
85+
expect(
86+
within(screen.getByTestId('failed-results-closed')).getByTestId('number-of-dbs')
87+
).toHaveTextContent('1')
88+
})
89+
90+
it('should call proper telemetry event after click', () => {
91+
const sendEventTelemetryMock = jest.fn();
92+
(sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock)
93+
94+
const mockedData: ImportDatabasesData = {
95+
total: 3,
96+
fail: [{ index: 0, status: 'fail', errors: [mockedError] }],
97+
partial: [{ index: 2, status: 'fail', errors: [mockedError] }],
98+
success: [{ index: 1, status: 'success', port: 1233, host: 'localhost' }]
99+
}
100+
render(<ResultsLog data={mockedData} />)
101+
102+
fireEvent.click(
103+
within(screen.getByTestId('success-results-closed')).getByRole('button')
104+
)
105+
106+
expect(sendEventTelemetry).toBeCalledWith({
107+
event: TelemetryEvent.CONFIG_DATABASES_REDIS_IMPORT_LOG_VIEWED,
108+
eventData: {
109+
length: 1,
110+
name: 'success'
111+
}
112+
});
113+
114+
(sendEventTelemetry as jest.Mock).mockRestore()
115+
116+
fireEvent.click(
117+
within(screen.getByTestId('success-results-open')).getByRole('button')
118+
)
119+
120+
expect(sendEventTelemetry).not.toBeCalled()
121+
})
122+
})
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { EuiCollapsibleNavGroup } from '@elastic/eui'
2+
import cx from 'classnames'
3+
import React, { useState } from 'react'
4+
5+
import { ImportDatabasesData } from 'uiSrc/slices/interfaces'
6+
import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'
7+
import TableResult from '../TableResult'
8+
9+
import styles from './styles.module.scss'
10+
11+
enum ResultsStatus {
12+
Success = 'success',
13+
Partial = 'partial',
14+
Failed = 'failed'
15+
}
16+
17+
export interface Props {
18+
data: ImportDatabasesData
19+
}
20+
21+
const ResultsLog = ({ data }: Props) => {
22+
const [openedNav, setOpenedNav] = useState<string>('')
23+
24+
const onToggle = (length: number = 0, isOpen: boolean, name: string) => {
25+
if (length === 0) return
26+
setOpenedNav(isOpen ? name : '')
27+
28+
if (isOpen) {
29+
sendEventTelemetry({
30+
event: TelemetryEvent.CONFIG_DATABASES_REDIS_IMPORT_LOG_VIEWED,
31+
eventData: {
32+
length,
33+
name
34+
}
35+
})
36+
}
37+
}
38+
39+
const CollapsibleNavTitle = ({ title, length = 0 }: { title: string, length: number }) => (
40+
<div className={styles.collapsibleNavTitle}>
41+
<span data-testid="nav-group-title">{title}</span>
42+
<span data-testid="number-of-dbs">{length}</span>
43+
</div>
44+
)
45+
46+
const getNavGroupState = (name: ResultsStatus) => (openedNav === name ? 'open' : 'closed')
47+
48+
return (
49+
<>
50+
<EuiCollapsibleNavGroup
51+
title={<CollapsibleNavTitle title="Fully imported" length={data?.success?.length} />}
52+
className={cx(styles.collapsibleNav, ResultsStatus.Success, { [styles.disabled]: !data?.success?.length })}
53+
isCollapsible
54+
initialIsOpen={false}
55+
onToggle={(isOpen) => onToggle(data?.success?.length, isOpen, ResultsStatus.Success)}
56+
forceState={getNavGroupState(ResultsStatus.Success)}
57+
data-testid={`success-results-${getNavGroupState(ResultsStatus.Success)}`}
58+
>
59+
<TableResult data={data?.success ?? []} />
60+
</EuiCollapsibleNavGroup>
61+
<EuiCollapsibleNavGroup
62+
title={<CollapsibleNavTitle title="Partially imported" length={data?.partial?.length} />}
63+
className={cx(styles.collapsibleNav, ResultsStatus.Partial, { [styles.disabled]: !data?.partial?.length })}
64+
isCollapsible
65+
initialIsOpen={false}
66+
onToggle={(isOpen) => onToggle(data?.partial?.length, isOpen, ResultsStatus.Partial)}
67+
forceState={getNavGroupState(ResultsStatus.Partial)}
68+
data-testid={`partial-results-${getNavGroupState(ResultsStatus.Partial)}`}
69+
>
70+
<TableResult data={data?.partial ?? []} />
71+
</EuiCollapsibleNavGroup>
72+
<EuiCollapsibleNavGroup
73+
title={<CollapsibleNavTitle title="Failed to import" length={data?.fail?.length} />}
74+
className={cx(styles.collapsibleNav, ResultsStatus.Failed, { [styles.disabled]: !data?.fail?.length })}
75+
isCollapsible
76+
initialIsOpen={false}
77+
onToggle={(isOpen) => onToggle(data?.fail?.length, isOpen, ResultsStatus.Failed)}
78+
forceState={getNavGroupState(ResultsStatus.Failed)}
79+
data-testid={`failed-results-${getNavGroupState(ResultsStatus.Failed)}`}
80+
>
81+
<TableResult data={data?.fail ?? []} />
82+
</EuiCollapsibleNavGroup>
83+
</>
84+
)
85+
}
86+
87+
export default ResultsLog
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import ResultsLog from './ResultsLog'
2+
3+
export default ResultsLog

0 commit comments

Comments
 (0)