diff --git a/packages/zowe-explorer/CHANGELOG.md b/packages/zowe-explorer/CHANGELOG.md index 80cfc95dbb..f9a52fcdb1 100644 --- a/packages/zowe-explorer/CHANGELOG.md +++ b/packages/zowe-explorer/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen ### New features and enhancements +- Users can now follow a prompt to create a new Zowe client configuration. The prompt displays when VS Code is opened with Zowe Explorer installed, but the user does not have any Zowe client configurations. [#3148](https://github.com/zowe/zowe-explorer-vscode/pull/3148) + ### Bug fixes - The "Zowe Resources" panel is now hidden by default until Zowe Explorer reveals it to display a table or other data. [#3113](https://github.com/zowe/zowe-explorer-vscode/issues/3113) @@ -15,6 +17,7 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen - Fixed issue where file extensions were removed from data sets, causing language detection to sometimes fail for Zowe Explorer extenders. [#3121](https://github.com/zowe/zowe-explorer-vscode/issues/3121) - Fixed an issue where copying and pasting a file/folder in the USS tree would fail abruptly, displaying an error. [#3128](https://github.com/zowe/zowe-explorer-vscode/issues/3128) - Removal of broken VSC command to `Zowe Explorer: Refresh Zowe Explorer`, use VS Code's `Extensions: Refresh` command instead. [#3100](https://github.com/zowe/zowe-explorer-vscode/issues/3100) +- Fixed issue where Zowe Explorer would reload the VS Code window during initialization when no config files are present. [#3147](https://github.com/zowe/zowe-explorer-vscode/issues/3147) ## `3.0.0-next.202409132122` diff --git a/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts index 42634d45cc..02844adc82 100644 --- a/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts @@ -94,6 +94,7 @@ async function createGlobalMocks() { getZoweDir: jest.fn(), }; }), + mockPromptUserWithNoConfigs: jest.fn(), mockUpdateCredMgrSetting: jest.fn(), mockWriteOverridesFile: jest.fn(), mockProfCacheProfileInfo: createInstanceOfProfileInfo(), @@ -360,6 +361,10 @@ async function createGlobalMocks() { get: globalMocks.mockImperativeProfileInfo, configurable: true, }); + Object.defineProperty(ProfilesUtils, "promptUserWithNoConfigs", { + value: globalMocks.mockPromptUserWithNoConfigs, + configurable: true, + }); // Create a mocked extension context const mockExtensionCreator = jest.fn( diff --git a/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts index 4fda6fcfde..520639e15b 100644 --- a/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts @@ -349,6 +349,10 @@ describe("ProfilesUtils unit tests", () => { inspect: jest.fn(), update: jest.fn(), }); + const onlyV1ProfsExistMock = new MockedProperty(imperative.ProfileInfo, "onlyV1ProfilesExist", { + configurable: true, + get: () => true, + }); const executeCommandMock = jest.spyOn(vscode.commands, "executeCommand").mockImplementation(); const getValueMock = jest.spyOn(ZoweLocalStorage, "getValue").mockReturnValue(undefined); const setValueMock = jest.spyOn(ZoweLocalStorage, "setValue").mockImplementation(); @@ -361,6 +365,42 @@ describe("ProfilesUtils unit tests", () => { getConfigurationMock.mockRestore(); getValueMock.mockRestore(); setValueMock.mockRestore(); + onlyV1ProfsExistMock[Symbol.dispose](); + + profInfoSpy.mockRestore(); + }); + + it("should not reload the window during migration if imperative.ProfileInfo.onlyV1ProfilesExist is false", async () => { + const profInfoSpy = jest.spyOn(ProfilesUtils, "getProfileInfo").mockReturnValueOnce({ + readProfilesFromDisk: jest.fn(), + hasValidSchema: false, + getTeamConfig: () => ({ + exists: false, + }), + } as never); + const getConfigurationMock = jest.spyOn(vscode.workspace, "getConfiguration").mockReturnValue({ + persistent: true, + get: jest.fn(), + has: jest.fn(), + inspect: jest.fn(), + update: jest.fn(), + }); + const onlyV1ProfsExistMock = new MockedProperty(imperative.ProfileInfo, "onlyV1ProfilesExist", { + configurable: true, + get: () => false, + }); + const executeCommandMock = jest.spyOn(vscode.commands, "executeCommand").mockImplementation(); + const getValueMock = jest.spyOn(ZoweLocalStorage, "getValue").mockReturnValue(undefined); + const setValueMock = jest.spyOn(ZoweLocalStorage, "setValue").mockImplementation(); + await ProfilesUtils.readConfigFromDisk(true); + expect(getConfigurationMock).toHaveBeenCalledWith("Zowe-USS-Persistent"); + expect(getValueMock).toHaveBeenCalledWith(Definitions.LocalStorageKey.V1_MIGRATION_STATUS); + expect(executeCommandMock).not.toHaveBeenCalledWith("workbench.action.reloadWindow"); + executeCommandMock.mockRestore(); + getConfigurationMock.mockRestore(); + getValueMock.mockRestore(); + setValueMock.mockRestore(); + onlyV1ProfsExistMock[Symbol.dispose](); profInfoSpy.mockRestore(); }); @@ -1170,33 +1210,108 @@ describe("ProfilesUtils unit tests", () => { }; } - it("should return early if the migration status is nullish", () => { + it("should return early if the migration status is nullish", async () => { const blockMocks = getBlockMocks(); blockMocks.getValueMock.mockReturnValueOnce(undefined); - ProfilesUtils.handleV1MigrationStatus(); + await ProfilesUtils.handleV1MigrationStatus(); expect(blockMocks.setValueMock).not.toHaveBeenCalled(); blockMocks.getValueMock.mockRestore(); }); - it("should call executeCommand with zowe.ds.addSession if the migration status is CreateConfigSelected", () => { + it("should call executeCommand with zowe.ds.addSession if the migration status is CreateConfigSelected", async () => { const blockMocks = getBlockMocks(); const executeCommandMock = jest.spyOn(vscode.commands, "executeCommand").mockImplementation(); blockMocks.getValueMock.mockReturnValueOnce(Definitions.V1MigrationStatus.CreateConfigSelected); blockMocks.setValueMock.mockImplementation(); - ProfilesUtils.handleV1MigrationStatus(); + await ProfilesUtils.handleV1MigrationStatus(); expect(executeCommandMock.mock.lastCall?.[0]).toBe("zowe.ds.addSession"); blockMocks.getValueMock.mockRestore(); blockMocks.setValueMock.mockRestore(); }); - it("should clear the v1 migration status once the migration status is handled", () => { + it("should clear the v1 migration status once the migration status is handled", async () => { const blockMocks = getBlockMocks(); blockMocks.getValueMock.mockReturnValueOnce(Definitions.V1MigrationStatus.JustMigrated); - blockMocks.setValueMock.mockImplementation(); - ProfilesUtils.handleV1MigrationStatus(); - expect(blockMocks.setValueMock).toHaveBeenCalledWith(Definitions.LocalStorageKey.V1_MIGRATION_STATUS, undefined); + await ProfilesUtils.handleV1MigrationStatus(); blockMocks.getValueMock.mockRestore(); - blockMocks.setValueMock.mockRestore(); + }); + }); + + describe("promptUserWithNoConfigs", () => { + it("prompts the user if they don't have any Zowe client configs", async () => { + const profInfoMock = jest.spyOn(ProfilesUtils, "getProfileInfo").mockResolvedValue({ + getTeamConfig: () => ({ exists: false }), + } as any); + const onlyV1ProfsExistMock = new MockedProperty(imperative.ProfileInfo, "onlyV1ProfilesExist", { + configurable: true, + get: () => false, + }); + const showMessageSpy = jest.spyOn(Gui, "showMessage"); + await ProfilesUtils.promptUserWithNoConfigs(); + expect(showMessageSpy).toHaveBeenCalledWith( + "No Zowe client configurations were detected. Click 'Create New' to create a new Zowe team configuration.", + { items: ["Create New"] } + ); + expect(profInfoMock).toHaveBeenCalled(); + profInfoMock.mockRestore(); + onlyV1ProfsExistMock[Symbol.dispose](); + }); + it("executes zowe.ds.addSession if the user selects 'Create New' in the prompt", async () => { + const profInfoMock = jest.spyOn(ProfilesUtils, "getProfileInfo").mockResolvedValue({ + getTeamConfig: () => ({ exists: false }), + } as any); + const onlyV1ProfsExistMock = new MockedProperty(imperative.ProfileInfo, "onlyV1ProfilesExist", { + configurable: true, + get: () => false, + }); + const showMessageSpy = jest.spyOn(Gui, "showMessage").mockResolvedValue("Create New"); + const executeCommandMock = jest.spyOn(vscode.commands, "executeCommand").mockImplementation(); + await ProfilesUtils.promptUserWithNoConfigs(); + expect(showMessageSpy).toHaveBeenCalledWith( + "No Zowe client configurations were detected. Click 'Create New' to create a new Zowe team configuration.", + { items: ["Create New"] } + ); + expect(profInfoMock).toHaveBeenCalled(); + expect(executeCommandMock).toHaveBeenCalledWith("zowe.ds.addSession"); + executeCommandMock.mockRestore(); + profInfoMock.mockRestore(); + onlyV1ProfsExistMock[Symbol.dispose](); + }); + it("does not prompt the user if they have a Zowe team config", async () => { + const profInfoMock = jest.spyOn(ProfilesUtils, "getProfileInfo").mockResolvedValue({ + getTeamConfig: () => ({ exists: true }), + } as any); + const onlyV1ProfsExistMock = new MockedProperty(imperative.ProfileInfo, "onlyV1ProfilesExist", { + configurable: true, + get: () => false, + }); + const showMessageSpy = jest.spyOn(Gui, "showMessage"); + await ProfilesUtils.promptUserWithNoConfigs(); + expect(showMessageSpy).not.toHaveBeenCalledWith( + "No Zowe client configurations were detected. Click 'Create New' to create a new Zowe team configuration.", + { items: ["Create New"] } + ); + expect(profInfoMock).toHaveBeenCalled(); + profInfoMock.mockRestore(); + onlyV1ProfsExistMock[Symbol.dispose](); + }); + it("does not prompt the user if they have v1 profiles", async () => { + const profInfoMock = jest.spyOn(ProfilesUtils, "getProfileInfo").mockResolvedValue({ + getTeamConfig: () => ({ exists: false }), + } as any); + const onlyV1ProfsExistMock = new MockedProperty(imperative.ProfileInfo, "onlyV1ProfilesExist", { + configurable: true, + get: () => true, + }); + const showMessageSpy = jest.spyOn(Gui, "showMessage"); + await ProfilesUtils.promptUserWithNoConfigs(); + expect(showMessageSpy).not.toHaveBeenCalledWith( + "No Zowe client configurations were detected. Click 'Create New' to create a new Zowe team configuration.", + { items: ["Create New"] } + ); + expect(profInfoMock).toHaveBeenCalled(); + profInfoMock.mockRestore(); + onlyV1ProfsExistMock[Symbol.dispose](); }); }); }); diff --git a/packages/zowe-explorer/l10n/bundle.l10n.json b/packages/zowe-explorer/l10n/bundle.l10n.json index 38aa626ec4..b1db509755 100644 --- a/packages/zowe-explorer/l10n/bundle.l10n.json +++ b/packages/zowe-explorer/l10n/bundle.l10n.json @@ -41,6 +41,8 @@ "Install": "Install", "Reload": "Reload", "No valid schema was found for the active team configuration. This may introduce issues with profiles in Zowe Explorer.": "No valid schema was found for the active team configuration. This may introduce issues with profiles in Zowe Explorer.", + "No Zowe client configurations were detected. Click 'Create New' to create a new Zowe team configuration.": "No Zowe client configurations were detected. Click 'Create New' to create a new Zowe team configuration.", + "Create New": "Create New", "\"Update Credentials\" operation not supported when \"autoStore\" is false": "\"Update Credentials\" operation not supported when \"autoStore\" is false", "Connection Name": "Connection Name", "Enter a name for the connection.": "Enter a name for the connection.", @@ -85,7 +87,6 @@ }, "Zowe Profiles initialized successfully.": "Zowe Profiles initialized successfully.", "Convert Existing Profiles": "Convert Existing Profiles", - "Create New": "Create New", "Operation cancelled": "Operation cancelled", "Tree Item is not a Zowe Explorer item.": "Tree Item is not a Zowe Explorer item.", "Zowe Explorer": "Zowe Explorer", @@ -170,32 +171,6 @@ "Required API functions for pasting (fileList and copy/uploadFromBuffer) were not found.": "Required API functions for pasting (fileList and copy/uploadFromBuffer) were not found.", "Uploading USS files...": "Uploading USS files...", "Error uploading files": "Error uploading files", - "The 'move' function is not implemented for this USS API.": "The 'move' function is not implemented for this USS API.", - "Could not list USS files: Empty path provided in URI": "Could not list USS files: Empty path provided in URI", - "Profile does not exist for this file.": "Profile does not exist for this file.", - "$(sync~spin) Saving USS file...": "$(sync~spin) Saving USS file...", - "Renaming {0} failed due to API error: {1}/File pathError message": { - "message": "Renaming {0} failed due to API error: {1}", - "comment": [ - "File path", - "Error message" - ] - }, - "Deleting {0} failed due to API error: {1}/File nameError message": { - "message": "Deleting {0} failed due to API error: {1}", - "comment": [ - "File name", - "Error message" - ] - }, - "No error details given": "No error details given", - "Error fetching destination {0} for paste action: {1}/USS pathError message": { - "message": "Error fetching destination {0} for paste action: {1}", - "comment": [ - "USS path", - "Error message" - ] - }, "Downloaded: {0}/Download time": { "message": "Downloaded: {0}", "comment": [ @@ -266,6 +241,32 @@ "initializeUSSFavorites.error.buttonRemove": "initializeUSSFavorites.error.buttonRemove", "File does not exist. It may have been deleted.": "File does not exist. It may have been deleted.", "$(sync~spin) Pulling from Mainframe...": "$(sync~spin) Pulling from Mainframe...", + "The 'move' function is not implemented for this USS API.": "The 'move' function is not implemented for this USS API.", + "Could not list USS files: Empty path provided in URI": "Could not list USS files: Empty path provided in URI", + "Profile does not exist for this file.": "Profile does not exist for this file.", + "$(sync~spin) Saving USS file...": "$(sync~spin) Saving USS file...", + "Renaming {0} failed due to API error: {1}/File pathError message": { + "message": "Renaming {0} failed due to API error: {1}", + "comment": [ + "File path", + "Error message" + ] + }, + "Deleting {0} failed due to API error: {1}/File nameError message": { + "message": "Deleting {0} failed due to API error: {1}", + "comment": [ + "File name", + "Error message" + ] + }, + "No error details given": "No error details given", + "Error fetching destination {0} for paste action: {1}/USS pathError message": { + "message": "Error fetching destination {0} for paste action: {1}", + "comment": [ + "USS path", + "Error message" + ] + }, "{0} location/Node type": { "message": "{0} location", "comment": [ diff --git a/packages/zowe-explorer/l10n/poeditor.json b/packages/zowe-explorer/l10n/poeditor.json index 162ecc36b7..62b615c080 100644 --- a/packages/zowe-explorer/l10n/poeditor.json +++ b/packages/zowe-explorer/l10n/poeditor.json @@ -437,6 +437,8 @@ "Install": "", "Reload": "", "No valid schema was found for the active team configuration. This may introduce issues with profiles in Zowe Explorer.": "", + "No Zowe client configurations were detected. Click 'Create New' to create a new Zowe team configuration.": "", + "Create New": "", "\"Update Credentials\" operation not supported when \"autoStore\" is false": "", "Connection Name": "", "Enter a name for the connection.": "", @@ -450,7 +452,6 @@ "Failed to initialize Zowe folder: {0}": "", "Zowe Profiles initialized successfully.": "", "Convert Existing Profiles": "", - "Create New": "", "Operation cancelled": "", "Tree Item is not a Zowe Explorer item.": "", "Zowe Explorer": "", @@ -490,14 +491,6 @@ "Required API functions for pasting (fileList and copy/uploadFromBuffer) were not found.": "", "Uploading USS files...": "", "Error uploading files": "", - "The 'move' function is not implemented for this USS API.": "", - "Could not list USS files: Empty path provided in URI": "", - "Profile does not exist for this file.": "", - "$(sync~spin) Saving USS file...": "", - "Renaming {0} failed due to API error: {1}": "", - "Deleting {0} failed due to API error: {1}": "", - "No error details given": "", - "Error fetching destination {0} for paste action: {1}": "", "Downloaded: {0}": "", "Encoding: {0}": "", "Binary": "", @@ -526,6 +519,14 @@ "initializeUSSFavorites.error.buttonRemove": "", "File does not exist. It may have been deleted.": "", "$(sync~spin) Pulling from Mainframe...": "", + "The 'move' function is not implemented for this USS API.": "", + "Could not list USS files: Empty path provided in URI": "", + "Profile does not exist for this file.": "", + "$(sync~spin) Saving USS file...": "", + "Renaming {0} failed due to API error: {1}": "", + "Deleting {0} failed due to API error: {1}": "", + "No error details given": "", + "Error fetching destination {0} for paste action: {1}": "", "{0} location": "", "Choose a location to create the {0}": "", "Name of file or directory": "", diff --git a/packages/zowe-explorer/src/extension.ts b/packages/zowe-explorer/src/extension.ts index 0eceaab8d0..8cc3e76434 100644 --- a/packages/zowe-explorer/src/extension.ts +++ b/packages/zowe-explorer/src/extension.ts @@ -55,8 +55,8 @@ export async function activate(context: vscode.ExtensionContext): Promise(key, defaultValue); } - public static setValue(key: Definitions.LocalStorageKey | PersistenceSchemaEnum, value: T): void { + public static setValue(key: Definitions.LocalStorageKey | PersistenceSchemaEnum, value: T): Thenable { ZoweLogger.trace("ZoweLocalStorage.setValue called."); - ZoweLocalStorage.storage.update(key, value); + return ZoweLocalStorage.storage.update(key, value); } } diff --git a/packages/zowe-explorer/src/utils/ProfilesUtils.ts b/packages/zowe-explorer/src/utils/ProfilesUtils.ts index 8b49b0ed38..857cedd08a 100644 --- a/packages/zowe-explorer/src/utils/ProfilesUtils.ts +++ b/packages/zowe-explorer/src/utils/ProfilesUtils.ts @@ -349,8 +349,8 @@ export class ProfilesUtils { // VS Code registers our updated TreeView IDs. Otherwise, VS Code's "Refresh Extensions" option will break v3 init. const ussPersistentSettings = vscode.workspace.getConfiguration("Zowe-USS-Persistent"); const upgradingFromV1 = ZoweLocalStorage.getValue(Definitions.LocalStorageKey.V1_MIGRATION_STATUS); - if (ussPersistentSettings != null && upgradingFromV1 == null) { - ZoweLocalStorage.setValue(Definitions.LocalStorageKey.V1_MIGRATION_STATUS, Definitions.V1MigrationStatus.JustMigrated); + if (ussPersistentSettings != null && upgradingFromV1 == null && imperative.ProfileInfo.onlyV1ProfilesExist) { + await ZoweLocalStorage.setValue(Definitions.LocalStorageKey.V1_MIGRATION_STATUS, Definitions.V1MigrationStatus.JustMigrated); await vscode.commands.executeCommand("workbench.action.reloadWindow"); } if (imperative.ProfileInfo.onlyV1ProfilesExist) { @@ -359,7 +359,7 @@ export class ProfilesUtils { } } - public static handleV1MigrationStatus(): void { + public static async handleV1MigrationStatus(): Promise { const migrationStatus = ZoweLocalStorage.getValue(Definitions.LocalStorageKey.V1_MIGRATION_STATUS); if (migrationStatus == null) { // If there is no v1 migration status, return. @@ -369,9 +369,29 @@ export class ProfilesUtils { // Open the "Add Session" quick pick if the user selected "Create New" in the v1 migration prompt. if (migrationStatus === Definitions.V1MigrationStatus.CreateConfigSelected) { vscode.commands.executeCommand("zowe.ds.addSession", SharedTreeProviders.ds); + await ZoweLocalStorage.setValue(Definitions.LocalStorageKey.V1_MIGRATION_STATUS, Definitions.V1MigrationStatus.JustMigrated); } + } - ZoweLocalStorage.setValue(Definitions.LocalStorageKey.V1_MIGRATION_STATUS, undefined); + /** + * Displays a notification if a user does not have any Zowe client configurations. + * + * This aims to help direct new Zowe Explorer users to create a new team configuration. + */ + public static async promptUserWithNoConfigs(): Promise { + const profInfo = await ProfilesUtils.getProfileInfo(); + if (!profInfo.getTeamConfig().exists && !imperative.ProfileInfo.onlyV1ProfilesExist) { + Gui.showMessage( + vscode.l10n.t("No Zowe client configurations were detected. Click 'Create New' to create a new Zowe team configuration."), + { + items: [vscode.l10n.t("Create New")], + } + ).then(async (selection) => { + if (selection === vscode.l10n.t("Create New")) { + await vscode.commands.executeCommand("zowe.ds.addSession"); + } + }); + } } public static async promptCredentials(node: IZoweTreeNode): Promise { @@ -537,7 +557,7 @@ export class ProfilesUtils { switch (selection) { case createButton: { ZoweLogger.info("Create new team configuration chosen."); - ZoweLocalStorage.setValue(Definitions.LocalStorageKey.V1_MIGRATION_STATUS, Definitions.V1MigrationStatus.CreateConfigSelected); + await ZoweLocalStorage.setValue(Definitions.LocalStorageKey.V1_MIGRATION_STATUS, Definitions.V1MigrationStatus.CreateConfigSelected); break; } case convertButton: {