@kinvolk/headlamp-plugin
Version:
The needed infrastructure for building Headlamp plugins.
191 lines (190 loc) • 10.2 kB
JavaScript
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') })] })] }));
}