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,373 @@
|
||||
import { Icon } from "../../icon";
|
||||
import { ManifestEditor } from "../tool/manifest-editor";
|
||||
import type { ToolType } from "../../../api/tool-types";
|
||||
import type { ToolDefinitionManifest } from "../../../api/tool-definitions";
|
||||
|
||||
export interface ToolTypeFormState {
|
||||
name: string;
|
||||
display_name: string;
|
||||
description: string;
|
||||
category: string;
|
||||
interface_type: "web" | "terminal";
|
||||
requires_port: boolean;
|
||||
default_port: string;
|
||||
definition_type: "compose" | "dockerfile" | "manifest";
|
||||
compose_template: string;
|
||||
dockerfile_template: string;
|
||||
readiness_command: string;
|
||||
readiness_timeout: string;
|
||||
readiness_interval: string;
|
||||
required_variables: string;
|
||||
startup_command: string;
|
||||
}
|
||||
|
||||
interface ToolTypeEditorPanelProps {
|
||||
isCreating: boolean;
|
||||
selectedToolType: ToolType | null;
|
||||
form: ToolTypeFormState;
|
||||
manifestData: Record<string, unknown> | null;
|
||||
manifestDefinitionId: string | null;
|
||||
baseDefinitions: ToolDefinitionManifest[];
|
||||
toolTypeError: string | null;
|
||||
toolTypeDirty: boolean;
|
||||
onFormChange: (changes: Partial<ToolTypeFormState>) => void;
|
||||
onManifestChange: (manifest: Record<string, unknown> | null) => void;
|
||||
onSubmit: (e: React.FormEvent) => void;
|
||||
onReset: () => void;
|
||||
}
|
||||
|
||||
export const ToolTypeEditorPanel = ({
|
||||
isCreating,
|
||||
selectedToolType,
|
||||
form,
|
||||
manifestData,
|
||||
manifestDefinitionId,
|
||||
baseDefinitions,
|
||||
toolTypeError,
|
||||
toolTypeDirty,
|
||||
onFormChange,
|
||||
onManifestChange,
|
||||
onSubmit,
|
||||
onReset,
|
||||
}: ToolTypeEditorPanelProps) => {
|
||||
const hasSelection = isCreating || selectedToolType;
|
||||
|
||||
return (
|
||||
<div style={{ flex: 1, overflow: "auto", padding: "1.5rem", minWidth: 0 }}>
|
||||
{!hasSelection ? (
|
||||
<div
|
||||
style={{
|
||||
textAlign: "center",
|
||||
paddingTop: "4rem",
|
||||
color: "var(--muted)",
|
||||
}}
|
||||
>
|
||||
<div style={{ opacity: 0.3, marginBottom: "1rem" }}>
|
||||
<Icon name="code" size="lg" />
|
||||
</div>
|
||||
<h3 style={{ margin: "0 0 0.5rem 0", fontWeight: 500 }}>
|
||||
Select a tool type
|
||||
</h3>
|
||||
<p style={{ margin: 0 }}>
|
||||
Choose a tool from the list to edit, or create a new one.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div style={{ marginBottom: "1.5rem" }}>
|
||||
<h1 style={{ margin: "0 0 0.5rem 0", fontSize: "1.5rem" }}>
|
||||
{isCreating
|
||||
? "Create Tool Type"
|
||||
: selectedToolType?.display_name}
|
||||
</h1>
|
||||
{!isCreating && (
|
||||
<p className="muted" style={{ margin: 0 }}>
|
||||
{selectedToolType?.name} · {selectedToolType?.definition_type}{" "}
|
||||
·{" "}
|
||||
{selectedToolType?.interface_type === "web"
|
||||
? `Port ${selectedToolType?.default_port}`
|
||||
: "Terminal"}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<form
|
||||
onSubmit={onSubmit}
|
||||
className="stack"
|
||||
style={{ gap: "1rem", maxWidth: "800px" }}
|
||||
>
|
||||
<div className="form-group">
|
||||
<label htmlFor="definition-type">Definition Type</label>
|
||||
<select
|
||||
id="definition-type"
|
||||
value={form.definition_type}
|
||||
onChange={(e) => {
|
||||
onFormChange({
|
||||
definition_type: e.target.value as
|
||||
| "compose"
|
||||
| "dockerfile"
|
||||
| "manifest",
|
||||
});
|
||||
}}
|
||||
className="form-input"
|
||||
disabled={!isCreating}
|
||||
>
|
||||
<option value="compose">Docker Compose</option>
|
||||
<option value="dockerfile">Dockerfile</option>
|
||||
<option value="manifest">Manifest (Declarative)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="row" style={{ gap: "1rem" }}>
|
||||
<div className="form-group" style={{ flex: 1 }}>
|
||||
<label htmlFor="tool-type-name">Name *</label>
|
||||
<input
|
||||
id="tool-type-name"
|
||||
type="text"
|
||||
value={form.name}
|
||||
onChange={(e) => {
|
||||
onFormChange({ name: e.target.value });
|
||||
}}
|
||||
disabled={!isCreating}
|
||||
placeholder="e.g., code-server"
|
||||
className="form-input"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group" style={{ flex: 1 }}>
|
||||
<label htmlFor="tool-type-display-name">
|
||||
Display Name *
|
||||
</label>
|
||||
<input
|
||||
id="tool-type-display-name"
|
||||
type="text"
|
||||
value={form.display_name}
|
||||
onChange={(e) => {
|
||||
onFormChange({ display_name: e.target.value });
|
||||
}}
|
||||
placeholder="e.g., VS Code Server"
|
||||
className="form-input"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="tool-type-description">Description</label>
|
||||
<input
|
||||
id="tool-type-description"
|
||||
type="text"
|
||||
value={form.description}
|
||||
onChange={(e) => {
|
||||
onFormChange({ description: e.target.value });
|
||||
}}
|
||||
placeholder="Optional description"
|
||||
className="form-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="row" style={{ gap: "1rem" }}>
|
||||
<div className="form-group" style={{ flex: 1 }}>
|
||||
<label htmlFor="tool-type-category">Category</label>
|
||||
<input
|
||||
id="tool-type-category"
|
||||
type="text"
|
||||
value={form.category}
|
||||
onChange={(e) => {
|
||||
onFormChange({ category: e.target.value });
|
||||
}}
|
||||
placeholder="e.g., editor, notebook, ai-assistant"
|
||||
className="form-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group" style={{ flex: 1 }}>
|
||||
<label htmlFor="tool-type-interface">Interface Type</label>
|
||||
<select
|
||||
id="tool-type-interface"
|
||||
value={form.interface_type}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value as "web" | "terminal";
|
||||
onFormChange({
|
||||
interface_type: value,
|
||||
requires_port: value === "web",
|
||||
default_port:
|
||||
value === "web" ? form.default_port : "",
|
||||
});
|
||||
}}
|
||||
className="form-input"
|
||||
>
|
||||
<option value="web">Web</option>
|
||||
<option value="terminal">Terminal</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{form.interface_type === "terminal" && (
|
||||
<div className="form-group">
|
||||
<label htmlFor="tool-type-startup-command">
|
||||
Startup Command
|
||||
</label>
|
||||
<input
|
||||
id="tool-type-startup-command"
|
||||
type="text"
|
||||
value={form.startup_command}
|
||||
onChange={(e) => {
|
||||
onFormChange({ startup_command: e.target.value });
|
||||
}}
|
||||
placeholder="e.g., cd /workspace && ls"
|
||||
className="form-input"
|
||||
/>
|
||||
<small className="form-help">
|
||||
Command to run before the interactive shell for each new
|
||||
terminal session.
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{form.requires_port && (
|
||||
<div className="form-group">
|
||||
<label htmlFor="tool-type-default-port">
|
||||
Default Port *
|
||||
</label>
|
||||
<input
|
||||
id="tool-type-default-port"
|
||||
type="number"
|
||||
value={form.default_port}
|
||||
onChange={(e) => {
|
||||
onFormChange({ default_port: e.target.value });
|
||||
}}
|
||||
placeholder="e.g., 8443"
|
||||
className="form-input"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{form.definition_type === "manifest" ? (
|
||||
<ManifestEditor
|
||||
manifest={manifestData}
|
||||
baseDefinitions={baseDefinitions}
|
||||
onChange={(m) => {
|
||||
onManifestChange(m);
|
||||
}}
|
||||
definitionId={manifestDefinitionId}
|
||||
/>
|
||||
) : (
|
||||
<div className="form-group">
|
||||
<label htmlFor="tool-type-template">
|
||||
{form.definition_type === "compose"
|
||||
? "Compose Template"
|
||||
: "Dockerfile"}{" "}
|
||||
*
|
||||
</label>
|
||||
<textarea
|
||||
id="tool-type-template"
|
||||
value={
|
||||
form.definition_type === "compose"
|
||||
? form.compose_template
|
||||
: form.dockerfile_template
|
||||
}
|
||||
onChange={(e) => {
|
||||
if (form.definition_type === "compose") {
|
||||
onFormChange({ compose_template: e.target.value });
|
||||
} else {
|
||||
onFormChange({ dockerfile_template: e.target.value });
|
||||
}
|
||||
}}
|
||||
rows={12}
|
||||
placeholder={
|
||||
form.definition_type === "compose"
|
||||
? "version: '3.8'\nservices:\n app:\n image: ..."
|
||||
: "FROM node:18\nWORKDIR /app\n..."
|
||||
}
|
||||
className="form-input"
|
||||
style={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: "0.875rem",
|
||||
}}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="readiness-command">
|
||||
Readiness Probe Command
|
||||
</label>
|
||||
<input
|
||||
id="readiness-command"
|
||||
type="text"
|
||||
value={form.readiness_command}
|
||||
onChange={(e) => {
|
||||
onFormChange({ readiness_command: e.target.value });
|
||||
}}
|
||||
placeholder="e.g., curl -f http://localhost:8080"
|
||||
className="form-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="row" style={{ gap: "1rem" }}>
|
||||
<div className="form-group" style={{ flex: 1 }}>
|
||||
<label htmlFor="readiness-timeout">Timeout (seconds)</label>
|
||||
<input
|
||||
id="readiness-timeout"
|
||||
type="number"
|
||||
value={form.readiness_timeout}
|
||||
onChange={(e) => {
|
||||
onFormChange({ readiness_timeout: e.target.value });
|
||||
}}
|
||||
className="form-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group" style={{ flex: 1 }}>
|
||||
<label htmlFor="readiness-interval">
|
||||
Interval (seconds)
|
||||
</label>
|
||||
<input
|
||||
id="readiness-interval"
|
||||
type="number"
|
||||
value={form.readiness_interval}
|
||||
onChange={(e) => {
|
||||
onFormChange({ readiness_interval: e.target.value });
|
||||
}}
|
||||
className="form-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Required Variables (comma-separated)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.required_variables}
|
||||
onChange={(e) => {
|
||||
onFormChange({ required_variables: e.target.value });
|
||||
}}
|
||||
placeholder="REPO_PATH, TOOL_NAME"
|
||||
className="form-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{toolTypeError && <p className="text-error">{toolTypeError}</p>}
|
||||
|
||||
<div className="dialog-actions" style={{ marginTop: "1rem" }}>
|
||||
<button type="submit">
|
||||
<Icon name={isCreating ? "add" : "save"} size="sm" />
|
||||
{isCreating ? "Create Tool Type" : "Save Changes"}
|
||||
</button>
|
||||
{toolTypeDirty && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onReset}
|
||||
className="button-secondary"
|
||||
>
|
||||
<Icon name="cancel" size="sm" /> Discard
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,161 @@
|
||||
import { Icon } from "../../icon";
|
||||
import type { ToolType } from "../../../api/tool-types";
|
||||
|
||||
interface ToolTypeListSidebarProps {
|
||||
toolTypes: ToolType[];
|
||||
selectedToolTypeId: string | null;
|
||||
onSelect: (toolType: ToolType) => void;
|
||||
onCreate: () => void;
|
||||
onDelete: (id: string) => void;
|
||||
}
|
||||
|
||||
export const ToolTypeListSidebar = ({
|
||||
toolTypes,
|
||||
selectedToolTypeId,
|
||||
onSelect,
|
||||
onCreate,
|
||||
onDelete,
|
||||
}: ToolTypeListSidebarProps) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: "280px",
|
||||
minWidth: "280px",
|
||||
borderRight: "1px solid var(--border)",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
background: "var(--panel)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ padding: "1rem", borderBottom: "1px solid var(--border)" }}
|
||||
>
|
||||
<h2 style={{ margin: 0, fontSize: "1.125rem" }}>Tool Workshop</h2>
|
||||
<p
|
||||
className="muted"
|
||||
style={{ margin: "0.25rem 0 0 0", fontSize: "0.875rem" }}
|
||||
>
|
||||
{toolTypes.length} tool type{toolTypes.length !== 1 ? "s" : ""}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, overflowY: "auto", padding: "0.5rem" }}>
|
||||
{toolTypes.map((toolType) => (
|
||||
<button
|
||||
key={toolType.id}
|
||||
onClick={() => onSelect(toolType)}
|
||||
style={{
|
||||
width: "100%",
|
||||
textAlign: "left",
|
||||
padding: "0.75rem 1rem",
|
||||
marginBottom: "0.25rem",
|
||||
borderRadius: "0.375rem",
|
||||
border: "none",
|
||||
background:
|
||||
selectedToolTypeId === toolType.id
|
||||
? "var(--brand)"
|
||||
: "transparent",
|
||||
color:
|
||||
selectedToolTypeId === toolType.id ? "white" : "var(--ink)",
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.75rem",
|
||||
transition: "background 0.15s",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (selectedToolTypeId !== toolType.id) {
|
||||
e.currentTarget.style.background = "#ece7df";
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (selectedToolTypeId !== toolType.id) {
|
||||
e.currentTarget.style.background = "transparent";
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9375rem",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
{toolType.display_name}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.8125rem",
|
||||
opacity: 0.8,
|
||||
marginTop: "0.125rem",
|
||||
}}
|
||||
>
|
||||
{toolType.category || "Uncategorized"} ·{" "}
|
||||
{toolType.interface_type === "web"
|
||||
? `Port ${toolType.default_port}`
|
||||
: "Terminal"}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete(toolType.id);
|
||||
}}
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
color:
|
||||
selectedToolTypeId === toolType.id
|
||||
? "rgba(255,255,255,0.8)"
|
||||
: "var(--muted)",
|
||||
cursor: "pointer",
|
||||
padding: "0.25rem",
|
||||
borderRadius: "0.25rem",
|
||||
flexShrink: 0,
|
||||
opacity: 0,
|
||||
}}
|
||||
className="delete-btn"
|
||||
title="Delete tool type"
|
||||
>
|
||||
<Icon name="delete" size="sm" />
|
||||
</button>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ padding: "1rem", borderTop: "1px solid var(--border)" }}>
|
||||
<button
|
||||
onClick={onCreate}
|
||||
style={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: "0.5rem",
|
||||
padding: "0.75rem",
|
||||
borderRadius: "0.5rem",
|
||||
border: "2px dashed var(--border)",
|
||||
background: "transparent",
|
||||
color: "var(--muted)",
|
||||
cursor: "pointer",
|
||||
fontWeight: 600,
|
||||
transition: "all 0.15s",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = "var(--brand)";
|
||||
e.currentTarget.style.color = "var(--brand)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = "var(--border)";
|
||||
e.currentTarget.style.color = "var(--muted)";
|
||||
}}
|
||||
>
|
||||
<Icon name="add" size="sm" /> New Tool Type
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,350 @@
|
||||
import { MobileListView } from "../mobile/mobile-list-view";
|
||||
import { MobileDetailView } from "../mobile/mobile-detail-view";
|
||||
import { MobileEditView } from "../mobile/mobile-edit-view";
|
||||
import { MobileFAB } from "../mobile/mobile-fab";
|
||||
import type { ToolType } from "../../../api/tool-types";
|
||||
import type { ToolTypeFormState } from "./ToolTypeEditorPanel";
|
||||
|
||||
export type MobileView = "list" | "detail" | "edit";
|
||||
|
||||
interface ToolWorkshopMobileViewProps {
|
||||
toolTypes: ToolType[];
|
||||
selectedToolType: ToolType | null;
|
||||
mobileView: MobileView;
|
||||
isCreating: boolean;
|
||||
toolTypeForm: ToolTypeFormState;
|
||||
toolTypeError: string | null;
|
||||
onViewChange: (view: MobileView) => void;
|
||||
onSelect: (toolType: ToolType) => void;
|
||||
onCreate: () => void;
|
||||
onDelete: (id: string) => void;
|
||||
onFormChange: (changes: Partial<ToolTypeFormState>) => void;
|
||||
onSubmit: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const ToolWorkshopMobileView = ({
|
||||
toolTypes,
|
||||
selectedToolType,
|
||||
mobileView,
|
||||
isCreating,
|
||||
toolTypeForm,
|
||||
toolTypeError,
|
||||
onViewChange,
|
||||
onSelect,
|
||||
onCreate,
|
||||
onDelete,
|
||||
onFormChange,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: ToolWorkshopMobileViewProps) => {
|
||||
if (mobileView === "list") {
|
||||
return (
|
||||
<div className="mobile-page">
|
||||
<div className="mobile-page-header">
|
||||
<h1>Tool Workshop</h1>
|
||||
<span className="muted">{toolTypes.length} tool types</span>
|
||||
</div>
|
||||
<MobileListView
|
||||
items={toolTypes.map((t) => ({
|
||||
id: t.id,
|
||||
title: t.display_name,
|
||||
subtitle: `${t.category || "Uncategorized"} · ${t.interface_type === "web" ? `Port ${t.default_port}` : "Terminal"}`,
|
||||
}))}
|
||||
onItemClick={(id) => {
|
||||
const toolType = toolTypes.find((t) => t.id === id);
|
||||
if (toolType) {
|
||||
onSelect(toolType);
|
||||
onViewChange("detail");
|
||||
}
|
||||
}}
|
||||
emptyMessage="No tool types yet"
|
||||
/>
|
||||
<MobileFAB
|
||||
onClick={() => {
|
||||
onCreate();
|
||||
onViewChange("edit");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (mobileView === "detail" && selectedToolType) {
|
||||
return (
|
||||
<MobileDetailView
|
||||
title={selectedToolType.display_name}
|
||||
subtitle={`${selectedToolType.name} · ${selectedToolType.definition_type} · ${selectedToolType.interface_type === "web" ? `Port ${selectedToolType.default_port}` : "Terminal"}`}
|
||||
fields={[
|
||||
{ label: "Name", value: selectedToolType.name },
|
||||
{ label: "Display Name", value: selectedToolType.display_name },
|
||||
{ label: "Description", value: selectedToolType.description },
|
||||
{ label: "Category", value: selectedToolType.category },
|
||||
{ label: "Interface Type", value: selectedToolType.interface_type },
|
||||
{
|
||||
label: "Requires Port",
|
||||
value: selectedToolType.requires_port,
|
||||
type: "boolean",
|
||||
},
|
||||
{ label: "Default Port", value: selectedToolType.default_port },
|
||||
{
|
||||
label: "Definition Type",
|
||||
value: selectedToolType.definition_type,
|
||||
},
|
||||
{
|
||||
label: "Startup Command",
|
||||
value: selectedToolType.startup_command,
|
||||
},
|
||||
{
|
||||
label: "Readiness Command",
|
||||
value: selectedToolType.readiness_probe?.command ?? null,
|
||||
},
|
||||
{
|
||||
label: "Readiness Timeout",
|
||||
value: selectedToolType.readiness_probe?.timeout ?? null,
|
||||
},
|
||||
{
|
||||
label: "Readiness Interval",
|
||||
value: selectedToolType.readiness_probe?.interval ?? null,
|
||||
},
|
||||
{
|
||||
label: "Required Variables",
|
||||
value: selectedToolType.required_variables?.join(", ") ?? null,
|
||||
},
|
||||
{
|
||||
label: "Compose Template",
|
||||
value: selectedToolType.compose_template,
|
||||
type: "code",
|
||||
},
|
||||
{
|
||||
label: "Dockerfile Template",
|
||||
value: selectedToolType.dockerfile_template,
|
||||
type: "code",
|
||||
},
|
||||
]}
|
||||
onEdit={() => {
|
||||
onViewChange("edit");
|
||||
}}
|
||||
onDelete={() => {
|
||||
void onDelete(selectedToolType.id);
|
||||
onViewChange("list");
|
||||
}}
|
||||
onBack={() => {
|
||||
onViewChange("list");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (mobileView === "edit") {
|
||||
return (
|
||||
<MobileEditView
|
||||
title={isCreating ? "Create Tool Type" : "Edit Tool Type"}
|
||||
onCancel={onCancel}
|
||||
onSave={() => {
|
||||
onSubmit();
|
||||
if (!toolTypeError) {
|
||||
onViewChange("list");
|
||||
}
|
||||
}}
|
||||
isSaving={false}
|
||||
>
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={toolTypeForm.name}
|
||||
onChange={(e) => onFormChange({ name: e.target.value })}
|
||||
className="mobile-form-input"
|
||||
placeholder="e.g., my-tool"
|
||||
/>
|
||||
</div>
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Display Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={toolTypeForm.display_name}
|
||||
onChange={(e) => onFormChange({ display_name: e.target.value })}
|
||||
className="mobile-form-input"
|
||||
placeholder="e.g., My Tool"
|
||||
/>
|
||||
</div>
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Description</label>
|
||||
<textarea
|
||||
value={toolTypeForm.description}
|
||||
onChange={(e) => onFormChange({ description: e.target.value })}
|
||||
className="mobile-form-textarea"
|
||||
placeholder="What does this tool do?"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Category</label>
|
||||
<input
|
||||
type="text"
|
||||
value={toolTypeForm.category}
|
||||
onChange={(e) => onFormChange({ category: e.target.value })}
|
||||
className="mobile-form-input"
|
||||
placeholder="e.g., development"
|
||||
/>
|
||||
</div>
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Interface Type</label>
|
||||
<select
|
||||
value={toolTypeForm.interface_type}
|
||||
onChange={(e) =>
|
||||
onFormChange({
|
||||
interface_type: e.target.value as "web" | "terminal",
|
||||
})
|
||||
}
|
||||
className="mobile-form-select"
|
||||
>
|
||||
<option value="web">Web</option>
|
||||
<option value="terminal">Terminal</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Requires Port</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={toolTypeForm.requires_port}
|
||||
onChange={(e) => onFormChange({ requires_port: e.target.checked })}
|
||||
className="mobile-form-checkbox"
|
||||
/>
|
||||
</div>
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Default Port</label>
|
||||
<input
|
||||
type="text"
|
||||
value={toolTypeForm.default_port}
|
||||
onChange={(e) => onFormChange({ default_port: e.target.value })}
|
||||
className="mobile-form-input"
|
||||
placeholder="e.g., 8080"
|
||||
/>
|
||||
</div>
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Definition Type</label>
|
||||
<select
|
||||
value={toolTypeForm.definition_type}
|
||||
onChange={(e) =>
|
||||
onFormChange({
|
||||
definition_type: e.target.value as "compose" | "dockerfile",
|
||||
})
|
||||
}
|
||||
className="mobile-form-select"
|
||||
>
|
||||
<option value="compose">Compose</option>
|
||||
<option value="dockerfile">Dockerfile</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Startup Command</label>
|
||||
<input
|
||||
type="text"
|
||||
value={toolTypeForm.startup_command}
|
||||
onChange={(e) => onFormChange({ startup_command: e.target.value })}
|
||||
className="mobile-form-input"
|
||||
placeholder="Command to run on startup"
|
||||
/>
|
||||
</div>
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Readiness Command</label>
|
||||
<input
|
||||
type="text"
|
||||
value={toolTypeForm.readiness_command}
|
||||
onChange={(e) => onFormChange({ readiness_command: e.target.value })}
|
||||
className="mobile-form-input"
|
||||
placeholder="e.g., curl -f http://localhost:8080/health"
|
||||
/>
|
||||
</div>
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Readiness Timeout</label>
|
||||
<input
|
||||
type="text"
|
||||
value={toolTypeForm.readiness_timeout}
|
||||
onChange={(e) => onFormChange({ readiness_timeout: e.target.value })}
|
||||
className="mobile-form-input"
|
||||
placeholder="30"
|
||||
/>
|
||||
</div>
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Readiness Interval</label>
|
||||
<input
|
||||
type="text"
|
||||
value={toolTypeForm.readiness_interval}
|
||||
onChange={(e) => onFormChange({ readiness_interval: e.target.value })}
|
||||
className="mobile-form-input"
|
||||
placeholder="2"
|
||||
/>
|
||||
</div>
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Required Variables</label>
|
||||
<input
|
||||
type="text"
|
||||
value={toolTypeForm.required_variables}
|
||||
onChange={(e) => onFormChange({ required_variables: e.target.value })}
|
||||
className="mobile-form-input"
|
||||
placeholder="VAR1, VAR2, VAR3"
|
||||
/>
|
||||
</div>
|
||||
{toolTypeForm.definition_type === "compose" && (
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Compose Template</label>
|
||||
<textarea
|
||||
value={toolTypeForm.compose_template}
|
||||
onChange={(e) => onFormChange({ compose_template: e.target.value })}
|
||||
className="mobile-form-textarea mobile-form-code"
|
||||
placeholder="version: '3'"
|
||||
rows={10}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{toolTypeForm.definition_type === "dockerfile" && (
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Dockerfile Template</label>
|
||||
<textarea
|
||||
value={toolTypeForm.dockerfile_template}
|
||||
onChange={(e) =>
|
||||
onFormChange({ dockerfile_template: e.target.value })
|
||||
}
|
||||
className="mobile-form-textarea mobile-form-code"
|
||||
placeholder="FROM ubuntu:22.04"
|
||||
rows={10}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</MobileEditView>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mobile-page">
|
||||
<div className="mobile-page-header">
|
||||
<h1>Tool Workshop</h1>
|
||||
<span className="muted">{toolTypes.length} tool types</span>
|
||||
</div>
|
||||
<MobileListView
|
||||
items={toolTypes.map((t) => ({
|
||||
id: t.id,
|
||||
title: t.display_name,
|
||||
subtitle: `${t.category || "Uncategorized"} · ${t.interface_type === "web" ? `Port ${t.default_port}` : "Terminal"}`,
|
||||
}))}
|
||||
onItemClick={(id) => {
|
||||
const toolType = toolTypes.find((t) => t.id === id);
|
||||
if (toolType) {
|
||||
onSelect(toolType);
|
||||
onViewChange("detail");
|
||||
}
|
||||
}}
|
||||
emptyMessage="No tool types yet"
|
||||
/>
|
||||
<MobileFAB
|
||||
onClick={() => {
|
||||
onCreate();
|
||||
onViewChange("edit");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user