diff --git a/src/web/src/__tests__/api/workspaceApi.test.tsx b/src/web/src/__tests__/api/workspaceApi.test.tsx index 7a29547e..0d7058ca 100644 --- a/src/web/src/__tests__/api/workspaceApi.test.tsx +++ b/src/web/src/__tests__/api/workspaceApi.test.tsx @@ -97,9 +97,12 @@ describe("Workspace API", () => { expect(result).toEqual({ name: "test-workspace-1", - plane: "azure-cli", + plane: "data-planetest-workspace-1", + resourceProvider: "test-workspace-1", folder: "/workspaces/test-workspace-1", - commandTree: {}, + commandTree: { + names: ["aaz"], + }, }); }); }); diff --git a/src/web/src/__tests__/components/WSEditorClientConfig.test.tsx b/src/web/src/__tests__/components/WSEditorClientConfig.test.tsx index 17ddd7db..5a9134be 100644 --- a/src/web/src/__tests__/components/WSEditorClientConfig.test.tsx +++ b/src/web/src/__tests__/components/WSEditorClientConfig.test.tsx @@ -54,6 +54,7 @@ describe("WSEditorClientConfigDialog", () => { beforeEach(() => { vi.clearAllMocks(); (specsApi.getPlanes as any).mockResolvedValue(mockPlanes); + (specsApi.getSwaggerModules as any).mockResolvedValue(["storage", "compute"]); (specsApi.getResourceProviders as any).mockResolvedValue(mockResourceProviders); (specsApi.getProviderResources as any).mockResolvedValue(mockProviderResources); (errorHandlerApi.getErrorMessage as any).mockReturnValue("Mock error message"); @@ -255,7 +256,7 @@ describe("WSEditorClientConfigDialog", () => { await user.click(updateButton); await waitFor(() => { - expect(screen.getByText("Plane is required.")).toBeInTheDocument(); + expect(screen.getByText("Module is required.")).toBeInTheDocument(); }); }); }); diff --git a/src/web/src/__tests__/integration/WSEditorClientConfig.integration.test.tsx b/src/web/src/__tests__/integration/WSEditorClientConfig.integration.test.tsx index ace84e2d..010c3201 100644 --- a/src/web/src/__tests__/integration/WSEditorClientConfig.integration.test.tsx +++ b/src/web/src/__tests__/integration/WSEditorClientConfig.integration.test.tsx @@ -2,7 +2,6 @@ import { describe, it, expect, vi, beforeEach, beforeAll, afterEach, afterAll } import { screen, waitFor } from "@testing-library/react"; 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/components/WSEditor/WSEditorClientConfig"; @@ -71,44 +70,6 @@ describe("WSEditorClientConfigDialog - Integration", () => { expect(screen.queryByText("Cancel")).not.toBeInTheDocument(); }); - it.skip("should cascade load planes → modules → providers → versions", async () => { - // @NOTE: skipping this workflow for now, there is servere delay in loading, will revisit once loading states are improved. - render(); - - // Switch to the resource property tab - const resourcePropertyTab = screen.getByRole("tab", { name: /By resource property/i }); - await userEvent.click(resourcePropertyTab); - - // --- MODULES --- - const moduleInput = screen.getByRole("combobox", { name: /Module/i }); - await userEvent.click(moduleInput); - - // Wait for the popper to render an option (it will display "storage", not "Microsoft.Storage") - const storageOption = await screen.findByRole("option", { name: /storage/i }); - await userEvent.click(storageOption); - - // --- PROVIDERS --- - const providerInput = screen.getByRole("combobox", { name: /Resource Provider/i }); - await userEvent.click(providerInput); - - // Providers are stripped of common prefix, so if API returned ["Microsoft.Storage"], - // and `commonPrefix = "Microsoft."`, you’ll actually see "Storage" in the DOM - const rpOption = await screen.findByRole("option", { name: /Storage/i }); - await userEvent.click(rpOption); - - // --- VERSIONS --- - const versionInput = screen.getByRole("combobox", { name: /API Version/i }); - await userEvent.click(versionInput); - - const versionOption = await screen.findByRole("option", { name: /2021-04-01/i }); - await userEvent.click(versionOption); - - // Final assertions (all cascades complete) - expect(moduleInput).toHaveValue("storage"); - expect(providerInput).toHaveValue("Storage"); - expect(versionInput).toHaveValue("2021-04-01"); - }); - it("should handle API errors gracefully during cascade loading", async () => { const user = userEvent.setup(); render( @@ -133,7 +94,7 @@ describe("WSEditorClientConfigDialog - Integration", () => { }); describe("Complete User Workflows", () => { - it("should complete template config setup end-to-end", async () => { + it("should handle user inputs for relevant fields", async () => { const user = userEvent.setup(); render(); @@ -163,210 +124,5 @@ describe("WSEditorClientConfigDialog - Integration", () => { expect(mockOnClose).toHaveBeenCalledWith(true); }); }); - - it.skip("should complete resource config setup end-to-end", async () => { - // @NOTE: revisit once workflows and loading states are improved - server.use( - http.get(`*/workspaces${mockWorkspaceUrl}/client-config`, () => { - return new HttpResponse(null, { status: 404 }); - }), - http.put(`*/workspaces${mockWorkspaceUrl}/client-config`, async ({ request }) => { - const body = await request.json(); - expect(body).toEqual({ - templates: undefined, - cloudMetadata: undefined, - resource: { - plane: "azure-cli", - module: "storage", - version: "2021-04-01", - id: "storageAccounts", - subresource: "properties.primaryEndpoints.blob", - }, - auth: { - aad: { - scopes: ["https://storage.azure.com/.default"], - }, - }, - }); - return HttpResponse.json({ success: true }); - }), - ); - - const user = userEvent.setup(); - render(); - - await waitFor(() => { - expect(screen.getByText("By resource property")).toBeInTheDocument(); - }); - - const resourceTab = screen.getByText("By resource property"); - await user.click(resourceTab); - - await waitFor(() => { - expect(screen.getByRole("combobox", { name: /module/i })).toBeInTheDocument(); - }); - - const moduleInput = screen.getByRole("combobox", { name: /module/i }); - await user.click(moduleInput); - await waitFor(() => { - expect(screen.getByText("storage")).toBeInTheDocument(); - }); - await user.click(screen.getByText("storage")); - - await waitFor(() => { - const rpInput = screen.getByLabelText("Resource Provider"); - expect(rpInput).toBeInTheDocument(); - }); - const rpInput = screen.getByLabelText("Resource Provider"); - await user.click(rpInput); - await waitFor(() => { - expect(screen.getByText("Microsoft.Storage")).toBeInTheDocument(); - }); - await user.click(screen.getByText("Microsoft.Storage")); - - await waitFor(() => { - const versionInput = screen.getByLabelText("API Version"); - expect(versionInput).toBeInTheDocument(); - }); - const versionInput = screen.getByLabelText("API Version"); - await user.click(versionInput); - await waitFor(() => { - expect(screen.getByText("2021-04-01")).toBeInTheDocument(); - }); - await user.click(screen.getByText("2021-04-01")); - - await waitFor(() => { - const resourceIdInput = screen.getByLabelText("Resource ID"); - expect(resourceIdInput).toBeInTheDocument(); - }); - const resourceIdInput = screen.getByLabelText("Resource ID"); - await user.click(resourceIdInput); - await waitFor(() => { - expect(screen.getByText("storageAccounts")).toBeInTheDocument(); - }); - await user.click(screen.getByText("storageAccounts")); - - const subresourceInput = screen.getByLabelText("Endpoint Property Index"); - await user.type(subresourceInput, "properties.primaryEndpoints.blob"); - - const aadScopeInput = screen.getByPlaceholderText(/Input Microsoft Entra\(AAD\) auth Scope/); - await user.clear(aadScopeInput); - await user.type(aadScopeInput, "https://storage.azure.com/.default"); - - const updateButton = screen.getByText("Update"); - await user.click(updateButton); - - await waitFor(() => { - expect(mockOnClose).toHaveBeenCalledWith(true); - }); - }); - - it.skip("should handle network errors during submission", async () => { - // @NOTE: revisit once workflows and loading states are improved - const user = userEvent.setup(); - render( - , - ); - - await waitFor(() => { - expect(document.querySelector("#AzureCloud")).toBeInTheDocument(); - }); - - const azureCloudInput = document.querySelector("#AzureCloud") as HTMLElement; - await user.type(azureCloudInput, "https://{vaultName}.vault.azure.net"); - - const aadScopeInput = screen.getByPlaceholderText(/Input Microsoft Entra\(AAD\) auth Scope/); - await user.type(aadScopeInput, "https://management.azure.com/.default"); - - const updateButton = screen.getByText("Update"); - await user.click(updateButton); - - await waitFor(() => { - expect(screen.getByText(/ResponseError:/)).toBeInTheDocument(); - }); - - expect(mockOnClose).not.toHaveBeenCalled(); - }); - - it.skip("should handle error recovery - fix validation error and retry", async () => { - // @NOTE: revisit once workflows and loading states are improved - server.use( - http.get(`*/workspaces${mockWorkspaceUrl}/client-config`, () => { - return new HttpResponse(null, { status: 404 }); - }), - http.put(`*/workspaces${mockWorkspaceUrl}/client-config`, () => { - return HttpResponse.json({ success: true }); - }), - ); - - const user = userEvent.setup(); - render(); - - await waitFor(() => { - expect(screen.getByText("Update")).toBeInTheDocument(); - }); - - const updateButton = screen.getByText("Update"); - await user.click(updateButton); - - await waitFor(() => { - expect(screen.getByText("Azure Cloud Endpoint Template is required.")).toBeInTheDocument(); - }); - - const azureCloudInput = document.querySelector("#AzureCloud") as HTMLElement; - await user.type(azureCloudInput, "https://{vaultName}.vault.azure.net"); - - const aadScopeInput = screen.getByPlaceholderText(/Input Microsoft Entra\(AAD\) auth Scope/); - await user.type(aadScopeInput, "https://management.azure.com/.default"); - - await user.click(updateButton); - - await waitFor(() => { - expect(mockOnClose).toHaveBeenCalledWith(true); - }); - }); - }); - - describe("Real-time Validation", () => { - it.skip("should validate template URLs in real-time", async () => { - // @NOTE: revisit once error/loading states are cleared up - const user = userEvent.setup(); - render(); - - await waitFor(() => { - expect(document.querySelector("#AzureCloud")).toBeInTheDocument(); - }); - - const azureCloudInput = document.querySelector("#AzureCloud") as HTMLElement; - await user.type(azureCloudInput, "invalid-url"); - - const updateButton = screen.getByText("Update"); - await user.click(updateButton); - - await waitFor( - () => { - expect(screen.queryByText("Azure Cloud Endpoint Template is invalid.")).not.toBeInTheDocument(); - }, - { timeout: 2000 }, - ); - await user.clear(azureCloudInput); - await user.type(azureCloudInput, "https://{vaultName}.vault.azure.net"); - - // Add AAD scope - const aadScopeInput = screen.getByPlaceholderText(/Input Microsoft Entra\(AAD\) auth Scope/); - await user.type(aadScopeInput, "https://management.azure.com/.default"); - - // Should be able to submit now - await user.click(updateButton); - - // Error should clear and submission should proceed - await waitFor(() => { - expect(screen.queryByText("Azure Cloud Endpoint Template is invalid.")).not.toBeInTheDocument(); - }); - }); }); }); diff --git a/src/web/src/__tests__/mocks/handlers.ts b/src/web/src/__tests__/mocks/handlers.ts index 7d4dedf2..1554ef9c 100644 --- a/src/web/src/__tests__/mocks/handlers.ts +++ b/src/web/src/__tests__/mocks/handlers.ts @@ -89,37 +89,198 @@ export const handlers = [ http.get("/AAZ/Editor/Workspaces/:name", ({ params }) => { return HttpResponse.json({ name: params.name, - plane: "azure-cli", + plane: `data-plane${params.name}`, folder: `/workspaces/${params.name}`, - commandTree: {}, + resourceProvider: `${params.name}`, + commandTree: { + names: ["aaz"], + }, }); }), http.get("/AAZ/Specs/Planes", () => { return HttpResponse.json([ { - name: "azure-cli", - displayName: "Azure CLI", - moduleOptions: ["storage", "compute", "network"], + client: "MgmtClient", + displayName: "Control plane", + name: "mgmt-plane", }, { - name: "azure-cli-extensions", - displayName: "Azure CLI Extensions", - moduleOptions: [], + client: "DataPlaneClient", + displayName: "Data plane", + name: "data-plane", }, ]); }), - http.get("/AAZ/Specs/Planes/:planeName/Modules", ({ params }) => { - if (params.planeName === "azure-cli") { - return HttpResponse.json(["storage", "compute", "network", "keyvault"]); + http.get("/AAZ/Specs/Planes/:planeName/Modules", () => { + return HttpResponse.json(["storage", "compute", "network", "keyvault"]); + }), + + http.get("/Swagger/Specs/mgmt-plane", () => { + return HttpResponse.json([ + { url: "/Swagger/Specs/mgmt-plane/addons", name: "addons" }, + { url: "/Swagger/Specs/mgmt-plane/compute", name: "compute" }, + { url: "/Swagger/Specs/mgmt-plane/network", name: "network" }, + { url: "/Swagger/Specs/mgmt-plane/keyvault", name: "keyvault" }, + { url: "/Swagger/Specs/mgmt-plane/containerservice", name: "containerservice" }, + { url: "/Swagger/Specs/mgmt-plane/storage", name: "storage" }, + ]); + }), + + // Resources + http.get("/Swagger/Specs/mgmt-plane/:moduleName/ResourceProviders/:rp/Resources", ({ params }) => { + let rpName = params.rp + ""; + switch (rpName?.toLowerCase()) { + case "storage": + rpName = "Microsoft.Storage"; + break; + case "compute": + rpName = "Microsoft.Compute"; + break; + case "network": + rpName = "Microsoft.Network"; + break; + case "keyvault": + rpName = "Microsoft.KeyVault"; + break; + case "addons": + rpName = "Microsoft.Addons"; + break; + } + + switch (rpName) { + case "Microsoft.Storage": + return HttpResponse.json([ + { + id: "storageAccounts", + versions: [ + { version: "2021-04-01", operations: { read: "GET", write: "PUT", delete: "DELETE", listKeys: "GET" } }, + { version: "2021-09-01", operations: { read: "GET", write: "PUT", delete: "DELETE", listKeys: "GET" } }, + { version: "2022-09-01", operations: { read: "GET", write: "PUT", delete: "DELETE", listKeys: "GET" } }, + { version: "2023-01-01", operations: { read: "GET", write: "PUT", delete: "DELETE", listKeys: "GET" } }, + ], + }, + { + id: "storageAccounts/blobServices", + versions: [ + { version: "2021-04-01", operations: { read: "GET", write: "PUT" } }, + { version: "2021-09-01", operations: { read: "GET", write: "PUT" } }, + { version: "2022-09-01", operations: { read: "GET", write: "PUT" } }, + ], + }, + ]); + + case "Microsoft.Compute": + return HttpResponse.json([ + { + id: "virtualMachines", + versions: [ + { + version: "2021-03-01", + operations: { read: "GET", write: "PUT", delete: "DELETE", start: "POST", stop: "POST" }, + }, + { + version: "2022-03-01", + operations: { read: "GET", write: "PUT", delete: "DELETE", start: "POST", stop: "POST" }, + }, + { + version: "2023-03-01", + operations: { read: "GET", write: "PUT", delete: "DELETE", start: "POST", stop: "POST" }, + }, + ], + }, + { + id: "disks", + versions: [ + { version: "2021-04-01", operations: { read: "GET", write: "PUT", delete: "DELETE" } }, + { version: "2022-03-02", operations: { read: "GET", write: "PUT", delete: "DELETE" } }, + ], + }, + ]); + + case "Microsoft.Network": + return HttpResponse.json([ + { + id: "virtualNetworks", + versions: [ + { version: "2021-02-01", operations: { read: "GET", write: "PUT", delete: "DELETE" } }, + { version: "2022-01-01", operations: { read: "GET", write: "PUT", delete: "DELETE" } }, + { version: "2023-02-01", operations: { read: "GET", write: "PUT", delete: "DELETE" } }, + ], + }, + { + id: "loadBalancers", + versions: [ + { version: "2021-02-01", operations: { read: "GET", write: "PUT", delete: "DELETE" } }, + { version: "2022-01-01", operations: { read: "GET", write: "PUT", delete: "DELETE" } }, + ], + }, + ]); + + case "Microsoft.KeyVault": + return HttpResponse.json([ + { + id: "vaults", + versions: [ + { version: "2021-10-01", operations: { read: "GET", write: "PUT", delete: "DELETE" } }, + { version: "2022-07-01", operations: { read: "GET", write: "PUT", delete: "DELETE" } }, + { version: "2023-02-01", operations: { read: "GET", write: "PUT", delete: "DELETE" } }, + ], + }, + ]); + + case "Microsoft.Addons": + return HttpResponse.json([ + { + id: "/subscriptions/{}/providers/microsoft.addons/supportproviders/{}/supportplantypes/{}", + opGroup: "SupportPlanType", + versions: [ + { + version: "2017-05-15", + operations: { + SupportPlanTypes_CreateOrUpdate: "PUT", + SupportPlanTypes_Delete: "DELETE", + SupportPlanTypes_Get: "GET", + }, + }, + { + version: "2018-03-01", + operations: { + SupportPlanTypes_CreateOrUpdate: "PUT", + SupportPlanTypes_Delete: "DELETE", + SupportPlanTypes_Get: "GET", + }, + }, + ], + }, + { + id: "/subscriptions/{}/providers/microsoft.addons/supportproviders/{}/supportplantypes", + opGroup: "CanonicalSupportPlanType", + versions: [ + { + version: "2017-05-15", + operations: { CanonicalSupportPlanTypes_Get: "GET" }, + }, + ], + }, + ]); + + default: + return HttpResponse.json([]); } - return HttpResponse.json(["extensions-module"]); }), - http.get("/Swagger/Specs/:planeName/:moduleName/ResourceProviders", () => { - const resourceProviders = ["Microsoft.Storage", "Microsoft.Compute", "Microsoft.Network", "Microsoft.KeyVault"]; - return HttpResponse.json(resourceProviders); + // Resource providers: + http.get("/Swagger/Specs/:planeName/:moduleName/ResourceProviders", ({ params }) => { + return HttpResponse.json([ + { + entryFiles: [`specification/${params.moduleName}/Microsoft.BlobStorage/main.tsp`], + name: `Microsoft.${params.moduleName}`, + type: "TypeSpec", + url: `/Swagger/Specs/${params.planeName}/${params.moduleName}/ResourceProviders/${params.moduleName}`, + }, + ]); }), http.get("/CLI/Az/Modules", () => { @@ -229,6 +390,15 @@ export const handlers = [ return HttpResponse.json(response); }), + http.get("/AAZ/Editor/Workspaces/:workspaceName/SwaggerDefault", ({ params }) => { + return HttpResponse.json({ + modNames: [`${params.workspaceName}`], + plane: `data-plane:${params.workspaceName}`, + rpName: `${params.workspaceName}`, + source: "TypeSpec", + }); + }), + http.get("/workspace/:name/Resources/*/V/*/Commands", () => { return HttpResponse.json([ { diff --git a/src/web/src/views/workspace/components/WSEditor/WSEditor.tsx b/src/web/src/views/workspace/components/WSEditor/WSEditor.tsx index 316bddd7..da5bfc17 100644 --- a/src/web/src/views/workspace/components/WSEditor/WSEditor.tsx +++ b/src/web/src/views/workspace/components/WSEditor/WSEditor.tsx @@ -7,7 +7,7 @@ import WSEditorToolBar from "./WSEditorToolBar"; import WSEditorCommandTree from "./WSEditorCommandTree"; import WSEditorCommandGroupContent from "../WSEditorCommandGroupContent"; import WSEditorCommandContent from "../WSEditorCommandContent"; -import WSEditorClientConfigDialog from "./WSEditorClientConfig"; +import WSEditorClientConfig from "./WSEditorClientConfig"; import type { CommandGroup, Command } from "../../interfaces"; import WSEditorExportDialog from "./WSEditorExportDialog"; import WSEditorDeleteDialog from "./WSEditorDeleteDialog"; @@ -293,7 +293,7 @@ const WSEditor = ({ params }: WSEditorProps) => { /> )} {dialogManager.showClientConfigDialog && ( - void; } -interface WSEditorClientConfigDialogState { - updating: boolean; - invalidText: string | undefined; - isAdd: boolean; - - endpointType: "template" | "http-operation"; - - templateAzureCloud: string; - templateAzureChinaCloud: string; - templateAzureUSGovernment: string; - templateAzureGermanCloud: string; - cloudMetadataSelectorIndex: string; - cloudMetadataPrefixTemplate: string; - - aadAuthScopes: string[]; - - planes: Plane[]; - planeOptions: string[]; - selectedPlane: string | null; - - moduleOptions: string[]; - moduleOptionsCommonPrefix: string; - selectedModule: string | null; - - resourceProviderOptions: string[]; - resourceProviderOptionsCommonPrefix: string; - selectedResourceProvider: string | null; - - versionOptions: string[]; - versionResourceIdMap: SwaggerVersionResourceIdMap; - selectedVersion: string | null; - - resourceIdOptions: string[]; - selectedResourceId: string | null; - subresource: string; -} - interface SwaggerVersionResourceIdMap { [version: string]: string[]; } @@ -99,217 +63,143 @@ const MiddlePadding = styled(Box)(() => ({ height: "1.5vh", })); -class WSEditorClientConfigDialog extends React.Component< - WSEditorClientConfigDialogProps, - WSEditorClientConfigDialogState -> { - constructor(props: WSEditorClientConfigDialogProps) { - super(props); - this.state = { - updating: false, - invalidText: undefined, - isAdd: true, - - endpointType: "template", - - templateAzureCloud: "", - templateAzureChinaCloud: "", - templateAzureUSGovernment: "", - templateAzureGermanCloud: "", - cloudMetadataSelectorIndex: "", - cloudMetadataPrefixTemplate: "", - - aadAuthScopes: [""], - - planes: [], - planeOptions: [], - selectedPlane: null, - - moduleOptions: [], - moduleOptionsCommonPrefix: "", - selectedModule: null, - - resourceProviderOptions: [], - resourceProviderOptionsCommonPrefix: "", - selectedResourceProvider: null, - - versionOptions: [], - versionResourceIdMap: {}, - selectedVersion: null, - - resourceIdOptions: [], - selectedResourceId: null, - subresource: "", - }; - } - - componentDidMount(): void { - this.loadPlanes().then(async () => { - await this.loadWorkspaceClientConfig(); - const { selectedPlane, selectedModule, selectedResourceProvider, selectedVersion } = this.state; - await this.onPlaneSelectorUpdate(selectedPlane ?? this.state.planeOptions[0]); - if (selectedModule) { - await this.loadResourceProviders(selectedModule); - } - if (selectedResourceProvider) { - await this.loadResources(selectedResourceProvider, selectedVersion); - } - }); - } +const WSEditorClientConfigDialog: React.FC = ({ workspaceUrl, open, onClose }) => { + const [updating, setUpdating] = useState(false); + const [invalidText, setInvalidText] = useState(undefined); + const [isAdd, setIsAdd] = useState(true); + + const [endpointType, setEndpointType] = useState<"template" | "http-operation">("template"); + + const [templateAzureCloud, setTemplateAzureCloud] = useState(""); + const [templateAzureChinaCloud, setTemplateAzureChinaCloud] = useState(""); + const [templateAzureUSGovernment, setTemplateAzureUSGovernment] = useState(""); + const [templateAzureGermanCloud, setTemplateAzureGermanCloud] = useState(""); + const [cloudMetadataSelectorIndex, setCloudMetadataSelectorIndex] = useState(""); + const [cloudMetadataPrefixTemplate, setCloudMetadataPrefixTemplate] = useState(""); + + const [aadAuthScopes, setAadAuthScopes] = useState([""]); + + const [selectedPlane, setSelectedPlane] = useState(null); + + const [moduleOptions, setModuleOptions] = useState([]); + const [moduleOptionsCommonPrefix, setModuleOptionsCommonPrefix] = useState(""); + const [selectedModule, setSelectedModule] = useState(null); + + const [resourceProviderOptions, setResourceProviderOptions] = useState([]); + const [resourceProviderOptionsCommonPrefix, setResourceProviderOptionsCommonPrefix] = useState(""); + const [selectedResourceProvider, setSelectedResourceProvider] = useState(null); + + const [versionOptions, setVersionOptions] = useState([]); + const [versionResourceIdMap, setVersionResourceIdMap] = useState({}); + const [selectedVersion, setSelectedVersion] = useState(null); - loadPlanes = async () => { + const [resourceIdOptions, setResourceIdOptions] = useState([]); + const [selectedResourceId, setSelectedResourceId] = useState(null); + const [subresource, setSubresource] = useState(""); + + const loadPlanes = useCallback(async () => { try { - this.setState({ - updating: true, - }); - - const planes = await specsApi.getPlanes(); - const planeOptions: string[] = planes.map((v: any) => v.displayName); - this.setState({ - planes: planes, - planeOptions: planeOptions, - updating: false, - }); - await this.onPlaneSelectorUpdate(planeOptions[0]); + setUpdating(true); + + const planesData = await specsApi.getPlanes(); + setUpdating(false); + + if (planesData.length > 0) { + const firstPlane = planesData[0]; + setSelectedPlane(firstPlane.displayName); + await loadSwaggerModules(firstPlane); + } } catch (err: any) { console.error(err); const message = errorHandlerApi.getErrorMessage(err); - this.setState({ - updating: false, - invalidText: `ResponseError: ${message}`, - }); + setUpdating(false); + setInvalidText(`ResponseError: ${message}`); } - }; + }, []); - onPlaneSelectorUpdate = async (planeDisplayName: string | null) => { - const plane = this.state.planes.find((v) => v.displayName === planeDisplayName) ?? null; - if (this.state.selectedPlane !== (plane?.displayName ?? null)) { - if (!plane) { - return; - } - this.setState({ - selectedPlane: plane?.displayName ?? null, - }); - await this.loadSwaggerModules(plane); - } else { - this.setState({ - selectedPlane: plane?.displayName ?? null, - }); - } - }; - - loadSwaggerModules = async (plane: Plane | null) => { + const loadSwaggerModules = useCallback(async (plane: Plane | null) => { if (plane !== null) { if (plane!.moduleOptions?.length) { - this.setState({ - moduleOptions: plane!.moduleOptions!, - moduleOptionsCommonPrefix: `/Swagger/Specs/${plane!.name}/`, - }); - await this.onModuleSelectionUpdate(null); + setModuleOptions(plane!.moduleOptions!); + setModuleOptionsCommonPrefix(`/Swagger/Specs/${plane!.name}/`); + await onModuleSelectionUpdate(null); } else { try { - this.setState({ - updating: true, - }); + setUpdating(true); const options = await specsApi.getSwaggerModules(plane!.name); - this.setState((preState) => { - const planes = preState.planes; - const index = planes.findIndex((v) => v.name === plane!.name); - planes[index].moduleOptions = options; - return { - ...preState, - updating: false, - planes: planes, - moduleOptions: options, - moduleOptionsCommonPrefix: `/Swagger/Specs/${plane!.name}/`, - }; - }); - await this.onModuleSelectionUpdate(null); + setUpdating(false); + setModuleOptions(options); + setModuleOptionsCommonPrefix(`/Swagger/Specs/${plane!.name}/`); + await onModuleSelectionUpdate(null); } catch (err: any) { console.error(err); const message = errorHandlerApi.getErrorMessage(err); - this.setState({ - updating: false, - invalidText: `ResponseError: ${message}`, - }); + setUpdating(false); + setInvalidText(`ResponseError: ${message}`); } } } else { - this.setState({ - moduleOptions: [], - moduleOptionsCommonPrefix: "", - }); - await this.onModuleSelectionUpdate(null); - } - }; - - onModuleSelectionUpdate = async (moduleValueUrl: string | null) => { - if (this.state.selectedModule !== moduleValueUrl) { - this.setState({ - selectedModule: moduleValueUrl, - }); - await this.loadResourceProviders(moduleValueUrl); - } else { - this.setState({ - selectedModule: moduleValueUrl, - }); + setModuleOptions([]); + setModuleOptionsCommonPrefix(""); + await onModuleSelectionUpdate(null); } - }; + }, []); - loadResourceProviders = async (moduleUrl: string | null) => { + const onModuleSelectionUpdate = useCallback( + async (moduleValueUrl: string | null) => { + if (selectedModule !== moduleValueUrl) { + setSelectedModule(moduleValueUrl); + await loadResourceProviders(moduleValueUrl); + } else { + setSelectedModule(moduleValueUrl); + } + }, + [selectedModule], + ); + + const loadResourceProviders = useCallback(async (moduleUrl: string | null) => { if (moduleUrl !== null) { try { - this.setState({ - updating: true, - }); + setUpdating(true); const options = await specsApi.getResourceProviders(moduleUrl); - const selectedResourceProvider = options.length === 1 ? options[0] : null; - this.setState({ - updating: false, - resourceProviderOptions: options, - resourceProviderOptionsCommonPrefix: `${moduleUrl}/ResourceProviders/`, - }); - this.onResourceProviderUpdate(selectedResourceProvider); + const selectedRP = options.length === 1 ? options[0] : null; + setUpdating(false); + setResourceProviderOptions(options); + setResourceProviderOptionsCommonPrefix(`${moduleUrl}/ResourceProviders/`); + onResourceProviderUpdate(selectedRP); } catch (err: any) { console.error(err); const message = errorHandlerApi.getErrorMessage(err); - this.setState({ - updating: false, - invalidText: `ResponseError: ${message}`, - }); + setUpdating(false); + setInvalidText(`ResponseError: ${message}`); } } else { - this.setState({ - resourceProviderOptions: [], - resourceProviderOptionsCommonPrefix: "", - }); - this.onResourceProviderUpdate(null); + setResourceProviderOptions([]); + setResourceProviderOptionsCommonPrefix(""); + onResourceProviderUpdate(null); } - }; - - onResourceProviderUpdate = async (resourceProviderUrl: string | null) => { - if (this.state.selectedResourceProvider !== resourceProviderUrl) { - this.setState({ - selectedResourceProvider: resourceProviderUrl, - }); - await this.loadResources(resourceProviderUrl, null); - } else { - this.setState({ - selectedResourceProvider: resourceProviderUrl, - }); - } - }; + }, []); + + const onResourceProviderUpdate = useCallback( + async (resourceProviderUrl: string | null) => { + if (selectedResourceProvider !== resourceProviderUrl) { + setSelectedResourceProvider(resourceProviderUrl); + await loadResources(resourceProviderUrl, null); + } else { + setSelectedResourceProvider(resourceProviderUrl); + } + }, + [selectedResourceProvider], + ); - loadResources = async (resourceProviderUrl: string | null, selectVersion: string | null) => { + const loadResources = useCallback(async (resourceProviderUrl: string | null, selectVersion: string | null) => { if (resourceProviderUrl != null) { - this.setState({ - invalidText: undefined, - updating: true, - }); + setInvalidText(undefined); + setUpdating(true); try { const resources = await specsApi.getProviderResources(resourceProviderUrl); - const versionResourceIdMap: SwaggerVersionResourceIdMap = {}; - const versionOptions: string[] = []; + const versionResIdMap: SwaggerVersionResourceIdMap = {}; + const versionOpts: string[] = []; const resourceIdList: string[] = []; resources.forEach((resource: any) => { resourceIdList.push(resource.id); @@ -324,685 +214,610 @@ class WSEditorClientConfigDialog extends React.Component< }) .map((v: any) => v.version); resourceVersions.forEach((v: any) => { - if (!(v in versionResourceIdMap)) { - versionResourceIdMap[v] = []; - versionOptions.push(v); + if (!(v in versionResIdMap)) { + versionResIdMap[v] = []; + versionOpts.push(v); } - versionResourceIdMap[v].push(resource.id); + versionResIdMap[v].push(resource.id); }); }); - versionOptions.sort((a, b) => a.localeCompare(b)).reverse(); + versionOpts.sort((a, b) => a.localeCompare(b)).reverse(); if ( selectVersion === null && - (versionOptions.length === 0 || versionOptions.findIndex((v) => v === selectVersion) < 0) + (versionOpts.length === 0 || versionOpts.findIndex((v) => v === selectVersion) < 0) ) { selectVersion = null; } - if (!selectVersion && versionOptions.length > 0) { - selectVersion = versionOptions[0]; + if (!selectVersion && versionOpts.length > 0) { + selectVersion = versionOpts[0]; } - this.setState({ - updating: false, - versionResourceIdMap: versionResourceIdMap, - versionOptions: versionOptions, - }); - this.onVersionUpdate(selectVersion); + setUpdating(false); + setVersionResourceIdMap(versionResIdMap); + setVersionOptions(versionOpts); + onVersionUpdate(selectVersion, versionResIdMap); } catch (err: any) { console.error(err); const message = errorHandlerApi.getErrorMessage(err); - this.setState({ - invalidText: `ResponseError: ${message}`, - }); + setInvalidText(`ResponseError: ${message}`); } } else { - this.setState({ - versionOptions: [], - }); - this.onVersionUpdate(null); + setVersionOptions([]); + onVersionUpdate(null); } - }; - - onVersionUpdate = (version: string | null) => { - this.setState((preState) => { - let selectedResourceId = preState.selectedResourceId; - let resourceIdOptions: string[] = []; - if (version != null) { - resourceIdOptions = [...preState.versionResourceIdMap[version]].sort((a, b) => - a.toString().localeCompare(b.toString()), - ); - if (selectedResourceId !== null && resourceIdOptions.findIndex((v) => v === selectedResourceId) < 0) { - selectedResourceId = null; + }, []); + + const onVersionUpdate = useCallback( + (version: string | null, versionResIdMap?: SwaggerVersionResourceIdMap) => { + const mapToUse = versionResIdMap || versionResourceIdMap; + let newSelectedResourceId = selectedResourceId; + let resourceIdOpts: string[] = []; + if (version != null && mapToUse[version]) { + resourceIdOpts = [...mapToUse[version]].sort((a, b) => a.toString().localeCompare(b.toString())); + if (newSelectedResourceId !== null && resourceIdOpts.findIndex((v) => v === newSelectedResourceId) < 0) { + newSelectedResourceId = null; } } - return { - ...preState, - resourceIdOptions: resourceIdOptions, - selectedVersion: version, - preferredAAZVersion: version, - selectedResourceId: selectedResourceId, - }; - }); - }; - - loadWorkspaceClientConfig = async () => { - this.setState({ updating: true }); + setResourceIdOptions(resourceIdOpts); + setSelectedVersion(version); + setSelectedResourceId(newSelectedResourceId); + }, + [selectedResourceId, versionResourceIdMap], + ); + + const loadWorkspaceClientConfig = useCallback(async () => { + setUpdating(true); try { - const clientConfigData = await workspaceApi.getClientConfig(this.props.workspaceUrl); + const clientConfigData = await workspaceApi.getClientConfig(workspaceUrl); const clientConfig: ClientConfig = { version: clientConfigData.version, auth: clientConfigData.auth, }; - let templateAzureCloud = ""; - let templateAzureChinaCloud = ""; - let templateAzureUSGovernment = ""; - let templateAzureGermanCloud = ""; - let cloudMetadataSelectorIndex = ""; - let cloudMetadataPrefixTemplate = ""; - let endpointType: "template" | "http-operation" = "template"; - let selectedPlane: string | null = null; - let selectedModule: string | null = null; - let selectedResourceProvider: string | null = null; - let selectedVersion: string | null = null; - let selectedResourceId: string | null = null; - let subresource: string = ""; - - if (clientConfigData.endpoints.type === "template") { + let templateAzureCloudVal = ""; + let templateAzureChinaCloudVal = ""; + let templateAzureUSGovernmentVal = ""; + let templateAzureGermanCloudVal = ""; + let cloudMetadataSelectorIndexVal = ""; + let cloudMetadataPrefixTemplateVal = ""; + let endpointTypeVal: "template" | "http-operation" = "template"; + let selectedPlaneVal: string | null = null; + let selectedModuleVal: string | null = null; + let selectedResourceProviderVal: string | null = null; + let selectedVersionVal: string | null = null; + let selectedResourceIdVal: string | null = null; + let subresourceVal: string = ""; + + if (clientConfigData.endpoints?.type === "template") { clientConfig.endpointTemplates = {}; clientConfigData.endpoints.templates.forEach((value: any) => { clientConfig.endpointTemplates![value.cloud] = value.template; }); clientConfig.endpointCloudMetadata = clientConfigData.endpoints.cloudMetadata; - endpointType = "template"; - templateAzureCloud = clientConfig.endpointTemplates!["AzureCloud"] ?? ""; - templateAzureChinaCloud = clientConfig.endpointTemplates!["AzureChinaCloud"] ?? ""; - templateAzureUSGovernment = clientConfig.endpointTemplates!["AzureUSGovernment"] ?? ""; - templateAzureGermanCloud = clientConfig.endpointTemplates!["AzureGermanCloud"] ?? ""; - cloudMetadataSelectorIndex = clientConfig.endpointCloudMetadata?.selectorIndex ?? ""; - cloudMetadataPrefixTemplate = clientConfig.endpointCloudMetadata?.prefixTemplate ?? ""; - } else if (clientConfigData.endpoints.type === "http-operation") { + endpointTypeVal = "template"; + templateAzureCloudVal = clientConfig.endpointTemplates!["AzureCloud"] ?? ""; + templateAzureChinaCloudVal = clientConfig.endpointTemplates!["AzureChinaCloud"] ?? ""; + templateAzureUSGovernmentVal = clientConfig.endpointTemplates!["AzureUSGovernment"] ?? ""; + templateAzureGermanCloudVal = clientConfig.endpointTemplates!["AzureGermanCloud"] ?? ""; + cloudMetadataSelectorIndexVal = clientConfig.endpointCloudMetadata?.selectorIndex ?? ""; + cloudMetadataPrefixTemplateVal = clientConfig.endpointCloudMetadata?.prefixTemplate ?? ""; + } else if (clientConfigData.endpoints?.type === "http-operation") { clientConfig.endpointResource = clientConfigData.endpoints.resource; const rpUrl: string = clientConfig.endpointResource!.swagger.split("/Paths/")[0]; const moduleUrl: string = rpUrl.split("/ResourceProviders/")[0]; const planeUrl: string = moduleUrl.split("/")[0]; - selectedResourceProvider = `/Swagger/Specs/${rpUrl}`; - selectedModule = `/Swagger/Specs/${moduleUrl}`; - selectedPlane = `/Swagger/Specs/${planeUrl}`; - selectedVersion = clientConfig.endpointResource!.version; - selectedResourceId = clientConfig.endpointResource!.id; - subresource = clientConfig.endpointResource!.subresource ?? ""; - endpointType = "http-operation"; + selectedResourceProviderVal = `/Swagger/Specs/${rpUrl}`; + selectedModuleVal = `/Swagger/Specs/${moduleUrl}`; + selectedPlaneVal = `/Swagger/Specs/${planeUrl}`; + selectedVersionVal = clientConfig.endpointResource!.version; + selectedResourceIdVal = clientConfig.endpointResource!.id; + subresourceVal = clientConfig.endpointResource!.subresource ?? ""; + endpointTypeVal = "http-operation"; } - this.setState({ - aadAuthScopes: clientConfig.auth.aad.scopes ?? [""], - endpointType: endpointType, - templateAzureCloud: templateAzureCloud, - templateAzureChinaCloud: templateAzureChinaCloud, - templateAzureUSGovernment: templateAzureUSGovernment, - templateAzureGermanCloud: templateAzureGermanCloud, - cloudMetadataSelectorIndex: cloudMetadataSelectorIndex, - cloudMetadataPrefixTemplate: cloudMetadataPrefixTemplate, - selectedPlane: selectedPlane, - selectedModule: selectedModule, - selectedResourceProvider: selectedResourceProvider, - selectedVersion: selectedVersion, - selectedResourceId: selectedResourceId, - subresource: subresource, - isAdd: false, - }); + setAadAuthScopes(clientConfig.auth.aad.scopes ?? [""]); + setEndpointType(endpointTypeVal); + setTemplateAzureCloud(templateAzureCloudVal); + setTemplateAzureChinaCloud(templateAzureChinaCloudVal); + setTemplateAzureUSGovernment(templateAzureUSGovernmentVal); + setTemplateAzureGermanCloud(templateAzureGermanCloudVal); + setCloudMetadataSelectorIndex(cloudMetadataSelectorIndexVal); + setCloudMetadataPrefixTemplate(cloudMetadataPrefixTemplateVal); + setSelectedPlane(selectedPlaneVal); + setSelectedModule(selectedModuleVal); + setSelectedResourceProvider(selectedResourceProviderVal); + setSelectedVersion(selectedVersionVal); + setSelectedResourceId(selectedResourceIdVal); + setSubresource(subresourceVal); + setIsAdd(false); } catch (err: any) { if (errorHandlerApi.isHttpError(err, 404)) { - this.setState({ - isAdd: true, - }); + setIsAdd(true); } else { console.error(err); const message = errorHandlerApi.getErrorMessage(err); - this.setState({ invalidText: `ResponseError: ${message}` }); + setInvalidText(`ResponseError: ${message}`); } } - this.setState({ updating: false }); - }; + setUpdating(false); + }, [workspaceUrl]); + + useEffect(() => { + const initializeComponent = async () => { + await loadPlanes(); + await loadWorkspaceClientConfig(); + }; + + if (open) { + initializeComponent(); + } + }, [open, loadPlanes, loadWorkspaceClientConfig]); - handleClose = () => { - this.props.onClose(false); - }; + const handleClose = useCallback(() => { + onClose(false); + }, [onClose]); - handleUpdate = async () => { - let { aadAuthScopes } = this.state; - const { endpointType } = this.state; + const handleUpdate = useCallback(async () => { + let currentAadAuthScopes = [...aadAuthScopes]; let templates: ClientEndpointTemplate[] | undefined = undefined; let resource: ClientEndpointResource | undefined = undefined; let cloudMetadata: ClientEndpointCloudMetadata | undefined = undefined; if (endpointType === "template") { - let { templateAzureCloud, templateAzureChinaCloud, templateAzureGermanCloud, templateAzureUSGovernment } = - this.state; - templateAzureCloud = templateAzureCloud.trim(); - if (templateAzureCloud.length < 1) { - this.setState({ - invalidText: "Azure Cloud Endpoint Template is required.", - }); + let currentTemplateAzureCloud = templateAzureCloud.trim(); + if (currentTemplateAzureCloud.length < 1) { + setInvalidText("Azure Cloud Endpoint Template is required."); return; } - templateAzureChinaCloud = templateAzureChinaCloud.trim(); - templateAzureUSGovernment = templateAzureUSGovernment.trim(); - templateAzureGermanCloud = templateAzureGermanCloud.trim(); + let currentTemplateAzureChinaCloud = templateAzureChinaCloud.trim(); + let currentTemplateAzureUSGovernment = templateAzureUSGovernment.trim(); + let currentTemplateAzureGermanCloud = templateAzureGermanCloud.trim(); const templateRegex = /^https:\/\/((\{[a-zA-Z0-9]+\})|([^{}.]+))(.((\{[a-zA-Z0-9]+\})|([^{}.]+)))*(\/)?$/; - if (!templateRegex.test(templateAzureCloud)) { - this.setState({ - invalidText: "Azure Cloud Endpoint Template is invalid.", - }); + if (!templateRegex.test(currentTemplateAzureCloud)) { + setInvalidText("Azure Cloud Endpoint Template is invalid."); return; } - if (templateAzureChinaCloud.length > 0 && !templateRegex.test(templateAzureChinaCloud)) { - this.setState({ - invalidText: "Azure China Cloud Endpoint Template is invalid.", - }); + if (currentTemplateAzureChinaCloud.length > 0 && !templateRegex.test(currentTemplateAzureChinaCloud)) { + setInvalidText("Azure China Cloud Endpoint Template is invalid."); return; } - if (templateAzureUSGovernment.length > 0 && !templateRegex.test(templateAzureUSGovernment)) { - this.setState({ - invalidText: "Azure US Government Endpoint Template is invalid.", - }); + if (currentTemplateAzureUSGovernment.length > 0 && !templateRegex.test(currentTemplateAzureUSGovernment)) { + setInvalidText("Azure US Government Endpoint Template is invalid."); return; } - if (templateAzureGermanCloud.length > 0 && !templateRegex.test(templateAzureGermanCloud)) { - this.setState({ - invalidText: "Azure German Cloud Endpoint Template is invalid.", - }); + if (currentTemplateAzureGermanCloud.length > 0 && !templateRegex.test(currentTemplateAzureGermanCloud)) { + setInvalidText("Azure German Cloud Endpoint Template is invalid."); return; } - templates = [{ cloud: "AzureCloud", template: templateAzureCloud }]; - if (templateAzureChinaCloud.length > 0) { - templates.push({ cloud: "AzureChinaCloud", template: templateAzureChinaCloud }); + templates = [{ cloud: "AzureCloud", template: currentTemplateAzureCloud }]; + if (currentTemplateAzureChinaCloud.length > 0) { + templates.push({ cloud: "AzureChinaCloud", template: currentTemplateAzureChinaCloud }); } - if (templateAzureUSGovernment.length > 0) { - templates.push({ cloud: "AzureUSGovernment", template: templateAzureUSGovernment }); + if (currentTemplateAzureUSGovernment.length > 0) { + templates.push({ cloud: "AzureUSGovernment", template: currentTemplateAzureUSGovernment }); } - if (templateAzureGermanCloud.length > 0) { - templates.push({ cloud: "AzureGermanCloud", template: templateAzureGermanCloud }); + if (currentTemplateAzureGermanCloud.length > 0) { + templates.push({ cloud: "AzureGermanCloud", template: currentTemplateAzureGermanCloud }); } - let { cloudMetadataSelectorIndex, cloudMetadataPrefixTemplate } = this.state; - cloudMetadataSelectorIndex = cloudMetadataSelectorIndex.trim(); - cloudMetadataPrefixTemplate = cloudMetadataPrefixTemplate.trim(); - if (cloudMetadataSelectorIndex.length < 1 && cloudMetadataPrefixTemplate.length > 0) { - this.setState({ - invalidText: "Cloud Metadata Selector Index is required.", - }); + let currentCloudMetadataSelectorIndex = cloudMetadataSelectorIndex.trim(); + let currentCloudMetadataPrefixTemplate = cloudMetadataPrefixTemplate.trim(); + if (currentCloudMetadataSelectorIndex.length < 1 && currentCloudMetadataPrefixTemplate.length > 0) { + setInvalidText("Cloud Metadata Selector Index is required."); return; - } else if (cloudMetadataSelectorIndex.length > 0) { + } else if (currentCloudMetadataSelectorIndex.length > 0) { cloudMetadata = { - selectorIndex: cloudMetadataSelectorIndex, + selectorIndex: currentCloudMetadataSelectorIndex, }; - if (cloudMetadataPrefixTemplate.length > 0) { - if (!templateRegex.test(cloudMetadataPrefixTemplate)) { - this.setState({ - invalidText: "Cloud Metadata Prefix is invalid.", - }); + if (currentCloudMetadataPrefixTemplate.length > 0) { + if (!templateRegex.test(currentCloudMetadataPrefixTemplate)) { + setInvalidText("Cloud Metadata Prefix is invalid."); return; } - cloudMetadata.prefixTemplate = cloudMetadataPrefixTemplate; + cloudMetadata.prefixTemplate = currentCloudMetadataPrefixTemplate; } } } else if (endpointType === "http-operation") { - let { subresource } = this.state; - const { - selectedPlane, - selectedModule, - selectedResourceProvider, - selectedVersion, - selectedResourceId, - moduleOptionsCommonPrefix, - } = this.state; - if (!selectedPlane) { - this.setState({ - invalidText: "Plane is required.", - }); - return; - } + let currentSubresource = subresource; if (!selectedModule) { - this.setState({ - invalidText: "Module is required.", - }); + setInvalidText("Module is required."); return; } if (!selectedResourceProvider) { - this.setState({ - invalidText: "Resource Provider is required.", - }); + setInvalidText("Resource Provider is required."); return; } if (!selectedVersion) { - this.setState({ - invalidText: "API Version is required.", - }); + setInvalidText("API Version is required."); return; } if (!selectedResourceId) { - this.setState({ - invalidText: "Resource ID is required.", - }); + setInvalidText("Resource ID is required."); return; } - subresource = subresource.trim(); - if (subresource.length < 1) { - this.setState({ - invalidText: "Endpoint Property Index is required.", - }); + currentSubresource = currentSubresource.trim(); + if (currentSubresource.length < 1) { + setInvalidText("Endpoint Property Index is required."); return; } resource = { - plane: selectedPlane.replace("/Swagger/Specs/", ""), + plane: selectedPlane?.replace("/Swagger/Specs/", "") ?? "", module: selectedModule.replace(moduleOptionsCommonPrefix, ""), version: selectedVersion, id: selectedResourceId, - subresource: subresource, + subresource: currentSubresource, }; } - aadAuthScopes = aadAuthScopes.map((scope) => scope.trim()).filter((scope) => scope.length > 0); - if (aadAuthScopes.length < 1) { - this.setState({ - invalidText: "MS Entra(AAD) Auth Scopes is required.", - }); + currentAadAuthScopes = currentAadAuthScopes.map((scope) => scope.trim()).filter((scope) => scope.length > 0); + if (currentAadAuthScopes.length < 1) { + setInvalidText("MS Entra(AAD) Auth Scopes is required."); return; } const auth = { aad: { - scopes: aadAuthScopes, + scopes: currentAadAuthScopes, }, }; - this.onUpdateClientConfig(templates, cloudMetadata, resource, auth); - }; - - onUpdateClientConfig = async ( - templates: ClientEndpointTemplate[] | undefined, - cloudMetadata: ClientEndpointCloudMetadata | undefined, - resource: ClientEndpointResource | undefined, - auth: ClientAuth, - ) => { - this.setState({ updating: true }); - try { - await workspaceApi.updateClientConfig(this.props.workspaceUrl, { - templates: templates, - cloudMetadata: cloudMetadata, - resource: resource, - auth: auth, - }); - this.setState({ updating: false }); - this.props.onClose(true); - } catch (err: any) { - console.error(err); - const message = errorHandlerApi.getErrorMessage(err); - this.setState({ invalidText: `ResponseError: ${message}` }); - this.setState({ updating: false }); - } - }; - - onRemoveAadScope = (idx: number) => { - this.setState((preState) => { - const aadAuthScopes: string[] = [ - ...preState.aadAuthScopes.slice(0, idx), - ...preState.aadAuthScopes.slice(idx + 1), - ]; - if (aadAuthScopes.length === 0) { - aadAuthScopes.push(""); + onUpdateClientConfig(templates, cloudMetadata, resource, auth); + }, [ + aadAuthScopes, + endpointType, + templateAzureCloud, + templateAzureChinaCloud, + templateAzureUSGovernment, + templateAzureGermanCloud, + cloudMetadataSelectorIndex, + cloudMetadataPrefixTemplate, + selectedPlane, + selectedModule, + selectedResourceProvider, + selectedVersion, + selectedResourceId, + subresource, + moduleOptionsCommonPrefix, + ]); + + const onUpdateClientConfig = useCallback( + async ( + templates: ClientEndpointTemplate[] | undefined, + cloudMetadata: ClientEndpointCloudMetadata | undefined, + resource: ClientEndpointResource | undefined, + auth: ClientAuth, + ) => { + setUpdating(true); + try { + await workspaceApi.updateClientConfig(workspaceUrl, { + templates: templates, + cloudMetadata: cloudMetadata, + resource: resource, + auth: auth, + }); + setUpdating(false); + onClose(true); + } catch (err: any) { + console.error(err); + const message = errorHandlerApi.getErrorMessage(err); + setInvalidText(`ResponseError: ${message}`); + setUpdating(false); } - return { - ...preState, - aadAuthScopes: aadAuthScopes, - }; - }); - }; - - onModifyAadScope = (scope: string, idx: number) => { - this.setState((preState) => { - return { - ...preState, - aadAuthScopes: [...preState.aadAuthScopes.slice(0, idx), scope, ...preState.aadAuthScopes.slice(idx + 1)], - }; - }); - }; - - onAddAadScope = () => { - this.setState((preState) => { - return { - ...preState, - aadAuthScopes: [...preState.aadAuthScopes, ""], - }; + }, + [workspaceUrl, onClose], + ); + + const onRemoveAadScope = useCallback((idx: number) => { + setAadAuthScopes((prev) => { + const newScopes = [...prev.slice(0, idx), ...prev.slice(idx + 1)]; + if (newScopes.length === 0) { + newScopes.push(""); + } + return newScopes; }); - }; - - buildAadScopeInput = (scope: string, idx: number) => { - return ( - - this.onRemoveAadScope(idx)} aria-label="remove"> - - - { - this.onModifyAadScope(event.target.value, idx); + }, []); + + const onModifyAadScope = useCallback((scope: string, idx: number) => { + setAadAuthScopes((prev) => [...prev.slice(0, idx), scope, ...prev.slice(idx + 1)]); + }, []); + + const onAddAadScope = useCallback(() => { + setAadAuthScopes((prev) => [...prev, ""]); + }, []); + + const buildAadScopeInput = useCallback( + (scope: string, idx: number) => { + return ( + - - ); - }; - - render() { - const { - invalidText, - updating, - isAdd, - aadAuthScopes, - endpointType, - templateAzureCloud, - templateAzureChinaCloud, - templateAzureUSGovernment, - templateAzureGermanCloud, - cloudMetadataSelectorIndex, - cloudMetadataPrefixTemplate, - } = this.state; - const { selectedModule, selectedResourceProvider, selectedVersion, selectedResourceId, subresource } = this.state; - return ( - - {isAdd ? "Setup Client Config" : "Modify Client Config"} - - {invalidText && ( - - {" "} - {invalidText}{" "} - - )} - - Endpoint - - - - { - this.setState({ - endpointType: newValue, - }); + > + onRemoveAadScope(idx)} aria-label="remove"> + + + { + onModifyAadScope(event.target.value, idx); + }} + sx={{ flexGrow: 1 }} + placeholder="Input Microsoft Entra(AAD) auth Scope here, e.g. https://metrics.monitor.azure.com/.default" + /> + + ); + }, + [onRemoveAadScope, onModifyAadScope], + ); + + return ( + + {isAdd ? "Setup Client Config" : "Modify Client Config"} + + {invalidText && ( + + {" "} + {invalidText}{" "} + + )} + + Endpoint + + + + { + setEndpointType(newValue); + }} + > + + + + + {endpointType === "template" && ( + + Default Templates + + - - - - - {endpointType === "template" && ( - { + setTemplateAzureCloud(event.target.value); }} - > - Default Templates - - { - this.setState({ - templateAzureCloud: event.target.value, - }); - }} - margin="dense" - required - /> - - { - this.setState({ - templateAzureChinaCloud: event.target.value, - }); - }} - margin="normal" - /> - - { - this.setState({ - templateAzureUSGovernment: event.target.value, - }); - }} - margin="normal" - /> - - { - this.setState({ - templateAzureGermanCloud: event.target.value, - }); - }} - margin="normal" - /> - From Cloud Metadata - - { - this.setState({ - cloudMetadataSelectorIndex: event.target.value, - }); - }} - margin="dense" - /> - - { - this.setState({ - cloudMetadataPrefixTemplate: event.target.value, - }); - }} - margin="dense" - /> - - .Suffix - - - )} - {endpointType === "http-operation" && ( + margin="dense" + required + /> + + { + setTemplateAzureChinaCloud(event.target.value); + }} + margin="normal" + /> + + { + setTemplateAzureUSGovernment(event.target.value); + }} + margin="normal" + /> + + { + setTemplateAzureGermanCloud(event.target.value); + }} + margin="normal" + /> + From Cloud Metadata + + { + setCloudMetadataSelectorIndex(event.target.value); + }} + margin="dense" + /> - - - - - - - { - this.setState({ - selectedResourceId: resourceId, - }); - }} - /> - { - this.setState({ - subresource: event.target.value, - }); + setCloudMetadataPrefixTemplate(event.target.value); }} margin="dense" - required /> + + .Suffix - )} - - - - MS Entra(AAD) Auth Scopes - - {aadAuthScopes?.map(this.buildAadScopeInput)} - - - - - One more scope - - - - {updating && ( - - )} - {!updating && ( - - {!isAdd && } - - + {endpointType === "http-operation" && ( + + + + + + + + { + setSelectedResourceId(resourceId); + }} + /> + + { + setSubresource(event.target.value); + }} + margin="dense" + required + /> + )} - - - ); - } -} + + + + MS Entra(AAD) Auth Scopes + + {aadAuthScopes?.map(buildAadScopeInput)} + + + + + One more scope + + + + {updating && ( + + + + )} + {!updating && ( + + {!isAdd && } + + + )} + + + ); +}; interface ClientEndpointTemplate { cloud: string; diff --git a/src/web/src/views/workspace/components/WSEditor/index.ts b/src/web/src/views/workspace/components/WSEditor/index.ts index b25ec00e..16668d05 100644 --- a/src/web/src/views/workspace/components/WSEditor/index.ts +++ b/src/web/src/views/workspace/components/WSEditor/index.ts @@ -6,4 +6,4 @@ export { default as WSEditorExportDialog } from "./WSEditorExportDialog"; export { default as WSEditorDeleteDialog } from "./WSEditorDeleteDialog"; export { default as WSEditorSwaggerReloadDialog } from "./WSEditorSwaggerReloadDialog"; export { default as WSRenameDialog } from "./WSRenameDialog"; -export { default as WSEditorClientConfigDialog } from "./WSEditorClientConfig"; +export { default as WSEditorClientConfig } from "./WSEditorClientConfig";