refactor: extract SshKeysPage components

- Extract use-ssh-keys hook for state management
- Extract SSHKeyCreateForm and SSHKeyList components
- Slim SshKeysPage from 277 to ~80 lines

Quality gates: tsc --noEmit passes, npm run build passes
This commit is contained in:
Developer
2026-06-05 19:27:38 +00:00
parent e49d049455
commit 96ce3f4c53
4 changed files with 386 additions and 252 deletions
@@ -0,0 +1,39 @@
import { Icon } from "../../icon";
interface Props {
newKeyName: string;
setNewKeyName: (name: string) => void;
generating: boolean;
onSubmit: (e: React.FormEvent) => void;
}
export const SSHKeyCreateForm = ({ newKeyName, setNewKeyName, generating, onSubmit }: Props) => {
return (
<form onSubmit={onSubmit} className="stack">
<div className="form-group">
<label htmlFor="key-name">Key Name</label>
<input
id="key-name"
type="text"
value={newKeyName}
onChange={(e) => setNewKeyName(e.target.value)}
placeholder="e.g., GitHub Work"
required
/>
</div>
<button type="submit" className="primary-button" disabled={generating}>
{generating ? (
<>
<Icon name="loading" size="sm" />
Generating...
</>
) : (
<>
<Icon name="add" size="sm" />
Generate SSH Key
</>
)}
</button>
</form>
);
};
@@ -0,0 +1,180 @@
import { Icon } from "../../icon";
import { EmptyState, ErrorState } from "../../data-states";
import type { SSHKey } from "../../../api/ssh-keys";
interface Props {
keys: SSHKey[];
status: "idle" | "loading" | "ready" | "error";
signPayloads: Record<string, string>;
signatures: Record<string, string>;
signing: Record<string, boolean>;
verifyPayloads: Record<string, string>;
verifySignatures: Record<string, string>;
verifyResults: Record<string, boolean | null>;
verifying: Record<string, boolean>;
onLoadKeys: () => void;
onDelete: (id: string) => void;
onCopy: (text: string) => void;
onSign: (id: string) => void;
onVerify: (id: string) => void;
onSignPayloadChange: (id: string, value: string) => void;
onVerifyPayloadChange: (id: string, value: string) => void;
onVerifySignatureChange: (id: string, value: string) => void;
}
export const SSHKeyList = ({
keys,
status,
signPayloads,
signatures,
signing,
verifyPayloads,
verifySignatures,
verifyResults,
verifying,
onLoadKeys,
onDelete,
onCopy,
onSign,
onVerify,
onSignPayloadChange,
onVerifyPayloadChange,
onVerifySignatureChange,
}: Props) => {
if (status === "error") {
return <ErrorState message="Failed to load SSH keys" onRetry={onLoadKeys} />;
}
if (keys.length === 0) {
return <EmptyState message="No SSH keys yet. Generate one above." />;
}
return (
<div className="keys-list">
{keys.map((key) => (
<div key={key.id} className="key-card">
<div className="key-header">
<h3>{key.name}</h3>
<button onClick={() => onDelete(key.id)} className="danger-button">
<Icon name="delete" size="sm" />
Delete
</button>
</div>
<div className="key-meta">
<span className="muted">
Created: {new Date(key.created_at).toLocaleDateString()}
</span>
</div>
<div className="key-public">
<code>{key.public_key.substring(0, 50)}...</code>
<button
onClick={() => onCopy(key.public_key)}
className="secondary-button"
>
<Icon name="copy" size="sm" />
Copy Full Key
</button>
</div>
<div className="key-signing">
<h4>Sign Payload</h4>
<div className="form-group">
<textarea
value={signPayloads[key.id] || ""}
onChange={(e) => onSignPayloadChange(key.id, e.target.value)}
placeholder="Enter payload to sign..."
rows={3}
/>
</div>
<button
onClick={() => onSign(key.id)}
disabled={signing[key.id] || !signPayloads[key.id]?.trim()}
className="primary-button"
>
{signing[key.id] ? (
<>
<Icon name="loading" size="sm" />
Signing...
</>
) : (
<>
<Icon name="edit" size="sm" />
Sign
</>
)}
</button>
{signatures[key.id] && (
<div className="signature-result">
<label>Signature (base64):</label>
<code>{signatures[key.id]}</code>
<button
onClick={() => onCopy(signatures[key.id])}
className="secondary-button"
>
<Icon name="copy" size="sm" />
Copy Signature
</button>
</div>
)}
</div>
<div className="key-verification">
<h4>Verify Signature</h4>
<div className="form-group">
<textarea
value={verifyPayloads[key.id] || ""}
onChange={(e) => onVerifyPayloadChange(key.id, e.target.value)}
placeholder="Enter payload..."
rows={2}
/>
</div>
<div className="form-group">
<textarea
value={verifySignatures[key.id] || ""}
onChange={(e) => onVerifySignatureChange(key.id, e.target.value)}
placeholder="Enter base64 signature..."
rows={2}
/>
</div>
<button
onClick={() => onVerify(key.id)}
disabled={
verifying[key.id] ||
!verifyPayloads[key.id]?.trim() ||
!verifySignatures[key.id]?.trim()
}
className="primary-button"
>
{verifying[key.id] ? (
<>
<Icon name="loading" size="sm" />
Verifying...
</>
) : (
<>
<Icon name="success" size="sm" />
Verify
</>
)}
</button>
{verifyResults[key.id] !== undefined && verifyResults[key.id] !== null && (
<div className={`verify-result ${verifyResults[key.id] ? "valid" : "invalid"}`}>
{verifyResults[key.id] ? (
<>
<Icon name="success" size="sm" />
Signature is valid
</>
) : (
<>
<Icon name="error" size="sm" />
Signature is invalid
</>
)}
</div>
)}
</div>
</div>
))}
</div>
);
};