From dd08d70df75481a7c066da4d47aba8cc603fab11 Mon Sep 17 00:00:00 2001 From: maxiadlovskii Date: Tue, 17 Oct 2023 08:20:23 +0200 Subject: [PATCH] Hotkey improvements: Add scratchpad support, add multiple hotkeys for actions. (#16925) * Fix displayInOverview flag. Add hotkeys to Scratchpad * Refactoring. Add option to use multiple hotkeys for the same action. Get displayInOverview and splitKey from hotkeys options instead of hardcoded values * Change scratchpad shortcut * Fix hotkey to copy scratchpad content. * Cleanup * Only execute scratchpad related logic, when scratchpad is visible. * Remove not needed useCallbacks. * Add missing types for scratchpad * Only show and do not toggle hotkeys overview when pressing related hotkey. --------- Co-authored-by: Linus Pahl --- .../src/components/hotkeys/HotkeysModal.tsx | 81 +++-- .../hotkeys/HotkeysModalContainer.tsx | 2 +- .../src/components/scratchpad/Scratchpad.tsx | 236 +-------------- .../components/scratchpad/ScratchpadModal.tsx | 279 ++++++++++++++++++ .../src/contexts/HotkeysContext.tsx | 7 +- .../src/contexts/HotkeysProvider.tsx | 9 + .../src/contexts/ScratchpadProvider.tsx | 9 + .../src/hooks/useHotkey.tsx | 18 +- graylog2-web-interface/src/routing/App.tsx | 10 +- 9 files changed, 382 insertions(+), 269 deletions(-) create mode 100644 graylog2-web-interface/src/components/scratchpad/ScratchpadModal.tsx diff --git a/graylog2-web-interface/src/components/hotkeys/HotkeysModal.tsx b/graylog2-web-interface/src/components/hotkeys/HotkeysModal.tsx index e716a186fb98..3f832ddcb794 100644 --- a/graylog2-web-interface/src/components/hotkeys/HotkeysModal.tsx +++ b/graylog2-web-interface/src/components/hotkeys/HotkeysModal.tsx @@ -17,6 +17,8 @@ import React from 'react'; import styled from 'styled-components'; import pick from 'lodash/pick'; +import isArray from 'lodash/isArray'; +import flattenDeep from 'lodash/flattenDeep'; import { KeyboardKey, Modal, Button } from 'components/bootstrap'; import useHotkeysContext from 'hooks/useHotkeysContext'; @@ -54,6 +56,7 @@ const KeysList = styled.div` display: inline-flex; gap: 5px; justify-content: right; + flex-wrap: wrap; `; const KeySeparator = styled.div` @@ -74,31 +77,57 @@ type KeyProps = { description: string, isEnabled: boolean, isMacOS: boolean, - keys: string, + keys: string | Array, + splitKey: string, } -const Key = ({ description, keys, combinationKey, isEnabled, isMacOS }: KeyProps) => { - const keysArray = keys.split(combinationKey); +type ShortcutKeysProps = { + keys: string | Array, + splitKey: string, + combinationKey: string, + isEnabled: boolean, + isMacOS: boolean +} - return ( - - {description} - - {keysArray.map((key, index) => { - const isLast = index === keysArray.length - 1; +const ShortcutKeys = ({ keys, splitKey, combinationKey, isEnabled, isMacOS }: ShortcutKeysProps) => { + const shortcutsArray = isArray(keys) ? keys : [keys]; + const splitShortcutsArray = flattenDeep(shortcutsArray.map((key) => key.split(splitKey))); - return ( - - {keyMapper(key, isMacOS)} - {!isLast && {combinationKey}} - - ); - })} - - + return ( + <> + {splitShortcutsArray.map((keysStr, keysStrIndex) => { + const keysArray = keysStr.split(combinationKey); + const isLastSplit = keysStrIndex === splitShortcutsArray.length - 1; + + return ( + + {keysArray.map((key, index) => { + const isLast = index === keysArray.length - 1; + + return ( + + {keyMapper(key, isMacOS)} + {!isLast && {combinationKey}} + + ); + })} + {!isLastSplit && or} + + ); + })} + ); }; +const Key = ({ description, keys, combinationKey, splitKey, isEnabled, isMacOS }: KeyProps) => ( + + {description} + + + + +); + type HotkeyCollectionSectionProps = { collection: HotkeyCollection, scope: ScopeName, @@ -108,7 +137,11 @@ type HotkeyCollectionSectionProps = { const HotkeyCollectionSection = ({ collection, scope, isMacOS }: HotkeyCollectionSectionProps) => { const { activeHotkeys } = useHotkeysContext(); const { title, description, actions } = collection; - const filtratedActions = Object.entries(actions).filter(([actionKey]) => activeHotkeys.has(`${scope}.${actionKey}`)); + const filtratedActions = Object.entries(actions).filter(([actionKey]) => { + const key: `${ScopeName}.${string}` = `${scope}.${actionKey}`; + + return activeHotkeys.has(key) && activeHotkeys.get(key).options.displayInOverview !== false; + }); if (!filtratedActions.length) { return null; @@ -119,15 +152,19 @@ const HotkeyCollectionSection = ({ collection, scope, isMacOS }: HotkeyCollectio

{description}

{filtratedActions.map(([actionKey, { description: keyDescription, keys, displayKeys }]) => { - const isEnabled = activeHotkeys.get(`${scope}.${actionKey}`)?.options?.enabled !== false; + const isEnabled = !!activeHotkeys.get(`${scope}.${actionKey}`)?.options?.enabled; + const splitKey = activeHotkeys.get(`${scope}.${actionKey}`)?.options?.splitKey; + const combinationKey = activeHotkeys.get(`${scope}.${actionKey}`)?.options?.combinationKey; + const reactKey = isArray(keys) ? keys.join(',') : keys; return ( + key={reactKey} /> ); })} diff --git a/graylog2-web-interface/src/components/hotkeys/HotkeysModalContainer.tsx b/graylog2-web-interface/src/components/hotkeys/HotkeysModalContainer.tsx index 049453561681..bceacfe8ffdf 100644 --- a/graylog2-web-interface/src/components/hotkeys/HotkeysModalContainer.tsx +++ b/graylog2-web-interface/src/components/hotkeys/HotkeysModalContainer.tsx @@ -26,7 +26,7 @@ const HotkeysModalContainer = () => { useHotkey({ actionKey: 'show-hotkeys-modal', - callback: toggleModal, + callback: () => setShow(true), scope: 'general', }); diff --git a/graylog2-web-interface/src/components/scratchpad/Scratchpad.tsx b/graylog2-web-interface/src/components/scratchpad/Scratchpad.tsx index e09ea4033a11..f3e1d492f165 100644 --- a/graylog2-web-interface/src/components/scratchpad/Scratchpad.tsx +++ b/graylog2-web-interface/src/components/scratchpad/Scratchpad.tsx @@ -14,245 +14,17 @@ * along with this program. If not, see * . */ -import React, { useContext, useState, useEffect, useRef } from 'react'; -import styled, { css } from 'styled-components'; -import chroma from 'chroma-js'; -import ClipboardJS from 'clipboard'; -import debounce from 'lodash/debounce'; +import React, { useContext } from 'react'; -import { OverlayTrigger } from 'components/common'; -import { Alert, Button, ButtonGroup, Tooltip, BootstrapModalConfirm } from 'components/bootstrap'; import { ScratchpadContext } from 'contexts/ScratchpadProvider'; -import InteractableModal from 'components/common/InteractableModal'; -import Icon from 'components/common/Icon'; -import Store from 'logic/local-storage/Store'; - -const DEFAULT_SCRATCHDATA = ''; -const TEXTAREA_ID = 'scratchpad-text-content'; -const STATUS_CLEARED = 'Cleared.'; -const STATUS_COPIED = 'Copied!'; -const STATUS_AUTOSAVED = 'Auto saved.'; -const STATUS_DEFAULT = ''; - -const ContentArea = styled.div` - display: flex; - flex-direction: column; - height: 100%; -`; - -const Textarea = styled.textarea<{ $copied: boolean }>(({ $copied, theme }) => css` - width: 100%; - padding: 3px; - resize: none; - flex: 1; - margin: 15px 0 7px; - border: 1px solid ${$copied ? theme.colors.variant.success : theme.colors.variant.lighter.default}; - box-shadow: inset 1px 1px 1px rgb(0 0 0 / 7.5%)${$copied && `, 0 0 8px ${chroma(theme.colors.variant.success).alpha(0.4).css()}`}; - transition: border 150ms ease-in-out, box-shadow 150ms ease-in-out; - font-family: ${theme.fonts.family.monospace}; - font-size: ${theme.fonts.size.body}; - - &:focus { - border-color: ${theme.colors.variant.light.info}; - outline: none; - } -`); - -const StyledAlert = styled(Alert)` - && { - padding: 6px 12px; - margin: 6px 0 0; - display: flex; - align-items: center; - } -`; - -const AlertNote = styled.em` - margin-left: 6px; - flex: 1; -`; - -const Footer = styled.footer(({ theme }) => css` - display: flex; - align-items: center; - padding: 7px 0 9px; - border-top: 1px solid ${theme.colors.gray[80]}; -`); - -const StatusMessage = styled.span<{ $visible: boolean }>(({ theme, $visible }) => css` - flex: 1; - color: ${theme.colors.variant.success}; - font-style: italic; - opacity: ${$visible ? '1' : '0'}; - transition: opacity 150ms ease-in-out; -`); +import ScratchpadModal from 'components/scratchpad/ScratchpadModal'; const Scratchpad = () => { - const clipboard = useRef(); - const textareaRef = useRef(); - const statusTimeout = useRef>(); - const { isScratchpadVisible, setScratchpadVisibility, localStorageItem } = useContext(ScratchpadContext); - const scratchpadStore = Store.get(localStorageItem) || {}; - const [isSecurityWarningConfirmed, setSecurityWarningConfirmed] = useState(scratchpadStore.securityConfirmed || false); - const [scratchData, setScratchData] = useState(scratchpadStore.value || DEFAULT_SCRATCHDATA); - const [size, setSize] = useState<{ width: string, height: string } | undefined>(scratchpadStore.size || undefined); - const [position, setPosition] = useState<{ x:number, y:number } | undefined>(scratchpadStore.position || undefined); - const [statusMessage, setStatusMessage] = useState(STATUS_DEFAULT); - const [showStatusMessage, setShowStatusMessage] = useState(false); - const [showModal, setShowModal] = useState(false); - - const writeData = (newData) => { - const currentStorage = Store.get(localStorageItem); - - Store.set(localStorageItem, { ...currentStorage, ...newData }); - }; - - const resetStatusTimer = () => { - if (statusTimeout.current) { - clearTimeout(statusTimeout.current); - } - - statusTimeout.current = setTimeout(() => { - setShowStatusMessage(false); - }, 1000); - }; - - const updateStatusMessage = (message) => { - setStatusMessage(message); - setShowStatusMessage(true); - resetStatusTimer(); - }; - - const handleChange = debounce(() => { - const { value } = textareaRef.current; - - setScratchData(value); - updateStatusMessage(STATUS_AUTOSAVED); - writeData({ value }); - }, 500); - - const handleDrag = (newPosition) => { - setPosition(newPosition); - writeData({ position: newPosition }); - }; - - const handleSize = (newSize) => { - setSize(newSize); - writeData({ size: newSize }); - }; - - const handleGotIt = () => { - setSecurityWarningConfirmed(true); - writeData({ securityConfirmed: true }); - }; - - const openConfirmClear = () => { - setShowModal(true); - }; - - const handleCancelClear = () => { - setShowModal(false); - }; - - const handleClearText = () => { - setScratchData(DEFAULT_SCRATCHDATA); - writeData({ value: DEFAULT_SCRATCHDATA }); - handleCancelClear(); - updateStatusMessage(STATUS_CLEARED); - }; - - useEffect(() => { - clipboard.current = new ClipboardJS('[data-clipboard-button]', {}); - - clipboard.current.on('success', () => { - updateStatusMessage(STATUS_COPIED); - }); - - return () => { - clipboard.current.destroy(); - - if (statusTimeout.current) { - clearTimeout(statusTimeout.current); - } - }; - }); - - useEffect(() => { - if (textareaRef.current && isScratchpadVisible) { - textareaRef.current.focus(); - textareaRef.current.value = scratchData; - } - }, [scratchData, isScratchpadVisible]); + const { isScratchpadVisible } = useContext(ScratchpadContext); if (!isScratchpadVisible) return null; - return ( - setScratchpadVisibility(false)} - onDrag={handleDrag} - onResize={handleSize} - size={size} - position={position}> - - {!isSecurityWarningConfirmed && ( - - - We recommend you do not store any sensitive information, such as passwords, in - this area. - - - - )} - -