refactor: reorganize CSS and Tool Workshop page

- Extract styles.css into styles/ directory (tokens, global, utilities, syntax-highlight, pages)
- Extract ToolWorkshopPage into components:
  - ToolTypeListSidebar, ToolTypeEditorPanel, ToolWorkshopMobileView
  - use-tool-workshop hook for state management
- Slim ToolWorkshopPage from 1,269 to 110 lines

Quality gates: tsc --noEmit passes, npm run build passes
This commit is contained in:
Developer
2026-06-05 18:58:14 +00:00
parent 7070867393
commit 61072f4c07
16 changed files with 7079 additions and 1245 deletions
+370
View File
@@ -0,0 +1,370 @@
import { useCallback, useEffect, useState } from "react";
import { extractErrorMessage } from "../utils/errors";
import {
createToolType,
deleteToolType,
listToolTypes,
updateToolType,
type CreateToolTypeRequest,
type ReadinessProbe,
type ToolType,
type UpdateToolTypeRequest,
} from "../api/tool-types";
import {
createToolDefinition,
getToolDefinition,
listToolDefinitions,
updateToolDefinition,
} from "../api/tool-definitions";
import type { ToolTypeFormState } from "../components/features/tool-workshop/ToolTypeEditorPanel";
type Status = "loading" | "ready" | "error";
const defaultForm: ToolTypeFormState = {
name: "",
display_name: "",
description: "",
category: "",
interface_type: "web",
requires_port: true,
default_port: "",
definition_type: "compose",
compose_template: "",
dockerfile_template: "",
readiness_command: "",
readiness_timeout: "30",
readiness_interval: "2",
required_variables: "",
startup_command: "",
};
export const useToolWorkshop = () => {
const [status, setStatus] = useState<Status>("loading");
const [toolTypes, setToolTypes] = useState<ToolType[]>([]);
const [baseDefinitions, setBaseDefinitions] = useState<
Awaited<ReturnType<typeof listToolDefinitions>>
>([]);
const [manifestData, setManifestData] = useState<Record<
string,
unknown
> | null>(null);
const [manifestDefinitionId, setManifestDefinitionId] = useState<
string | null
>(null);
const [selectedToolTypeId, setSelectedToolTypeId] = useState<string | null>(
null,
);
const [isCreating, setIsCreating] = useState(false);
const [toolTypeForm, setToolTypeForm] = useState<ToolTypeFormState>(defaultForm);
const [toolTypeError, setToolTypeError] = useState<string | null>(null);
const [toolTypeDirty, setToolTypeDirty] = useState(false);
const selectedToolType =
(toolTypes || []).find((t) => t.id === selectedToolTypeId) || null;
const loadData = useCallback(async () => {
setStatus("loading");
try {
const [types, defs] = await Promise.all([
listToolTypes(),
listToolDefinitions(),
]);
setToolTypes(types || []);
setBaseDefinitions((defs || []).filter((d) => d.is_base));
setStatus("ready");
} catch {
setStatus("error");
}
}, []);
useEffect(() => {
void loadData();
}, [loadData]);
const resetToolTypeForm = () => {
setToolTypeForm(defaultForm);
setToolTypeError(null);
setToolTypeDirty(false);
setManifestData(null);
setManifestDefinitionId(null);
};
const populateToolTypeForm = async (toolType: ToolType) => {
setToolTypeForm({
name: toolType.name,
display_name: toolType.display_name,
description: toolType.description || "",
category: toolType.category || "",
interface_type: (toolType.interface_type as "web" | "terminal") || "web",
requires_port: toolType.requires_port ?? true,
default_port: toolType.default_port?.toString() || "",
definition_type:
(toolType.definition_type as "compose" | "dockerfile" | "manifest") ||
"compose",
compose_template: toolType.compose_template || "",
dockerfile_template: toolType.dockerfile_template || "",
readiness_command: toolType.readiness_probe?.command || "",
readiness_timeout: toolType.readiness_probe?.timeout?.toString() || "30",
readiness_interval: toolType.readiness_probe?.interval?.toString() || "2",
required_variables: toolType.required_variables?.join(", ") || "",
startup_command: toolType.startup_command || "",
});
setToolTypeError(null);
setToolTypeDirty(false);
setManifestDefinitionId(toolType.manifest_id || null);
if (toolType.definition_type === "manifest" && toolType.manifest_id) {
try {
const defn = await getToolDefinition(toolType.manifest_id);
setManifestData(defn.manifest);
} catch {
setManifestData(null);
}
} else {
setManifestData(null);
}
};
const handleSelectToolType = (toolType: ToolType | null) => {
if (toolTypeDirty) {
if (!window.confirm("You have unsaved changes. Discard them?")) {
return;
}
}
if (toolType) {
setSelectedToolTypeId(toolType.id);
setIsCreating(false);
void populateToolTypeForm(toolType);
} else {
setSelectedToolTypeId(null);
}
};
const handleCreateNew = () => {
if (toolTypeDirty) {
if (!window.confirm("You have unsaved changes. Discard them?")) {
return;
}
}
setSelectedToolTypeId(null);
setIsCreating(true);
resetToolTypeForm();
};
const handleToolTypeSubmit = async (e?: React.FormEvent) => {
e?.preventDefault();
setToolTypeError(null);
if (!toolTypeForm.name.trim() || !toolTypeForm.display_name.trim()) {
setToolTypeError("Name and display name are required");
return;
}
if (
toolTypeForm.requires_port &&
(!toolTypeForm.default_port.trim() ||
isNaN(Number(toolTypeForm.default_port)))
) {
setToolTypeError("Default port is required and must be a number");
return;
}
if (toolTypeForm.definition_type !== "manifest") {
const template =
toolTypeForm.definition_type === "compose"
? toolTypeForm.compose_template
: toolTypeForm.dockerfile_template;
if (!template.trim()) {
setToolTypeError(
`${toolTypeForm.definition_type === "compose" ? "Compose" : "Dockerfile"} template is required`,
);
return;
}
} else if (!manifestData) {
setToolTypeError(
"Manifest data is required for manifest definition type",
);
return;
}
const variables = toolTypeForm.required_variables
.split(",")
.map((v) => v.trim())
.filter((v) => v.length > 0);
const readinessProbe: ReadinessProbe | undefined =
toolTypeForm.readiness_command.trim()
? {
command: toolTypeForm.readiness_command.trim(),
timeout: parseInt(toolTypeForm.readiness_timeout) || 30,
interval: parseInt(toolTypeForm.readiness_interval) || 2,
}
: undefined;
const template =
toolTypeForm.definition_type === "compose"
? toolTypeForm.compose_template
: toolTypeForm.dockerfile_template;
try {
if (isCreating) {
let manifestId: string | undefined;
if (toolTypeForm.definition_type === "manifest" && manifestData) {
const manifestPayload = {
name: toolTypeForm.name.trim(),
display_name: toolTypeForm.display_name.trim(),
description: toolTypeForm.description.trim() || undefined,
category: toolTypeForm.category.trim() || undefined,
interface_type: toolTypeForm.interface_type,
base_image: (manifestData.base_image as string) || undefined,
base_definition_id:
(manifestData.base_definition_id as string) || undefined,
manifest: manifestData,
};
const newManifest = await createToolDefinition(manifestPayload);
manifestId = newManifest.id;
}
const input: CreateToolTypeRequest = {
name: toolTypeForm.name.trim(),
display_name: toolTypeForm.display_name.trim(),
description: toolTypeForm.description.trim() || undefined,
category: toolTypeForm.category.trim() || undefined,
interface_type: toolTypeForm.interface_type,
requires_port: toolTypeForm.requires_port,
default_port: toolTypeForm.requires_port
? Number(toolTypeForm.default_port)
: 0,
definition_type: toolTypeForm.definition_type,
manifest_id: manifestId,
compose_template:
toolTypeForm.definition_type === "compose" ? template : undefined,
dockerfile_template:
toolTypeForm.definition_type === "dockerfile"
? template
: undefined,
readiness_probe: readinessProbe,
required_variables: variables,
startup_command: toolTypeForm.startup_command.trim() || undefined,
};
const newTool = await createToolType(input);
setIsCreating(false);
setSelectedToolTypeId(newTool.id);
setToolTypeDirty(false);
} else if (selectedToolType) {
let manifestId = selectedToolType.manifest_id || undefined;
if (toolTypeForm.definition_type === "manifest" && manifestData) {
if (manifestId) {
await updateToolDefinition(manifestId, {
display_name: toolTypeForm.display_name.trim(),
description: toolTypeForm.description.trim() || undefined,
category: toolTypeForm.category.trim() || undefined,
manifest: manifestData,
});
} else {
const manifestPayload = {
name: toolTypeForm.name.trim(),
display_name: toolTypeForm.display_name.trim(),
description: toolTypeForm.description.trim() || undefined,
category: toolTypeForm.category.trim() || undefined,
interface_type: toolTypeForm.interface_type,
base_image: (manifestData.base_image as string) || undefined,
base_definition_id:
(manifestData.base_definition_id as string) || undefined,
manifest: manifestData,
};
const newManifest = await createToolDefinition(manifestPayload);
manifestId = newManifest.id;
}
}
const input: UpdateToolTypeRequest = {
display_name: toolTypeForm.display_name.trim(),
description: toolTypeForm.description.trim() || undefined,
category: toolTypeForm.category.trim() || undefined,
interface_type: toolTypeForm.interface_type,
requires_port: toolTypeForm.requires_port,
default_port: toolTypeForm.requires_port
? Number(toolTypeForm.default_port)
: 0,
definition_type: toolTypeForm.definition_type,
manifest_id:
toolTypeForm.definition_type === "manifest"
? manifestId
: undefined,
compose_template:
toolTypeForm.definition_type === "compose" ? template : undefined,
dockerfile_template:
toolTypeForm.definition_type === "dockerfile"
? template
: undefined,
readiness_probe: readinessProbe,
required_variables: variables,
startup_command: toolTypeForm.startup_command.trim() || undefined,
};
await updateToolType(selectedToolType.id, input);
setToolTypeDirty(false);
}
await loadData();
} catch (err) {
setToolTypeError(extractErrorMessage(err));
}
};
const handleDeleteToolType = async (id: string) => {
if (
!window.confirm(
"Delete this tool type? All associated configs will be removed.",
)
)
return;
try {
await deleteToolType(id);
if (selectedToolTypeId === id) {
setSelectedToolTypeId(null);
setIsCreating(false);
resetToolTypeForm();
}
await loadData();
} catch {
alert("Failed to delete tool type");
}
};
const handleFormChange = (changes: Partial<ToolTypeFormState>) => {
setToolTypeForm((prev) => ({ ...prev, ...changes }));
setToolTypeDirty(true);
};
const handleReset = () => {
if (isCreating) {
resetToolTypeForm();
} else if (selectedToolType) {
void populateToolTypeForm(selectedToolType);
}
};
return {
status,
toolTypes,
baseDefinitions,
selectedToolType,
selectedToolTypeId,
isCreating,
toolTypeForm,
manifestData,
manifestDefinitionId,
toolTypeError,
toolTypeDirty,
loadData,
handleSelectToolType,
handleCreateNew,
handleToolTypeSubmit,
handleDeleteToolType,
handleFormChange,
handleReset,
setManifestData,
setToolTypeDirty,
};
};