diff --git a/redisinsight/ui/src/config/default.ts b/redisinsight/ui/src/config/default.ts index efacc5591b..70b3c9305f 100644 --- a/redisinsight/ui/src/config/default.ts +++ b/redisinsight/ui/src/config/default.ts @@ -84,6 +84,10 @@ export const defaultConfig = { 'RI_DATABASE_OVERVIEW_MINIMUM_REFRESH_INTERVAL', 1, ), + rejsonMonacoEditorMaxThreshold: intEnv( + 'RI_REJSON_MONACO_EDITOR_MAX_THRESHOLD', + 10_000, + ), }, features: { envDependent: { diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/change-editor-type-button/ChangeEditorTypeButton.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/change-editor-type-button/ChangeEditorTypeButton.spec.tsx index ae97c1de04..cf34b98d66 100644 --- a/redisinsight/ui/src/pages/browser/modules/key-details/components/change-editor-type-button/ChangeEditorTypeButton.spec.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/change-editor-type-button/ChangeEditorTypeButton.spec.tsx @@ -1,12 +1,16 @@ import React from 'react' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import ChangeEditorTypeButton, { ButtonMode } from './ChangeEditorTypeButton' + +import ChangeEditorTypeButton from './ChangeEditorTypeButton' const mockSwitchEditorType = jest.fn() +let mockIsTextEditorDisabled = false + jest.mock('./useChangeEditorType', () => ({ useChangeEditorType: () => ({ switchEditorType: mockSwitchEditorType, + isTextEditorDisabled: mockIsTextEditorDisabled, }), })) @@ -16,32 +20,38 @@ describe('ChangeEditorTypeButton', () => { }) it('should render an enabled button with default tooltip', async () => { + mockIsTextEditorDisabled = false + render() const button = screen.getByRole('button', { name: /change editor type/i }) expect(button).toBeEnabled() await userEvent.hover(button) - expect( await screen.findByText('Edit value in text editor'), ).toBeInTheDocument() }) - it('should render a disabled button with read-only tooltip', async () => { - render() + it('should render a disabled button with a tooltip', async () => { + mockIsTextEditorDisabled = true + + render() const button = screen.getByRole('button', { name: /change editor type/i }) expect(button).toBeDisabled() await userEvent.hover(button) - expect( - await screen.findByText('This JSON is too large to edit'), + await screen.findByText( + 'This JSON document is too large to view or edit in full.', + ), ).toBeInTheDocument() }) it('should call switchEditorType on click when not disabled', async () => { + mockIsTextEditorDisabled = false + render() const button = screen.getByRole('button', { name: /change editor type/i }) @@ -51,7 +61,9 @@ describe('ChangeEditorTypeButton', () => { }) it('should not call switchEditorType when disabled', async () => { - render() + mockIsTextEditorDisabled = true + + render() const button = screen.getByRole('button', { name: /change editor type/i }) await userEvent.click(button) diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/change-editor-type-button/ChangeEditorTypeButton.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/change-editor-type-button/ChangeEditorTypeButton.tsx index 6243117d94..f1ed5317b9 100644 --- a/redisinsight/ui/src/pages/browser/modules/key-details/components/change-editor-type-button/ChangeEditorTypeButton.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/change-editor-type-button/ChangeEditorTypeButton.tsx @@ -2,24 +2,12 @@ import React from 'react' import { EuiButtonIcon, EuiToolTip } from '@elastic/eui' import { useChangeEditorType } from './useChangeEditorType' -export enum ButtonMode { - editable = 'editable', - readOnly = 'readOnly', -} - -export type ChangeEditorTypeButtonProps = { - mode?: ButtonMode -} - -const ChangeEditorTypeButton = ({ - mode = ButtonMode.editable, -}: ChangeEditorTypeButtonProps) => { - const { switchEditorType } = useChangeEditorType() - const isReadMode = mode === ButtonMode.readOnly +const ChangeEditorTypeButton = () => { + const { switchEditorType, isTextEditorDisabled } = useChangeEditorType() - const isDisabled = isReadMode - const tooltip = isReadMode - ? 'This JSON is too large to edit' + const isDisabled = isTextEditorDisabled + const tooltip = isTextEditorDisabled + ? 'This JSON document is too large to view or edit in full.' : 'Edit value in text editor' return ( diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/change-editor-type-button/index.ts b/redisinsight/ui/src/pages/browser/modules/key-details/components/change-editor-type-button/index.ts index 1f50dcadb4..a3834f345f 100644 --- a/redisinsight/ui/src/pages/browser/modules/key-details/components/change-editor-type-button/index.ts +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/change-editor-type-button/index.ts @@ -1,5 +1,4 @@ import ChangeEditorTypeButton from './ChangeEditorTypeButton' -export { ButtonMode as ChangeEditorTypeButtonMode } from './ChangeEditorTypeButton' export { useChangeEditorType } from './useChangeEditorType' export default ChangeEditorTypeButton diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/change-editor-type-button/useChangeEditorType.spec.ts b/redisinsight/ui/src/pages/browser/modules/key-details/components/change-editor-type-button/useChangeEditorType.spec.ts index 77a05620d7..2200e59b8b 100644 --- a/redisinsight/ui/src/pages/browser/modules/key-details/components/change-editor-type-button/useChangeEditorType.spec.ts +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/change-editor-type-button/useChangeEditorType.spec.ts @@ -1,6 +1,8 @@ import * as reactRedux from 'react-redux' import { renderHook, act } from '@testing-library/react-hooks' import { EditorType } from 'uiSrc/slices/interfaces' +import { FeatureFlags } from 'uiSrc/constants' + import { useChangeEditorType } from './useChangeEditorType' jest.mock('react-redux', () => ({ @@ -56,4 +58,62 @@ describe('useChangeEditorType', () => { payload: EditorType.Default, }) }) + + describe('isTextEditorDisabled', () => { + it('should be false when isWithinThreshold is true', () => { + mockedUseSelector + .mockImplementationOnce(() => ({ + editorType: EditorType.Default, + isWithinThreshold: true, + })) + .mockImplementationOnce(() => ({ + [FeatureFlags.envDependent]: { flag: false }, + })) + + const { result } = renderHook(() => useChangeEditorType()) + expect(result.current.isTextEditorDisabled).toBe(false) + }) + + it('should be false when not within threshold but feature flag is true', () => { + mockedUseSelector + .mockImplementationOnce(() => ({ + editorType: EditorType.Default, + isWithinThreshold: false, + })) + .mockImplementationOnce(() => ({ + [FeatureFlags.envDependent]: { flag: true }, + })) + + const { result } = renderHook(() => useChangeEditorType()) + expect(result.current.isTextEditorDisabled).toBe(false) + }) + + it('should be true when not within threshold and feature flag is false', () => { + mockedUseSelector + .mockImplementationOnce(() => ({ + editorType: EditorType.Default, + isWithinThreshold: false, + })) + .mockImplementationOnce(() => ({ + [FeatureFlags.envDependent]: { flag: false }, + })) + + const { result } = renderHook(() => useChangeEditorType()) + expect(result.current.isTextEditorDisabled).toBe(true) + }) + + it('should be true when envDependentFeature is undefined', () => { + mockedUseSelector + .mockImplementationOnce(() => ({ + editorType: EditorType.Default, + isWithinThreshold: false, + })) + .mockImplementationOnce(() => ({ + [FeatureFlags.envDependent]: undefined, + })) + + const { result } = renderHook(() => useChangeEditorType()) + expect(result.current.isTextEditorDisabled).toBe(true) + }) + }) }) diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/change-editor-type-button/useChangeEditorType.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/change-editor-type-button/useChangeEditorType.tsx index 1c77b1dca4..893b13ce30 100644 --- a/redisinsight/ui/src/pages/browser/modules/key-details/components/change-editor-type-button/useChangeEditorType.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/change-editor-type-button/useChangeEditorType.tsx @@ -1,11 +1,18 @@ import { useCallback } from 'react' import { useSelector, useDispatch } from 'react-redux' +import { FeatureFlags } from 'uiSrc/constants' +import { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features' import { rejsonSelector, setEditorType } from 'uiSrc/slices/browser/rejson' import { EditorType } from 'uiSrc/slices/interfaces' export const useChangeEditorType = () => { const dispatch = useDispatch() - const { editorType } = useSelector(rejsonSelector) + const { editorType, isWithinThreshold } = useSelector(rejsonSelector) + const { [FeatureFlags.envDependent]: envDependentFeature } = useSelector( + appFeatureFlagsFeaturesSelector, + ) + + const isTextEditorDisabled = !isWithinThreshold && !envDependentFeature?.flag const switchEditorType = useCallback(() => { const opposite = @@ -13,5 +20,5 @@ export const useChangeEditorType = () => { dispatch(setEditorType(opposite)) }, [dispatch, editorType]) - return { switchEditorType, editorType } + return { switchEditorType, editorType, isTextEditorDisabled } } diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/monaco-editor/MonacoEditor.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/monaco-editor/MonacoEditor.spec.tsx index e6fe514993..479f2042a0 100644 --- a/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/monaco-editor/MonacoEditor.spec.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/monaco-editor/MonacoEditor.spec.tsx @@ -10,6 +10,11 @@ const mockStore = configureStore({ browser: { rejson: {}, }, + app: { + features: { + featureFlags: { features: { envDependent: { flag: true } } }, + }, + }, }), }) diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/rejson-details/RejsonDetails.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/rejson-details/RejsonDetails.tsx index 7a60ec4107..148e404795 100644 --- a/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/rejson-details/RejsonDetails.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/rejson-details/RejsonDetails.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react' -import { useDispatch, useSelector } from 'react-redux' +import { useDispatch } from 'react-redux' import { EuiButtonIcon } from '@elastic/eui' import cx from 'classnames' import { @@ -9,16 +9,12 @@ import { setReJSONDataAction, } from 'uiSrc/slices/browser/rejson' import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' -import { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features' -import { FeatureFlags } from 'uiSrc/constants' import { getBrackets, isRealArray, isRealObject, wrapPath } from '../utils' import { BaseProps, ObjectTypes } from '../interfaces' import RejsonDynamicTypes from '../rejson-dynamic-types' import { AddItem } from '../components' -import ChangeEditorTypeButton, { - ChangeEditorTypeButtonMode, -} from '../../change-editor-type-button' +import ChangeEditorTypeButton from '../../change-editor-type-button' import styles from '../styles.module.scss' @@ -36,10 +32,6 @@ const RejsonDetails = (props: BaseProps) => { const [addRootKVPair, setAddRootKVPair] = useState(false) - const { [FeatureFlags.envDependent]: envDependentFeature } = useSelector( - appFeatureFlagsFeaturesSelector, - ) - const dispatch = useDispatch() const handleFetchVisualisationResults = ( @@ -104,13 +96,7 @@ const RejsonDetails = (props: BaseProps) => { 'start', )} - + )} (fetchReJSON(key, '$', data.length, resetData)) dispatch(setEditorType(EditorType.Default)) + dispatch( + setIsWithinThreshold( + data.size <= riConfig.browser.rejsonMonacoEditorMaxThreshold, + ), + ) } if (data.type === KeyTypes.Stream) { const { viewType } = state.browser.stream diff --git a/redisinsight/ui/src/slices/browser/rejson.ts b/redisinsight/ui/src/slices/browser/rejson.ts index e9b648883a..ef7e49ddb3 100644 --- a/redisinsight/ui/src/slices/browser/rejson.ts +++ b/redisinsight/ui/src/slices/browser/rejson.ts @@ -48,6 +48,7 @@ export const initialState: InitialStateRejson = { type: '', }, editorType: EditorType.Default, + isWithinThreshold: false, } // A slice for recipes @@ -114,6 +115,9 @@ const rejsonSlice = createSlice({ setEditorType: (state, { payload }: PayloadAction) => { state.editorType = payload }, + setIsWithinThreshold: (state, { payload }: PayloadAction) => { + state.isWithinThreshold = payload + }, }, }) @@ -132,6 +136,7 @@ export const { removeRejsonKeySuccess, removeRejsonKeyFailure, setEditorType, + setIsWithinThreshold, } = rejsonSlice.actions // A selector diff --git a/redisinsight/ui/src/slices/interfaces/instances.ts b/redisinsight/ui/src/slices/interfaces/instances.ts index 4d6dd7f8c8..9d90aaa0de 100644 --- a/redisinsight/ui/src/slices/interfaces/instances.ts +++ b/redisinsight/ui/src/slices/interfaces/instances.ts @@ -497,6 +497,7 @@ export interface InitialStateRejson { error: Nullable data: GetRejsonRlResponse editorType: EditorType + isWithinThreshold: boolean } export interface ICredentialsRedisCluster { diff --git a/redisinsight/ui/src/slices/tests/browser/keys.spec.ts b/redisinsight/ui/src/slices/tests/browser/keys.spec.ts index 6a5bf392eb..075b67cb7a 100644 --- a/redisinsight/ui/src/slices/tests/browser/keys.spec.ts +++ b/redisinsight/ui/src/slices/tests/browser/keys.spec.ts @@ -1,9 +1,9 @@ import { cloneDeep } from 'lodash' import { AxiosError } from 'axios' import { configureStore } from '@reduxjs/toolkit' +import { getConfig } from 'uiSrc/config' import { BrowserColumns, - DEFAULT_SHOWN_COLUMNS, KeyTypes, KeyValueFormat, ModulesKeyTypes, @@ -19,7 +19,6 @@ import { clearStoreActions, initialStateDefault, mockedStore, - mockStore, } from 'uiSrc/utils/test-utils' import { addErrorNotification, @@ -33,7 +32,10 @@ import { } from 'uiSrc/slices/app/context' import { MOCK_TIMESTAMP } from 'uiSrc/mocks/data/dateNow' import { rootReducer } from 'uiSrc/slices/store' -import { setEditorType } from 'uiSrc/slices/browser/rejson' +import { + setEditorType, + setIsWithinThreshold, +} from 'uiSrc/slices/browser/rejson' import { EditorType } from 'uiSrc/slices/interfaces' import { CreateHashWithExpireDto } from 'apiSrc/modules/browser/hash/dto' import { @@ -109,6 +111,9 @@ import reducer, { refreshKey, } from '../../browser/keys' +const riConfig = getConfig() +const REJSON_THRESHOLD = riConfig.browser.rejsonMonacoEditorMaxThreshold + jest.mock('uiSrc/services', () => ({ ...jest.requireActual('uiSrc/services'), })) @@ -1400,6 +1405,54 @@ describe('keys slice', () => { ]), ) }) + + it('should set isWithinThreshold to true when length is within threshold', async () => { + // Arrange + const data = { + name: stringToBuffer('rejson'), + type: KeyTypes.ReJSON, + ttl: -1, + size: REJSON_THRESHOLD, + length: REJSON_THRESHOLD + 100, // just to make sure this isn't used instead of size + } + const responsePayload = { data, status: 200 } + + apiService.post = jest.fn().mockResolvedValue(responsePayload) + + // Act + await store.dispatch(fetchKeyInfo(data.name)) + + // Assert + expect(store.getActions()).toEqual( + expect.arrayContaining([ + expect.objectContaining(setIsWithinThreshold(true)), + ]), + ) + }) + + it('should set isWithinThreshold to false when length exceeds threshold', async () => { + // Arrange + const data = { + name: stringToBuffer('rejson'), + type: KeyTypes.ReJSON, + ttl: -1, + size: REJSON_THRESHOLD + 1, + length: REJSON_THRESHOLD, // just to make sure this isn't used instead of size + } + const responsePayload = { data, status: 200 } + + apiService.post = jest.fn().mockResolvedValue(responsePayload) + + // Act + await store.dispatch(fetchKeyInfo(data.name)) + + // Assert + expect(store.getActions()).toEqual( + expect.arrayContaining([ + expect.objectContaining(setIsWithinThreshold(false)), + ]), + ) + }) }) describe('refreshKeyInfoAction', () => {