refactor: rename files to PascalCase components and kebab-case APIs (Task 4.4)
- Rename all component files to PascalCase matching exported names - Move components into feature directories (git/, session/, project/, terminal/, workspace/, ui/, layout/) - Rename all page files to PascalCase with Page suffix - Rename all API files to kebab-case - Update all imports across codebase with corrected relative depths - Preserve git history via git mv Quality gates: tsc (pass), eslint (pass), 66/74 tests pass (8 pre-existing failures) Refs: repo-restructure Task 4.4
This commit is contained in:
@@ -5,7 +5,7 @@ import {
|
||||
deleteConfigFolder,
|
||||
listConfigFolders,
|
||||
updateConfigFolder,
|
||||
} from "../api/config_folders";
|
||||
} from "../api/config-folders";
|
||||
|
||||
const mockGet = vi.fn();
|
||||
const mockPost = vi.fn();
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
listToolTypes,
|
||||
updateToolType,
|
||||
validateToolType,
|
||||
} from "../api/tool_types";
|
||||
} from "../api/tool-types";
|
||||
|
||||
const mockGet = vi.fn();
|
||||
const mockPost = vi.fn();
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { render, screen } from "@testing-library/react";
|
||||
import { MemoryRouter, Route, Routes } from "react-router-dom";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { ProtectedRoute } from "./protected-route";
|
||||
import { ProtectedRoute } from "./ProtectedRoute";
|
||||
|
||||
const mockUseAuth = vi.fn();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Session } from "../../../types/session";
|
||||
import { Icon } from "../../../components/icon";
|
||||
import { Icon } from "../../ui/Icon";
|
||||
|
||||
interface ActiveSessionsListProps {
|
||||
sessions: Session[];
|
||||
@@ -28,7 +28,11 @@ export const ActiveSessionsList = ({
|
||||
<article className="card session-card" key={session.id}>
|
||||
<div className="stack-sm">
|
||||
<div className="row row-tight">
|
||||
<h3>{session.display_name || session.tool_type_name || "Unnamed Session"}</h3>
|
||||
<h3>
|
||||
{session.display_name ||
|
||||
session.tool_type_name ||
|
||||
"Unnamed Session"}
|
||||
</h3>
|
||||
<span className={`status-badge ${session.status}`}>
|
||||
{session.status}
|
||||
</span>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from "react";
|
||||
import type { Project } from "../../../types/project";
|
||||
import type { GitRepository } from "../../../types/git-repository";
|
||||
import type { ToolType } from "../../../types/tool-type";
|
||||
import { Icon } from "../../../components/icon";
|
||||
import { Icon } from "../../ui/Icon";
|
||||
|
||||
interface QuickCreateFormProps {
|
||||
projects: Project[];
|
||||
|
||||
@@ -24,7 +24,9 @@ export const RecentSessionsSection = ({
|
||||
<article className="recent-session-item" key={session.id}>
|
||||
<div className="recent-session-info">
|
||||
<span className="recent-session-name">
|
||||
{session.display_name || session.tool_type_name || "Unnamed Session"}
|
||||
{session.display_name ||
|
||||
session.tool_type_name ||
|
||||
"Unnamed Session"}
|
||||
</span>
|
||||
<span className="muted">
|
||||
{session.project_name} · {session.tool_type_name}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import styles from "./features/git/CommitDialog.module.css";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { Icon } from "./icon";
|
||||
import { Icon } from "../../ui/Icon";
|
||||
|
||||
interface CommitDialogProps {
|
||||
isOpen: boolean;
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { commitChanges } from "../api/git_repositories";
|
||||
import { commitChanges } from "../../../api/git-repositories";
|
||||
import styles from "./features/git/CommitPanel.module.css";
|
||||
|
||||
interface CommitPanelProps {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { Icon } from "../../icon";
|
||||
import { Icon } from "../../ui/Icon";
|
||||
import { apiClient } from "../../../api/client";
|
||||
import type { GitStatus } from "../../../types/git-repository";
|
||||
|
||||
|
||||
+7
-7
@@ -1,13 +1,13 @@
|
||||
import styles from "./features/git/FileEditor.module.css";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { apiClient } from "../api/client";
|
||||
import { useAuth } from "../state/auth";
|
||||
import { CodeEditor } from "../components/code-editor";
|
||||
import { CommitDialog } from "../components/commit-dialog";
|
||||
import { Icon } from "../components/icon";
|
||||
import { SyntaxHighlighter } from "../components/syntax-highlighter";
|
||||
import { detectLanguage } from "../utils/language";
|
||||
import { apiClient } from "../../../api/client";
|
||||
import { useAuth } from "../../../state/auth";
|
||||
import { CodeEditor } from "../../ui/CodeEditor";
|
||||
import { CommitDialog } from "./CommitDialog";
|
||||
import { Icon } from "../../ui/Icon";
|
||||
import { SyntaxHighlighter } from "./SyntaxHighlighter";
|
||||
import { detectLanguage } from "../../../utils/language";
|
||||
|
||||
interface FileEditorProps {
|
||||
projectId: string;
|
||||
+3
-3
@@ -8,9 +8,9 @@ import {
|
||||
pullRepository,
|
||||
pushRepository,
|
||||
type GitStatus,
|
||||
} from "../api/git_repositories";
|
||||
import { Icon } from "./icon";
|
||||
import { MergeDialog } from "./merge-dialog";
|
||||
} from "../../../api/git-repositories";
|
||||
import { Icon } from "../../ui/Icon";
|
||||
import { MergeDialog } from "./MergeDialog";
|
||||
import styles from "./features/git/GitToolbar.module.css";
|
||||
|
||||
interface GitToolbarProps {
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import styles from "./features/git/MergeDialog.module.css";
|
||||
import { useState } from "react";
|
||||
|
||||
import { mergeBranches } from "../api/git_repositories";
|
||||
import { Icon } from "./icon";
|
||||
import { mergeBranches } from "../../../api/git-repositories";
|
||||
import { Icon } from "../../ui/Icon";
|
||||
|
||||
interface MergeDialogProps {
|
||||
projectId: string;
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { Icon } from "./icon";
|
||||
import { highlightCode, loadLanguage } from "../utils/language";
|
||||
import { Icon } from "../../ui/Icon";
|
||||
import { highlightCode, loadLanguage } from "../../../utils/language";
|
||||
|
||||
interface SyntaxHighlighterProps {
|
||||
code: string;
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { GitRepository } from "../../../types/git-repository";
|
||||
import type { GitStatus } from "../../../api/git_repositories";
|
||||
import type { GitStatus } from "../../../api/git-repositories";
|
||||
import type { ToolType } from "../../../types/tool-type";
|
||||
import { FileBrowser } from "./FileBrowser";
|
||||
import { CommitPanel } from "../../../components/commit-panel";
|
||||
import { InstanceList } from "../../../components/instance-list";
|
||||
import { CommitPanel } from "../git/CommitPanel";
|
||||
import { InstanceList } from "../session/InstanceList";
|
||||
|
||||
interface WorkspaceSidebarProps {
|
||||
projectId: string;
|
||||
@@ -29,10 +29,7 @@ export const WorkspaceSidebar = ({
|
||||
<div className="sidebar-section">
|
||||
<label className="form-field">
|
||||
Repository
|
||||
<select
|
||||
value={repoId}
|
||||
onChange={(e) => onRepoChange(e.target.value)}
|
||||
>
|
||||
<select value={repoId} onChange={(e) => onRepoChange(e.target.value)}>
|
||||
{repositories.map((repo) => (
|
||||
<option key={repo.id} value={repo.id}>
|
||||
{repo.name}
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { RepositoriesSettingsTab } from "./repositories-settings-tab";
|
||||
import * as gitRepositoriesApi from "../api/git_repositories";
|
||||
import { RepositoriesSettingsTab } from "./RepositoriesSettingsTab";
|
||||
import * as gitRepositoriesApi from "../../../api/git-repositories";
|
||||
|
||||
const mockRepositories = [
|
||||
{
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
import type { GitRepository } from "../types/git-repository";
|
||||
import { deleteRepository, listRepositories } from "../api/git_repositories";
|
||||
import { RepositoryCreateDialog } from "./repository-create-dialog";
|
||||
import { Icon } from "./icon";
|
||||
import type { GitRepository } from "../../../types/git-repository";
|
||||
import { deleteRepository, listRepositories } from "../../../api/git-repositories";
|
||||
import { RepositoryCreateDialog } from "./RepositoryCreateDialog";
|
||||
import { Icon } from "../../ui/Icon";
|
||||
|
||||
export const RepositoriesSettingsTab: React.FC = () => {
|
||||
const { projectId } = useParams<{ projectId: string }>();
|
||||
+3
-3
@@ -3,9 +3,9 @@ import { useEffect, useRef, useState } from "react";
|
||||
import type {
|
||||
GitRepositoryCreate,
|
||||
URLParseResult,
|
||||
} from "../types/git-repository";
|
||||
import { createRepository, parseGitUrl } from "../api/git_repositories";
|
||||
import { Icon } from "./icon";
|
||||
} from "../../../types/git-repository";
|
||||
import { createRepository, parseGitUrl } from "../../../api/git-repositories";
|
||||
import { Icon } from "../../ui/Icon";
|
||||
|
||||
type CreateMode = "clone" | "blank";
|
||||
type UrlValidationStatus =
|
||||
@@ -17,7 +17,7 @@ vi.mock("@/api/settings", () => ({
|
||||
updateUserConfig: vi.fn(),
|
||||
}));
|
||||
|
||||
import { listRepositories } from "@/api/git_repositories";
|
||||
import { listRepositories } from "@/api/git-repositories";
|
||||
import { createInstance } from "@/api/sessions";
|
||||
|
||||
const mockProjects = [
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { listRepositories } from "@/api/git_repositories";
|
||||
import { listRepositories } from "@/api/git-repositories";
|
||||
import { createInstance, startInstance } from "@/api/sessions";
|
||||
import { updateUserConfig } from "@/api/settings";
|
||||
import { Icon } from "@/components/icon";
|
||||
import { Icon } from "@/components/ui/Icon";
|
||||
import type { Project } from "@/types/project";
|
||||
import type { GitRepository } from "@/types/git-repository";
|
||||
import type { ToolType } from "@/types/tool-type";
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Icon } from "./icon";
|
||||
import type { ToolInstance } from "../types/tool-instance";
|
||||
import type { ToolType } from "../types/tool-type";
|
||||
import { Icon } from "../../ui/Icon";
|
||||
import type { ToolInstance } from "../../../types/tool-instance";
|
||||
import type { ToolType } from "../../../types/tool-type";
|
||||
import {
|
||||
checkInstanceHealth,
|
||||
createInstance,
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
restartInstance,
|
||||
startInstance,
|
||||
stopInstance,
|
||||
} from "../api/sessions";
|
||||
} from "../../../api/sessions";
|
||||
import styles from "./features/session/InstanceList.module.css";
|
||||
|
||||
const API_BASE_URL =
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { Icon } from "@/components/icon";
|
||||
import { Icon } from "@/components/ui/Icon";
|
||||
import type { Session } from "@/types/session";
|
||||
|
||||
interface SessionCardProps {
|
||||
|
||||
+2
-2
@@ -5,11 +5,11 @@ import { SerializeAddon } from "xterm-addon-serialize";
|
||||
import { WebLinksAddon } from "xterm-addon-web-links";
|
||||
import "xterm/css/xterm.css";
|
||||
|
||||
import { useTerminalConnection } from "../hooks/use-terminal-connection";
|
||||
import { useTerminalConnection } from "../../../hooks/use-terminal-connection";
|
||||
import type {
|
||||
ServerControlMessage,
|
||||
TerminalConnectionState,
|
||||
} from "../types/terminal";
|
||||
} from "../../../types/terminal";
|
||||
import styles from "./features/terminal/TerminalComponent.module.css";
|
||||
|
||||
interface TerminalProps {
|
||||
@@ -12,14 +12,20 @@ interface ToolConfigFormProps {
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const ToolConfigForm = ({ editingConfig, onSubmit, onCancel }: ToolConfigFormProps) => {
|
||||
export const ToolConfigForm = ({
|
||||
editingConfig,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: ToolConfigFormProps) => {
|
||||
const [formData, setFormData] = useState({
|
||||
key: editingConfig?.key ?? "",
|
||||
value: editingConfig?.value ?? "",
|
||||
config_type: editingConfig?.config_type ?? "env",
|
||||
file_path: editingConfig?.file_path ?? "",
|
||||
});
|
||||
const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "saved" | "error">("idle");
|
||||
const [saveStatus, setSaveStatus] = useState<
|
||||
"idle" | "saving" | "saved" | "error"
|
||||
>("idle");
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -53,7 +59,9 @@ export const ToolConfigForm = ({ editingConfig, onSubmit, onCancel }: ToolConfig
|
||||
<select
|
||||
id="config-type"
|
||||
value={formData.config_type}
|
||||
onChange={(e) => setFormData({ ...formData, config_type: e.target.value })}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, config_type: e.target.value })
|
||||
}
|
||||
className="form-input"
|
||||
>
|
||||
<option value="env">Environment Variable</option>
|
||||
@@ -67,7 +75,9 @@ export const ToolConfigForm = ({ editingConfig, onSubmit, onCancel }: ToolConfig
|
||||
id="config-file-path"
|
||||
type="text"
|
||||
value={formData.file_path}
|
||||
onChange={(e) => setFormData({ ...formData, file_path: e.target.value })}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, file_path: e.target.value })
|
||||
}
|
||||
placeholder="e.g., /app/config.json"
|
||||
className="form-input"
|
||||
required
|
||||
@@ -79,19 +89,24 @@ export const ToolConfigForm = ({ editingConfig, onSubmit, onCancel }: ToolConfig
|
||||
<textarea
|
||||
id="config-value"
|
||||
value={formData.value}
|
||||
onChange={(e) => setFormData({ ...formData, value: e.target.value })}
|
||||
placeholder={formData.config_type === "env" ? "Enter value..." : "Enter file contents..."}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, value: e.target.value })
|
||||
}
|
||||
placeholder={
|
||||
formData.config_type === "env"
|
||||
? "Enter value..."
|
||||
: "Enter file contents..."
|
||||
}
|
||||
className="form-input"
|
||||
rows={formData.config_type === "file" ? 8 : 2}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="row" style={{ gap: "0.5rem", justifyContent: "flex-end" }}>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button"
|
||||
onClick={onCancel}
|
||||
>
|
||||
<div
|
||||
className="row"
|
||||
style={{ gap: "0.5rem", justifyContent: "flex-end" }}
|
||||
>
|
||||
<button type="button" className="secondary-button" onClick={onCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="primary-button">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Icon } from "../../../components/icon";
|
||||
import { Icon } from "../../ui/Icon";
|
||||
import type { ToolConfig } from "../../../types/tool-config";
|
||||
|
||||
interface ToolConfigListProps {
|
||||
|
||||
@@ -1,23 +1,46 @@
|
||||
import { useState } from "react";
|
||||
import { Icon } from "../../../components/icon";
|
||||
import type { ToolType, CreateToolTypeRequest, UpdateToolTypeRequest } from "../../../types/tool-type";
|
||||
import { Icon } from "../../ui/Icon";
|
||||
import type {
|
||||
ToolType,
|
||||
CreateToolTypeRequest,
|
||||
UpdateToolTypeRequest,
|
||||
} from "../../../types/tool-type";
|
||||
|
||||
interface ToolTypeFormProps {
|
||||
mode: "create" | "edit";
|
||||
toolType?: ToolType | null;
|
||||
onSubmit: (input: CreateToolTypeRequest | UpdateToolTypeRequest) => Promise<void>;
|
||||
onSubmit: (
|
||||
input: CreateToolTypeRequest | UpdateToolTypeRequest,
|
||||
) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const ToolTypeForm = ({ mode, toolType, onSubmit, onCancel }: ToolTypeFormProps) => {
|
||||
export const ToolTypeForm = ({
|
||||
mode,
|
||||
toolType,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: ToolTypeFormProps) => {
|
||||
const [formName, setFormName] = useState(toolType?.name ?? "");
|
||||
const [formDisplayName, setFormDisplayName] = useState(toolType?.display_name ?? "");
|
||||
const [formDescription, setFormDescription] = useState(toolType?.description ?? "");
|
||||
const [formDisplayName, setFormDisplayName] = useState(
|
||||
toolType?.display_name ?? "",
|
||||
);
|
||||
const [formDescription, setFormDescription] = useState(
|
||||
toolType?.description ?? "",
|
||||
);
|
||||
const [formCategory, setFormCategory] = useState(toolType?.category ?? "");
|
||||
const [formInterfaces, setFormInterfaces] = useState<string[]>(toolType?.interfaces ?? []);
|
||||
const [formPort, setFormPort] = useState(toolType?.default_port?.toString() ?? "");
|
||||
const [formTemplate, setFormTemplate] = useState(toolType?.compose_template ?? "");
|
||||
const [formVariables, setFormVariables] = useState(toolType?.required_variables?.join(", ") ?? "");
|
||||
const [formInterfaces, setFormInterfaces] = useState<string[]>(
|
||||
toolType?.interfaces ?? [],
|
||||
);
|
||||
const [formPort, setFormPort] = useState(
|
||||
toolType?.default_port?.toString() ?? "",
|
||||
);
|
||||
const [formTemplate, setFormTemplate] = useState(
|
||||
toolType?.compose_template ?? "",
|
||||
);
|
||||
const [formVariables, setFormVariables] = useState(
|
||||
toolType?.required_variables?.join(", ") ?? "",
|
||||
);
|
||||
const [formError, setFormError] = useState<string | null>(null);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
@@ -33,7 +56,10 @@ export const ToolTypeForm = ({ mode, toolType, onSubmit, onCancel }: ToolTypeFor
|
||||
return;
|
||||
}
|
||||
|
||||
const variables = formVariables.split(",").map((v) => v.trim()).filter((v) => v.length > 0);
|
||||
const variables = formVariables
|
||||
.split(",")
|
||||
.map((v) => v.trim())
|
||||
.filter((v) => v.length > 0);
|
||||
const base = {
|
||||
display_name: formDisplayName.trim(),
|
||||
description: formDescription.trim() || undefined,
|
||||
@@ -46,13 +72,18 @@ export const ToolTypeForm = ({ mode, toolType, onSubmit, onCancel }: ToolTypeFor
|
||||
|
||||
try {
|
||||
if (mode === "create") {
|
||||
await onSubmit({ name: formName.trim(), ...base } as CreateToolTypeRequest);
|
||||
await onSubmit({
|
||||
name: formName.trim(),
|
||||
...base,
|
||||
} as CreateToolTypeRequest);
|
||||
} else {
|
||||
await onSubmit(base as UpdateToolTypeRequest);
|
||||
}
|
||||
} catch (err) {
|
||||
const axiosError = err as { response?: { data?: { detail?: string } } };
|
||||
setFormError(axiosError?.response?.data?.detail || "Failed to save tool type");
|
||||
setFormError(
|
||||
axiosError?.response?.data?.detail || "Failed to save tool type",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -69,42 +100,89 @@ export const ToolTypeForm = ({ mode, toolType, onSubmit, onCancel }: ToolTypeFor
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-group">
|
||||
<label>Name (unique identifier)</label>
|
||||
<input type="text" value={formName} onChange={(e) => setFormName(e.target.value)} disabled={mode === "edit"} placeholder="e.g., code-server" />
|
||||
<input
|
||||
type="text"
|
||||
value={formName}
|
||||
onChange={(e) => setFormName(e.target.value)}
|
||||
disabled={mode === "edit"}
|
||||
placeholder="e.g., code-server"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Display Name</label>
|
||||
<input type="text" value={formDisplayName} onChange={(e) => setFormDisplayName(e.target.value)} placeholder="e.g., VS Code Server" />
|
||||
<input
|
||||
type="text"
|
||||
value={formDisplayName}
|
||||
onChange={(e) => setFormDisplayName(e.target.value)}
|
||||
placeholder="e.g., VS Code Server"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Description</label>
|
||||
<input type="text" value={formDescription} onChange={(e) => setFormDescription(e.target.value)} placeholder="Optional description" />
|
||||
<input
|
||||
type="text"
|
||||
value={formDescription}
|
||||
onChange={(e) => setFormDescription(e.target.value)}
|
||||
placeholder="Optional description"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Category</label>
|
||||
<input type="text" value={formCategory} onChange={(e) => setFormCategory(e.target.value)} placeholder="e.g., editor, notebook, ai-assistant" />
|
||||
<input
|
||||
type="text"
|
||||
value={formCategory}
|
||||
onChange={(e) => setFormCategory(e.target.value)}
|
||||
placeholder="e.g., editor, notebook, ai-assistant"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Interfaces</label>
|
||||
<div className="checkbox-group">
|
||||
<label className="checkbox-label">
|
||||
<input type="checkbox" checked={formInterfaces.includes("web")} onChange={() => toggleInterface("web")} /> Web
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formInterfaces.includes("web")}
|
||||
onChange={() => toggleInterface("web")}
|
||||
/>{" "}
|
||||
Web
|
||||
</label>
|
||||
<label className="checkbox-label">
|
||||
<input type="checkbox" checked={formInterfaces.includes("terminal")} onChange={() => toggleInterface("terminal")} /> Terminal
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formInterfaces.includes("terminal")}
|
||||
onChange={() => toggleInterface("terminal")}
|
||||
/>{" "}
|
||||
Terminal
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Default Port *</label>
|
||||
<input type="number" value={formPort} onChange={(e) => setFormPort(e.target.value)} placeholder="e.g., 8443" required />
|
||||
<input
|
||||
type="number"
|
||||
value={formPort}
|
||||
onChange={(e) => setFormPort(e.target.value)}
|
||||
placeholder="e.g., 8443"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Compose Template (YAML)</label>
|
||||
<textarea value={formTemplate} onChange={(e) => setFormTemplate(e.target.value)} rows={10} placeholder="version: '3.8' services: app: image: ..." />
|
||||
<textarea
|
||||
value={formTemplate}
|
||||
onChange={(e) => setFormTemplate(e.target.value)}
|
||||
rows={10}
|
||||
placeholder="version: '3.8' services: app: image: ..."
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Required Variables (comma-separated)</label>
|
||||
<input type="text" value={formVariables} onChange={(e) => setFormVariables(e.target.value)} placeholder="REPO_PATH, TOOL_NAME" />
|
||||
<input
|
||||
type="text"
|
||||
value={formVariables}
|
||||
onChange={(e) => setFormVariables(e.target.value)}
|
||||
placeholder="REPO_PATH, TOOL_NAME"
|
||||
/>
|
||||
</div>
|
||||
{formError && <p className="text-error">{formError}</p>}
|
||||
<div className="dialog-actions">
|
||||
@@ -112,7 +190,11 @@ export const ToolTypeForm = ({ mode, toolType, onSubmit, onCancel }: ToolTypeFor
|
||||
<Icon name={mode === "create" ? "add" : "save"} size="sm" />
|
||||
{mode === "create" ? "Create" : "Update"}
|
||||
</button>
|
||||
<button type="button" onClick={onCancel} className="button-secondary">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="button-secondary"
|
||||
>
|
||||
<Icon name="cancel" size="sm" /> Cancel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Icon } from "../../../components/icon";
|
||||
import { Icon } from "../../ui/Icon";
|
||||
import type { ToolType } from "../../../types/tool-type";
|
||||
|
||||
interface ToolTypeListProps {
|
||||
@@ -55,34 +55,32 @@ export const ToolTypeList = ({
|
||||
<Icon name="delete" size="sm" />
|
||||
Delete
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{deleteConfirmId === toolType.id && (
|
||||
<div className="dialog-overlay">
|
||||
<div className="dialog">
|
||||
<p>
|
||||
Delete tool type "{toolType.display_name}"?
|
||||
</p>
|
||||
<div className="dialog-actions">
|
||||
<button
|
||||
onClick={() => onDelete(toolType.id)}
|
||||
className="button-danger"
|
||||
>
|
||||
<Icon name="delete" size="sm" />
|
||||
Delete
|
||||
</button>
|
||||
<button onClick={() => setDeleteConfirmId(null)}>
|
||||
<Icon name="cancel" size="sm" />
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
{deleteConfirmId === toolType.id && (
|
||||
<div className="dialog-overlay">
|
||||
<div className="dialog">
|
||||
<p>Delete tool type "{toolType.display_name}"?</p>
|
||||
<div className="dialog-actions">
|
||||
<button
|
||||
onClick={() => onDelete(toolType.id)}
|
||||
className="button-danger"
|
||||
>
|
||||
<Icon name="delete" size="sm" />
|
||||
Delete
|
||||
</button>
|
||||
<button onClick={() => setDeleteConfirmId(null)}>
|
||||
<Icon name="cancel" size="sm" />
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Icon } from "../../../components/icon";
|
||||
import { Icon } from "../../ui/Icon";
|
||||
import {
|
||||
createConfigFolder,
|
||||
deleteConfigFolder,
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
type ConfigFolder,
|
||||
type CreateConfigFolderRequest,
|
||||
type UpdateConfigFolderRequest,
|
||||
} from "../../../api/config_folders";
|
||||
} from "../../../api/config-folders";
|
||||
|
||||
export const ConfigFoldersTab = () => {
|
||||
const [folders, setFolders] = useState<ConfigFolder[]>([]);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Icon } from "../../../components/icon";
|
||||
import { Icon } from "../../ui/Icon";
|
||||
import {
|
||||
createToolConfig,
|
||||
deleteToolConfig,
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
updateToolConfig,
|
||||
type CreateToolConfigRequest,
|
||||
type ToolConfig,
|
||||
} from "../../../api/tool_configs";
|
||||
import { listToolTypes, type ToolType } from "../../../api/tool_types";
|
||||
} from "../../../api/tool-configs";
|
||||
import { listToolTypes, type ToolType } from "../../../api/tool-types";
|
||||
|
||||
export const ToolConfigsTab = () => {
|
||||
const [configs, setConfigs] = useState<ToolConfig[]>([]);
|
||||
|
||||
@@ -9,7 +9,7 @@ vi.mock("../../../api/tool_types", () => ({
|
||||
updateToolType: vi.fn(),
|
||||
}));
|
||||
|
||||
import { listToolTypes } from "../../../api/tool_types";
|
||||
import { listToolTypes } from "../../../api/tool-types";
|
||||
|
||||
describe("ToolTypesTab", () => {
|
||||
it("renders loading state initially", () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Icon } from "../../../components/icon";
|
||||
import { Icon } from "../../ui/Icon";
|
||||
import {
|
||||
createToolType,
|
||||
deleteToolType,
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
type ReadinessProbe,
|
||||
type ToolType,
|
||||
type UpdateToolTypeRequest,
|
||||
} from "../../../api/tool_types";
|
||||
} from "../../../api/tool-types";
|
||||
|
||||
export const ToolTypesTab = () => {
|
||||
const [toolTypes, setToolTypes] = useState<ToolType[]>([]);
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { Icon } from "./icon";
|
||||
import { Icon } from "../../ui/Icon";
|
||||
|
||||
interface WorkspaceHeaderProps {
|
||||
project: {
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { Link, NavLink, Outlet } from "react-router-dom";
|
||||
|
||||
import { getUserSessions } from "../api/sessions";
|
||||
import type { Session } from "../types/session";
|
||||
import { useTheme } from "../hooks/use-theme";
|
||||
import { useAuth } from "../state/auth";
|
||||
import { useSessions } from "../state/sessions";
|
||||
import { Icon } from "./icon";
|
||||
import type { IconName } from "../utils/icons";
|
||||
import { getUserSessions } from "../../api/sessions";
|
||||
import type { Session } from "../../types/session";
|
||||
import { useTheme } from "../../hooks/use-theme";
|
||||
import { useAuth } from "../../state/auth";
|
||||
import { useSessions } from "../../state/sessions";
|
||||
import { Icon } from "../ui/Icon";
|
||||
import type { IconName } from "../../utils/icons";
|
||||
import styles from "./layout/AppShell.module.css";
|
||||
|
||||
const NAV_ITEMS: { to: string; label: string; icon: IconName }[] = [
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect } from "react";
|
||||
import Editor from "react-simple-code-editor";
|
||||
import { highlightCode, loadLanguage } from "../utils/language";
|
||||
import { highlightCode, loadLanguage } from "../../utils/language";
|
||||
|
||||
interface CodeEditorProps {
|
||||
value: string;
|
||||
@@ -1,3 +1,4 @@
|
||||
export { Icon } from "./Icon";
|
||||
export { LoadingState } from "./LoadingState";
|
||||
export { ErrorState } from "./ErrorState";
|
||||
export { StatusBadge } from "./StatusBadge";
|
||||
|
||||
@@ -12,7 +12,9 @@ import type { Session } from "../types/session";
|
||||
|
||||
export function useDashboardActions(onRefresh: () => Promise<void>) {
|
||||
const navigate = useNavigate();
|
||||
const [saveState, setSaveState] = useState<"idle" | "saving" | "error">("idle");
|
||||
const [saveState, setSaveState] = useState<"idle" | "saving" | "error">(
|
||||
"idle",
|
||||
);
|
||||
const [actionBusy, setActionBusy] = useState<string | null>(null);
|
||||
|
||||
const handleCreate = useCallback(
|
||||
@@ -60,7 +62,11 @@ export function useDashboardActions(onRefresh: () => Promise<void>) {
|
||||
async (session: Session) => {
|
||||
setActionBusy(session.id);
|
||||
try {
|
||||
await stopInstance(session.project_id, session.repository_id, session.id);
|
||||
await stopInstance(
|
||||
session.project_id,
|
||||
session.repository_id,
|
||||
session.id,
|
||||
);
|
||||
await onRefresh();
|
||||
} finally {
|
||||
setActionBusy(null);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { HomePage } from "./dashboard";
|
||||
import { HomePage } from "./DashboardPage";
|
||||
|
||||
const mockDashboard = vi.fn();
|
||||
const mockSessions = vi.fn();
|
||||
@@ -0,0 +1,193 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { getDashboardSummary, type DashboardSummary } from "../api/dashboard";
|
||||
import { getUserSessions } from "../api/sessions";
|
||||
import { listProjects } from "../api/projects";
|
||||
import { listToolTypes } from "../api/tool-types";
|
||||
import type { Session as SessionApi } from "../types/session";
|
||||
import type { Project } from "../types/project";
|
||||
import type { ToolType } from "../types/tool-type";
|
||||
import {
|
||||
DashboardSummary as DashboardSummaryComponent,
|
||||
ActiveSessionsList,
|
||||
ProjectsSection,
|
||||
QuickCreateForm,
|
||||
RecentSessionsSection,
|
||||
} from "../components/features/dashboard";
|
||||
import { LoadingState, ErrorState } from "../components/ui";
|
||||
import { useDashboardActions } from "../hooks/use-dashboard-actions";
|
||||
import { listRepositories } from "../api/git-repositories";
|
||||
import type { GitRepository } from "../types/git-repository";
|
||||
|
||||
type HomeStatus = "loading" | "ready" | "error";
|
||||
type SessionView = SessionApi;
|
||||
|
||||
export const HomePage = () => {
|
||||
const navigate = useNavigate();
|
||||
const [status, setStatus] = useState<HomeStatus>("loading");
|
||||
const [summary, setSummary] = useState<DashboardSummary | null>(null);
|
||||
const [sessions, setSessions] = useState<SessionView[]>([]);
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [repositories, setRepositories] = useState<GitRepository[]>([]);
|
||||
const [toolTypes, setToolTypes] = useState<ToolType[]>([]);
|
||||
const safeSessions = Array.isArray(sessions) ? sessions : [];
|
||||
|
||||
const loadHome = useCallback(async () => {
|
||||
setStatus("loading");
|
||||
try {
|
||||
const [dashboard, sessionData, projectData, toolTypeData] =
|
||||
await Promise.all([
|
||||
getDashboardSummary(),
|
||||
getUserSessions(),
|
||||
listProjects(),
|
||||
listToolTypes(),
|
||||
]);
|
||||
setSummary(dashboard);
|
||||
setSessions(sessionData as SessionView[]);
|
||||
setProjects(projectData);
|
||||
setToolTypes(toolTypeData);
|
||||
setStatus("ready");
|
||||
} catch {
|
||||
setStatus("error");
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void loadHome();
|
||||
}, [loadHome]);
|
||||
|
||||
const loadRepos = useCallback(async (projectId: string) => {
|
||||
try {
|
||||
setRepositories(await listRepositories(projectId));
|
||||
} catch {
|
||||
setRepositories([]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const activeSessions = useMemo(
|
||||
() =>
|
||||
safeSessions.filter((s) =>
|
||||
["running", "building", "pending"].includes(s.status),
|
||||
),
|
||||
[safeSessions],
|
||||
);
|
||||
const recentSessions = useMemo(
|
||||
() =>
|
||||
safeSessions
|
||||
.filter((s) => ["stopped", "error"].includes(s.status))
|
||||
.slice(0, 5),
|
||||
[safeSessions],
|
||||
);
|
||||
|
||||
const actions = useDashboardActions(loadHome);
|
||||
|
||||
return (
|
||||
<section className="stack home-page">
|
||||
<header className="home-hero card">
|
||||
<div className="stack-sm">
|
||||
<p className="eyebrow">Workspace overview</p>
|
||||
<h1>Home</h1>
|
||||
<p className="muted">
|
||||
Open sessions, available projects, and the fastest path back into
|
||||
work.
|
||||
</p>
|
||||
</div>
|
||||
<div className="home-hero-actions">
|
||||
<button
|
||||
className="primary-button"
|
||||
type="button"
|
||||
onClick={() => navigate("/projects")}
|
||||
>
|
||||
New Project
|
||||
</button>
|
||||
<button
|
||||
className="secondary-button"
|
||||
type="button"
|
||||
onClick={() => navigate("/settings")}
|
||||
>
|
||||
Settings
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{status === "loading" && <LoadingState message="Loading overview..." />}
|
||||
{status === "error" && (
|
||||
<ErrorState
|
||||
message="Unable to load your workspace overview."
|
||||
onRetry={() => void loadHome()}
|
||||
/>
|
||||
)}
|
||||
|
||||
{status === "ready" && summary && (
|
||||
<>
|
||||
<DashboardSummaryComponent
|
||||
summary={summary}
|
||||
activeSessionsCount={activeSessions.length}
|
||||
/>
|
||||
|
||||
<section className="card stack home-section">
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<p className="eyebrow">Open sessions</p>
|
||||
<h2>{activeSessions.length}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<ActiveSessionsList
|
||||
sessions={activeSessions}
|
||||
actionBusy={actions.actionBusy}
|
||||
onOpen={actions.handleOpen}
|
||||
onStop={actions.handleStop}
|
||||
onDelete={actions.handleDelete}
|
||||
onRecreateTunnel={actions.handleRecreateTunnel}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="card stack home-section">
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<p className="eyebrow">Available projects</p>
|
||||
<h2>{projects.length}</h2>
|
||||
</div>
|
||||
<button
|
||||
className="secondary-button"
|
||||
type="button"
|
||||
onClick={() => navigate("/projects")}
|
||||
>
|
||||
View all
|
||||
</button>
|
||||
</div>
|
||||
<ProjectsSection
|
||||
projects={projects}
|
||||
onOpenProject={(id) => navigate(`/projects/${id}`)}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="card stack home-section">
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<p className="eyebrow">Quick create</p>
|
||||
<h2>Start a session</h2>
|
||||
</div>
|
||||
</div>
|
||||
<QuickCreateForm
|
||||
projects={projects}
|
||||
repositories={repositories}
|
||||
toolTypes={toolTypes}
|
||||
saveState={actions.saveState}
|
||||
onSubmit={actions.handleCreate}
|
||||
onProjectChange={loadRepos}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<RecentSessionsSection
|
||||
sessions={recentSessions}
|
||||
onOpen={actions.handleOpen}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export { HomePage as DashboardPage };
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
import { getCommitDetail, getRepositoryHistory, type CommitDetail, type CommitHistoryEntry } from "../api/git_repositories";
|
||||
import { Icon } from "../components/icon";
|
||||
import { getCommitDetail, getRepositoryHistory, type CommitDetail, type CommitHistoryEntry } from "../api/git-repositories";
|
||||
import { Icon } from "../components/ui/Icon";
|
||||
|
||||
export const GitHistoryPage = () => {
|
||||
const { projectId, repoId } = useParams<{ projectId: string; repoId: string }>();
|
||||
+3
-3
@@ -2,9 +2,9 @@ import { useCallback, useEffect, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
import type { GitRepository } from "../types/git-repository";
|
||||
import { deleteRepository, listRepositories } from "../api/git_repositories";
|
||||
import { Icon } from "../components/icon";
|
||||
import { RepositoryCreateDialog } from "../components/repository-create-dialog";
|
||||
import { deleteRepository, listRepositories } from "../api/git-repositories";
|
||||
import { Icon } from "../components/ui/Icon";
|
||||
import { RepositoryCreateDialog } from "../components/features/project/RepositoryCreateDialog";
|
||||
|
||||
type RepoStatus = "loading" | "ready" | "error";
|
||||
|
||||
@@ -18,7 +18,7 @@ export const NotFoundPage = () => {
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000";
|
||||
|
||||
import { Icon } from "../components/icon";
|
||||
import { Icon } from "../components/ui/Icon";
|
||||
|
||||
export const LoginRedirectPage = () => {
|
||||
const nextPath = new URLSearchParams(window.location.search).get("next") ?? "/";
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { getProfile, updateProfile, uploadAvatar } from "../api/profile";
|
||||
import { Icon } from "../components/icon";
|
||||
import { Icon } from "../components/ui/Icon";
|
||||
import { useAuth } from "../state/auth";
|
||||
import type { UserProfile } from "../api/profile";
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useParams, Link, useNavigate, Routes, Route } from "react-router-dom";
|
||||
import { SettingsTabLayout } from "../components/settings-tab-layout";
|
||||
import { RepositoriesSettingsTab } from "../components/repositories-settings-tab";
|
||||
import { SettingsTabLayout } from "../components/features/settings/SettingsTabLayout";
|
||||
import { RepositoriesSettingsTab } from "../components/features/project/RepositoriesSettingsTab";
|
||||
import { apiClient } from "../api/client";
|
||||
import { Project } from "../types";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { ProjectsPage } from "./projects";
|
||||
import { ProjectsPage } from "./ProjectsPage";
|
||||
import * as projectsApi from "../api/projects";
|
||||
|
||||
const mockProjects = [
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
listProjects,
|
||||
type ProjectCreateInput,
|
||||
} from "../api/projects";
|
||||
import { Icon } from "../components/icon";
|
||||
import { Icon } from "../components/ui/Icon";
|
||||
import type { Project } from "../types";
|
||||
|
||||
type ProjectsStatus = "loading" | "ready" | "error";
|
||||
@@ -8,13 +8,13 @@ import {
|
||||
getRepositoryStatus,
|
||||
listRepositories,
|
||||
type GitStatus,
|
||||
} from "../api/git_repositories";
|
||||
} from "../api/git-repositories";
|
||||
import { WorkspaceSidebar } from "../components/features/git";
|
||||
import { FileEditor } from "../components/file-editor";
|
||||
import { WorkspaceHeader } from "../components/workspace-header";
|
||||
import { GitToolbar } from "../components/git-toolbar";
|
||||
import { FileEditor } from "../components/features/git/FileEditor";
|
||||
import { WorkspaceHeader } from "../components/features/workspace/WorkspaceHeader";
|
||||
import { GitToolbar } from "../components/features/git/GitToolbar";
|
||||
import { LoadingState, ErrorState } from "../components/ui";
|
||||
import { listToolTypes } from "../api/tool_types";
|
||||
import { listToolTypes } from "../api/tool-types";
|
||||
|
||||
type WorkspaceStatus = "loading" | "ready" | "error" | "empty";
|
||||
|
||||
@@ -133,10 +133,7 @@ export const RepoWorkspace = () => {
|
||||
return (
|
||||
<section className="repo-workspace">
|
||||
{project && (
|
||||
<WorkspaceHeader
|
||||
project={project}
|
||||
currentRepo={selectedRepo || null}
|
||||
/>
|
||||
<WorkspaceHeader project={project} currentRepo={selectedRepo || null} />
|
||||
)}
|
||||
|
||||
{status === "loading" && (
|
||||
@@ -180,9 +177,7 @@ export const RepoWorkspace = () => {
|
||||
onRefresh={() => {
|
||||
void loadBranches();
|
||||
void loadGitStatus();
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("refresh-file-tree"),
|
||||
);
|
||||
window.dispatchEvent(new CustomEvent("refresh-file-tree"));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -197,18 +192,13 @@ export const RepoWorkspace = () => {
|
||||
onRepoChange={handleRepoChange}
|
||||
onCommit={() => {
|
||||
void loadGitStatus();
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("refresh-file-tree"),
|
||||
);
|
||||
window.dispatchEvent(new CustomEvent("refresh-file-tree"));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<main className="workspace-main">
|
||||
{selectedRepoId && (
|
||||
<FileEditor
|
||||
projectId={projectId!}
|
||||
repoId={selectedRepoId}
|
||||
/>
|
||||
<FileEditor projectId={projectId!} repoId={selectedRepoId} />
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
@@ -3,9 +3,9 @@ import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { listProjects } from "../api/projects";
|
||||
import { getUserSessions } from "../api/sessions";
|
||||
import { listToolTypes } from "../api/tool_types";
|
||||
import { listToolTypes } from "../api/tool-types";
|
||||
import { getUserConfig } from "../api/settings";
|
||||
import { Icon } from "../components/icon";
|
||||
import { Icon } from "../components/ui/Icon";
|
||||
import { LoadingState, ErrorState } from "../components/ui";
|
||||
import { CreateSessionForm, SessionList } from "../components/features/session";
|
||||
import type { Project } from "../types/project";
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from "react";
|
||||
import { Link, Outlet, useLocation, useOutletContext } from "react-router-dom";
|
||||
|
||||
import { getUserConfig, updateUserConfig, type UserConfig, type UserConfigUpdate } from "../api/settings";
|
||||
import { Icon } from "../components/icon";
|
||||
import { Icon } from "../components/ui/Icon";
|
||||
|
||||
type SettingsStatus = "loading" | "ready" | "error";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { createSSHKey, deleteSSHKey, listSSHKeys, type SSHKey } from "../api/ssh_keys";
|
||||
import { Icon } from "../components/icon";
|
||||
import { createSSHKey, deleteSSHKey, listSSHKeys, type SSHKey } from "../api/ssh-keys";
|
||||
import { Icon } from "../components/ui/Icon";
|
||||
|
||||
export const SSHKeysPage = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { TerminalComponent } from "../components/terminal";
|
||||
import { Icon } from "../components/icon";
|
||||
import { TerminalComponent } from "../components/features/terminal/TerminalComponent";
|
||||
import { Icon } from "../components/ui/Icon";
|
||||
|
||||
export const TerminalPage: React.FC = () => {
|
||||
const { instanceId } = useParams<{ instanceId: string }>();
|
||||
@@ -1,17 +1,20 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Icon } from "../components/icon";
|
||||
import { Icon } from "../components/ui/Icon";
|
||||
import type { ToolType } from "../types/tool-type";
|
||||
import type { ToolConfig } from "../types/tool-config";
|
||||
import { listToolTypes } from "../api/tool_types";
|
||||
import { listToolTypes } from "../api/tool-types";
|
||||
import {
|
||||
createToolConfig,
|
||||
deleteToolConfig,
|
||||
listToolConfigs,
|
||||
updateToolConfig,
|
||||
} from "../api/tool_configs";
|
||||
import { ToolConfigForm, ToolConfigList } from "../components/features/tool-configs";
|
||||
} from "../api/tool-configs";
|
||||
import {
|
||||
ToolConfigForm,
|
||||
ToolConfigList,
|
||||
} from "../components/features/tool-configs";
|
||||
|
||||
type ConfigStatus = "loading" | "ready" | "error";
|
||||
|
||||
@@ -86,12 +89,16 @@ export const ToolConfigsPage = () => {
|
||||
};
|
||||
|
||||
const selectedTool = toolTypes.find((t) => t.id === selectedToolType);
|
||||
const filteredConfigs = configs.filter((c) => c.tool_type_id === selectedToolType);
|
||||
const filteredConfigs = configs.filter(
|
||||
(c) => c.tool_type_id === selectedToolType,
|
||||
);
|
||||
|
||||
if (status === "loading") {
|
||||
return (
|
||||
<section className="stack">
|
||||
<div className="page-header"><h1>Tool Configurations</h1></div>
|
||||
<div className="page-header">
|
||||
<h1>Tool Configurations</h1>
|
||||
</div>
|
||||
<p className="muted">Loading...</p>
|
||||
</section>
|
||||
);
|
||||
@@ -100,10 +107,16 @@ export const ToolConfigsPage = () => {
|
||||
if (status === "error") {
|
||||
return (
|
||||
<section className="stack">
|
||||
<div className="page-header"><h1>Tool Configurations</h1></div>
|
||||
<div className="page-header">
|
||||
<h1>Tool Configurations</h1>
|
||||
</div>
|
||||
<div className="card stack">
|
||||
<p>Failed to load configurations</p>
|
||||
<button className="secondary-button" onClick={() => void loadData()} type="button">
|
||||
<button
|
||||
className="secondary-button"
|
||||
onClick={() => void loadData()}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="refresh" size="sm" /> Retry
|
||||
</button>
|
||||
</div>
|
||||
@@ -118,7 +131,11 @@ export const ToolConfigsPage = () => {
|
||||
<p className="eyebrow">Settings</p>
|
||||
<h1>Tool Configurations</h1>
|
||||
</div>
|
||||
<button className="secondary-button" type="button" onClick={() => navigate("/settings")}>
|
||||
<button
|
||||
className="secondary-button"
|
||||
type="button"
|
||||
onClick={() => navigate("/settings")}
|
||||
>
|
||||
Back to settings
|
||||
</button>
|
||||
</div>
|
||||
@@ -136,18 +153,24 @@ export const ToolConfigsPage = () => {
|
||||
className="form-input"
|
||||
>
|
||||
{toolTypes.map((tool) => (
|
||||
<option key={tool.id} value={tool.id}>{tool.display_name}</option>
|
||||
<option key={tool.id} value={tool.id}>
|
||||
{tool.display_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{selectedTool && (
|
||||
<p className="muted" style={{ marginTop: "0.5rem" }}>
|
||||
Category: {selectedTool.category} · Interfaces: {selectedTool.interfaces?.join(", ")}
|
||||
Category: {selectedTool.category} · Interfaces:{" "}
|
||||
{selectedTool.interfaces?.join(", ")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="card stack">
|
||||
<div className="row" style={{ justifyContent: "space-between", alignItems: "center" }}>
|
||||
<div
|
||||
className="row"
|
||||
style={{ justifyContent: "space-between", alignItems: "center" }}
|
||||
>
|
||||
<h2>Configuration Variables</h2>
|
||||
<button
|
||||
className="primary-button small"
|
||||
@@ -160,7 +183,11 @@ export const ToolConfigsPage = () => {
|
||||
<Icon name="add" size="sm" /> Add Config
|
||||
</button>
|
||||
</div>
|
||||
<ToolConfigList configs={filteredConfigs} onEdit={handleEdit} onDelete={handleDelete} />
|
||||
<ToolConfigList
|
||||
configs={filteredConfigs}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showForm && (
|
||||
@@ -1,14 +1,18 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import type { ToolType, CreateToolTypeRequest, UpdateToolTypeRequest } from "../types/tool-type";
|
||||
import type {
|
||||
ToolType,
|
||||
CreateToolTypeRequest,
|
||||
UpdateToolTypeRequest,
|
||||
} from "../types/tool-type";
|
||||
import {
|
||||
createToolType,
|
||||
deleteToolType,
|
||||
listToolTypes,
|
||||
updateToolType,
|
||||
} from "../api/tool_types";
|
||||
import { Icon } from "../components/icon";
|
||||
} from "../api/tool-types";
|
||||
import { Icon } from "../components/ui/Icon";
|
||||
import { ToolTypeForm, ToolTypeList } from "../components/features/tool-types";
|
||||
|
||||
type ToolTypesStatus = "loading" | "ready" | "error";
|
||||
@@ -17,7 +21,9 @@ export const ToolTypesPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const [status, setStatus] = useState<ToolTypesStatus>("loading");
|
||||
const [toolTypes, setToolTypes] = useState<ToolType[]>([]);
|
||||
const [dialogMode, setDialogMode] = useState<"none" | "create" | "edit">("none");
|
||||
const [dialogMode, setDialogMode] = useState<"none" | "create" | "edit">(
|
||||
"none",
|
||||
);
|
||||
const [editingToolType, setEditingToolType] = useState<ToolType | null>(null);
|
||||
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null);
|
||||
|
||||
@@ -52,7 +58,9 @@ export const ToolTypesPage = () => {
|
||||
setEditingToolType(null);
|
||||
};
|
||||
|
||||
const handleSubmit = async (input: CreateToolTypeRequest | UpdateToolTypeRequest) => {
|
||||
const handleSubmit = async (
|
||||
input: CreateToolTypeRequest | UpdateToolTypeRequest,
|
||||
) => {
|
||||
if (dialogMode === "create") {
|
||||
await createToolType(input as CreateToolTypeRequest);
|
||||
} else if (editingToolType) {
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { ToolWorkshopPage } from "./tool-workshop";
|
||||
import * as toolTypesApi from "../api/tool_types";
|
||||
import * as toolConfigsApi from "../api/tool_configs";
|
||||
import * as configFoldersApi from "../api/config_folders";
|
||||
import { ToolWorkshopPage } from "./ToolWorkshopPage";
|
||||
import * as toolTypesApi from "../api/tool-types";
|
||||
import * as toolConfigsApi from "../api/tool-configs";
|
||||
import * as configFoldersApi from "../api/config-folders";
|
||||
|
||||
const mockToolTypes = [
|
||||
{
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { Icon } from "../components/icon";
|
||||
import { Icon } from "../components/ui";
|
||||
import {
|
||||
ToolTypesTab,
|
||||
ToolConfigsTab,
|
||||
@@ -1,110 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { getDashboardSummary, type DashboardSummary } from "../api/dashboard";
|
||||
import { getUserSessions } from "../api/sessions";
|
||||
import { listProjects } from "../api/projects";
|
||||
import { listToolTypes } from "../api/tool_types";
|
||||
import type { Session as SessionApi } from "../types/session";
|
||||
import type { Project } from "../types/project";
|
||||
import type { ToolType } from "../types/tool-type";
|
||||
import {
|
||||
DashboardSummary as DashboardSummaryComponent,
|
||||
ActiveSessionsList,
|
||||
ProjectsSection,
|
||||
QuickCreateForm,
|
||||
RecentSessionsSection,
|
||||
} from "../components/features/dashboard";
|
||||
import { LoadingState, ErrorState } from "../components/ui";
|
||||
import { useDashboardActions } from "../hooks/use-dashboard-actions";
|
||||
import { listRepositories } from "../api/git_repositories";
|
||||
import type { GitRepository } from "../types/git-repository";
|
||||
|
||||
type HomeStatus = "loading" | "ready" | "error";
|
||||
type SessionView = SessionApi;
|
||||
|
||||
export const HomePage = () => {
|
||||
const navigate = useNavigate();
|
||||
const [status, setStatus] = useState<HomeStatus>("loading");
|
||||
const [summary, setSummary] = useState<DashboardSummary | null>(null);
|
||||
const [sessions, setSessions] = useState<SessionView[]>([]);
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [repositories, setRepositories] = useState<GitRepository[]>([]);
|
||||
const [toolTypes, setToolTypes] = useState<ToolType[]>([]);
|
||||
const safeSessions = Array.isArray(sessions) ? sessions : [];
|
||||
|
||||
const loadHome = useCallback(async () => {
|
||||
setStatus("loading");
|
||||
try {
|
||||
const [dashboard, sessionData, projectData, toolTypeData] = await Promise.all([
|
||||
getDashboardSummary(),
|
||||
getUserSessions(),
|
||||
listProjects(),
|
||||
listToolTypes(),
|
||||
]);
|
||||
setSummary(dashboard);
|
||||
setSessions(sessionData as SessionView[]);
|
||||
setProjects(projectData);
|
||||
setToolTypes(toolTypeData);
|
||||
setStatus("ready");
|
||||
} catch {
|
||||
setStatus("error");
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => { void loadHome(); }, [loadHome]);
|
||||
|
||||
const loadRepos = useCallback(async (projectId: string) => {
|
||||
try { setRepositories(await listRepositories(projectId)); }
|
||||
catch { setRepositories([]); }
|
||||
}, []);
|
||||
|
||||
const activeSessions = useMemo(() => safeSessions.filter((s) => ["running", "building", "pending"].includes(s.status)), [safeSessions]);
|
||||
const recentSessions = useMemo(() => safeSessions.filter((s) => ["stopped", "error"].includes(s.status)).slice(0, 5), [safeSessions]);
|
||||
|
||||
const actions = useDashboardActions(loadHome);
|
||||
|
||||
return (
|
||||
<section className="stack home-page">
|
||||
<header className="home-hero card">
|
||||
<div className="stack-sm">
|
||||
<p className="eyebrow">Workspace overview</p>
|
||||
<h1>Home</h1>
|
||||
<p className="muted">Open sessions, available projects, and the fastest path back into work.</p>
|
||||
</div>
|
||||
<div className="home-hero-actions">
|
||||
<button className="primary-button" type="button" onClick={() => navigate("/projects")}>New Project</button>
|
||||
<button className="secondary-button" type="button" onClick={() => navigate("/settings")}>Settings</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{status === "loading" && <LoadingState message="Loading overview..." />}
|
||||
{status === "error" && <ErrorState message="Unable to load your workspace overview." onRetry={() => void loadHome()} />}
|
||||
|
||||
{status === "ready" && summary && (
|
||||
<>
|
||||
<DashboardSummaryComponent summary={summary} activeSessionsCount={activeSessions.length} />
|
||||
|
||||
<section className="card stack home-section">
|
||||
<div className="page-header"><div><p className="eyebrow">Open sessions</p><h2>{activeSessions.length}</h2></div></div>
|
||||
<ActiveSessionsList sessions={activeSessions} actionBusy={actions.actionBusy} onOpen={actions.handleOpen} onStop={actions.handleStop} onDelete={actions.handleDelete} onRecreateTunnel={actions.handleRecreateTunnel} />
|
||||
</section>
|
||||
|
||||
<section className="card stack home-section">
|
||||
<div className="page-header"><div><p className="eyebrow">Available projects</p><h2>{projects.length}</h2></div><button className="secondary-button" type="button" onClick={() => navigate("/projects")}>View all</button></div>
|
||||
<ProjectsSection projects={projects} onOpenProject={(id) => navigate(`/projects/${id}`)} />
|
||||
</section>
|
||||
|
||||
<section className="card stack home-section">
|
||||
<div className="page-header"><div><p className="eyebrow">Quick create</p><h2>Start a session</h2></div></div>
|
||||
<QuickCreateForm projects={projects} repositories={repositories} toolTypes={toolTypes} saveState={actions.saveState} onSubmit={actions.handleCreate} onProjectChange={loadRepos} />
|
||||
</section>
|
||||
|
||||
<RecentSessionsSection sessions={recentSessions} onOpen={actions.handleOpen} />
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export { HomePage as DashboardPage };
|
||||
+18
-16
@@ -1,21 +1,22 @@
|
||||
import { Navigate, Route, Routes } from "react-router-dom";
|
||||
|
||||
import { AppShell } from "./components/app-shell";
|
||||
import { ProtectedRoute } from "./components/protected-route";
|
||||
import { HomePage } from "./pages/dashboard";
|
||||
import { LoginRedirectPage, NotFoundPage } from "./pages/placeholder";
|
||||
import { ProfilePage } from "./pages/profile";
|
||||
import { ProjectsPage } from "./pages/projects";
|
||||
import { GitRepositoriesPage } from "./pages/git-repositories";
|
||||
import { GitHistoryPage } from "./pages/git-history";
|
||||
import { ProjectSettingsPage } from "./pages/project-settings";
|
||||
import { RepoWorkspace } from "./pages/repo-workspace";
|
||||
import { SettingsPage, GeneralSettingsTab } from "./pages/settings";
|
||||
import { TerminalPage } from "./pages/terminal";
|
||||
import { ToolWorkshopPage } from "./pages/tool-workshop";
|
||||
import { SSHKeysPage } from "./pages/ssh-keys";
|
||||
import { ToolConfigsPage } from "./pages/tool-configs";
|
||||
import { ToolTypesPage } from "./pages/tool-types";
|
||||
import { AppShell } from "./components/layout/AppShell";
|
||||
import { ProtectedRoute } from "./components/ProtectedRoute";
|
||||
import { HomePage } from "./pages/DashboardPage";
|
||||
import { LoginRedirectPage, NotFoundPage } from "./pages/PlaceholderPage";
|
||||
import { ProfilePage } from "./pages/ProfilePage";
|
||||
import { ProjectsPage } from "./pages/ProjectsPage";
|
||||
import { GitRepositoriesPage } from "./pages/GitRepositoriesPage";
|
||||
import { GitHistoryPage } from "./pages/GitHistoryPage";
|
||||
import { ProjectSettingsPage } from "./pages/ProjectSettingsPage";
|
||||
import { RepoWorkspace } from "./pages/RepoWorkspacePage";
|
||||
import { SettingsPage, GeneralSettingsTab } from "./pages/SettingsPage";
|
||||
import { TerminalPage } from "./pages/TerminalPage";
|
||||
import { ToolWorkshopPage } from "./pages/ToolWorkshopPage";
|
||||
import { SSHKeysPage } from "./pages/SshKeysPage";
|
||||
import { ToolConfigsPage } from "./pages/ToolConfigsPage";
|
||||
import { ToolTypesPage } from "./pages/ToolTypesPage";
|
||||
import { SessionsPage } from "./pages/SessionsPage";
|
||||
|
||||
export const AppRouter = () => {
|
||||
return (
|
||||
@@ -50,6 +51,7 @@ export const AppRouter = () => {
|
||||
</Route>
|
||||
<Route path="tool-workshop" element={<ToolWorkshopPage />} />
|
||||
<Route path="instances/:instanceId/terminal" element={<TerminalPage />} />
|
||||
<Route path="sessions" element={<SessionsPage />} />
|
||||
</Route>
|
||||
<Route path="/404" element={<NotFoundPage />} />
|
||||
<Route path="*" element={<Navigate to="/404" replace />} />
|
||||
|
||||
Reference in New Issue
Block a user