refactor: extract shared validation and reduce duplication
- Extract tool_types validation to shared module (validate_compose_yaml, check_port_exposed, validate_required_variables) - Extract _get_user and _get_owned_project to auth/dependencies.py - Create useAsyncData hook and apply to 6 pages - Create extractErrorMessage utility - TypeScript and build pass
This commit is contained in:
@@ -1,13 +1,12 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { createSSHKey, deleteSSHKey, listSSHKeys, signPayload, verifySignature, type SSHKey } from "../api/ssh_keys";
|
||||
import { Icon } from "../components/icon";
|
||||
import { useAsyncData } from "../hooks/use-async-data";
|
||||
|
||||
export const SSHKeysPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const [keys, setKeys] = useState<SSHKey[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { data: keys, status, error, reload: loadKeys } = useAsyncData<SSHKey[]>(listSSHKeys, []);
|
||||
const [newKeyName, setNewKeyName] = useState("");
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const [signPayloads, setSignPayloads] = useState<Record<string, string>>({});
|
||||
@@ -17,23 +16,9 @@ export const SSHKeysPage = () => {
|
||||
const [verifySignatures, setVerifySignatures] = useState<Record<string, string>>({});
|
||||
const [verifyResults, setVerifyResults] = useState<Record<string, boolean | null>>({});
|
||||
const [verifying, setVerifying] = useState<Record<string, boolean>>({});
|
||||
const [mutationError, setMutationError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadKeys();
|
||||
}, []);
|
||||
|
||||
async function loadKeys() {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await listSSHKeys();
|
||||
setKeys(data);
|
||||
setError(null);
|
||||
} catch {
|
||||
setError("Failed to load SSH keys");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
const safeKeys = keys ?? [];
|
||||
|
||||
async function handleGenerate(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
@@ -45,7 +30,7 @@ export const SSHKeysPage = () => {
|
||||
setNewKeyName("");
|
||||
await loadKeys();
|
||||
} catch {
|
||||
setError("Failed to generate SSH key");
|
||||
setMutationError("Failed to generate SSH key");
|
||||
} finally {
|
||||
setGenerating(false);
|
||||
}
|
||||
@@ -58,7 +43,7 @@ export const SSHKeysPage = () => {
|
||||
await deleteSSHKey(keyId);
|
||||
await loadKeys();
|
||||
} catch {
|
||||
setError("Failed to delete SSH key");
|
||||
setMutationError("Failed to delete SSH key");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,9 +59,9 @@ export const SSHKeysPage = () => {
|
||||
setSigning((prev) => ({ ...prev, [keyId]: true }));
|
||||
const result = await signPayload(keyId, { payload: payload.trim() });
|
||||
setSignatures((prev) => ({ ...prev, [keyId]: result.signature }));
|
||||
setError(null);
|
||||
setMutationError(null);
|
||||
} catch {
|
||||
setError("Failed to sign payload");
|
||||
setMutationError("Failed to sign payload");
|
||||
} finally {
|
||||
setSigning((prev) => ({ ...prev, [keyId]: false }));
|
||||
}
|
||||
@@ -94,15 +79,15 @@ export const SSHKeysPage = () => {
|
||||
signature: signature.trim(),
|
||||
});
|
||||
setVerifyResults((prev) => ({ ...prev, [keyId]: result.valid }));
|
||||
setError(null);
|
||||
setMutationError(null);
|
||||
} catch {
|
||||
setError("Failed to verify signature");
|
||||
setMutationError("Failed to verify signature");
|
||||
} finally {
|
||||
setVerifying((prev) => ({ ...prev, [keyId]: false }));
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) return <div>Loading...</div>;
|
||||
if (status === "loading") return <div>Loading...</div>;
|
||||
|
||||
return (
|
||||
<section className="stack">
|
||||
@@ -116,7 +101,7 @@ export const SSHKeysPage = () => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && <div className="error">{error}</div>}
|
||||
{mutationError && <div className="error">{mutationError}</div>}
|
||||
|
||||
<form onSubmit={handleGenerate} className="stack">
|
||||
<div className="form-group">
|
||||
@@ -145,11 +130,21 @@ export const SSHKeysPage = () => {
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{status === "error" && (
|
||||
<div className="card stack">
|
||||
<p>Failed to load SSH keys</p>
|
||||
<button className="secondary-button" onClick={() => loadKeys()} type="button">
|
||||
<Icon name="refresh" size="sm" />
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="keys-list">
|
||||
{keys.length === 0 ? (
|
||||
{safeKeys.length === 0 ? (
|
||||
<p className="muted">No SSH keys yet. Generate one above.</p>
|
||||
) : (
|
||||
keys.map((key) => (
|
||||
safeKeys.map((key) => (
|
||||
<div key={key.id} className="key-card">
|
||||
<div className="key-header">
|
||||
<h3>{key.name}</h3>
|
||||
|
||||
Reference in New Issue
Block a user