Skip to content

Commit c04c564

Browse files
Merge pull request #3487 from RedisInsight/fe/feature/RI-5211_add_promt_popup
#RI-5211 - add confirm leave the page popup
2 parents 9541182 + 9c14448 commit c04c564

File tree

13 files changed

+256
-14
lines changed

13 files changed

+256
-14
lines changed

redisinsight/ui/src/mocks/data/rdi.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { PipelineStatus } from 'uiSrc/slices/interfaces'
1+
import { IPipelineStatus, PipelineStatus } from 'uiSrc/slices/interfaces'
22

33
export const MOCK_RDI_PIPELINE_CONFIG = `connections:
44
target:
@@ -52,10 +52,10 @@ export const MOCK_RDI_PIPELINE_JSON_DATA = {
5252
}
5353
}
5454

55-
export const MOCK_RDI_PIPELINE_STATUS_DATA = {
55+
export const MOCK_RDI_PIPELINE_STATUS_DATA: IPipelineStatus = {
5656
components: { processor: 'ready' },
5757
pipelines: {
58-
default: {
58+
defaults: {
5959
status: PipelineStatus.Starting,
6060
state: 'some',
6161
tasks: 'none',

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

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,16 @@ jest.mock('uiSrc/slices/app/context', () => ({
4242
}),
4343
}))
4444

45-
jest.mock('react-router-dom', () => ({
46-
...jest.requireActual('react-router-dom'),
47-
useHistory: () => ({
48-
push: jest.fn,
49-
}),
50-
}))
51-
5245
let store: typeof mockedStore
5346
beforeEach(() => {
5447
cleanup()
5548
store = cloneDeep(mockedStore)
5649
store.clearActions()
50+
51+
reactRouterDom.useHistory = jest.fn().mockReturnValue({
52+
push: jest.fn(),
53+
block: jest.fn(() => jest.fn())
54+
})
5755
})
5856

5957
/**
@@ -135,7 +133,10 @@ describe('InstancePage', () => {
135133

136134
it('should redirect to rdi pipeline management page', async () => {
137135
const pushMock = jest.fn()
138-
reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })
136+
reactRouterDom.useHistory = jest.fn().mockReturnValue({
137+
push: pushMock,
138+
block: jest.fn(() => jest.fn())
139+
})
139140

140141
reactRouterDom.useLocation = jest.fn().mockReturnValue({ pathname: Pages.rdiPipeline(RDI_INSTANCE_ID_MOCK) })
141142

@@ -157,7 +158,10 @@ describe('InstancePage', () => {
157158
})
158159

159160
const pushMock = jest.fn()
160-
reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })
161+
reactRouterDom.useHistory = jest.fn().mockReturnValue({
162+
push: pushMock,
163+
block: jest.fn(() => jest.fn())
164+
})
161165

162166
reactRouterDom.useLocation = jest.fn().mockReturnValue({ pathname: Pages.rdiPipeline(RDI_INSTANCE_ID_MOCK) })
163167

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import { IPipeline } from 'uiSrc/slices/interfaces'
2525
import { Nullable, pipelineToJson } from 'uiSrc/utils'
2626

2727
import InstancePageRouter from './InstancePageRouter'
28+
import { ConfirmLeavePagePopup } from './components'
29+
import { useUndeployedChangesPrompt } from './hooks'
2830

2931
export interface Props {
3032
routes: IRoute[]
@@ -43,6 +45,7 @@ const RdiInstancePage = ({ routes = [] }: Props) => {
4345
const { rdiInstanceId } = useParams<{ rdiInstanceId: string }>()
4446
const { lastPage, contextRdiInstanceId } = useSelector(appContextSelector)
4547
const { data } = useSelector(rdiPipelineSelector)
48+
const { showModal, handleCloseModal, handleConfirmLeave } = useUndeployedChangesPrompt()
4649

4750
const [initialFormValues, setInitialFormValues] = useState<IPipeline>(getInitialValues(data))
4851
const formikRef = useRef<FormikProps<IPipeline>>(null)
@@ -96,7 +99,10 @@ const RdiInstancePage = ({ routes = [] }: Props) => {
9699
onSubmit={onSubmit}
97100
innerRef={formikRef}
98101
>
99-
<InstancePageRouter routes={routes} />
102+
<>
103+
<InstancePageRouter routes={routes} />
104+
{showModal && <ConfirmLeavePagePopup onClose={handleCloseModal} onConfirm={handleConfirmLeave} />}
105+
</>
100106
</Formik>
101107
)
102108
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React from 'react'
2+
import { useFormikContext } from 'formik'
3+
import { render, fireEvent, screen } from 'uiSrc/utils/test-utils'
4+
import { MOCK_RDI_PIPELINE_DATA } from 'uiSrc/mocks/data/rdi'
5+
import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'
6+
import ConfirmLeavePagePopup, { Props } from './ConfirmLeavePagePopup'
7+
8+
const mockProps: Props = {
9+
onClose: jest.fn(),
10+
onConfirm: jest.fn()
11+
}
12+
13+
jest.mock('formik')
14+
15+
jest.mock('uiSrc/telemetry', () => ({
16+
...jest.requireActual('uiSrc/telemetry'),
17+
sendEventTelemetry: jest.fn()
18+
}))
19+
20+
describe('ConfirmLeavePagePopup', () => {
21+
beforeEach(() => {
22+
const mockUseFormikContext = {
23+
setFieldValue: jest.fn,
24+
values: MOCK_RDI_PIPELINE_DATA,
25+
};
26+
(useFormikContext as jest.Mock).mockReturnValue(mockUseFormikContext)
27+
})
28+
29+
it('should render', () => {
30+
expect(render(<ConfirmLeavePagePopup {...mockProps} />)).toBeTruthy()
31+
})
32+
33+
it('should call proper telemetry event', async () => {
34+
const sendEventTelemetryMock = jest.fn();
35+
(sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock)
36+
37+
render(<ConfirmLeavePagePopup {...mockProps} />)
38+
39+
expect(sendEventTelemetry).toBeCalledWith({
40+
event: TelemetryEvent.RDI_UNSAVED_CHANGES_MESSAGE_DISPLAYED,
41+
eventData: {
42+
id: 'rdiInstanceId',
43+
}
44+
});
45+
(sendEventTelemetry as jest.Mock).mockRestore()
46+
})
47+
48+
it('should call onConfirm', async () => {
49+
const onConfirmMock = jest.fn()
50+
render(<ConfirmLeavePagePopup {...mockProps} onConfirm={onConfirmMock} />)
51+
52+
fireEvent.click(screen.getByTestId('confirm-leave-page'))
53+
54+
expect(onConfirmMock).toBeCalled()
55+
})
56+
})
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import React, { useEffect } from 'react'
2+
import {
3+
EuiModal,
4+
EuiModalBody,
5+
EuiTitle, EuiText, EuiButton,
6+
} from '@elastic/eui'
7+
import { useParams } from 'react-router-dom'
8+
9+
import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'
10+
import Download from 'uiSrc/pages/rdi/pipeline-management/components/download/Download'
11+
12+
import styles from './styles.module.scss'
13+
14+
export interface Props {
15+
onClose: () => void
16+
onConfirm: () => void
17+
}
18+
19+
const ConfirmLeavePagePopup = (props: Props) => {
20+
const { onClose, onConfirm } = props
21+
22+
const { rdiInstanceId } = useParams<{ rdiInstanceId: string }>()
23+
24+
useEffect(() => {
25+
sendEventTelemetry({
26+
event: TelemetryEvent.RDI_UNSAVED_CHANGES_MESSAGE_DISPLAYED,
27+
eventData: {
28+
id: rdiInstanceId,
29+
}
30+
})
31+
}, [])
32+
33+
return (
34+
<EuiModal className={styles.container} onClose={onClose} data-testid="oauth-select-account-dialog">
35+
<EuiModalBody className={styles.modalBody}>
36+
<section className={styles.content}>
37+
<EuiTitle size="s">
38+
<h3 className={styles.title}>Leaving pipeline?</h3>
39+
</EuiTitle>
40+
<EuiText className={styles.text}>
41+
There are undeployed changes that could be lost if you navigate away.
42+
Consider downloading the pipeline to save changes locally.
43+
</EuiText>
44+
</section>
45+
<div className={styles.footer}>
46+
<Download />
47+
<EuiButton
48+
fill
49+
color="secondary"
50+
size="s"
51+
className={styles.button}
52+
onClick={() => onConfirm()}
53+
data-testid="confirm-leave-page"
54+
aria-labelledby="confirm leave the page"
55+
>
56+
Proceed
57+
</EuiButton>
58+
</div>
59+
</EuiModalBody>
60+
</EuiModal>
61+
)
62+
}
63+
64+
export default ConfirmLeavePagePopup
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import ConfirmLeavePagePopup from './ConfirmLeavePagePopup'
2+
3+
export default ConfirmLeavePagePopup
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
.container {
2+
width: 612px !important;
3+
min-width: 612px !important;
4+
padding: 40px;
5+
background-color: var(--euiColorEmptyShade);
6+
}
7+
8+
.title {
9+
font-size: 28px !important;
10+
font-style: normal;
11+
font-weight: 500;
12+
line-height: 34px;
13+
padding: 16px 0;
14+
}
15+
16+
.text {
17+
font: normal normal normal 14px/17px Graphik, sans-serif !important;
18+
color: var(--euiTextSubduedColor) !important;
19+
}
20+
21+
.footer {
22+
display: flex;
23+
justify-content: space-between;
24+
align-items: center;
25+
padding-top: 40px;
26+
padding-bottom: 1px;
27+
}
28+
29+
.button {
30+
height: 36px !important;
31+
32+
:global(.euiButtonContent.euiButton__content) {
33+
padding: 0 16px;
34+
}
35+
}
36+
37+
:global {
38+
.euiModal__closeIcon {
39+
right: 35px;
40+
top: 35px;
41+
}
42+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import ConfirmLeavePagePopup from './confirm-leave-page-popup'
2+
3+
export {
4+
ConfirmLeavePagePopup
5+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './useUndeployedChangesPrompt'
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { useEffect, useState } from 'react'
2+
import { useSelector } from 'react-redux'
3+
import { useHistory, useParams } from 'react-router-dom'
4+
import { Location } from 'history'
5+
import { rdiPipelineSelector } from 'uiSrc/slices/rdi/pipeline'
6+
import { Pages } from 'uiSrc/constants'
7+
import { Nullable } from 'uiSrc/utils'
8+
9+
export const useUndeployedChangesPrompt = () => {
10+
const { changes } = useSelector(rdiPipelineSelector)
11+
const [showModal, setShowModal] = useState<boolean>(false)
12+
const [nextLocation, setNextLocation] = useState<Nullable<Location<unknown>>>(null)
13+
const [shouldBlockLeaving, setShouldBlockLeaving] = useState<boolean>(false)
14+
15+
const { rdiInstanceId } = useParams<{ rdiInstanceId: string }>()
16+
const history = useHistory()
17+
18+
useEffect(() => {
19+
setShouldBlockLeaving(!!Object.keys(changes).length)
20+
}, [changes])
21+
22+
useEffect(() => {
23+
// @ts-ignore
24+
const unlistenBlockChecker = history.block((location: Location<unknown>) => {
25+
if (shouldBlockLeaving && !location?.pathname.startsWith(Pages.rdiPipeline(rdiInstanceId))) {
26+
setNextLocation(location)
27+
setShowModal(true)
28+
return false
29+
}
30+
return true
31+
})
32+
33+
return () => {
34+
unlistenBlockChecker()
35+
}
36+
}, [shouldBlockLeaving])
37+
38+
const handleCloseModal = () => {
39+
setShowModal(false)
40+
setNextLocation(null)
41+
}
42+
43+
const handleConfirmLeave = () => {
44+
setShowModal(false)
45+
setShouldBlockLeaving(false)
46+
}
47+
48+
useEffect(() => {
49+
if (!shouldBlockLeaving && nextLocation) {
50+
history.push(nextLocation.pathname)
51+
}
52+
}, [shouldBlockLeaving, nextLocation, history])
53+
54+
return {
55+
showModal,
56+
handleCloseModal,
57+
handleConfirmLeave
58+
}
59+
}

0 commit comments

Comments
 (0)