feat: implement copy and link

This commit is contained in:
2026-09-20 21:40:25 +02:00
parent e05d589311
commit 10b890b5ae
15 changed files with 1564 additions and 198 deletions
@@ -1,52 +1,76 @@
import { useState } from 'react';
import { DSP } from '../../../../types';
import { Channel, InputSource } from '../../types';
import { DSP408 } from '../../../../types';
import { Channel, InputSource, InputType, DiscreteFrequency } from '../../types';
import { SidebarId } from '../../constants';
import Button from '../../../../components/Button/Button';
import Card from '../../../../components/Card/Card';
import Led from '../../../../components/Led/Led';
import ChoiceSelect from '../../../../components/ChoiceSelect/ChoiceSelect';
import LinkChannelsModal from './LinkChannelsModal';
import CopyChannelModal from './CopyChannelModal';
import './MiscellaneousSection.css';
import { EyeIcon, EyeOffIcon } from '../../../../assets/icons/EyesIcons';
const INPUT_TYPES: readonly InputType[] = Object.values(InputType);
const DISCRETE_FREQUENCIES: readonly DiscreteFrequency[] = Object.values(DiscreteFrequency);
function MiscellaneousPanel({
dsp,
onConnect,
onDisconnect,
factoryReset,
copyChannel,
setInputSource,
linkChannels,
unlockDevice,
lockDevice,
setActiveSidebar,
}: {
dsp: DSP;
onConnect: () => void;
onDisconnect: () => void;
dsp: DSP408;
factoryReset: (force: boolean) => Promise<boolean>;
copyChannel: (channel1: Channel, channel2: Channel) => Promise<boolean>;
setInputSource: (inputSource: InputSource) => Promise<boolean>;
linkChannels: (source: Channel, destinations: Channel[]) => Promise<boolean>;
unlockDevice: (password: string) => Promise<boolean>;
lockDevice: (password: string) => Promise<boolean>;
setActiveSidebar: (id: SidebarId) => void;
}) {
const [isFactoryResetting, setIsFactoryResetting] = useState(false);
const [isResetConfirmOpen, setIsResetConfirmOpen] = useState(false);
const [isLinkModalOpen, setIsLinkModalOpen] = useState(false);
const [isCopyModalOpen, setIsCopyModalOpen] = useState(false);
const statusLabel =
dsp.status === 'connected'
? 'Connected'
: dsp.status === 'connecting'
? 'Connecting…'
: 'Disconnected';
const [inputSource, setInputSourceState] = useState<InputSource>({
type: InputType.Analog,
frequency: DiscreteFrequency.F20,
});
const handleFactoryReset = async () => {
const confirmed = window.confirm(
'Are you sure you want to factory reset this DSP? This cannot be undone.',
);
const [password, setPassword] = useState('');
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
if (!confirmed) {
return;
}
const handleInputTypeChange = async (type: InputType) => {
const nextSource: InputSource = {
type,
frequency: type === InputType.SineWave ? inputSource.frequency : DiscreteFrequency.F20,
};
setInputSourceState(nextSource);
await setInputSource(nextSource);
};
const handleFrequencyChange = async (frequency: DiscreteFrequency) => {
const nextSource: InputSource = {
...inputSource,
frequency,
};
setInputSourceState(nextSource);
await setInputSource(nextSource);
};
const handleFactoryReset = () => {
setIsResetConfirmOpen(true);
};
const confirmFactoryReset = async () => {
setIsResetConfirmOpen(false);
setIsFactoryResetting(true);
try {
@@ -56,85 +80,175 @@ function MiscellaneousPanel({
}
};
const handleLock = async () => {
if (!password) {
return;
}
const success = await lockDevice(password);
if (success) {
setPassword('');
setActiveSidebar('overview');
}
};
return (
<div className="connection-section">
<Card className="connection-panel">
<div className="connection-panel__header">
<div className="connection-panel__header-text">
<h1 className="connection-panel__title" title={dsp.name}>
{dsp.name}
</h1>
<div className="connection-panel__type">{dsp.type}</div>
<div className="miscellaneous-section">
<Card className="miscellaneous-panel">
{/* Input Source */}
<section className="miscellaneous-panel__section">
<div className="miscellaneous-panel__section-header">
<h2>Input Source</h2>
<p>Select the signal source used by the DSP.</p>
</div>
<div className="connection-panel__status">
<span className="connection-panel__led">
<Led status={dsp.status} />
</span>
<div className="miscellaneous-panel__row">
<div className="miscellaneous-panel__field">
<label htmlFor="misc-input-type">Source</label>
<span
className={`status-label status-label--${dsp.status}`}
>
{statusLabel}
</span>
<ChoiceSelect<InputType>
id="misc-input-type"
value={inputSource.type}
options={INPUT_TYPES}
onChange={handleInputTypeChange}
/>
</div>
{inputSource.type === InputType.SineWave && (
<div className="miscellaneous-panel__field">
<label htmlFor="misc-frequency">Frequency</label>
<ChoiceSelect<DiscreteFrequency>
id="misc-frequency"
value={inputSource.frequency}
options={DISCRETE_FREQUENCIES}
formatLabel={(frequency) => {
if (frequency === DiscreteFrequency.F31_5) {
return '31.5 Hz';
}
return `${frequency.slice(1)} Hz`;
}}
onChange={handleFrequencyChange}
/>
</div>
)}
</div>
</div>
</section>
<dl className="connection-panel__specs">
<div className="connection-panel__spec">
<dt>IP address</dt>
<dd className="connection-panel__mono" title={dsp.ip}>
{dsp.ip}
</dd>
{/* Channel Tools */}
<section className="miscellaneous-panel__section">
<div className="miscellaneous-panel__section-header">
<h2>Channel Tools</h2>
<p>Copy settings between channels or link multiple channels.</p>
</div>
<div className="connection-panel__spec">
<dt>Port</dt>
<dd className="connection-panel__mono">{dsp.port}</dd>
</div>
<div className="connection-panel__spec">
<dt>Device ID</dt>
<dd
className="connection-panel__mono"
title={String(dsp.deviceId) || undefined}
>
{dsp.deviceId || '—'}
</dd>
</div>
</dl>
<div className="connection-panel__actions">
{dsp.status === 'connected' ? (
<Button variant="outline" onClick={onDisconnect}>
Disconnect
<div className="miscellaneous-panel__actions">
<Button variant="outline" onClick={() => setIsCopyModalOpen(true)}>
Copy Channel
</Button>
) : (
<Button
variant="primary"
onClick={onConnect}
disabled={dsp.status === 'connecting'}
>
{dsp.status === 'connecting' ? 'Connecting…' : 'Connect'}
</Button>
)}
</div>
<div className="miscellaneous-panel__actions">
<Button
variant="outline"
onClick={handleFactoryReset}
disabled={
dsp.status !== 'connected' || isFactoryResetting
}
>
{isFactoryResetting ? 'Resetting…' : 'Factory Reset'}
</Button>
</div>
<Button variant="outline" onClick={() => setIsLinkModalOpen(true)}>
Link Channels
</Button>
</div>
</section>
{/* Security */}
<section className="miscellaneous-panel__section">
<div className="miscellaneous-panel__section-header">
<h2>Device Security</h2>
<p>Lock access to the DSP.</p>
</div>
<div className="miscellaneous-panel__field">
<label htmlFor="misc-password">Password</label>
<div className="miscellaneous-panel__password-wrap">
<input
id="misc-password"
type={isPasswordVisible ? 'text' : 'password'}
maxLength={4}
value={password}
placeholder="Enter password"
onChange={(e) => {
const value = e.target.value;
if (/^[\x20-\x7E]*$/.test(value)) {
setPassword(value);
}
}}
/>
<button
type="button"
className="miscellaneous-panel__password-toggle"
onClick={() => setIsPasswordVisible((v) => !v)}
aria-label={isPasswordVisible ? 'Hide password' : 'Show password'}
aria-pressed={isPasswordVisible}
tabIndex={-1}
>
{isPasswordVisible ? <EyeOffIcon /> : <EyeIcon />}
</button>
</div>
</div>
<div className="miscellaneous-panel__actions">
<Button variant="outline" onClick={handleLock} disabled={password.length !== 4}>
Lock Device
</Button>
</div>
</section>
{/* Factory Reset */}
<section className="miscellaneous-panel__section miscellaneous-panel__section--danger">
<div className="miscellaneous-panel__section-header">
<h2>Factory Reset</h2>
<p>Restore the DSP to its factory configuration. This cannot be undone.</p>
</div>
<div className="miscellaneous-panel__actions">
<Button variant="outline" onClick={handleFactoryReset} disabled={isFactoryResetting}>
{isFactoryResetting ? 'Resetting...' : 'Factory Reset'}
</Button>
</div>
</section>
</Card>
{isLinkModalOpen && (
<LinkChannelsModal
dsp={dsp}
onClose={() => setIsLinkModalOpen(false)}
linkChannels={linkChannels}
/>
)}
{isCopyModalOpen && (
<CopyChannelModal onClose={() => setIsCopyModalOpen(false)} copyChannel={copyChannel} />
)}
{isResetConfirmOpen && (
<div className="confirm-modal-overlay" onClick={() => setIsResetConfirmOpen(false)}>
<div className="confirm-modal" onClick={(e) => e.stopPropagation()}>
<h3>Factory Reset DSP?</h3>
<p>
This will restore the DSP to its factory configuration. This action cannot be undone.
</p>
<div className="confirm-modal__actions">
<Button variant="outline" onClick={() => setIsResetConfirmOpen(false)}>
Cancel
</Button>
<Button variant="primary" onClick={confirmFactoryReset}>
Factory Reset
</Button>
</div>
</div>
</div>
)}
</div>
);
}
export default MiscellaneousPanel;
export default MiscellaneousPanel;