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:
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user