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:
@@ -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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user