UNPKG

@kinvolk/headlamp-plugin

Version:

The needed infrastructure for building Headlamp plugins.

191 lines (190 loc) 10.2 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; /* * Copyright 2025 The Kubernetes Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { Icon } from '@iconify/react'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import IconButton from '@mui/material/IconButton'; import { alpha, useTheme } from '@mui/material/styles'; import TextField from '@mui/material/TextField'; import Tooltip from '@mui/material/Tooltip'; import Typography from '@mui/material/Typography'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { formatShortcutKey } from '../../../lib/useShortcut'; import { useTypedSelector } from '../../../redux/hooks'; import { resetAllShortcuts, resetShortcut, setShortcut, setShortcutsDialogOpen, } from '../../../redux/shortcutsSlice'; import { Dialog } from '../../common/Dialog'; import NameValueTable from '../../common/NameValueTable'; function ShortcutEditor({ shortcut, isEditing, checkConflict, onStartEdit, onSave, onCancel, onReset, }) { const { t } = useTranslation(); const theme = useTheme(); const [recordedKey, setRecordedKey] = useState(''); const [isRecording, setIsRecording] = useState(false); useEffect(() => { if (isEditing) { // eslint-disable-next-line react-hooks/set-state-in-effect setRecordedKey(''); setIsRecording(true); } else { setIsRecording(false); setRecordedKey(''); } }, [isEditing]); const handleKeyDown = useCallback((event) => { if (!isRecording) return; event.preventDefault(); event.stopPropagation(); const key = event.key.toLowerCase(); if (['control', 'shift', 'alt', 'meta'].includes(key)) { return; } if (key === 'escape') { onCancel(); return; } const parts = []; if (event.ctrlKey) parts.push('ctrl'); if (event.shiftKey) parts.push('shift'); if (event.altKey) parts.push('alt'); if (event.metaKey) parts.push('meta'); let keyName = key; if (key === ' ') keyName = 'space'; if (key === 'arrowup') keyName = 'ArrowUp'; if (key === 'arrowdown') keyName = 'ArrowDown'; if (key === 'arrowleft') keyName = 'ArrowLeft'; if (key === 'arrowright') keyName = 'ArrowRight'; parts.push(keyName); const newKey = parts.join('+'); setRecordedKey(newKey); setIsRecording(false); }, [isRecording, onCancel]); const isModified = shortcut.key !== shortcut.defaultKey; const conflict = checkConflict && recordedKey ? checkConflict(recordedKey) : undefined; if (isEditing) { return (_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1 }, children: [_jsxs(Box, { sx: { display: 'flex', flexDirection: 'column', gap: 0.5, width: 150 }, children: [_jsx(TextField, { size: "small", autoFocus // eslint-disable-line jsx-a11y/no-autofocus : true, value: recordedKey ? formatShortcutKey(recordedKey) : '', placeholder: isRecording ? t('Press keys...') : '', onKeyDown: handleKeyDown, onClick: () => setIsRecording(true), sx: { '& .MuiInputBase-input': { textAlign: 'center', cursor: 'default', fontFamily: 'monospace', fontSize: '0.9rem', }, }, InputProps: { readOnly: true, sx: { backgroundColor: isRecording ? alpha(theme.palette.warning.main, 0.1) : undefined, height: 32, }, } }), conflict && (_jsx(Typography, { variant: "caption", color: "error", children: t('Conflicts with: {{name}}', { name: conflict }) }))] }), _jsxs(Box, { sx: { display: 'flex', gap: 0.5 }, children: [_jsx(Button, { size: "small", variant: "contained", onClick: () => onSave(recordedKey), disabled: !recordedKey || !!conflict, sx: { minWidth: 60 }, children: t('Save') }), _jsx(Button, { size: "small", onClick: onCancel, sx: { minWidth: 60 }, children: t('Cancel') })] })] })); } return (_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', width: '100%', gap: 1 }, children: [_jsx(Button, { onClick: onStartEdit, sx: { textTransform: 'none', cursor: 'pointer', fontFamily: 'monospace', fontSize: '0.9rem', fontWeight: 600, backgroundColor: isModified ? alpha(theme.palette.primary.main, 0.1) : theme.palette.background.muted, color: isModified ? theme.palette.primary.main : theme.palette.text.primary, border: '1px solid', borderColor: isModified ? theme.palette.primary.main : theme.palette.divider, borderRadius: '4px', px: 1, py: 0.5, minWidth: '60px', textAlign: 'center', boxShadow: `0 2px 0 ${theme.palette.divider}`, '&:hover': { backgroundColor: alpha(theme.palette.primary.main, 0.2), borderColor: theme.palette.primary.main, }, }, children: formatShortcutKey(shortcut.key) }), isModified && (_jsx(Tooltip, { title: t('Reset to default'), children: _jsx(IconButton, { size: "small", onClick: onReset, children: _jsx(Icon, { icon: "mdi:restore", width: 16 }) }) }))] })); } export function ShortcutsList() { const { t } = useTranslation(); const dispatch = useDispatch(); const shortcuts = useTypedSelector(state => state.shortcuts.shortcuts); const [editingId, setEditingId] = useState(null); const handleSave = (id, key) => { dispatch(setShortcut({ id, key })); setEditingId(null); }; const handleReset = (id) => { dispatch(resetShortcut(id)); }; const getConflict = useCallback((currentId, newKey) => { const lowerNewKey = newKey.toLowerCase(); for (const id in shortcuts) { if (id !== currentId && shortcuts[id].key.toLowerCase() === lowerNewKey) { return shortcuts[id].name; } } return undefined; }, [shortcuts]); const categories = ['search', 'navigation', 'general']; const groupedShortcuts = useMemo(() => Object.values(shortcuts).reduce((acc, shortcut) => { const cat = shortcut.category; if (!acc[cat]) acc[cat] = []; acc[cat].push(shortcut); return acc; }, {}), [shortcuts]); const categoryLabels = useMemo(() => ({ navigation: t('Navigation'), search: t('Search'), general: t('General'), }), [t]); const rows = categories.flatMap(cat => { if (!groupedShortcuts[cat]) return []; const categoryHeader = { name: (_jsx(Typography, { variant: "button", sx: { fontWeight: 'bold', letterSpacing: '0.1em' }, children: categoryLabels[cat] || cat })), withHighlightStyle: true, value: null, }; const shortcutRows = groupedShortcuts[cat].map(shortcut => ({ name: (_jsxs(Box, { children: [_jsx(Typography, { variant: "body2", sx: { fontWeight: 500, color: 'text.primary' }, children: shortcut.name }), _jsx(Typography, { variant: "caption", sx: { color: 'text.secondary', display: 'block' }, children: shortcut.description })] })), value: (_jsx(ShortcutEditor, { shortcut: shortcut, isEditing: editingId === shortcut.id, onStartEdit: () => setEditingId(shortcut.id), onSave: key => handleSave(shortcut.id, key), onCancel: () => setEditingId(null), onReset: () => handleReset(shortcut.id), checkConflict: editingId === shortcut.id ? key => getConflict(shortcut.id, key) : undefined })), })); return [categoryHeader, ...shortcutRows]; }); return (_jsx(Box, { sx: { mt: 2 }, children: _jsx(NameValueTable, { rows: rows }) })); } export default function ShortcutsSettings() { const { t } = useTranslation(); const dispatch = useDispatch(); const isOpen = useTypedSelector(state => state.shortcuts.isShortcutsDialogOpen); const handleClose = () => { dispatch(setShortcutsDialogOpen(false)); }; return (_jsxs(Dialog, { open: isOpen, onClose: handleClose, maxWidth: "sm", title: t('Keyboard Shortcuts'), titleProps: { focusTitle: true }, fullWidth: true, children: [_jsx(DialogContent, { sx: { p: 0 }, children: _jsx(Box, { sx: { px: 2, py: 1 }, children: _jsx(ShortcutsList, {}) }) }), _jsxs(DialogActions, { sx: { px: 2, py: 2, justifyContent: 'space-between' }, children: [_jsx(Button, { size: "small", startIcon: _jsx(Icon, { icon: "mdi:restore" }), onClick: () => dispatch(resetAllShortcuts()), variant: "contained", color: "secondary", children: t('Reset All to Defaults') }), _jsx(Button, { onClick: handleClose, variant: "contained", color: "primary", children: t('Close') })] })] })); }