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:
Alex Blank
2026-05-25 22:50:18 +02:00
parent 4c216dd1ca
commit ab79080f0b
31 changed files with 258 additions and 266 deletions
+34
View File
@@ -0,0 +1,34 @@
import { Icon } from "./icon";
interface LoadingStateProps {
message?: string;
}
export const LoadingState = ({ message = "Loading..." }: LoadingStateProps) => (
<p className="muted">{message}</p>
);
interface ErrorStateProps {
message?: string;
onRetry?: () => void;
}
export const ErrorState = ({ message = "Failed to load", onRetry }: ErrorStateProps) => (
<div className="card stack">
<p>{message}</p>
{onRetry && (
<button className="secondary-button" onClick={onRetry} type="button">
<Icon name="refresh" size="sm" />
Retry
</button>
)}
</div>
);
interface EmptyStateProps {
message: string;
}
export const EmptyState = ({ message }: EmptyStateProps) => (
<p className="muted">{message}</p>
);
+158
View File
@@ -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,
};
}
+3 -5
View File
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useState } from "react";
import { EmptyState, ErrorState, LoadingState } from "../components/data-states";
import { Icon } from "../components/icon";
import { useMobileViewport } from "../hooks/use-mobile-viewport";
import { extractErrorMessage } from "../utils/errors";
@@ -408,7 +409,7 @@ export const ConfigProfilesPage = () => {
if (status === "loading") {
return (
<div className="container">
<p>Loading Config Profiles...</p>
<LoadingState message="Loading Config Profiles..." />
</div>
);
}
@@ -416,10 +417,7 @@ export const ConfigProfilesPage = () => {
if (status === "error") {
return (
<div className="container">
<p className="text-error">Failed to load Config Profiles.</p>
<button onClick={loadData}>
<Icon name="refresh" size="sm" /> Retry
</button>
<ErrorState message="Failed to load Config Profiles." onRetry={loadData} />
</div>
);
}
+15 -71
View File
@@ -2,15 +2,17 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { getDashboardSummary, type DashboardSummary } from "../api/dashboard";
import { getUserSessions, startInstance, stopInstance, deleteInstance, recreateInstanceTunnel, checkInstanceHealth, type Session as SessionApi, type InstanceHealth } from "../api/sessions";
import { getUserSessions, checkInstanceHealth, type Session as SessionApi, type InstanceHealth } from "../api/sessions";
import { listProjects } from "../api/projects";
import { listRepositories, type GitRepository } from "../api/git_repositories";
import { listToolTypes, type ToolType } from "../api/tool_types";
import { updateUserConfig } from "../api/settings";
import type { Project } from "../types";
import { EmptyState, ErrorState, LoadingState } from "../components/data-states";
import { Icon } from "../components/icon";
import { CreateSessionForm } from "../components/create-session-form";
import { SessionList } from "../components/session-list";
import { useInstanceActions } from "../hooks/use-instance-actions";
type HomeStatus = "loading" | "ready" | "error";
@@ -31,7 +33,6 @@ export const HomePage = () => {
const [repositories, setRepositories] = useState<GitRepository[]>([]);
const [toolTypes, setToolTypes] = useState<ToolType[]>([]);
const [selectedProject, setSelectedProject] = useState("");
const [actionBusy, setActionBusy] = useState<string | null>(null);
const [tunnelHealth, setTunnelHealth] = useState<Record<string, InstanceHealth>>({});
const safeSessions = Array.isArray(sessions) ? sessions : [];
@@ -58,6 +59,15 @@ export const HomePage = () => {
void loadHome();
}, [loadHome]);
const {
loadingSessionId: actionBusy,
handleOpen,
handleStart,
handleStop,
handleDelete,
handleRecreateTunnel,
} = useInstanceActions({ onRefresh: loadHome });
// Poll tunnel health every 30 seconds for running instances
useEffect(() => {
const checkHealth = async () => {
@@ -130,64 +140,6 @@ export const HomePage = () => {
await loadHome();
};
const handleOpen = (session: SessionView) => {
if (session.url) {
window.open(session.url, "_blank", "noopener,noreferrer");
return;
}
if (session.tool_type_interfaces.includes("terminal")) {
navigate(`/instances/${session.id}/terminal`);
return;
}
navigate(`/projects/${session.project_id}`);
};
const handleStop = async (session: SessionView) => {
if (actionBusy === session.id) return;
setActionBusy(session.id);
try {
await stopInstance(session.project_id, session.repository_id, session.id);
await loadHome();
} finally {
setActionBusy(null);
}
};
const handleDelete = async (session: SessionView) => {
if (actionBusy === session.id) return;
setActionBusy(session.id);
try {
await deleteInstance(session.project_id, session.repository_id, session.id);
setSessions((prev) => prev.filter((s) => s.id !== session.id));
} catch {
// error - session remains in state
} finally {
setActionBusy(null);
}
};
const handleRecreateTunnel = async (session: SessionView) => {
if (actionBusy === session.id) return;
setActionBusy(session.id);
try {
await recreateInstanceTunnel(session.project_id, session.repository_id, session.id);
await loadHome();
} finally {
setActionBusy(null);
}
};
const handleStart = async (session: SessionView) => {
if (actionBusy === session.id) return;
setActionBusy(session.id);
try {
await startInstance(session.project_id, session.repository_id, session.id);
await loadHome();
} finally {
setActionBusy(null);
}
};
return (
<section className="stack home-page">
<header className="home-hero card">
@@ -202,17 +154,9 @@ export const HomePage = () => {
</div>
</header>
{status === "loading" && <p className="muted">Loading overview...</p>}
{status === "loading" && <LoadingState message="Loading overview..." />}
{status === "error" && (
<div className="card stack">
<p>Unable to load your workspace overview.</p>
<button className="secondary-button" type="button" onClick={() => void loadHome()}>
<Icon name="refresh" size="sm" />
Retry
</button>
</div>
)}
{status === "error" && <ErrorState message="Unable to load your workspace overview." onRetry={() => void loadHome()} />}
{status === "ready" && summary && (
<>
@@ -260,7 +204,7 @@ export const HomePage = () => {
<button className="secondary-button" type="button" onClick={() => navigate("/projects")}>View all</button>
</div>
{projects.length === 0 ? (
<p className="muted">No projects yet.</p>
<EmptyState message="No projects yet." />
) : (
<div className="home-project-grid">
{projects.map((project) => (
+3 -6
View File
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { getCommitDetail, getRepositoryHistory, type CommitDetail, type CommitHistoryEntry, type CommitHistoryResponse } from "../api/git_repositories";
import { EmptyState, ErrorState, LoadingState } from "../components/data-states";
import { Icon } from "../components/icon";
import { useAsyncData } from "../hooks/use-async-data";
@@ -54,7 +55,7 @@ export const GitHistoryPage = () => {
if (status === "loading") {
return (
<section className="stack">
<p className="muted">Loading commit history...</p>
<LoadingState message="Loading commit history..." />
</section>
);
}
@@ -62,11 +63,7 @@ export const GitHistoryPage = () => {
if (status === "error") {
return (
<section className="stack">
<p>Failed to load commit history</p>
<button className="secondary-button" onClick={() => reload()} type="button">
<Icon name="refresh" size="sm" />
Retry
</button>
<ErrorState message="Failed to load commit history" onRetry={reload} />
</section>
);
}
+4 -11
View File
@@ -6,6 +6,7 @@ import {
listRepositories,
} from "../api/git_repositories";
import type { GitRepository } from "../api/git_repositories";
import { EmptyState, ErrorState, LoadingState } from "../components/data-states";
import { Icon } from "../components/icon";
import { RepositoryCreateDialog } from "../components/repository-create-dialog";
import { useAsyncData } from "../hooks/use-async-data";
@@ -48,19 +49,11 @@ export const GitRepositoriesPage = () => {
</button>
</div>
{status === "loading" && <p className="muted">Loading repositories...</p>}
{status === "loading" && <LoadingState message="Loading repositories..." />}
{status === "error" && (
<div className="card stack">
<p>Failed to load repositories</p>
<button className="secondary-button" onClick={() => reload()} type="button">
<Icon name="refresh" size="sm" />
Retry
</button>
</div>
)}
{status === "error" && <ErrorState message="Failed to load repositories" onRetry={reload} />}
{isEmpty && <p className="muted">No repositories yet. Create your first repository above.</p>}
{isEmpty && <EmptyState message="No repositories yet. Create your first repository above." />}
{status === "ready" && safeRepositories.length > 0 && (
<div className="repository-list">
+3 -10
View File
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { getProfile, updateProfile, uploadAvatar } from "../api/profile";
import { EmptyState, ErrorState, LoadingState } from "../components/data-states";
import { Icon } from "../components/icon";
import { useAuth } from "../state/auth";
import { useAsyncData } from "../hooks/use-async-data";
@@ -91,17 +92,9 @@ export const ProfilePage = () => {
<section className="stack">
<h1>Profile</h1>
{displayStatus === "loading" && <p className="muted">Loading profile...</p>}
{displayStatus === "loading" && <LoadingState message="Loading profile..." />}
{displayStatus === "error" && (
<div className="card stack">
<p>Failed to load profile</p>
<button className="secondary-button" onClick={() => reload()} type="button">
<Icon name="refresh" size="sm" />
Retry
</button>
</div>
)}
{displayStatus === "error" && <ErrorState message="Failed to load profile" onRetry={reload} />}
{(displayStatus === "ready" || displayStatus === "saving") && profile && (
<div className="card stack">
+4 -11
View File
@@ -10,6 +10,7 @@ import {
type ProjectCreateInput,
type ProjectUpdateInput,
} from "../api/projects";
import { EmptyState, ErrorState, LoadingState } from "../components/data-states";
import { Icon } from "../components/icon";
import { useAsyncData } from "../hooks/use-async-data";
import type { Project } from "../types";
@@ -101,19 +102,11 @@ export const ProjectsPage = () => {
</button>
</div>
{status === "loading" && <p className="muted">Loading projects...</p>}
{status === "loading" && <LoadingState message="Loading projects..." />}
{status === "error" && (
<div className="card stack">
<p>Failed to load projects</p>
<button className="secondary-button" onClick={() => reload()} type="button">
<Icon name="refresh" size="sm" />
Retry
</button>
</div>
)}
{status === "error" && <ErrorState message="Failed to load projects" onRetry={reload} />}
{isEmpty && <p className="muted">No projects yet. Create your first project above.</p>}
{isEmpty && <EmptyState message="No projects yet. Create your first project above." />}
{status === "ready" && safeProjects.length > 0 && (
<div className="project-list">
+5 -14
View File
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useState } from "react";
import { EmptyState, ErrorState, LoadingState } from "../components/data-states";
import { Icon } from "../components/icon";
import { useMobileViewport } from "../hooks/use-mobile-viewport";
@@ -164,26 +165,16 @@ export const RepoWorkspace = () => {
)}
{status === "loading" && (
<p className="muted">Loading repositories...</p>
<LoadingState message="Loading repositories..." />
)}
{status === "error" && (
<div className="card stack">
<p>Failed to load repositories</p>
<button
className="secondary-button"
onClick={() => void loadRepositories()}
type="button"
>
<Icon name="refresh" size="sm" />
Retry
</button>
</div>
<ErrorState message="Failed to load repositories" onRetry={() => void loadRepositories()} />
)}
{status === "empty" && (
<div className="card stack">
<p>No repositories in this project yet.</p>
<EmptyState message="No repositories in this project yet." />
<Link
className="primary-button"
to={`/projects/${projectId}/settings/repositories`}
@@ -486,7 +477,7 @@ const FileBrowser = ({
</button>
)}
{entries.length === 0 && (
<p className="muted">No files in this repository yet.</p>
<EmptyState message="No files in this repository yet." />
)}
{entries.map((entry) => {
const fileStatus = entry.type === "file" ? getFileStatus(entry.path) : null;
+18 -115
View File
@@ -7,18 +7,15 @@ import { listRepositories, type GitRepository } from "../api/git_repositories";
import {
getUserSessions,
type Session,
deleteInstance,
stopInstance,
startInstance,
checkInstanceHealth,
recreateInstanceTunnel,
} from "../api/sessions";
import { listToolTypes, type ToolType } from "../api/tool_types";
import { getUserConfig, updateUserConfig } from "../api/settings";
import { Icon } from "../components/icon";
import { EmptyState, ErrorState, LoadingState } from "../components/data-states";
import { CreateSessionForm } from "../components/create-session-form";
import { SessionList } from "../components/session-list";
import { SessionCard } from "../components/session-card";
import { useInstanceActions } from "../hooks/use-instance-actions";
import type { InstanceHealth } from "../api/sessions";
type SessionsStatus = "loading" | "ready" | "error";
@@ -34,11 +31,7 @@ export const SessionsPage = () => {
const [toolTypes, setToolTypes] = useState<ToolType[]>([]);
const [selectedProject, setSelectedProject] = useState<string>("");
const [dirtyDeleteSession, setDirtyDeleteSession] = useState<Session | null>(null);
const [dirtyDeleteFiles, setDirtyDeleteFiles] = useState<string[]>([]);
const [tunnelHealth, setTunnelHealth] = useState<Record<string, InstanceHealth>>({});
const [loadingSessionId, setLoadingSessionId] = useState<string | null>(null);
const loadSessions = useCallback(async () => {
setStatus("loading");
@@ -83,7 +76,18 @@ export const SessionsPage = () => {
void loadToolTypes();
}, []);
const {
loadingSessionId,
dirtyDeleteSession,
dirtyDeleteFiles,
handleOpen,
handleStart,
handleStop,
handleDelete,
handleForceDelete,
handleRecreateTunnel,
clearDirtyDelete,
} = useInstanceActions({ onRefresh: loadSessions });
// Poll health every 30 seconds for active web-enabled instances
useEffect(() => {
@@ -154,116 +158,15 @@ export const SessionsPage = () => {
await loadSessions();
};
const handleStop = async (session: Session) => {
if (loadingSessionId === session.id) return;
setLoadingSessionId(session.id);
try {
await stopInstance(session.project_id, session.repository_id, session.id);
await loadSessions();
} catch {
// ignore
} finally {
setLoadingSessionId(null);
}
};
const handleDelete = 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([]);
// Remove from local state immediately
setSessions((prev) => prev.filter((s) => s.id !== session.id));
} 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);
}
};
const handleForceDelete = 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([]);
setSessions((prev) => prev.filter((s) => s.id !== session.id));
} catch {
// ignore
} finally {
setLoadingSessionId(null);
}
};
const handleRecreateTunnel = async (session: Session) => {
if (loadingSessionId === session.id) return;
setLoadingSessionId(session.id);
try {
await recreateInstanceTunnel(
session.project_id,
session.repository_id,
session.id
);
// Refresh sessions to get new URL
await loadSessions();
} catch {
// ignore
} finally {
setLoadingSessionId(null);
}
};
const handleStart = async (session: Session) => {
if (loadingSessionId === session.id) return;
setLoadingSessionId(session.id);
try {
await startInstance(session.project_id, session.repository_id, session.id);
await loadSessions();
} catch {
// ignore
} finally {
setLoadingSessionId(null);
}
};
const handleOpen = (session: Session) => {
if (session.url) {
window.open(session.url, '_blank', 'noopener,noreferrer');
} else if (session.tool_type_interfaces?.includes("terminal")) {
navigate(`/instances/${session.id}/terminal`);
} else {
navigate(`/projects/${session.project_id}`);
}
};
return (
<section className="stack sessions-page">
<div className="page-header">
<h1>Sessions</h1>
</div>
{status === "loading" && <p className="muted">Loading sessions...</p>}
{status === "loading" && <LoadingState message="Loading sessions..." />}
{status === "error" && (
<div className="card stack">
<p>Failed to load sessions</p>
<button className="secondary-button" onClick={() => void loadSessions()} type="button">
<Icon name="refresh" size="sm" />
Retry
</button>
</div>
)}
{status === "error" && <ErrorState message="Failed to load sessions" onRetry={() => void loadSessions()} />}
{status === "ready" && (
<>
@@ -311,7 +214,7 @@ export const SessionsPage = () => {
{/* Dirty Delete Confirmation Modal */}
{dirtyDeleteSession && (
<div className="modal-overlay" onClick={() => setDirtyDeleteSession(null)}>
<div className="modal-overlay" onClick={clearDirtyDelete}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<h3>Uncommitted Changes</h3>
<p>
@@ -330,7 +233,7 @@ export const SessionsPage = () => {
<div className="modal-actions">
<button
className="secondary-button"
onClick={() => setDirtyDeleteSession(null)}
onClick={clearDirtyDelete}
type="button"
>
Cancel
+3 -6
View File
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
import { Link, Outlet, useLocation, useOutletContext } from "react-router-dom";
import { getUserConfig, updateUserConfig, type UserConfig, type UserConfigUpdate } from "../api/settings";
import { EmptyState, ErrorState, LoadingState } from "../components/data-states";
import { Icon } from "../components/icon";
import { useAsyncData } from "../hooks/use-async-data";
@@ -71,17 +72,13 @@ export const SettingsPage = () => {
};
if (status === "loading") {
return <section className="stack"><p className="muted">Loading settings...</p></section>;
return <section className="stack"><LoadingState message="Loading settings..." /></section>;
}
if (status === "error") {
return (
<section className="stack">
<p>Failed to load settings</p>
<button className="secondary-button" onClick={() => reload()} type="button">
<Icon name="refresh" size="sm" />
Retry
</button>
<ErrorState message="Failed to load settings" onRetry={reload} />
</section>
);
}
+4 -11
View File
@@ -1,6 +1,7 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { createSSHKey, deleteSSHKey, listSSHKeys, signPayload, verifySignature, type SSHKey } from "../api/ssh_keys";
import { EmptyState, ErrorState, LoadingState } from "../components/data-states";
import { Icon } from "../components/icon";
import { useAsyncData } from "../hooks/use-async-data";
@@ -87,7 +88,7 @@ export const SSHKeysPage = () => {
}
}
if (status === "loading") return <div>Loading...</div>;
if (status === "loading") return <LoadingState message="Loading SSH keys..." />;
return (
<section className="stack">
@@ -130,19 +131,11 @@ 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>
)}
{status === "error" && <ErrorState message="Failed to load SSH keys" onRetry={loadKeys} />}
<div className="keys-list">
{safeKeys.length === 0 ? (
<p className="muted">No SSH keys yet. Generate one above.</p>
<EmptyState message="No SSH keys yet. Generate one above." />
) : (
safeKeys.map((key) => (
<div key={key.id} className="key-card">
+4 -6
View File
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useState } from "react";
import { EmptyState, ErrorState, LoadingState } from "../components/data-states";
import { Icon } from "../components/icon";
import { useMobileViewport } from "../hooks/use-mobile-viewport";
import { extractErrorMessage } from "../utils/errors";
@@ -491,7 +492,7 @@ export const ToolWorkshopPage = () => {
if (status === "loading") {
return (
<div className="container">
<p>Loading Tool Workshop...</p>
<LoadingState message="Loading Tool Workshop..." />
</div>
);
}
@@ -499,10 +500,7 @@ export const ToolWorkshopPage = () => {
if (status === "error") {
return (
<div className="container">
<p className="text-error">Failed to load Tool Workshop.</p>
<button onClick={loadData}>
<Icon name="refresh" size="sm" /> Retry
</button>
<ErrorState message="Failed to load Tool Workshop." onRetry={loadData} />
</div>
);
}
@@ -1305,7 +1303,7 @@ export const ToolWorkshopPage = () => {
<div className="stack" style={{ gap: "0.5rem" }}>
{toolConfigs.length === 0 ? (
<p className="muted">No configurations for this tool type yet.</p>
<EmptyState message="No configurations for this tool type yet." />
) : (
toolConfigs.map((config) => (
<div