error handling, coderabbit
This commit is contained in:
+34
-4
@@ -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>;
|
||||
|
||||
@@ -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 };
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user