diff --git a/redisinsight/ui/src/components/confirmation-popover/ConfirmationPopover.spec.tsx b/redisinsight/ui/src/components/confirmation-popover/ConfirmationPopover.spec.tsx new file mode 100644 index 0000000000..564b8d7c26 --- /dev/null +++ b/redisinsight/ui/src/components/confirmation-popover/ConfirmationPopover.spec.tsx @@ -0,0 +1,217 @@ +import React from 'react' +import { render, screen, fireEvent } from 'uiSrc/utils/test-utils' +import ConfirmationPopover, { ConfirmationPopoverProps } from './ConfirmationPopover' + +const mockClosePopover = jest.fn() +const mockButtonClick = jest.fn() + +const defaultProps: ConfirmationPopoverProps = { + isOpen: true, + closePopover: mockClosePopover, + button: Trigger, + confirmButton: ( + + Confirm + + ), +} + +const renderConfirmationPopover = (props: Partial = {}) => { + return render() +} + +describe('ConfirmationPopover', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('Basic Rendering', () => { + it('should render with required props only', () => { + renderConfirmationPopover() + + expect(screen.getByTestId('trigger-button')).toBeInTheDocument() + expect(screen.getByTestId('confirm-button')).toBeInTheDocument() + }) + + it('should render the confirm button', () => { + renderConfirmationPopover() + + const confirmButton = screen.getByTestId('confirm-button') + expect(confirmButton).toBeInTheDocument() + expect(confirmButton).toHaveTextContent('Confirm') + }) + + it('should not render title when not provided', () => { + renderConfirmationPopover() + + expect(screen.queryByRole('heading')).not.toBeInTheDocument() + }) + + it('should not render message when not provided', () => { + renderConfirmationPopover() + + expect(screen.queryByText(/message/i)).not.toBeInTheDocument() + }) + }) + + describe('Optional Props', () => { + it('should render title when provided', () => { + const title = 'Confirmation Required' + renderConfirmationPopover({ title }) + + expect(screen.getByText(title)).toBeInTheDocument() + }) + + it('should render message when provided', () => { + const message = 'Are you sure you want to proceed?' + renderConfirmationPopover({ message }) + + expect(screen.getByText(message)).toBeInTheDocument() + }) + + it('should render both title and message when provided', () => { + const title = 'Delete Item' + const message = 'This action cannot be undone.' + renderConfirmationPopover({ title, message }) + + expect(screen.getByText(title)).toBeInTheDocument() + expect(screen.getByText(message)).toBeInTheDocument() + }) + + it('should render with custom confirm button', () => { + const customConfirmButton = ( + + Delete Forever + + ) + renderConfirmationPopover({ confirmButton: customConfirmButton }) + + const customButton = screen.getByTestId('custom-confirm') + expect(customButton).toBeInTheDocument() + expect(customButton).toHaveTextContent('Delete Forever') + expect(customButton).toHaveClass('custom-class') + }) + }) + + describe('RiPopover Integration', () => { + it('should pass through isOpen prop to RiPopover', () => { + const { rerender } = renderConfirmationPopover({ isOpen: false }) + + expect(screen.queryByTestId('confirm-button')).not.toBeInTheDocument() + rerender() + expect(screen.getByTestId('confirm-button')).toBeInTheDocument() + }) + + it('should pass through closePopover prop to RiPopover', () => { + renderConfirmationPopover() + + fireEvent.click(document.body) + }) + + it('should pass through button prop to RiPopover', () => { + const customButton = Custom Trigger + renderConfirmationPopover({ button: customButton }) + + expect(screen.getByTestId('custom-trigger')).toBeInTheDocument() + expect(screen.getByTestId('custom-trigger')).toHaveTextContent('Custom Trigger') + }) + + it('should pass through additional RiPopover props', () => { + const additionalProps = { + anchorPosition: 'rightCenter' as const, + panelPaddingSize: 'l' as const, + 'data-testid': 'custom-popover', + } + + renderConfirmationPopover(additionalProps) + + expect(screen.getByTestId('custom-popover')).toBeInTheDocument() + }) + }) + + describe('User Interactions', () => { + it('should handle confirm button click', () => { + renderConfirmationPopover() + + const confirmButton = screen.getByTestId('confirm-button') + fireEvent.click(confirmButton) + + expect(mockButtonClick).toHaveBeenCalledTimes(1) + }) + + it('should handle trigger button interaction', () => { + const triggerClickMock = jest.fn() + const customTrigger = ( + + Trigger + + ) + + renderConfirmationPopover({ button: customTrigger }) + + const triggerButton = screen.getByTestId('trigger-button') + fireEvent.click(triggerButton) + + expect(triggerClickMock).toHaveBeenCalledTimes(1) + }) + + it('should not interfere with confirm button when disabled', () => { + const disabledConfirmButton = ( + + Confirm + + ) + + renderConfirmationPopover({ confirmButton: disabledConfirmButton }) + + const confirmButton = screen.getByTestId('confirm-button') + expect(confirmButton).toBeDisabled() + + fireEvent.click(confirmButton) + expect(mockButtonClick).not.toHaveBeenCalled() + }) + }) + + describe('Layout and Structure', () => { + it('should have correct structure with title and message', () => { + const title = 'Confirm Action' + const message = 'This is a test message' + renderConfirmationPopover({ title, message }) + + const titleElement = screen.getByText(title) + const messageElement = screen.getByText(message) + const confirmButton = screen.getByTestId('confirm-button') + + expect(titleElement).toBeInTheDocument() + expect(messageElement).toBeInTheDocument() + expect(confirmButton).toBeInTheDocument() + + const allElements = screen.getAllByText(/Confirm Action|This is a test message|Confirm/) + expect(allElements[0]).toBe(titleElement) + expect(allElements[1]).toBe(messageElement) + }) + + it('should maintain proper spacing between elements', () => { + const title = 'Test Title' + const message = 'Test Message' + renderConfirmationPopover({ title, message }) + + expect(screen.getByText(title)).toBeInTheDocument() + expect(screen.getByText(message)).toBeInTheDocument() + expect(screen.getByTestId('confirm-button')).toBeInTheDocument() + }) + + it('should handle long text content gracefully', () => { + const longTitle = 'This is a very long title that might wrap to multiple lines' + const longMessage = 'This is a very long message that should break properly with word-break styling' + + renderConfirmationPopover({ + title: longTitle, + message: longMessage + }) + + expect(screen.getByText(longTitle)).toBeInTheDocument() + expect(screen.getByText(longMessage)).toBeInTheDocument() + }) + }) +}) diff --git a/redisinsight/ui/src/components/confirmation-popover/ConfirmationPopover.tsx b/redisinsight/ui/src/components/confirmation-popover/ConfirmationPopover.tsx new file mode 100644 index 0000000000..4c1c7280fc --- /dev/null +++ b/redisinsight/ui/src/components/confirmation-popover/ConfirmationPopover.tsx @@ -0,0 +1,33 @@ +import React from 'react' +import { RiPopover, RiPopoverProps } from 'uiSrc/components' +import styled from 'styled-components' +import { Col, Row } from 'uiSrc/components/base/layout/flex' +import { Text, Title } from 'uiSrc/components/base/text' + +const PopoverContentWrapper = styled(Col)` + word-break: break-all; + max-width: 300px; +` + +export interface ConfirmationPopoverProps + extends Omit { + title?: string + message?: string + confirmButton: React.ReactNode +} + +const ConfirmationPopover = (props: ConfirmationPopoverProps) => { + const { title, message, confirmButton, ...rest } = props + + return ( + + + {title && {title}} + {message && {message}} + {confirmButton} + + + ) +} + +export default ConfirmationPopover diff --git a/redisinsight/ui/src/components/confirmation-popover/index.ts b/redisinsight/ui/src/components/confirmation-popover/index.ts new file mode 100644 index 0000000000..d1cba7dc6e --- /dev/null +++ b/redisinsight/ui/src/components/confirmation-popover/index.ts @@ -0,0 +1,3 @@ +import ConfirmationPopover from './ConfirmationPopover' + +export default ConfirmationPopover diff --git a/redisinsight/ui/src/components/index.ts b/redisinsight/ui/src/components/index.ts index cb255bd4ad..5b880242c6 100644 --- a/redisinsight/ui/src/components/index.ts +++ b/redisinsight/ui/src/components/index.ts @@ -29,6 +29,7 @@ import CodeBlock from './code-block' import ShowChildByCondition from './show-child-by-condition' import FeatureFlagComponent from './feature-flag-component' import AutoRefresh from './auto-refresh' +import ConfirmationPopover from './confirmation-popover' import { ModuleNotLoaded, FilterNotAvailable } from './messages' import RdiInstanceHeader from './rdi-instance-header' import { @@ -81,6 +82,7 @@ export { ModuleNotLoaded, FilterNotAvailable, AutoRefresh, + ConfirmationPopover, RdiInstanceHeader, RecommendationBody, RecommendationBadges, diff --git a/redisinsight/ui/src/components/inline-item-editor/InlineItemEditor.spec.tsx b/redisinsight/ui/src/components/inline-item-editor/InlineItemEditor.spec.tsx index e23f26d9d5..171f5a140b 100644 --- a/redisinsight/ui/src/components/inline-item-editor/InlineItemEditor.spec.tsx +++ b/redisinsight/ui/src/components/inline-item-editor/InlineItemEditor.spec.tsx @@ -88,7 +88,7 @@ describe('InlineItemEditor', () => { }) fireEvent.click(screen.getByTestId(/apply-btn/)) - expect(queryByTestId('approve-popover')).toBeInTheDocument() + expect(queryByTestId('confirm-popover')).toBeInTheDocument() expect(onApplyMock).not.toBeCalled() }) @@ -109,7 +109,7 @@ describe('InlineItemEditor', () => { }) fireEvent.click(screen.getByTestId(/apply-btn/)) - expect(queryByTestId('approve-popover')).toBeInTheDocument() + expect(queryByTestId('confirm-popover')).toBeInTheDocument() expect(onApplyMock).not.toBeCalled() fireEvent.click(screen.getByTestId(/save-btn/)) diff --git a/redisinsight/ui/src/components/inline-item-editor/InlineItemEditor.tsx b/redisinsight/ui/src/components/inline-item-editor/InlineItemEditor.tsx index 52c8938884..c0f2a79fe7 100644 --- a/redisinsight/ui/src/components/inline-item-editor/InlineItemEditor.tsx +++ b/redisinsight/ui/src/components/inline-item-editor/InlineItemEditor.tsx @@ -4,13 +4,13 @@ import cx from 'classnames' import { useTheme } from '@redis-ui/styles' import * as keys from 'uiSrc/constants/keys' -import { RiPopover, RiTooltip } from 'uiSrc/components/base' +import { RiTooltip } from 'uiSrc/components/base' import { FlexItem } from 'uiSrc/components/base/layout/flex' import { WindowEvent } from 'uiSrc/components/base/utils/WindowEvent' import { FocusTrap } from 'uiSrc/components/base/utils/FocusTrap' import { OutsideClickDetector } from 'uiSrc/components/base/utils' import { DestructiveButton } from 'uiSrc/components/base/forms/buttons' -import { Text } from 'uiSrc/components/base/text' +import ConfirmationPopover from 'uiSrc/components/confirmation-popover' import { ActionsContainer, @@ -276,7 +276,7 @@ const InlineItemEditor = (props: Props) => { )} {approveByValidation && ( - setIsShowApprovePopover(false)} @@ -286,38 +286,20 @@ const InlineItemEditor = (props: Props) => { )} panelClassName={cx(styles.popoverPanel)} button={ApplyBtn} - > - - - {!!approveText?.title && ( - - {approveText?.title} - - )} - - {approveText?.text} - - - - - Save - - - - + title={approveText?.title} + message={approveText?.text} + confirmButton={ + + Save + + } + /> )} diff --git a/redisinsight/ui/src/pages/browser/components/delete-key-popover/DeleteKeyPopover.tsx b/redisinsight/ui/src/pages/browser/components/delete-key-popover/DeleteKeyPopover.tsx index 97b48865d7..a479fa6fe2 100644 --- a/redisinsight/ui/src/pages/browser/components/delete-key-popover/DeleteKeyPopover.tsx +++ b/redisinsight/ui/src/pages/browser/components/delete-key-popover/DeleteKeyPopover.tsx @@ -1,5 +1,4 @@ import React from 'react' -import styled from 'styled-components' import cx from 'classnames' import { KeyTypes, ModulesKeyTypes } from 'uiSrc/constants' @@ -10,9 +9,7 @@ import { IconButton, } from 'uiSrc/components/base/forms/buttons' import { DeleteIcon } from 'uiSrc/components/base/icons' -import { Text, Title } from 'uiSrc/components/base/text' -import { RiPopover } from 'uiSrc/components/base' -import { Col, Row } from 'uiSrc/components/base/layout/flex' +import ConfirmationPopover from 'uiSrc/components/confirmation-popover' export interface DeleteProps { nameString: string @@ -25,11 +22,6 @@ export interface DeleteProps { onOpenPopover: (index: number, type: KeyTypes | ModulesKeyTypes) => void } -const PopoverContentWrapper = styled(Col)` - word-break: break-all; - max-width: 300px; -` - export const DeleteKeyPopover = ({ nameString, name, @@ -46,7 +38,7 @@ export const DeleteKeyPopover = ({ } return ( - } onClick={(e) => e.stopPropagation()} - > - - {formatLongName(nameString)} - will be deleted. - - onDelete(name)} - data-testid="submit-delete-key" - > - Delete - - - - + title={formatLongName(nameString)} + message="will be deleted." + confirmButton={ + onDelete(name)} + data-testid="submit-delete-key" + > + Delete + + } + /> ) } diff --git a/redisinsight/ui/src/pages/browser/modules/key-details-header/KeyDetailsHeader.tsx b/redisinsight/ui/src/pages/browser/modules/key-details-header/KeyDetailsHeader.tsx index bfb44bca56..f92b7941d2 100644 --- a/redisinsight/ui/src/pages/browser/modules/key-details-header/KeyDetailsHeader.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details-header/KeyDetailsHeader.tsx @@ -167,14 +167,16 @@ const KeyDetailsHeader = ({ - - - + + + + + void @@ -62,7 +60,7 @@ const KeyDetailsHeaderDelete = ({ onDelete }: Props) => { } return ( - { data-testid="delete-key-btn" /> } - > - - - - {tooltipContent} - - will be deleted. - - - onDelete(keyBuffer!)} - className={styles.popoverDeleteBtn} - data-testid="delete-key-confirm-btn" - > - Delete - - - - + title={tooltipContent} + message="will be deleted." + confirmButton={ + onDelete(keyBuffer!)} + data-testid="delete-key-confirm-btn" + > + Delete + + } + /> ) } diff --git a/redisinsight/ui/src/pages/browser/modules/key-details-header/styles.module.scss b/redisinsight/ui/src/pages/browser/modules/key-details-header/styles.module.scss index 285e7818f8..20b3d939ff 100644 --- a/redisinsight/ui/src/pages/browser/modules/key-details-header/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/modules/key-details-header/styles.module.scss @@ -44,7 +44,6 @@ } .container { - min-height: 108px; padding: 18px 18px 12px 18px; border-bottom: 1px solid var(--euiColorLightShade); min-width: 100%; diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/no-key-selected/NoKeySelected.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/no-key-selected/NoKeySelected.tsx index 871e15bb28..d95bd230d6 100644 --- a/redisinsight/ui/src/pages/browser/modules/key-details/components/no-key-selected/NoKeySelected.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/no-key-selected/NoKeySelected.tsx @@ -59,7 +59,7 @@ export const NoKeySelected = (props: Props) => { - + {error ? ( {error} ) : (