refactor(decnet_web/Webhooks): extract FormRow + SecretModal
This commit is contained in:
105
decnet_web/src/components/Webhooks/FormRow.tsx
Normal file
105
decnet_web/src/components/Webhooks/FormRow.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import React from 'react';
|
||||
import { Save, X } from '../../icons';
|
||||
import type { FormState, SimpleEvent } from './types';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
form: FormState;
|
||||
setForm: React.Dispatch<React.SetStateAction<FormState>>;
|
||||
onSave: (e: React.FormEvent) => void;
|
||||
onCancel: () => void;
|
||||
saving: boolean;
|
||||
isEdit: boolean;
|
||||
onToggleSimple: (n: SimpleEvent) => void;
|
||||
}
|
||||
|
||||
const FormRow: React.FC<Props> = ({
|
||||
title, form, setForm, onSave, onCancel, saving, isEdit, onToggleSimple,
|
||||
}) => (
|
||||
<tr className="wh-form-row">
|
||||
<td colSpan={7}>
|
||||
<form className="wh-form-grid" onSubmit={onSave}>
|
||||
<label className="wh-form-title">{title}</label>
|
||||
|
||||
<label>NAME</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.name}
|
||||
onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))}
|
||||
placeholder="shuffle-prod"
|
||||
required
|
||||
maxLength={64}
|
||||
/>
|
||||
|
||||
<label>URL</label>
|
||||
<input
|
||||
type="url"
|
||||
value={form.url}
|
||||
onChange={(e) => setForm((f) => ({ ...f, url: e.target.value }))}
|
||||
placeholder="https://shuffle.example.com/api/v1/hooks/webhook_xxx"
|
||||
required
|
||||
/>
|
||||
|
||||
<label>
|
||||
SECRET {isEdit && <span className="wh-form-hint">(blank = keep existing)</span>}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={form.secret}
|
||||
onChange={(e) => setForm((f) => ({ ...f, secret: e.target.value }))}
|
||||
placeholder={isEdit ? '—' : 'leave blank to auto-generate'}
|
||||
minLength={16}
|
||||
maxLength={256}
|
||||
/>
|
||||
|
||||
<label>SIMPLE EVENTS</label>
|
||||
<div className="wh-checkbox-group">
|
||||
{(['AttackerDetail', 'DeckyStatus', 'SystemStatus'] as const).map((name) => (
|
||||
<label key={name}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.simple_events.includes(name)}
|
||||
onChange={() => onToggleSimple(name)}
|
||||
/>
|
||||
{name}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<label>
|
||||
ADVANCED PATTERNS
|
||||
<br />
|
||||
<span className="wh-form-hint">(one per line, NATS-style)</span>
|
||||
</label>
|
||||
<textarea
|
||||
value={form.topic_patterns}
|
||||
onChange={(e) => setForm((f) => ({ ...f, topic_patterns: e.target.value }))}
|
||||
placeholder={'attacker.>\ndecky.*.state'}
|
||||
/>
|
||||
|
||||
<label>ENABLED</label>
|
||||
<div className="wh-checkbox-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.enabled}
|
||||
onChange={(e) => setForm((f) => ({ ...f, enabled: e.target.checked }))}
|
||||
/>
|
||||
Receive events
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="wh-form-buttons">
|
||||
<button type="button" className="btn ghost" onClick={onCancel} disabled={saving}>
|
||||
<X size={12} /> CANCEL
|
||||
</button>
|
||||
<button type="submit" className="btn violet" disabled={saving}>
|
||||
<Save size={12} /> {saving ? 'SAVING…' : isEdit ? 'SAVE CHANGES' : 'CREATE'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
export default FormRow;
|
||||
28
decnet_web/src/components/Webhooks/SecretModal.test.tsx
Normal file
28
decnet_web/src/components/Webhooks/SecretModal.test.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import SecretModal from './SecretModal';
|
||||
|
||||
describe('SecretModal', () => {
|
||||
it('renders the secret and fires onClose for DONE', () => {
|
||||
const onClose = vi.fn();
|
||||
render(<SecretModal name="shuffle" secret="abc123def456ghi7" onClose={onClose} />);
|
||||
expect(screen.getByText('abc123def456ghi7')).toBeInTheDocument();
|
||||
expect(screen.getByText(/SHUFFLE/)).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByText('DONE'));
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('closes when backdrop is clicked but not when the inner modal is clicked', () => {
|
||||
const onClose = vi.fn();
|
||||
const { container } = render(
|
||||
<SecretModal name="x" secret="s" onClose={onClose} />,
|
||||
);
|
||||
fireEvent.click(container.querySelector('.wh-secret-modal-backdrop')!);
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
fireEvent.click(container.querySelector('.wh-secret-modal')!);
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
46
decnet_web/src/components/Webhooks/SecretModal.tsx
Normal file
46
decnet_web/src/components/Webhooks/SecretModal.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import React, { useState } from 'react';
|
||||
import { AlertTriangle, Check, Copy } from '../../icons';
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
secret: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const SecretModal: React.FC<Props> = ({ name, secret, onClose }) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const copy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(secret);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1500);
|
||||
} catch {
|
||||
/* no-op — browsers without clipboard perms will just see no feedback */
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className="wh-secret-modal-backdrop"
|
||||
onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
|
||||
>
|
||||
<div className="wh-secret-modal">
|
||||
<h3>WEBHOOK SECRET · {name.toUpperCase()}</h3>
|
||||
<div className="wh-secret-warn">
|
||||
<AlertTriangle size={14} />
|
||||
<span>COPY THIS NOW — IT WILL NOT BE SHOWN AGAIN. THE HMAC ON EVERY DELIVERY IS SIGNED WITH THIS VALUE.</span>
|
||||
</div>
|
||||
<div className="wh-secret-value">{secret}</div>
|
||||
<div className="wh-secret-actions">
|
||||
<button className="btn ghost" onClick={copy}>
|
||||
<Copy size={12} /> {copied ? 'COPIED' : 'COPY'}
|
||||
</button>
|
||||
<button className="btn violet" onClick={onClose}>
|
||||
<Check size={12} /> DONE
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecretModal;
|
||||
Reference in New Issue
Block a user