@@ -100,7 +100,7 @@ vi.mock("../../views/workspace/WSEditorSwaggerPicker", () => ({
),
}));
-vi.mock("../../views/workspace/WSEditorClientConfig", () => ({
+vi.mock("../../views/workspace/components/WSEditor/WSEditorClientConfig", () => ({
default: ({ workspaceUrl, open, onClose }: any) =>
open ? (
diff --git a/src/web/src/__tests__/components/WSEditorClientConfig.test.tsx b/src/web/src/__tests__/components/WSEditorClientConfig.test.tsx
index 083d70ab..17ddd7db 100644
--- a/src/web/src/__tests__/components/WSEditorClientConfig.test.tsx
+++ b/src/web/src/__tests__/components/WSEditorClientConfig.test.tsx
@@ -1,8 +1,8 @@
-import { describe, it, expect, vi, beforeEach } from "vitest";
+import { describe, it, expect, beforeEach, vi } from "vitest";
import { screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { render } from "../test-utils";
-import WSEditorClientConfigDialog from "../../views/workspace/WSEditorClientConfig";
+import WSEditorClientConfigDialog from "../../views/workspace/components/WSEditor/WSEditorClientConfig";
import { workspaceApi, specsApi, errorHandlerApi } from "../../services";
vi.mock("../../services", () => ({
diff --git a/src/web/src/__tests__/components/WSEditorCommandArgumentsContent.test.tsx b/src/web/src/__tests__/components/WSEditorCommandArgumentsContent.test.tsx
index 4f0c10bb..71da5a75 100644
--- a/src/web/src/__tests__/components/WSEditorCommandArgumentsContent.test.tsx
+++ b/src/web/src/__tests__/components/WSEditorCommandArgumentsContent.test.tsx
@@ -1,8 +1,8 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { fireEvent, screen } from "@testing-library/react";
-import WSEditorCommandArgumentsContent from "../../views/workspace/WSEditorCommandArgumentsContent";
-import type { CMDArg, ClsArgDefinitionMap } from "../../views/workspace/WSEditorCommandArgumentsContent";
+import WSEditorCommandArgumentsContent from "../../views/workspace/components/WSEditorCommandArgumentsContent/WSEditorCommandArgumentsContent";
import { render } from "../test-utils";
+import { ClsArgDefinitionMap, CMDArg } from "../../views/workspace/components/WSEditorCommandArgumentsContent";
vi.mock("../../services/commandApi");
vi.mock("../../services/errorHandlerApi");
diff --git a/src/web/src/__tests__/components/WSEditorCommandContent.test.tsx b/src/web/src/__tests__/components/WSEditorCommandContent.test.tsx
index e62bf91d..dbca2c27 100644
--- a/src/web/src/__tests__/components/WSEditorCommandContent.test.tsx
+++ b/src/web/src/__tests__/components/WSEditorCommandContent.test.tsx
@@ -1,7 +1,7 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { fireEvent, screen, waitFor, within } from "@testing-library/react";
-import WSEditorCommandContent from "../../views/workspace/WSEditorCommandContent";
-import type { Command, Example, Resource } from "../../views/workspace/WSEditorCommandContent";
+import WSEditorCommandContent from "../../views/workspace/components/WSEditorCommandContent";
+import type { Command, Example, Resource } from "../../views/workspace/interfaces";
import { render } from "../test-utils";
import { commandApi } from "../../services/commandApi";
diff --git a/src/web/src/__tests__/components/WSEditorCommandGroupContent.test.tsx b/src/web/src/__tests__/components/WSEditorCommandGroupContent.test.tsx
index 15c6058d..0565e713 100644
--- a/src/web/src/__tests__/components/WSEditorCommandGroupContent.test.tsx
+++ b/src/web/src/__tests__/components/WSEditorCommandGroupContent.test.tsx
@@ -1,7 +1,7 @@
import { render, screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { vi } from "vitest";
-import WSEditorCommandGroupContent from "../../views/workspace/WSEditorCommandGroupContent";
+import WSEditorCommandGroupContent from "../../views/workspace/components/WSEditorCommandGroupContent/WSEditorCommandGroupContent";
import * as commandApi from "../../services/commandApi";
interface CommandGroup {
diff --git a/src/web/src/__tests__/components/WSEditorCommandTree.test.tsx b/src/web/src/__tests__/components/WSEditorCommandTree.test.tsx
index f7575e64..098b3c18 100644
--- a/src/web/src/__tests__/components/WSEditorCommandTree.test.tsx
+++ b/src/web/src/__tests__/components/WSEditorCommandTree.test.tsx
@@ -1,7 +1,10 @@
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { vi } from "vitest";
-import WSEditorCommandTree, { CommandTreeNode, CommandTreeLeaf } from "../../views/workspace/WSEditorCommandTree";
+import WSEditorCommandTree, {
+ CommandTreeNode,
+ CommandTreeLeaf,
+} from "../../views/workspace/components/WSEditor/WSEditorCommandTree";
describe("WSEditorCommandTree", () => {
const mockLeaf: CommandTreeLeaf = {
diff --git a/src/web/src/__tests__/components/WSEditorSwaggerPicker.test.tsx b/src/web/src/__tests__/components/WSEditorSwaggerPicker.test.tsx
index 8a1feb93..e2cd9f85 100644
--- a/src/web/src/__tests__/components/WSEditorSwaggerPicker.test.tsx
+++ b/src/web/src/__tests__/components/WSEditorSwaggerPicker.test.tsx
@@ -1,6 +1,7 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { fireEvent, screen, waitFor } from "@testing-library/react";
-import WSEditorSwaggerPicker, { SwaggerItemSelector } from "../../views/workspace/WSEditorSwaggerPicker";
+import WSEditorSwaggerPicker from "../../views/workspace/components/WSEditorSwaggerPicker";
+import SwaggerItemSelector from "../../views/workspace/common/SwaggerItemSelector";
import { render } from "../test-utils";
import { workspaceApi, specsApi } from "../../services";
@@ -210,6 +211,71 @@ describe("WSEditorSwaggerPicker", () => {
expect(vi.mocked(workspaceApi).getWorkspaceResourcesByName).toHaveBeenCalledWith("test-workspace");
});
});
+
+ it("calls getResourceProvidersWithType with type=OpenAPI parameter", async () => {
+ render(
);
+
+ await waitFor(() => {
+ expect(vi.mocked(specsApi).getResourceProvidersWithType).toHaveBeenCalledWith(
+ "/Swagger/Specs/ResourceManagement/microsoft.storage",
+ "Swagger",
+ );
+ });
+ });
+
+ it("passes sourceOverride parameter when loading resource providers", async () => {
+ vi.mocked(workspaceApi).getSwaggerDefault.mockResolvedValue({
+ ...mockSwaggerDefault,
+ source: "TypeSpec",
+ });
+
+ render(
);
+
+ await waitFor(() => {
+ expect(vi.mocked(specsApi).getResourceProvidersWithType).toHaveBeenCalledWith(
+ "/Swagger/Specs/ResourceManagement/microsoft.storage",
+ "TypeSpec",
+ );
+ });
+ });
+
+ it("reloads resources when existingResources change", async () => {
+ const { rerender } = render(
);
+
+ await waitFor(() => {
+ expect(vi.mocked(specsApi).getResourceProvidersWithType).toHaveBeenCalledTimes(1);
+ });
+
+ const newMockWorkspaceResources = [
+ ...mockWorkspaceResources,
+ {
+ id: "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{accountName}/blobServices/default",
+ },
+ ];
+ vi.mocked(workspaceApi).getWorkspaceResourcesByName.mockResolvedValue(newMockWorkspaceResources);
+
+ rerender(
);
+
+ expect(vi.mocked(specsApi).getResourceProvidersWithType).toHaveBeenCalledWith(
+ "/Swagger/Specs/ResourceManagement/microsoft.storage",
+ "Swagger",
+ );
+ });
+
+ it("correctly filters existing resources when displaying available options", async () => {
+ const availableResources = [mockResources[0], mockResources[1]];
+ vi.mocked(specsApi).getProviderResources.mockResolvedValue(availableResources);
+
+ render(
);
+
+ await waitFor(() => {
+ expect(vi.mocked(workspaceApi).getWorkspaceResourcesByName).toHaveBeenCalledWith("test-workspace");
+ });
+
+ await waitFor(() => {
+ expect(vi.mocked(specsApi).getProviderResources).toHaveBeenCalled();
+ });
+ });
});
describe("Resource Selection", () => {
@@ -490,6 +556,125 @@ describe("WSEditorSwaggerPicker", () => {
});
});
+ describe("API Type Parameter Handling", () => {
+ it("includes type=OpenAPI parameter for Swagger sources", async () => {
+ vi.mocked(workspaceApi).getSwaggerDefault.mockResolvedValue({
+ ...mockSwaggerDefault,
+ source: "Swagger",
+ });
+
+ render(
);
+
+ await waitFor(() => {
+ expect(vi.mocked(specsApi).getResourceProvidersWithType).toHaveBeenCalledWith(
+ "/Swagger/Specs/ResourceManagement/microsoft.storage",
+ "Swagger",
+ );
+ });
+ });
+
+ it("includes type=TypeSpec parameter for TypeSpec sources", async () => {
+ vi.mocked(workspaceApi).getSwaggerDefault.mockResolvedValue({
+ ...mockSwaggerDefault,
+ source: "TypeSpec",
+ });
+
+ render(
);
+
+ await waitFor(() => {
+ expect(vi.mocked(specsApi).getResourceProvidersWithType).toHaveBeenCalledWith(
+ "/Swagger/Specs/ResourceManagement/microsoft.storage",
+ "TypeSpec",
+ );
+ });
+ });
+
+ it("uses default source when no source override is available", async () => {
+ vi.mocked(workspaceApi).getSwaggerDefault.mockResolvedValue({
+ ...mockSwaggerDefault,
+ source: undefined,
+ });
+
+ render(
);
+
+ await waitFor(() => {
+ expect(vi.mocked(specsApi).getResourceProvidersWithType).toHaveBeenCalledWith(
+ "/Swagger/Specs/ResourceManagement/microsoft.storage",
+ undefined,
+ );
+ });
+ });
+ });
+
+ describe("Existing Resources State Management", () => {
+ it("reloads resources when existing workspace resources change", async () => {
+ render(
);
+
+ await waitFor(() => {
+ expect(vi.mocked(specsApi).getResourceProvidersWithType).toHaveBeenCalledTimes(1);
+ });
+
+ expect(vi.mocked(specsApi).getResourceProvidersWithType).toHaveBeenCalledWith(
+ "/Swagger/Specs/ResourceManagement/microsoft.storage",
+ "Swagger",
+ );
+ });
+
+ it("prevents duplicate resources from appearing in selector", async () => {
+ const duplicateResource = {
+ id: "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{accountName}",
+ };
+
+ vi.mocked(workspaceApi).getWorkspaceResourcesByName.mockResolvedValue([duplicateResource]);
+ vi.mocked(specsApi).getProviderResources.mockResolvedValue([
+ {
+ ...mockResources[0],
+ id: duplicateResource.id,
+ },
+ mockResources[1],
+ ]);
+
+ render(
);
+
+ await waitFor(() => {
+ expect(vi.mocked(workspaceApi).getWorkspaceResourcesByName).toHaveBeenCalledWith("test-workspace");
+ });
+
+ await waitFor(() => {
+ expect(vi.mocked(specsApi).getProviderResources).toHaveBeenCalled();
+ });
+ });
+
+ it("handles empty workspace resources correctly", async () => {
+ vi.mocked(workspaceApi).getWorkspaceResourcesByName.mockResolvedValue([]);
+
+ render(
);
+
+ await waitFor(() => {
+ expect(vi.mocked(workspaceApi).getWorkspaceResourcesByName).toHaveBeenCalledWith("test-workspace");
+ });
+
+ await waitFor(() => {
+ expect(vi.mocked(specsApi).getResourceProvidersWithType).toHaveBeenCalledWith(
+ "/Swagger/Specs/ResourceManagement/microsoft.storage",
+ "Swagger",
+ );
+ });
+ });
+
+ it("validates existingResources dependency in useCallback", async () => {
+ render(
);
+
+ await waitFor(() => {
+ expect(vi.mocked(workspaceApi).getWorkspaceResourcesByName).toHaveBeenCalledWith("test-workspace");
+ });
+
+ await waitFor(() => {
+ expect(vi.mocked(specsApi).getResourceProvidersWithType).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+
describe("Close Functionality", () => {
it("calls onClose when close button is clicked", async () => {
const onCloseMock = vi.fn();
diff --git a/src/web/src/__tests__/components/WorkspaceSelector.test.tsx b/src/web/src/__tests__/components/WorkspaceSelector.test.tsx
index c44a5759..5efbcdba 100644
--- a/src/web/src/__tests__/components/WorkspaceSelector.test.tsx
+++ b/src/web/src/__tests__/components/WorkspaceSelector.test.tsx
@@ -1,8 +1,8 @@
-import { describe, it, expect, vi, beforeEach } from "vitest";
+import { describe, it, expect, beforeEach, vi } from "vitest";
import { screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { render } from "../test-utils";
-import WorkspaceSelector from "../../views/workspace/WorkspaceSelector";
+import WorkspaceSelector from "../../views/workspace/components/WorkspaceInstruction/WorkspaceSelector";
import { workspaceApi } from "../../services";
vi.mock("../../services", () => ({
@@ -298,4 +298,38 @@ describe("Workspace Management", () => {
expect(result).toEqual(expectedResult);
});
});
+
+ describe("WorkspaceCreateDialog Component", () => {
+ it("should pre-select the first plane after planes are loaded in the create dialog", async () => {
+ const user = userEvent.setup();
+ const mockPlanes = [
+ { name: "MgmtClient", displayName: "Control plane", moduleOptions: ["mod1", "mod2"] },
+ { name: "DataPlaneClient", displayName: "Data plane", moduleOptions: ["mod3"] },
+ ];
+ const { specsApi } = await import("../../services");
+ (specsApi.getPlanes as any).mockResolvedValue(mockPlanes);
+
+ render(
);
+
+ await waitFor(() => {
+ expect(workspaceApi.getWorkspaces).toHaveBeenCalled();
+ });
+
+ const autocomplete = screen.getByLabelText("Select Workspace");
+ await user.click(autocomplete);
+ await user.type(autocomplete, "new-test-workspace");
+
+ const createOption = await screen.findByText('Create "new-test-workspace"');
+ await user.click(createOption);
+
+ await screen.findByText("Create a new workspace");
+
+ await waitFor(() => {
+ expect(specsApi.getPlanes).toHaveBeenCalled();
+ });
+
+ const planeDropdown = await screen.findByLabelText(/Plane/i);
+ expect(planeDropdown).toHaveValue("Control plane");
+ });
+ });
});
diff --git a/src/web/src/__tests__/components/cli/CLIModGeneratorProfileCommandTree.test.tsx b/src/web/src/__tests__/components/cli/CLIModGeneratorProfileCommandTree.test.tsx
new file mode 100644
index 00000000..88091068
--- /dev/null
+++ b/src/web/src/__tests__/components/cli/CLIModGeneratorProfileCommandTree.test.tsx
@@ -0,0 +1,401 @@
+import { render, screen, fireEvent, waitFor } from "@testing-library/react";
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import CLIModGeneratorProfileCommandTree from "../../../views/cli/components/CLIModGeneratorProfileCommandTree";
+import { ProfileCommandTree } from "../../../views/cli/utils/commandTreeInitialization";
+
+vi.mock("@mui/lab/TreeView", () => ({
+ default: ({ children, ...props }: any) => (
+
+ {children}
+
+ ),
+}));
+
+vi.mock("@mui/lab/TreeItem", () => ({
+ default: ({ label, children, nodeId, ...props }: any) => (
+
+
{label}
+ {children &&
{children}
}
+
+ ),
+}));
+
+describe("CLIModGeneratorProfileCommandTree", () => {
+ const mockOnChange = vi.fn();
+ const mockOnLoadCommands = vi.fn();
+
+ const mockProfileCommandTree: ProfileCommandTree = {
+ name: "test-profile",
+ commandGroups: {
+ "test-group": {
+ id: "test-group",
+ names: ["test-group"],
+ commands: {
+ "test-command": {
+ id: "test-group/test-command",
+ names: ["test-group", "test-command"],
+ selected: false,
+ modified: false,
+ loading: false,
+ },
+ "selected-command": {
+ id: "test-group/selected-command",
+ names: ["test-group", "selected-command"],
+ selected: true,
+ selectedVersion: "2023-01-01",
+ registered: true,
+ modified: false,
+ loading: false,
+ versions: [
+ { name: "2023-01-01", stage: "stable" },
+ { name: "2022-12-01", stage: "preview" },
+ ],
+ },
+ },
+ loading: false,
+ selected: undefined,
+ },
+ "empty-group": {
+ id: "empty-group",
+ names: ["empty-group"],
+ commands: {},
+ loading: false,
+ selected: false,
+ },
+ },
+ };
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockOnLoadCommands.mockResolvedValue([]);
+ });
+
+ it("should render the tree view with command groups", () => {
+ render(
+
,
+ );
+
+ expect(screen.getByTestId("cli-command-tree")).toBeInTheDocument();
+ // Should have 1 command group + 2 command items + 1 nested command group = 4 total
+ const commandGroups = screen.getAllByTestId(/^command-group-/);
+ const commandItems = screen.getAllByTestId("tree-item");
+ expect(commandGroups.length + commandItems.length).toBe(4);
+ });
+
+ it("should display command group names correctly", () => {
+ render(
+
,
+ );
+
+ expect(screen.getByText("test-group")).toBeInTheDocument();
+ expect(screen.getByText("empty-group")).toBeInTheDocument();
+ });
+
+ it("should display command names correctly", () => {
+ render(
+
,
+ );
+
+ expect(screen.getByText("test-command")).toBeInTheDocument();
+ expect(screen.getByText("selected-command")).toBeInTheDocument();
+ });
+
+ it("should show command group checkboxes with correct states", () => {
+ render(
+
,
+ );
+
+ const checkboxes = screen.getAllByRole("checkbox");
+ expect(checkboxes).toHaveLength(4);
+
+ const emptyGroupCheckbox = checkboxes.find((checkbox) => checkbox.closest('[data-node-id="empty-group"]'));
+ expect(emptyGroupCheckbox).toHaveProperty("checked", false);
+ });
+
+ it("should show version selector for selected commands", () => {
+ render(
+
,
+ );
+
+ expect(screen.getByDisplayValue("2023-01-01")).toBeInTheDocument();
+ expect(screen.getByText("Version")).toBeInTheDocument();
+ });
+
+ it("should show registered/unregistered selector for selected commands", () => {
+ render(
+
,
+ );
+
+ expect(screen.getByText("Command table")).toBeInTheDocument();
+ expect(screen.getByText("Registered")).toBeInTheDocument();
+ });
+
+ it("should call onChange when command is selected", async () => {
+ render(
+
,
+ );
+
+ const testCommandCheckbox = screen
+ .getAllByRole("checkbox")
+ .find((checkbox) => checkbox.closest('[data-node-id="test-group/test-command"]'));
+
+ expect(testCommandCheckbox).toBeDefined();
+ fireEvent.click(testCommandCheckbox!);
+
+ await waitFor(() => {
+ expect(mockOnChange).toHaveBeenCalled();
+ });
+ });
+
+ it("should call onChange when command group is selected", async () => {
+ render(
+
,
+ );
+
+ const groupCheckbox = screen
+ .getAllByRole("checkbox")
+ .find((checkbox) => checkbox.closest('[data-node-id="empty-group"]'));
+
+ expect(groupCheckbox).toBeDefined();
+ fireEvent.click(groupCheckbox!);
+
+ await waitFor(() => {
+ expect(mockOnChange).toHaveBeenCalled();
+ });
+ });
+
+ it("should call onChange when version is changed", async () => {
+ render(
+
,
+ );
+
+ const versionSelect = screen.getByDisplayValue("2023-01-01");
+ fireEvent.change(versionSelect, { target: { value: "2022-12-01" } });
+
+ await waitFor(() => {
+ expect(mockOnChange).toHaveBeenCalled();
+ });
+ });
+
+ it("should call onChange when registration status is changed", async () => {
+ render(
+
,
+ );
+
+ const registrationSelects = screen.getAllByRole("combobox");
+ expect(registrationSelects).toHaveLength(2);
+
+ const commandTableSelect = registrationSelects[1];
+ fireEvent.mouseDown(commandTableSelect);
+
+ const unregisteredOption = screen.getByText("Unregistered");
+ fireEvent.click(unregisteredOption);
+
+ await waitFor(() => {
+ expect(mockOnChange).toHaveBeenCalled();
+ });
+ });
+
+ it("should show loading state for commands that are loading", () => {
+ const loadingTree: ProfileCommandTree = {
+ name: "test-profile",
+ commandGroups: {
+ "test-group": {
+ id: "test-group",
+ names: ["test-group"],
+ commands: {
+ "loading-command": {
+ id: "test-group/loading-command",
+ names: ["test-group", "loading-command"],
+ selected: true,
+ modified: false,
+ loading: true,
+ },
+ },
+ loading: false,
+ selected: true,
+ },
+ },
+ };
+
+ render(
+
,
+ );
+
+ expect(screen.getByText("Loading...")).toBeInTheDocument();
+ });
+
+ it("should show edit icon correctly based on command state", () => {
+ const mixedTree: ProfileCommandTree = {
+ name: "test-profile",
+ commandGroups: {
+ "test-group": {
+ id: "test-group",
+ names: ["test-group"],
+ commands: {
+ "modified-command": {
+ id: "test-group/modified-command",
+ names: ["test-group", "modified-command"],
+ selected: true,
+ selectedVersion: "2023-01-01",
+ modified: true,
+ loading: false,
+ },
+ "unmodified-with-version": {
+ id: "test-group/unmodified-with-version",
+ names: ["test-group", "unmodified-with-version"],
+ selected: true,
+ selectedVersion: "2023-01-01",
+ modified: false,
+ loading: false,
+ },
+ "unmodified-no-version": {
+ id: "test-group/unmodified-no-version",
+ names: ["test-group", "unmodified-no-version"],
+ selected: false,
+ modified: false,
+ loading: false,
+ // No selectedVersion
+ },
+ },
+ loading: false,
+ selected: undefined,
+ },
+ },
+ };
+
+ render(
+
,
+ );
+
+ // Should show 2 EditIcons total:
+ // 1. Secondary color icon for modified command
+ // 2. Disabled icon button for unmodified command with version
+ const editIcons = screen.getAllByTestId("EditIcon");
+ expect(editIcons).toHaveLength(2);
+
+ // The modified command should have a secondary color edit icon
+ expect(editIcons[0]).toHaveClass("MuiSvgIcon-colorSecondary");
+
+ // The unmodified command with version should have a disabled edit icon button
+ expect(editIcons[1]).toHaveClass("MuiSvgIcon-colorDisabled");
+ });
+
+ it("should call onLoadCommands when selecting a command without versions", async () => {
+ const treeWithoutVersions: ProfileCommandTree = {
+ name: "test-profile",
+ commandGroups: {
+ "test-group": {
+ id: "test-group",
+ names: ["test-group"],
+ commands: {
+ "no-versions-command": {
+ id: "test-group/no-versions-command",
+ names: ["test-group", "no-versions-command"],
+ selected: false,
+ modified: false,
+ loading: false,
+ // No versions defined
+ },
+ },
+ loading: false,
+ selected: false,
+ },
+ },
+ };
+
+ render(
+
,
+ );
+
+ const commandCheckbox = screen
+ .getAllByRole("checkbox")
+ .find((checkbox) => checkbox.closest('[data-node-id="test-group/no-versions-command"]'));
+
+ expect(commandCheckbox).toBeDefined();
+ fireEvent.click(commandCheckbox!);
+
+ await waitFor(() => {
+ expect(mockOnChange).toHaveBeenCalled();
+ });
+
+ expect(commandCheckbox).toHaveProperty("checked", false);
+ });
+
+ it("should prevent event propagation on checkbox clicks", () => {
+ const mockStopPropagation = vi.fn();
+ const mockPreventDefault = vi.fn();
+
+ render(
+
,
+ );
+
+ const checkbox = screen.getAllByRole("checkbox")[0];
+
+ const mockEvent = {
+ stopPropagation: mockStopPropagation,
+ preventDefault: mockPreventDefault,
+ target: { checked: true },
+ } as any;
+
+ fireEvent.click(checkbox, mockEvent);
+
+ expect(mockOnChange).toHaveBeenCalled();
+ });
+});
diff --git a/src/web/src/__tests__/hooks/useTreeState.test.ts b/src/web/src/__tests__/hooks/useTreeState.test.ts
new file mode 100644
index 00000000..6e29fc50
--- /dev/null
+++ b/src/web/src/__tests__/hooks/useTreeState.test.ts
@@ -0,0 +1,237 @@
+import { renderHook, act } from "@testing-library/react";
+import { useTreeState } from "../../views/workspace/hooks/useTreeState";
+import type { Command, CommandGroup } from "../../views/workspace/interfaces";
+
+describe("useTreeState", () => {
+ const mockCommandGroupMap = {
+ "group:automanage": {
+ id: "group:automanage",
+ names: ["automanage"],
+ } as CommandGroup,
+ "group:automanage/configuration-profile": {
+ id: "group:automanage/configuration-profile",
+ names: ["automanage", "configuration-profile"],
+ } as CommandGroup,
+ "group:automanage/configuration-profile/assignment": {
+ id: "group:automanage/configuration-profile/assignment",
+ names: ["automanage", "configuration-profile", "assignment"],
+ } as CommandGroup,
+ "group:storage": {
+ id: "group:storage",
+ names: ["storage"],
+ } as CommandGroup,
+ "group:storage/account": {
+ id: "group:storage/account",
+ names: ["storage", "account"],
+ } as CommandGroup,
+ };
+
+ const mockCommandMap = {
+ "command:automanage/configuration-profile/assignment/create": {
+ id: "command:automanage/configuration-profile/assignment/create",
+ names: ["automanage", "configuration-profile", "assignment", "create"],
+ } as Command,
+ };
+
+ const mockCommandTree = [
+ {
+ id: "group:automanage",
+ names: ["automanage"],
+ canDelete: true,
+ nodes: [
+ {
+ id: "group:automanage/configuration-profile",
+ names: ["automanage", "configuration-profile"],
+ canDelete: true,
+ nodes: [
+ {
+ id: "group:automanage/configuration-profile/assignment",
+ names: ["automanage", "configuration-profile", "assignment"],
+ canDelete: true,
+ leaves: [
+ {
+ id: "command:automanage/configuration-profile/assignment/create",
+ names: ["automanage", "configuration-profile", "assignment", "create"],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ id: "group:storage",
+ names: ["storage"],
+ canDelete: true,
+ nodes: [
+ {
+ id: "group:storage/account",
+ names: ["storage", "account"],
+ canDelete: true,
+ },
+ ],
+ },
+ ];
+
+ describe("updateExpanded with autoExpandAll", () => {
+ it("should expand all command groups when autoExpandAll is true", () => {
+ const { result } = renderHook(() => useTreeState(mockCommandMap, {}, mockCommandTree));
+
+ act(() => {
+ result.current.updateExpanded(mockCommandGroupMap, undefined, true);
+ });
+
+ const expandedArray = Array.from(result.current.expanded);
+
+ // Should include all command groups
+ expect(expandedArray).toContain("group:automanage");
+ expect(expandedArray).toContain("group:automanage/configuration-profile");
+ expect(expandedArray).toContain("group:automanage/configuration-profile/assignment");
+ expect(expandedArray).toContain("group:storage");
+ expect(expandedArray).toContain("group:storage/account");
+
+ // Should include all parent paths for hierarchy
+ expect(expandedArray).toContain("group:automanage");
+ expect(expandedArray).toContain("group:automanage/configuration-profile");
+ expect(expandedArray).toContain("group:storage");
+
+ // Total count should be all unique paths
+ expect(expandedArray).toHaveLength(5);
+ });
+
+ it("should expand all groups regardless of existing expanded state when autoExpandAll is true", () => {
+ const { result } = renderHook(() => useTreeState(mockCommandMap, {}, mockCommandTree));
+
+ // First, manually expand only one group
+ act(() => {
+ result.current.updateExpanded({ "group:storage": mockCommandGroupMap["group:storage"] }, undefined, false);
+ });
+
+ // Should only have storage expanded initially
+ expect(Array.from(result.current.expanded)).toEqual(["group:storage"]);
+
+ // Now call with autoExpandAll=true
+ act(() => {
+ result.current.updateExpanded(mockCommandGroupMap, undefined, true);
+ });
+
+ const expandedArray = Array.from(result.current.expanded);
+
+ // Should now include ALL groups, not just storage
+ expect(expandedArray).toContain("group:automanage");
+ expect(expandedArray).toContain("group:automanage/configuration-profile");
+ expect(expandedArray).toContain("group:automanage/configuration-profile/assignment");
+ expect(expandedArray).toContain("group:storage");
+ expect(expandedArray).toContain("group:storage/account");
+ });
+
+ it("should only expand new groups when autoExpandAll is false", () => {
+ const initialCommandGroupMap = {
+ "group:storage": mockCommandGroupMap["group:storage"],
+ };
+
+ const { result } = renderHook(() => useTreeState(mockCommandMap, initialCommandGroupMap, mockCommandTree));
+
+ // First, expand storage (which already exists in initial map)
+ act(() => {
+ result.current.updateExpanded(initialCommandGroupMap, undefined, false);
+ });
+
+ // Should be empty since storage already existed in the initial map
+ expect(Array.from(result.current.expanded)).toHaveLength(0);
+
+ // Now add new groups with autoExpandAll=false
+ act(() => {
+ result.current.updateExpanded(mockCommandGroupMap, undefined, false);
+ });
+
+ const expandedArray = Array.from(result.current.expanded);
+
+ // Should only include new groups (not storage since it existed before)
+ expect(expandedArray).toContain("group:automanage");
+ expect(expandedArray).toContain("group:automanage/configuration-profile");
+ expect(expandedArray).toContain("group:automanage/configuration-profile/assignment");
+ expect(expandedArray).toContain("group:storage/account"); // This is new
+
+ // Should NOT include storage since it existed in the original commandGroupMap
+ expect(expandedArray).not.toContain("group:storage");
+ });
+
+ it("should include parent paths for deeply nested groups", () => {
+ const deeplyNestedMap = {
+ "group:level1/level2/level3/level4": {
+ id: "group:level1/level2/level3/level4",
+ names: ["level1", "level2", "level3", "level4"],
+ } as CommandGroup,
+ };
+
+ const { result } = renderHook(() => useTreeState(mockCommandMap, {}, mockCommandTree));
+
+ act(() => {
+ result.current.updateExpanded(deeplyNestedMap, undefined, true);
+ });
+
+ const expandedArray = Array.from(result.current.expanded);
+
+ // Should include the group itself
+ expect(expandedArray).toContain("group:level1/level2/level3/level4");
+
+ // Should include all parent paths for proper hierarchy
+ expect(expandedArray).toContain("group:level1/level2");
+ expect(expandedArray).toContain("group:level1/level2/level3");
+ });
+
+ it("should preserve existing expanded state when adding new groups with autoExpandAll=true", () => {
+ const { result } = renderHook(() => useTreeState(mockCommandMap, {}, mockCommandTree));
+
+ // Start with some manual expansion
+ act(() => {
+ result.current.handleCommandTreeToggle(["group:storage"]);
+ });
+
+ expect(Array.from(result.current.expanded)).toEqual(["group:storage"]);
+
+ // Now call updateExpanded with autoExpandAll=true
+ act(() => {
+ result.current.updateExpanded(mockCommandGroupMap, undefined, true);
+ });
+
+ const expandedArray = Array.from(result.current.expanded);
+
+ // Should still contain the manually expanded group
+ expect(expandedArray).toContain("group:storage");
+
+ // Plus all the auto-expanded groups
+ expect(expandedArray).toContain("group:automanage");
+ expect(expandedArray).toContain("group:automanage/configuration-profile");
+ expect(expandedArray).toContain("group:automanage/configuration-profile/assignment");
+ expect(expandedArray).toContain("group:storage/account");
+ });
+ });
+
+ describe("basic functionality", () => {
+ it("should initialize and auto-select first group with its path expanded", () => {
+ const { result } = renderHook(() => useTreeState(mockCommandMap, mockCommandGroupMap, mockCommandTree));
+
+ // Should auto-select the first group from commandTree
+ expect(result.current.selected?.id).toBe("group:automanage");
+
+ // Should auto-expand the path to the selected group
+ expect(result.current.expanded.size).toBe(1);
+ expect(Array.from(result.current.expanded)).toContain("group:automanage");
+ });
+
+ it("should handle command tree toggle", () => {
+ const { result } = renderHook(() => useTreeState(mockCommandMap, mockCommandGroupMap, mockCommandTree));
+
+ act(() => {
+ result.current.handleCommandTreeToggle(["group:storage", "group:automanage"]);
+ });
+
+ const expandedArray = Array.from(result.current.expanded);
+ expect(expandedArray).toContain("group:storage");
+ expect(expandedArray).toContain("group:automanage");
+ expect(expandedArray).toHaveLength(2);
+ });
+ });
+});
diff --git a/src/web/src/__tests__/integration/WSEditorClientConfig.integration.test.tsx b/src/web/src/__tests__/integration/WSEditorClientConfig.integration.test.tsx
index 5b121f2a..ace84e2d 100644
--- a/src/web/src/__tests__/integration/WSEditorClientConfig.integration.test.tsx
+++ b/src/web/src/__tests__/integration/WSEditorClientConfig.integration.test.tsx
@@ -4,7 +4,7 @@ import userEvent from "@testing-library/user-event";
import { setupServer } from "msw/node";
import { http, HttpResponse } from "msw";
import { render } from "../test-utils";
-import WSEditorClientConfigDialog from "../../views/workspace/WSEditorClientConfig";
+import WSEditorClientConfigDialog from "../../views/workspace/components/WSEditor/WSEditorClientConfig";
const mockConsoleError = vi.spyOn(console, "error").mockImplementation(() => {});
diff --git a/src/web/src/__tests__/integration/WorkspaceSelector.integration.test.tsx b/src/web/src/__tests__/integration/WorkspaceSelector.integration.test.tsx
index 1f75c8d8..3cf5af9c 100644
--- a/src/web/src/__tests__/integration/WorkspaceSelector.integration.test.tsx
+++ b/src/web/src/__tests__/integration/WorkspaceSelector.integration.test.tsx
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, vi } from "vitest";
import { screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { render } from "../test-utils";
-import WorkspaceSelector from "../../views/workspace/WorkspaceSelector";
+import WorkspaceSelector from "../../views/workspace/components/WorkspaceInstruction/WorkspaceSelector";
// Mock window.location for navigation testing
const mockLocation = {
diff --git a/src/web/src/__tests__/unit/argumentDecoding.test.ts b/src/web/src/__tests__/unit/argumentDecoding.test.ts
new file mode 100644
index 00000000..72640b51
--- /dev/null
+++ b/src/web/src/__tests__/unit/argumentDecoding.test.ts
@@ -0,0 +1,825 @@
+import { describe, it, expect } from "vitest";
+import { DecodeArgs } from "../../views/workspace/utils/decodeArgs";
+
+describe("Argument Decoding Functions", () => {
+ describe("DecodeArgs", () => {
+ it("should decode empty argument groups", () => {
+ const result = DecodeArgs([]);
+
+ expect(result.args).toEqual([]);
+ expect(result.clsArgDefineMap).toEqual({});
+ });
+
+ it("should decode basic string arguments", () => {
+ const argGroups = [
+ {
+ args: [
+ {
+ var: "resource_group",
+ options: ["--resource-group", "-g"],
+ type: "string",
+ required: true,
+ stage: "Stable",
+ hide: false,
+ group: "",
+ nullable: false,
+ help: {
+ short: "Resource group name",
+ lines: ["The name of the resource group"],
+ },
+ },
+ ],
+ },
+ ];
+
+ const result = DecodeArgs(argGroups);
+
+ expect(result.args).toHaveLength(1);
+ expect(result.args[0]).toMatchObject({
+ var: "resource_group",
+ options: ["--resource-group", "-g"],
+ type: "string",
+ required: true,
+ stage: "Stable",
+ hide: false,
+ group: "",
+ nullable: false,
+ });
+ expect(result.args[0].help?.short).toBe("Resource group name");
+ expect(result.args[0].help?.lines).toEqual(["The name of the resource group"]);
+ });
+
+ it("should decode integer arguments with defaults", () => {
+ const argGroups = [
+ {
+ args: [
+ {
+ var: "count",
+ options: ["--count", "-c"],
+ type: "integer",
+ required: false,
+ stage: "Stable",
+ hide: false,
+ group: "",
+ nullable: false,
+ default: {
+ value: 1,
+ },
+ help: {
+ short: "Number of instances",
+ },
+ },
+ ],
+ },
+ ];
+
+ const result = DecodeArgs(argGroups);
+
+ expect(result.args).toHaveLength(1);
+ expect(result.args[0]).toMatchObject({
+ var: "count",
+ type: "integer",
+ required: false,
+ default: {
+ value: 1,
+ },
+ });
+ });
+
+ it("should decode boolean arguments", () => {
+ const argGroups = [
+ {
+ args: [
+ {
+ var: "force",
+ options: ["--force"],
+ type: "boolean",
+ required: false,
+ stage: "Stable",
+ hide: false,
+ group: "",
+ nullable: false,
+ help: {
+ short: "Force the operation",
+ },
+ },
+ ],
+ },
+ ];
+
+ const result = DecodeArgs(argGroups);
+
+ expect(result.args).toHaveLength(1);
+ expect(result.args[0]).toMatchObject({
+ var: "force",
+ type: "boolean",
+ required: false,
+ });
+ });
+
+ it("should decode array arguments", () => {
+ const argGroups = [
+ {
+ args: [
+ {
+ var: "tags",
+ options: ["--tags"],
+ type: "array
",
+ required: false,
+ stage: "Stable",
+ hide: false,
+ group: "",
+ nullable: false,
+ item: {
+ type: "string",
+ nullable: false,
+ },
+ singularOptions: ["--tag"],
+ help: {
+ short: "Tags for the resource",
+ },
+ },
+ ],
+ },
+ ];
+
+ const result = DecodeArgs(argGroups);
+
+ expect(result.args).toHaveLength(1);
+ expect(result.args[0]).toMatchObject({
+ var: "tags",
+ type: "array",
+ required: false,
+ singularOptions: ["--tag"],
+ });
+ });
+
+ it("should decode object arguments with nested properties", () => {
+ const argGroups = [
+ {
+ args: [
+ {
+ var: "properties",
+ options: ["--properties"],
+ type: "object",
+ required: false,
+ stage: "Stable",
+ hide: false,
+ group: "",
+ nullable: false,
+ args: [
+ {
+ var: "name",
+ options: ["--name"],
+ type: "string",
+ required: true,
+ stage: "Stable",
+ hide: false,
+ group: "",
+ nullable: false,
+ help: {
+ short: "Property name",
+ },
+ },
+ {
+ var: "value",
+ options: ["--value"],
+ type: "string",
+ required: true,
+ stage: "Stable",
+ hide: false,
+ group: "",
+ nullable: false,
+ help: {
+ short: "Property value",
+ },
+ },
+ ],
+ help: {
+ short: "Object properties",
+ },
+ },
+ ],
+ },
+ ];
+
+ const result = DecodeArgs(argGroups);
+
+ expect(result.args).toHaveLength(1);
+ expect(result.args[0]).toMatchObject({
+ var: "properties",
+ type: "object",
+ required: false,
+ });
+ // For object args with nested properties, they are stored directly on the arg
+ const objArg = result.args[0] as any;
+ expect(objArg.args).toHaveLength(2);
+ expect(objArg.args[0].var).toBe("name");
+ expect(objArg.args[1].var).toBe("value");
+ });
+
+ it("should decode dictionary arguments", () => {
+ const argGroups = [
+ {
+ args: [
+ {
+ var: "metadata",
+ options: ["--metadata"],
+ type: "object",
+ required: false,
+ stage: "Stable",
+ hide: false,
+ group: "",
+ nullable: false,
+ additionalProps: {
+ item: {
+ type: "string",
+ nullable: false,
+ },
+ },
+ help: {
+ short: "Metadata dictionary",
+ },
+ },
+ ],
+ },
+ ];
+
+ const result = DecodeArgs(argGroups);
+
+ expect(result.args).toHaveLength(1);
+ expect(result.args[0]).toMatchObject({
+ var: "metadata",
+ type: "dict",
+ required: false,
+ });
+ });
+
+ it("should decode enum arguments", () => {
+ const argGroups = [
+ {
+ args: [
+ {
+ var: "sku",
+ options: ["--sku"],
+ type: "string",
+ required: false,
+ stage: "Stable",
+ hide: false,
+ group: "",
+ nullable: false,
+ enum: {
+ items: [
+ { name: "Standard", value: "Standard", hide: false },
+ { name: "Premium", value: "Premium", hide: false },
+ { name: "Basic", value: "Basic", hide: true },
+ ],
+ },
+ help: {
+ short: "SKU type",
+ },
+ },
+ ],
+ },
+ ];
+
+ const result = DecodeArgs(argGroups);
+
+ expect(result.args).toHaveLength(1);
+ expect(result.args[0]).toMatchObject({
+ var: "sku",
+ type: "string",
+ required: false,
+ hasEnum: true,
+ });
+ // The enum information is encoded into the argument during decoding
+ // We can test that hasEnum is true, which indicates enum processing occurred
+ expect(result.args[0].hasEnum).toBe(true);
+ });
+
+ it("should decode class reference arguments", () => {
+ const argGroups = [
+ {
+ args: [
+ {
+ var: "config",
+ options: ["--config"],
+ type: "@ConfigurationClass",
+ required: false,
+ stage: "Stable",
+ hide: false,
+ group: "",
+ nullable: false,
+ clsName: "ConfigurationClass",
+ help: {
+ short: "Configuration object",
+ },
+ },
+ ],
+ },
+ ];
+
+ const result = DecodeArgs(argGroups);
+
+ expect(result.args).toHaveLength(1);
+ expect(result.args[0]).toMatchObject({
+ var: "config",
+ type: "@ConfigurationClass",
+ required: false,
+ clsName: "ConfigurationClass",
+ });
+ });
+
+ it("should decode password arguments with prompt", () => {
+ const argGroups = [
+ {
+ args: [
+ {
+ var: "password",
+ options: ["--password", "-p"],
+ type: "password",
+ required: true,
+ stage: "Stable",
+ hide: false,
+ group: "",
+ nullable: false,
+ prompt: {
+ msg: "Enter password:",
+ confirm: true,
+ },
+ help: {
+ short: "User password",
+ },
+ },
+ ],
+ },
+ ];
+
+ const result = DecodeArgs(argGroups);
+
+ expect(result.args).toHaveLength(1);
+ expect(result.args[0]).toMatchObject({
+ var: "password",
+ type: "password",
+ required: true,
+ });
+ expect(result.args[0].prompt).toMatchObject({
+ msg: "Enter password:",
+ confirm: true,
+ });
+ });
+
+ it("should handle multiple argument groups", () => {
+ const argGroups = [
+ {
+ args: [
+ {
+ var: "name",
+ options: ["--name", "-n"],
+ type: "string",
+ required: true,
+ stage: "Stable",
+ hide: false,
+ group: "Basic",
+ nullable: false,
+ help: {
+ short: "Resource name",
+ },
+ },
+ ],
+ },
+ {
+ args: [
+ {
+ var: "location",
+ options: ["--location", "-l"],
+ type: "string",
+ required: false,
+ stage: "Stable",
+ hide: false,
+ group: "Advanced",
+ nullable: false,
+ help: {
+ short: "Resource location",
+ },
+ },
+ ],
+ },
+ ];
+
+ const result = DecodeArgs(argGroups);
+
+ expect(result.args).toHaveLength(2);
+ expect(result.args[0].var).toBe("name");
+ expect(result.args[0].group).toBe("Basic");
+ expect(result.args[1].var).toBe("location");
+ expect(result.args[1].group).toBe("Advanced");
+ });
+
+ it("should handle complex nested class definitions", () => {
+ const argGroups = [
+ {
+ args: [
+ {
+ var: "properties",
+ options: ["--properties"],
+ type: "object",
+ required: false,
+ stage: "Stable",
+ hide: false,
+ group: "",
+ nullable: false,
+ cls: "PropertiesClass",
+ args: [
+ {
+ var: "nested_config",
+ options: ["--nested-config"],
+ type: "object",
+ required: false,
+ stage: "Stable",
+ hide: false,
+ group: "",
+ nullable: false,
+ cls: "NestedConfigClass",
+ args: [
+ {
+ var: "deep_property",
+ options: ["--deep-property"],
+ type: "string",
+ required: false,
+ stage: "Stable",
+ hide: false,
+ group: "",
+ nullable: false,
+ help: {
+ short: "Deep nested property",
+ },
+ },
+ ],
+ help: {
+ short: "Nested configuration",
+ },
+ },
+ ],
+ help: {
+ short: "Properties object",
+ },
+ },
+ ],
+ },
+ ];
+
+ const result = DecodeArgs(argGroups);
+
+ expect(result.args).toHaveLength(1);
+ expect(result.args[0]).toMatchObject({
+ var: "properties",
+ type: "@PropertiesClass",
+ clsName: "PropertiesClass",
+ });
+ expect(result.clsArgDefineMap).toHaveProperty("PropertiesClass");
+ expect(result.clsArgDefineMap).toHaveProperty("NestedConfigClass");
+ });
+
+ it("should sort options by length in descending order", () => {
+ const argGroups = [
+ {
+ args: [
+ {
+ var: "name",
+ options: ["-n", "--name", "--full-name"],
+ type: "string",
+ required: true,
+ stage: "Stable",
+ hide: false,
+ group: "",
+ nullable: false,
+ help: {
+ short: "Resource name",
+ },
+ },
+ ],
+ },
+ ];
+
+ const result = DecodeArgs(argGroups);
+
+ expect(result.args[0].options).toEqual(["--full-name", "--name", "-n"]);
+ });
+
+ it("should handle missing optional properties gracefully", () => {
+ const argGroups = [
+ {
+ args: [
+ {
+ var: "minimal",
+ options: ["--minimal"],
+ type: "string",
+ // Missing optional properties like required, stage, hide, etc.
+ },
+ ],
+ },
+ ];
+
+ const result = DecodeArgs(argGroups);
+
+ expect(result.args).toHaveLength(1);
+ expect(result.args[0]).toMatchObject({
+ var: "minimal",
+ options: ["--minimal"],
+ type: "string",
+ required: false, // Default value
+ stage: "Stable", // Default value
+ hide: false, // Default value
+ group: "", // Default value
+ nullable: false, // Default value
+ });
+ });
+
+ it("should handle blank values for different types", () => {
+ const argGroups = [
+ {
+ args: [
+ {
+ var: "optional_string",
+ options: ["--optional-string"],
+ type: "string",
+ required: false,
+ stage: "Stable",
+ hide: false,
+ group: "",
+ nullable: false,
+ blank: {
+ value: "",
+ },
+ help: {
+ short: "Optional string with blank",
+ },
+ },
+ ],
+ },
+ ];
+
+ const result = DecodeArgs(argGroups);
+
+ expect(result.args).toHaveLength(1);
+ expect(result.args[0]).toMatchObject({
+ var: "optional_string",
+ type: "string",
+ blank: {
+ value: "",
+ },
+ });
+ });
+
+ it("should handle array of different primitive types", () => {
+ const testCases = [
+ { type: "array", itemType: "integer" },
+ { type: "array", itemType: "float" },
+ { type: "array", itemType: "boolean" },
+ ];
+
+ testCases.forEach(({ type, itemType }) => {
+ const argGroups = [
+ {
+ args: [
+ {
+ var: "test_array",
+ options: ["--test-array"],
+ type,
+ required: false,
+ stage: "Stable",
+ hide: false,
+ group: "",
+ nullable: false,
+ item: {
+ type: itemType,
+ nullable: false,
+ },
+ help: {
+ short: `Array of ${itemType}`,
+ },
+ },
+ ],
+ },
+ ];
+
+ const result = DecodeArgs(argGroups);
+
+ expect(result.args).toHaveLength(1);
+ expect(result.args[0]).toMatchObject({
+ var: "test_array",
+ type,
+ required: false,
+ });
+ });
+ });
+
+ it("should handle configuration keys and ID parts", () => {
+ const argGroups = [
+ {
+ args: [
+ {
+ var: "subscription_id",
+ options: ["--subscription"],
+ type: "SubscriptionId",
+ required: false,
+ stage: "Stable",
+ hide: false,
+ group: "",
+ nullable: false,
+ idPart: "subscription",
+ configurationKey: "core.subscription_id",
+ help: {
+ short: "Subscription ID",
+ },
+ },
+ ],
+ },
+ ];
+
+ const result = DecodeArgs(argGroups);
+
+ expect(result.args).toHaveLength(1);
+ expect(result.args[0]).toMatchObject({
+ var: "subscription_id",
+ type: "SubscriptionId",
+ idPart: "subscription",
+ configurationKey: "core.subscription_id",
+ });
+ });
+
+ it("should handle enum extension support", () => {
+ const argGroups = [
+ {
+ args: [
+ {
+ var: "extensible_enum",
+ options: ["--extensible-enum"],
+ type: "string",
+ required: false,
+ stage: "Stable",
+ hide: false,
+ group: "",
+ nullable: false,
+ enum: {
+ items: [
+ { name: "Option1", value: "option1", hide: false },
+ { name: "Option2", value: "option2", hide: false },
+ ],
+ supportExtension: true,
+ },
+ help: {
+ short: "Extensible enum",
+ },
+ },
+ ],
+ },
+ ];
+
+ const result = DecodeArgs(argGroups);
+
+ expect(result.args).toHaveLength(1);
+ expect(result.args[0]).toMatchObject({
+ var: "extensible_enum",
+ type: "string",
+ supportEnumExtension: true,
+ hasEnum: true,
+ });
+ });
+ });
+
+ describe("Edge Cases and Error Handling", () => {
+ it("should handle null and undefined responses gracefully", () => {
+ expect(() => DecodeArgs([])).not.toThrow();
+ expect(() => DecodeArgs([{ args: [] }])).not.toThrow();
+ });
+
+ it("should handle arguments with missing help", () => {
+ const argGroups = [
+ {
+ args: [
+ {
+ var: "no_help",
+ options: ["--no-help"],
+ type: "string",
+ required: false,
+ stage: "Stable",
+ hide: false,
+ group: "",
+ nullable: false,
+ // No help property
+ },
+ ],
+ },
+ ];
+
+ const result = DecodeArgs(argGroups);
+
+ expect(result.args).toHaveLength(1);
+ expect(result.args[0].help).toBeUndefined();
+ });
+
+ it("should handle arguments with missing defaults", () => {
+ const argGroups = [
+ {
+ args: [
+ {
+ var: "no_default",
+ options: ["--no-default"],
+ type: "string",
+ required: false,
+ stage: "Stable",
+ hide: false,
+ group: "",
+ nullable: false,
+ // No default property
+ },
+ ],
+ },
+ ];
+
+ const result = DecodeArgs(argGroups);
+
+ expect(result.args).toHaveLength(1);
+ expect(result.args[0].default).toBeUndefined();
+ });
+
+ it("should handle array arguments without item definition", () => {
+ const argGroups = [
+ {
+ args: [
+ {
+ var: "invalid_array",
+ options: ["--invalid-array"],
+ type: "array",
+ required: false,
+ stage: "Stable",
+ hide: false,
+ group: "",
+ nullable: false,
+ // Missing item property - this should cause an error
+ },
+ ],
+ },
+ ];
+
+ expect(() => DecodeArgs(argGroups)).toThrow("Invalid array object. Item is not defined");
+ });
+
+ it("should handle unknown argument types", () => {
+ const argGroups = [
+ {
+ args: [
+ {
+ var: "unknown_type",
+ options: ["--unknown-type"],
+ type: "unknown_custom_type",
+ required: false,
+ stage: "Stable",
+ hide: false,
+ group: "",
+ nullable: false,
+ },
+ ],
+ },
+ ];
+
+ expect(() => DecodeArgs(argGroups)).toThrow("Unknown type 'unknown_custom_type'");
+ });
+
+ it("should handle dict with any type", () => {
+ const argGroups = [
+ {
+ args: [
+ {
+ var: "any_dict",
+ options: ["--any-dict"],
+ type: "object",
+ required: false,
+ stage: "Stable",
+ hide: false,
+ group: "",
+ nullable: false,
+ additionalProps: {
+ anyType: true,
+ },
+ help: {
+ short: "Dictionary with any value type",
+ },
+ },
+ ],
+ },
+ ];
+
+ const result = DecodeArgs(argGroups);
+
+ expect(result.args).toHaveLength(1);
+ expect(result.args[0]).toMatchObject({
+ var: "any_dict",
+ type: "dict",
+ anyType: true,
+ });
+ });
+ });
+});
diff --git a/src/web/src/__tests__/unit/cli/CLIModGeneratorProfileCommandTree.test.ts b/src/web/src/__tests__/unit/cli/CLIModGeneratorProfileCommandTree.test.ts
new file mode 100644
index 00000000..8166a7fe
--- /dev/null
+++ b/src/web/src/__tests__/unit/cli/CLIModGeneratorProfileCommandTree.test.ts
@@ -0,0 +1,222 @@
+import { describe, it, expect } from "vitest";
+import {
+ ProfileCommandTree,
+ initializeCommandTreeByModView,
+ exportModViewProfile,
+} from "../../../views/cli/utils/commandTreeInitialization";
+import { CLIModViewProfile } from "../../../views/cli/interfaces";
+import { CLISpecsSimpleCommandTree } from "../../../views/cli/components/CLIModuleGenerator";
+
+describe("CLIModGeneratorProfileCommandTree", () => {
+ describe("initializeCommandTreeByModView", () => {
+ it("should initialize command tree with empty profile", () => {
+ const profileName = "test-profile";
+ const view: CLIModViewProfile | null = null;
+ const simpleTree: CLISpecsSimpleCommandTree = {
+ root: {
+ names: ["root"],
+ commands: {},
+ commandGroups: {
+ "test-group": {
+ names: ["test-group"],
+ commands: {
+ "test-command": {
+ names: ["test-group", "test-command"],
+ },
+ },
+ commandGroups: {},
+ },
+ },
+ },
+ };
+
+ const result = initializeCommandTreeByModView(profileName, view, simpleTree);
+
+ expect(result.name).toBe(profileName);
+ expect(result.commandGroups).toBeDefined();
+ expect(result.commandGroups["test-group"]).toBeDefined();
+ expect(result.commandGroups["test-group"].names).toEqual(["test-group"]);
+ expect(result.commandGroups["test-group"].commands).toBeDefined();
+ expect(result.commandGroups["test-group"].commands!["test-command"]).toBeDefined();
+ expect(result.commandGroups["test-group"].commands!["test-command"].names).toEqual([
+ "test-group",
+ "test-command",
+ ]);
+ expect(result.commandGroups["test-group"].commands!["test-command"].selected).toBe(false);
+ });
+
+ it("should initialize command tree with profile view", () => {
+ const profileName = "test-profile";
+ const view: CLIModViewProfile = {
+ name: "test-profile",
+ commandGroups: {
+ "test-group": {
+ names: ["test-group"],
+ commands: {
+ "test-command": {
+ names: ["test-group", "test-command"],
+ version: "2023-01-01",
+ registered: true,
+ modified: false,
+ },
+ },
+ },
+ },
+ };
+ const simpleTree: CLISpecsSimpleCommandTree = {
+ root: {
+ names: ["root"],
+ commands: {},
+ commandGroups: {
+ "test-group": {
+ names: ["test-group"],
+ commands: {
+ "test-command": {
+ names: ["test-group", "test-command"],
+ },
+ },
+ commandGroups: {},
+ },
+ },
+ },
+ };
+
+ const result = initializeCommandTreeByModView(profileName, view, simpleTree);
+
+ expect(result.name).toBe(profileName);
+ expect(result.commandGroups["test-group"].commands!["test-command"].selected).toBe(true);
+ expect(result.commandGroups["test-group"].commands!["test-command"].selectedVersion).toBe("2023-01-01");
+ expect(result.commandGroups["test-group"].commands!["test-command"].registered).toBe(true);
+ });
+
+ it("should throw error for missing command groups in aaz", () => {
+ const profileName = "test-profile";
+ const view: CLIModViewProfile = {
+ name: "test-profile",
+ commandGroups: {
+ "missing-group": {
+ names: ["missing-group"],
+ commands: {},
+ },
+ },
+ };
+ const simpleTree: CLISpecsSimpleCommandTree = {
+ root: {
+ names: ["root"],
+ commands: {},
+ commandGroups: {},
+ },
+ };
+
+ expect(() => {
+ initializeCommandTreeByModView(profileName, view, simpleTree);
+ }).toThrow("Miss command groups in aaz: `az missing-group`");
+ });
+ });
+
+ describe("exportModViewProfile", () => {
+ it("should export profile with selected commands", () => {
+ const tree: ProfileCommandTree = {
+ name: "test-profile",
+ commandGroups: {
+ "test-group": {
+ id: "test-group",
+ names: ["test-group"],
+ commands: {
+ "test-command": {
+ id: "test-group/test-command",
+ names: ["test-group", "test-command"],
+ selected: true,
+ selectedVersion: "2023-01-01",
+ registered: true,
+ modified: false,
+ loading: false,
+ },
+ },
+ loading: false,
+ selected: true,
+ },
+ },
+ };
+
+ const result = exportModViewProfile(tree);
+
+ expect(result.name).toBe("test-profile");
+ expect(result.commandGroups).toBeDefined();
+ expect(result.commandGroups!["test-group"]).toBeDefined();
+ expect(result.commandGroups!["test-group"].names).toEqual(["test-group"]);
+ expect(result.commandGroups!["test-group"].commands!["test-command"]).toBeDefined();
+ expect(result.commandGroups!["test-group"].commands!["test-command"].names).toEqual([
+ "test-group",
+ "test-command",
+ ]);
+ expect(result.commandGroups!["test-group"].commands!["test-command"].version).toBe("2023-01-01");
+ expect(result.commandGroups!["test-group"].commands!["test-command"].registered).toBe(true);
+ expect(result.commandGroups!["test-group"].commands!["test-command"].modified).toBe(false);
+ });
+
+ it("should exclude unselected commands", () => {
+ const tree: ProfileCommandTree = {
+ name: "test-profile",
+ commandGroups: {
+ "test-group": {
+ id: "test-group",
+ names: ["test-group"],
+ commands: {
+ "selected-command": {
+ id: "test-group/selected-command",
+ names: ["test-group", "selected-command"],
+ selected: true,
+ selectedVersion: "2023-01-01",
+ registered: true,
+ modified: false,
+ loading: false,
+ },
+ "unselected-command": {
+ id: "test-group/unselected-command",
+ names: ["test-group", "unselected-command"],
+ selected: false,
+ modified: false,
+ loading: false,
+ },
+ },
+ loading: false,
+ selected: undefined,
+ },
+ },
+ };
+
+ const result = exportModViewProfile(tree);
+
+ expect(result.commandGroups!["test-group"].commands!["selected-command"]).toBeDefined();
+ expect(result.commandGroups!["test-group"].commands!["unselected-command"]).toBeUndefined();
+ });
+
+ it("should exclude command groups marked as false", () => {
+ const tree: ProfileCommandTree = {
+ name: "test-profile",
+ commandGroups: {
+ "selected-group": {
+ id: "selected-group",
+ names: ["selected-group"],
+ commands: {},
+ loading: false,
+ selected: true,
+ },
+ "unselected-group": {
+ id: "unselected-group",
+ names: ["unselected-group"],
+ commands: {},
+ loading: false,
+ selected: false,
+ },
+ },
+ };
+
+ const result = exportModViewProfile(tree);
+
+ expect(result.commandGroups!["selected-group"]).toBeDefined();
+ expect(result.commandGroups!["unselected-group"]).toBeUndefined();
+ });
+ });
+});
diff --git a/src/web/src/__tests__/unit/convertArgDefaultText.test.ts b/src/web/src/__tests__/unit/convertArgDefaultText.test.ts
new file mode 100644
index 00000000..56deba5f
--- /dev/null
+++ b/src/web/src/__tests__/unit/convertArgDefaultText.test.ts
@@ -0,0 +1,189 @@
+import { describe, it, expect } from "vitest";
+import { convertArgDefaultText, type SupportedArgType } from "../../views/workspace/utils/convertArgDefaultText";
+
+describe("convertArgDefaultText", () => {
+ describe("String types", () => {
+ const stringTypes: SupportedArgType[] = [
+ "byte",
+ "binary",
+ "duration",
+ "date",
+ "dateTime",
+ "time",
+ "uuid",
+ "password",
+ "SubscriptionId",
+ "ResourceGroupName",
+ "ResourceId",
+ "ResourceLocation",
+ "string",
+ ];
+
+ stringTypes.forEach((type) => {
+ it(`should convert valid ${type} values`, () => {
+ expect(convertArgDefaultText(" hello world ", type)).toBe("hello world");
+ expect(convertArgDefaultText("test-value", type)).toBe("test-value");
+ });
+
+ it(`should throw error for empty ${type} values`, () => {
+ expect(() => convertArgDefaultText("", type)).toThrow(`Not supported empty value: ''`);
+ expect(() => convertArgDefaultText(" ", type)).toThrow(`Not supported empty value: ' '`);
+ });
+ });
+ });
+
+ describe("Integer types", () => {
+ const integerTypes: SupportedArgType[] = ["integer32", "integer64", "integer"];
+
+ integerTypes.forEach((type) => {
+ it(`should convert valid ${type} values`, () => {
+ expect(convertArgDefaultText("42", type)).toBe(42);
+ expect(convertArgDefaultText(" 123 ", type)).toBe(123);
+ expect(convertArgDefaultText("-456", type)).toBe(-456);
+ expect(convertArgDefaultText("0", type)).toBe(0);
+ });
+
+ it(`should throw error for invalid ${type} values`, () => {
+ expect(() => convertArgDefaultText("abc", type)).toThrow(`Not supported default value for integer type: 'abc'`);
+ expect(() => convertArgDefaultText("12.34", type)).toThrow(
+ `Not supported default value for integer type: '12.34'`,
+ );
+ expect(() => convertArgDefaultText("", type)).toThrow(`Not supported default value for integer type: ''`);
+ });
+ });
+ });
+
+ describe("Float types", () => {
+ const floatTypes: SupportedArgType[] = ["float32", "float64", "float"];
+
+ floatTypes.forEach((type) => {
+ it(`should convert valid ${type} values`, () => {
+ expect(convertArgDefaultText("42.5", type)).toBe(42.5);
+ expect(convertArgDefaultText(" 123.456 ", type)).toBe(123.456);
+ expect(convertArgDefaultText("-456.789", type)).toBe(-456.789);
+ expect(convertArgDefaultText("0.0", type)).toBe(0.0);
+ expect(convertArgDefaultText("42", type)).toBe(42);
+ });
+
+ it(`should throw error for invalid ${type} values`, () => {
+ expect(() => convertArgDefaultText("abc", type)).toThrow(`Not supported default value for float type: 'abc'`);
+ expect(() => convertArgDefaultText("", type)).toThrow(`Not supported default value for float type: ''`);
+ });
+ });
+ });
+
+ describe("Boolean type", () => {
+ it("should convert true values", () => {
+ expect(convertArgDefaultText("true", "boolean")).toBe(true);
+ expect(convertArgDefaultText("TRUE", "boolean")).toBe(true);
+ expect(convertArgDefaultText(" True ", "boolean")).toBe(true);
+ expect(convertArgDefaultText("yes", "boolean")).toBe(true);
+ expect(convertArgDefaultText("YES", "boolean")).toBe(true);
+ });
+
+ it("should convert false values", () => {
+ expect(convertArgDefaultText("false", "boolean")).toBe(false);
+ expect(convertArgDefaultText("FALSE", "boolean")).toBe(false);
+ expect(convertArgDefaultText(" False ", "boolean")).toBe(false);
+ expect(convertArgDefaultText("no", "boolean")).toBe(false);
+ expect(convertArgDefaultText("NO", "boolean")).toBe(false);
+ });
+
+ it("should throw error for invalid boolean values", () => {
+ expect(() => convertArgDefaultText("maybe", "boolean")).toThrow(
+ `Not supported default value for boolean type: 'maybe'`,
+ );
+ expect(() => convertArgDefaultText("1", "boolean")).toThrow(`Not supported default value for boolean type: '1'`);
+ expect(() => convertArgDefaultText("", "boolean")).toThrow(`Not supported default value for boolean type: ''`);
+ });
+ });
+
+ describe("Any type", () => {
+ it("should convert integer values", () => {
+ expect(convertArgDefaultText("42", "any")).toBe(42);
+ expect(convertArgDefaultText("-123", "any")).toBe(-123);
+ });
+
+ it("should convert float values", () => {
+ expect(convertArgDefaultText("42.5", "any")).toBe(42.5);
+ expect(convertArgDefaultText("-123.456", "any")).toBe(-123.456);
+ });
+
+ it("should convert null values", () => {
+ expect(convertArgDefaultText("null", "any")).toBe(null);
+ expect(convertArgDefaultText("NULL", "any")).toBe(null);
+ });
+
+ it("should convert boolean values", () => {
+ expect(convertArgDefaultText("true", "any")).toBe(true);
+ expect(convertArgDefaultText("false", "any")).toBe(false);
+ expect(convertArgDefaultText("yes", "any")).toBe(true);
+ expect(convertArgDefaultText("no", "any")).toBe(false);
+ });
+
+ it("should convert string values as fallback", () => {
+ expect(convertArgDefaultText("hello", "any")).toBe("hello");
+ expect(convertArgDefaultText("some text", "any")).toBe("some text");
+ });
+ });
+
+ describe("Object type", () => {
+ it("should parse valid JSON objects", () => {
+ expect(convertArgDefaultText('{"key": "value"}', "object")).toEqual({ key: "value" });
+ expect(convertArgDefaultText('{"number": 42, "boolean": true}', "object")).toEqual({ number: 42, boolean: true });
+ expect(convertArgDefaultText(' {"nested": {"key": "value"}} ', "object")).toEqual({ nested: { key: "value" } });
+ });
+
+ it("should throw error for invalid JSON", () => {
+ expect(() => convertArgDefaultText("{invalid json}", "object")).toThrow();
+ expect(() => convertArgDefaultText("not json at all", "object")).toThrow();
+ });
+ });
+
+ describe("Array types", () => {
+ it("should parse valid JSON arrays for array types", () => {
+ expect(convertArgDefaultText("[1, 2, 3]", "array")).toEqual([1, 2, 3]);
+ expect(convertArgDefaultText('["a", "b", "c"]', "array")).toEqual(["a", "b", "c"]);
+ expect(convertArgDefaultText(" [] ", "array")).toEqual([]);
+ });
+
+ it("should throw error for invalid JSON arrays", () => {
+ expect(() => convertArgDefaultText("[invalid, json]", "array")).toThrow();
+ expect(() => convertArgDefaultText("not an array", "array")).toThrow();
+ });
+ });
+
+ describe("Dictionary types", () => {
+ it("should parse valid JSON objects for dict types", () => {
+ expect(convertArgDefaultText('{"key1": "value1"}', "dict")).toEqual({ key1: "value1" });
+ expect(convertArgDefaultText('{"a": 1, "b": 2}', "dict")).toEqual({ a: 1, b: 2 });
+ expect(convertArgDefaultText(" {} ", "dict")).toEqual({});
+ });
+
+ it("should throw error for invalid JSON dictionaries", () => {
+ expect(() => convertArgDefaultText("{invalid: json}", "dict")).toThrow();
+ expect(() => convertArgDefaultText("not a dict", "dict")).toThrow();
+ });
+ });
+
+ describe("Unsupported types", () => {
+ it("should throw error for unsupported types", () => {
+ expect(() => convertArgDefaultText("value", "unsupported")).toThrow("Not supported type: unsupported");
+ expect(() => convertArgDefaultText("value", "custom-type")).toThrow("Not supported type: custom-type");
+ });
+ });
+
+ describe("Edge cases", () => {
+ it("should handle whitespace correctly", () => {
+ expect(convertArgDefaultText(" value ", "string")).toBe("value");
+ expect(convertArgDefaultText(" 42 ", "integer")).toBe(42);
+ expect(convertArgDefaultText(" true ", "boolean")).toBe(true);
+ });
+
+ it("should handle case sensitivity for any type", () => {
+ expect(convertArgDefaultText("TRUE", "any")).toBe(true);
+ expect(convertArgDefaultText("FALSE", "any")).toBe(false);
+ expect(convertArgDefaultText("NULL", "any")).toBe(null);
+ });
+ });
+});
diff --git a/src/web/src/__tests__/unit/decodeResponseCommand.test.ts b/src/web/src/__tests__/unit/decodeResponseCommand.test.ts
new file mode 100644
index 00000000..f6a431b0
--- /dev/null
+++ b/src/web/src/__tests__/unit/decodeResponseCommand.test.ts
@@ -0,0 +1,160 @@
+import { describe, it, expect } from "vitest";
+import { DecodeResponseCommand } from "../../views/workspace/utils/decodeResponseCommand";
+import type { ResponseCommand } from "../../views/workspace/interfaces";
+
+describe("DecodeResponseCommand", () => {
+ it("should decode basic response command", () => {
+ const responseCommand: ResponseCommand = {
+ names: ["group", "command"],
+ help: {
+ short: "Test command",
+ lines: ["This is a test command", "With multiple lines"],
+ },
+ stage: "Stable",
+ version: "1.0.0",
+ resources: [
+ {
+ id: "test-resource",
+ version: "1.0.0",
+ swagger: "https://example.com/swagger",
+ },
+ ],
+ };
+
+ const result = DecodeResponseCommand(responseCommand);
+
+ expect(result).toEqual({
+ id: "command:group/command",
+ names: ["group", "command"],
+ help: {
+ short: "Test command",
+ lines: ["This is a test command", "With multiple lines"],
+ },
+ stage: "Stable",
+ version: "1.0.0",
+ resources: [
+ {
+ id: "test-resource",
+ version: "1.0.0",
+ swagger: "https://example.com/swagger",
+ },
+ ],
+ });
+ });
+
+ it("should handle default stage when not provided", () => {
+ const responseCommand: ResponseCommand = {
+ names: ["test"],
+ version: "1.0.0",
+ resources: [],
+ };
+
+ const result = DecodeResponseCommand(responseCommand);
+
+ expect(result.stage).toBe("Stable");
+ expect(result.id).toBe("command:test");
+ });
+
+ it("should handle confirmation message", () => {
+ const responseCommand: ResponseCommand = {
+ names: ["test"],
+ version: "1.0.0",
+ resources: [],
+ confirmation: "Are you sure?",
+ };
+
+ const result = DecodeResponseCommand(responseCommand);
+
+ expect(result.confirmation).toBe("Are you sure?");
+ });
+
+ it("should decode arguments when argGroups are provided", () => {
+ const responseCommand: ResponseCommand = {
+ names: ["test"],
+ version: "1.0.0",
+ resources: [],
+ argGroups: [
+ {
+ args: [
+ {
+ var: "name",
+ options: ["--name", "-n"],
+ type: "string",
+ required: true,
+ stage: "Stable",
+ hide: false,
+ group: "",
+ nullable: false,
+ help: {
+ short: "Resource name",
+ },
+ },
+ ],
+ },
+ ],
+ };
+
+ const result = DecodeResponseCommand(responseCommand);
+
+ expect(result.args).toBeDefined();
+ expect(result.args).toHaveLength(1);
+ expect(result.args![0].var).toBe("name");
+ expect(result.clsArgDefineMap).toBeDefined();
+ });
+
+ it("should handle optional properties correctly", () => {
+ const responseCommand: ResponseCommand = {
+ names: ["test"],
+ version: "1.0.0",
+ resources: [],
+ examples: [
+ {
+ name: "Example 1",
+ commands: ["az test --name myname"],
+ },
+ ],
+ outputs: [
+ {
+ type: "string",
+ ref: "#/definitions/TestOutput",
+ value: "result",
+ },
+ ],
+ };
+
+ const result = DecodeResponseCommand(responseCommand);
+
+ expect(result.examples).toEqual([
+ {
+ name: "Example 1",
+ commands: ["az test --name myname"],
+ },
+ ]);
+ expect(result.outputs).toEqual([
+ {
+ type: "string",
+ ref: "#/definitions/TestOutput",
+ value: "result",
+ },
+ ]);
+ });
+
+ it("should generate correct command ID from names", () => {
+ const testCases = [
+ { names: ["single"], expected: "command:single" },
+ { names: ["group", "command"], expected: "command:group/command" },
+ { names: ["a", "b", "c", "d"], expected: "command:a/b/c/d" },
+ ];
+
+ testCases.forEach(({ names, expected }) => {
+ const responseCommand: ResponseCommand = {
+ names,
+ version: "1.0.0",
+ resources: [],
+ };
+
+ const result = DecodeResponseCommand(responseCommand);
+ expect(result.id).toBe(expected);
+ });
+ });
+});
diff --git a/src/web/src/__tests__/unit/spliceArgOptionsString.test.ts b/src/web/src/__tests__/unit/spliceArgOptionsString.test.ts
new file mode 100644
index 00000000..6734a708
--- /dev/null
+++ b/src/web/src/__tests__/unit/spliceArgOptionsString.test.ts
@@ -0,0 +1,200 @@
+import { spliceArgOptionsString } from "../../views/workspace/utils/spliceArgOptionsString";
+
+const createMockArg = (overrides: any = {}): any => ({
+ options: [],
+ type: "string",
+ var: "test",
+ group: "",
+ required: false,
+ hide: false,
+ stage: "Stable" as const,
+ nullable: false,
+ ...overrides,
+});
+
+describe("spliceArgOptionsString", () => {
+ describe("Basic option formatting", () => {
+ it("should format single character options with single dash at depth 0", () => {
+ const arg = createMockArg({
+ options: ["v", "h"],
+ });
+
+ const result = spliceArgOptionsString(arg, 0);
+ expect(result).toBe("-v -h");
+ });
+
+ it("should format multi-character options with double dash at depth 0", () => {
+ const arg = createMockArg({
+ options: ["verbose", "help"],
+ });
+
+ const result = spliceArgOptionsString(arg, 0);
+ expect(result).toBe("--verbose --help");
+ });
+
+ it("should format mixed single and multi-character options at depth 0", () => {
+ const arg = createMockArg({
+ options: ["v", "verbose", "h", "help"],
+ });
+
+ const result = spliceArgOptionsString(arg, 0);
+ expect(result).toBe("-v --verbose -h --help");
+ });
+
+ it("should format options with dot prefix at depth > 0", () => {
+ const arg = createMockArg({
+ options: ["property", "prop"],
+ });
+
+ const result = spliceArgOptionsString(arg, 1);
+ expect(result).toBe(".property .prop");
+ });
+ });
+
+ describe("Array arguments with singular options", () => {
+ it("should append singular options for array arguments at depth 0", () => {
+ const arg = createMockArg({
+ options: ["items"],
+ type: "array",
+ item: { type: "string" },
+ singularOptions: ["item", "i"],
+ });
+
+ const result = spliceArgOptionsString(arg, 0);
+ expect(result).toBe("--items (--item -i)");
+ });
+
+ it("should append singular options for array arguments at depth > 0", () => {
+ const arg = createMockArg({
+ options: ["items"],
+ type: "array",
+ item: { type: "string" },
+ singularOptions: ["item"],
+ });
+
+ const result = spliceArgOptionsString(arg, 1);
+ expect(result).toBe(".items (.item)");
+ });
+
+ it("should handle array arguments without singular options", () => {
+ const arg = createMockArg({
+ options: ["items"],
+ type: "array",
+ item: { type: "string" },
+ });
+
+ const result = spliceArgOptionsString(arg, 0);
+ expect(result).toBe("--items");
+ });
+ });
+
+ describe("Class arguments with singular options", () => {
+ it("should append singular options for class arguments at depth 0", () => {
+ const arg = createMockArg({
+ options: ["configs"],
+ type: "@Config",
+ clsName: "Config",
+ singularOptions: ["config", "c"],
+ });
+
+ const result = spliceArgOptionsString(arg, 0);
+ expect(result).toBe("--configs (--config -c)");
+ });
+
+ it("should append singular options for class arguments at depth > 0", () => {
+ const arg = createMockArg({
+ options: ["configs"],
+ type: "@Config",
+ clsName: "Config",
+ singularOptions: ["config"],
+ });
+
+ const result = spliceArgOptionsString(arg, 2);
+ expect(result).toBe(".configs (.config)");
+ });
+
+ it("should handle class arguments without singular options", () => {
+ const arg = createMockArg({
+ options: ["config"],
+ type: "@Config",
+ clsName: "Config",
+ });
+
+ const result = spliceArgOptionsString(arg, 0);
+ expect(result).toBe("--config");
+ });
+ });
+
+ describe("Edge cases", () => {
+ it("should handle empty options array", () => {
+ const arg = createMockArg({
+ options: [],
+ });
+
+ const result = spliceArgOptionsString(arg, 0);
+ expect(result).toBe("");
+ });
+
+ it("should handle single option", () => {
+ const arg = createMockArg({
+ options: ["single"],
+ });
+
+ const result = spliceArgOptionsString(arg, 0);
+ expect(result).toBe("--single");
+ });
+
+ it("should handle different depths", () => {
+ const arg = createMockArg({
+ options: ["test"],
+ });
+
+ expect(spliceArgOptionsString(arg, 0)).toBe("--test");
+ expect(spliceArgOptionsString(arg, 1)).toBe(".test");
+ expect(spliceArgOptionsString(arg, 5)).toBe(".test");
+ });
+
+ it("should handle both array and class singular options pattern correctly", () => {
+ const argWithBothPatterns = createMockArg({
+ options: ["items"],
+ type: "array<@Config>",
+ item: { type: "@Config" },
+ singularOptions: ["item"],
+ clsName: "Config",
+ });
+
+ const result = spliceArgOptionsString(argWithBothPatterns, 0);
+ expect(result).toBe("--items (--item)");
+ });
+ });
+
+ describe("Complex scenarios", () => {
+ it("should handle complex array argument with mixed option lengths", () => {
+ const arg = createMockArg({
+ options: ["resource-groups", "rg"],
+ type: "array",
+ var: "resourceGroups",
+ required: true,
+ item: { type: "string" },
+ singularOptions: ["resource-group", "g"],
+ });
+
+ const result = spliceArgOptionsString(arg, 0);
+ expect(result).toBe("--resource-groups --rg (--resource-group -g)");
+ });
+
+ it("should handle nested class argument with singular options", () => {
+ const arg = createMockArg({
+ options: ["configurations"],
+ type: "@Configuration",
+ var: "configs",
+ group: "advanced",
+ clsName: "Configuration",
+ singularOptions: ["configuration", "config", "c"],
+ });
+
+ const result = spliceArgOptionsString(arg, 1);
+ expect(result).toBe(".configurations (.configuration .config .c)");
+ });
+ });
+});
diff --git a/src/web/src/components/AppAppBar.tsx b/src/web/src/components/AppAppBar.tsx
deleted file mode 100644
index 943156d0..00000000
--- a/src/web/src/components/AppAppBar.tsx
+++ /dev/null
@@ -1,134 +0,0 @@
-import * as React from "react";
-import Box from "@mui/material/Box";
-import Link from "@mui/material/Link";
-import { AppBar, Toolbar } from "@mui/material";
-import theme from "../theme";
-import Button from "@mui/material/Button";
-import Menu from "@mui/material/Menu";
-import MenuItem from "@mui/material/MenuItem";
-
-type AppAppBarProps = {
- pageName: string | null;
-};
-
-type AppAppBarState = {
- anchorEl: null | HTMLElement;
-};
-
-class AppAppBar extends React.Component {
- constructor(props: AppAppBarProps) {
- super(props);
- this.state = {
- anchorEl: null,
- };
- }
-
- handleMenuOpen = (event: React.MouseEvent) => {
- this.setState({ anchorEl: event.currentTarget });
- };
-
- handleMenuClose = () => {
- this.setState({ anchorEl: null });
- };
-
- render() {
- const { anchorEl } = this.state;
-
- return (
-
-
-
-
-
- {"Home"}
-
-
-
- {"Workspace"}
-
- {/*
-
- {'Commands'}
- */}
-
-
- {"CLI"}
-
-
-
-
-
-
-
-
- );
- }
-}
-
-export { AppAppBar };
diff --git a/src/web/src/components/AppNavBar.tsx b/src/web/src/components/AppNavBar.tsx
new file mode 100644
index 00000000..699692ee
--- /dev/null
+++ b/src/web/src/components/AppNavBar.tsx
@@ -0,0 +1,101 @@
+import React, { useState } from "react";
+import Box from "@mui/material/Box";
+import Link from "@mui/material/Link";
+import { AppBar, Toolbar } from "@mui/material";
+import theme from "../theme";
+import Button from "@mui/material/Button";
+import Menu from "@mui/material/Menu";
+import MenuItem from "@mui/material/MenuItem";
+
+type AppNavBarProps = {
+ pageName: string | null;
+};
+
+const AppNavBar: React.FC = ({ pageName }) => {
+ const [anchorEl, setAnchorEl] = useState(null);
+
+ const handleMenuOpen = (event: React.MouseEvent) => {
+ setAnchorEl(event.currentTarget);
+ };
+
+ const handleMenuClose = () => {
+ setAnchorEl(null);
+ };
+
+ return (
+
+
+
+
+
+ Home
+
+
+
+ Workspace
+
+
+
+ CLI
+
+
+
+
+
+
+
+
+ );
+};
+
+export { AppNavBar };
diff --git a/src/web/src/components/EditorPageLayout.tsx b/src/web/src/components/EditorPageLayout.tsx
index a52b22c3..c1db759d 100644
--- a/src/web/src/components/EditorPageLayout.tsx
+++ b/src/web/src/components/EditorPageLayout.tsx
@@ -1,5 +1,5 @@
+import React from "react";
import { styled, Box } from "@mui/material";
-import * as React from "react";
const PageContainer = styled(Box)(({ theme }) => ({
color: theme.palette.common.white,
@@ -25,13 +25,13 @@ const Background = styled(Box)({
zIndex: -2,
});
-export default function EditorPageLayout(props: React.HTMLAttributes) {
- const { children } = props;
-
+const EditorPageLayout: React.FC> = ({ children }) => {
return (
-
+ <>
{children}
-
+ >
);
-}
+};
+
+export default EditorPageLayout;
diff --git a/src/web/src/components/PageLayout.tsx b/src/web/src/components/PageLayout.tsx
index 2e551ea7..8d233720 100644
--- a/src/web/src/components/PageLayout.tsx
+++ b/src/web/src/components/PageLayout.tsx
@@ -1,5 +1,5 @@
+import React from "react";
import { Container, styled, Box } from "@mui/material";
-import * as React from "react";
const PageContainer = styled(Container)(({ theme }) => ({
color: theme.palette.common.white,
@@ -25,13 +25,13 @@ const Background = styled(Box)({
zIndex: -2,
});
-export default function PageLayout(props: React.HTMLAttributes) {
- const { children } = props;
-
+const PageLayout: React.FC> = ({ children }) => {
return (
-
+ <>
{children}
-
+ >
);
-}
+};
+
+export default PageLayout;
diff --git a/src/web/src/constants/index.ts b/src/web/src/constants/index.ts
new file mode 100644
index 00000000..3aa23a61
--- /dev/null
+++ b/src/web/src/constants/index.ts
@@ -0,0 +1,4 @@
+/**
+ * The command prefix for Azure CLI commands
+ */
+export const COMMAND_PREFIX = "az ";
diff --git a/src/web/src/index.tsx b/src/web/src/index.tsx
index 8804c0cd..9cd4fb99 100644
--- a/src/web/src/index.tsx
+++ b/src/web/src/index.tsx
@@ -1,44 +1,43 @@
import React from "react";
-import ReactDOM from "react-dom";
+import { createRoot } from "react-dom/client";
import { HashRouter, Routes, Route } from "react-router-dom";
+import { ThemeProvider } from "@mui/material/styles";
+import CssBaseline from "@mui/material/CssBaseline";
import "./index.css";
import App from "./App";
import HomePage from "./views/home/HomePage";
-import WorkspacePage from "./views/workspace/WorkspacePage";
-import WorkspaceInstruction from "./views/workspace/WorkspaceInstruction";
-import { WSEditor } from "./views/workspace/WSEditor";
-import CommandsPage from "./views/commands/CommandsPage";
-import CLIPage from "./views/cli/CLIPage";
-import CLIInstruction from "./views/cli/CLIInstruction";
-import { CLIModuleGenerator } from "./views/cli/CLIModuleGenerator";
-// import reportWebVitals from './reportWebVitals';
+import WorkspacePage from "./views/workspace/components/WorkspacePage";
+import WorkspaceInstruction from "./views/workspace/components/WorkspaceInstruction";
+import { WSEditor } from "./views/workspace/components/WSEditor";
+import CLIPage from "./views/cli/components/CLIPage";
+import CLIInstruction from "./views/cli/components/CLIInstruction";
+import { CLIModuleGenerator } from "./views/cli/components/CLIModuleGenerator";
+import theme from "./theme";
-ReactDOM.render(
+const container = document.getElementById("root");
+const root = createRoot(container!);
+root.render(
-
-
- }>
- } />
- } />
- }>
- } />
- } />
- } />
+
+
+
+
+ }>
+ } />
+ } />
+ }>
+ } />
+ } />
+ } />
+
+ }>
+ } />
+ }>
+ } />
+
- }>
- }>
- } />
- }>
- } />
-
-
-
-
+
+
+
,
- document.getElementById("root"),
);
-
-// If you want to start measuring performance in your app, pass a function
-// to log results (for example: reportWebVitals(console.log))
-// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
-// reportWebVitals();
diff --git a/src/web/src/logo.svg b/src/web/src/logo.svg
deleted file mode 100644
index 9dfc1c05..00000000
--- a/src/web/src/logo.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/web/src/react-app-env.d.ts b/src/web/src/react-app-env.d.ts
deleted file mode 100644
index 6431bc5f..00000000
--- a/src/web/src/react-app-env.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-///
diff --git a/src/web/src/reportWebVitals.js b/src/web/src/reportWebVitals.js
deleted file mode 100644
index 9ecd33f9..00000000
--- a/src/web/src/reportWebVitals.js
+++ /dev/null
@@ -1,13 +0,0 @@
-const reportWebVitals = (onPerfEntry) => {
- if (onPerfEntry && onPerfEntry instanceof Function) {
- import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
- getCLS(onPerfEntry);
- getFID(onPerfEntry);
- getFCP(onPerfEntry);
- getLCP(onPerfEntry);
- getTTFB(onPerfEntry);
- });
- }
-};
-
-export default reportWebVitals;
diff --git a/src/web/src/setupTests.js b/src/web/src/setupTests.js
deleted file mode 100644
index 1dd407a6..00000000
--- a/src/web/src/setupTests.js
+++ /dev/null
@@ -1,5 +0,0 @@
-// jest-dom adds custom jest matchers for asserting on DOM nodes.
-// allows you to do things like:
-// expect(element).toHaveTextContent(/react/i)
-// learn more: https://github.com/testing-library/jest-dom
-import "@testing-library/jest-dom";
diff --git a/src/web/src/views/cli/CLIInstruction.tsx b/src/web/src/views/cli/CLIInstruction.tsx
deleted file mode 100644
index 37954e73..00000000
--- a/src/web/src/views/cli/CLIInstruction.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import * as React from "react";
-import { Typography, Box } from "@mui/material";
-import { styled } from "@mui/material/styles";
-import CLIModuleSelector from "./CLIModuleSelector";
-import { AppAppBar } from "../../components/AppAppBar";
-import PageLayout from "../../components/PageLayout";
-
-const MiddlePadding = styled(Box)(() => ({
- height: "6vh",
-}));
-
-const SpacePadding = styled(Box)(() => ({
- width: "3vh",
-}));
-
-class CLIInstruction extends React.Component {
- render() {
- return (
-
-
-
-
-
-
-
- Please select a CLI Module
-
-
-
-
-
-
- Or
-
-
-
-
-
-
-
-
-
- );
- }
-}
-
-export default CLIInstruction;
diff --git a/src/web/src/views/cli/CLIModGeneratorProfileCommandTree.tsx b/src/web/src/views/cli/CLIModGeneratorProfileCommandTree.tsx
deleted file mode 100644
index 39f2f1b0..00000000
--- a/src/web/src/views/cli/CLIModGeneratorProfileCommandTree.tsx
+++ /dev/null
@@ -1,975 +0,0 @@
-import * as React from "react";
-import TreeView from "@mui/lab/TreeView";
-import TreeItem from "@mui/lab/TreeItem";
-
-import ArrowRightIcon from "@mui/icons-material/ArrowRight";
-import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
-import FolderIcon from "@mui/icons-material/Folder";
-import EditIcon from "@mui/icons-material/Edit";
-import {
- Box,
- Checkbox,
- FormControl,
- Typography,
- Select,
- MenuItem,
- styled,
- TypographyProps,
- InputLabel,
- IconButton,
-} from "@mui/material";
-import {
- CLIModViewCommand,
- CLIModViewCommandGroup,
- CLIModViewCommandGroups,
- CLIModViewCommands,
- CLIModViewProfile,
-} from "./CLIModuleCommon";
-import {
- CLISpecsCommand,
- CLISpecsCommandGroup,
- CLISpecsSimpleCommand,
- CLISpecsSimpleCommandGroup,
- CLISpecsSimpleCommandTree,
-} from "./CLIModuleGenerator";
-
-const CommandGroupTypography = styled(Typography)(({ theme }) => ({
- color: theme.palette.primary.main,
- fontFamily: "'Work Sans', sans-serif",
- fontSize: 17,
- fontWeight: 600,
-}));
-
-const CommandTypography = styled(Typography)(({ theme }) => ({
- color: theme.palette.primary.main,
- fontFamily: "'Work Sans', sans-serif",
- fontSize: 20,
- fontWeight: 400,
-}));
-
-const SelectionTypography = styled(Typography)(({ theme }) => ({
- color: theme.palette.grey[700],
- fontFamily: "'Work Sans', sans-serif",
- fontSize: 15,
- fontWeight: 400,
-}));
-
-const RegisteredTypography = styled(SelectionTypography)(() => ({}));
-
-const UnregisteredTypography = styled(SelectionTypography)(() => ({
- color: "#d9c136",
-}));
-
-interface CommandItemProps {
- command: ProfileCTCommand;
- onUpdateCommand: (name: string, updater: (oldCommand: ProfileCTCommand) => ProfileCTCommand) => void;
- onLoadCommand(names: string[]): Promise;
-}
-
-const CommandItem: React.FC = React.memo(({ command, onUpdateCommand, onLoadCommand }) => {
- const leafName = command.names[command.names.length - 1];
-
- const selectCommand = React.useCallback(
- (selected: boolean) => {
- onUpdateCommand(leafName, (oldCommand) => {
- if (oldCommand.versions === undefined && selected === true) {
- onLoadCommand(oldCommand.names);
- }
- return {
- ...oldCommand,
- loading: selected && oldCommand.versions === undefined,
- selected: selected,
- selectedVersion: selected
- ? oldCommand.selectedVersion
- ? oldCommand.selectedVersion
- : oldCommand.versions
- ? oldCommand.versions[0].name
- : undefined
- : oldCommand.selectedVersion,
- modified: true,
- };
- });
- },
- [onUpdateCommand, onLoadCommand],
- );
-
- const selectVersion = React.useCallback(
- (version: string) => {
- onUpdateCommand(leafName, (oldCommand) => {
- return {
- ...oldCommand,
- selectedVersion: version,
- modified: true,
- };
- });
- },
- [onUpdateCommand],
- );
-
- const selectRegistered = React.useCallback(
- (registered: boolean) => {
- onUpdateCommand(leafName, (oldCommand) => {
- return {
- ...oldCommand,
- registered: registered,
- modified: true,
- };
- });
- },
- [onUpdateCommand],
- );
-
- return (
-
- {
- selectCommand(!command.selected);
- event.stopPropagation();
- event.preventDefault();
- }}
- />
-
- {leafName}
-
- {!command.modified && command.selectedVersion !== undefined && (
- {
- selectCommand(true);
- }}
- >
-
-
- )}
- {command.modified && }
-
-
- {command.versions !== undefined && command.selectedVersion !== undefined && (
-
-
- Version
-
-
-
- Command table
-
-
-
- )}
- {command.loading === true && command.selected && (
-
-
- Loading...
-
-
- )}
-
- }
- onClick={(event) => {
- event.stopPropagation();
- event.preventDefault();
- }}
- />
- );
-});
-
-interface CommandGroupItemProps {
- commandGroup: ProfileCTCommandGroup;
- onUpdateCommandGroup: (
- name: string,
- updater: (oldCommandGroup: ProfileCTCommandGroup) => ProfileCTCommandGroup,
- ) => void;
- onLoadCommands: (names: string[][]) => Promise;
-}
-
-const CommandGroupItem: React.FC = React.memo(
- ({ commandGroup, onUpdateCommandGroup, onLoadCommands }) => {
- const nodeName = commandGroup.names[commandGroup.names.length - 1];
- const selected = commandGroup.selected ?? false;
-
- const onUpdateCommand = React.useCallback(
- (name: string, updater: (oldCommand: ProfileCTCommand) => ProfileCTCommand) => {
- onUpdateCommandGroup(nodeName, (oldCommandGroup) => {
- const commands = {
- ...oldCommandGroup.commands,
- [name]: updater(oldCommandGroup.commands![name]),
- };
- const selected = calculateSelected(commands, oldCommandGroup.commandGroups ?? {});
- return {
- ...oldCommandGroup,
- commands: commands,
- selected: selected,
- };
- });
- },
- [onUpdateCommandGroup],
- );
-
- const onUpdateSubCommandGroup = React.useCallback(
- (name: string, updater: (oldCommandGroup: ProfileCTCommandGroup) => ProfileCTCommandGroup) => {
- onUpdateCommandGroup(nodeName, (oldCommandGroup) => {
- const commandGroups = {
- ...oldCommandGroup.commandGroups,
- [name]: updater(oldCommandGroup.commandGroups![name]),
- };
- const commands = oldCommandGroup.commands;
- const selected = calculateSelected(commands ?? {}, commandGroups);
- return {
- ...oldCommandGroup,
- commandGroups: commandGroups,
- selected: selected,
- };
- });
- },
- [onUpdateCommandGroup],
- );
-
- const onLoadCommand = React.useCallback(
- async (names: string[]) => {
- await onLoadCommands([names]);
- },
- [onLoadCommands],
- );
-
- const updateCommandSelected = (command: ProfileCTCommand, selected: boolean): ProfileCTCommand => {
- if (selected === command.selected) {
- return command;
- }
- return {
- ...command,
- selected: selected,
- selectedVersion: selected
- ? command.selectedVersion
- ? command.selectedVersion
- : command.versions
- ? command.versions[0].name
- : undefined
- : command.selectedVersion,
- modified: true,
- };
- };
-
- const updateGroupSelected = (group: ProfileCTCommandGroup, selected: boolean): ProfileCTCommandGroup => {
- if (selected === group.selected) {
- return group;
- }
- const commands = group.commands
- ? Object.fromEntries(
- Object.entries(group.commands).map(([key, value]) => [key, updateCommandSelected(value, selected)]),
- )
- : undefined;
- const commandGroups = group.commandGroups
- ? Object.fromEntries(
- Object.entries(group.commandGroups).map(([key, value]) => [key, updateGroupSelected(value, selected)]),
- )
- : undefined;
- return {
- ...group,
- commands: commands,
- commandGroups: commandGroups,
- selected: selected,
- };
- };
-
- const selectCommandGroup = React.useCallback(
- (selected: boolean) => {
- onUpdateCommandGroup(nodeName, (oldCommandGroup) => {
- const selectedGroup = updateGroupSelected(oldCommandGroup, selected);
- const [loadingNamesList, newGroup] = prepareLoadCommandsOfCommandGroup(selectedGroup);
- if (loadingNamesList.length > 0) {
- onLoadCommands(loadingNamesList);
- }
- return newGroup;
- });
- },
- [onUpdateCommandGroup, onLoadCommands],
- );
-
- return (
-
- {
- selectCommandGroup(!selected);
- event.stopPropagation();
- event.preventDefault();
- }}
- />
-
- {nodeName}
-
- }
- >
- {commandGroup.commands !== undefined &&
- Object.values(commandGroup.commands).map((command) => (
-
- ))}
- {commandGroup.commandGroups !== undefined &&
- Object.values(commandGroup.commandGroups).map((group) => (
-
- ))}
-
- );
- },
-);
-
-interface CLIModGeneratorProfileCommandTreeProps {
- profile?: string;
- profileCommandTree: ProfileCommandTree;
- onChange: (updater: ((oldProfileCommandTree: ProfileCommandTree) => ProfileCommandTree) | ProfileCommandTree) => void;
- onLoadCommands: (namesList: string[][]) => Promise;
-}
-
-const CLIModGeneratorProfileCommandTree: React.FC = ({
- profileCommandTree,
- onChange,
- onLoadCommands,
-}) => {
- const [defaultExpanded, _] = React.useState(GetDefaultExpanded(profileCommandTree));
-
- const onUpdateCommandGroup = React.useCallback(
- (name: string, updater: (oldCommandGroup: ProfileCTCommandGroup) => ProfileCTCommandGroup) => {
- onChange((profileCommandTree) => {
- return {
- ...profileCommandTree,
- commandGroups: {
- ...profileCommandTree.commandGroups,
- [name]: updater(profileCommandTree.commandGroups[name]),
- },
- };
- });
- },
- [onChange],
- );
-
- const handleBatchedLoadedCommands = React.useCallback(
- (commands: CLISpecsCommand[]) => {
- onChange((profileCommandTree) => {
- const newTree = commands.reduce((tree, command) => {
- return (
- genericUpdateCommand(tree, command.names, (unloadedCommand) => {
- return decodeProfileCTCommand(
- command,
- unloadedCommand.selected,
- unloadedCommand.modified,
- unloadedCommand.registered,
- unloadedCommand.selectedVersion,
- );
- }) ?? tree
- );
- }, profileCommandTree);
- return newTree;
- });
- },
- [onChange],
- );
-
- const onLoadAndDecodeCommands = React.useCallback(
- async (names: string[][]) => {
- const commands = await onLoadCommands(names);
- handleBatchedLoadedCommands(commands);
- },
- [onLoadCommands],
- );
-
- React.useEffect(() => {
- const [loadingNamesList, newTree] = PrepareLoadCommands(profileCommandTree);
- if (loadingNamesList.length > 0) {
- onChange(newTree);
- onLoadCommands(loadingNamesList).then((commands) => {
- handleBatchedLoadedCommands(commands);
- });
- }
- }, [profileCommandTree]);
-
- return (
-
- }
- defaultExpandIcon={}
- >
- {Object.values(profileCommandTree.commandGroups).map((commandGroup) => (
-
- ))}
-
-
- );
-};
-
-interface ProfileCommandTree {
- name: string;
- commandGroups: ProfileCTCommandGroups;
-}
-
-interface ProfileCTCommandGroups {
- [name: string]: ProfileCTCommandGroup;
-}
-
-interface ProfileCTCommands {
- [name: string]: ProfileCTCommand;
-}
-
-interface ProfileCTCommandGroup {
- id: string;
- names: string[];
- // We use simple command tree now.
- // `help` is not used.
- // help: string;
-
- commandGroups?: ProfileCTCommandGroups;
- commands?: ProfileCTCommands;
- waitCommand?: CLIModViewCommand;
-
- loading: boolean;
- selected?: boolean;
-}
-
-interface ProfileCTCommand {
- id: string;
- names: string[];
- // help: string;
-
- versions?: ProfileCTCommandVersion[];
-
- selectedVersion?: string;
- registered?: boolean;
- modified: boolean;
-
- loading: boolean;
- selected: boolean;
-}
-
-interface ProfileCTCommandVersion {
- name: string;
- stage: string;
-}
-
-function decodeProfileCTCommandVersion(response: any): ProfileCTCommandVersion {
- return {
- name: response.name,
- stage: response.stage,
- };
-}
-
-function decodeProfileCTCommand(
- response: CLISpecsCommand,
- selected: boolean = false,
- modified: boolean = false,
- registered: boolean | undefined = undefined,
- selectedVersion: string | undefined = undefined,
-): ProfileCTCommand {
- const versions = response.versions?.map((value: any) => decodeProfileCTCommandVersion(value));
- const command = {
- id: response.names.join("/"),
- names: [...response.names],
- // help: response.help.short,
- versions: versions,
- modified: modified,
- loading: false,
- selected: selected,
- registered: registered,
- };
- if (selected) {
- let version: string | undefined;
- if (selectedVersion !== undefined) {
- version = selectedVersion;
- } else {
- version = versions ? versions[0].name : undefined;
- }
-
- return {
- ...command,
- selectedVersion: version,
- };
- } else {
- return command;
- }
-}
-
-function decodeProfileCTCommandGroup(response: CLISpecsCommandGroup, selected: boolean = false): ProfileCTCommandGroup {
- const commands =
- response.commands !== undefined
- ? Object.fromEntries(
- Object.entries(response.commands).map(([name, command]) => [
- name,
- decodeProfileCTCommand(command, selected, selected, undefined),
- ]),
- )
- : undefined;
- const commandGroups =
- response.commandGroups !== undefined
- ? Object.fromEntries(
- Object.entries(response.commandGroups).map(([name, group]) => [
- name,
- decodeProfileCTCommandGroup(group, selected),
- ]),
- )
- : undefined;
- return {
- id: response.names.join("/"),
- names: [...response.names],
- // help: response.help?.short ?? '',
- commandGroups: commandGroups,
- commands: commands,
- loading: false,
- selected: selected,
- };
-}
-
-function BuildProfileCommandTree(profileName: string, response: CLISpecsCommandGroup): ProfileCommandTree {
- const commandGroups =
- response.commandGroups !== undefined
- ? Object.fromEntries(
- Object.entries(response.commandGroups).map(([name, group]) => [name, decodeProfileCTCommandGroup(group)]),
- )
- : {};
- return {
- name: profileName,
- commandGroups: commandGroups,
- };
-}
-
-function getDefaultExpandedOfCommandGroup(commandGroup: ProfileCTCommandGroup): string[] {
- const expandedIds = commandGroup.commandGroups
- ? Object.values(commandGroup.commandGroups).flatMap((value) =>
- value.selected !== false ? [value.id, ...getDefaultExpandedOfCommandGroup(value)] : [],
- )
- : [];
- return expandedIds;
-}
-
-function GetDefaultExpanded(tree: ProfileCommandTree): string[] {
- return Object.values(tree.commandGroups).flatMap((value) => {
- const ids = getDefaultExpandedOfCommandGroup(value);
- if (value.selected !== false) {
- ids.push(value.id);
- }
- return ids;
- });
-}
-
-function prepareLoadCommandsOfCommandGroup(commandGroup: ProfileCTCommandGroup): [string[][], ProfileCTCommandGroup] {
- const namesList: string[][] = [];
- const commands = commandGroup.commands
- ? Object.fromEntries(
- Object.entries(commandGroup.commands).map(([key, value]) => {
- if (value.selected === true && value.versions === undefined && value.loading === false) {
- namesList.push(value.names);
- return [
- key,
- {
- ...value,
- loading: true,
- },
- ];
- }
- return [key, value];
- }),
- )
- : undefined;
- const commandGroups = commandGroup.commandGroups
- ? Object.fromEntries(
- Object.entries(commandGroup.commandGroups).map(([key, value]) => {
- const [namesListSub, updatedGroup] = prepareLoadCommandsOfCommandGroup(value);
- namesList.push(...namesListSub);
- return [key, updatedGroup];
- }),
- )
- : undefined;
- if (namesList.length > 0) {
- return [
- namesList,
- {
- ...commandGroup,
- commands: commands,
- commandGroups: commandGroups,
- },
- ];
- } else {
- return [[], commandGroup];
- }
-}
-
-function PrepareLoadCommands(tree: ProfileCommandTree): [string[][], ProfileCommandTree] {
- const namesList: string[][] = [];
- const commandGroups = Object.fromEntries(
- Object.entries(tree.commandGroups).map(([key, value]) => {
- const [namesListSub, updatedGroup] = prepareLoadCommandsOfCommandGroup(value);
- namesList.push(...namesListSub);
- return [key, updatedGroup];
- }),
- );
- if (namesList.length > 0) {
- return [
- namesList,
- {
- ...tree,
- commandGroups: commandGroups,
- },
- ];
- } else {
- return [[], tree];
- }
-}
-
-function genericUpdateCommand(
- tree: ProfileCommandTree,
- names: string[],
- updater: (command: ProfileCTCommand) => ProfileCTCommand | undefined,
-): ProfileCommandTree | undefined {
- const nodes: ProfileCTCommandGroup[] = [];
- for (const name of names.slice(0, -1)) {
- const node = nodes.length === 0 ? tree : nodes[nodes.length - 1];
- if (node.commandGroups === undefined) {
- throw new Error("Invalid names: " + names.join(" "));
- }
- nodes.push(node.commandGroups[name]);
- }
- let currentCommandGroup = nodes[nodes.length - 1];
- const updatedCommand = updater(currentCommandGroup.commands![names[names.length - 1]]);
- if (updatedCommand === undefined) {
- return undefined;
- }
- const commands = {
- ...currentCommandGroup.commands,
- [names[names.length - 1]]: updatedCommand,
- };
- const groupSelected = calculateSelected(commands, currentCommandGroup.commandGroups!);
- currentCommandGroup = {
- ...currentCommandGroup,
- commands: commands,
- selected: groupSelected,
- };
- for (const node of nodes.reverse().slice(1)) {
- const commandGroups = {
- ...node.commandGroups,
- [currentCommandGroup.names[currentCommandGroup.names.length - 1]]: currentCommandGroup,
- };
- const selected = calculateSelected(node.commands ?? {}, commandGroups);
- currentCommandGroup = {
- ...node,
- commandGroups: commandGroups,
- selected: selected,
- };
- }
- return {
- ...tree,
- commandGroups: {
- ...tree.commandGroups,
- [currentCommandGroup.names[currentCommandGroup.names.length - 1]]: currentCommandGroup,
- },
- };
-}
-
-function calculateSelected(commands: ProfileCTCommands, commandGroups: ProfileCTCommandGroups): boolean | undefined {
- const commandsAllSelected = Object.values(commands).reduce((pre, value) => {
- return pre && value.selected;
- }, true);
- const commandsAllUnselected = Object.values(commands).reduce((pre, value) => {
- return pre && !value.selected;
- }, true);
- const commandGroupsAllSelected = Object.values(commandGroups).reduce((pre, value) => {
- return pre && value.selected === true;
- }, true);
- const commandGroupsAllUnselected = Object.values(commandGroups).reduce((pre, value) => {
- return pre && value.selected === false;
- }, true);
- if (commandsAllUnselected && commandGroupsAllUnselected) {
- return false;
- } else if (commandsAllSelected && commandGroupsAllSelected) {
- return true;
- } else {
- return undefined;
- }
-}
-
-function initializeCommandByModView(
- view: CLIModViewCommand | undefined,
- simpleCommand: CLISpecsSimpleCommand,
-): ProfileCTCommand {
- return {
- id: simpleCommand.names.join("/"),
- names: simpleCommand.names,
- modified: false,
- loading: false,
- selected: view !== undefined && view.version !== undefined,
- selectedVersion: view !== undefined ? view.version : undefined,
- registered: view !== undefined ? view.registered : true,
- };
-}
-
-function initializeCommandGroupByModView(
- view: CLIModViewCommandGroup | undefined,
- simpleCommandGroup: CLISpecsSimpleCommandGroup,
-): ProfileCTCommandGroup {
- const commands =
- simpleCommandGroup.commands !== undefined
- ? Object.fromEntries(
- Object.entries(simpleCommandGroup.commands).map(([key, value]) => [
- key,
- initializeCommandByModView(view?.commands?.[key], value),
- ]),
- )
- : undefined;
- const commandGroups =
- simpleCommandGroup.commandGroups !== undefined
- ? Object.fromEntries(
- Object.entries(simpleCommandGroup.commandGroups).map(([key, value]) => [
- key,
- initializeCommandGroupByModView(view?.commandGroups?.[key], value),
- ]),
- )
- : undefined;
- const leftCommands = Object.entries(view?.commands ?? {})
- .filter(([key, _]) => commands?.[key] === undefined)
- .map(([_, value]) => value.names)
- .map((names) => "`az " + names.join(" ") + "`");
- const leftCommandGroups = Object.entries(view?.commandGroups ?? {})
- .filter(([key, _]) => commandGroups?.[key] === undefined)
- .map(([_, value]) => value.names)
- .map((names) => "`az " + names.join(" ") + "`");
- const errors = [];
- if (leftCommands.length > 0) {
- errors.push(`Miss commands in aaz: ${leftCommands.join(", ")}`);
- }
- if (leftCommandGroups.length > 0) {
- errors.push(`Miss command groups in aaz: ${leftCommandGroups.join(", ")}`);
- }
- if (errors.length > 0) {
- throw new Error(
- "\n" +
- errors.join("\n") +
- "\nSee: https://azure.github.io/aaz-dev-tools/pages/usage/cli-generator/#miss-command-models.",
- );
- }
- const selected = calculateSelected(commands ?? {}, commandGroups ?? {});
- return {
- id: simpleCommandGroup.names.join("/"),
- names: simpleCommandGroup.names,
- commands: commands,
- commandGroups: commandGroups,
- waitCommand: view?.waitCommand,
- loading: false,
- selected: selected,
- };
-}
-
-function InitializeCommandTreeByModView(
- profileName: string,
- view: CLIModViewProfile | null,
- simpleTree: CLISpecsSimpleCommandTree,
-): ProfileCommandTree {
- const commandGroups = Object.fromEntries(
- Object.entries(simpleTree.root.commandGroups).map(([key, value]) => [
- key,
- initializeCommandGroupByModView(view?.commandGroups?.[key], value),
- ]),
- );
- const leftCommandGroups = Object.entries(view?.commandGroups ?? {})
- .filter(([key, _]) => commandGroups?.[key] === undefined)
- .map(([_, value]) => value.names)
- .map((names) => "`az " + names.join(" ") + "`");
- if (leftCommandGroups.length > 0) {
- throw new Error(
- `\nMiss command groups in aaz: ${leftCommandGroups.join(", ")}\nSee: https://azure.github.io/aaz-dev-tools/pages/usage/cli-generator/#miss-command-models.`,
- );
- }
- return {
- name: profileName,
- commandGroups: commandGroups,
- };
-}
-
-function ExportModViewCommand(command: ProfileCTCommand): CLIModViewCommand | undefined {
- if (command.selectedVersion === undefined) {
- return undefined;
- }
-
- return {
- names: command.names,
- registered: command.registered!,
- version: command.selectedVersion!,
- modified: command.modified,
- };
-}
-
-function ExportModViewCommandGroup(commandGroup: ProfileCTCommandGroup): CLIModViewCommandGroup | undefined {
- if (commandGroup.selected === false) {
- return undefined;
- }
-
- let commands: CLIModViewCommands | undefined = undefined;
- if (commandGroup.commands !== undefined) {
- commands = {};
-
- Object.values(commandGroup.commands!).forEach((value) => {
- const view = ExportModViewCommand(value);
- if (view !== undefined) {
- commands![value.names[value.names.length - 1]] = view;
- }
- });
- }
-
- let commandGroups: CLIModViewCommandGroups | undefined = undefined;
- if (commandGroup.commandGroups !== undefined) {
- commandGroups = {};
-
- Object.values(commandGroup.commandGroups!).forEach((value) => {
- const view = ExportModViewCommandGroup(value);
- if (view !== undefined) {
- commandGroups![value.names[value.names.length - 1]] = view;
- }
- });
- }
- return {
- names: commandGroup.names,
- commandGroups: commandGroups,
- commands: commands,
- waitCommand: commandGroup.waitCommand,
- };
-}
-
-function ExportModViewProfile(tree: ProfileCommandTree): CLIModViewProfile {
- const commandGroups: CLIModViewCommandGroups = {};
-
- Object.values(tree.commandGroups).forEach((value) => {
- const view = ExportModViewCommandGroup(value);
- if (view !== undefined) {
- commandGroups[value.names[value.names.length - 1]] = view;
- }
- });
-
- return {
- name: tree.name,
- commandGroups: commandGroups,
- };
-}
-
-export default CLIModGeneratorProfileCommandTree;
-
-export type { ProfileCommandTree };
-
-export { InitializeCommandTreeByModView, BuildProfileCommandTree, ExportModViewProfile };
diff --git a/src/web/src/views/cli/CLIModGeneratorProfileTabs.tsx b/src/web/src/views/cli/CLIModGeneratorProfileTabs.tsx
deleted file mode 100644
index a1cd079a..00000000
--- a/src/web/src/views/cli/CLIModGeneratorProfileTabs.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import * as React from "react";
-import Tabs from "@mui/material/Tabs";
-import Tab from "@mui/material/Tab";
-
-interface CLIModGeneratorProfileTabsProps {
- value: string;
- profiles: string[];
- onChange: (newValue: string) => void;
-}
-
-class CLIModGeneratorProfileTabs extends React.Component {
- render() {
- const { value, profiles, onChange } = this.props;
- return (
- {
- onChange(newValue);
- }}
- aria-label="Vertical tabs example"
- sx={{ borderRight: 1, borderColor: "divider" }}
- >
- {profiles.map((profile, _idx) => {
- return (
-
- );
- })}
-
- );
- }
-}
-
-export default CLIModGeneratorProfileTabs;
diff --git a/src/web/src/views/cli/CLIModGeneratorToolBar.tsx b/src/web/src/views/cli/CLIModGeneratorToolBar.tsx
deleted file mode 100644
index 809bdb27..00000000
--- a/src/web/src/views/cli/CLIModGeneratorToolBar.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import * as React from "react";
-import { AppBar, Button, IconButton, Toolbar, Typography, Tooltip, Box } from "@mui/material";
-import HomeIcon from "@mui/icons-material/Home";
-
-interface CLIModGeneratorToolBarProps {
- moduleName: string;
- onHomePage: () => void;
- onGenerate: () => void;
-}
-
-class CLIModGeneratorToolBar extends React.Component {
- render() {
- const { moduleName, onHomePage, onGenerate } = this.props;
- return (
-
- theme.zIndex.drawer + 1 }}>
-
-
-
-
- GENERATION
-
-
-
-
- {moduleName}
-
-
-
-
-
-
-
-
-
-
-
- );
- }
-}
-
-export default CLIModGeneratorToolBar;
diff --git a/src/web/src/views/cli/CLIModuleGenerator.tsx b/src/web/src/views/cli/CLIModuleGenerator.tsx
deleted file mode 100644
index 24d10a6f..00000000
--- a/src/web/src/views/cli/CLIModuleGenerator.tsx
+++ /dev/null
@@ -1,400 +0,0 @@
-import * as React from "react";
-import {
- Backdrop,
- Box,
- Button,
- CircularProgress,
- Dialog,
- DialogActions,
- DialogContent,
- DialogTitle,
- Drawer,
- LinearProgress,
- Toolbar,
- Alert,
-} from "@mui/material";
-import { useParams } from "react-router";
-import { cliApi, errorHandlerApi } from "../../services";
-import CLIModGeneratorToolBar from "./CLIModGeneratorToolBar";
-import CLIModGeneratorProfileCommandTree, {
- ExportModViewProfile,
- InitializeCommandTreeByModView,
- ProfileCommandTree,
-} from "./CLIModGeneratorProfileCommandTree";
-import CLIModGeneratorProfileTabs from "./CLIModGeneratorProfileTabs";
-import { CLIModView, CLIModViewProfiles } from "./CLIModuleCommon";
-
-interface CLISpecsSimpleCommand {
- names: string[];
-}
-
-interface CLISpecsSimpleCommands {
- [name: string]: CLISpecsSimpleCommand;
-}
-
-interface CLISpecsSimpleCommandGroups {
- [name: string]: CLISpecsSimpleCommandGroup;
-}
-
-interface CLISpecsSimpleCommandGroup {
- names: string[];
- commands: CLISpecsSimpleCommands;
- commandGroups: CLISpecsSimpleCommandGroups;
-}
-
-interface CLISpecsSimpleCommandTree {
- root: CLISpecsSimpleCommandGroup;
-}
-
-interface CLISpecsHelp {
- short: string;
- lines?: string[];
-}
-
-interface CLISpecsResource {
- plane: string;
- id: string;
- version: string;
- subresource?: string;
-}
-
-interface CLISpecsCommandExample {
- name: string;
- commands: string[];
-}
-
-interface CLISpecsCommandVersion {
- name: string;
- stage?: string;
- resources: CLISpecsResource[];
- examples?: CLISpecsCommandExample[];
-}
-
-interface CLISpecsCommand {
- names: string[];
- help: CLISpecsHelp;
- versions: CLISpecsCommandVersion[];
-}
-
-interface CLISpecsCommandGroup {
- names: string[];
- help?: CLISpecsHelp;
- commands?: CLISpecsCommands;
- commandGroups?: CLISpecsCommandGroups;
-}
-
-interface CLISpecsCommandGroups {
- [name: string]: CLISpecsCommandGroup;
-}
-
-interface CLISpecsCommands {
- [name: string]: CLISpecsCommand;
-}
-
-async function retrieveCommand(names: string[]): Promise {
- return await cliApi.getSpecsCommand(names);
-}
-
-async function retrieveCommands(namesList: string[][]): Promise {
- return await cliApi.retrieveCommands(namesList);
-}
-
-const useSpecsCommandTree: () => (namesList: string[][]) => Promise = () => {
- const commandCache = React.useRef(new Map>());
-
- const fetchCommands = React.useCallback(
- async (namesList: string[][]) => {
- const promiseResults = [];
- const uncachedNamesList = [];
- for (const names of namesList) {
- const cachedPromise = commandCache.current.get(names.join("/"));
- if (!cachedPromise) {
- uncachedNamesList.push(names);
- } else {
- promiseResults.push(cachedPromise);
- }
- }
- if (uncachedNamesList.length === 0) {
- return await Promise.all(promiseResults);
- } else if (uncachedNamesList.length === 1) {
- const commandPromise = retrieveCommand(uncachedNamesList[0]);
- commandCache.current.set(uncachedNamesList[0].join("/"), commandPromise);
- return await Promise.all(promiseResults.concat(commandPromise));
- } else {
- const uncachedCommandsPromise = retrieveCommands(uncachedNamesList);
- uncachedNamesList.forEach((names, idx) => {
- commandCache.current.set(
- names.join("/"),
- uncachedCommandsPromise.then((commands) => commands[idx]),
- );
- });
- return (await Promise.all(promiseResults)).concat(await uncachedCommandsPromise);
- }
- },
- [commandCache],
- );
- return fetchCommands;
-};
-
-interface ProfileCommandTrees {
- [name: string]: ProfileCommandTree;
-}
-
-interface CLIModuleGeneratorProps {
- params: {
- repoName: string;
- moduleName: string;
- };
-}
-
-const CLIModuleGenerator: React.FC = ({ params }) => {
- const [loading, setLoading] = React.useState(false);
- const [invalidText, setInvalidText] = React.useState(undefined);
- const [profiles, setProfiles] = React.useState([]);
- const [commandTrees, setCommandTrees] = React.useState({});
- const [selectedProfile, setSelectedProfile] = React.useState(undefined);
- const [showGenerateDialog, setShowGenerateDialog] = React.useState(false);
-
- const fetchCommands = useSpecsCommandTree();
-
- React.useEffect(() => {
- loadModule();
- }, []);
-
- const loadModule = async () => {
- try {
- setLoading(true);
- setInvalidText(undefined);
- const profiles = await cliApi.getCliProfiles();
- const modView: CLIModView = await cliApi.getCliModule(params.repoName, params.moduleName);
- const simpleTree: CLISpecsSimpleCommandTree = await cliApi.getSimpleCommandTree();
-
- Object.keys(modView!.profiles).forEach((profile) => {
- const idx = profiles.findIndex((v) => v === profile);
- if (idx === -1) {
- throw new Error(`Invalid profile ${profile}`);
- }
- });
-
- const commandTrees = Object.fromEntries(
- profiles.map((profile) => {
- return [profile, InitializeCommandTreeByModView(profile, modView!.profiles[profile] ?? null, simpleTree)];
- }),
- );
-
- const selectedProfile = profiles.length > 0 ? profiles[0] : undefined;
- setProfiles(profiles);
- setCommandTrees(commandTrees);
- setSelectedProfile(selectedProfile);
- setLoading(false);
- } catch (err: any) {
- console.error(err);
- setInvalidText(errorHandlerApi.getErrorMessage(err));
- }
- };
-
- const selectedCommandTree = selectedProfile ? commandTrees[selectedProfile] : undefined;
-
- const handleBackToHomepage = () => {
- window.open("/?#/cli", "_blank");
- };
-
- const handleGenerate = () => {
- setShowGenerateDialog(true);
- };
-
- const handleGenerationClose = () => {
- setShowGenerateDialog(false);
- };
-
- const onProfileChange = React.useCallback((selectedProfile: string) => {
- setSelectedProfile(selectedProfile);
- }, []);
-
- const onSelectedProfileTreeUpdate = React.useCallback(
- (updater: ((oldTree: ProfileCommandTree) => ProfileCommandTree) | ProfileCommandTree) => {
- setCommandTrees((commandTrees) => {
- const selectedCommandTree = commandTrees[selectedProfile!];
- const newTree = typeof updater === "function" ? updater(selectedCommandTree!) : updater;
- return { ...commandTrees, [selectedProfile!]: newTree };
- });
- },
- [selectedProfile],
- );
-
- return (
-
-
-
-
-
- {selectedProfile !== undefined && (
-
- )}
-
-
-
- {selectedCommandTree !== undefined && (
-
- )}
-
-
- {showGenerateDialog && (
-
- )}
- theme.zIndex.drawer + 1 }} open={loading}>
- {invalidText !== undefined ? (
- {
- setInvalidText(undefined);
- setLoading(false);
- }}
- >
- {invalidText}
-
- ) : (
-
- )}
-
-
- );
-};
-
-function GenerateDialog(props: {
- repoName: string;
- moduleName: string;
- profileCommandTrees: ProfileCommandTrees;
- open: boolean;
- onClose: (generated: boolean) => void;
-}) {
- const [updating, setUpdating] = React.useState(false);
- const [invalidText, setInvalidText] = React.useState(undefined);
-
- const handleClose = () => {
- props.onClose(false);
- };
-
- const handleGenerateAll = async () => {
- const profiles: CLIModViewProfiles = {};
- Object.values(props.profileCommandTrees).forEach((tree) => {
- profiles[tree.name] = ExportModViewProfile(tree);
- });
- const data = {
- name: props.moduleName,
- profiles: profiles,
- };
-
- setUpdating(true);
- try {
- await cliApi.updateCliModule(props.repoName, props.moduleName, data);
- setUpdating(false);
- props.onClose(true);
- } catch (err: any) {
- console.error(err);
- setInvalidText(errorHandlerApi.getErrorMessage(err));
- setUpdating(false);
- }
- };
-
- const handleGenerateModified = async () => {
- const profiles: CLIModViewProfiles = {};
- Object.values(props.profileCommandTrees).forEach((tree) => {
- profiles[tree.name] = ExportModViewProfile(tree);
- });
- const data = {
- name: props.moduleName,
- profiles: profiles,
- };
-
- setUpdating(true);
- try {
- await cliApi.patchCliModule(props.repoName, props.moduleName, data);
- setUpdating(false);
- props.onClose(true);
- } catch (err: any) {
- console.error(err);
- setInvalidText(errorHandlerApi.getErrorMessage(err));
- setUpdating(false);
- }
- };
-
- return (
-
- );
-}
-
-const CLIModuleGeneratorWrapper = (props: any) => {
- const params = useParams();
- return ;
-};
-
-export type {
- CLISpecsCommandGroup,
- CLISpecsCommand,
- CLISpecsSimpleCommandTree,
- CLISpecsSimpleCommandGroup,
- CLISpecsSimpleCommand,
-};
-export { CLIModuleGeneratorWrapper as CLIModuleGenerator };
diff --git a/src/web/src/views/cli/CLIPage.tsx b/src/web/src/views/cli/CLIPage.tsx
deleted file mode 100644
index 419b62f1..00000000
--- a/src/web/src/views/cli/CLIPage.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import * as React from "react";
-import withRoot from "../../withRoot";
-import { Outlet } from "react-router";
-
-class CLIPage extends React.Component {
- render() {
- return (
-
-
-
- );
- }
-}
-
-export default withRoot(CLIPage);
diff --git a/src/web/src/views/cli/components/CLIInstruction.tsx b/src/web/src/views/cli/components/CLIInstruction.tsx
new file mode 100644
index 00000000..bac59756
--- /dev/null
+++ b/src/web/src/views/cli/components/CLIInstruction.tsx
@@ -0,0 +1,67 @@
+import React from "react";
+import { Typography, Box } from "@mui/material";
+import { styled } from "@mui/material/styles";
+import CLIModuleSelector from "./CLIModuleSelector";
+import { AppNavBar } from "../../../components/AppNavBar";
+import PageLayout from "../../../components/PageLayout";
+
+const MiddlePadding = styled(Box)(() => ({
+ height: "6vh",
+}));
+
+const SpacePadding = styled(Box)(() => ({
+ width: "3vh",
+}));
+
+const CLIInstruction: React.FC = () => {
+ return (
+ <>
+
+
+
+
+
+
+ Please select a CLI Module
+
+
+
+
+
+
+ Or
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default CLIInstruction;
diff --git a/src/web/src/views/cli/components/CLIModGeneratorProfileCommandTree.tsx b/src/web/src/views/cli/components/CLIModGeneratorProfileCommandTree.tsx
new file mode 100644
index 00000000..dcbbc7d6
--- /dev/null
+++ b/src/web/src/views/cli/components/CLIModGeneratorProfileCommandTree.tsx
@@ -0,0 +1,197 @@
+import React, { useState, useCallback, useEffect } from "react";
+import TreeView from "@mui/lab/TreeView";
+
+import ArrowRightIcon from "@mui/icons-material/ArrowRight";
+import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
+import { CLISpecsCommand } from "./CLIModuleGenerator";
+import CommandGroupItem from "./CommandGroupItem";
+import {
+ calculateSelected,
+ prepareLoadCommandsOfCommandGroup,
+ type ProfileCTCommandGroup,
+ type ProfileCTCommand,
+} from "../utils/commandTreeUtils";
+import { ProfileCommandTree, decodeProfileCTCommand } from "../utils/commandTreeInitialization";
+
+interface CLIModGeneratorProfileCommandTreeProps {
+ profile?: string;
+ profileCommandTree: ProfileCommandTree;
+ onChange: (updater: ((oldProfileCommandTree: ProfileCommandTree) => ProfileCommandTree) | ProfileCommandTree) => void;
+ onLoadCommands: (namesList: string[][]) => Promise;
+}
+
+const CLIModGeneratorProfileCommandTree: React.FC = ({
+ profileCommandTree,
+ onChange,
+ onLoadCommands,
+}) => {
+ const [defaultExpanded, _] = useState(getDefaultExpanded(profileCommandTree));
+
+ const onUpdateCommandGroup = useCallback(
+ (name: string, updater: (oldCommandGroup: ProfileCTCommandGroup) => ProfileCTCommandGroup) => {
+ onChange((profileCommandTree) => {
+ return {
+ ...profileCommandTree,
+ commandGroups: {
+ ...profileCommandTree.commandGroups,
+ [name]: updater(profileCommandTree.commandGroups[name]),
+ },
+ };
+ });
+ },
+ [onChange],
+ );
+
+ const handleBatchedLoadedCommands = useCallback(
+ (commands: CLISpecsCommand[]) => {
+ onChange((profileCommandTree) => {
+ const newTree = commands.reduce((tree, command) => {
+ return (
+ genericUpdateCommand(tree, command.names, (unloadedCommand) => {
+ return decodeProfileCTCommand(
+ command,
+ unloadedCommand.selected,
+ unloadedCommand.modified,
+ unloadedCommand.registered,
+ unloadedCommand.selectedVersion,
+ );
+ }) ?? tree
+ );
+ }, profileCommandTree);
+ return newTree;
+ });
+ },
+ [onChange],
+ );
+
+ const onLoadAndDecodeCommands = useCallback(
+ async (names: string[][]) => {
+ const commands = await onLoadCommands(names);
+ handleBatchedLoadedCommands(commands);
+ },
+ [onLoadCommands],
+ );
+
+ useEffect(() => {
+ const [loadingNamesList, newTree] = prepareLoadCommands(profileCommandTree);
+ if (loadingNamesList.length > 0) {
+ onChange(newTree);
+ onLoadCommands(loadingNamesList).then((commands) => {
+ handleBatchedLoadedCommands(commands);
+ });
+ }
+ }, [profileCommandTree]);
+
+ return (
+
+ }
+ defaultExpandIcon={}
+ data-testid="cli-command-tree"
+ >
+ {Object.values(profileCommandTree.commandGroups).map((commandGroup) => (
+
+ ))}
+
+
+ );
+};
+
+const getDefaultExpandedOfCommandGroup = (commandGroup: ProfileCTCommandGroup): string[] => {
+ const expandedIds = commandGroup.commandGroups
+ ? Object.values(commandGroup.commandGroups).flatMap((value) =>
+ value.selected !== false ? [value.id, ...getDefaultExpandedOfCommandGroup(value)] : [],
+ )
+ : [];
+ return expandedIds;
+};
+
+const getDefaultExpanded = (tree: ProfileCommandTree): string[] => {
+ return Object.values(tree.commandGroups).flatMap((value) => {
+ const ids = getDefaultExpandedOfCommandGroup(value);
+ if (value.selected !== false) {
+ ids.push(value.id);
+ }
+ return ids;
+ });
+};
+
+const prepareLoadCommands = (tree: ProfileCommandTree): [string[][], ProfileCommandTree] => {
+ const namesList: string[][] = [];
+ const commandGroups = Object.fromEntries(
+ Object.entries(tree.commandGroups).map(([key, value]) => {
+ const [namesListSub, updatedGroup] = prepareLoadCommandsOfCommandGroup(value);
+ namesList.push(...namesListSub);
+ return [key, updatedGroup];
+ }),
+ );
+ if (namesList.length > 0) {
+ return [
+ namesList,
+ {
+ ...tree,
+ commandGroups: commandGroups,
+ },
+ ];
+ } else {
+ return [[], tree];
+ }
+};
+
+const genericUpdateCommand = (
+ tree: ProfileCommandTree,
+ names: string[],
+ updater: (command: ProfileCTCommand) => ProfileCTCommand | undefined,
+): ProfileCommandTree | undefined => {
+ const nodes: ProfileCTCommandGroup[] = [];
+ for (const name of names.slice(0, -1)) {
+ const node = nodes.length === 0 ? tree : nodes[nodes.length - 1];
+ if (node.commandGroups === undefined) {
+ throw new Error("Invalid names: " + names.join(" "));
+ }
+ nodes.push(node.commandGroups[name]);
+ }
+ let currentCommandGroup = nodes[nodes.length - 1];
+ const updatedCommand = updater(currentCommandGroup.commands![names[names.length - 1]]);
+ if (updatedCommand === undefined) {
+ return undefined;
+ }
+ const commands = {
+ ...currentCommandGroup.commands,
+ [names[names.length - 1]]: updatedCommand,
+ };
+ const groupSelected = calculateSelected(commands, currentCommandGroup.commandGroups!);
+ currentCommandGroup = {
+ ...currentCommandGroup,
+ commands: commands,
+ selected: groupSelected,
+ };
+ for (const node of nodes.reverse().slice(1)) {
+ const commandGroups = {
+ ...node.commandGroups,
+ [currentCommandGroup.names[currentCommandGroup.names.length - 1]]: currentCommandGroup,
+ };
+ const selected = calculateSelected(node.commands ?? {}, commandGroups);
+ currentCommandGroup = {
+ ...node,
+ commandGroups: commandGroups,
+ selected: selected,
+ };
+ }
+ return {
+ ...tree,
+ commandGroups: {
+ ...tree.commandGroups,
+ [currentCommandGroup.names[currentCommandGroup.names.length - 1]]: currentCommandGroup,
+ },
+ };
+};
+
+export default CLIModGeneratorProfileCommandTree;
diff --git a/src/web/src/views/cli/components/CLIModGeneratorProfileTabs.tsx b/src/web/src/views/cli/components/CLIModGeneratorProfileTabs.tsx
new file mode 100644
index 00000000..49365ed6
--- /dev/null
+++ b/src/web/src/views/cli/components/CLIModGeneratorProfileTabs.tsx
@@ -0,0 +1,36 @@
+import React from "react";
+import Tabs from "@mui/material/Tabs";
+import Tab from "@mui/material/Tab";
+
+interface CLIModGeneratorProfileTabsProps {
+ value: string;
+ profiles: string[];
+ onChange: (newValue: string) => void;
+}
+
+const CLIModGeneratorProfileTabs: React.FC = ({ value, profiles, onChange }) => {
+ return (
+ {
+ onChange(newValue);
+ }}
+ aria-label="Vertical tabs example"
+ sx={{ borderRight: 1, borderColor: "divider" }}
+ >
+ {profiles.map((profile) => (
+
+ ))}
+
+ );
+};
+
+export default CLIModGeneratorProfileTabs;
diff --git a/src/web/src/views/cli/components/CLIModGeneratorToolBar.tsx b/src/web/src/views/cli/components/CLIModGeneratorToolBar.tsx
new file mode 100644
index 00000000..d1d6b26d
--- /dev/null
+++ b/src/web/src/views/cli/components/CLIModGeneratorToolBar.tsx
@@ -0,0 +1,48 @@
+import React from "react";
+import { AppBar, Button, IconButton, Toolbar, Typography, Tooltip, Box } from "@mui/material";
+import HomeIcon from "@mui/icons-material/Home";
+
+interface CLIModGeneratorToolBarProps {
+ moduleName: string;
+ onHomePage: () => void;
+ onGenerate: () => void;
+}
+
+const CLIModGeneratorToolBar: React.FC = ({ moduleName, onHomePage, onGenerate }) => {
+ return (
+ <>
+ theme.zIndex.drawer + 1 }}>
+
+
+
+
+ GENERATION
+
+
+
+
+ {moduleName}
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default CLIModGeneratorToolBar;
diff --git a/src/web/src/views/cli/components/CLIModuleGenerator.tsx b/src/web/src/views/cli/components/CLIModuleGenerator.tsx
new file mode 100644
index 00000000..e05d3a84
--- /dev/null
+++ b/src/web/src/views/cli/components/CLIModuleGenerator.tsx
@@ -0,0 +1,200 @@
+import { useState, useEffect, useCallback, Fragment, FC } from "react";
+import { Backdrop, Box, CircularProgress, Drawer, Toolbar, Alert } from "@mui/material";
+import { useParams } from "react-router";
+import { cliApi, errorHandlerApi } from "../../../services";
+import CLIModGeneratorToolBar from "./CLIModGeneratorToolBar";
+import CLIModGeneratorProfileCommandTree from "./CLIModGeneratorProfileCommandTree";
+import { initializeCommandTreeByModView, ProfileCommandTree } from "../utils/commandTreeInitialization";
+import CLIModGeneratorProfileTabs from "./CLIModGeneratorProfileTabs";
+import { CLIModView } from "../interfaces";
+import GenerateDialog, { type ProfileCommandTrees } from "./GenerateDialog";
+import { useSpecsCommandTree } from "../hooks";
+
+interface CLISpecsSimpleCommand {
+ names: string[];
+}
+
+interface CLISpecsSimpleCommands {
+ [name: string]: CLISpecsSimpleCommand;
+}
+
+interface CLISpecsSimpleCommandGroups {
+ [name: string]: CLISpecsSimpleCommandGroup;
+}
+
+interface CLISpecsSimpleCommandGroup {
+ names: string[];
+ commands: CLISpecsSimpleCommands;
+ commandGroups: CLISpecsSimpleCommandGroups;
+}
+
+interface CLISpecsSimpleCommandTree {
+ root: CLISpecsSimpleCommandGroup;
+}
+
+interface CLIModuleGeneratorProps {
+ params: {
+ repoName: string;
+ moduleName: string;
+ };
+}
+
+const CLIModuleGenerator: FC = ({ params }) => {
+ const [loading, setLoading] = useState(false);
+ const [invalidText, setInvalidText] = useState(undefined);
+ const [profiles, setProfiles] = useState([]);
+ const [commandTrees, setCommandTrees] = useState({});
+ const [selectedProfile, setSelectedProfile] = useState(undefined);
+ const [showGenerateDialog, setShowGenerateDialog] = useState(false);
+
+ const fetchCommands = useSpecsCommandTree();
+
+ useEffect(() => {
+ loadModule();
+ }, []);
+
+ const loadModule = async () => {
+ try {
+ setLoading(true);
+ setInvalidText(undefined);
+ const profiles = await cliApi.getCliProfiles();
+ const modView: CLIModView = await cliApi.getCliModule(params.repoName, params.moduleName);
+ const simpleTree: CLISpecsSimpleCommandTree = await cliApi.getSimpleCommandTree();
+
+ Object.keys(modView!.profiles).forEach((profile) => {
+ const idx = profiles.findIndex((v) => v === profile);
+ if (idx === -1) {
+ throw new Error(`Invalid profile ${profile}`);
+ }
+ });
+
+ const commandTrees = Object.fromEntries(
+ profiles.map((profile) => {
+ return [profile, initializeCommandTreeByModView(profile, modView!.profiles[profile] ?? null, simpleTree)];
+ }),
+ );
+
+ const selectedProfile = profiles.length > 0 ? profiles[0] : undefined;
+ setProfiles(profiles);
+ setCommandTrees(commandTrees);
+ setSelectedProfile(selectedProfile);
+ setLoading(false);
+ } catch (err: any) {
+ console.error(err);
+ setInvalidText(errorHandlerApi.getErrorMessage(err));
+ }
+ };
+
+ const selectedCommandTree = selectedProfile ? commandTrees[selectedProfile] : undefined;
+
+ const handleBackToHomepage = () => {
+ window.open("/?#/cli", "_blank");
+ };
+
+ const handleGenerate = () => {
+ setShowGenerateDialog(true);
+ };
+
+ const handleGenerationClose = () => {
+ setShowGenerateDialog(false);
+ };
+
+ const onProfileChange = useCallback((selectedProfile: string) => {
+ setSelectedProfile(selectedProfile);
+ }, []);
+
+ const onSelectedProfileTreeUpdate = useCallback(
+ (updater: ((oldTree: ProfileCommandTree) => ProfileCommandTree) | ProfileCommandTree) => {
+ setCommandTrees((commandTrees) => {
+ const selectedCommandTree = commandTrees[selectedProfile!];
+ const newTree = typeof updater === "function" ? updater(selectedCommandTree!) : updater;
+ return { ...commandTrees, [selectedProfile!]: newTree };
+ });
+ },
+ [selectedProfile],
+ );
+
+ return (
+
+
+
+
+
+ {selectedProfile !== undefined && (
+
+ )}
+
+
+
+ {selectedCommandTree !== undefined && (
+
+ )}
+
+
+ {showGenerateDialog && (
+
+ )}
+ theme.zIndex.drawer + 1 }} open={loading}>
+ {invalidText !== undefined ? (
+ {
+ setInvalidText(undefined);
+ setLoading(false);
+ }}
+ >
+ {invalidText}
+
+ ) : (
+
+ )}
+
+
+ );
+};
+
+const CLIModuleGeneratorWrapper = (props: any) => {
+ const params = useParams();
+ return ;
+};
+
+export type { CLISpecsCommand } from "../hooks";
+export type { CLISpecsSimpleCommandTree, CLISpecsSimpleCommandGroup, CLISpecsSimpleCommand };
+export { CLIModuleGeneratorWrapper as CLIModuleGenerator };
diff --git a/src/web/src/views/cli/CLIModuleSelector.tsx b/src/web/src/views/cli/components/CLIModuleSelector.tsx
similarity index 99%
rename from src/web/src/views/cli/CLIModuleSelector.tsx
rename to src/web/src/views/cli/components/CLIModuleSelector.tsx
index aa52544b..5ba6d9b5 100644
--- a/src/web/src/views/cli/CLIModuleSelector.tsx
+++ b/src/web/src/views/cli/components/CLIModuleSelector.tsx
@@ -9,7 +9,7 @@ import {
TextField,
Button,
} from "@mui/material";
-import { cliApi, errorHandlerApi } from "../../services";
+import { cliApi, errorHandlerApi } from "../../../services";
import * as React from "react";
interface CLIModule {
diff --git a/src/web/src/views/cli/components/CLIPage.tsx b/src/web/src/views/cli/components/CLIPage.tsx
new file mode 100644
index 00000000..c7cbba4a
--- /dev/null
+++ b/src/web/src/views/cli/components/CLIPage.tsx
@@ -0,0 +1,12 @@
+import { FC } from "react";
+import { Outlet } from "react-router";
+
+const CLIPage: FC = () => {
+ return (
+ <>
+
+ >
+ );
+};
+
+export default CLIPage;
diff --git a/src/web/src/views/cli/components/CommandGroupItem.tsx b/src/web/src/views/cli/components/CommandGroupItem.tsx
new file mode 100644
index 00000000..c92aaa9e
--- /dev/null
+++ b/src/web/src/views/cli/components/CommandGroupItem.tsx
@@ -0,0 +1,190 @@
+import React, { memo, useCallback } from "react";
+import TreeItem from "@mui/lab/TreeItem";
+import FolderIcon from "@mui/icons-material/Folder";
+import { Box, Checkbox, Typography, styled, TypographyProps } from "@mui/material";
+import CommandItem from "./CommandItem";
+import {
+ calculateSelected,
+ prepareLoadCommandsOfCommandGroup,
+ type ProfileCTCommandGroup,
+ type ProfileCTCommand,
+} from "../utils/commandTreeUtils";
+
+const CommandGroupTypography = styled(Typography)(({ theme }) => ({
+ color: theme.palette.primary.main,
+ fontFamily: "'Work Sans', sans-serif",
+ fontSize: 17,
+ fontWeight: 600,
+}));
+
+interface CommandGroupItemProps {
+ commandGroup: ProfileCTCommandGroup;
+ onUpdateCommandGroup: (
+ name: string,
+ updater: (oldCommandGroup: ProfileCTCommandGroup) => ProfileCTCommandGroup,
+ ) => void;
+ onLoadCommands: (names: string[][]) => Promise;
+}
+
+const CommandGroupItem: React.FC = memo(
+ ({ commandGroup, onUpdateCommandGroup, onLoadCommands }) => {
+ const nodeName = commandGroup.names[commandGroup.names.length - 1];
+ const selected = commandGroup.selected ?? false;
+
+ const onUpdateCommand = useCallback(
+ (name: string, updater: (oldCommand: ProfileCTCommand) => ProfileCTCommand) => {
+ onUpdateCommandGroup(nodeName, (oldCommandGroup) => {
+ const commands = {
+ ...oldCommandGroup.commands,
+ [name]: updater(oldCommandGroup.commands![name]),
+ };
+ const selected = calculateSelected(commands, oldCommandGroup.commandGroups ?? {});
+ return {
+ ...oldCommandGroup,
+ commands: commands,
+ selected: selected,
+ };
+ });
+ },
+ [onUpdateCommandGroup, nodeName],
+ );
+
+ const onUpdateSubCommandGroup = useCallback(
+ (name: string, updater: (oldCommandGroup: ProfileCTCommandGroup) => ProfileCTCommandGroup) => {
+ onUpdateCommandGroup(nodeName, (oldCommandGroup) => {
+ const commandGroups = {
+ ...oldCommandGroup.commandGroups,
+ [name]: updater(oldCommandGroup.commandGroups![name]),
+ };
+ const commands = oldCommandGroup.commands;
+ const selected = calculateSelected(commands ?? {}, commandGroups);
+ return {
+ ...oldCommandGroup,
+ commandGroups: commandGroups,
+ selected: selected,
+ };
+ });
+ },
+ [onUpdateCommandGroup, nodeName],
+ );
+
+ const onLoadCommand = useCallback(
+ async (names: string[]) => {
+ await onLoadCommands([names]);
+ },
+ [onLoadCommands],
+ );
+
+ const updateCommandSelected = (command: ProfileCTCommand, selected: boolean): ProfileCTCommand => {
+ if (selected === command.selected) {
+ return command;
+ }
+ return {
+ ...command,
+ selected: selected,
+ selectedVersion: selected
+ ? command.selectedVersion
+ ? command.selectedVersion
+ : command.versions
+ ? command.versions[0].name
+ : undefined
+ : command.selectedVersion,
+ modified: true,
+ };
+ };
+
+ const updateGroupSelected = (group: ProfileCTCommandGroup, selected: boolean): ProfileCTCommandGroup => {
+ if (selected === group.selected) {
+ return group;
+ }
+ const commands = group.commands
+ ? Object.fromEntries(
+ Object.entries(group.commands).map(([key, value]) => [key, updateCommandSelected(value, selected)]),
+ )
+ : undefined;
+ const commandGroups = group.commandGroups
+ ? Object.fromEntries(
+ Object.entries(group.commandGroups).map(([key, value]) => [key, updateGroupSelected(value, selected)]),
+ )
+ : undefined;
+ return {
+ ...group,
+ commands: commands,
+ commandGroups: commandGroups,
+ selected: selected,
+ };
+ };
+
+ const selectCommandGroup = useCallback(
+ (selected: boolean) => {
+ onUpdateCommandGroup(nodeName, (oldCommandGroup) => {
+ const selectedGroup = updateGroupSelected(oldCommandGroup, selected);
+ const [loadingNamesList, newGroup] = prepareLoadCommandsOfCommandGroup(selectedGroup);
+ if (loadingNamesList.length > 0) {
+ onLoadCommands(loadingNamesList);
+ }
+ return newGroup;
+ });
+ },
+ [onUpdateCommandGroup, onLoadCommands, nodeName],
+ );
+
+ return (
+
+ {
+ selectCommandGroup(!selected);
+ event.stopPropagation();
+ event.preventDefault();
+ }}
+ />
+
+ {nodeName}
+
+ }
+ >
+ {commandGroup.commands !== undefined &&
+ Object.values(commandGroup.commands).map((command) => (
+
+ ))}
+ {commandGroup.commandGroups !== undefined &&
+ Object.values(commandGroup.commandGroups).map((group) => (
+
+ ))}
+
+ );
+ },
+);
+
+CommandGroupItem.displayName = "CommandGroupItem";
+
+export default CommandGroupItem;
+
+export type { CommandGroupItemProps };
diff --git a/src/web/src/views/cli/components/CommandItem.tsx b/src/web/src/views/cli/components/CommandItem.tsx
new file mode 100644
index 00000000..615230ef
--- /dev/null
+++ b/src/web/src/views/cli/components/CommandItem.tsx
@@ -0,0 +1,246 @@
+import React, { memo, useCallback } from "react";
+import TreeItem from "@mui/lab/TreeItem";
+import EditIcon from "@mui/icons-material/Edit";
+import {
+ Box,
+ Checkbox,
+ FormControl,
+ Typography,
+ Select,
+ MenuItem,
+ styled,
+ TypographyProps,
+ InputLabel,
+ IconButton,
+} from "@mui/material";
+import { type ProfileCTCommand } from "../utils/commandTreeUtils";
+
+const CommandTypography = styled(Typography)(({ theme }) => ({
+ color: theme.palette.primary.main,
+ fontFamily: "'Work Sans', sans-serif",
+ fontSize: 20,
+ fontWeight: 400,
+}));
+
+const SelectionTypography = styled(Typography)(({ theme }) => ({
+ color: theme.palette.grey[700],
+ fontFamily: "'Work Sans', sans-serif",
+ fontSize: 15,
+ fontWeight: 400,
+}));
+
+const RegisteredTypography = styled(SelectionTypography)(() => ({}));
+
+const UnregisteredTypography = styled(SelectionTypography)(() => ({
+ color: "#d9c136",
+}));
+
+interface CommandItemProps {
+ command: ProfileCTCommand;
+ onUpdateCommand: (name: string, updater: (oldCommand: ProfileCTCommand) => ProfileCTCommand) => void;
+ onLoadCommand(names: string[]): Promise;
+}
+
+const CommandItem: React.FC = memo(({ command, onUpdateCommand, onLoadCommand }) => {
+ const leafName = command.names[command.names.length - 1];
+
+ const selectCommand = useCallback(
+ (selected: boolean) => {
+ onUpdateCommand(leafName, (oldCommand) => {
+ if (oldCommand.versions === undefined && selected === true) {
+ onLoadCommand(oldCommand.names);
+ }
+ return {
+ ...oldCommand,
+ loading: selected && oldCommand.versions === undefined,
+ selected: selected,
+ selectedVersion: selected
+ ? oldCommand.selectedVersion
+ ? oldCommand.selectedVersion
+ : oldCommand.versions
+ ? oldCommand.versions[0].name
+ : undefined
+ : oldCommand.selectedVersion,
+ modified: true,
+ };
+ });
+ },
+ [onUpdateCommand, onLoadCommand, leafName],
+ );
+
+ const selectVersion = useCallback(
+ (version: string) => {
+ onUpdateCommand(leafName, (oldCommand) => {
+ return {
+ ...oldCommand,
+ selectedVersion: version,
+ modified: true,
+ };
+ });
+ },
+ [onUpdateCommand, leafName],
+ );
+
+ const selectRegistered = useCallback(
+ (registered: boolean) => {
+ onUpdateCommand(leafName, (oldCommand) => {
+ return {
+ ...oldCommand,
+ registered: registered,
+ modified: true,
+ };
+ });
+ },
+ [onUpdateCommand, leafName],
+ );
+
+ return (
+
+ {
+ selectCommand(!command.selected);
+ event.stopPropagation();
+ event.preventDefault();
+ }}
+ />
+
+ {leafName}
+
+ {!command.modified && command.selectedVersion !== undefined && (
+ {
+ selectCommand(true);
+ }}
+ >
+
+
+ )}
+ {command.modified && }
+
+
+ {command.versions !== undefined && command.selectedVersion !== undefined && (
+
+
+ Version
+
+
+
+ Command table
+
+
+
+ )}
+ {command.loading === true && command.selected && (
+
+
+ Loading...
+
+
+ )}
+
+ }
+ onClick={(event) => {
+ event.stopPropagation();
+ event.preventDefault();
+ }}
+ />
+ );
+});
+
+CommandItem.displayName = "CommandItem";
+
+export default CommandItem;
+
+export type { CommandItemProps };
diff --git a/src/web/src/views/cli/components/GenerateDialog.tsx b/src/web/src/views/cli/components/GenerateDialog.tsx
new file mode 100644
index 00000000..4f95560a
--- /dev/null
+++ b/src/web/src/views/cli/components/GenerateDialog.tsx
@@ -0,0 +1,101 @@
+import { useState } from "react";
+import { Alert, Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, LinearProgress } from "@mui/material";
+import { cliApi, errorHandlerApi } from "../../../services";
+import { exportModViewProfile, type ProfileCommandTree } from "../utils/commandTreeInitialization";
+import { type CLIModViewProfiles } from "../interfaces";
+
+interface ProfileCommandTrees {
+ [name: string]: ProfileCommandTree;
+}
+
+interface GenerateDialogProps {
+ repoName: string;
+ moduleName: string;
+ profileCommandTrees: ProfileCommandTrees;
+ open: boolean;
+ onClose: (generated: boolean) => void;
+}
+
+const GenerateDialog = (props: GenerateDialogProps) => {
+ const [updating, setUpdating] = useState(false);
+ const [invalidText, setInvalidText] = useState(undefined);
+
+ const handleClose = () => {
+ props.onClose(false);
+ };
+
+ const handleGenerateAll = async () => {
+ const profiles: CLIModViewProfiles = {};
+ Object.values(props.profileCommandTrees).forEach((tree) => {
+ profiles[tree.name] = exportModViewProfile(tree);
+ });
+ const data = {
+ name: props.moduleName,
+ profiles: profiles,
+ };
+
+ setUpdating(true);
+ try {
+ await cliApi.updateCliModule(props.repoName, props.moduleName, data);
+ setUpdating(false);
+ props.onClose(true);
+ } catch (err: any) {
+ console.error(err);
+ setInvalidText(errorHandlerApi.getErrorMessage(err));
+ setUpdating(false);
+ }
+ };
+
+ const handleGenerateModified = async () => {
+ const profiles: CLIModViewProfiles = {};
+ Object.values(props.profileCommandTrees).forEach((tree) => {
+ profiles[tree.name] = exportModViewProfile(tree);
+ });
+ const data = {
+ name: props.moduleName,
+ profiles: profiles,
+ };
+
+ setUpdating(true);
+ try {
+ await cliApi.patchCliModule(props.repoName, props.moduleName, data);
+ setUpdating(false);
+ props.onClose(true);
+ } catch (err: any) {
+ console.error(err);
+ setInvalidText(errorHandlerApi.getErrorMessage(err));
+ setUpdating(false);
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default GenerateDialog;
+export type { ProfileCommandTrees };
diff --git a/src/web/src/views/cli/hooks/index.ts b/src/web/src/views/cli/hooks/index.ts
new file mode 100644
index 00000000..450dd0a6
--- /dev/null
+++ b/src/web/src/views/cli/hooks/index.ts
@@ -0,0 +1,8 @@
+export { useSpecsCommandTree } from "./useSpecsCommandTree";
+export type {
+ CLISpecsCommand,
+ CLISpecsHelp,
+ CLISpecsResource,
+ CLISpecsCommandExample,
+ CLISpecsCommandVersion,
+} from "./useSpecsCommandTree";
diff --git a/src/web/src/views/cli/hooks/useSpecsCommandTree.ts b/src/web/src/views/cli/hooks/useSpecsCommandTree.ts
new file mode 100644
index 00000000..959acf59
--- /dev/null
+++ b/src/web/src/views/cli/hooks/useSpecsCommandTree.ts
@@ -0,0 +1,77 @@
+import { useCallback, useRef } from "react";
+import { cliApi } from "../../../services";
+
+export interface CLISpecsHelp {
+ short: string;
+ lines?: string[];
+}
+
+export interface CLISpecsResource {
+ plane: string;
+ id: string;
+ version: string;
+ subresource?: string;
+}
+
+export interface CLISpecsCommandExample {
+ name: string;
+ commands: string[];
+}
+
+export interface CLISpecsCommandVersion {
+ name: string;
+ stage?: string;
+ resources: CLISpecsResource[];
+ examples?: CLISpecsCommandExample[];
+}
+
+export interface CLISpecsCommand {
+ names: string[];
+ help: CLISpecsHelp;
+ versions: CLISpecsCommandVersion[];
+}
+
+const retrieveCommand = async (names: string[]): Promise => {
+ return await cliApi.getSpecsCommand(names);
+};
+
+const retrieveCommands = async (namesList: string[][]): Promise => {
+ return await cliApi.retrieveCommands(namesList);
+};
+
+export const useSpecsCommandTree = (): ((namesList: string[][]) => Promise) => {
+ const commandCache = useRef(new Map>());
+
+ const fetchCommands = useCallback(
+ async (namesList: string[][]) => {
+ const promiseResults = [];
+ const uncachedNamesList = [];
+ for (const names of namesList) {
+ const cachedPromise = commandCache.current.get(names.join("/"));
+ if (!cachedPromise) {
+ uncachedNamesList.push(names);
+ } else {
+ promiseResults.push(cachedPromise);
+ }
+ }
+ if (uncachedNamesList.length === 0) {
+ return await Promise.all(promiseResults);
+ } else if (uncachedNamesList.length === 1) {
+ const commandPromise = retrieveCommand(uncachedNamesList[0]);
+ commandCache.current.set(uncachedNamesList[0].join("/"), commandPromise);
+ return await Promise.all(promiseResults.concat(commandPromise));
+ } else {
+ const uncachedCommandsPromise = retrieveCommands(uncachedNamesList);
+ uncachedNamesList.forEach((names, idx) => {
+ commandCache.current.set(
+ names.join("/"),
+ uncachedCommandsPromise.then((commands) => commands[idx]),
+ );
+ });
+ return (await Promise.all(promiseResults)).concat(await uncachedCommandsPromise);
+ }
+ },
+ [commandCache],
+ );
+ return fetchCommands;
+};
diff --git a/src/web/src/views/cli/CLIModuleCommon.tsx b/src/web/src/views/cli/interfaces/cliModule.ts
similarity index 100%
rename from src/web/src/views/cli/CLIModuleCommon.tsx
rename to src/web/src/views/cli/interfaces/cliModule.ts
diff --git a/src/web/src/views/cli/interfaces/index.ts b/src/web/src/views/cli/interfaces/index.ts
new file mode 100644
index 00000000..6574c0d7
--- /dev/null
+++ b/src/web/src/views/cli/interfaces/index.ts
@@ -0,0 +1,9 @@
+export type {
+ CLIModView,
+ CLIModViewProfile,
+ CLIModViewProfiles,
+ CLIModViewCommandGroup,
+ CLIModViewCommandGroups,
+ CLIModViewCommand,
+ CLIModViewCommands,
+} from "./cliModule";
diff --git a/src/web/src/views/cli/utils/commandTreeInitialization.ts b/src/web/src/views/cli/utils/commandTreeInitialization.ts
new file mode 100644
index 00000000..f481d795
--- /dev/null
+++ b/src/web/src/views/cli/utils/commandTreeInitialization.ts
@@ -0,0 +1,228 @@
+import {
+ CLIModViewCommand,
+ CLIModViewCommandGroup,
+ CLIModViewCommandGroups,
+ CLIModViewCommands,
+ CLIModViewProfile,
+} from "../interfaces";
+import {
+ CLISpecsCommand,
+ CLISpecsSimpleCommand,
+ CLISpecsSimpleCommandGroup,
+ CLISpecsSimpleCommandTree,
+} from "../components/CLIModuleGenerator";
+import {
+ calculateSelected,
+ type ProfileCTCommandGroup,
+ type ProfileCTCommand,
+ type ProfileCTCommandGroups,
+ type ProfileCTCommandVersion,
+} from "./commandTreeUtils";
+
+export interface ProfileCommandTree {
+ name: string;
+ commandGroups: ProfileCTCommandGroups;
+}
+
+export const decodeProfileCTCommandVersion = (response: any): ProfileCTCommandVersion => {
+ return {
+ name: response.name,
+ stage: response.stage,
+ };
+};
+
+export const decodeProfileCTCommand = (
+ response: CLISpecsCommand,
+ selected: boolean = false,
+ modified: boolean = false,
+ registered: boolean | undefined = undefined,
+ selectedVersion: string | undefined = undefined,
+): ProfileCTCommand => {
+ const versions = response.versions?.map((value: any) => decodeProfileCTCommandVersion(value));
+ const command = {
+ id: response.names.join("/"),
+ names: [...response.names],
+ versions: versions,
+ modified: modified,
+ loading: false,
+ selected: selected,
+ registered: registered,
+ };
+ if (selected) {
+ let version: string | undefined;
+ if (selectedVersion !== undefined) {
+ version = selectedVersion;
+ } else {
+ version = versions ? versions[0].name : undefined;
+ }
+
+ return {
+ ...command,
+ selectedVersion: version,
+ };
+ } else {
+ return command;
+ }
+};
+
+const initializeCommandByModView = (
+ view: CLIModViewCommand | undefined,
+ simpleCommand: CLISpecsSimpleCommand,
+): ProfileCTCommand => {
+ return {
+ id: simpleCommand.names.join("/"),
+ names: simpleCommand.names,
+ modified: false,
+ loading: false,
+ selected: view !== undefined && view.version !== undefined,
+ selectedVersion: view !== undefined ? view.version : undefined,
+ registered: view !== undefined ? view.registered : true,
+ };
+};
+
+const initializeCommandGroupByModView = (
+ view: CLIModViewCommandGroup | undefined,
+ simpleCommandGroup: CLISpecsSimpleCommandGroup,
+): ProfileCTCommandGroup => {
+ const commands =
+ simpleCommandGroup.commands !== undefined
+ ? Object.fromEntries(
+ Object.entries(simpleCommandGroup.commands).map(([key, value]) => [
+ key,
+ initializeCommandByModView(view?.commands?.[key], value),
+ ]),
+ )
+ : undefined;
+ const commandGroups =
+ simpleCommandGroup.commandGroups !== undefined
+ ? Object.fromEntries(
+ Object.entries(simpleCommandGroup.commandGroups).map(([key, value]) => [
+ key,
+ initializeCommandGroupByModView(view?.commandGroups?.[key], value),
+ ]),
+ )
+ : undefined;
+ const leftCommands = Object.entries(view?.commands ?? {})
+ .filter(([key, _]) => commands?.[key] === undefined)
+ .map(([_, value]) => value.names)
+ .map((names) => "`az " + names.join(" ") + "`");
+ const leftCommandGroups = Object.entries(view?.commandGroups ?? {})
+ .filter(([key, _]) => commandGroups?.[key] === undefined)
+ .map(([_, value]) => value.names)
+ .map((names) => "`az " + names.join(" ") + "`");
+ const errors = [];
+ if (leftCommands.length > 0) {
+ errors.push(`Miss commands in aaz: ${leftCommands.join(", ")}`);
+ }
+ if (leftCommandGroups.length > 0) {
+ errors.push(`Miss command groups in aaz: ${leftCommandGroups.join(", ")}`);
+ }
+ if (errors.length > 0) {
+ throw new Error(
+ "\n" +
+ errors.join("\n") +
+ "\nSee: https://azure.github.io/aaz-dev-tools/pages/usage/cli-generator/#miss-command-models.",
+ );
+ }
+ const selected = calculateSelected(commands ?? {}, commandGroups ?? {});
+ return {
+ id: simpleCommandGroup.names.join("/"),
+ names: simpleCommandGroup.names,
+ commands: commands,
+ commandGroups: commandGroups,
+ waitCommand: view?.waitCommand,
+ loading: false,
+ selected: selected,
+ };
+};
+
+export const initializeCommandTreeByModView = (
+ profileName: string,
+ view: CLIModViewProfile | null,
+ simpleTree: CLISpecsSimpleCommandTree,
+): ProfileCommandTree => {
+ const commandGroups = Object.fromEntries(
+ Object.entries(simpleTree.root.commandGroups).map(([key, value]) => [
+ key,
+ initializeCommandGroupByModView(view?.commandGroups?.[key], value),
+ ]),
+ );
+ const leftCommandGroups = Object.entries(view?.commandGroups ?? {})
+ .filter(([key, _]) => commandGroups?.[key] === undefined)
+ .map(([_, value]) => value.names)
+ .map((names) => "`az " + names.join(" ") + "`");
+ if (leftCommandGroups.length > 0) {
+ throw new Error(
+ `\nMiss command groups in aaz: ${leftCommandGroups.join(", ")}\nSee: https://azure.github.io/aaz-dev-tools/pages/usage/cli-generator/#miss-command-models.`,
+ );
+ }
+ return {
+ name: profileName,
+ commandGroups: commandGroups,
+ };
+};
+
+const exportModViewCommand = (command: ProfileCTCommand): CLIModViewCommand | undefined => {
+ if (command.selectedVersion === undefined) {
+ return undefined;
+ }
+
+ return {
+ names: command.names,
+ registered: command.registered!,
+ version: command.selectedVersion!,
+ modified: command.modified,
+ };
+};
+
+const exportModViewCommandGroup = (commandGroup: ProfileCTCommandGroup): CLIModViewCommandGroup | undefined => {
+ if (commandGroup.selected === false) {
+ return undefined;
+ }
+
+ let commands: CLIModViewCommands | undefined = undefined;
+ if (commandGroup.commands !== undefined) {
+ commands = {};
+
+ Object.values(commandGroup.commands!).forEach((value) => {
+ const view = exportModViewCommand(value);
+ if (view !== undefined) {
+ commands![value.names[value.names.length - 1]] = view;
+ }
+ });
+ }
+
+ let commandGroups: CLIModViewCommandGroups | undefined = undefined;
+ if (commandGroup.commandGroups !== undefined) {
+ commandGroups = {};
+
+ Object.values(commandGroup.commandGroups!).forEach((value) => {
+ const view = exportModViewCommandGroup(value);
+ if (view !== undefined) {
+ commandGroups![value.names[value.names.length - 1]] = view;
+ }
+ });
+ }
+ return {
+ names: commandGroup.names,
+ commandGroups: commandGroups,
+ commands: commands,
+ waitCommand: commandGroup.waitCommand,
+ };
+};
+
+export const exportModViewProfile = (tree: ProfileCommandTree): CLIModViewProfile => {
+ const commandGroups: CLIModViewCommandGroups = {};
+
+ Object.values(tree.commandGroups).forEach((value) => {
+ const view = exportModViewCommandGroup(value);
+ if (view !== undefined) {
+ commandGroups[value.names[value.names.length - 1]] = view;
+ }
+ });
+
+ return {
+ name: tree.name,
+ commandGroups: commandGroups,
+ };
+};
diff --git a/src/web/src/views/cli/utils/commandTreeUtils.ts b/src/web/src/views/cli/utils/commandTreeUtils.ts
new file mode 100644
index 00000000..7fc7e678
--- /dev/null
+++ b/src/web/src/views/cli/utils/commandTreeUtils.ts
@@ -0,0 +1,107 @@
+interface ProfileCTCommandGroups {
+ [name: string]: ProfileCTCommandGroup;
+}
+
+interface ProfileCTCommands {
+ [name: string]: ProfileCTCommand;
+}
+
+interface ProfileCTCommandGroup {
+ id: string;
+ names: string[];
+ commandGroups?: ProfileCTCommandGroups;
+ commands?: ProfileCTCommands;
+ waitCommand?: any;
+ loading: boolean;
+ selected?: boolean;
+}
+
+interface ProfileCTCommand {
+ id: string;
+ names: string[];
+ versions?: ProfileCTCommandVersion[];
+ selectedVersion?: string;
+ registered?: boolean;
+ modified: boolean;
+ loading: boolean;
+ selected: boolean;
+}
+
+interface ProfileCTCommandVersion {
+ name: string;
+ stage: string;
+}
+
+function calculateSelected(commands: ProfileCTCommands, commandGroups: ProfileCTCommandGroups): boolean | undefined {
+ const commandsAllSelected = Object.values(commands).reduce((pre, value) => {
+ return pre && value.selected;
+ }, true);
+ const commandsAllUnselected = Object.values(commands).reduce((pre, value) => {
+ return pre && !value.selected;
+ }, true);
+ const commandGroupsAllSelected = Object.values(commandGroups).reduce((pre, value) => {
+ return pre && value.selected === true;
+ }, true);
+ const commandGroupsAllUnselected = Object.values(commandGroups).reduce((pre, value) => {
+ return pre && value.selected === false;
+ }, true);
+ if (commandsAllUnselected && commandGroupsAllUnselected) {
+ return false;
+ } else if (commandsAllSelected && commandGroupsAllSelected) {
+ return true;
+ } else {
+ return undefined;
+ }
+}
+
+function prepareLoadCommandsOfCommandGroup(commandGroup: ProfileCTCommandGroup): [string[][], ProfileCTCommandGroup] {
+ const namesList: string[][] = [];
+ const commands = commandGroup.commands
+ ? Object.fromEntries(
+ Object.entries(commandGroup.commands).map(([key, value]) => {
+ if (value.selected === true && value.versions === undefined && value.loading === false) {
+ namesList.push(value.names);
+ return [
+ key,
+ {
+ ...value,
+ loading: true,
+ },
+ ];
+ }
+ return [key, value];
+ }),
+ )
+ : undefined;
+ const commandGroups = commandGroup.commandGroups
+ ? Object.fromEntries(
+ Object.entries(commandGroup.commandGroups).map(([key, value]) => {
+ const [namesListSub, updatedGroup] = prepareLoadCommandsOfCommandGroup(value);
+ namesList.push(...namesListSub);
+ return [key, updatedGroup];
+ }),
+ )
+ : undefined;
+ if (namesList.length > 0) {
+ return [
+ namesList,
+ {
+ ...commandGroup,
+ commands: commands,
+ commandGroups: commandGroups,
+ },
+ ];
+ } else {
+ return [[], commandGroup];
+ }
+}
+
+export { calculateSelected, prepareLoadCommandsOfCommandGroup };
+
+export type {
+ ProfileCTCommandGroup,
+ ProfileCTCommand,
+ ProfileCTCommandGroups,
+ ProfileCTCommands,
+ ProfileCTCommandVersion,
+};
diff --git a/src/web/src/views/commands/CommandsPage.tsx b/src/web/src/views/commands/CommandsPage.tsx
deleted file mode 100644
index b01987b8..00000000
--- a/src/web/src/views/commands/CommandsPage.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import * as React from "react";
-import withRoot from "../../withRoot";
-import { AppAppBar } from "../../components/AppAppBar";
-
-class CommandsPage extends React.Component {
- render() {
- return (
-
-
- {/*
- Commands Page
- */}
-
- );
- }
-}
-
-export default withRoot(CommandsPage);
diff --git a/src/web/src/views/home/HomePage.tsx b/src/web/src/views/home/HomePage.tsx
index 18ea1ffa..1b291230 100644
--- a/src/web/src/views/home/HomePage.tsx
+++ b/src/web/src/views/home/HomePage.tsx
@@ -2,8 +2,7 @@ import * as React from "react";
import { Typography, Box, Link, Stepper, Step, StepButton, StepContent, TypographyProps } from "@mui/material";
import { styled } from "@mui/material/styles";
-import withRoot from "../../withRoot";
-import { AppAppBar } from "../../components/AppAppBar";
+import { AppNavBar } from "../../components/AppNavBar";
import PageLayout from "../../components/PageLayout";
const MiddlePadding = styled(Box)(() => ({
@@ -33,7 +32,7 @@ function HomePage() {
return (
-
+
- {"Introduce"}
+ {"Introduction"}
@@ -94,11 +93,11 @@ function HomePage() {
}
- {"Go to "}
+ {"Go to the "}
- Introduction
+ introduction
- {" for more details."}
+ {" page in our docs for more details."}
@@ -108,10 +107,10 @@ function HomePage() {
- {"The definition of API in swagger/TypeSpec is required before using AAZDev tool."}
+ {"The definition of API specs in Swagger/TypeSpec is required before using the AAZDev tool."}
- {"Please make sure the API specs has been defined in "}
+ {"Please make sure the API specs have been defined in the "}
azure-rest-api-specs
- {" repo or "}
+ {" repo or the "}
{"Model editors can help you build command models."}
- {"To build command models from swagger/TypeSpec,"}
+ {"To build command models from Swagger/TypeSpec,"}
- {"please use "}
+ {"please use the "}
Workspace
@@ -160,7 +159,7 @@ function HomePage() {
{"To convert command models to CLI code,"}
- {"please use "}
+ {"please use the "}
CLI
@@ -179,4 +178,4 @@ function HomePage() {
);
}
-export default withRoot(HomePage);
+export default HomePage;
diff --git a/src/web/src/views/workspace/WSEditor.tsx b/src/web/src/views/workspace/WSEditor.tsx
deleted file mode 100644
index ac4bb02f..00000000
--- a/src/web/src/views/workspace/WSEditor.tsx
+++ /dev/null
@@ -1,1160 +0,0 @@
-import * as React from "react";
-import {
- Box,
- Dialog,
- Slide,
- Drawer,
- Toolbar,
- DialogTitle,
- DialogContent,
- DialogActions,
- LinearProgress,
- Button,
- List,
- ListSubheader,
- Paper,
- ListItemButton,
- ListItemIcon,
- Checkbox,
- ListItemText,
- ListItem,
- TextField,
- Alert,
-} from "@mui/material";
-import { useParams } from "react-router";
-import { TransitionProps } from "@mui/material/transitions";
-import WSEditorSwaggerPicker from "./WSEditorSwaggerPicker";
-import WSEditorToolBar from "./WSEditorToolBar";
-import WSEditorCommandTree, { CommandTreeLeaf, CommandTreeNode } from "./WSEditorCommandTree";
-import WSEditorCommandGroupContent, {
- CommandGroup,
- DecodeResponseCommandGroup,
- ResponseCommandGroup,
- ResponseCommandGroups,
-} from "./WSEditorCommandGroupContent";
-import WSEditorCommandContent, {
- Command,
- Resource,
- DecodeResponseCommand,
- ResponseCommand,
-} from "./WSEditorCommandContent";
-import WSEditorClientConfigDialog from "./WSEditorClientConfig";
-import { getTypespecRPResourcesOperations } from "../../typespec";
-import { workspaceApi, specsApi, errorHandlerApi } from "../../services";
-
-interface CommandGroupMap {
- [id: string]: CommandGroup;
-}
-
-interface CommandMap {
- [id: string]: Command;
-}
-
-interface WSEditorProps {
- params: {
- workspaceName: string;
- };
-}
-
-interface WSEditorState {
- name: string;
- workspaceUrl: string;
- plane: string;
- source: string;
- clientConfigurable: boolean;
-
- selected: Command | CommandGroup | null;
- reloadTimestamp: number | null;
- expanded: Set;
-
- commandMap: CommandMap;
- commandGroupMap: CommandGroupMap;
- commandTree: CommandTreeNode[];
-
- showSwaggerResourcePicker: boolean;
- showSwaggerReloadDialog: boolean;
- showClientConfigDialog: boolean;
- showExportDialog: boolean;
- showDeleteDialog: boolean;
- showModifyDialog: boolean;
-}
-
-const swaggerResourcePickerTransition = React.forwardRef(function swaggerResourcePickerTransition(
- props: TransitionProps & { children: React.ReactElement },
- ref: React.Ref,
-) {
- return ;
-});
-
-const drawerWidth = 300;
-
-class WSEditor extends React.Component {
- constructor(props: WSEditorProps) {
- super(props);
- this.state = {
- name: this.props.params.workspaceName,
- workspaceUrl: `/AAZ/Editor/Workspaces/${this.props.params.workspaceName}`,
- plane: "",
- source: "",
- clientConfigurable: false,
- selected: null,
- reloadTimestamp: null,
- expanded: new Set(),
- commandMap: {},
- commandGroupMap: {},
- commandTree: [],
- showSwaggerResourcePicker: false,
- showSwaggerReloadDialog: false,
- showClientConfigDialog: false,
- showExportDialog: false,
- showDeleteDialog: false,
- showModifyDialog: false,
- };
- }
-
- componentDidMount() {
- this.loadWorkspace();
- }
-
- loadWorkspace = async (preSelectedId?: string | null) => {
- const { workspaceUrl } = this.state;
- if (preSelectedId === undefined) {
- preSelectedId = this.state.selected?.id;
- }
-
- try {
- const planeNames = await specsApi.getPlaneNames();
- const workspaceData = await workspaceApi.getWorkspace(workspaceUrl);
- const reloadTimestamp = Date.now();
- const commandMap: CommandMap = {};
- const commandGroupMap: CommandGroupMap = {};
-
- const buildCommand = (command_1: ResponseCommand): CommandTreeLeaf => {
- const cmd: Command = DecodeResponseCommand(command_1);
- commandMap[cmd.id] = cmd;
- return {
- id: cmd.id,
- names: [...cmd.names],
- };
- };
-
- const buildCommandGroup = (commandGroup_1: ResponseCommandGroup): CommandTreeNode => {
- const group: CommandGroup = DecodeResponseCommandGroup(commandGroup_1);
-
- commandGroupMap[group.id] = group;
-
- const node: CommandTreeNode = {
- id: group.id,
- names: [...group.names],
- canDelete: group.canDelete,
- };
-
- if (typeof commandGroup_1.commands === "object" && commandGroup_1.commands !== null) {
- node["leaves"] = [];
-
- for (const name in commandGroup_1.commands) {
- const subLeave = buildCommand(commandGroup_1.commands[name]);
- node["leaves"].push(subLeave);
- }
- node["leaves"].sort((a, b) => a.id.localeCompare(b.id));
- if (node["leaves"].length > 0) {
- node.canDelete = false;
- }
- }
-
- if (typeof commandGroup_1.commandGroups === "object" && commandGroup_1.commandGroups !== null) {
- node["nodes"] = [];
- for (const name_1 in commandGroup_1.commandGroups) {
- const subNode = buildCommandGroup(commandGroup_1.commandGroups[name_1]);
- node["nodes"].push(subNode);
- if (!subNode.canDelete) {
- node.canDelete = false;
- }
- }
- node["nodes"].sort((a_1, b_1) => a_1.id.localeCompare(b_1.id));
- }
-
- if ((node["leaves"]?.length ?? 0) > 1) {
- node.canDelete = false;
- }
- group.canDelete = node.canDelete;
- return node;
- };
-
- const commandTree: CommandTreeNode[] = [];
-
- if (workspaceData.commandTree.commandGroups) {
- const cmdGroups: ResponseCommandGroups = workspaceData.commandTree.commandGroups;
- for (const key in cmdGroups) {
- commandTree.push(buildCommandGroup(cmdGroups[key]));
- }
- commandTree.sort((a_2, b_2) => a_2.id.localeCompare(b_2.id));
- }
-
- let selected: Command | CommandGroup | null = null;
-
- if (preSelectedId != null) {
- if (preSelectedId.startsWith("command:")) {
- let id: string = preSelectedId;
- if (id in commandMap) {
- selected = commandMap[id];
- } else {
- id = "group:" + id.slice(8);
- let parts = id.split("/");
- while (parts.length > 1 && !(id in commandGroupMap)) {
- parts = parts.slice(0, -1);
- id = parts.join("/");
- }
- if (id in commandGroupMap) {
- selected = commandGroupMap[id];
- }
- }
- } else if (preSelectedId.startsWith("group:")) {
- let id_1: string = preSelectedId;
- let parts_1 = id_1.split("/");
- while (parts_1.length > 1 && !(id_1 in commandGroupMap)) {
- parts_1 = parts_1.slice(0, -1);
- id_1 = parts_1.join("/");
- }
- if (id_1 in commandGroupMap) {
- selected = commandGroupMap[id_1];
- }
- }
- }
-
- if (selected === null && commandTree.length > 0) {
- selected = commandGroupMap[commandTree[0].id];
- }
-
- // when the plane name not included in the built-in planes, it is a client configurable plane
- const clientConfigurable = !planeNames.includes(workspaceData.plane);
- this.setState((preState) => {
- const newExpanded = new Set();
-
- // clean up removed group Id
- preState.expanded.forEach((value) => {
- if (value in commandGroupMap) {
- newExpanded.add(value);
- }
- });
-
- // expand new groupId by default
- for (const groupId in commandGroupMap) {
- if (!(groupId in preState.commandGroupMap)) {
- newExpanded.add(groupId);
- }
- }
-
- return {
- ...preState,
- plane: workspaceData.plane,
- source: workspaceData.source,
- clientConfigurable: clientConfigurable,
- commandTree: commandTree,
- selected: selected,
- reloadTimestamp: reloadTimestamp,
- commandMap: commandMap,
- commandGroupMap: commandGroupMap,
- expanded: newExpanded,
- };
- });
-
- if (selected) {
- let expandedId = selected.id;
- if (expandedId.startsWith("command:")) {
- expandedId = expandedId.replace("command:", "group:").split("/").slice(0, -1).join("/");
- }
- const expandedIdParts = expandedId.split("/");
- this.setState((preState) => {
- const newExpanded = new Set(preState.expanded);
- expandedIdParts.forEach((_value, idx) => {
- newExpanded.add(expandedIdParts.slice(0, idx + 1).join("/"));
- });
- return {
- ...preState,
- expanded: newExpanded,
- };
- });
- }
-
- if (clientConfigurable) {
- const clientConfig = await this.getWorkspaceClientConfig(workspaceUrl);
- if (clientConfig == null) {
- this.showClientConfigDialog();
- return;
- }
- }
-
- if (commandTree.length === 0) {
- this.showSwaggerResourcePicker();
- }
- } catch (err) {
- return console.error(err);
- }
- };
-
- getWorkspaceClientConfig = async (workspaceUrl: string) => {
- return await workspaceApi.getWorkspaceClientConfig(workspaceUrl);
- };
-
- showClientConfigDialog = () => {
- this.setState({ showClientConfigDialog: true });
- };
-
- showSwaggerResourcePicker = () => {
- this.setState({ showSwaggerResourcePicker: true });
- };
-
- showSwaggerReloadDialog = () => {
- this.setState({ showSwaggerReloadDialog: true });
- };
-
- handleSwaggerReloadDialogClose = async (reloaded: boolean) => {
- if (reloaded) {
- await this.loadWorkspace();
- }
- this.setState({
- showSwaggerReloadDialog: false,
- });
- };
-
- handleSwaggerResourcePickerClose = (updated: boolean) => {
- if (updated) {
- this.loadWorkspace();
- }
- this.setState({
- showSwaggerResourcePicker: false,
- });
- };
-
- handleBackToHomepage = (blank: boolean) => {
- if (blank) {
- window.open("/?#/workspace", "_blank");
- } else {
- window.location.href = "/?#/workspace";
- }
- };
-
- handleGenerate = () => {
- this.setState({
- showExportDialog: true,
- });
- };
-
- handleGenerationClose = (_exported: boolean, showClientConfigDialog: boolean) => {
- this.setState({
- showExportDialog: false,
- });
- if (showClientConfigDialog) {
- this.setState({
- showClientConfigDialog: true,
- });
- }
- };
-
- handleDelete = () => {
- this.setState({
- showDeleteDialog: true,
- });
- };
-
- handleDeleteClose = (deleted: boolean) => {
- this.setState({
- showDeleteDialog: false,
- });
- if (deleted) {
- this.handleBackToHomepage(false);
- }
- };
-
- handleModify = () => {
- this.setState({
- showModifyDialog: true,
- });
- };
-
- handleModifyClose = (newWSName: string | null) => {
- this.setState({
- showModifyDialog: false,
- });
- if (!newWSName) {
- return;
- }
- setTimeout(() => {
- const target_url = `/?#/workspace/` + newWSName;
- window.location.href = target_url;
- window.location.reload();
- });
- };
-
- handleCommandTreeSelect = (nodeId: string) => {
- if (nodeId.startsWith("command:")) {
- this.setState((preState) => {
- const selected = preState.commandMap[nodeId];
- return {
- ...preState,
- selected: selected,
- };
- });
- } else if (nodeId.startsWith("group:")) {
- this.setState((preState) => {
- const selected = preState.commandGroupMap[nodeId];
- return {
- ...preState,
- selected: selected,
- };
- });
- }
- };
-
- handleCommandGroupUpdate = (commandGroup: CommandGroup | null) => {
- this.loadWorkspace(commandGroup?.id);
- };
-
- handleCommandUpdate = (command: Command | null) => {
- this.loadWorkspace(command?.id);
- };
-
- handleCommandTreeToggle = (nodeIds: string[]) => {
- const newExpanded = new Set(nodeIds);
- this.setState({
- expanded: newExpanded,
- });
- };
-
- handleClientConfigDialogClose = (updated: boolean) => {
- this.setState({
- showClientConfigDialog: false,
- });
- if (updated) {
- this.loadWorkspace();
- }
- };
-
- render() {
- const {
- showSwaggerResourcePicker,
- showSwaggerReloadDialog,
- showExportDialog,
- showDeleteDialog,
- showModifyDialog,
- plane,
- source,
- name,
- commandTree,
- selected,
- reloadTimestamp,
- workspaceUrl,
- expanded,
- showClientConfigDialog,
- clientConfigurable,
- } = this.state;
- const expandedIds: string[] = [];
- expanded.forEach((expandId) => {
- expandedIds.push(expandId);
- });
- return (
-
- {
- this.handleBackToHomepage(true);
- }}
- onGenerate={this.handleGenerate}
- onDelete={this.handleDelete}
- onModify={this.handleModify}
- >
-
-
-
-
- {selected != null && (
-
- )}
-
-
-
-
- {selected != null && selected.id.startsWith("group:") && (
-
- )}
- {selected != null && selected.id.startsWith("command:") && (
-
- )}
-
-
-
-
- {showModifyDialog && (
-
- )}
- {showDeleteDialog && (
-
- )}
- {showExportDialog && (
-
- )}
- {showSwaggerReloadDialog && (
-
- )}
- {showClientConfigDialog && (
-
- )}
-
- );
- }
-}
-
-interface WSEditorExportDialogProps {
- workspaceUrl: string;
- open: boolean;
- clientConfigurable: boolean;
- onClose: (exported: boolean, showClientConfigDialog: boolean) => void;
-}
-
-interface WSEditorExportDialogState {
- updating: boolean;
- invalidText: string | undefined;
- clientConfigOOD: boolean;
-}
-class WSEditorExportDialog extends React.Component {
- constructor(props: WSEditorExportDialogProps) {
- super(props);
- this.state = {
- updating: false,
- invalidText: undefined,
- clientConfigOOD: false,
- };
- }
-
- componentDidMount(): void {
- if (this.props.clientConfigurable) {
- this.verifyClientConfig();
- }
- }
-
- handleClose = () => {
- this.props.onClose(false, false);
- };
-
- verifyClientConfig = async () => {
- this.setState({ updating: true });
- try {
- await workspaceApi.verifyClientConfig(this.props.workspaceUrl);
- this.setState({ clientConfigOOD: false, updating: false });
- } catch (err: any) {
- // catch 409 error
- if (errorHandlerApi.isHttpError(err, 409)) {
- this.setState({
- invalidText: `The client config in this workspace is out of date. Please refresh it first.`,
- clientConfigOOD: true,
- updating: false,
- });
- return;
- } else {
- console.error(err);
- this.setState({
- invalidText: errorHandlerApi.getErrorMessage(err),
- updating: false,
- });
- }
- }
- };
-
- inheritClientConfig = async () => {
- this.setState({ updating: true });
- try {
- await workspaceApi.inheritClientConfig(this.props.workspaceUrl);
- this.setState({ clientConfigOOD: false, updating: false });
- this.props.onClose(false, true);
- } catch (err: any) {
- console.error(err);
- this.setState({
- invalidText: errorHandlerApi.getErrorMessage(err),
- updating: false,
- });
- }
- };
-
- handleExport = async () => {
- this.setState({ updating: true });
-
- try {
- await workspaceApi.generateWorkspace(this.props.workspaceUrl);
- this.setState({ updating: false });
- this.props.onClose(false, false);
- } catch (err: any) {
- console.error(err);
- this.setState({
- invalidText: errorHandlerApi.getErrorMessage(err),
- updating: false,
- });
- }
- };
-
- render(): React.ReactNode {
- const { updating, invalidText, clientConfigOOD } = this.state;
- return (
-
- );
- }
-}
-
-function WSEditorDeleteDialog(props: { workspaceName: string; open: boolean; onClose: (deleted: boolean) => void }) {
- const [updating, setUpdating] = React.useState(false);
- const [invalidText, setInvalidText] = React.useState(undefined);
- const [confirmName, setConfirmName] = React.useState(undefined);
-
- const handleClose = () => {
- props.onClose(false);
- };
-
- const handleDelete = () => {
- setUpdating(true);
- workspaceApi
- .deleteWorkspace(props.workspaceName)
- .then(() => {
- setUpdating(false);
- props.onClose(true);
- })
- .catch((err: any) => {
- console.error(err);
- setInvalidText(errorHandlerApi.getErrorMessage(err));
- setUpdating(false);
- });
- };
-
- return (
-
- );
-}
-
-interface WSEditorSwaggerReloadDialogProps {
- workspaceName: string;
- workspaceUrl: string;
- open: boolean;
- source: string;
- onClose: (exported: boolean) => void;
-}
-
-interface WSEditorSwaggerReloadDialogState {
- updating: boolean;
- invalidText?: string;
- resourceOptions: Resource[];
- selectedResources: Set;
-}
-
-class WSEditorSwaggerReloadDialog extends React.Component<
- WSEditorSwaggerReloadDialogProps,
- WSEditorSwaggerReloadDialogState
-> {
- constructor(props: WSEditorSwaggerReloadDialogProps) {
- super(props);
- this.state = {
- updating: false,
- invalidText: undefined,
- resourceOptions: [],
- selectedResources: new Set(),
- };
- }
-
- componentDidMount() {
- this.loadResourceOptions();
- }
-
- loadResourceOptions = async () => {
- this.setState({
- invalidText: undefined,
- updating: true,
- });
- try {
- const resources: Resource[] = await workspaceApi.getWorkspaceResources(this.props.workspaceUrl);
- this.setState({
- updating: false,
- resourceOptions: resources,
- selectedResources: new Set(resources.map((resource) => resource.id)),
- });
- } catch (err: any) {
- console.error(err);
- this.setState({
- invalidText: errorHandlerApi.getErrorMessage(err),
- updating: false,
- });
- }
- };
-
- handleClose = () => {
- this.props.onClose(false);
- };
-
- handleReload = async () => {
- const { selectedResources, resourceOptions } = this.state;
- const data = {
- resources: resourceOptions
- .filter((option) => selectedResources.has(option.id))
- .map((option) => {
- return {
- id: option.id,
- version: option.version,
- };
- }),
- };
-
- if (data.resources.length === 0) {
- this.props.onClose(false);
- return;
- }
-
- this.setState({
- invalidText: undefined,
- updating: true,
- });
-
- try {
- if (this.props.source.toLowerCase() === "typespec") {
- const swaggerDefault = await workspaceApi.getWorkspaceSwaggerDefault(this.props.workspaceName);
- const { modNames, rpName, source } = swaggerDefault;
- if (!modNames || modNames.length === 0 || !rpName || !source || source.toLowerCase() !== "typespec") {
- this.setState({
- invalidText: "Invalid workspace info",
- updating: false,
- });
- return;
- }
- const resourceProviderUrl =
- "/Swagger/Specs/" +
- swaggerDefault.plane +
- "/" +
- swaggerDefault.modNames.join("/") +
- `/ResourceProviders/${swaggerDefault.rpName}/TypeSpec`;
- const requestBody = {
- version: resourceOptions[0].version,
- resources: data.resources,
- resourceProviderUrl: resourceProviderUrl,
- };
- console.log("request emitter data: ", requestBody);
- const emitterOptionRes = await getTypespecRPResourcesOperations(requestBody);
- console.log("emitterResourceOptionRes: ", emitterOptionRes);
- if (emitterOptionRes.length === 0) {
- this.setState({
- invalidText: "Invalid resource operation emitter info",
- updating: false,
- });
- return;
- }
- data.resources = emitterOptionRes;
- await workspaceApi.reloadTypespecResources(this.props.workspaceUrl, data);
- } else {
- await workspaceApi.reloadSwaggerResources(this.props.workspaceUrl, data);
- }
-
- this.setState({
- updating: false,
- });
- this.props.onClose(true);
- } catch (err: any) {
- console.error(err);
- this.setState({
- invalidText: errorHandlerApi.getErrorMessage(err),
- updating: false,
- });
- }
- };
-
- onSelectedAllClick = () => {
- this.setState((preState) => {
- return {
- ...preState,
- selectedResources:
- preState.selectedResources.size > 0 ? new Set() : new Set(preState.resourceOptions.map((op) => op.id)),
- };
- });
- };
-
- onResourceItemClick = (resourceId: string) => {
- return () => {
- this.setState((preState) => {
- const selectedResources = new Set(preState.selectedResources);
- if (selectedResources.has(resourceId)) {
- selectedResources.delete(resourceId);
- } else {
- selectedResources.add(resourceId);
- }
- return {
- ...preState,
- selectedResources: selectedResources,
- };
- });
- };
- };
-
- render() {
- const { invalidText, selectedResources, updating, resourceOptions } = this.state;
-
- return (
-
- );
- }
-}
-
-interface WSRenameDialogProps {
- workspaceUrl: string;
- workspaceName: string;
- open: boolean;
- onClose: (newWSName: string | null) => void;
-}
-
-interface WSRenameDialogState {
- newWSName: string;
- invalidText?: string;
- updating: boolean;
-}
-
-class WSRenameDialog extends React.Component {
- constructor(props: WSRenameDialogProps) {
- super(props);
- this.state = {
- newWSName: this.props.workspaceName,
- updating: false,
- };
- }
-
- handleModify = () => {
- const { newWSName } = this.state;
- const { workspaceUrl, workspaceName } = this.props;
-
- const nName = newWSName.trim();
- if (nName.length < 1) {
- this.setState({
- invalidText: `Field 'Name' is required.`,
- });
- return;
- }
-
- this.setState({
- invalidText: undefined,
- });
- this.setState({
- updating: true,
- });
-
- if (workspaceName === nName) {
- this.setState({
- updating: false,
- });
- this.props.onClose(null);
- } else {
- workspaceApi
- .renameWorkspace(workspaceUrl, nName)
- .then((res: any) => {
- this.setState({
- updating: false,
- });
- this.props.onClose(res.name);
- })
- .catch((err: any) => {
- this.setState({
- updating: false,
- invalidText: errorHandlerApi.getErrorMessage(err),
- });
- });
- }
- };
-
- handleClose = () => {
- this.setState({
- invalidText: undefined,
- });
- this.props.onClose(null);
- };
-
- render() {
- const { invalidText, updating } = this.state;
- return (
-
- );
- }
-}
-
-const WSEditorWrapper = (props: any) => {
- const params = useParams();
-
- return ;
-};
-
-export { WSEditorWrapper as WSEditor };
diff --git a/src/web/src/views/workspace/WSEditorCommandArgumentsContent.tsx b/src/web/src/views/workspace/WSEditorCommandArgumentsContent.tsx
deleted file mode 100644
index b88c364e..00000000
--- a/src/web/src/views/workspace/WSEditorCommandArgumentsContent.tsx
+++ /dev/null
@@ -1,2627 +0,0 @@
-import {
- Alert,
- Box,
- Button,
- ButtonBase,
- CardContent,
- Checkbox,
- Dialog,
- DialogActions,
- DialogContent,
- DialogTitle,
- FormControlLabel,
- InputLabel,
- LinearProgress,
- Radio,
- RadioGroup,
- styled,
- Switch,
- TextField,
- Typography,
- TypographyProps,
-} from "@mui/material";
-import { ChevronRight } from "@mui/icons-material";
-import AddIcon from "@mui/icons-material/Add";
-import ArrowBackIosIcon from "@mui/icons-material/ArrowBackIos";
-import CallSplitSharpIcon from "@mui/icons-material/CallSplitSharp";
-import EditIcon from "@mui/icons-material/Edit";
-import ImportExportIcon from "@mui/icons-material/ImportExport";
-
-import { commandApi, errorHandlerApi } from "../../services";
-import pluralize from "pluralize";
-import React, { useEffect, useState } from "react";
-import WSECArgumentSimilarPicker, { ArgSimilarTree, BuildArgSimilarTree } from "./argument/WSECArgumentSimilarPicker";
-import {
- CardTitleTypography,
- ExperimentalTypography,
- LongHelpTypography,
- PreviewTypography,
- ShortHelpPlaceHolderTypography,
- ShortHelpTypography,
- SmallExperimentalTypography,
- SmallPreviewTypography,
- StableTypography,
- SubtitleTypography,
-} from "./WSEditorTheme";
-
-function WSEditorCommandArgumentsContent(props: {
- commandUrl: string;
- args: CMDArg[];
- clsArgDefineMap: ClsArgDefinitionMap;
- onReloadArgs: () => Promise;
- onAddSubCommand: (argVar: string, subArgOptions: { var: string; options: string }[], argStackNames: string[]) => void;
-}) {
- const [displayArgumentDialog, setDisplayArgumentDialog] = useState(false);
- const [editArg, setEditArg] = useState(undefined);
- const [, setEditArgIdxStack] = useState(undefined);
- const [displayFlattenDialog, setDisplayFlattenDialog] = useState(false);
- const [displayUnwrapClsDialog, setDisplayUnwrapClsDialog] = useState(false);
-
- const handleArgumentDialogClose = async (updated: boolean) => {
- if (updated) {
- props.onReloadArgs();
- }
- setDisplayArgumentDialog(false);
- setEditArg(undefined);
- setEditArgIdxStack(undefined);
- };
-
- const handleEditArgument = (arg: CMDArg, argIdxStack: ArgIdx[]) => {
- setEditArg(arg);
- setEditArgIdxStack(argIdxStack);
- setDisplayArgumentDialog(true);
- };
-
- const handleFlattenDialogClose = async (flattened: boolean) => {
- if (flattened) {
- props.onReloadArgs();
- }
- setDisplayFlattenDialog(false);
- setEditArg(undefined);
- setEditArgIdxStack(undefined);
- };
-
- const handleArgumentFlatten = (arg: CMDArg, argIdxStack: ArgIdx[]) => {
- setEditArg(arg);
- setEditArgIdxStack(argIdxStack);
- setDisplayFlattenDialog(true);
- };
-
- const handleUnwrapClsDialogClose = async (unwrapped: boolean) => {
- if (unwrapped) {
- props.onReloadArgs();
- }
- setDisplayUnwrapClsDialog(false);
- setEditArg(undefined);
- setEditArgIdxStack(undefined);
- };
-
- const handleUnwrapClsArgument = (arg: CMDArg, argIdxStack: ArgIdx[]) => {
- setEditArg(arg);
- setEditArgIdxStack(argIdxStack);
- setDisplayUnwrapClsDialog(true);
- };
-
- const handleAddSubcommand = (arg: CMDArg, argIdxStack: ArgIdx[]) => {
- const argVar = arg.var;
- const argStackNames = argIdxStack.map((argIdx) => {
- let name = argIdx.displayKey;
- while (name.startsWith("-")) {
- name = name.slice(1);
- }
- if (name.endsWith("[]") || name.endsWith("{}")) {
- name = name.slice(0, name.length - 2);
- name = pluralize.singular(name);
- }
- return name;
- });
- let a: CMDArgBase | undefined = arg;
- if (a.type.startsWith("@")) {
- const clsName = (a as CMDClsArg).clsName;
- a = props.clsArgDefineMap[clsName];
- }
- if (a.type.startsWith("dict<")) {
- a = (a as CMDDictArgBase).item;
- } else if (a.type.startsWith("array<")) {
- a = (a as CMDArrayArgBase).item;
- }
- let subArgOptions: { var: string; options: string }[] = [];
- if (a !== undefined) {
- let subArgs;
- if (a.type.startsWith("@")) {
- const clsName = (a as CMDClsArg).clsName;
- subArgs = (props.clsArgDefineMap[clsName] as CMDObjectArgBase).args;
- } else {
- subArgs = (a as CMDObjectArg).args;
- }
- subArgOptions = subArgs.map((value) => {
- return {
- var: value.var,
- options: value.options.join(" "),
- };
- });
- }
-
- props.onAddSubCommand(argVar, subArgOptions, argStackNames);
- };
-
- return (
-
-
-
- [ ARGUMENT ]
-
-
-
-
- {displayArgumentDialog && (
-
- )}
- {displayFlattenDialog && (
-
- )}
- {displayUnwrapClsDialog && (
-
- )}
-
- );
-}
-
-interface ArgIdx {
- var: string;
- displayKey: string;
-}
-
-function ArgumentNavigation(props: {
- commandUrl: string;
- args: CMDArg[];
- clsArgDefineMap: ClsArgDefinitionMap;
- onEdit: (arg: CMDArg, argIdxStack: ArgIdx[]) => void;
- onFlatten: (arg: CMDArg, argIdxStack: ArgIdx[]) => void;
- onUnwrap: (arg: CMDArg, argIdxStack: ArgIdx[]) => void;
- onAddSubcommand: (arg: CMDArg, argIdxStack: ArgIdx[]) => void;
-}) {
- const [argIdxStack, setArgIdxStack] = useState([]);
-
- const getArgProps = (
- selectedArgBase: CMDArgBase,
- ): { title: string; props: CMDArg[]; flattenArgVar: string | undefined } | undefined => {
- if (selectedArgBase.type.startsWith("@")) {
- const clsArgDefine = props.clsArgDefineMap[(selectedArgBase as CMDClsArgBase).clsName];
- const clsArgProps = getArgProps(clsArgDefine);
- if (clsArgProps !== undefined && clsArgDefine.type === "object") {
- clsArgProps!.flattenArgVar = (selectedArgBase as CMDClsArg).var;
- }
- return clsArgProps;
- }
- if (selectedArgBase.type === "object") {
- return {
- title: "Props",
- props: (selectedArgBase as CMDObjectArgBase).args,
- flattenArgVar: (selectedArgBase as CMDObjectArg).var,
- };
- } else if (selectedArgBase.type.startsWith("dict<")) {
- const item = (selectedArgBase as CMDDictArgBase).item;
- const itemProps = item ? getArgProps(item) : undefined;
- if (!itemProps) {
- return undefined;
- }
- return {
- title: "Dict Element Props",
- props: itemProps.props,
- flattenArgVar: undefined,
- };
- } else if (selectedArgBase.type.startsWith("array<")) {
- const itemProps = getArgProps((selectedArgBase as CMDArrayArgBase).item);
- if (!itemProps) {
- return undefined;
- }
- return {
- title: "Array Element Props",
- props: itemProps.props,
- flattenArgVar: undefined,
- };
- } else {
- return undefined;
- }
- };
-
- const getSelectedArg = (stack: ArgIdx[]): CMDArg | undefined => {
- if (stack.length === 0) {
- return undefined;
- } else {
- let args: CMDArg[] = [...props.args];
- let selectedArg: CMDArg | undefined = undefined;
- for (const i in stack) {
- const argVar = stack[i].var;
- selectedArg = args.find((arg) => arg.var === argVar);
- if (!selectedArg) {
- break;
- }
- args = getArgProps(selectedArg)?.props ?? [];
- }
- return selectedArg;
- }
- };
-
- useEffect(() => {
- setArgIdxStack([]);
- }, [props.commandUrl]);
-
- useEffect(() => {
- // update argument idx stack
- const stack = [...argIdxStack];
- while (stack.length > 0 && !getSelectedArg(stack)) {
- stack.pop();
- }
- setArgIdxStack(stack);
- }, [props.args, props.clsArgDefineMap]);
-
- const handleSelectSubArg = (subArgVar: string) => {
- let subArg;
- if (argIdxStack.length > 0) {
- const arg = getSelectedArg(argIdxStack);
- if (!arg) {
- return;
- }
- subArg = getArgProps(arg)?.props.find((a) => a.var === subArgVar);
- } else {
- subArg = props.args.find((a) => a.var === subArgVar);
- }
-
- if (!subArg) {
- return;
- }
- const argIdx: ArgIdx = {
- var: subArg.var,
- displayKey: subArg.options[0],
- };
- if (argIdxStack.length === 0) {
- if (argIdx.displayKey.length === 1) {
- argIdx.displayKey = `-${argIdx.displayKey}`;
- } else {
- argIdx.displayKey = `--${argIdx.displayKey}`;
- }
- }
-
- let argType = subArg.type;
- if (argType.startsWith("@")) {
- argType = props.clsArgDefineMap[(subArg as CMDClsArg).clsName].type;
- }
- if (argType.startsWith("dict<")) {
- argIdx.displayKey += "{}";
- } else if (argType.startsWith("array<")) {
- argIdx.displayKey += "[]";
- }
-
- setArgIdxStack([...argIdxStack, argIdx]);
- };
-
- const handleChangeArgIdStack = (end: number) => {
- setArgIdxStack(argIdxStack.slice(0, end));
- };
-
- const buildArgumentReviewer = () => {
- const selectedArg = getSelectedArg(argIdxStack);
- if (!selectedArg) {
- return <>>;
- }
-
- const stage = selectedArg.stage;
-
- return (
-
-
-
- {stage === "Stable" && {stage}}
- {stage === "Preview" && {stage}}
- {stage === "Experimental" && {stage}}
-
- {
- props.onEdit(selectedArg, argIdxStack);
- }}
- onUnwrap={() => {
- props.onUnwrap(selectedArg, argIdxStack);
- }}
- />
-
- );
- };
-
- const buildArgumentPropsReviewer = () => {
- if (argIdxStack.length === 0) {
- if (props.args.length === 0) {
- return <>>;
- }
- return (
-
- );
- } else {
- const selectedArg = getSelectedArg(argIdxStack);
- if (!selectedArg) {
- return <>>;
- }
- const argProps = getArgProps(selectedArg);
- if (!argProps) {
- return <>>;
- }
- const canFlatten = argProps.flattenArgVar !== undefined;
- return (
- {
- props.onFlatten(selectedArg!, argIdxStack);
- }
- : undefined
- }
- onAddSubcommand={() => {
- props.onAddSubcommand(selectedArg!, argIdxStack);
- }}
- onSelectSubArg={handleSelectSubArg}
- />
- );
- }
- };
-
- return (
-
- {argIdxStack.length > 0 && {buildArgumentReviewer()}}
- {buildArgumentPropsReviewer()}
-
- );
-}
-
-const NavBarItemTypography = styled(Typography)(({ theme }) => ({
- color: theme.palette.primary.main,
- fontFamily: "'Work Sans', sans-serif",
- fontSize: 14,
- fontWeight: 400,
-}));
-
-const NavBarItemHightLightedTypography = styled(NavBarItemTypography)(() => ({
- color: "#5d64cf",
-}));
-
-function ArgNavBar(props: { argIdxStack: ArgIdx[]; onChangeArgIdStack: (end: number) => void }) {
- return (
-
-
- {
- props.onChangeArgIdStack(0);
- }}
- >
-
-
- {props.argIdxStack.slice(0, -1).map((argIdx, index) => (
- {
- props.onChangeArgIdStack(index + 1);
- }}
- >
-
- {index > 0 ? `.${argIdx.displayKey}` : argIdx.displayKey}
-
-
- ))}
- {
- props.onChangeArgIdStack(props.argIdxStack.length);
- }}
- >
-
- {props.argIdxStack.length > 1
- ? `.${props.argIdxStack[props.argIdxStack.length - 1].displayKey}`
- : props.argIdxStack[props.argIdxStack.length - 1].displayKey}
-
-
-
-
- );
-}
-
-const spliceArgOptionsString = (arg: CMDArg, depth: number) => {
- let optionsString = arg.options
- .map((option) => {
- if (depth === 0) {
- if (option.length === 1) {
- return "-" + option;
- } else {
- return "--" + option;
- }
- } else {
- return "." + option;
- }
- })
- .join(" ");
-
- if ((arg as CMDArrayArg).singularOptions) {
- const singularOptionString = (arg as CMDArrayArg)
- .singularOptions!.map((option) => {
- if (depth === 0) {
- if (option.length === 1) {
- return "-" + option;
- } else {
- return "--" + option;
- }
- } else {
- return "." + option;
- }
- })
- .join(" ");
- optionsString += ` (${singularOptionString})`;
- } else if ((arg as CMDClsArg).singularOptions) {
- const singularOptionString = (arg as CMDArrayArg)
- .singularOptions!.map((option) => {
- if (depth === 0) {
- if (option.length === 1) {
- return "-" + option;
- } else {
- return "--" + option;
- }
- } else {
- return "." + option;
- }
- })
- .join(" ");
- optionsString += ` (${singularOptionString})`;
- }
-
- return optionsString;
-};
-
-const ArgNameTypography = styled(Typography)(({ theme }) => ({
- color: theme.palette.primary.main,
- fontFamily: "'Roboto Condensed', sans-serif",
- fontSize: 26,
- fontWeight: 700,
-}));
-
-const ArgTypeTypography = styled(Typography)(({ theme }) => ({
- color: theme.palette.primary.main,
- fontFamily: "'Roboto Condensed', sans-serif",
- fontSize: 16,
- fontWeight: 700,
-}));
-
-const ArgRequiredTypography = styled(Typography)(() => ({
- color: "#fad105",
- fontFamily: "'Roboto Condensed', sans-serif",
- fontSize: 16,
- fontWeight: 200,
-}));
-
-const ArgEditTypography = styled(Typography)(() => ({
- color: "#5d64cf",
- fontFamily: "'Work Sans', sans-serif",
- fontSize: 14,
- fontWeight: 400,
-}));
-
-const ArgChoicesTypography = styled(Typography)(({ theme }) => ({
- color: theme.palette.primary.main,
- fontFamily: "'Roboto Condensed', sans-serif",
- fontSize: 14,
- fontWeight: 700,
-}));
-
-function ArgumentReviewer(props: { arg: CMDArg; depth: number; onEdit: () => void; onUnwrap: () => void }) {
- const [choices, setChoices] = useState([]);
-
- const buildArgOptionsString = () => {
- const argOptionsString = spliceArgOptionsString(props.arg, props.depth - 1);
- return {argOptionsString};
- };
-
- useEffect(() => {
- const newChoices: string[] = [];
- if ((props.arg as CMDStringArg).enum) {
- const items = (props.arg as CMDStringArg).enum!.items;
- for (const idx in items) {
- const enumItem = items[idx];
- newChoices.push(enumItem.name);
- }
- } else if ((props.arg as CMDNumberArg).enum) {
- const items = (props.arg as CMDNumberArg).enum!.items;
- for (const idx in items) {
- const enumItem = items[idx];
- newChoices.push(enumItem.name);
- }
- }
- setChoices(newChoices);
- }, [props.arg]);
-
- const getUnwrapKeywords = () => {
- if (props.arg.type.startsWith("@")) {
- return "Unwrap";
- } else if (props.arg.type.startsWith("array")) {
- if ((props.arg as CMDArrayArg).item?.type.startsWith("@")) {
- return "Unwrap Element";
- }
- } else if (props.arg.type.startsWith("dict")) {
- if ((props.arg as CMDDictArg).item?.type.startsWith("@")) {
- return "Unwrap Element";
- }
- }
- return null;
- };
-
- const getDefaultValueToString = () => {
- if (
- props.arg.type === "object" ||
- props.arg.type.startsWith("dict<") ||
- props.arg.type.startsWith("array<") ||
- props.arg.type.startsWith("@")
- ) {
- if (props.arg.default !== undefined && props.arg.default !== null) {
- return JSON.stringify(props.arg.default.value);
- }
- } else {
- if (props.arg.default !== undefined && props.arg.default !== null) {
- return props.arg.default.value.toString();
- }
- }
- return "";
- };
-
- return (
-
-
-
- {buildArgOptionsString()}
- }
- onClick={() => {
- props.onEdit();
- }}
- >
- Edit
-
-
-
-
-
- {`/${props.arg.type}/`}
-
- {getUnwrapKeywords() !== null && (
- }
- onClick={() => {
- props.onUnwrap();
- }}
- >
- {getUnwrapKeywords()!}
-
- )}
-
- {props.arg.required && [Required]}
-
- {(props.arg.default !== undefined || choices.length > 0 || props.arg.configurationKey !== undefined) && (
-
- {choices.length > 0 && (
- {`Choices: ` + choices.join(", ")}
- )}
- {props.arg.default !== undefined && (
- {`Default: ${getDefaultValueToString()}`}
- )}
- {props.arg.configurationKey !== undefined && (
-
- {`ConfigurationKey: ${props.arg.configurationKey}`}
-
- )}
-
- )}
- {props.arg.help?.short && (
- {props.arg.help?.short}
- )}
- {!props.arg.help?.short && (
-
- Please add argument short summary!
-
- )}
- {props.arg.help?.lines && (
-
- {props.arg.help.lines.map((line, idx) => (
- {line}
- ))}
-
- )}
-
-
- );
-}
-
-function ArgumentDialog(props: {
- commandUrl: string;
- arg: CMDArg;
- clsArgDefineMap: ClsArgDefinitionMap;
- open: boolean;
- onClose: (updated: boolean) => Promise;
-}) {
- const [updating, setUpdating] = useState(false);
- const [stage, setStage] = useState("");
- const [invalidText, setInvalidText] = useState(undefined);
- const [options, setOptions] = useState("");
- const [singularOptions, setSingularOptions] = useState(undefined);
- const [group, setGroup] = useState("");
- const [hide, setHide] = useState(false);
- const [supportEnumExtension, setSupportEnumExtension] = useState(false);
- const [shortHelp, setShortHelp] = useState("");
- const [longHelp, setLongHelp] = useState("");
- const [argSimilarTree, setArgSimilarTree] = useState(undefined);
- const [argSimilarTreeExpandedIds, setArgSimilarTreeExpandedIds] = useState([]);
- const [argSimilarTreeArgIdsUpdated, setArgSimilarTreeArgIdsUpdated] = useState([]);
- const [hasDefault, setHasDefault] = useState(false);
- const [defaultValue, setDefaultValue] = useState(undefined);
- const [defaultValueInJson, setDefaultValueInJson] = useState(false);
- const [hasPrompt, setHasPrompt] = useState(false);
- const [promptMsg, setPromptMsg] = useState(undefined);
- const [promptConfirm, setPromptConfirm] = useState(undefined);
- const [configurationKey, setConfigurationKey] = useState("");
- const [isClientArg, setIsClientArg] = useState(false);
-
- const handleClose = () => {
- setInvalidText(undefined);
- props.onClose(false);
- };
-
- const verifyModification = () => {
- setInvalidText(undefined);
- const name = options.trim();
- const sName = singularOptions?.trim() ?? undefined;
- const sHelp = shortHelp.trim();
- const lHelp = longHelp.trim();
- const gName = group.trim();
- const cfgKey = configurationKey.trim();
-
- const names = name.split(" ").filter((n) => n.length > 0);
- const sNames = sName?.split(" ").filter((n) => n.length > 0) ?? undefined;
-
- if (names.length < 1) {
- setInvalidText(`Argument 'Option names' is required.`);
- return undefined;
- }
-
- for (const idx in names) {
- const piece = names[idx];
- if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(piece)) {
- setInvalidText(`Invalid 'Option name': '${piece}'. Supported regular expression is: [a-z0-9]+(-[a-z0-9]+)* `);
- return undefined;
- }
- }
-
- if (sNames) {
- for (const idx in sNames) {
- const piece = sNames[idx];
- if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(piece)) {
- setInvalidText(
- `Invalid 'Singular option name': '${piece}'. Supported regular expression is: [a-z0-9]+(-[a-z0-9]+)* `,
- );
- return undefined;
- }
- }
- }
-
- if (
- sHelp.length < 1 &&
- names.find((n) => {
- return n === "subscription" || n === "resource-group";
- }) === undefined
- ) {
- setInvalidText(`Field 'Short Summary' is required.`);
- return undefined;
- }
-
- let lines: string[] | null = null;
- if (lHelp.length > 1) {
- lines = lHelp.split("\n").filter((l) => l.length > 0);
- }
-
- let argCfgKey: string | null = null;
- if (cfgKey.length > 0) {
- argCfgKey = cfgKey;
- }
-
- let argDefault = undefined;
- if (hasDefault === false) {
- if (props.arg.default !== undefined) {
- argDefault = null;
- }
- } else if (hasDefault === true) {
- if (defaultValue === undefined) {
- setInvalidText(`Field 'Default Value' is undefined.`);
- return undefined;
- } else {
- try {
- let argType = props.arg.type;
- if (argType.startsWith("@")) {
- argType = props.clsArgDefineMap[(props.arg as CMDClsArg).clsName].type;
- }
- argDefault = {
- value: convertArgDefaultText(defaultValue!, argType),
- };
- } catch (err: any) {
- setInvalidText(`Field 'Default Value' is invalid: ${err.message}.`);
- return undefined;
- }
- if (props.arg.default !== undefined && props.arg.default.value === argDefault.value) {
- argDefault = undefined;
- }
- }
- }
-
- let argPrompt = undefined;
- if (hasPrompt === false) {
- if (props.arg.prompt !== undefined) {
- argPrompt = null;
- }
- } else if (hasPrompt === true) {
- if (promptMsg === undefined) {
- setInvalidText(`Field 'Prompt Message' is undefined.`);
- return undefined;
- } else {
- const msg = promptMsg.trim();
- if (msg.length < 1) {
- setInvalidText(`Field 'Prompt Message' is empty.`);
- return undefined;
- }
- if (!msg.endsWith(":")) {
- setInvalidText(`Field 'Prompt Message' must end with a colon.`);
- return undefined;
- }
- argPrompt = {
- msg: msg,
- confirm: promptConfirm,
- };
- }
- }
-
- return {
- options: names,
- singularOptions: sNames,
- stage: stage,
- group: gName,
- hide: hide,
- help: {
- short: sHelp,
- lines: lines,
- },
- default: argDefault,
- prompt: argPrompt,
- configurationKey: argCfgKey,
- supportEnumExtension: supportEnumExtension,
- };
- };
-
- const handleModify = async () => {
- const data = verifyModification();
- if (data === undefined) {
- return;
- }
-
- setUpdating(true);
-
- const argumentUrl = `${props.commandUrl}/Arguments/${props.arg.var}`;
-
- try {
- await commandApi.updateCommandArgument(argumentUrl, data);
- setUpdating(false);
- await props.onClose(true);
- } catch (err: any) {
- console.error(err);
- setInvalidText(errorHandlerApi.getErrorMessage(err));
- setUpdating(false);
- }
- };
-
- const handleDisplaySimilar = async () => {
- if (verifyModification() === undefined) {
- return;
- }
-
- setUpdating(true);
-
- try {
- const res = await commandApi.findSimilarArguments(props.commandUrl, props.arg.var);
- setUpdating(false);
- const { tree, expandedIds } = BuildArgSimilarTree(res);
- setArgSimilarTree(tree);
- setArgSimilarTreeExpandedIds(expandedIds);
- setArgSimilarTreeArgIdsUpdated([]);
- } catch (err: any) {
- console.error(err);
- setInvalidText(errorHandlerApi.getErrorMessage(err));
- setUpdating(false);
- }
- };
-
- const handleDisableSimilar = () => {
- setArgSimilarTree(undefined);
- setArgSimilarTreeExpandedIds([]);
- };
-
- const onSimilarTreeUpdated = (newTree: ArgSimilarTree) => {
- setArgSimilarTree(newTree);
- };
-
- const onSimilarTreeExpandedIdsUpdated = (expandedIds: string[]) => {
- setArgSimilarTreeExpandedIds(expandedIds);
- };
-
- const handleModifySimilar = async () => {
- const data = verifyModification();
- if (data === undefined) {
- return;
- }
-
- setUpdating(true);
- let invalidText = "";
- const updatedIds: string[] = [...argSimilarTreeArgIdsUpdated];
- for (const idx in argSimilarTree!.selectedArgIds) {
- const argId = argSimilarTree!.selectedArgIds[idx];
- if (updatedIds.indexOf(argId) === -1) {
- try {
- await commandApi.updateArgumentById(argId, data);
- updatedIds.push(argId);
- setArgSimilarTreeArgIdsUpdated([...updatedIds]);
- } catch (err: any) {
- console.error(err);
- invalidText += errorHandlerApi.getErrorMessage(err);
- }
- }
- }
-
- if (invalidText.length > 0) {
- setInvalidText(invalidText);
- setUpdating(false);
- } else {
- setUpdating(false);
- await props.onClose(true);
- }
- };
-
- useEffect(() => {
- const { arg, clsArgDefineMap } = props;
- setIsClientArg(arg.var.startsWith("$Client."));
-
- setOptions(arg.options.join(" "));
- if (arg.type.startsWith("array")) {
- setSingularOptions((arg as CMDArrayArg).singularOptions?.join(" ") ?? "");
- } else if (arg.type.startsWith("@") && clsArgDefineMap[(arg as CMDClsArg).clsName].type.startsWith("array")) {
- setSingularOptions((arg as CMDClsArg).singularOptions?.join(" ") ?? "");
- } else {
- setSingularOptions(undefined);
- }
-
- if (
- arg.type === "object" ||
- arg.type.startsWith("dict<") ||
- arg.type.startsWith("array<") ||
- arg.type.startsWith("@")
- ) {
- setHasPrompt(undefined);
- } else {
- setHasPrompt(arg.prompt !== undefined);
- if (arg.prompt !== undefined) {
- setPromptMsg(arg.prompt.msg);
- setPromptConfirm(undefined);
- }
- }
- if (arg.type === "password") {
- setPromptConfirm((arg as CMDPasswordArg).prompt?.confirm ?? false);
- }
- setStage(props.arg.stage);
- setGroup(props.arg.group);
- setHide(props.arg.hide);
- setSupportEnumExtension(props.arg.supportEnumExtension || false);
- setShortHelp(props.arg.help?.short ?? "");
- setLongHelp(props.arg.help?.lines?.join("\n") ?? "");
- setConfigurationKey(props.arg.configurationKey ?? "");
- setUpdating(false);
- setArgSimilarTree(undefined);
- setArgSimilarTreeExpandedIds([]);
-
- if (
- arg.type === "object" ||
- arg.type.startsWith("dict<") ||
- arg.type.startsWith("array<") ||
- arg.type.startsWith("@")
- ) {
- setDefaultValueInJson(true);
- if (props.arg.default !== undefined && props.arg.default !== null) {
- setHasDefault(true);
- setDefaultValue(JSON.stringify(props.arg.default.value));
- } else {
- setHasDefault(false);
- setDefaultValue(undefined);
- }
- } else {
- setDefaultValueInJson(false);
- if (props.arg.default !== undefined && props.arg.default !== null) {
- setHasDefault(true);
- setDefaultValue(props.arg.default.value.toString());
- } else {
- setHasDefault(false);
- setDefaultValue(undefined);
- }
- }
- }, [props.arg]);
-
- return (
-
- );
-}
-
-function FlattenDialog(props: {
- commandUrl: string;
- arg: CMDArg;
- clsArgDefineMap: ClsArgDefinitionMap;
- open: boolean;
- onClose: (flattened: boolean) => Promise;
-}) {
- const [updating, setUpdating] = useState(false);
- const [invalidText, setInvalidText] = useState(undefined);
- const [subArgOptions, setSubArgOptions] = useState<{ var: string; options: string }[]>([]);
- const [argSimilarTree, setArgSimilarTree] = useState(undefined);
- const [argSimilarTreeExpandedIds, setArgSimilarTreeExpandedIds] = useState([]);
- const [argSimilarTreeArgIdsUpdated, setArgSimilarTreeArgIdsUpdated] = useState([]);
-
- useEffect(() => {
- const { arg, clsArgDefineMap } = props;
- let subArgs;
- if (arg.type.startsWith("@")) {
- const clsName = (arg as CMDClsArg).clsName;
- subArgs = (clsArgDefineMap[clsName] as CMDObjectArgBase).args;
- } else {
- subArgs = (arg as CMDObjectArg).args;
- }
- const subArgOptions = subArgs.map((value) => {
- return {
- var: value.var,
- options: value.options.join(" "),
- };
- });
-
- setSubArgOptions(subArgOptions);
- setArgSimilarTree(undefined);
- setArgSimilarTreeExpandedIds([]);
- }, [props.arg]);
-
- const handleClose = () => {
- setInvalidText(undefined);
- props.onClose(false);
- };
-
- const verifyFlatten = () => {
- setInvalidText(undefined);
- const argOptions: { [argVar: string]: string[] } = {};
- let invalidText: string | undefined = undefined;
-
- subArgOptions.forEach((arg, idx) => {
- const names = arg.options.split(" ").filter((n) => n.length > 0);
- if (names.length < 1) {
- invalidText = `Prop ${idx + 1} option name is required.`;
- return undefined;
- }
-
- for (const idx in names) {
- const piece = names[idx];
- if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(piece)) {
- invalidText = `Invalid 'Prop ${idx + 1} option name': '${piece}'. Supported regular expression is: [a-z0-9]+(-[a-z0-9]+)* `;
- return undefined;
- }
- }
- argOptions[arg.var] = names;
- });
- if (invalidText !== undefined) {
- setInvalidText(invalidText);
- return undefined;
- }
-
- return {
- subArgsOptions: argOptions,
- };
- };
-
- const handleFlatten = async () => {
- const data = verifyFlatten();
- if (data === undefined) {
- return;
- }
-
- setUpdating(true);
-
- const flattenUrl = `${props.commandUrl}/Arguments/${props.arg.var}/Flatten`;
-
- try {
- await commandApi.flattenArgument(flattenUrl, data);
- setUpdating(false);
- await props.onClose(true);
- } catch (err: any) {
- console.error(err);
- setInvalidText(errorHandlerApi.getErrorMessage(err));
- setUpdating(false);
- }
- };
-
- const handleDisplaySimilar = async () => {
- if (verifyFlatten() === undefined) {
- return;
- }
-
- setUpdating(true);
-
- try {
- const res = await commandApi.findSimilarArguments(props.commandUrl, props.arg.var);
- setUpdating(false);
- const { tree, expandedIds } = BuildArgSimilarTree(res);
- setArgSimilarTree(tree);
- setArgSimilarTreeExpandedIds(expandedIds);
- setArgSimilarTreeArgIdsUpdated([]);
- } catch (err: any) {
- console.error(err);
- setInvalidText(errorHandlerApi.getErrorMessage(err));
- setUpdating(false);
- }
- };
-
- const handleDisableSimilar = () => {
- setArgSimilarTree(undefined);
- setArgSimilarTreeExpandedIds([]);
- };
-
- const onSimilarTreeUpdated = (newTree: ArgSimilarTree) => {
- setArgSimilarTree(newTree);
- };
-
- const onSimilarTreeExpandedIdsUpdated = (expandedIds: string[]) => {
- setArgSimilarTreeExpandedIds(expandedIds);
- };
-
- const handleFlattenSimilar = async () => {
- const data = verifyFlatten();
- if (data === undefined) {
- return;
- }
- setUpdating(true);
- let invalidText = "";
- const updatedIds: string[] = [...argSimilarTreeArgIdsUpdated];
- for (const idx in argSimilarTree!.selectedArgIds) {
- const argId = argSimilarTree!.selectedArgIds[idx];
- if (updatedIds.indexOf(argId) === -1) {
- const flattenUrl = `${argId}/Flatten`;
- try {
- await commandApi.flattenArgument(flattenUrl, data);
- updatedIds.push(argId);
- setArgSimilarTreeArgIdsUpdated([...updatedIds]);
- } catch (err: any) {
- console.error(err);
- invalidText += errorHandlerApi.getErrorMessage(err);
- }
- }
- }
-
- if (invalidText.length > 0) {
- setInvalidText(invalidText);
- setUpdating(false);
- } else {
- setUpdating(false);
- await props.onClose(true);
- }
- };
-
- const buildSubArgText = (arg: { var: string; options: string }, idx: number) => {
- return (
- {
- const options = subArgOptions.map((value) => {
- if (value.var === arg.var) {
- return {
- ...value,
- options: event.target.value,
- };
- } else {
- return value;
- }
- });
- setSubArgOptions(options);
- }}
- margin="normal"
- required
- />
- );
- };
-
- return (
-
- );
-}
-
-function UnwrapClsDialog(props: {
- commandUrl: string;
- arg: CMDArg;
- open: boolean;
- onClose: (unwrapped: boolean) => Promise;
-}) {
- const [updating, setUpdating] = useState(false);
- const [invalidText, setInvalidText] = useState(undefined);
-
- const handleClose = () => {
- setInvalidText(undefined);
- props.onClose(false);
- };
-
- const handleUnwrap = async () => {
- setUpdating(true);
-
- let argVar = props.arg.var;
- if (props.arg.type.startsWith("array")) {
- if ((props.arg as CMDArrayArg).item?.type.startsWith("@")) {
- argVar += "[]";
- }
- } else if (props.arg.type.startsWith("dict")) {
- if ((props.arg as CMDDictArg).item?.type.startsWith("@")) {
- argVar += "{}";
- }
- }
-
- const flattenUrl = `${props.commandUrl}/Arguments/${argVar}/UnwrapClass`;
-
- try {
- await commandApi.unwrapClassArgument(flattenUrl);
- setUpdating(false);
- await props.onClose(true);
- } catch (err: any) {
- console.error(err);
- setInvalidText(errorHandlerApi.getErrorMessage(err));
- setUpdating(false);
- }
- };
-
- return (
-
- );
-}
-
-const PropArgTypeTypography = styled(Typography)(({ theme }) => ({
- color: theme.palette.primary.main,
- fontFamily: "'Work Sans', sans-serif",
- fontSize: 10,
- fontWeight: 400,
-}));
-
-const PropRequiredTypography = styled(Typography)(() => ({
- color: "#dba339",
- fontFamily: "'Work Sans', sans-serif",
- fontSize: 10,
- fontWeight: 400,
-}));
-
-const PropHiddenTypography = styled(Typography)(() => ({
- color: "#8888C3",
- fontFamily: "'Work Sans', sans-serif",
- fontSize: 10,
- fontWeight: 400,
-}));
-
-const ArgGroupTypography = styled(Typography)(({ theme }) => ({
- color: theme.palette.primary.main,
- fontFamily: "'Roboto Condensed', sans-serif",
- fontSize: 18,
- fontWeight: 200,
-}));
-
-const PropArgOptionTypography = styled(Typography)(({ theme }) => ({
- color: theme.palette.primary.main,
- fontFamily: "'Roboto Condensed', sans-serif",
- fontSize: 16,
- fontWeight: 700,
-}));
-
-const PropHiddenArgOptionTypography = styled(Typography)(() => ({
- color: "#8888C3",
- fontFamily: "'Roboto Condensed', sans-serif",
- fontSize: 16,
- fontWeight: 700,
-}));
-
-const PropArgShortSummaryTypography = styled(Typography)(({ theme }) => ({
- color: theme.palette.primary.main,
- fontFamily: "'Roboto Condensed', sans-serif",
- fontSize: 14,
- fontStyle: "italic",
- fontWeight: 400,
-}));
-
-function ArgumentPropsReviewer(props: {
- title: string;
- args: CMDArg[];
- onFlatten?: () => void;
- onAddSubcommand?: () => void;
- selectedArg?: CMDArg;
- depth: number;
- onSelectSubArg: (subArgVar: string) => void;
-}) {
- const groupArgs: { [name: string]: CMDArg[] } = {};
- if (props.args !== undefined) {
- props.args.forEach((arg) => {
- const groupName: string = arg.group.length > 0 ? arg.group : "";
- if (!(groupName in groupArgs)) {
- groupArgs[groupName] = [];
- }
- groupArgs[groupName].push(arg);
- });
- }
-
- const groups: ArgGroup[] = [];
-
- for (const groupName in groupArgs) {
- groupArgs[groupName].sort((a, b) => {
- if (a.required && !b.required) {
- return -1;
- } else if (!a.required && b.required) {
- return 1;
- }
- return a.options[0].localeCompare(b.options[0]);
- });
- groups.push({
- name: groupName,
- args: groupArgs[groupName],
- });
- }
- groups.sort((a, b) => a.name.localeCompare(b.name));
-
- const checkCanAddSubcommand = () => {
- if (props.selectedArg && props.args.length > 0) {
- return true;
- }
- return false;
- };
-
- const buildArg = (arg: CMDArg, idx: number) => {
- const argOptionsString = spliceArgOptionsString(arg, props.depth);
- return (
-
-
- {
- props.onSelectSubArg(arg.var);
- }}
- >
- {!arg.hide && {argOptionsString}}
- {arg.hide && (
- {argOptionsString}
- )}
-
-
-
- {arg.stage === "Preview" && (
- {arg.stage}
- )}
- {arg.stage === "Experimental" && (
- {arg.stage}
- )}
-
-
-
- {`/${arg.type}/`}
-
- {arg.required && [Required]}
- {arg.hide && [Hidden]}
-
- {arg.help && (
-
- {arg.help.short}
-
- )}
-
-
- );
- };
-
- const buildArgGroup = (group: ArgGroup, idx: number) => {
- return (
-
-
-
-
-
- {group.args.map(buildArg)}
-
-
- );
- };
-
- if (groups.length === 0) {
- return <>>;
- }
-
- return (
-
-
- {props.title}
- {props.onFlatten !== undefined && (
- }
- onClick={props.onFlatten}
- >
- Flatten
-
- )}
-
- {/* {props.onUnflatten !== undefined && }
- onClick={props.onUnflatten}
- >
- Unflatten
- } */}
-
- {props.onAddSubcommand !== undefined && checkCanAddSubcommand() && (
- }
- onClick={props.onAddSubcommand}
- >
- Subcommands
-
- )}
-
- {groups.map(buildArgGroup)}
-
- );
-}
-
-interface ArgGroup {
- name: string;
- args: CMDArg[];
-}
-
-type CMDArgHelp = {
- short: string;
- lines?: string[];
- refCommands?: string[];
-};
-
-type CMDArgDefault = {
- value: T | null;
-};
-
-type CMDArgBlank = {
- value: T | null;
-};
-
-type CMDArgEnumItem = {
- name: string;
- hide: boolean;
- value: T;
-};
-
-type CMDArgEnum = {
- items: CMDArgEnumItem[];
-};
-
-interface CMDArgPromptInput {
- msg: string;
-}
-
-interface CMDPasswordArgPromptInput extends CMDArgPromptInput {
- confirm: boolean;
-}
-
-interface CMDArgBase {
- type: string;
- nullable: boolean;
- blank?: CMDArgBlank;
-}
-
-interface CMDArg extends CMDArgBase {
- var: string;
- options: string[];
-
- required: boolean;
- stage: "Stable" | "Preview" | "Experimental";
- hide: boolean;
- group: string;
- help?: CMDArgHelp;
-
- default?: CMDArgDefault;
- idPart?: string;
- prompt?: CMDArgPromptInput;
- configurationKey?: string;
- supportEnumExtension?: boolean;
- hasEnum?: boolean;
-}
-
-interface CMDArgBaseT extends CMDArgBase {
- blank?: CMDArgBlank;
-}
-
-interface CMDArgT extends CMDArg {
- default?: CMDArgDefault;
- blank?: CMDArgBlank;
-}
-
-// type: starts with "@"
-interface CMDClsArgBase extends CMDArgBase {
- clsName: string;
-}
-
-interface CMDClsArg extends CMDClsArgBase, CMDArg {
- singularOptions?: string[]; // for list use only
-}
-
-// type: string
-interface CMDStringArgBase extends CMDArgBaseT {
- enum?: CMDArgEnum;
- // fmt?: CMDStringFormat
-}
-
-interface CMDStringArg extends CMDStringArgBase, CMDArgT {}
-
-// // type: byte
-// interface CMDByteArgBase extends CMDStringArgBase { }
-
-// // type: binary
-// interface CMDBinaryArgBase extends CMDStringArgBase { }
-
-// // type: duration
-// interface CMDDurationArgBase extends CMDStringArgBase { }
-
-// // type: date As defined by full-date - https://xml2rfc.tools.ietf.org/public/rfc/html/rfc3339.html#anchor14
-// interface CMDDateArgBase extends CMDStringArgBase { }
-
-// // type: dateTime As defined by date-time - https://xml2rfc.tools.ietf.org/public/rfc/html/rfc3339.html#anchor14
-// interface CMDDateTimeArgBase extends CMDStringArgBase { }
-
-// interface CMDTimeArgBase extends CMDStringArgBase { }
-
-// // type: uuid
-// interface CMDUuidArgBase extends CMDStringArgBase { }
-
-// type: password
-interface CMDPasswordArgBase extends CMDStringArgBase {}
-interface CMDPasswordArg extends CMDPasswordArgBase, CMDStringArg {
- prompt?: CMDPasswordArgPromptInput;
-}
-
-// // type: SubscriptionId
-// interface CMDSubscriptionIdArgBase extends CMDStringArgBase { }
-
-// // type: ResourceGroupName
-// interface CMDResourceGroupNameArgBase extends CMDStringArgBase { }
-
-// // type: ResourceId
-// interface CMDResourceIdNameArgBase extends CMDStringArgBase { }
-
-// // type: ResourceLocation
-// interface CMDResourceLocationNameArgBase extends CMDStringArgBase { }
-
-interface CMDNumberArgBase extends CMDArgBaseT {
- enum?: CMDArgEnum;
- // fmt?: CMDIntegerFormat
-}
-interface CMDNumberArg extends CMDNumberArgBase, CMDArgT {}
-
-// // type: integer
-// interface CMDIntegerArgBase extends CMDNumberArgBase { }
-
-// // type: integer32
-// interface CMDInteger32ArgBase extends CMDNumberArgBase { }
-
-// // type: integer32
-// interface CMDInteger64ArgBase extends CMDNumberArgBase { }
-
-// // type: float
-// interface CMDFloatArgBase extends CMDNumberArgBase { }
-
-// // type: float32
-// interface CMDFloat32ArgBase extends CMDNumberArgBase { }
-
-// // type: float64
-// interface CMDFloat64ArgBase extends CMDNumberArgBase { }
-
-// // type: boolean
-// interface CMDBooleanArgBase extends CMDArgBaseT { }
-
-// type: object
-interface CMDObjectArgBase extends CMDArgBase {
- // fmt?: CMDObjectFormat
- args: CMDArg[];
-}
-
-interface CMDObjectArg extends CMDObjectArgBase, CMDArg {}
-// type: dict
-interface CMDDictArgBase extends CMDArgBase {
- item?: CMDArgBase;
- anyType: boolean;
-}
-interface CMDDictArg extends CMDDictArgBase, CMDArg {}
-
-// type: array
-interface CMDArrayArgBase extends CMDArgBase {
- // fmt?: CMDArrayFormat
- item: CMDArgBase;
-}
-
-interface CMDArrayArg extends CMDArrayArgBase, CMDArg {
- singularOptions?: string[];
-}
-
-type ClsArgDefinitionMap = {
- [clsName: string]: CMDArgBase;
-};
-
-function decodeArgEnumItem(response: any): CMDArgEnumItem {
- return {
- name: response.name,
- hide: response.hide ?? false,
- value: response.value as T,
- };
-}
-
-function decodeArgEnum(response: any): CMDArgEnum {
- const argEnum: CMDArgEnum = {
- items: response.items.map((item: any) => decodeArgEnumItem(item)),
- };
- return argEnum;
-}
-
-function decodeArgBlank(response: any | undefined): CMDArgBlank | undefined {
- if (response === undefined || response === null) {
- return undefined;
- }
-
- return {
- value: response.value as T | null,
- };
-}
-
-function decodeArgDefault(response: any | undefined): CMDArgDefault | undefined {
- if (response === undefined || response === null) {
- return undefined;
- }
-
- return {
- value: response.value as T | null,
- };
-}
-
-function decodeArgPromptInput(response: any): CMDArgPromptInput | undefined {
- if (response === undefined || response === null) {
- return undefined;
- }
-
- return {
- msg: response.msg as string,
- };
-}
-
-function decodePasswordArgPromptInput(response: any): CMDPasswordArgPromptInput | undefined {
- if (response === undefined || response === null) {
- return undefined;
- }
-
- return {
- msg: response.msg as string,
- confirm: (response.confirm ?? false) as boolean,
- };
-}
-
-function decodeArgBase(response: any): {
- argBase: CMDArgBase;
- clsDefineMap: ClsArgDefinitionMap;
-} {
- let argBase: any = {
- type: response.type,
- nullable: (response.nullable ?? false) as boolean,
- };
-
- let clsDefineMap: ClsArgDefinitionMap = {};
-
- switch (response.type) {
- case "byte":
- case "binary":
- case "duration":
- case "date":
- case "dateTime":
- case "time":
- case "uuid":
- case "password":
- case "SubscriptionId":
- case "ResourceGroupName":
- case "ResourceId":
- case "ResourceLocation":
- case "string":
- if (response.enum) {
- argBase = {
- ...argBase,
- enum: decodeArgEnum(response.enum),
- };
- }
- if (response.blank) {
- argBase = {
- ...argBase,
- blank: decodeArgBlank(response.blank),
- };
- }
- break;
- case "integer32":
- case "integer64":
- case "integer":
- if (response.enum) {
- argBase = {
- ...argBase,
- enum: decodeArgEnum(response.enum),
- };
- }
- if (response.blank) {
- argBase = {
- ...argBase,
- blank: decodeArgBlank(response.blank),
- };
- }
- break;
- case "float32":
- case "float64":
- case "float":
- if (response.enum) {
- argBase = {
- ...argBase,
- enum: decodeArgEnum(response.enum),
- };
- }
- if (response.blank) {
- argBase = {
- ...argBase,
- blank: decodeArgBlank(response.blank),
- };
- }
- break;
- case "boolean":
- if (response.blank) {
- argBase = {
- ...argBase,
- blank: decodeArgBlank(response.blank),
- };
- }
- break;
- case "any":
- if (response.blank) {
- argBase = {
- ...argBase,
- blank: decodeArgBlank(response.blank),
- };
- }
- break;
- case "object":
- if (response.args && Array.isArray(response.args) && response.args.length > 0) {
- const args: CMDArg[] = response.args.map((resSubArg: any) => {
- const subArgParse = decodeArg(resSubArg);
- clsDefineMap = {
- ...clsDefineMap,
- ...subArgParse.clsDefineMap,
- };
- return subArgParse.arg;
- });
- argBase = {
- ...argBase,
- args: args,
- };
- } else if (response.additionalProps && response.additionalProps.item) {
- // Convert additionalProps to dict argBaseType
- const itemArgBaseParse = decodeArgBase(response.additionalProps.item);
- clsDefineMap = {
- ...clsDefineMap,
- ...itemArgBaseParse.clsDefineMap,
- };
- const argBaseType = `dict`;
- argBase = {
- ...argBase,
- type: argBaseType,
- item: itemArgBaseParse.argBase,
- anyType: false,
- };
- } else if (response.additionalProps && response.additionalProps.anyType) {
- const argBaseType = `dict`;
- argBase = {
- ...argBase,
- type: argBaseType,
- anyType: true,
- };
- }
-
- if (response.cls) {
- const clsName = response.cls;
- clsDefineMap[clsName] = argBase;
- argBase = {
- type: `@${response.cls}`,
- clsName: clsName,
- };
- }
- break;
- default:
- if (response.type.startsWith("array<")) {
- if (response.item) {
- const itemArgBaseParse = decodeArgBase(response.item);
- clsDefineMap = {
- ...clsDefineMap,
- ...itemArgBaseParse.clsDefineMap,
- };
- const argBaseType = `array<${itemArgBaseParse.argBase.type}>`;
- argBase = {
- ...argBase,
- type: argBaseType,
- item: itemArgBaseParse.argBase,
- };
- } else {
- throw Error("Invalid array object. Item is not defined");
- }
-
- if (response.cls) {
- const clsName = response.cls;
- clsDefineMap[clsName] = argBase;
- argBase = {
- type: `@${response.cls}`,
- clsName: clsName,
- };
- }
- } else if (response.type.startsWith("@")) {
- argBase["clsName"] = response.type.slice(1);
- } else {
- console.error(`Unknown type '${response.type}'`);
- throw Error(`Unknown type '${response.type}'`);
- }
- }
-
- return {
- argBase: argBase,
- clsDefineMap: clsDefineMap,
- };
-}
-
-function decodeArgHelp(response: any): CMDArgHelp {
- return {
- short: response.short,
- lines: response.lines,
- refCommands: response.refCommands,
- };
-}
-
-function decodeArg(response: any): {
- arg: CMDArg;
- clsDefineMap: ClsArgDefinitionMap;
-} {
- const { argBase, clsDefineMap } = decodeArgBase(response);
- const options = (response.options as string[]).sort((a, b) => a.length - b.length).reverse();
- const help = response.help ? decodeArgHelp(response.help) : undefined;
- const prompt = response.prompt ? decodeArgPromptInput(response.prompt) : undefined;
-
- let arg: any = {
- ...argBase,
- var: response.var as string,
- options: options,
- required: (response.required ?? false) as boolean,
- stage: (response.stage ?? "Stable") as "Stable" | "Preview" | "Experimental",
- hide: (response.hide ?? false) as boolean,
- group: (response.group ?? "") as string,
- help: help,
- idPart: response.idPart,
- prompt: prompt,
- configurationKey: response.configurationKey,
- supportEnumExtension: response.enum?.supportExtension || response.item?.enum?.supportExtension || false,
- hasEnum: response.enum?.items?.length > 0 || response.item?.enum?.items?.length > 0 || false,
- };
-
- switch (argBase.type) {
- case "byte":
- case "binary":
- case "duration":
- case "date":
- case "dateTime":
- case "time":
- case "uuid":
- case "SubscriptionId":
- case "ResourceGroupName":
- case "ResourceId":
- case "ResourceLocation":
- case "string":
- if (response.default) {
- arg = {
- ...arg,
- default: decodeArgDefault(response.default),
- };
- }
- break;
- case "password":
- if (response.prompt) {
- arg = {
- ...arg,
- prompt: decodePasswordArgPromptInput(response.prompt),
- };
- }
- if (response.default) {
- arg = {
- ...arg,
- default: decodeArgDefault(response.default),
- };
- }
- break;
- case "integer32":
- case "integer64":
- case "integer":
- if (response.default) {
- arg = {
- ...arg,
- default: decodeArgDefault(response.default),
- };
- }
- break;
- case "float32":
- case "float64":
- case "float":
- if (response.default) {
- arg = {
- ...arg,
- default: decodeArgDefault(response.default),
- };
- }
- break;
- case "boolean":
- if (response.default) {
- arg = {
- ...arg,
- default: decodeArgDefault(response.default),
- };
- }
- break;
- case "any":
- if (response.default) {
- arg = {
- ...arg,
- default: decodeArgDefault(response.default),
- };
- }
- break;
- case "object":
- if (response.default) {
- arg = {
- ...arg,
- default: decodeArgDefault