feat: enforce single tool type with port config
- Replace interfaces array with interface_type string and requires_port boolean - Add database migration for schema change - Update backend model, API schemas, and validation - Update frontend types and tool workshop UI - Add dropdown for interface type selection - Conditionally show/hide port fields based on requires_port - Update tests and mock data - All frontend tests pass (37/37) - Frontend typecheck and lint pass
This commit is contained in:
@@ -48,7 +48,8 @@ export const ToolWorkshopPage = () => {
|
||||
display_name: "",
|
||||
description: "",
|
||||
category: "",
|
||||
interfaces: [] as string[],
|
||||
interface_type: "web" as "web" | "terminal",
|
||||
requires_port: true,
|
||||
default_port: "",
|
||||
definition_type: "compose" as "compose" | "dockerfile",
|
||||
compose_template: "",
|
||||
@@ -120,7 +121,8 @@ export const ToolWorkshopPage = () => {
|
||||
display_name: "",
|
||||
description: "",
|
||||
category: "",
|
||||
interfaces: [],
|
||||
interface_type: "web",
|
||||
requires_port: true,
|
||||
default_port: "",
|
||||
definition_type: "compose",
|
||||
compose_template: "",
|
||||
@@ -140,7 +142,8 @@ export const ToolWorkshopPage = () => {
|
||||
display_name: toolType.display_name,
|
||||
description: toolType.description || "",
|
||||
category: toolType.category || "",
|
||||
interfaces: toolType.interfaces || [],
|
||||
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 || "compose",
|
||||
compose_template: toolType.compose_template || "",
|
||||
@@ -205,7 +208,7 @@ export const ToolWorkshopPage = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!toolTypeForm.default_port.trim() || isNaN(Number(toolTypeForm.default_port))) {
|
||||
if (toolTypeForm.requires_port && (!toolTypeForm.default_port.trim() || isNaN(Number(toolTypeForm.default_port)))) {
|
||||
setToolTypeError("Default port is required and must be a number");
|
||||
return;
|
||||
}
|
||||
@@ -239,8 +242,9 @@ export const ToolWorkshopPage = () => {
|
||||
display_name: toolTypeForm.display_name.trim(),
|
||||
description: toolTypeForm.description.trim() || undefined,
|
||||
category: toolTypeForm.category.trim() || undefined,
|
||||
interfaces: toolTypeForm.interfaces.length > 0 ? toolTypeForm.interfaces : undefined,
|
||||
default_port: Number(toolTypeForm.default_port),
|
||||
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,
|
||||
compose_template: toolTypeForm.definition_type === "compose" ? template : undefined,
|
||||
dockerfile_template: toolTypeForm.definition_type === "dockerfile" ? template : undefined,
|
||||
@@ -256,8 +260,9 @@ export const ToolWorkshopPage = () => {
|
||||
display_name: toolTypeForm.display_name.trim(),
|
||||
description: toolTypeForm.description.trim() || undefined,
|
||||
category: toolTypeForm.category.trim() || undefined,
|
||||
interfaces: toolTypeForm.interfaces.length > 0 ? toolTypeForm.interfaces : undefined,
|
||||
default_port: Number(toolTypeForm.default_port),
|
||||
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,
|
||||
compose_template: toolTypeForm.definition_type === "compose" ? template : undefined,
|
||||
dockerfile_template: toolTypeForm.definition_type === "dockerfile" ? template : undefined,
|
||||
@@ -553,7 +558,7 @@ export const ToolWorkshopPage = () => {
|
||||
{toolType.display_name}
|
||||
</div>
|
||||
<div style={{ fontSize: "0.8125rem", opacity: 0.8, marginTop: "0.125rem" }}>
|
||||
{toolType.category || "Uncategorized"} · Port {toolType.default_port || "N/A"}
|
||||
{toolType.category || "Uncategorized"} · {toolType.interface_type === "web" ? `Port ${toolType.default_port}` : "Terminal"}
|
||||
</div>
|
||||
</div>
|
||||
{toolType.is_builtin && (
|
||||
@@ -649,7 +654,7 @@ export const ToolWorkshopPage = () => {
|
||||
</h1>
|
||||
{!isCreating && (
|
||||
<p className="muted" style={{ margin: 0 }}>
|
||||
{selectedToolType?.name} · {selectedToolType?.definition_type} · Port {selectedToolType?.default_port}
|
||||
{selectedToolType?.name} · {selectedToolType?.definition_type} · {selectedToolType?.interface_type === "web" ? `Port ${selectedToolType?.default_port}` : "Terminal"}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -763,6 +768,30 @@ export const ToolWorkshopPage = () => {
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group" style={{ flex: 1 }}>
|
||||
<label htmlFor="tool-type-interface">Interface Type</label>
|
||||
<select
|
||||
id="tool-type-interface"
|
||||
value={toolTypeForm.interface_type}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value as "web" | "terminal";
|
||||
setToolTypeForm({
|
||||
...toolTypeForm,
|
||||
interface_type: value,
|
||||
requires_port: value === "web",
|
||||
default_port: value === "web" ? toolTypeForm.default_port : "",
|
||||
});
|
||||
setToolTypeDirty(true);
|
||||
}}
|
||||
className="form-input"
|
||||
>
|
||||
<option value="web">Web</option>
|
||||
<option value="terminal">Terminal</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{toolTypeForm.requires_port && (
|
||||
<div className="form-group">
|
||||
<label htmlFor="tool-type-default-port">Default Port *</label>
|
||||
<input
|
||||
id="tool-type-default-port"
|
||||
@@ -777,30 +806,7 @@ export const ToolWorkshopPage = () => {
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Interfaces</label>
|
||||
<div className="checkbox-group">
|
||||
{["web", "terminal"].map((iface) => (
|
||||
<label key={iface} className="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={toolTypeForm.interfaces.includes(iface)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setToolTypeForm({ ...toolTypeForm, interfaces: [...toolTypeForm.interfaces, iface] });
|
||||
} else {
|
||||
setToolTypeForm({ ...toolTypeForm, interfaces: toolTypeForm.interfaces.filter((i) => i !== iface) });
|
||||
}
|
||||
setToolTypeDirty(true);
|
||||
}}
|
||||
/>
|
||||
{iface}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="tool-type-template">{toolTypeForm.definition_type === "compose" ? "Compose Template" : "Dockerfile"} *</label>
|
||||
|
||||
Reference in New Issue
Block a user