feat: implement copy and link
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user