diff --git a/redisinsight/menu.ts b/redisinsight/menu.ts index 2522cff033..d7e336b4fd 100644 --- a/redisinsight/menu.ts +++ b/redisinsight/menu.ts @@ -156,7 +156,7 @@ export default class MenuBuilder { { label: 'License Terms', click() { - shell.openExternal('https://github.com/RedisInsight/RedisInsight/blob/master/LICENSE'); + shell.openExternal('https://github.com/RedisInsight/RedisInsight/blob/main/LICENSE'); }, }, { @@ -256,7 +256,7 @@ export default class MenuBuilder { { label: 'License Terms', click() { - shell.openExternal('https://github.com/RedisInsight/RedisInsight/blob/master/LICENSE'); + shell.openExternal('https://github.com/RedisInsight/RedisInsight/blob/main/LICENSE'); }, }, { diff --git a/redisinsight/ui/src/assets/img/play_icon.svg b/redisinsight/ui/src/assets/img/play_icon.svg new file mode 100644 index 0000000000..3bd5ecb48d --- /dev/null +++ b/redisinsight/ui/src/assets/img/play_icon.svg @@ -0,0 +1 @@ + diff --git a/redisinsight/ui/src/components/query/Query/Query.tsx b/redisinsight/ui/src/components/query/Query/Query.tsx index 2c638f2c6e..8617393146 100644 --- a/redisinsight/ui/src/components/query/Query/Query.tsx +++ b/redisinsight/ui/src/components/query/Query/Query.tsx @@ -1,12 +1,10 @@ -import React, { useContext, useEffect } from 'react' +import React, { useContext, useEffect, useRef } from 'react' import { useSelector } from 'react-redux' -import { findIndex } from 'lodash' -import { decode } from 'html-entities' +import { compact, findIndex } from 'lodash' import cx from 'classnames' import { EuiButtonIcon, EuiText, EuiToolTip } from '@elastic/eui' import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api' import MonacoEditor from 'react-monaco-editor' -import { useParams } from 'react-router-dom' import { Theme, @@ -15,17 +13,17 @@ import { KEYBOARD_SHORTCUTS, } from 'uiSrc/constants' import { - getMultiCommands, + decoration, + geMonacoAction, getRedisCompletionProvider, getRedisMonarchTokensProvider, - removeMonacoComments, - splitMonacoValuePerLines + MonacoAction, + Nullable, + toModelDeltaDecoration } from 'uiSrc/utils' -import { ThemeContext } from 'uiSrc/contexts/themeContext' -import { WBQueryType } from 'uiSrc/pages/workbench/constants' import { KeyboardShortcut } from 'uiSrc/components' +import { ThemeContext } from 'uiSrc/contexts/themeContext' import { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands' -import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import styles from './styles.module.scss' @@ -34,20 +32,26 @@ export interface Props { loading: boolean; setQueryEl: Function; setQuery: (script: string) => void; - onSubmit: (query?: string, historyId?: number, type?: WBQueryType) => void; + onSubmit: (query?: string) => void; onKeyDown?: (e: React.KeyboardEvent, script: string) => void; } +interface IEditorMount { + editor: monacoEditor.editor.IStandaloneCodeEditor + monaco: typeof monacoEditor +} + +let decorations: string[] = [] + const Query = (props: Props) => { const { query = '', setQuery, onKeyDown, onSubmit, setQueryEl } = props - const { instanceId = '' } = useParams<{ instanceId: string }>() const { commandsArray: REDIS_COMMANDS_ARRAY, spec: REDIS_COMMANDS_SPEC } = useSelector(appRedisCommandsSelector) - const editorRef = React.createRef() const { theme } = useContext(ThemeContext) + const monacoObjects = useRef>(null) let disposeCompletionItemProvider = () => {} useEffect(() => @@ -57,6 +61,27 @@ const Query = (props: Props) => { }, []) + useEffect(() => { + if (!monacoObjects.current) return + const commands = query.split('\n') + const { monaco, editor } = monacoObjects.current + const notCommandRegEx = /^[\s|//]/ + + const newDecorations = compact(commands.map((command, index) => { + if (!command || notCommandRegEx.test(command)) return null + const lineNumber = index + 1 + + return toModelDeltaDecoration( + decoration(monaco, `decoration_${lineNumber}`, lineNumber, 1, lineNumber, 1) + ) + })) + + decorations = editor.deltaDecorations( + decorations, + newDecorations + ) + }, [query]) + const onChange = (value: string = '') => { setQuery(value) } @@ -65,35 +90,7 @@ const Query = (props: Props) => { onKeyDown?.(e, query) } - const sendEventSubmitTelemetry = (commandInit = query) => { - const eventData = (() => { - const commands = splitMonacoValuePerLines(commandInit) - - const [commandLine, ...rest] = commands.map((command = '') => { - const matchedCommand = REDIS_COMMANDS_ARRAY.find((commandName) => - command.toUpperCase().startsWith(commandName)) - return matchedCommand ?? command.split(' ')?.[0] - }) - const multiCommands = getMultiCommands(rest) - - const command = removeMonacoComments(decode([commandLine, multiCommands].join('\n')).trim()) - - return { - command, - databaseId: instanceId, - multiple: multiCommands ? 'Multiple' : 'Single' - } - })() - - sendEventTelemetry({ - event: TelemetryEvent.WORKBENCH_COMMAND_SUBMITTED, - eventData - }) - } - const handleSubmit = (value?: string) => { - sendEventSubmitTelemetry(value) - onSubmit(value) } @@ -101,14 +98,13 @@ const Query = (props: Props) => { editor: monacoEditor.editor.IStandaloneCodeEditor, monaco: typeof monacoEditor ) => { + monacoObjects.current = { editor, monaco } + editor.focus() setQueryEl(editor) setupMonacoRedisLang(monaco) - editor.addCommand( - monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, - () => handleSubmit(editor.getValue()) - ) + editor.addAction(geMonacoAction(MonacoAction.Submit, (editor) => handleSubmit(editor.getValue()), monaco)) } const setupMonacoRedisLang = (monaco: typeof monacoEditor) => { @@ -134,6 +130,8 @@ const Query = (props: Props) => { padding: { top: 10 }, automaticLayout: true, formatOnPaste: false, + glyphMargin: true, + lineNumbersMinChars: 4 // fontFamily: 'Inconsolata', // fontSize: 16, // minimap: { @@ -145,11 +143,11 @@ const Query = (props: Props) => {
diff --git a/redisinsight/ui/src/components/query/QueryWrapper.tsx b/redisinsight/ui/src/components/query/QueryWrapper.tsx index 5bf95ce62d..f4888865e8 100644 --- a/redisinsight/ui/src/components/query/QueryWrapper.tsx +++ b/redisinsight/ui/src/components/query/QueryWrapper.tsx @@ -1,9 +1,13 @@ import React from 'react' import { useSelector } from 'react-redux' import { EuiLoadingContent } from '@elastic/eui' +import { decode } from 'html-entities' +import { useParams } from 'react-router-dom' + +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands' +import { getMultiCommands, removeMonacoComments, splitMonacoValuePerLines } from 'uiSrc/utils' import Query from './Query' - import styles from './Query/styles.module.scss' export interface Props { @@ -12,11 +16,46 @@ export interface Props { setQuery: (script: string) => void; setQueryEl: Function; onKeyDown?: (e: React.KeyboardEvent, script: string) => void; - onSubmit: () => void; + onSubmit: (value?: string) => void; } const QueryWrapper = (props: Props) => { const { query = '', loading, setQuery, setQueryEl, onKeyDown, onSubmit } = props - const { loading: isCommandsLoading } = useSelector(appRedisCommandsSelector) + const { instanceId = '' } = useParams<{ instanceId: string }>() + const { + loading: isCommandsLoading, + commandsArray: REDIS_COMMANDS_ARRAY, + } = useSelector(appRedisCommandsSelector) + + const sendEventSubmitTelemetry = (commandInit = query) => { + const eventData = (() => { + const commands = splitMonacoValuePerLines(commandInit) + + const [commandLine, ...rest] = commands.map((command = '') => { + const matchedCommand = REDIS_COMMANDS_ARRAY.find((commandName) => + command.toUpperCase().startsWith(commandName)) + return matchedCommand ?? command.split(' ')?.[0] + }) + const multiCommands = getMultiCommands(rest) + + const command = removeMonacoComments(decode([commandLine, multiCommands].join('\n')).trim()) + + return { + command, + databaseId: instanceId, + multiple: multiCommands ? 'Multiple' : 'Single' + } + })() + + sendEventTelemetry({ + event: TelemetryEvent.WORKBENCH_COMMAND_SUBMITTED, + eventData + }) + } + + const handleSubmit = (value?: string) => { + sendEventSubmitTelemetry(value) + onSubmit(value) + } const Placeholder = (
@@ -34,7 +73,7 @@ const QueryWrapper = (props: Props) => { setQuery={setQuery} setQueryEl={setQueryEl} onKeyDown={onKeyDown} - onSubmit={onSubmit} + onSubmit={handleSubmit} /> ) } diff --git a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesList/DatabasesList.tsx b/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesList/DatabasesList.tsx index aba608aa5b..3301e5eee5 100644 --- a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesList/DatabasesList.tsx +++ b/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesList/DatabasesList.tsx @@ -1,4 +1,6 @@ import { + Direction, + Criteria, EuiBasicTableColumn, EuiButton, EuiFlexGroup, @@ -44,7 +46,6 @@ function DatabasesList({ }: Props) { const [columns, setColumns] = useState(first(columnVariations)) const [selection, setSelection] = useState([]) - const [isPopoverOpen, setIsPopoverOpen] = useState(false) const { loading, data: instances } = useSelector(instancesSelector) @@ -112,6 +113,19 @@ function DatabasesList({ }), }) + const onTableChange = ({ sort, page }: Criteria) => { + // calls also with page changing + if (sort && !page) { + sendEventSortedTelemetry(sort) + } + } + + const sendEventSortedTelemetry = (sort: { field: keyof Instance; direction: Direction }) => + sendEventTelemetry({ + event: TelemetryEvent.CONFIG_DATABASES_DATABASE_LIST_SORTED, + eventData: sort + }) + const deleteBtn = ( diff --git a/redisinsight/ui/src/styles/base/_monaco.scss b/redisinsight/ui/src/styles/base/_monaco.scss index fad89d7547..06d1c62e41 100644 --- a/redisinsight/ui/src/styles/base/_monaco.scss +++ b/redisinsight/ui/src/styles/base/_monaco.scss @@ -16,3 +16,23 @@ font-size: 1.6em; } } + +.monaco-editor.redisLanguage-editor .suggest-icon { + display: none !important; +} + +.monaco-glyph-run-command { + color: var(--rsSubmitBtn); + opacity: .5 !important; + margin-left: 10px; + + &::before { + content: ''; + width: 12px; + height: 12px; + background-image: url('uiSrc/assets/img/play_icon.svg'); + background-size: contain; + font-size: 16px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + } +} diff --git a/redisinsight/ui/src/telemetry/events.ts b/redisinsight/ui/src/telemetry/events.ts index ea94ed9ef7..2911f670dc 100644 --- a/redisinsight/ui/src/telemetry/events.ts +++ b/redisinsight/ui/src/telemetry/events.ts @@ -7,6 +7,7 @@ export enum TelemetryEvent { CONFIG_DATABASES_MULTIPLE_DATABASES_DELETE_CLICKED = 'CONFIG_DATABASES_MULTIPLE_DATABASES_DELETE_CLICKED', CONFIG_DATABASES_DATABASE_EDIT_CLICKED = 'CONFIG_DATABASES_DATABASE_EDIT_CLICKED', CONFIG_DATABASES_DATABASE_EDIT_CANCELLED_CLICKED = 'CONFIG_DATABASES_DATABASE_EDIT_CANCELLED_CLICKED', + CONFIG_DATABASES_DATABASE_LIST_SORTED = 'CONFIG_DATABASES_DATABASE_LIST_SORTED', CONFIG_DATABASES_OPEN_DATABASE_BUTTON_CLICKED = 'CONFIG_DATABASES_OPEN_DATABASE_BUTTON_CLICKED', CONFIG_DATABASES_HOST_PORT_COPIED = 'CONFIG_DATABASES_HOST_PORT_COPIED', diff --git a/redisinsight/ui/src/utils/index.ts b/redisinsight/ui/src/utils/index.ts index cce24124dd..677b511f67 100644 --- a/redisinsight/ui/src/utils/index.ts +++ b/redisinsight/ui/src/utils/index.ts @@ -27,6 +27,8 @@ export * from './formatBytes' export * from './instanceModules' export * from './monacoRedisComplitionProvider' export * from './monacoRedisMonarchTokensProvider' +export * from './monacoActions' +export * from './monacoDecorations' export * from './handlePlatforms' export * from './plugins' diff --git a/redisinsight/ui/src/utils/monacoActions.ts b/redisinsight/ui/src/utils/monacoActions.ts new file mode 100644 index 0000000000..b66a6850cf --- /dev/null +++ b/redisinsight/ui/src/utils/monacoActions.ts @@ -0,0 +1,22 @@ +import * as monacoEditor from 'monaco-editor' + +export enum MonacoAction { + Submit = 'submit' +} + +export const geMonacoAction = ( + actionId: MonacoAction, + action: (editor: monacoEditor.editor.IStandaloneCodeEditor, ...args: any[]) => void | Promise, + monaco: typeof monacoEditor, +): monacoEditor.editor.IActionDescriptor => { + if (actionId === MonacoAction.Submit) { + return { + id: 'submit', + label: 'Run Commands', + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter], + run: action + } + } + + return { id: '', label: '', run: () => {} } +} diff --git a/redisinsight/ui/src/utils/monacoDecorations.ts b/redisinsight/ui/src/utils/monacoDecorations.ts new file mode 100644 index 0000000000..6fa1695728 --- /dev/null +++ b/redisinsight/ui/src/utils/monacoDecorations.ts @@ -0,0 +1,30 @@ +import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api' + +export interface ILightWeightDecoration { + id: string + range: monacoEditor.IRange +} + +interface IModelDeltaDecoration extends monacoEditor.editor.IModelDeltaDecoration {} + +export const toModelDeltaDecoration = (dec: ILightWeightDecoration):IModelDeltaDecoration => ({ + range: dec.range, + options: { + className: dec.id, + isWholeLine: false, + glyphMarginClassName: 'monaco-glyph-run-command', + // glyphMarginHoverMessage: { value: 'Run command' } + } +}) + +export const decoration = ( + monaco: typeof monacoEditor, + id: string, + startLineNumber: number, + startColumn: number, + endLineNumber: number, + endColum: number +): ILightWeightDecoration => ({ + id, + range: new monaco.Range(startLineNumber, startColumn, endLineNumber, endColum) +}) diff --git a/redisinsight/ui/src/utils/test-utils.tsx b/redisinsight/ui/src/utils/test-utils.tsx index 904fedc130..ec73768284 100644 --- a/redisinsight/ui/src/utils/test-utils.tsx +++ b/redisinsight/ui/src/utils/test-utils.tsx @@ -25,11 +25,13 @@ import { initialState as initialStateNotifications } from 'uiSrc/slices/app/noti import { initialState as initialStateAppInfo } from 'uiSrc/slices/app/info' import { initialState as initialStateAppContext } from 'uiSrc/slices/app/context' import { initialState as initialStateAppRedisCommands } from 'uiSrc/slices/app/redis-commands' +import { initialState as initialStateAppPluginsReducer } from 'uiSrc/slices/app/plugins' import { initialState as initialStateCliSettings } from 'uiSrc/slices/cli/cli-settings' import { initialState as initialStateCliOutput } from 'uiSrc/slices/cli/cli-output' import { initialState as initialStateUserSettings } from 'uiSrc/slices/user/user-settings' import { initialState as initialStateWBResults } from 'uiSrc/slices/workbench/wb-results' import { initialState as initialStateWBSettings } from 'uiSrc/slices/workbench/wb-settings' +import { initialState as initialStateWBEnablementArea } from 'uiSrc/slices/workbench/wb-enablement-area' interface Options { initialState?: RootState; @@ -44,7 +46,8 @@ const initialStateDefault: RootState = { info: cloneDeep(initialStateAppInfo), notifications: cloneDeep(initialStateNotifications), context: cloneDeep(initialStateAppContext), - redisCommands: cloneDeep(initialStateAppRedisCommands) + redisCommands: cloneDeep(initialStateAppRedisCommands), + plugins: cloneDeep(initialStateAppPluginsReducer) }, connections: { instances: cloneDeep(initialStateInstances), @@ -73,6 +76,7 @@ const initialStateDefault: RootState = { workbench: { results: cloneDeep(initialStateWBResults), settings: cloneDeep(initialStateWBSettings), + enablementArea: cloneDeep(initialStateWBEnablementArea), }, }