File transfer
This commit is contained in:
158
resources/scripts/addons/advanced-admin/console/CommandInput.tsx
Normal file
158
resources/scripts/addons/advanced-admin/console/CommandInput.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* Console Command Input with Autocomplete
|
||||
* Ball Studios <https://git.balls.studio>
|
||||
*/
|
||||
|
||||
import React, { useState, useRef, useCallback, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import http from '@/api/http';
|
||||
import AutocompleteDropdown from './AutocompleteDropdown';
|
||||
import SyntaxHelper from './SyntaxHelper';
|
||||
import DangerCommandWarning from './DangerCommandWarning';
|
||||
|
||||
interface Suggestion {
|
||||
command: string;
|
||||
description: string;
|
||||
syntax: string;
|
||||
dangerous: boolean;
|
||||
danger_reason?: string;
|
||||
plugin: string;
|
||||
examples: string[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onSend: (command: string) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const DANGEROUS = ['stop','restart','ban','ban-ip','op','deop','whitelist off','save-off','kill @e','kill @a'];
|
||||
|
||||
export default function CommandInput({ onSend, disabled = false }: Props) {
|
||||
const { id: serverUuid } = useParams<{ id: string }>();
|
||||
const [value, setValue] = useState('');
|
||||
const [suggestions, setSugs]= useState<Suggestion[]>([]);
|
||||
const [selected, setSelected]= useState(-1);
|
||||
const [showDanger, setDanger]= useState<Suggestion | null>(null);
|
||||
const [activeSug, setActiveSug] = useState<Suggestion | null>(null);
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const isDanger = (cmd: string) =>
|
||||
DANGEROUS.some(d => cmd.trim().toLowerCase().startsWith(d));
|
||||
|
||||
const fetchSuggestions = useCallback(async (q: string) => {
|
||||
if (!q.trim() || !serverUuid) { setSugs([]); return; }
|
||||
try {
|
||||
const { data } = await http.get(
|
||||
`/api/client/servers/${serverUuid}/addons/console/autocomplete`,
|
||||
{ params: { q } }
|
||||
);
|
||||
setSugs(data.data ?? []);
|
||||
setSelected(-1);
|
||||
} catch {
|
||||
setSugs([]);
|
||||
}
|
||||
}, [serverUuid]);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const v = e.target.value;
|
||||
setValue(v);
|
||||
clearTimeout(debounceRef.current);
|
||||
debounceRef.current = setTimeout(() => fetchSuggestions(v), 150);
|
||||
|
||||
// Update active suggestion for syntax helper
|
||||
const match = suggestions.find(s => s.command === v.split(' ')[0]);
|
||||
setActiveSug(match ?? null);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'ArrowDown') { e.preventDefault(); setSelected(s => Math.min(s + 1, suggestions.length - 1)); }
|
||||
if (e.key === 'ArrowUp') { e.preventDefault(); setSelected(s => Math.max(s - 1, -1)); }
|
||||
if (e.key === 'Tab' && suggestions.length > 0) {
|
||||
e.preventDefault();
|
||||
const idx = selected >= 0 ? selected : 0;
|
||||
applySelection(suggestions[idx]);
|
||||
}
|
||||
if (e.key === 'Escape') { setSugs([]); setSelected(-1); }
|
||||
if (e.key === 'Enter') { e.preventDefault(); submit(); }
|
||||
};
|
||||
|
||||
const applySelection = (sug: Suggestion) => {
|
||||
setValue(sug.command + ' ');
|
||||
setSugs([]);
|
||||
setSelected(-1);
|
||||
setActiveSug(sug);
|
||||
inputRef.current?.focus();
|
||||
};
|
||||
|
||||
const submit = () => {
|
||||
const cmd = value.trim();
|
||||
if (!cmd) return;
|
||||
if (isDanger(cmd)) {
|
||||
const sug = suggestions.find(s => cmd.startsWith(s.command)) ?? {
|
||||
command: cmd, description: '', syntax: '', dangerous: true, danger_reason: 'Potentially destructive command.', plugin: '', examples: [],
|
||||
};
|
||||
setDanger(sug);
|
||||
return;
|
||||
}
|
||||
send(cmd);
|
||||
};
|
||||
|
||||
const send = (cmd: string) => {
|
||||
onSend(cmd);
|
||||
setValue('');
|
||||
setSugs([]);
|
||||
setActiveSug(null);
|
||||
setDanger(null);
|
||||
};
|
||||
|
||||
useEffect(() => () => clearTimeout(debounceRef.current), []);
|
||||
|
||||
return (
|
||||
<div className='addon-console-input-wrap'>
|
||||
<div className='addon-console-input-row'>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type='text'
|
||||
className='addon-console-input'
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder='Enter command…'
|
||||
disabled={disabled}
|
||||
autoComplete='off'
|
||||
spellCheck={false}
|
||||
aria-label='Console command input'
|
||||
aria-autocomplete='list'
|
||||
/>
|
||||
<button
|
||||
className='addon-console-send'
|
||||
onClick={submit}
|
||||
disabled={disabled || !value.trim()}
|
||||
aria-label='Send command'
|
||||
>▶</button>
|
||||
</div>
|
||||
|
||||
{/* Syntax helper */}
|
||||
{activeSug && <SyntaxHelper suggestion={activeSug} />}
|
||||
|
||||
{/* Dropdown */}
|
||||
{suggestions.length > 0 && (
|
||||
<AutocompleteDropdown
|
||||
suggestions={suggestions}
|
||||
selectedIndex={selected}
|
||||
onSelect={applySelection}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Danger modal */}
|
||||
{showDanger && (
|
||||
<DangerCommandWarning
|
||||
suggestion={showDanger}
|
||||
onConfirm={() => send(value.trim())}
|
||||
onCancel={() => { setDanger(null); inputRef.current?.focus(); }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user