diff --git a/redisinsight/ui/src/components/cli/components/cli-body/CliBody/CliBody.tsx b/redisinsight/ui/src/components/cli/components/cli-body/CliBody/CliBody.tsx index b0a3ea2b72..92ea09205b 100644 --- a/redisinsight/ui/src/components/cli/components/cli-body/CliBody/CliBody.tsx +++ b/redisinsight/ui/src/components/cli/components/cli-body/CliBody/CliBody.tsx @@ -231,7 +231,7 @@ const CliBody = (props: Props) => {
{data}
{!error && !(loading || settingsLoading) ? ( - + { } const handleSubmit = () => { - const commandLine = decode(command).trim() + const [commandLine, countRepeat] = getCommandRepeat(decode(command).trim()) const unsupportedCommand = checkUnsupportedCommand(unsupportedCommands, commandLine) + dispatch(concatToOutput(cliCommandOutput(command))) + + if (!isRepeatCountCorrect(countRepeat)) { + dispatch(processUnrepeatableNumber(commandLine, resetCommand)) + return + } if (unsupportedCommand) { dispatch(processUnsupportedCommand(commandLine, unsupportedCommand, resetCommand)) return } - sendCommand(commandLine) + for (let i = 0; i < countRepeat; i++) { + sendCommand(commandLine) + } } const sendCommand = (command: string) => { diff --git a/redisinsight/ui/src/components/cli/components/cli-input/CliInputWrapper.tsx b/redisinsight/ui/src/components/cli/components/cli-input/CliInputWrapper.tsx index fba2233f58..bc849e6945 100644 --- a/redisinsight/ui/src/components/cli/components/cli-input/CliInputWrapper.tsx +++ b/redisinsight/ui/src/components/cli/components/cli-input/CliInputWrapper.tsx @@ -1,6 +1,7 @@ import { isUndefined } from 'lodash' import React from 'react' import { useSelector } from 'react-redux' +import { getCommandRepeat } from 'uiSrc/utils' import { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands' import CliAutocomplete from './CliAutocomplete' @@ -17,7 +18,9 @@ export interface Props { const CliInputWrapper = (props: Props) => { const { command = '', wordsTyped, setInputEl, setCommand, onKeyDown } = props const { spec: ALL_REDIS_COMMANDS } = useSelector(appRedisCommandsSelector) - const [firstCommand, secondCommand] = command.split(' ') + + const [commandLine, repeatCommand] = getCommandRepeat(command) + const [firstCommand, secondCommand] = commandLine.split(' ') const firstCommandMatch = firstCommand.toUpperCase() const secondCommandMatch = `${firstCommandMatch} ${secondCommand ? secondCommand.toUpperCase() : null}` @@ -35,7 +38,11 @@ const CliInputWrapper = (props: Props) => { onKeyDown={onKeyDown} /> {matchedCmd && ( - + )} ) diff --git a/redisinsight/ui/src/constants/cliOutput.tsx b/redisinsight/ui/src/constants/cliOutput.tsx index 1eaffc6b20..be22867c29 100644 --- a/redisinsight/ui/src/constants/cliOutput.tsx +++ b/redisinsight/ui/src/constants/cliOutput.tsx @@ -22,7 +22,7 @@ export const ConnectionSuccessOutputText = [ 'Connected.', '\n', 'Ready to execute commands.', - '\n\n\n', + '\n\n', ] const unsupportedCommandTextCli = ' is not supported by the RedisInsight CLI. The list of all unsupported commands: ' @@ -32,4 +32,5 @@ export const cliTexts = { commandLine + unsupportedCommandTextCli + commands, WORKBENCH_UNSUPPORTED_COMMANDS: (commandLine: string, commands: string) => commandLine + unsupportedCommandTextWorkbench + commands, + REPEAT_COUNT_INVALID: 'Invalid repeat command option value' } diff --git a/redisinsight/ui/src/pages/instance/InstancePage.tsx b/redisinsight/ui/src/pages/instance/InstancePage.tsx index bd90800939..a307381f38 100644 --- a/redisinsight/ui/src/pages/instance/InstancePage.tsx +++ b/redisinsight/ui/src/pages/instance/InstancePage.tsx @@ -8,14 +8,14 @@ import { fetchInstanceAction, getDatabaseConfigInfoAction, } from 'uiSrc/slices/instances' -import { BrowserStorageItem } from 'uiSrc/constants' -import { localStorageService } from 'uiSrc/services' import { appContextSelector, setAppContextConnectedInstanceId, setAppContextInitialState, } from 'uiSrc/slices/app/context' import { resetKeys } from 'uiSrc/slices/keys' +import { BrowserStorageItem } from 'uiSrc/constants' +import { localStorageService } from 'uiSrc/services' import { resetOutput } from 'uiSrc/slices/cli/cli-output' import { cliSettingsSelector } from 'uiSrc/slices/cli/cli-settings' import BottomGroupComponents from 'uiSrc/components/bottom-group-components/BottomGroupComponents' diff --git a/redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.tsx b/redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.tsx index 55de157c13..030981b575 100644 --- a/redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.tsx +++ b/redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.tsx @@ -13,6 +13,8 @@ import { cliParseTextResponse, splitMonacoValuePerLines, getMultiCommands, + isRepeatCountCorrect, + getCommandRepeat, } from 'uiSrc/utils' import { sendWBCommandAction, @@ -123,12 +125,21 @@ const WBViewWrapper = () => { } }, [historyItems]) - const getUnsupportedCommandResponse = (commandLine = '') => { + const getUnsupportedCommandResponse = (command = '') => { + const [commandLine, countRepeat] = getCommandRepeat(command) const { modules } = state.instance const { unsupportedCommands, blockingCommands } = state const unsupportedCommand = checkUnsupportedCommand(unsupportedCommands, commandLine) || checkBlockingCommand(blockingCommands, commandLine) + if (!isRepeatCountCorrect(countRepeat)) { + return cliParseTextResponse( + cliTexts.REPEAT_COUNT_INVALID, + commandLine, + CommandExecutionStatus.Fail, + ) + } + if (unsupportedCommand) { return cliParseTextResponse( cliTexts.WORKBENCH_UNSUPPORTED_COMMANDS( diff --git a/redisinsight/ui/src/slices/cli/cli-output.ts b/redisinsight/ui/src/slices/cli/cli-output.ts index 139bc87174..a79d9a7334 100644 --- a/redisinsight/ui/src/slices/cli/cli-output.ts +++ b/redisinsight/ui/src/slices/cli/cli-output.ts @@ -4,7 +4,6 @@ import { CliOutputFormatterType, cliTexts } from 'uiSrc/constants/cliOutput' import { apiService, localStorageService } from 'uiSrc/services' import { ApiEndpoints, BrowserStorageItem } from 'uiSrc/constants' import { - cliCommandOutput, cliParseTextResponseWithOffset, cliParseTextResponseWithRedirect, } from 'uiSrc/utils/cliHelper' @@ -92,7 +91,6 @@ export function sendCliCommandAction( const state = stateInit() const { id = '' } = state.connections?.instances?.connectedInstance - dispatch(concatToOutput(cliCommandOutput(command))) if (command === '') { onSuccessAction?.() return @@ -134,7 +132,6 @@ export function sendCliClusterCommandAction( const state = stateInit() const { id = '' } = state.connections.instances?.connectedInstance - dispatch(concatToOutput(cliCommandOutput(command))) if (command === '') { onSuccessAction?.() return @@ -192,8 +189,6 @@ export function processUnsupportedCommand( const state = stateInit() const { unsupportedCommands } = state.cli.settings - dispatch(concatToOutput(cliCommandOutput(command))) - dispatch( concatToOutput( cliParseTextResponseWithOffset( @@ -210,3 +205,22 @@ export function processUnsupportedCommand( onSuccessAction?.() } } + +export function processUnrepeatableNumber( + command: string = '', + onSuccessAction?: () => void +) { + return async (dispatch: AppDispatch) => { + dispatch( + concatToOutput( + cliParseTextResponseWithOffset( + cliTexts.REPEAT_COUNT_INVALID, + command, + CommandExecutionStatus.Fail + ) + ) + ) + + onSuccessAction?.() + } +} diff --git a/redisinsight/ui/src/slices/tests/cli/cli-output.spec.ts b/redisinsight/ui/src/slices/tests/cli/cli-output.spec.ts index b9ec78d88c..8c4d2b6766 100644 --- a/redisinsight/ui/src/slices/tests/cli/cli-output.spec.ts +++ b/redisinsight/ui/src/slices/tests/cli/cli-output.spec.ts @@ -177,7 +177,6 @@ describe('cliOutput slice', () => { // Assert const expectedActions = [ - concatToOutput(cliCommandOutput(command)), concatToOutput( cliParseTextResponseWithOffset( cliTexts.CLI_UNSUPPORTED_COMMANDS(command, unsupportedCommands.join(', ')), @@ -210,7 +209,6 @@ describe('cliOutput slice', () => { // Assert const expectedActions = [ - concatToOutput(cliCommandOutput(command)), sendCliCommand(), sendCliCommandSuccess(), concatToOutput(cliParseTextResponseWithOffset(data.response, command, data.status)), @@ -234,7 +232,6 @@ describe('cliOutput slice', () => { // Assert const expectedActions = [ - concatToOutput(cliCommandOutput(command)), sendCliCommand(), sendCliCommandSuccess(), concatToOutput(cliParseTextResponseWithOffset(data.response, command, data.status)), @@ -261,7 +258,6 @@ describe('cliOutput slice', () => { // Assert const expectedActions = [ - concatToOutput(cliCommandOutput(command)), sendCliCommand(), sendCliCommandFailure(responsePayload.response.data.message), concatToOutput(cliParseTextResponseWithOffset(errorMessage, command, CommandExecutionStatus.Fail)), @@ -300,7 +296,6 @@ describe('cliOutput slice', () => { // Assert const expectedActions = [ - concatToOutput(cliCommandOutput(command)), sendCliCommand(), sendCliCommandSuccess(), concatToOutput( @@ -331,7 +326,6 @@ describe('cliOutput slice', () => { // Assert const expectedActions = [ - concatToOutput(cliCommandOutput(command)), sendCliCommand(), sendCliCommandSuccess(), concatToOutput( @@ -361,7 +355,6 @@ describe('cliOutput slice', () => { // Assert const expectedActions = [ - concatToOutput(cliCommandOutput(command)), sendCliCommand(), sendCliCommandFailure(responsePayload.response.data.message), concatToOutput(cliParseTextResponseWithOffset(errorMessage, command, CommandExecutionStatus.Fail)), diff --git a/redisinsight/ui/src/utils/cliHelper.tsx b/redisinsight/ui/src/utils/cliHelper.tsx index 38daa69140..4459f7b00c 100644 --- a/redisinsight/ui/src/utils/cliHelper.tsx +++ b/redisinsight/ui/src/utils/cliHelper.tsx @@ -28,14 +28,14 @@ const cliParseTextResponseWithRedirect = ( const { host, port, slot } = redirectTo redirectMessage = `-> Redirected to slot [${slot}] located at ${host}:${port}` } - return [redirectMessage, '\n', cliParseTextResponse(text, command, status), '\n\n'] + return [redirectMessage, '\n', cliParseTextResponse(text, command, status), '\n'] } const cliParseTextResponseWithOffset = ( text: string = '', command: string = '', status: CommandExecutionStatus = CommandExecutionStatus.Success, -) => [cliParseTextResponse(text, command, status), '\n\n'] +) => [cliParseTextResponse(text, command, status), '\n'] const cliParseTextResponse = ( text: string = '', @@ -60,7 +60,7 @@ const cliParseTextResponse = ( ) -const cliCommandOutput = (command: string) => [bashTextValue(), cliCommandWrapper(command), '\n'] +const cliCommandOutput = (command: string) => ['\n', bashTextValue(), cliCommandWrapper(command), '\n'] const bashTextValue = () => '> ' diff --git a/redisinsight/ui/src/utils/commands.ts b/redisinsight/ui/src/utils/commands.ts index 0435a12e2a..7af2896d82 100644 --- a/redisinsight/ui/src/utils/commands.ts +++ b/redisinsight/ui/src/utils/commands.ts @@ -1,4 +1,4 @@ -import { flatten, isArray, isEmpty, reject } from 'lodash' +import { flatten, isArray, isEmpty, isNumber, reject, toNumber, isNaN, isInteger } from 'lodash' import { CommandArgsType, CommandGroup, @@ -86,3 +86,17 @@ export const getDocUrlForCommand = ( return `https://redis.io/commands/${command}` } } + +export const getCommandRepeat = (command = ''): [string, number] => { + const [countRepeatStr, ...restCommand] = command.split(' ') + let countRepeat = toNumber(countRepeatStr) + let commandLine = restCommand.join(' ') + if (!isNumber(countRepeat) || isNaN(countRepeat) || !command) { + countRepeat = 1 + commandLine = command + } + + return [commandLine, countRepeat] +} + +export const isRepeatCountCorrect = (number: number): boolean => number >= 1 && isInteger(number) diff --git a/redisinsight/ui/src/utils/monacoRedisComplitionProvider.ts b/redisinsight/ui/src/utils/monacoRedisComplitionProvider.ts index 4bd7a830c4..39bc66297c 100644 --- a/redisinsight/ui/src/utils/monacoRedisComplitionProvider.ts +++ b/redisinsight/ui/src/utils/monacoRedisComplitionProvider.ts @@ -1,3 +1,4 @@ +import { isNaN } from 'lodash' import * as monacoEditor from 'monaco-editor' import { ICommand, ICommandArgGenerated, ICommands } from 'uiSrc/constants' import { generateArgs, generateArgsNames, getDocUrlForCommand } from 'uiSrc/utils/commands' @@ -65,12 +66,19 @@ monacoEditor.languages.CompletionItemProvider => { position: monacoEditor.Position ): monacoEditor.languages.CompletionList => { const word = model.getWordUntilPosition(position) + const line = model.getLineContent(position.lineNumber) + const indexOfSpace = line.indexOf(' ') + const range = { startLineNumber: position.lineNumber, endLineNumber: position.lineNumber, - startColumn: 1, - endColumn: word.endColumn + endColumn: word.endColumn, + startColumn: + word.startColumn > indexOfSpace && !isNaN(+line.slice(0, indexOfSpace)) + ? indexOfSpace + 2 + : 1, } + // display suggestions only for words that don't belong to a folding area if (!model.getValueInRange(range).startsWith(' ')) { return { diff --git a/redisinsight/ui/src/utils/monacoUtils.ts b/redisinsight/ui/src/utils/monacoUtils.ts index c0a830eb86..1b6560205b 100644 --- a/redisinsight/ui/src/utils/monacoUtils.ts +++ b/redisinsight/ui/src/utils/monacoUtils.ts @@ -3,6 +3,7 @@ import { isEmpty, isUndefined, reject } from 'lodash' import { ICommands } from 'uiSrc/constants' import { IMonacoCommand } from './monacoInterfaces' import { Nullable } from './types' +import { getCommandRepeat, isRepeatCountCorrect } from './commands' const COMMENT_SYMBOLS = '//' const BLANK_LINE_REGEX = /^\s*\n/gm @@ -21,7 +22,20 @@ const removeCommentsFromLine = (text: string = '', prefix: string = ''): string return prefix + text.replace(/\/\/.*/, '') } -export const splitMonacoValuePerLines = (command = '') => command.split(/\n(?=[^\s])/g) +export const splitMonacoValuePerLines = (command = '') => { + const linesResult: string[] = [] + const lines = command.split(/\n(?=[^\s])/g) + lines.forEach((line) => { + const [commandLine, countRepeat] = getCommandRepeat(line) + + if (!isRepeatCountCorrect(countRepeat)) { + linesResult.push(line) + return + } + linesResult.push(...Array(countRepeat).fill(commandLine)) + }) + return linesResult +} export const getMultiCommands = (commands:string[] = []) => reject(commands, isEmpty).join('\n') ?? '' diff --git a/redisinsight/ui/src/utils/tests/monacoUtils.spec.ts b/redisinsight/ui/src/utils/tests/monacoUtils.spec.ts index 9caad52b33..6f2aa26a47 100644 --- a/redisinsight/ui/src/utils/tests/monacoUtils.spec.ts +++ b/redisinsight/ui/src/utils/tests/monacoUtils.spec.ts @@ -1,4 +1,4 @@ -import { multilineCommandToOneLine, removeMonacoComments } from 'uiSrc/utils' +import { multilineCommandToOneLine, removeMonacoComments, splitMonacoValuePerLines } from 'uiSrc/utils' describe('removeMonacoComments', () => { const cases = [ @@ -68,3 +68,35 @@ describe('multilineCommandToOneLine', () => { } ) }) + +describe('splitMonacoValuePerLines', () => { + const cases = [ + // Multi commands + [ + 'get test\nget test2\nget bar', + ['get test', 'get test2', 'get bar'] + ], + // Multi commands a lot of lines + [ + 'get test\nget test2\nget bar\nget bar\nget bar\nget bar\nget bar\nget bar', + ['get test', 'get test2', 'get bar', 'get bar', 'get bar', 'get bar', 'get bar', 'get bar'] + ], + // Multi commands with repeating + [ + 'get test\n3 get test2\nget bar', + ['get test', 'get test2', 'get test2', 'get test2', 'get bar'] + ], + // Multi commands with repeating syntax error + [ + 'get test\n3get test2\nget bar', + ['get test', '3get test2', 'get bar'] + ], + ] + test.each(cases)( + 'given %p as argument, returns %p', + (arg: string, expectedResult) => { + const result = splitMonacoValuePerLines(arg) + expect(result).toEqual(expectedResult) + } + ) +})