error handling, coderabbit

This commit is contained in:
lda
2026-05-03 15:39:10 +07:00 Verified
parent 155458791e
commit 3f8abc1571
33 changed files with 393 additions and 298 deletions
+34 -4
View File
@@ -153,6 +153,25 @@ export type WakeFleetDeviceResponse = {
};
};
export interface APIErrorResponse {
error: {
code: string;
message: string;
details?: any;
};
}
export class APIError extends Error {
constructor(
message: string,
public code: string,
public details: any,
) {
super(message);
this.name = "APIError";
}
}
async function request<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(path, {
...init,
@@ -164,11 +183,22 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
if (!res.ok) {
const raw = await res.text();
let detail = raw;
let parsed: APIErrorResponse | null = null;
try {
detail = JSON.stringify(JSON.parse(raw), null, 2);
} catch {}
throw new Error(`${res.status} ${res.statusText}\n${detail}`);
parsed = JSON.parse(raw);
// assume APIError
if (parsed?.error?.details) {
parsed.error.details = JSON.stringify(parsed.error.details);
}
} catch {
// not JSON
}
throw new APIError(
`${res.status} ${res.statusText}\n${raw}`,
parsed?.error?.code ?? "unknown",
parsed?.error?.details ?? parsed?.error?.message ?? raw,
);
}
return res.json() as Promise<T>;
+4 -1
View File
@@ -78,6 +78,7 @@ export function DevicesPage({ agents, onAfterWake }: Props) {
presence,
known,
agentId: agentId === "all" ? "" : agentId,
visibility: "all",
limit: 500,
}),
fetchKnownDevices(),
@@ -373,7 +374,9 @@ function FleetDeviceRow({
: "";
return (
<div className={`grid gap-3 rounded-md border border-l-[3px] ${presenceBorder} bg-card px-3 py-3 text-sm xl:grid-cols-[minmax(10rem,1.3fr)_8rem_minmax(9rem,1fr)_minmax(10rem,1fr)_minmax(10rem,1fr)_8rem_8rem] xl:items-center xl:gap-2`}>
<div
className={`grid gap-3 rounded-md border border-l-[3px] ${presenceBorder} bg-card px-3 py-3 text-sm xl:grid-cols-[minmax(10rem,1.3fr)_8rem_minmax(9rem,1fr)_minmax(10rem,1fr)_minmax(10rem,1fr)_8rem_8rem] xl:items-center xl:gap-2`}
>
<button className="min-w-0 text-left" type="button" onClick={onDetails}>
<div className="truncate font-medium">{device.display_name}</div>
<div className="mt-1 flex flex-wrap gap-1">
@@ -96,14 +96,19 @@ export function FleetDeviceDetailsDialog({
(kd) => kd.device_id !== currentDevice.known_device?.device_id,
);
async function wrap(fn: () => Promise<void>) {
async function wrap(fn: () => Promise<void | { changed?: boolean }>) {
setError("");
setActionBusy(true);
try {
await fn();
onChanged();
} catch (err) {
const res = await fn();
if (!res || res.changed !== false) {
onChanged();
}
} catch (err: any) {
setError(String(err));
if (err && (err.changed || err.partial)) {
onChanged();
}
} finally {
setActionBusy(false);
}
@@ -125,12 +130,25 @@ export function FleetDeviceDetailsDialog({
const target = knownDevices.find((kd) => kd.device_id === targetDeviceId);
await wrap(async () => {
const ids = fullKnown ? unattachedIdentifiers : rowIdentifiers;
for (const id of ids) {
await attachDeviceIdentifier(targetDeviceId, id);
if (ids.length === 0) return;
const results = await Promise.allSettled(
ids.map((id) => attachDeviceIdentifier(targetDeviceId, id)),
);
const changed = results.some((r) => r.status === "fulfilled");
const errors = results.filter((r) => r.status === "rejected");
if (errors.length) {
const err = errors[0].reason as Error;
if (changed) (err as any).changed = true;
throw err;
}
toast.success(
`Attached ${ids.length} identifier(s) to "${target?.display_name ?? targetDeviceId}"`,
);
return { changed };
});
}