refactor: consolidate loading/error states and extract instance actions hook
Frontend: - Create reusable DataStates components (LoadingState, ErrorState, EmptyState) - Refactor 12 pages to use shared state components instead of inline JSX - Extract useInstanceActions hook to eliminate session action duplication - Update dashboard and sessions pages to use shared hook OpenSpec: - Archive completed mobile-app-usability change (44/44 tasks) - Archive completed add-config-profiles change (15/15 tasks) Quality: TypeScript check passes, production build succeeds
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import {
|
||||
stopInstance,
|
||||
deleteInstance,
|
||||
startInstance,
|
||||
recreateInstanceTunnel,
|
||||
} from "../api/sessions";
|
||||
import type { Session } from "../api/sessions";
|
||||
|
||||
interface UseInstanceActionsOptions {
|
||||
onRefresh: () => Promise<void>;
|
||||
}
|
||||
|
||||
interface UseInstanceActionsReturn {
|
||||
loadingSessionId: string | null;
|
||||
dirtyDeleteSession: Session | null;
|
||||
dirtyDeleteFiles: string[];
|
||||
handleOpen: (session: Session) => void;
|
||||
handleStart: (session: Session) => Promise<void>;
|
||||
handleStop: (session: Session) => Promise<void>;
|
||||
handleDelete: (session: Session) => Promise<void>;
|
||||
handleForceDelete: (session: Session) => Promise<void>;
|
||||
handleRecreateTunnel: (session: Session) => Promise<void>;
|
||||
clearDirtyDelete: () => void;
|
||||
}
|
||||
|
||||
export function useInstanceActions(
|
||||
options: UseInstanceActionsOptions
|
||||
): UseInstanceActionsReturn {
|
||||
const { onRefresh } = options;
|
||||
const [loadingSessionId, setLoadingSessionId] = useState<string | null>(null);
|
||||
const [dirtyDeleteSession, setDirtyDeleteSession] = useState<Session | null>(null);
|
||||
const [dirtyDeleteFiles, setDirtyDeleteFiles] = useState<string[]>([]);
|
||||
|
||||
const handleOpen = useCallback((session: Session) => {
|
||||
if (session.url) {
|
||||
window.open(session.url, "_blank", "noopener,noreferrer");
|
||||
return;
|
||||
}
|
||||
if (session.tool_type_interfaces?.includes("terminal")) {
|
||||
window.location.href = `/instances/${session.id}/terminal`;
|
||||
return;
|
||||
}
|
||||
window.location.href = `/projects/${session.project_id}`;
|
||||
}, []);
|
||||
|
||||
const handleStart = useCallback(
|
||||
async (session: Session) => {
|
||||
if (loadingSessionId === session.id) return;
|
||||
setLoadingSessionId(session.id);
|
||||
try {
|
||||
await startInstance(session.project_id, session.repository_id, session.id);
|
||||
await onRefresh();
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
setLoadingSessionId(null);
|
||||
}
|
||||
},
|
||||
[loadingSessionId, onRefresh]
|
||||
);
|
||||
|
||||
const handleStop = useCallback(
|
||||
async (session: Session) => {
|
||||
if (loadingSessionId === session.id) return;
|
||||
setLoadingSessionId(session.id);
|
||||
try {
|
||||
await stopInstance(session.project_id, session.repository_id, session.id);
|
||||
await onRefresh();
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
setLoadingSessionId(null);
|
||||
}
|
||||
},
|
||||
[loadingSessionId, onRefresh]
|
||||
);
|
||||
|
||||
const handleDelete = useCallback(
|
||||
async (session: Session) => {
|
||||
if (loadingSessionId === session.id) return;
|
||||
setLoadingSessionId(session.id);
|
||||
try {
|
||||
await deleteInstance(session.project_id, session.repository_id, session.id);
|
||||
setDirtyDeleteSession(null);
|
||||
setDirtyDeleteFiles([]);
|
||||
await onRefresh();
|
||||
} catch (error) {
|
||||
const axiosError = error as {
|
||||
response?: { status?: number; data?: { detail?: { changed_files?: string[] } } };
|
||||
};
|
||||
if (axiosError.response?.status === 409) {
|
||||
const detail = axiosError.response.data?.detail;
|
||||
if (detail?.changed_files) {
|
||||
setDirtyDeleteSession(session);
|
||||
setDirtyDeleteFiles(detail.changed_files);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setLoadingSessionId(null);
|
||||
}
|
||||
},
|
||||
[loadingSessionId, onRefresh]
|
||||
);
|
||||
|
||||
const handleForceDelete = useCallback(
|
||||
async (session: Session) => {
|
||||
if (loadingSessionId === session.id) return;
|
||||
setLoadingSessionId(session.id);
|
||||
try {
|
||||
await deleteInstance(session.project_id, session.repository_id, session.id, true);
|
||||
setDirtyDeleteSession(null);
|
||||
setDirtyDeleteFiles([]);
|
||||
await onRefresh();
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
setLoadingSessionId(null);
|
||||
}
|
||||
},
|
||||
[loadingSessionId, onRefresh]
|
||||
);
|
||||
|
||||
const handleRecreateTunnel = useCallback(
|
||||
async (session: Session) => {
|
||||
if (loadingSessionId === session.id) return;
|
||||
setLoadingSessionId(session.id);
|
||||
try {
|
||||
await recreateInstanceTunnel(session.project_id, session.repository_id, session.id);
|
||||
await onRefresh();
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
setLoadingSessionId(null);
|
||||
}
|
||||
},
|
||||
[loadingSessionId, onRefresh]
|
||||
);
|
||||
|
||||
const clearDirtyDelete = useCallback(() => {
|
||||
setDirtyDeleteSession(null);
|
||||
setDirtyDeleteFiles([]);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
loadingSessionId,
|
||||
dirtyDeleteSession,
|
||||
dirtyDeleteFiles,
|
||||
handleOpen,
|
||||
handleStart,
|
||||
handleStop,
|
||||
handleDelete,
|
||||
handleForceDelete,
|
||||
handleRecreateTunnel,
|
||||
clearDirtyDelete,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user