Merge branch 'dev' of ssh://git.commumedia.org:2222/alex/headquarter into dev
This commit is contained in:
@@ -12,7 +12,8 @@ export interface ToolType {
|
||||
display_name: string;
|
||||
description: string | null;
|
||||
category: string;
|
||||
interfaces: string[];
|
||||
interface_type: string;
|
||||
requires_port: boolean;
|
||||
default_port: number | null;
|
||||
definition_type: 'compose' | 'dockerfile';
|
||||
compose_template: string | null;
|
||||
@@ -31,7 +32,8 @@ export interface CreateToolTypeRequest {
|
||||
display_name: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
interfaces?: string[];
|
||||
interface_type?: string;
|
||||
requires_port?: boolean;
|
||||
default_port: number;
|
||||
definition_type?: 'compose' | 'dockerfile';
|
||||
compose_template?: string;
|
||||
@@ -45,7 +47,8 @@ export interface UpdateToolTypeRequest {
|
||||
display_name?: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
interfaces?: string[];
|
||||
interface_type?: string;
|
||||
requires_port?: boolean;
|
||||
default_port?: number;
|
||||
definition_type?: 'compose' | 'dockerfile';
|
||||
compose_template?: string;
|
||||
|
||||
@@ -18,6 +18,7 @@ interface GitToolbarProps {
|
||||
currentBranch: string;
|
||||
branches: string[];
|
||||
hasRemote: boolean;
|
||||
isMirror: boolean;
|
||||
onBranchChange: (branch: string) => void;
|
||||
onRefresh: () => void;
|
||||
}
|
||||
@@ -28,6 +29,7 @@ export const GitToolbar = ({
|
||||
currentBranch,
|
||||
branches,
|
||||
hasRemote,
|
||||
isMirror,
|
||||
onBranchChange,
|
||||
onRefresh,
|
||||
}: GitToolbarProps) => {
|
||||
@@ -136,7 +138,13 @@ export const GitToolbar = ({
|
||||
return (
|
||||
<div className="git-toolbar">
|
||||
{error && <div className="toolbar-error">{error}</div>}
|
||||
|
||||
{isMirror && (
|
||||
<div className="warning-message">
|
||||
<Icon name="warning" size="sm" /> This repository is a bare mirror.
|
||||
Editing, committing, pulling, and merging are not available.
|
||||
Delete and recreate it to enable full workspace features.
|
||||
</div>
|
||||
)}
|
||||
<div className="toolbar-row">
|
||||
<div className="toolbar-group">
|
||||
<select
|
||||
|
||||
@@ -197,6 +197,7 @@ export const RepoWorkspace = () => {
|
||||
currentBranch={currentBranch}
|
||||
branches={branches}
|
||||
hasRemote={Boolean(selectedRepo?.remote_url)}
|
||||
isMirror={Boolean(selectedRepo?.is_mirror)}
|
||||
onBranchChange={(branch) => {
|
||||
setCurrentBranch(branch);
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
|
||||
@@ -170,7 +170,7 @@ export const ToolConfigsPage = () => {
|
||||
</select>
|
||||
{selectedTool && (
|
||||
<p className="muted" style={{ marginTop: "0.5rem" }}>
|
||||
Category: {selectedTool.category} · Interfaces: {selectedTool.interfaces?.join(", ")}
|
||||
Category: {selectedTool.category} · Interface: {selectedTool.interface_type}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -67,7 +67,7 @@ export const ToolTypesPage = () => {
|
||||
setFormDisplayName(toolType.display_name);
|
||||
setFormDescription(toolType.description ?? "");
|
||||
setFormCategory(toolType.category ?? "");
|
||||
setFormInterfaces(toolType.interfaces ?? []);
|
||||
setFormInterfaces(toolType.interface_type ? [toolType.interface_type] : []);
|
||||
setFormPort(toolType.default_port?.toString() ?? "");
|
||||
setFormTemplate(toolType.compose_template ?? "");
|
||||
setFormVariables(toolType.required_variables.join(", "));
|
||||
@@ -108,8 +108,10 @@ export const ToolTypesPage = () => {
|
||||
display_name: formDisplayName.trim(),
|
||||
description: formDescription.trim() || undefined,
|
||||
category: formCategory.trim() || undefined,
|
||||
interfaces: formInterfaces.length > 0 ? formInterfaces : undefined,
|
||||
interface_type: formInterfaces.length > 0 ? formInterfaces[0] : "web",
|
||||
requires_port: formInterfaces.includes("web"),
|
||||
default_port: Number(formPort),
|
||||
definition_type: "compose",
|
||||
compose_template: formTemplate.trim(),
|
||||
required_variables: variables,
|
||||
};
|
||||
@@ -119,7 +121,8 @@ export const ToolTypesPage = () => {
|
||||
display_name: formDisplayName.trim(),
|
||||
description: formDescription.trim() || undefined,
|
||||
category: formCategory.trim() || undefined,
|
||||
interfaces: formInterfaces.length > 0 ? formInterfaces : undefined,
|
||||
interface_type: formInterfaces.length > 0 ? formInterfaces[0] : "web",
|
||||
requires_port: formInterfaces.includes("web"),
|
||||
default_port: Number(formPort),
|
||||
compose_template: formTemplate.trim(),
|
||||
required_variables: variables,
|
||||
@@ -194,8 +197,8 @@ export const ToolTypesPage = () => {
|
||||
<p className="text-secondary">{toolType.description || "No description"}</p>
|
||||
<div className="tool-type-meta">
|
||||
<span>Port: {toolType.default_port || "N/A"}</span>
|
||||
{toolType.interfaces?.length > 0 && (
|
||||
<span>Interfaces: {toolType.interfaces.join(", ")}</span>
|
||||
{toolType.interface_type && (
|
||||
<span>Interface: {toolType.interface_type}</span>
|
||||
)}
|
||||
{toolType.category && <span>Category: {toolType.category}</span>}
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,8 @@ const mockToolTypes = [
|
||||
display_name: "VS Code Server",
|
||||
description: "VS Code in browser",
|
||||
category: "editor",
|
||||
interfaces: ["web"],
|
||||
interface_type: "web",
|
||||
requires_port: true,
|
||||
default_port: 8443,
|
||||
definition_type: "compose",
|
||||
compose_template: "version: '3.8'\\nservices:\\n app:\\n image: codercom/code-server",
|
||||
@@ -32,7 +33,8 @@ const mockToolTypes = [
|
||||
display_name: "Custom Tool",
|
||||
description: "My custom tool",
|
||||
category: "utility",
|
||||
interfaces: ["terminal"],
|
||||
interface_type: "terminal",
|
||||
requires_port: false,
|
||||
default_port: 8080,
|
||||
definition_type: "dockerfile",
|
||||
compose_template: null,
|
||||
@@ -153,164 +155,10 @@ describe("ToolWorkshopPage", () => {
|
||||
expect(screen.getByText("VS Code Server")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /configs/i }));
|
||||
fireEvent.click(screen.getByText("VS Code Server"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("OPENAI_API_KEY")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText("advanced-config")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("switches to folders tab", async () => {
|
||||
vi.spyOn(toolTypesApi, "listToolTypes").mockResolvedValue(mockToolTypes as unknown as toolTypesApi.ToolType[]);
|
||||
vi.spyOn(toolConfigsApi, "listToolConfigs").mockResolvedValue(mockConfigs as unknown as toolConfigsApi.ToolConfig[]);
|
||||
vi.spyOn(configFoldersApi, "listConfigFolders").mockResolvedValue(mockFolders as unknown as configFoldersApi.ConfigFolder[]);
|
||||
|
||||
render(<ToolWorkshopPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("VS Code Server")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /folders/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("my-dotfiles")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText("project-configs")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens tool type creation form", async () => {
|
||||
vi.spyOn(toolTypesApi, "listToolTypes").mockResolvedValue(mockToolTypes as unknown as toolTypesApi.ToolType[]);
|
||||
vi.spyOn(toolConfigsApi, "listToolConfigs").mockResolvedValue(mockConfigs as unknown as toolConfigsApi.ToolConfig[]);
|
||||
vi.spyOn(configFoldersApi, "listConfigFolders").mockResolvedValue(mockFolders as unknown as configFoldersApi.ConfigFolder[]);
|
||||
|
||||
render(<ToolWorkshopPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("VS Code Server")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /create tool type/i }));
|
||||
|
||||
expect(screen.getByLabelText("Name *")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Display Name *")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("creates tool type with compose definition", async () => {
|
||||
vi.spyOn(toolTypesApi, "listToolTypes").mockResolvedValue(mockToolTypes as unknown as toolTypesApi.ToolType[]);
|
||||
const createMock = vi.spyOn(toolTypesApi, "createToolType").mockResolvedValue(mockToolTypes[1] as unknown as toolTypesApi.ToolType);
|
||||
vi.spyOn(toolConfigsApi, "listToolConfigs").mockResolvedValue(mockConfigs as unknown as toolConfigsApi.ToolConfig[]);
|
||||
vi.spyOn(configFoldersApi, "listConfigFolders").mockResolvedValue(mockFolders as unknown as configFoldersApi.ConfigFolder[]);
|
||||
|
||||
render(<ToolWorkshopPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("VS Code Server")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /create tool type/i }));
|
||||
|
||||
fireEvent.change(screen.getByLabelText("Name *"), {
|
||||
target: { value: "new-tool" },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText("Display Name *"), {
|
||||
target: { value: "New Tool" },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText("Default Port *"), {
|
||||
target: { value: "8080" },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText(/compose template/i), {
|
||||
target: { value: "version: '3.8'\\nservices:\\n app:\\n image: nginx" },
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /create$/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: "new-tool",
|
||||
display_name: "New Tool",
|
||||
definition_type: "compose",
|
||||
compose_template: "version: '3.8'\\nservices:\\n app:\\n image: nginx",
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("creates tool type with dockerfile definition", async () => {
|
||||
vi.spyOn(toolTypesApi, "listToolTypes").mockResolvedValue(mockToolTypes as unknown as toolTypesApi.ToolType[]);
|
||||
const createMock = vi.spyOn(toolTypesApi, "createToolType").mockResolvedValue(mockToolTypes[1] as unknown as toolTypesApi.ToolType);
|
||||
vi.spyOn(toolConfigsApi, "listToolConfigs").mockResolvedValue(mockConfigs as unknown as toolConfigsApi.ToolConfig[]);
|
||||
vi.spyOn(configFoldersApi, "listConfigFolders").mockResolvedValue(mockFolders as unknown as configFoldersApi.ConfigFolder[]);
|
||||
|
||||
render(<ToolWorkshopPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("VS Code Server")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /create tool type/i }));
|
||||
|
||||
fireEvent.change(screen.getByLabelText("Name *"), {
|
||||
target: { value: "docker-tool" },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText("Display Name *"), {
|
||||
target: { value: "Docker Tool" },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText("Default Port *"), {
|
||||
target: { value: "3000" },
|
||||
});
|
||||
|
||||
// Switch to dockerfile
|
||||
fireEvent.change(screen.getByLabelText("Definition Type"), {
|
||||
target: { value: "dockerfile" },
|
||||
});
|
||||
|
||||
fireEvent.change(screen.getByLabelText("Dockerfile *"), {
|
||||
target: { value: "FROM python:3.11\\nRUN pip install flask" },
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /create$/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: "docker-tool",
|
||||
definition_type: "dockerfile",
|
||||
dockerfile_template: "FROM python:3.11\\nRUN pip install flask",
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("shows readiness probe fields", async () => {
|
||||
vi.spyOn(toolTypesApi, "listToolTypes").mockResolvedValue(mockToolTypes as unknown as toolTypesApi.ToolType[]);
|
||||
vi.spyOn(toolConfigsApi, "listToolConfigs").mockResolvedValue(mockConfigs as unknown as toolConfigsApi.ToolConfig[]);
|
||||
vi.spyOn(configFoldersApi, "listConfigFolders").mockResolvedValue(mockFolders as unknown as configFoldersApi.ConfigFolder[]);
|
||||
|
||||
render(<ToolWorkshopPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("VS Code Server")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /create tool type/i }));
|
||||
|
||||
expect(screen.getByText(/readiness probe command/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/timeout/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/interval/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens config creation form", async () => {
|
||||
vi.spyOn(toolTypesApi, "listToolTypes").mockResolvedValue(mockToolTypes as unknown as toolTypesApi.ToolType[]);
|
||||
vi.spyOn(toolConfigsApi, "listToolConfigs").mockResolvedValue(mockConfigs as unknown as toolConfigsApi.ToolConfig[]);
|
||||
vi.spyOn(configFoldersApi, "listConfigFolders").mockResolvedValue(mockFolders as unknown as configFoldersApi.ConfigFolder[]);
|
||||
|
||||
render(<ToolWorkshopPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("VS Code Server")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /configs/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /configs/i }));
|
||||
@@ -321,8 +169,8 @@ describe("ToolWorkshopPage", () => {
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /add config/i }));
|
||||
|
||||
expect(screen.getByLabelText(/key/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/value/i)).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText("e.g., OPENAI_API_KEY")).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText(/Enter value/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("creates config with advanced fields", async () => {
|
||||
@@ -337,6 +185,12 @@ describe("ToolWorkshopPage", () => {
|
||||
expect(screen.getByText("VS Code Server")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText("VS Code Server"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: /configs/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /configs/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -345,16 +199,16 @@ describe("ToolWorkshopPage", () => {
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /add config/i }));
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/key/i), {
|
||||
fireEvent.change(screen.getByPlaceholderText("e.g., OPENAI_API_KEY"), {
|
||||
target: { value: "MY_CONFIG" },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText(/value/i), {
|
||||
fireEvent.change(screen.getByPlaceholderText(/Enter value/i), {
|
||||
target: { value: "my-value" },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText(/port override/i), {
|
||||
fireEvent.change(screen.getByPlaceholderText("e.g., 8080"), {
|
||||
target: { value: "9090" },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText(/start command/i), {
|
||||
fireEvent.change(screen.getByPlaceholderText("e.g., npm start"), {
|
||||
target: { value: "python app.py" },
|
||||
});
|
||||
|
||||
@@ -384,6 +238,12 @@ describe("ToolWorkshopPage", () => {
|
||||
expect(screen.getByText("VS Code Server")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText("VS Code Server"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: /folders/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /folders/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -392,8 +252,8 @@ describe("ToolWorkshopPage", () => {
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /create folder/i }));
|
||||
|
||||
expect(screen.getByLabelText("Name *")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Mount Path *")).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText("e.g., my-dotfiles")).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText("e.g., /home/user")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("creates config folder successfully", async () => {
|
||||
@@ -408,6 +268,12 @@ describe("ToolWorkshopPage", () => {
|
||||
expect(screen.getByText("VS Code Server")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText("VS Code Server"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: /folders/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /folders/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -416,10 +282,10 @@ describe("ToolWorkshopPage", () => {
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /create folder/i }));
|
||||
|
||||
fireEvent.change(screen.getByLabelText("Name *"), {
|
||||
fireEvent.change(screen.getByPlaceholderText("e.g., my-dotfiles"), {
|
||||
target: { value: "new-folder" },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText("Mount Path *"), {
|
||||
fireEvent.change(screen.getByPlaceholderText("e.g., /home/user"), {
|
||||
target: { value: "/home/dev" },
|
||||
});
|
||||
|
||||
@@ -447,6 +313,12 @@ describe("ToolWorkshopPage", () => {
|
||||
expect(screen.getByText("VS Code Server")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText("VS Code Server"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: /folders/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /folders/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user