Files
headquarter/apps/web/src/components/features/ssh-keys/SSHKeyList.tsx
T
Developer 96ce3f4c53 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
2026-06-05 19:27:38 +00:00

181 lines
5.5 KiB
TypeScript

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>
);
};