From 816d27040cbeaaeb9c7d9437c7a3dbe62d87cf82 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 5 Nov 2024 10:20:15 -0500 Subject: [PATCH 1/5] fix(v3): Pass `responseTimeout` in z/OSMF MVS and USS API calls (#3292) * fix: pass responseTimeout to API functions Signed-off-by: Trae Yelovich * refactor: remove fallback for spreading newOptions Signed-off-by: Trae Yelovich * refactor: use optional chaining; work on resolving tests Signed-off-by: Trae Yelovich * refactor: pass profile props to tests, fix types Signed-off-by: Trae Yelovich * tests: resolve failing cases in ZE Signed-off-by: Trae Yelovich * chore: update changelog entry for ZE API Signed-off-by: Trae Yelovich * refactor: remove fallback for spreading undefined options Signed-off-by: Trae Yelovich * fix: add missing functions to MvsApi test list Signed-off-by: Trae Yelovich --------- Signed-off-by: Trae Yelovich --- packages/zowe-explorer-api/CHANGELOG.md | 2 + .../ZoweExplorerZosmfApi.unit.test.ts | 138 ++++++++++-------- .../src/profiles/ZoweExplorerZosmfApi.ts | 107 ++++++++++---- .../ZoweExplorerZosmfApi.unit.test.ts | 8 +- .../ZoweExplorerZosmfApi.unit.test.ts.snap | 4 + 5 files changed, 175 insertions(+), 84 deletions(-) diff --git a/packages/zowe-explorer-api/CHANGELOG.md b/packages/zowe-explorer-api/CHANGELOG.md index 9fb5d5010f..1f9354756e 100644 --- a/packages/zowe-explorer-api/CHANGELOG.md +++ b/packages/zowe-explorer-api/CHANGELOG.md @@ -10,6 +10,8 @@ All notable changes to the "zowe-explorer-api" extension will be documented in t ### Bug fixes +- Fixed an issue where the `responseTimeout` profile property was ignored for z/OSMF MVS and USS API calls. [#3225](https://github.com/zowe/zowe-explorer-vscode/issues/3225) + ## `3.0.2` ### New features and enhancements diff --git a/packages/zowe-explorer-api/__tests__/__unit__/profiles/ZoweExplorerZosmfApi.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/profiles/ZoweExplorerZosmfApi.unit.test.ts index ca00c29803..6882b75541 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/profiles/ZoweExplorerZosmfApi.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/profiles/ZoweExplorerZosmfApi.unit.test.ts @@ -21,32 +21,37 @@ import { ZoweExplorerZosmf } from "../../../src/profiles/ZoweExplorerZosmfApi"; import { FileManagement } from "../../../src/utils/FileManagement"; import { MainframeInteraction } from "../../../src/extend"; +type ParametersWithProfileArgs = F extends (...args: infer P) => any ? [...Parameters, profileProperties?: object] : never; + type ITestApi = { [K in keyof T]: { name: K; spy: jest.SpyInstance; - args: jest.ArgsType; - transform?: (args: jest.ArgsType) => any[]; + args: ParametersWithProfileArgs; + transform?: (args: ParametersWithProfileArgs) => any[]; }; }[keyof T]; -type ITestProfile = { - host: string; - port: number; - basePath: string; - rejectUnauthorized: boolean; - user?: string; - password?: string; +const fakeProperties = { + responseTimeout: 60, }; -const fakeProfile: ITestProfile = { +const fakeProfile: imperative.IProfile = { host: "example.com", port: 443, basePath: "/api/v1", rejectUnauthorized: true, user: "admin", password: "123456", + ...fakeProperties, +}; +const loadedProfile: imperative.IProfileLoaded = { + profile: fakeProfile, + message: "", + type: "zosmf", + failNotFound: false, }; + const fakeSession = imperative.Session.createFromUrl(new URL("https://example.com")); const mISshSession: zosuss.ISshSession = { @@ -74,7 +79,7 @@ async function expectUnixCommandApiWithSshSession( async function expectApiWithSession({ name, spy, args, transform }: ITestApi, apiInstance: MainframeInteraction.ICommon): Promise { spy.mockClear().mockResolvedValue(undefined); const getSessionSpy = jest.spyOn(apiInstance, "getSession").mockReturnValue(fakeSession); - await apiInstance[name as string](...args); + await apiInstance[name as string](...Object.values(args)); expect(getSessionSpy).toHaveBeenCalledTimes(1); const params: unknown[] = transform ? transform(args) : args; expect(spy).toHaveBeenCalledWith(fakeSession, ...params); @@ -96,20 +101,22 @@ describe("ZosmfUssApi", () => { password: "password", protocol: "http", user: "aZosmfUser", + ...fakeProperties, }, } as imperative.IProfileLoaded; it("should include profile properties in the built session object", () => { - const api = new ZoweExplorerZosmf.UssApi(); + const api = new ZoweExplorerZosmf.UssApi(loadedProfile); - const transformedProps = { ...exampleProfile.profile, hostname: exampleProfile.profile?.host }; + const transformedProps: Record = { ...exampleProfile.profile, hostname: exampleProfile.profile?.host, ...fakeProperties }; delete transformedProps["host"]; + delete transformedProps["responseTimeout"]; expect((api as any)._getSession(exampleProfile).mISession).toMatchObject(transformedProps); }); }); describe("updateAttributes", () => { - const ussApi = new ZoweExplorerZosmf.UssApi(); + const ussApi = new ZoweExplorerZosmf.UssApi(loadedProfile); const getSessionMock = jest.spyOn(ussApi, "getSession").mockReturnValue(fakeSession); const putUSSPayload = jest.spyOn(zosfiles.Utilities, "putUSSPayload").mockResolvedValue(Buffer.from("test")); @@ -203,20 +210,20 @@ describe("ZosmfUssApi", () => { it("uploads a file from buffer", async () => { const uploadFileSpy = jest.spyOn(zosfiles.Upload, "bufferToUssFile").mockImplementation(); - const zosmfApi = new ZoweExplorerZosmf.UssApi(); + const zosmfApi = new ZoweExplorerZosmf.UssApi(loadedProfile); const buf = Buffer.from("123abc"); await zosmfApi.uploadFromBuffer(buf, "/some/uss/path"); - expect(uploadFileSpy).toHaveBeenCalledWith(zosmfApi.getSession(), "/some/uss/path", buf, undefined); + expect(uploadFileSpy).toHaveBeenCalledWith(zosmfApi.getSession(), "/some/uss/path", buf, fakeProperties); }); it("constants should be unchanged", () => { - const zosmfApi = new ZoweExplorerZosmf.UssApi(); + const zosmfApi = new ZoweExplorerZosmf.UssApi(loadedProfile); expect(zosmfApi.getProfileTypeName()).toMatchSnapshot(); expect(zosmfApi.getTokenTypeName()).toMatchSnapshot(); }); it("getSessionFromCommandArgument should build session from arguments", () => { - const zosmfApi = new ZoweExplorerZosmf.UssApi(); + const zosmfApi = new ZoweExplorerZosmf.UssApi(loadedProfile); const session = zosmfApi.getSessionFromCommandArgument(fakeProfile as unknown as imperative.ICommandArguments); expect(session).toBeDefined(); const sessCfg: imperative.ISession = { @@ -225,6 +232,7 @@ describe("ZosmfUssApi", () => { type: imperative.SessConstants.AUTH_TYPE_BASIC, }; delete sessCfg["host"]; + delete sessCfg["responseTimeout"]; expect(session.ISession).toMatchObject(sessCfg); }); @@ -234,17 +242,18 @@ describe("ZosmfUssApi", () => { } as unknown as imperative.IProfileLoaded); const session = zosmfApi.getSession(); expect(session).toBeDefined(); - const sessCfg: Partial & { hostname: string; type: string } = { + const sessCfg: Partial & { hostname: string; type: string } = { ...fakeProfile, hostname: fakeProfile.host, type: imperative.SessConstants.AUTH_TYPE_BASIC, }; delete sessCfg.host; + delete sessCfg["responseTimeout"]; expect(session.ISession).toMatchObject(sessCfg); }); it("getSession should build session from profile with token", () => { - const fakeProfileWithToken = { + const fakeProfileWithToken: imperative.IProfile = { ...fakeProfile, tokenType: imperative.SessConstants.TOKEN_TYPE_JWT, tokenValue: "fakeToken", @@ -256,12 +265,13 @@ describe("ZosmfUssApi", () => { } as unknown as imperative.IProfileLoaded); const session = zosmfApi.getSession(); expect(session).toBeDefined(); - const sessCfg: Partial & { hostname: string; type: string } = { + const sessCfg: Partial & { hostname: string; type: string } = { ...fakeProfileWithToken, hostname: fakeProfileWithToken.host, type: imperative.SessConstants.AUTH_TYPE_TOKEN, }; delete sessCfg.host; + delete sessCfg["responseTimeout"]; expect(session.ISession).toMatchObject(sessCfg); }); @@ -274,7 +284,7 @@ describe("ZosmfUssApi", () => { }); it("getStatus should validate active profile", async () => { - const zosmfApi = new ZoweExplorerZosmf.UssApi(); + const zosmfApi = new ZoweExplorerZosmf.UssApi(loadedProfile); const checkStatusSpy = jest.spyOn(zosmf.CheckStatus, "getZosmfInfo").mockResolvedValue({}); const status = await zosmfApi.getStatus({ profile: fakeProfile } as unknown as imperative.IProfileLoaded, "zosmf"); expect(status).toBe("active"); @@ -282,7 +292,7 @@ describe("ZosmfUssApi", () => { }); it("getStatus should validate inactive profile", async () => { - const zosmfApi = new ZoweExplorerZosmf.UssApi(); + const zosmfApi = new ZoweExplorerZosmf.UssApi(loadedProfile); const checkStatusSpy = jest.spyOn(zosmf.CheckStatus, "getZosmfInfo").mockResolvedValue(undefined as unknown as zosmf.IZosmfInfoResponse); const status = await zosmfApi.getStatus({ profile: fakeProfile } as unknown as imperative.IProfileLoaded, "zosmf"); expect(status).toBe("inactive"); @@ -290,7 +300,7 @@ describe("ZosmfUssApi", () => { }); it("should test that copy calls zowe.Utilities.putUSSPayload", async () => { - const api = new ZoweExplorerZosmf.UssApi(); + const api = new ZoweExplorerZosmf.UssApi(loadedProfile); api.getSession = jest.fn(); const response = Buffer.from("hello world!"); @@ -303,13 +313,13 @@ describe("ZosmfUssApi", () => { }); it("getStatus should validate unverified profile", async () => { - const zosmfApi = new ZoweExplorerZosmf.UssApi(); + const zosmfApi = new ZoweExplorerZosmf.UssApi(loadedProfile); const status = await zosmfApi.getStatus({ profile: fakeProfile } as unknown as imperative.IProfileLoaded, "sample"); expect(status).toBe("unverified"); }); it("login and logout should call APIML endpoints", async () => { - const zosmfApi = new ZoweExplorerZosmf.UssApi(); + const zosmfApi = new ZoweExplorerZosmf.UssApi(loadedProfile); const loginSpy = jest.spyOn(Login, "apimlLogin").mockResolvedValue(""); const logoutSpy = jest.spyOn(Logout, "apimlLogout").mockResolvedValue(); @@ -321,7 +331,7 @@ describe("ZosmfUssApi", () => { }); it("should retrieve the tag of a file", async () => { - const zosmfApi = new ZoweExplorerZosmf.UssApi(); + const zosmfApi = new ZoweExplorerZosmf.UssApi(loadedProfile); jest.spyOn(JSON, "parse").mockReturnValue({ stdout: ["-t UTF-8 tesfile.txt"], }); @@ -334,7 +344,7 @@ describe("ZosmfUssApi", () => { }); it("should update the tag attribute when passed in", async () => { - const zosmfApi = new ZoweExplorerZosmf.UssApi(); + const zosmfApi = new ZoweExplorerZosmf.UssApi(loadedProfile); const changeTagSpy = jest.fn(); Object.defineProperty(zosfiles.Utilities, "putUSSPayload", { value: changeTagSpy, @@ -345,7 +355,7 @@ describe("ZosmfUssApi", () => { }); it("calls putUSSPayload to move a directory from old path to new path", async () => { - const api = new ZoweExplorerZosmf.UssApi(); + const api = new ZoweExplorerZosmf.UssApi(loadedProfile); const putUssPayloadSpy = jest.fn(); Object.defineProperty(zosfiles.Utilities, "putUSSPayload", { value: putUssPayloadSpy, @@ -364,7 +374,7 @@ describe("ZosmfUssApi", () => { { name: "fileList", spy: jest.spyOn(zosfiles.List, "fileList"), - args: ["ussPath"], + args: ["ussPath", fakeProperties], }, { name: "isFileTagBinOrAscii", @@ -374,33 +384,33 @@ describe("ZosmfUssApi", () => { { name: "getContents", spy: jest.spyOn(zosfiles.Download, "ussFile"), - args: ["ussPath", {}], + args: ["ussPath", fakeProperties], }, { name: "putContent", spy: jest.spyOn(zosfiles.Upload, "fileToUssFile"), - args: ["localPath", "ussPath", {}], + args: ["localPath", "ussPath", fakeProperties], }, { name: "uploadDirectory", spy: jest.spyOn(zosfiles.Upload, "dirToUSSDirRecursive"), - args: ["localPath", "ussPath", {}], + args: ["localPath", "ussPath", fakeProperties], }, { name: "create", spy: jest.spyOn(zosfiles.Create, "uss"), - args: ["ussPath", "file", "777"], + args: ["ussPath", "file", "777", fakeProperties], }, { name: "delete", spy: jest.spyOn(zosfiles.Delete, "ussFile"), - args: ["/ussPath", false], - transform: (args) => [args[0].slice(1), args[1]], + args: ["/ussPath", false, fakeProperties], + transform: (args) => [args[0].slice(1), args[1], fakeProperties], }, { name: "delete", spy: jest.spyOn(zosfiles.Delete, "ussFile"), - args: ["ussPath", false], + args: ["ussPath", false, fakeProperties], }, { name: "rename", @@ -410,7 +420,7 @@ describe("ZosmfUssApi", () => { ]; ussApis.forEach((ussApi) => { it(`${ussApi?.name} should inject session into Zowe API`, async () => { - await expectApiWithSession(ussApi, new ZoweExplorerZosmf.UssApi()); + await expectApiWithSession(ussApi, new ZoweExplorerZosmf.UssApi(loadedProfile)); }); }); }); @@ -420,38 +430,38 @@ describe("ZosmfMvsApi", () => { { name: "dataSet", spy: jest.spyOn(zosfiles.List, "dataSet"), - args: ["dsname", {}], + args: ["dsname", fakeProperties], }, { name: "allMembers", spy: jest.spyOn(zosfiles.List, "allMembers"), - args: ["dsname", {}], + args: ["dsname", fakeProperties], }, { name: "getContents", spy: jest.spyOn(zosfiles.Download, "dataSet"), - args: ["dsname", {}], + args: ["dsname", fakeProperties], }, { name: "putContents", spy: jest.spyOn(zosfiles.Upload, "pathToDataSet"), - args: ["localPath", "dsname", {}], + args: ["localPath", "dsname", fakeProperties], }, { name: "createDataSet", spy: jest.spyOn(zosfiles.Create, "dataSet"), - args: [0, "dsname", {}], + args: [0, "dsname", fakeProperties], }, { name: "createDataSetMember", spy: jest.spyOn(zosfiles.Upload, "bufferToDataSet"), - args: ["dsname", {}], + args: ["dsname", fakeProperties], transform: (args) => [Buffer.from(""), ...args], }, { name: "allocateLikeDataSet", spy: jest.spyOn(zosfiles.Create, "dataSetLike"), - args: ["dsname1", "dsname2"], + args: ["dsname1", "dsname2", fakeProperties], }, { name: "copyDataSetMember", @@ -459,15 +469,18 @@ describe("ZosmfMvsApi", () => { args: [ { dsn: "dsname1", member: "member1" }, { dsn: "dsname2", member: "member2" }, - { "from-dataset": { dsn: "dsname1", member: "member1" } }, + { "from-dataset": { dsn: "dsname1", member: "member1" }, ...fakeProperties }, ], transform: (args) => [args[1], args[2]], }, { name: "copyDataSetMember", spy: jest.spyOn(zosfiles.Copy, "dataSet"), - args: [{ dsn: "dsname1", member: "member1" }, { dsn: "dsname2", member: "member2" }, {} as any], - transform: (args) => [args[1], { "from-dataset": args[0] }], + args: [ + { dsn: "dsname1", member: "member1" }, + { dsn: "dsname2", member: "member2" }, + ], + transform: (args) => [args[1], { "from-dataset": args[0], ...fakeProperties }], }, { name: "copyDataSetMember", @@ -476,46 +489,57 @@ describe("ZosmfMvsApi", () => { { dsn: "dsname1", member: "member1" }, { dsn: "dsname2", member: "member2" }, ], - transform: (args) => [args[1], { "from-dataset": args[0] }], + transform: (args) => [args[1], { "from-dataset": args[0], ...fakeProperties }], }, { name: "renameDataSet", spy: jest.spyOn(zosfiles.Rename, "dataSet"), - args: ["dsname1", "dsname2"], + args: ["dsname1", "dsname2", fakeProperties], }, { name: "renameDataSetMember", spy: jest.spyOn(zosfiles.Rename, "dataSetMember"), - args: ["dsname", "member1", "member2"], + args: ["dsname", "member1", "member2", fakeProperties], }, { name: "hMigrateDataSet", spy: jest.spyOn(zosfiles.HMigrate, "dataSet"), - args: ["dsname"], + args: ["dsname", fakeProperties], }, { name: "hRecallDataSet", spy: jest.spyOn(zosfiles.HRecall, "dataSet"), - args: ["dsname"], + args: ["dsname", fakeProperties], }, { name: "deleteDataSet", spy: jest.spyOn(zosfiles.Delete, "dataSet"), - args: ["dsname", {}], + args: ["dsname", fakeProperties], + }, + { + name: "dataSetsMatchingPattern", + spy: jest.spyOn(zosfiles.List, "dataSetsMatchingPattern"), + args: [["SAMPLE.A*", "SAMPLE.B*"], fakeProperties], + }, + { + name: "copyDataSet", + spy: jest.spyOn(zosfiles.Copy, "dataSet"), + args: ["FROM.NAME", "TO.NAME", undefined, undefined, fakeProperties], + transform: (args) => [{ dsn: args[1] }, { enq: undefined, "from-dataset": { dsn: args[0] }, replace: undefined, ...fakeProperties }], }, ]; mvsApis.forEach((mvsApi) => { it(`${mvsApi?.name} should inject session into Zowe API`, async () => { - await expectApiWithSession(mvsApi, new ZoweExplorerZosmf.MvsApi()); + await expectApiWithSession(mvsApi, new ZoweExplorerZosmf.MvsApi(loadedProfile)); }); }); it("uploads a data set from buffer", async () => { const uploadFileSpy = jest.spyOn(zosfiles.Upload, "bufferToDataSet").mockImplementation(); - const zosmfApi = new ZoweExplorerZosmf.MvsApi(); + const zosmfApi = new ZoweExplorerZosmf.MvsApi(loadedProfile); const buf = Buffer.from("123abc"); await zosmfApi.uploadFromBuffer(buf, "SOME.DS(MEMB)"); - expect(uploadFileSpy).toHaveBeenCalledWith(zosmfApi.getSession(), buf, "SOME.DS(MEMB)", undefined); + expect(uploadFileSpy).toHaveBeenCalledWith(zosmfApi.getSession(), buf, "SOME.DS(MEMB)", fakeProperties); }); }); diff --git a/packages/zowe-explorer-api/src/profiles/ZoweExplorerZosmfApi.ts b/packages/zowe-explorer-api/src/profiles/ZoweExplorerZosmfApi.ts index c2fad23677..340da8f7b4 100644 --- a/packages/zowe-explorer-api/src/profiles/ZoweExplorerZosmfApi.ts +++ b/packages/zowe-explorer-api/src/profiles/ZoweExplorerZosmfApi.ts @@ -119,7 +119,7 @@ export namespace ZoweExplorerZosmf { */ export class UssApi extends CommonApi implements MainframeInteraction.IUss { public fileList(ussFilePath: string): Promise { - return zosfiles.List.fileList(this.getSession(), ussFilePath); + return zosfiles.List.fileList(this.getSession(), ussFilePath, { responseTimeout: this.profile?.profile?.responseTimeout }); } public isFileTagBinOrAscii(ussFilePath: string): Promise { @@ -127,11 +127,14 @@ export namespace ZoweExplorerZosmf { } public getContents(inputFilePath: string, options: zosfiles.IDownloadSingleOptions): Promise { - return zosfiles.Download.ussFile(this.getSession(), inputFilePath, options); + return zosfiles.Download.ussFile(this.getSession(), inputFilePath, { + responseTimeout: this.profile?.profile?.responseTimeout, + ...options, + }); } public copy(outputPath: string, options?: Omit): Promise { - return zosfiles.Utilities.putUSSPayload(this.getSession(), outputPath, { ...(options ?? {}), request: "copy" }); + return zosfiles.Utilities.putUSSPayload(this.getSession(), outputPath, { ...options, request: "copy" }); } public async move(oldPath: string, newPath: string): Promise { @@ -142,11 +145,17 @@ export namespace ZoweExplorerZosmf { } public uploadFromBuffer(buffer: Buffer, filePath: string, options?: zosfiles.IUploadOptions): Promise { - return zosfiles.Upload.bufferToUssFile(this.getSession(), filePath, buffer, options); + return zosfiles.Upload.bufferToUssFile(this.getSession(), filePath, buffer, { + responseTimeout: this.profile?.profile?.responseTimeout, + ...options, + }); } public putContent(inputFilePath: string, ussFilePath: string, options: zosfiles.IUploadOptions): Promise { - return zosfiles.Upload.fileToUssFile(this.getSession(), inputFilePath, ussFilePath, options); + return zosfiles.Upload.fileToUssFile(this.getSession(), inputFilePath, ussFilePath, { + responseTimeout: this.profile?.profile?.responseTimeout, + ...options, + }); } public async updateAttributes(ussPath: string, attributes: Partial): Promise { @@ -199,17 +208,20 @@ export namespace ZoweExplorerZosmf { ussDirectoryPath: string, options?: zosfiles.IUploadOptions ): Promise { - return zosfiles.Upload.dirToUSSDirRecursive(this.getSession(), inputDirectoryPath, ussDirectoryPath, options); + return zosfiles.Upload.dirToUSSDirRecursive(this.getSession(), inputDirectoryPath, ussDirectoryPath, { + responseTimeout: this.profile?.profile?.responseTimeout, + ...options, + }); } public create(ussPath: string, type: string, mode?: string): Promise { - return zosfiles.Create.uss(this.getSession(), ussPath, type, mode); + return zosfiles.Create.uss(this.getSession(), ussPath, type, mode, { responseTimeout: this.profile?.profile?.responseTimeout }); } public delete(ussPath: string, recursive?: boolean): Promise { // handle zosmf api issue with file paths const fixedName = ussPath.startsWith("/") ? ussPath.substring(1) : ussPath; - return zosfiles.Delete.ussFile(this.getSession(), fixedName, recursive); + return zosfiles.Delete.ussFile(this.getSession(), fixedName, recursive, { responseTimeout: this.profile?.profile?.responseTimeout }); } public async rename(currentUssPath: string, newUssPath: string): Promise { @@ -235,23 +247,35 @@ export namespace ZoweExplorerZosmf { */ export class MvsApi extends CommonApi implements MainframeInteraction.IMvs { public dataSet(filter: string, options?: zosfiles.IListOptions): Promise { - return zosfiles.List.dataSet(this.getSession(), filter, options); + return zosfiles.List.dataSet(this.getSession(), filter, { responseTimeout: this.profile?.profile?.responseTimeout, ...options }); } public allMembers(dataSetName: string, options?: zosfiles.IListOptions): Promise { - return zosfiles.List.allMembers(this.getSession(), dataSetName, options); + return zosfiles.List.allMembers(this.getSession(), dataSetName, { + responseTimeout: this.profile?.profile?.responseTimeout, + ...options, + }); } public getContents(dataSetName: string, options?: zosfiles.IDownloadSingleOptions): Promise { - return zosfiles.Download.dataSet(this.getSession(), dataSetName, options); + return zosfiles.Download.dataSet(this.getSession(), dataSetName, { + responseTimeout: this.profile?.profile?.responseTimeout, + ...options, + }); } public uploadFromBuffer(buffer: Buffer, dataSetName: string, options?: zosfiles.IUploadOptions): Promise { - return zosfiles.Upload.bufferToDataSet(this.getSession(), buffer, dataSetName, options); + return zosfiles.Upload.bufferToDataSet(this.getSession(), buffer, dataSetName, { + responseTimeout: this.profile?.profile?.responseTimeout, + ...options, + }); } public putContents(inputFilePath: string, dataSetName: string, options?: zosfiles.IUploadOptions): Promise { - return zosfiles.Upload.pathToDataSet(this.getSession(), inputFilePath, dataSetName, options); + return zosfiles.Upload.pathToDataSet(this.getSession(), inputFilePath, dataSetName, { + responseTimeout: this.profile?.profile?.responseTimeout, + ...options, + }); } public createDataSet( @@ -259,15 +283,23 @@ export namespace ZoweExplorerZosmf { dataSetName: string, options?: Partial ): Promise { - return zosfiles.Create.dataSet(this.getSession(), dataSetType, dataSetName, options); + return zosfiles.Create.dataSet(this.getSession(), dataSetType, dataSetName, { + responseTimeout: this.profile?.profile?.responseTimeout, + ...options, + }); } public createDataSetMember(dataSetName: string, options?: zosfiles.IUploadOptions): Promise { - return zosfiles.Upload.bufferToDataSet(this.getSession(), Buffer.from(""), dataSetName, options); + return zosfiles.Upload.bufferToDataSet(this.getSession(), Buffer.from(""), dataSetName, { + responseTimeout: this.profile?.profile?.responseTimeout, + ...options, + }); } public allocateLikeDataSet(dataSetName: string, likeDataSetName: string): Promise { - return zosfiles.Create.dataSetLike(this.getSession(), dataSetName, likeDataSetName); + return zosfiles.Create.dataSetLike(this.getSession(), dataSetName, likeDataSetName, { + responseTimeout: this.profile?.profile?.responseTimeout, + }); } public copyDataSetMember( @@ -282,7 +314,7 @@ export namespace ZoweExplorerZosmf { } else { newOptions = { ...options, - ...{ "from-dataset": { dsn: fromDataSetName, member: fromMemberName } }, + "from-dataset": { dsn: fromDataSetName, member: fromMemberName }, }; } } else { @@ -290,34 +322,59 @@ export namespace ZoweExplorerZosmf { // we will need to break the interface definition in the ZoweExplorerApi newOptions = { "from-dataset": { dsn: fromDataSetName, member: fromMemberName } }; } - return zosfiles.Copy.dataSet(this.getSession(), { dsn: toDataSetName, member: toMemberName }, newOptions); + return zosfiles.Copy.dataSet( + this.getSession(), + { dsn: toDataSetName, member: toMemberName }, + { + responseTimeout: this.profile?.profile?.responseTimeout, + ...newOptions, + } + ); } public renameDataSet(currentDataSetName: string, newDataSetName: string): Promise { - return zosfiles.Rename.dataSet(this.getSession(), currentDataSetName, newDataSetName); + return zosfiles.Rename.dataSet(this.getSession(), currentDataSetName, newDataSetName, { + responseTimeout: this.profile?.profile?.responseTimeout, + }); } public renameDataSetMember(dataSetName: string, oldMemberName: string, newMemberName: string): Promise { - return zosfiles.Rename.dataSetMember(this.getSession(), dataSetName, oldMemberName, newMemberName); + return zosfiles.Rename.dataSetMember(this.getSession(), dataSetName, oldMemberName, newMemberName, { + responseTimeout: this.profile?.profile?.responseTimeout, + }); } public hMigrateDataSet(dataSetName: string): Promise { - return zosfiles.HMigrate.dataSet(this.getSession(), dataSetName); + return zosfiles.HMigrate.dataSet(this.getSession(), dataSetName, { + responseTimeout: this.profile?.profile?.responseTimeout, + }); } public hRecallDataSet(dataSetName: string): Promise { - return zosfiles.HRecall.dataSet(this.getSession(), dataSetName); + return zosfiles.HRecall.dataSet(this.getSession(), dataSetName, { + responseTimeout: this.profile?.profile?.responseTimeout, + }); } public deleteDataSet(dataSetName: string, options?: zosfiles.IDeleteDatasetOptions): Promise { - return zosfiles.Delete.dataSet(this.getSession(), dataSetName, options); + return zosfiles.Delete.dataSet(this.getSession(), dataSetName, { + responseTimeout: this.profile?.profile?.responseTimeout, + ...options, + }); } public dataSetsMatchingPattern(filter: string[], options?: zosfiles.IDsmListOptions): Promise { - return zosfiles.List.dataSetsMatchingPattern(this.getSession(), filter, options); + return zosfiles.List.dataSetsMatchingPattern(this.getSession(), filter, { + responseTimeout: this.profile?.profile?.responseTimeout, + ...options, + }); } public copyDataSet(fromDataSetName: string, toDataSetName: string, enq?: string, replace?: boolean): Promise { - return zosfiles.Copy.dataSet(this.getSession(), { dsn: toDataSetName }, { "from-dataset": { dsn: fromDataSetName }, enq, replace }); + return zosfiles.Copy.dataSet( + this.getSession(), + { dsn: toDataSetName }, + { "from-dataset": { dsn: fromDataSetName }, enq, replace, responseTimeout: this.profile?.profile?.responseTimeout } + ); } } diff --git a/packages/zowe-explorer/__tests__/__unit__/extending/ZoweExplorerZosmfApi.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/extending/ZoweExplorerZosmfApi.unit.test.ts index f206d99696..1f2b445f09 100644 --- a/packages/zowe-explorer/__tests__/__unit__/extending/ZoweExplorerZosmfApi.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/extending/ZoweExplorerZosmfApi.unit.test.ts @@ -30,7 +30,9 @@ describe("Zosmf API tests", () => { const api = new ZoweExplorerZosmf.MvsApi(); api.getSession = jest.fn(); - await api.copyDataSetMember({ dsn: "IBM.FROM", member: "IEFBR14" }, { dsn: "IBM.TO", member: "IEFBR15" }); + await api.copyDataSetMember({ dsn: "IBM.FROM", member: "IEFBR14" }, { dsn: "IBM.TO", member: "IEFBR15" }, { + responseTimeout: undefined, + } as any); }); it("should test that copy data set uses enq", async () => { @@ -46,7 +48,7 @@ describe("Zosmf API tests", () => { await api.copyDataSetMember( { dsn: "IBM.FROM", member: "IEFBR14" }, { dsn: "IBM.TO", member: "IEFBR15" }, - { enq: "SHR", "from-dataset": { dsn: "BROADCOM.FROM" } } + { enq: "SHR", "from-dataset": { dsn: "BROADCOM.FROM" }, responseTimeout: undefined } ); }); @@ -62,6 +64,7 @@ describe("Zosmf API tests", () => { api.getSession = jest.fn(); await api.copyDataSetMember({ dsn: "IBM.FROM", member: "IEFBR14" }, { dsn: "IBM.TO", member: "IEFBR15" }, { enq: "SHR", + responseTimeout: undefined, } as any); }); @@ -80,6 +83,7 @@ describe("Zosmf API tests", () => { await api.putContent("someLocalFile.txt", "/some/remote", { encoding: "285", + responseTimeout: undefined, }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/extending/__snapshots__/ZoweExplorerZosmfApi.unit.test.ts.snap b/packages/zowe-explorer/__tests__/__unit__/extending/__snapshots__/ZoweExplorerZosmfApi.unit.test.ts.snap index c3eed47e87..b8a072080a 100644 --- a/packages/zowe-explorer/__tests__/__unit__/extending/__snapshots__/ZoweExplorerZosmfApi.unit.test.ts.snap +++ b/packages/zowe-explorer/__tests__/__unit__/extending/__snapshots__/ZoweExplorerZosmfApi.unit.test.ts.snap @@ -3,6 +3,7 @@ exports[`Zosmf API tests should test putContent method passes all options to Zowe api method 1`] = ` { "encoding": "285", + "responseTimeout": undefined, } `; @@ -12,6 +13,7 @@ exports[`Zosmf API tests should test that copy data set uses default options 1`] "dsn": "IBM.FROM", "member": "IEFBR14", }, + "responseTimeout": undefined, } `; @@ -21,6 +23,7 @@ exports[`Zosmf API tests should test that copy data set uses enq 1`] = ` "from-dataset": { "dsn": "BROADCOM.FROM", }, + "responseTimeout": undefined, } `; @@ -31,5 +34,6 @@ exports[`Zosmf API tests should test that copy data set uses enq only 1`] = ` "dsn": "IBM.FROM", "member": "IEFBR14", }, + "responseTimeout": undefined, } `; From b1de4d149027598c7cc599d018cffb7546fdada6 Mon Sep 17 00:00:00 2001 From: likhithanimma1 <142219673+likhithanimma1@users.noreply.github.com> Date: Wed, 6 Nov 2024 18:51:23 +0530 Subject: [PATCH 2/5] Remove inconsistency in translation strings (#3272) * Remove inconsistency in translation strings Signed-off-by: likhithanimma1 <142219673+likhithanimma1@users.noreply.github.com> * Translation of half of strings in bundle.l10n.json Signed-off-by: likhithanimma1 <142219673+likhithanimma1@users.noreply.github.com> * Translate strings and fix unit tests Signed-off-by: likhithanimma1 <142219673+likhithanimma1@users.noreply.github.com> * Fix unit tests Signed-off-by: likhithanimma1 <142219673+likhithanimma1@users.noreply.github.com> * Add changelogs Signed-off-by: likhithanimma1 <142219673+likhithanimma1@users.noreply.github.com> * Commit Ana's suggestions to package.nls.json file Signed-off-by: likhithanimma1 <142219673+likhithanimma1@users.noreply.github.com> * Changes to l10 file Signed-off-by: likhithanimma1 <142219673+likhithanimma1@users.noreply.github.com> * Revert certificate key to lowercase Signed-off-by: likhithanimma1 <142219673+likhithanimma1@users.noreply.github.com> * Change credential manager back to lower case Signed-off-by: likhithanimma1 <142219673+likhithanimma1@users.noreply.github.com> --------- Signed-off-by: likhithanimma1 <142219673+likhithanimma1@users.noreply.github.com> Signed-off-by: Billie Simmons Co-authored-by: Billie Simmons --- packages/zowe-explorer-api/CHANGELOG.md | 1 + .../src/vscode/ZoweVsCodeExtension.ts | 2 +- packages/zowe-explorer/CHANGELOG.md | 1 + .../commands/MvsCommandHandler.unit.test.ts | 20 +-- .../commands/TsoCommandHandler.unit.test.ts | 20 +-- .../commands/UnixCommandHandler.unit.test.ts | 24 +-- .../configuration/Profiles.unit.test.ts | 6 +- .../management/ProfileManagement.unit.test.ts | 6 +- .../DatasetActions.extended.unit.test.ts | 2 +- .../trees/dataset/DatasetActions.unit.test.ts | 16 +- .../trees/job/JobActions.unit.test.ts | 6 +- .../trees/uss/USSActions.unit.test.ts | 2 +- .../utils/CertificateWizard.unit.test.ts | 2 +- .../__unit__/utils/ProfilesUtils.unit.test.ts | 10 +- packages/zowe-explorer/l10n/bundle.l10n.json | 152 +++++++++--------- packages/zowe-explorer/l10n/poeditor.json | 114 +++++++------ packages/zowe-explorer/package.nls.json | 22 +-- .../src/commands/MvsCommandHandler.ts | 4 +- .../src/commands/TsoCommandHandler.ts | 10 +- .../src/commands/UnixCommandHandler.ts | 6 +- .../src/configuration/Profiles.ts | 20 +-- .../src/management/ProfileManagement.ts | 10 +- .../src/trees/dataset/DatasetActions.ts | 10 +- .../src/trees/dataset/DatasetTemplates.ts | 2 +- .../src/trees/dataset/DatasetTree.ts | 14 +- .../zowe-explorer/src/trees/job/JobActions.ts | 2 +- .../zowe-explorer/src/trees/job/JobTree.ts | 6 +- .../zowe-explorer/src/trees/uss/USSActions.ts | 2 +- packages/zowe-explorer/src/utils/AuthUtils.ts | 2 +- .../src/utils/CertificateWizard.ts | 4 +- .../zowe-explorer/src/utils/ProfilesUtils.ts | 16 +- .../src/webviews/src/edit-attributes/App.tsx | 4 +- 32 files changed, 259 insertions(+), 259 deletions(-) diff --git a/packages/zowe-explorer-api/CHANGELOG.md b/packages/zowe-explorer-api/CHANGELOG.md index 1f9354756e..7d048bbc7b 100644 --- a/packages/zowe-explorer-api/CHANGELOG.md +++ b/packages/zowe-explorer-api/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to the "zowe-explorer-api" extension will be documented in t ### Bug fixes +- Fixed an issue to review inconsistent capitalization across translation strings. [#2935](https://github.com/zowe/zowe-explorer-vscode/issues/2935) - Fixed an issue where the `responseTimeout` profile property was ignored for z/OSMF MVS and USS API calls. [#3225](https://github.com/zowe/zowe-explorer-vscode/issues/3225) ## `3.0.2` diff --git a/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts b/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts index 080d68422b..8b390e98dc 100644 --- a/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts +++ b/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts @@ -170,7 +170,7 @@ export class ZoweVsCodeExtension { ]; const response = await Gui.showQuickPick(qpItems, { placeHolder: "Select an authentication method for obtaining token", - title: `[${baseProfile.name}] Log in to authentication service`, + title: `[${baseProfile.name}] Log in to Authentication Service`, }); if (response === qpItems[0]) { const creds = await ZoweVsCodeExtension.promptUserPass({ session: updSession.ISession, rePrompt: true }); diff --git a/packages/zowe-explorer/CHANGELOG.md b/packages/zowe-explorer/CHANGELOG.md index 7315dc1fe8..84ac4e381c 100644 --- a/packages/zowe-explorer/CHANGELOG.md +++ b/packages/zowe-explorer/CHANGELOG.md @@ -20,6 +20,7 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen - Fixed an issue where calling `vscode.workspace.fs.readFile` with a PDS member URI would throw an error when the PDS already existed as a filesystem entry. [#3267](https://github.com/zowe/zowe-explorer-vscode/issues/3267) - Fixed issue where Zowe Explorer would present the "No configs detected" notification when initialized in a workspace without a Zowe team configuration. [#3280](https://github.com/zowe/zowe-explorer-vscode/issues/3280) - Reduced the number of MVS API calls performed by `vscode.workspace.fs.readFile` when fetching the contents of a data set entry. [#3278](https://github.com/zowe/zowe-explorer-vscode/issues/3278) +- Fixed an issue to review inconsistent capitalization across translation strings. [#2935](https://github.com/zowe/zowe-explorer-vscode/issues/2935) ## `3.0.2` diff --git a/packages/zowe-explorer/__tests__/__unit__/commands/MvsCommandHandler.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/commands/MvsCommandHandler.unit.test.ts index 66ac72ebbf..4797525890 100644 --- a/packages/zowe-explorer/__tests__/__unit__/commands/MvsCommandHandler.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/commands/MvsCommandHandler.unit.test.ts @@ -189,7 +189,7 @@ describe("mvsCommandActions unit testing", () => { expect(showQuickPick.mock.calls[0][1]).toEqual({ canPickMany: false, ignoreFocusOut: true, - placeHolder: "Select the Profile to use to submit the command", + placeHolder: "Select the profile to use to submit the command", }); expect(showInputBox.mock.calls.length).toBe(1); expect(appendLine.mock.calls.length).toBe(2); @@ -232,7 +232,7 @@ describe("mvsCommandActions unit testing", () => { expect(showQuickPick.mock.calls[0][1]).toEqual({ canPickMany: false, ignoreFocusOut: true, - placeHolder: "Select the Profile to use to submit the command", + placeHolder: "Select the profile to use to submit the command", }); expect(showInputBox.mock.calls.length).toBe(0); expect(appendLine.mock.calls.length).toBe(2); @@ -276,7 +276,7 @@ describe("mvsCommandActions unit testing", () => { expect(showQuickPick.mock.calls[0][1]).toEqual({ canPickMany: false, ignoreFocusOut: true, - placeHolder: "Select the Profile to use to submit the command", + placeHolder: "Select the profile to use to submit the command", }); expect(showInputBox.mock.calls.length).toBe(1); expect(showErrorMessage.mock.calls.length).toBe(1); @@ -317,7 +317,7 @@ describe("mvsCommandActions unit testing", () => { expect(showQuickPick.mock.calls[0][1]).toEqual({ canPickMany: false, ignoreFocusOut: true, - placeHolder: "Select the Profile to use to submit the command", + placeHolder: "Select the profile to use to submit the command", }); expect(showInformationMessage.mock.calls.length).toBe(1); expect(showInformationMessage.mock.calls[0][0]).toEqual("No selection made. Operation cancelled."); @@ -356,7 +356,7 @@ describe("mvsCommandActions unit testing", () => { expect(showQuickPick.mock.calls[0][1]).toEqual({ canPickMany: false, ignoreFocusOut: true, - placeHolder: "Select the Profile to use to submit the command", + placeHolder: "Select the profile to use to submit the command", }); expect(showInputBox.mock.calls.length).toBe(1); expect(showInformationMessage.mock.calls.length).toBe(1); @@ -413,7 +413,7 @@ describe("mvsCommandActions unit testing", () => { expect(showQuickPick.mock.calls[0][1]).toEqual({ canPickMany: false, ignoreFocusOut: true, - placeHolder: "Select the Profile to use to submit the command", + placeHolder: "Select the profile to use to submit the command", }); expect(showInputBox.mock.calls.length).toBe(0); }); @@ -457,7 +457,7 @@ describe("mvsCommandActions unit testing", () => { expect(showQuickPick.mock.calls[0][1]).toEqual({ canPickMany: false, ignoreFocusOut: true, - placeHolder: "Select the Profile to use to submit the command", + placeHolder: "Select the profile to use to submit the command", }); expect(showInputBox.mock.calls.length).toBe(1); }); @@ -499,7 +499,7 @@ describe("mvsCommandActions unit testing", () => { expect(showQuickPick.mock.calls[0][1]).toEqual({ canPickMany: false, ignoreFocusOut: true, - placeHolder: "Select the Profile to use to submit the command", + placeHolder: "Select the profile to use to submit the command", }); expect(showInputBox.mock.calls.length).toBe(1); }); @@ -547,7 +547,7 @@ describe("mvsCommandActions unit testing", () => { await mvsActions.issueMvsCommand(); expect(showInformationMessage.mock.calls.length).toBe(1); - expect(showInformationMessage.mock.calls[0][0]).toEqual("Operation Cancelled"); + expect(showInformationMessage.mock.calls[0][0]).toEqual("Operation cancelled"); }); it("tests the issueMvsCommand function from a session", async () => { @@ -615,7 +615,7 @@ describe("mvsCommandActions unit testing", () => { expect(showQuickPick.mock.calls[0][1]).toEqual({ canPickMany: false, ignoreFocusOut: true, - placeHolder: "Select the Profile to use to submit the command", + placeHolder: "Select the profile to use to submit the command", }); expect(showInputBox.mock.calls.length).toBe(0); expect(showErrorMessage.mock.calls.length).toBe(1); diff --git a/packages/zowe-explorer/__tests__/__unit__/commands/TsoCommandHandler.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/commands/TsoCommandHandler.unit.test.ts index 88d79146f6..61764f5472 100644 --- a/packages/zowe-explorer/__tests__/__unit__/commands/TsoCommandHandler.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/commands/TsoCommandHandler.unit.test.ts @@ -189,7 +189,7 @@ describe("TsoCommandHandler unit testing", () => { expect(showQuickPick.mock.calls[0][1]).toEqual({ canPickMany: false, ignoreFocusOut: true, - placeHolder: "Select the Profile to use to submit the TSO command", + placeHolder: "Select the profile to use to submit the TSO command", }); expect(showInputBox.mock.calls.length).toBe(1); expect(appendLine.mock.calls.length).toBe(2); @@ -231,7 +231,7 @@ describe("TsoCommandHandler unit testing", () => { expect(showQuickPick.mock.calls[0][1]).toEqual({ canPickMany: false, ignoreFocusOut: true, - placeHolder: "Select the Profile to use to submit the TSO command", + placeHolder: "Select the profile to use to submit the TSO command", }); expect(showInputBox.mock.calls.length).toBe(0); expect(appendLine.mock.calls.length).toBe(2); @@ -275,7 +275,7 @@ describe("TsoCommandHandler unit testing", () => { expect(showQuickPick.mock.calls[0][1]).toEqual({ canPickMany: false, ignoreFocusOut: true, - placeHolder: "Select the Profile to use to submit the TSO command", + placeHolder: "Select the profile to use to submit the TSO command", }); expect(showInputBox.mock.calls.length).toBe(1); expect(showErrorMessage.mock.calls.length).toBe(1); @@ -316,7 +316,7 @@ describe("TsoCommandHandler unit testing", () => { expect(showQuickPick.mock.calls[0][1]).toEqual({ canPickMany: false, ignoreFocusOut: true, - placeHolder: "Select the Profile to use to submit the TSO command", + placeHolder: "Select the profile to use to submit the TSO command", }); expect(showInformationMessage.mock.calls.length).toBe(1); expect(showInformationMessage.mock.calls[0][0]).toEqual("No selection made. Operation cancelled."); @@ -355,7 +355,7 @@ describe("TsoCommandHandler unit testing", () => { expect(showQuickPick.mock.calls[0][1]).toEqual({ canPickMany: false, ignoreFocusOut: true, - placeHolder: "Select the Profile to use to submit the TSO command", + placeHolder: "Select the profile to use to submit the TSO command", }); expect(showInputBox.mock.calls.length).toBe(1); expect(showInformationMessage.mock.calls.length).toBe(1); @@ -412,7 +412,7 @@ describe("TsoCommandHandler unit testing", () => { expect(showQuickPick.mock.calls[0][1]).toEqual({ canPickMany: false, ignoreFocusOut: true, - placeHolder: "Select the Profile to use to submit the TSO command", + placeHolder: "Select the profile to use to submit the TSO command", }); expect(showInputBox.mock.calls.length).toBe(0); }); @@ -456,7 +456,7 @@ describe("TsoCommandHandler unit testing", () => { expect(showQuickPick.mock.calls[0][1]).toEqual({ canPickMany: false, ignoreFocusOut: true, - placeHolder: "Select the Profile to use to submit the TSO command", + placeHolder: "Select the profile to use to submit the TSO command", }); expect(showInputBox.mock.calls.length).toBe(1); }); @@ -498,7 +498,7 @@ describe("TsoCommandHandler unit testing", () => { expect(showQuickPick.mock.calls[0][1]).toEqual({ canPickMany: false, ignoreFocusOut: true, - placeHolder: "Select the Profile to use to submit the TSO command", + placeHolder: "Select the profile to use to submit the TSO command", }); expect(showInputBox.mock.calls.length).toBe(1); }); @@ -546,7 +546,7 @@ describe("TsoCommandHandler unit testing", () => { await tsoActions.issueTsoCommand(); expect(showInformationMessage.mock.calls.length).toBe(1); - expect(showInformationMessage.mock.calls[0][0]).toEqual("Operation Cancelled"); + expect(showInformationMessage.mock.calls[0][0]).toEqual("Operation cancelled"); }); it("tests the issueTsoCommand function from a session", async () => { @@ -614,7 +614,7 @@ describe("TsoCommandHandler unit testing", () => { expect(showQuickPick.mock.calls[0][1]).toEqual({ canPickMany: false, ignoreFocusOut: true, - placeHolder: "Select the Profile to use to submit the TSO command", + placeHolder: "Select the profile to use to submit the TSO command", }); expect(showInputBox.mock.calls.length).toBe(0); expect(showErrorMessage.mock.calls.length).toBe(1); diff --git a/packages/zowe-explorer/__tests__/__unit__/commands/UnixCommandHandler.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/commands/UnixCommandHandler.unit.test.ts index 76faf51285..dc0448c977 100644 --- a/packages/zowe-explorer/__tests__/__unit__/commands/UnixCommandHandler.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/commands/UnixCommandHandler.unit.test.ts @@ -249,7 +249,7 @@ describe("UnixCommand Actions Unit Testing", () => { expect(showQuickPick.mock.calls[0][1]).toEqual({ canPickMany: false, ignoreFocusOut: true, - placeHolder: "Select the Profile to use to submit the Unix command", + placeHolder: "Select the profile to use to submit the Unix command", }); expect(showInputBox.mock.calls.length).toBe(2); @@ -306,7 +306,7 @@ describe("UnixCommand Actions Unit Testing", () => { expect(showQuickPick.mock.calls[0][1]).toEqual({ canPickMany: false, ignoreFocusOut: true, - placeHolder: "Select the Profile to use to submit the Unix command", + placeHolder: "Select the profile to use to submit the Unix command", }); expect(showInputBox.mock.calls.length).toBe(1); expect(appendLine.mock.calls.length).toBe(2); @@ -338,11 +338,11 @@ describe("UnixCommand Actions Unit Testing", () => { expect(showQuickPick.mock.calls[0][1]).toEqual({ canPickMany: false, ignoreFocusOut: true, - placeHolder: "Select the Profile to use to submit the Unix command", + placeHolder: "Select the profile to use to submit the Unix command", }); expect(showInputBox.mock.calls.length).toBe(1); expect(showInformationMessage.mock.calls.length).toBe(1); - expect(showInformationMessage.mock.calls[0][0]).toEqual("Operation Cancelled"); + expect(showInformationMessage.mock.calls[0][0]).toEqual("Operation cancelled"); }); it("tests the issueUnixCommand function user escapes the commandbox", async () => { @@ -369,11 +369,11 @@ describe("UnixCommand Actions Unit Testing", () => { expect(showQuickPick.mock.calls[0][1]).toEqual({ canPickMany: false, ignoreFocusOut: true, - placeHolder: "Select the Profile to use to submit the Unix command", + placeHolder: "Select the profile to use to submit the Unix command", }); expect(showInputBox.mock.calls.length).toBe(2); expect(showInformationMessage.mock.calls.length).toBe(1); - expect(showInformationMessage.mock.calls[0][0]).toEqual("Operation Cancelled"); + expect(showInformationMessage.mock.calls[0][0]).toEqual("Operation cancelled"); }); it("tests the issueUnixCommand function - issueUnixCommand throws an error", async () => { @@ -402,7 +402,7 @@ describe("UnixCommand Actions Unit Testing", () => { expect(showQuickPick.mock.calls[0][1]).toEqual({ canPickMany: false, ignoreFocusOut: true, - placeHolder: "Select the Profile to use to submit the Unix command", + placeHolder: "Select the profile to use to submit the Unix command", }); expect(showInputBox.mock.calls.length).toBe(2); expect(showErrorMessage.mock.calls.length).toBe(1); @@ -435,7 +435,7 @@ describe("UnixCommand Actions Unit Testing", () => { await unixActions.issueUnixCommand(); - expect(showInformationMessage.mock.calls[0][0]).toEqual("Operation Cancelled"); + expect(showInformationMessage.mock.calls[0][0]).toEqual("Operation cancelled"); }); it("tests the issueUnixCommand function user starts typing a value in quick pick", async () => { @@ -479,7 +479,7 @@ describe("UnixCommand Actions Unit Testing", () => { expect(showQuickPick.mock.calls[0][1]).toEqual({ canPickMany: false, ignoreFocusOut: true, - placeHolder: "Select the Profile to use to submit the Unix command", + placeHolder: "Select the profile to use to submit the Unix command", }); expect(showInputBox.mock.calls.length).toBe(1); }); @@ -498,7 +498,7 @@ describe("UnixCommand Actions Unit Testing", () => { await unixActions.issueUnixCommand(); expect(showInformationMessage.mock.calls.length).toBe(1); - expect(showInformationMessage.mock.calls[0][0]).toEqual("Operation Cancelled"); + expect(showInformationMessage.mock.calls[0][0]).toEqual("Operation cancelled"); }); it("tests the issueUnixCommand function no profiles error", async () => { @@ -589,7 +589,7 @@ describe("UnixCommand Actions Unit Testing", () => { value: jest.fn().mockReturnValueOnce({ profile: { user: "testuser", password: "testpassword" } } as any), configurable: true, }); - expect(showInformationMessage.mock.calls[0][0]).toEqual("Operation Cancelled"); + expect(showInformationMessage.mock.calls[0][0]).toEqual("Operation cancelled"); }); it("getCommand API is not implemented", async () => { @@ -634,6 +634,6 @@ describe("UnixCommand Actions Unit Testing", () => { await (unixActions as any).userSelectProfile(); expect(showInformationMessage.mock.calls.length).toBe(1); - expect(showInformationMessage.mock.calls[0][0]).toEqual("Operation Cancelled"); + expect(showInformationMessage.mock.calls[0][0]).toEqual("Operation cancelled"); }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/configuration/Profiles.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/configuration/Profiles.unit.test.ts index 8f82c2a620..3b239989bd 100644 --- a/packages/zowe-explorer/__tests__/__unit__/configuration/Profiles.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/configuration/Profiles.unit.test.ts @@ -395,7 +395,7 @@ describe("Profiles Unit Tests - Function editZoweConfigFile", () => { spy.mockResolvedValueOnce(undefined); await Profiles.getInstance().editZoweConfigFile(); expect(spy).toHaveBeenCalled(); - expect(globalMocks.mockShowInformationMessage.mock.calls[0][0]).toBe("Operation Cancelled"); + expect(globalMocks.mockShowInformationMessage.mock.calls[0][0]).toBe("Operation cancelled"); spy.mockClear(); }); it("Tests that editZoweConfigFile opens correct file when Global is selected", async () => { @@ -500,7 +500,7 @@ describe("Profiles Unit Tests - Function createZoweSchema", () => { spy.mockResolvedValueOnce(undefined); await Profiles.getInstance().createZoweSchema(blockMocks.testDatasetTree); expect(spy).toHaveBeenCalled(); - expect(globalMocks.mockShowInformationMessage.mock.calls[0][0]).toBe("Operation Cancelled"); + expect(globalMocks.mockShowInformationMessage.mock.calls[0][0]).toBe("Operation cancelled"); spy.mockClear(); }); it("Tests that createZoweSchema will open correct config file when cancelling creation in location with existing config file", async () => { @@ -810,7 +810,7 @@ describe("Profiles Unit Tests - function getDeleteProfile", () => { const showMessageSpy = jest.spyOn(Gui, "showMessage"); jest.spyOn(Gui, "showQuickPick").mockResolvedValue(undefined); await expect(privateProfile.getDeleteProfile()).resolves.toEqual(undefined); - expect(showMessageSpy).toHaveBeenCalledWith("Operation Cancelled"); + expect(showMessageSpy).toHaveBeenCalledWith("Operation cancelled"); }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/management/ProfileManagement.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/management/ProfileManagement.unit.test.ts index 203cc8296a..382efe9f4b 100644 --- a/packages/zowe-explorer/__tests__/__unit__/management/ProfileManagement.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/management/ProfileManagement.unit.test.ts @@ -137,7 +137,7 @@ describe("ProfileManagement unit tests", () => { mocks.mockResolveQp.mockResolvedValueOnce(undefined); await ProfileManagement.manageProfile(mocks.mockDsSessionNode); expect(mocks.debugLogSpy).toHaveBeenCalledWith(mocks.logMsg); - expect(mocks.opCancelledSpy).toHaveBeenCalledWith("Operation Cancelled"); + expect(mocks.opCancelledSpy).toHaveBeenCalledWith("Operation cancelled"); }); it("profile using basic authentication should see promptCredentials called when Update Credentials chosen", async () => { const mocks = createBlockMocks(createGlobalMocks()); @@ -204,7 +204,7 @@ describe("ProfileManagement unit tests", () => { mocks.mockResolveQp.mockResolvedValueOnce(undefined); await ProfileManagement.manageProfile(mocks.mockDsSessionNode); expect(mocks.debugLogSpy).toHaveBeenCalledWith(mocks.logMsg); - expect(mocks.opCancelledSpy).toHaveBeenCalledWith("Operation Cancelled"); + expect(mocks.opCancelledSpy).toHaveBeenCalledWith("Operation cancelled"); }); it("profile using token authentication should see ssoLogin called when Log in to authentication service chosen", async () => { const mocks = createBlockMocks(createGlobalMocks()); @@ -264,7 +264,7 @@ describe("ProfileManagement unit tests", () => { mocks.mockResolveQp.mockResolvedValueOnce(undefined); await ProfileManagement.manageProfile(mocks.mockDsSessionNode); expect(mocks.debugLogSpy).toHaveBeenCalledWith(mocks.logMsg); - expect(mocks.opCancelledSpy).toHaveBeenCalledWith("Operation Cancelled"); + expect(mocks.opCancelledSpy).toHaveBeenCalledWith("Operation cancelled"); }); it("profile with no authentication method should see promptCredentials called when Add Basic Credentials chosen", async () => { const mocks = createBlockMocks(createGlobalMocks()); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetActions.extended.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetActions.extended.unit.test.ts index dc3bb0048e..98ffa3e517 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetActions.extended.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetActions.extended.unit.test.ts @@ -156,7 +156,7 @@ describe("mvsNodeActions", () => { await DatasetActions.uploadDialog(node, testTree); expect(globalMocks.showOpenDialog).toHaveBeenCalled(); - expect(globalMocks.showInformationMessage.mock.calls.map((call) => call[0])).toEqual(["Operation Cancelled"]); + expect(globalMocks.showInformationMessage.mock.calls.map((call) => call[0])).toEqual(["Operation cancelled"]); expect(globalMocks.openTextDocument).not.toHaveBeenCalled(); expect(testTree.refreshElement).not.toHaveBeenCalled(); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetActions.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetActions.unit.test.ts index 9352efd1fc..87904d1ec3 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetActions.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetActions.unit.test.ts @@ -182,7 +182,7 @@ describe("Dataset Actions Unit Tests - Function createMember", () => { expect(newNode?.contextValue).toBe(Constants.DS_MEMBER_CONTEXT); expect(newNode?.command.command).toBe("vscode.open"); expect(mySpy).toHaveBeenCalledWith({ - placeHolder: "Name of Member", + placeHolder: "Name of member", validateInput: expect.any(Function), }); expect(mocked(zosfiles.Upload.bufferToDataSet)).toHaveBeenCalledWith( @@ -256,7 +256,7 @@ describe("Dataset Actions Unit Tests - Function createMember", () => { await DatasetActions.createMember(parent, blockMocks.testDatasetTree); expect(parent.children.find((node) => node.label === "TESTMEMBER")).toBeDefined(); - expect(mySpy).toHaveBeenCalledWith({ placeHolder: "Name of Member", validateInput: expect.any(Function) }); + expect(mySpy).toHaveBeenCalledWith({ placeHolder: "Name of member", validateInput: expect.any(Function) }); expect(mocked(zosfiles.Upload.bufferToDataSet)).toHaveBeenCalledWith( blockMocks.zosmfSession, Buffer.from(""), @@ -2426,7 +2426,7 @@ describe("Dataset Actions Unit Tests - Function createFile", () => { mocked(vscode.window.showQuickPick).mockResolvedValueOnce(undefined); await DatasetActions.createFile(node, blockMocks.testDatasetTree); - expect(mocked(Gui.showMessage)).toHaveBeenCalledWith("Operation Cancelled"); + expect(mocked(Gui.showMessage)).toHaveBeenCalledWith("Operation cancelled"); expect(createDataSetSpy).not.toHaveBeenCalled(); }); it("Tests that user can edit the node label", async () => { @@ -2649,7 +2649,7 @@ describe("Dataset Actions Unit Tests - Function createFile", () => { node.contextValue = Constants.DS_DS_CONTEXT; await DatasetActions.createFile(node, blockMocks.testDatasetTree); - expect(createDataSetSpy).toHaveBeenCalledWith("Operation Cancelled"); + expect(createDataSetSpy).toHaveBeenCalledWith("Operation cancelled"); }); it("Checking opCancelled message shown when no ds type chosen", async () => { createGlobalMocks(); @@ -2673,7 +2673,7 @@ describe("Dataset Actions Unit Tests - Function createFile", () => { const createDataSetSpy = jest.spyOn(Gui, "showMessage"); await DatasetActions.createFile(node, blockMocks.testDatasetTree); - expect(createDataSetSpy).toHaveBeenCalledWith("Operation Cancelled"); + expect(createDataSetSpy).toHaveBeenCalledWith("Operation cancelled"); }); it("Checking opCancelled message shown when user escapes allocate or edit attributes options", async () => { createGlobalMocks(); @@ -2698,7 +2698,7 @@ describe("Dataset Actions Unit Tests - Function createFile", () => { const createDataSetSpy = jest.spyOn(Gui, "showMessage"); await DatasetActions.createFile(node, blockMocks.testDatasetTree); - expect(createDataSetSpy).toHaveBeenCalledWith("Operation Cancelled"); + expect(createDataSetSpy).toHaveBeenCalledWith("Operation cancelled"); }); it("Checking opCancelled message shown when user escapes during edit attributes", async () => { createGlobalMocks(); @@ -2727,7 +2727,7 @@ describe("Dataset Actions Unit Tests - Function createFile", () => { const createDataSetSpy = jest.spyOn(Gui, "showMessage"); await DatasetActions.createFile(node, blockMocks.testDatasetTree); - expect(createDataSetSpy).toHaveBeenCalledWith("Operation Cancelled"); + expect(createDataSetSpy).toHaveBeenCalledWith("Operation cancelled"); }); it("Checking opCancelled message shown when no template name supplied", async () => { createGlobalMocks(); @@ -2774,7 +2774,7 @@ describe("Dataset Actions Unit Tests - Function createFile", () => { expect(templateSpy).toHaveBeenCalled(); expect(addTempSpy).toHaveBeenCalledTimes(0); - expect(opCancelledSpy).toHaveBeenCalledWith("Operation Cancelled"); + expect(opCancelledSpy).toHaveBeenCalledWith("Operation cancelled"); templateSpy.mockClear(); addTempSpy.mockClear(); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/job/JobActions.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/job/JobActions.unit.test.ts index ee043a6f07..e45170166d 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/job/JobActions.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/job/JobActions.unit.test.ts @@ -394,7 +394,9 @@ describe("Jobs Actions Unit Tests - Function downloadSingleSpool", () => { expect(getSpoolFilesSpy).not.toHaveBeenCalled(); expect(mocked(Gui.showOpenDialog)).not.toHaveBeenCalled(); expect(mocked(Gui.errorMessage)).toHaveBeenCalled(); - expect(mocked(Gui.errorMessage).mock.calls[0][0]).toContain("Download Single Spool operation not implemented by extender"); + expect(mocked(Gui.errorMessage).mock.calls[0][0]).toContain( + "Download single spool operation not implemented by extender. Please contact the extension developer(s)." + ); }); }); @@ -642,7 +644,7 @@ describe("Jobs Actions Unit Tests - Function submitJcl", () => { await DatasetActions.submitJcl(blockMocks.testDatasetTree, undefined); expect(submitJclSpy).not.toHaveBeenCalled(); - expect(messageSpy).toHaveBeenCalledWith("Operation Cancelled"); + expect(messageSpy).toHaveBeenCalledWith("Operation cancelled"); }); it("Checking API error on submit of active text editor content as JCL", async () => { diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSActions.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSActions.unit.test.ts index 3cd04ee5c5..1a4ee05bbe 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSActions.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSActions.unit.test.ts @@ -531,7 +531,7 @@ describe("USS Action Unit Tests - Functions uploadDialog & uploadFile", () => { globalMocks.showOpenDialog.mockReturnValue(undefined); await USSActions.uploadDialog(blockMocks.ussNode, blockMocks.testUSSTree, true); expect(globalMocks.showOpenDialog).toHaveBeenCalled(); - expect(globalMocks.showInformationMessage.mock.calls.map((call) => call[0])).toEqual(["Operation Cancelled"]); + expect(globalMocks.showInformationMessage.mock.calls.map((call) => call[0])).toEqual(["Operation cancelled"]); }); it("Tests that uploadDialog() throws an error successfully", async () => { diff --git a/packages/zowe-explorer/__tests__/__unit__/utils/CertificateWizard.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/utils/CertificateWizard.unit.test.ts index 960b22b688..82f451b476 100644 --- a/packages/zowe-explorer/__tests__/__unit__/utils/CertificateWizard.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/utils/CertificateWizard.unit.test.ts @@ -102,7 +102,7 @@ describe("CertificateWizard", () => { await (certWizard as any).onDidReceiveMessage({ command: "close", }); - expect(traceMock).toHaveBeenCalledWith("User dismissed the certificate wizard."); + expect(traceMock).toHaveBeenCalledWith("User dismissed the Certificate Wizard."); }); it("handles the get_localization message", async () => { 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 cf99a6b098..80be2384c8 100644 --- a/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts @@ -232,7 +232,7 @@ describe("ProfilesUtils unit tests", () => { await AuthUtils.errorHandling(errorDetails, label, moreInfo); expect(showErrorSpy).toHaveBeenCalledTimes(1); expect(promptCredentialsSpy).not.toHaveBeenCalled(); - expect(showMsgSpy).toHaveBeenCalledWith("Operation Cancelled"); + expect(showMsgSpy).toHaveBeenCalledWith("Operation cancelled"); showErrorSpy.mockClear(); showMsgSpy.mockClear(); promptCredentialsSpy.mockClear(); @@ -373,7 +373,7 @@ describe("ProfilesUtils unit tests", () => { }); jest.spyOn(ZoweVsCodeExtension as any, "promptUserPass").mockResolvedValue([]); await ProfilesUtils.promptCredentials(null as any); - expect(Gui.showMessage).toHaveBeenCalledWith("Operation Cancelled"); + expect(Gui.showMessage).toHaveBeenCalledWith("Operation cancelled"); }); it("shows an info message if the profile credentials were updated", async () => { @@ -799,7 +799,7 @@ describe("ProfilesUtils unit tests", () => { Buffer.from( JSON.stringify({ overrides: { - credentialManager: "My Custom Credential Manager", + credentialManager: "My Custom credential manager", }, }) ) @@ -809,7 +809,7 @@ describe("ProfilesUtils unit tests", () => { configurable: true, }); - expect(ProfilesUtils.getCredentialManagerOverride()).toBe("My Custom Credential Manager"); + expect(ProfilesUtils.getCredentialManagerOverride()).toBe("My Custom credential manager"); expect(zoweLoggerTraceSpy).toHaveBeenCalledTimes(1); }); @@ -1012,7 +1012,7 @@ describe("ProfilesUtils unit tests", () => { getTeamConfig: jest.fn().mockReturnValue({ configName: "zowe.config.json" }), getAllProfiles: jest.fn().mockReturnValue([createValidIProfile(), createAltTypeIProfile()]), } as never); - const infoMsgSpy = jest.spyOn(Gui, "infoMessage").mockResolvedValueOnce("Convert Existing Profiles" as any); + const infoMsgSpy = jest.spyOn(Gui, "infoMessage").mockResolvedValueOnce("Convert existing profiles" as any); Object.defineProperty(imperative, "ConvertMsgFmt", { value: jest.fn().mockReturnValue({ REPORT_LINE: 1, diff --git a/packages/zowe-explorer/l10n/bundle.l10n.json b/packages/zowe-explorer/l10n/bundle.l10n.json index b2e9f22c0d..8058d3142e 100644 --- a/packages/zowe-explorer/l10n/bundle.l10n.json +++ b/packages/zowe-explorer/l10n/bundle.l10n.json @@ -32,7 +32,7 @@ "User": "User", "Group": "Group", "All": "All", - "File properties": "File properties", + "File Properties": "File Properties", "Tag": "Tag", "Owner": "Owner", "Apply changes": "Apply changes", @@ -47,8 +47,8 @@ "Certificate Key File": "Certificate Key File", "Submit": "Submit", "Cancel": "Cancel", - "Zowe explorer profiles are being set as unsecured.": "Zowe explorer profiles are being set as unsecured.", - "Zowe explorer profiles are being set as secured.": "Zowe explorer profiles are being set as secured.", + "Zowe Explorer profiles are being set as unsecured.": "Zowe Explorer profiles are being set as unsecured.", + "Zowe Explorer profiles are being set as secured.": "Zowe Explorer profiles are being set as secured.", "Custom credential manager failed to activate": "Custom credential manager failed to activate", "Custom credential manager {0} found, attempting to activate./Credential manager display name": { "message": "Custom credential manager {0} found, attempting to activate.", @@ -86,7 +86,7 @@ "\"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.", - "Operation Cancelled": "Operation Cancelled", + "Operation cancelled": "Operation cancelled", "Credentials for {0} were successfully updated/Profile name": { "message": "Credentials for {0} were successfully updated", "comment": [ @@ -100,8 +100,8 @@ ] }, "Reading imperative.json failed. Will try to create file.": "Reading imperative.json failed. Will try to create file.", - "Reading imperative.json Credential Manager.\n {0}/File content": { - "message": "Reading imperative.json Credential Manager.\n {0}", + "Reading imperative.json credential manager.\n {0}/File content": { + "message": "Reading imperative.json credential manager.\n {0}", "comment": [ "File content" ] @@ -112,8 +112,8 @@ "Settings file" ] }, - "Updating imperative.json Credential Manager to {0}.\n{1}/Default credential override settingNew credential override setting": { - "message": "Updating imperative.json Credential Manager to {0}.\n{1}", + "Updating imperative.json credential manager to {0}.\n{1}/Default credential override settingNew credential override setting": { + "message": "Updating imperative.json credential manager to {0}.\n{1}", "comment": [ "Default credential override setting", "New credential override setting" @@ -125,9 +125,9 @@ "Error message" ] }, - "Zowe Profiles initialized successfully.": "Zowe Profiles initialized successfully.", - "Convert Existing Profiles": "Convert Existing Profiles", - "Tree Item is not a Zowe Explorer item.": "Tree Item is not a Zowe Explorer item.", + "Zowe profiles initialized successfully.": "Zowe profiles initialized successfully.", + "Convert existing profiles": "Convert existing profiles", + "Tree item is not a Zowe Explorer item.": "Tree item is not a Zowe Explorer item.", "Zowe Explorer": "Zowe Explorer", "Initialized logger for Zowe Explorer": "Initialized logger for Zowe Explorer", "This log file can be found at {0}/Log file location": { @@ -150,12 +150,12 @@ ] }, "All Files": "All Files", - "User dismissed the certificate wizard.": "User dismissed the certificate wizard.", + "User dismissed the Certificate Wizard.": "User dismissed the Certificate Wizard.", "Certificate Wizard": "Certificate Wizard", "Enter the path to the certificate for authenticating the connection.": "Enter the path to the certificate for authenticating the connection.", "Certificate Files": "Certificate Files", "Select Certificate": "Select Certificate", - "Enter the path to the certificate key for authenticating the connection.": "Enter the path to the certificate key for authenticating the connection.", + "Certificate Key for Authentication.": "Certificate Key for Authentication.", "Certificate Keys": "Certificate Keys", "Select Certificate Key": "Select Certificate Key", "Required parameter 'host' must not be blank.": "Required parameter 'host' must not be blank.", @@ -208,6 +208,32 @@ "Profile auth error": "Profile auth error", "Profile is not authenticated, please log in to continue": "Profile is not authenticated, please log in to continue", "Retrieving response from USS list API": "Retrieving response from USS list API", + "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.", + "Saving USS file...": "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": [ @@ -278,32 +304,6 @@ "initializeUSSFavorites.error.buttonRemove": "initializeUSSFavorites.error.buttonRemove", "File does not exist. It may have been deleted.": "File does not exist. It may have been deleted.", "Pulling from Mainframe...": "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.", - "Saving USS file...": "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": [ @@ -418,16 +418,16 @@ "Profile name" ] }, - "Error: You have Zowe Job favorites that refer to a non-existent CLI profile named: {0}.\n To resolve this, you can remove {0} from the Favorites section of Zowe Explorer's Jobs view.\n Would you like to do this now? {1}/Profile nameApplication name": { - "message": "Error: You have Zowe Job favorites that refer to a non-existent CLI profile named: {0}.\n To resolve this, you can remove {0} from the Favorites section of Zowe Explorer's Jobs view.\n Would you like to do this now? {1}", + "Error: You have Zowe job favorites that refer to a non-existent CLI profile named: {0}.\n To resolve this, you can remove {0} from the Favorites section of Zowe Explorer's Jobs view.\n Would you like to do this now? {1}/Profile nameApplication name": { + "message": "Error: You have Zowe job favorites that refer to a non-existent CLI profile named: {0}.\n To resolve this, you can remove {0} from the Favorites section of Zowe Explorer's Jobs view.\n Would you like to do this now? {1}", "comment": [ "Profile name", "Application name" ] }, "Remove": "Remove", - "This will remove all favorited Jobs items for profile {0}. Continue?/Profile name": { - "message": "This will remove all favorited Jobs items for profile {0}. Continue?", + "This will remove all favorited jobs items for profile {0}. Continue?/Profile name": { + "message": "This will remove all favorited jobs items for profile {0}. Continue?", "comment": [ "Profile name" ] @@ -441,10 +441,10 @@ "URI path" ] }, - "Polling: {0}.../Unique Spool name": { + "Polling: {0}.../Unique spool name": { "message": "Polling: {0}...", "comment": [ - "Unique Spool name" + "Unique spool name" ] }, "Filter: {0}/The new filter": { @@ -521,7 +521,7 @@ "Deleted jobs" ] }, - "Download Single Spool operation not implemented by extender. Please contact the extension developer(s).": "Download Single Spool operation not implemented by extender. Please contact the extension developer(s).", + "Download single spool operation not implemented by extender. Please contact the extension developer(s).": "Download single spool operation not implemented by extender. Please contact the extension developer(s).", "No spool files found for {0}/Spool node label": { "message": "No spool files found for {0}", "comment": [ @@ -598,29 +598,29 @@ "Profile name" ] }, - "Error: You have Zowe Data Set favorites that refer to a non-existent CLI profile named: {0}.\n To resolve this, you can remove {0} from the Favorites section of Zowe Explorer's Data Sets view.\n Would you like to do this now? {1}/Profile nameApplication name": { - "message": "Error: You have Zowe Data Set favorites that refer to a non-existent CLI profile named: {0}.\n To resolve this, you can remove {0} from the Favorites section of Zowe Explorer's Data Sets view.\n Would you like to do this now? {1}", + "Error: You have Zowe data set favorites that refer to a non-existent CLI profile named: {0}.\n To resolve this, you can remove {0} from the Favorites section of Zowe Explorer's Data Sets view.\n Would you like to do this now? {1}/Profile nameApplication name": { + "message": "Error: You have Zowe data set favorites that refer to a non-existent CLI profile named: {0}.\n To resolve this, you can remove {0} from the Favorites section of Zowe Explorer's Data Sets view.\n Would you like to do this now? {1}", "comment": [ "Profile name", "Application name" ] }, "PDS already in favorites": "PDS already in favorites", - "This will remove all favorited Data Sets items for profile {0}. Continue?/Profile name": { - "message": "This will remove all favorited Data Sets items for profile {0}. Continue?", + "This will remove all favorited data sets items for profile {0}. Continue?/Profile name": { + "message": "This will remove all favorited data sets items for profile {0}. Continue?", "comment": [ "Profile name" ] }, "Node does not exist. It may have been deleted.": "Node does not exist. It may have been deleted.", "Prompting the user for a data set pattern": "Prompting the user for a data set pattern", - "Search Data Sets: use a comma to separate multiple patterns": "Search Data Sets: use a comma to separate multiple patterns", + "Search data sets: use a comma to separate multiple patterns": "Search data sets: use a comma to separate multiple patterns", "Enter valid member name": "Enter valid member name", - "Rename operation cancelled.": "Rename operation cancelled.", - "Renaming data set {0}/Old Data Set name": { + "Rename Operation cancelled.": "Rename Operation cancelled.", + "Renaming data set {0}/Old data set name": { "message": "Renaming data set {0}", "comment": [ - "Old Data Set name" + "Old data set name" ] }, "Enter a valid data set name.": "Enter a valid data set name.", @@ -680,7 +680,7 @@ "Resetting data set templates array.": "Resetting data set templates array.", "Updating data set templates.": "Updating data set templates.", "Adding new data set template {0}.": "Adding new data set template {0}.", - "Data set template save location": "Data set template save location", + "Data Set Template Save Location": "Data Set Template Save Location", "Choose the setting location to save the data set template...": "Choose the setting location to save the data set template...", "Save as User setting": "Save as User setting", "Save as Workspace setting": "Save as Workspace setting", @@ -719,10 +719,10 @@ "You must select a profile.": "You must select a profile.", "Enter the name of the data set to copy attributes from": "Enter the name of the data set to copy attributes from", "You must enter a new data set name.": "You must enter a new data set name.", - "Allocating data set like {0}./Like Data Set name": { + "Allocating data set like {0}./Like data set name": { "message": "Allocating data set like {0}.", "comment": [ - "Like Data Set name" + "Like data set name" ] }, "Enter a name for the new data set": "Enter a name for the new data set", @@ -757,7 +757,7 @@ "Data Sets deleted" ] }, - "Name of Member": "Name of Member", + "Name of member": "Name of member", "Creating new data set member {0}/Data Set member name": { "message": "Creating new data set member {0}", "comment": [ @@ -788,7 +788,7 @@ "Document file name" ] }, - "Select the Profile to use to submit the job": "Select the Profile to use to submit the job", + "Select the profile to use to submit the job": "Select the profile to use to submit the job", "No profiles available": "No profiles available", "Session for submitting JCL was null or undefined!": "Session for submitting JCL was null or undefined!", "Job submitted {0}/Job ID and set job command": { @@ -855,7 +855,7 @@ }, "Unable to gather more information": "Unable to gather more information", "Invalid paste. Copy data set(s) first.": "Invalid paste. Copy data set(s) first.", - "Name of Data Set Member": "Name of Data Set Member", + "Name of data set member": "Name of data set member", "Copying data sets is not supported.": "Copying data sets is not supported.", "Replace": "Replace", "The data set member already exists.\nDo you want to replace it?": "The data set member already exists.\nDo you want to replace it?", @@ -883,10 +883,8 @@ "Hide Profile": "Hide Profile", "Hide profile name from tree view": "Hide profile name from tree view", "Change the Authentication Method": "Change the Authentication Method", - "Change the authentication method": "Change the authentication method", - "Log in to authentication service": "Log in to authentication service", "Log in to obtain a new token value": "Log in to obtain a new token value", - "Log out of authentication service": "Log out of authentication service", + "Log out of Authentication Service": "Log out of Authentication Service", "Log out to invalidate and remove stored token value": "Log out to invalidate and remove stored token value", "Profile {0} is using basic authentication. Choose a profile action./Profile name": { "message": "Profile {0} is using basic authentication. Choose a profile action.", @@ -949,9 +947,9 @@ }, "Reload Window": "Reload Window", "The Team configuration file has been opened in the editor. Editing or removal of profiles will need to be done manually.": "The Team configuration file has been opened in the editor. Editing or removal of profiles will need to be done manually.", - "Choose \"Create new...\" to define or select a profile to add to the DATA SETS Explorer": "Choose \"Create new...\" to define or select a profile to add to the DATA SETS Explorer", - "Choose \"Create new...\" to define or select a profile to add to the JOBS Explorer": "Choose \"Create new...\" to define or select a profile to add to the JOBS Explorer", - "Choose \"Create new...\" to define or select a profile to add to the USS Explorer": "Choose \"Create new...\" to define or select a profile to add to the USS Explorer", + "Choose \"Create new...\" to define or select a profile to add to the DATA SETS tree": "Choose \"Create new...\" to define or select a profile to add to the DATA SETS tree", + "Choose \"Create new...\" to define or select a profile to add to the JOBS tree": "Choose \"Create new...\" to define or select a profile to add to the JOBS tree", + "Choose \"Create new...\" to define or select a profile to add to the USS tree": "Choose \"Create new...\" to define or select a profile to add to the USS tree", "Profile selection has been cancelled.": "Profile selection has been cancelled.", "The profile {0} has been added to the {1} tree./chosen profiletree type": { "message": "The profile {0} has been added to the {1} tree.", @@ -1016,13 +1014,13 @@ }, "To change the authentication": "To change the authentication", "To continue in current authentication": "To continue in current authentication", - "Do you wish to change the Authentication": "Do you wish to change the Authentication", - "Cannot switch to Token-based Authentication for profile {0}.": "Cannot switch to Token-based Authentication for profile {0}.", + "Do you wish to change the authentication": "Do you wish to change the authentication", + "Cannot switch to token-based authentication for profile {0}.": "Cannot switch to token-based authentication for profile {0}.", "Login using token-based authentication service was successful for profile {0}.": "Login using token-based authentication service was successful for profile {0}.", - "Unable to switch to Token-based authentication for profile {0}.": "Unable to switch to Token-based authentication for profile {0}.", + "Unable to switch to token-based authentication for profile {0}.": "Unable to switch to token-based authentication for profile {0}.", "Login using basic authentication was successful for profile {0}.": "Login using basic authentication was successful for profile {0}.", - "Unable to switch to Basic authentication for profile {0}.": "Unable to switch to Basic authentication for profile {0}.", - "Unable to Switch Authentication for profile {0}.": "Unable to Switch Authentication for profile {0}.", + "Unable to switch to basic authentication for profile {0}.": "Unable to switch to basic authentication for profile {0}.", + "Unable to switch authentication for profile {0}.": "Unable to switch authentication for profile {0}.", "Logout from authentication service was successful for {0}./Service profile name": { "message": "Logout from authentication service was successful for {0}.", "comment": [ @@ -1099,8 +1097,8 @@ "Profile type" ] }, - "Select the ssh Profile.": "Select the ssh Profile.", - "Select the Profile to use to submit the Unix command": "Select the Profile to use to submit the Unix command", + "Select the SSH Profile.": "Select the SSH Profile.", + "Select the profile to use to submit the Unix command": "Select the profile to use to submit the Unix command", "Select a Unix command to run against {0} (An option to edit will follow)/Current work directory": { "message": "Select a Unix command to run against {0} (An option to edit will follow)", "comment": [ @@ -1117,7 +1115,7 @@ "Unix command submitted.": "Unix command submitted.", "Create a new TSO command": "Create a new TSO command", "Zowe TSO Command": "Zowe TSO Command", - "Select the Profile to use to submit the TSO command": "Select the Profile to use to submit the TSO command", + "Select the profile to use to submit the TSO command": "Select the profile to use to submit the TSO command", "Profile is invalid": "Profile is invalid", "Select a TSO command to run against {0} (An option to edit will follow)/Host name": { "message": "Select a TSO command to run against {0} (An option to edit will follow)", @@ -1135,13 +1133,13 @@ "No command entered.": "No command entered.", "TSO command submitted.": "TSO command submitted.", "No account number was supplied.": "No account number was supplied.", - "Select the TSO Profile to use for account number.": "Select the TSO Profile to use for account number.", + "Select the TSO profile to use for account number.": "Select the TSO profile to use for account number.", "Account Number": "Account Number", "Enter the account number for the TSO connection.": "Enter the account number for the TSO connection.", - "Operation Cancelled.": "Operation Cancelled.", + "Operation cancelled.": "Operation cancelled.", "Create a new MVS command": "Create a new MVS command", "Zowe MVS Command": "Zowe MVS Command", - "Select the Profile to use to submit the command": "Select the Profile to use to submit the command", + "Select the profile to use to submit the command": "Select the profile to use to submit the command", "Select an MVS command to run against {0} (An option to edit will follow)/Host name": { "message": "Select an MVS command to run against {0} (An option to edit will follow)", "comment": [ diff --git a/packages/zowe-explorer/l10n/poeditor.json b/packages/zowe-explorer/l10n/poeditor.json index 0062ee86b3..f63ce6d2c2 100644 --- a/packages/zowe-explorer/l10n/poeditor.json +++ b/packages/zowe-explorer/l10n/poeditor.json @@ -3,7 +3,7 @@ "Zowe Explorer": "" }, "description": { - "VS Code extension, powered by Zowe CLI, that streamlines interaction with mainframe data sets, USS files, and jobs": "" + "VS Code extension, powered by Zowe CLI, that streamlines interaction with mainframe Data Sets, USS files, and Jobs": "" }, "viewsContainers.activitybar": { "Zowe Explorer": "" @@ -66,7 +66,7 @@ "Add to Workspace": "" }, "removeFavProfile": { - "Remove profile from Favorites": "" + "Remove Profile from Favorites": "" }, "addSession": { "Add Profile to Data Sets View": "" @@ -177,10 +177,10 @@ "Delete Job": "" }, "runModifyCommand": { - "Issue Modify command": "" + "Issue Modify Command": "" }, "runStopCommand": { - "Issue Stop command": "" + "Issue Stop Command": "" }, "refreshJobsServer": { "Refresh": "" @@ -243,13 +243,13 @@ "Require user confirmation before submitting a job": "" }, "zowe.jobs.confirmSubmission.yourJobs": { - "Your jobs": "" + "Your Jobs": "" }, "zowe.jobs.confirmSubmission.otherUserJobs": { - "Other user jobs": "" + "Other user Jobs": "" }, "zowe.jobs.confirmSubmission.allJobs": { - "All jobs": "" + "All Jobs": "" }, "zowe.jobs.confirmSubmission.disabled": { "Disabled": "" @@ -381,16 +381,16 @@ "Paste": "" }, "jobs.sortBy": { - "Sort jobs...": "" + "Sort Jobs...": "" }, "jobs.filterBy": { - "Filter jobs...": "" + "Filter Jobs...": "" }, "ds.filterBy": { - "Filter PDS members...": "" + "Filter PDS Members...": "" }, "ds.sortBy": { - "Sort PDS members...": "" + "Sort PDS Members...": "" }, "issueUnixCmd": { "Issue Unix Command": "" @@ -449,7 +449,7 @@ "User": "", "Group": "", "All": "", - "File properties": "", + "File Properties": "", "Tag": "", "Owner": "", "Apply changes": "", @@ -464,8 +464,8 @@ "Certificate Key File": "", "Submit": "", "Cancel": "", - "Zowe explorer profiles are being set as unsecured.": "", - "Zowe explorer profiles are being set as secured.": "", + "Zowe Explorer profiles are being set as unsecured.": "", + "Zowe Explorer profiles are being set as secured.": "", "Custom credential manager failed to activate": "", "Custom credential manager {0} found, attempting to activate.": "", "Yes, globally": "", @@ -488,17 +488,17 @@ "\"Update Credentials\" operation not supported when \"autoStore\" is false": "", "Connection Name": "", "Enter a name for the connection.": "", - "Operation Cancelled": "", + "Operation cancelled": "", "Credentials for {0} were successfully updated": "", "Zowe home directory is located at {0}": "", "Reading imperative.json failed. Will try to create file.": "", - "Reading imperative.json Credential Manager.\n {0}": "", + "Reading imperative.json credential manager.\n {0}": "", "Failed to parse JSON file {0}. Will try to re-create the file.": "", - "Updating imperative.json Credential Manager to {0}.\n{1}": "", + "Updating imperative.json credential manager to {0}.\n{1}": "", "Failed to initialize Zowe folder: {0}": "", - "Zowe Profiles initialized successfully.": "", - "Convert Existing Profiles": "", - "Tree Item is not a Zowe Explorer item.": "", + "Zowe profiles initialized successfully.": "", + "Convert existing profiles": "", + "Tree item is not a Zowe Explorer item.": "", "Zowe Explorer": "", "Initialized logger for Zowe Explorer": "", "This log file can be found at {0}": "", @@ -506,12 +506,12 @@ "Update": "", "Zowe Explorer now has a VS Code logger with a default log level of INFO.\n \nIt looks like the Zowe CLI's ZOWE_APP_LOG_LEVEL={0}.\n \nWould you like Zowe Explorer to update to the the same log level?": "", "All Files": "", - "User dismissed the certificate wizard.": "", + "User dismissed the Certificate Wizard.": "", "Certificate Wizard": "", "Enter the path to the certificate for authenticating the connection.": "", "Certificate Files": "", "Select Certificate": "", - "Enter the path to the certificate key for authenticating the connection.": "", + "Certificate Key for Authentication.": "", "Certificate Keys": "", "Select Certificate Key": "", "Required parameter 'host' must not be blank.": "", @@ -534,6 +534,14 @@ "Profile auth error": "", "Profile is not authenticated, please log in to continue": "", "Retrieving response from USS list API": "", + "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.": "", + "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": "", @@ -562,14 +570,6 @@ "initializeUSSFavorites.error.buttonRemove": "", "File does not exist. It may have been deleted.": "", "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.": "", - "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": "", @@ -634,9 +634,9 @@ "Initializing profiles with jobs favorites.": "", "No jobs favorites found.": "", "Loading profile: {0} for jobs favorites": "", - "Error: You have Zowe Job favorites that refer to a non-existent CLI profile named: {0}.\n To resolve this, you can remove {0} from the Favorites section of Zowe Explorer's Jobs view.\n Would you like to do this now? {1}": "", + "Error: You have Zowe job favorites that refer to a non-existent CLI profile named: {0}.\n To resolve this, you can remove {0} from the Favorites section of Zowe Explorer's Jobs view.\n Would you like to do this now? {1}": "", "Remove": "", - "This will remove all favorited Jobs items for profile {0}. Continue?": "", + "This will remove all favorited jobs items for profile {0}. Continue?": "", "Enter a job ID": "", "Job search cancelled.": "", "The polling interval must be greater than or equal to 1000ms.": "", @@ -668,7 +668,7 @@ "Job {0} was deleted.": "", "Are you sure you want to delete the following {0} items?\nThis will permanently remove the following jobs from your system.\n\n{1}": "", "The following jobs were deleted: {0}": "", - "Download Single Spool operation not implemented by extender. Please contact the extension developer(s).": "", + "Download single spool operation not implemented by extender. Please contact the extension developer(s).": "", "No spool files found for {0}": "", "Modify Command": "", "Command response: {0}": "", @@ -698,14 +698,14 @@ "No data set favorites found.": "", "Error creating data set favorite node: {0} for profile {1}.": "", "Loading profile: {0} for data set favorites": "", - "Error: You have Zowe Data Set favorites that refer to a non-existent CLI profile named: {0}.\n To resolve this, you can remove {0} from the Favorites section of Zowe Explorer's Data Sets view.\n Would you like to do this now? {1}": "", + "Error: You have Zowe data set favorites that refer to a non-existent CLI profile named: {0}.\n To resolve this, you can remove {0} from the Favorites section of Zowe Explorer's Data Sets view.\n Would you like to do this now? {1}": "", "PDS already in favorites": "", - "This will remove all favorited Data Sets items for profile {0}. Continue?": "", + "This will remove all favorited data sets items for profile {0}. Continue?": "", "Node does not exist. It may have been deleted.": "", "Prompting the user for a data set pattern": "", - "Search Data Sets: use a comma to separate multiple patterns": "", + "Search data sets: use a comma to separate multiple patterns": "", "Enter valid member name": "", - "Rename operation cancelled.": "", + "Rename Operation cancelled.": "", "Renaming data set {0}": "", "Enter a valid data set name.": "", "all PDS members in {0}": "", @@ -720,7 +720,7 @@ "Resetting data set templates array.": "", "Updating data set templates.": "", "Adding new data set template {0}.": "", - "Data set template save location": "", + "Data Set Template Save Location": "", "Choose the setting location to save the data set template...": "", "Save as User setting": "", "Save as Workspace setting": "", @@ -755,7 +755,7 @@ "Are you sure you want to delete the following {0} item(s)?\nThis will permanently remove these data sets and/or members from your system.\n\n{1}": "", "Deleting items": "", "The following {0} item(s) were deleted: {1}": "", - "Name of Member": "", + "Name of member": "", "Creating new data set member {0}": "", "Unable to create member.": "", "Allocating new data set": "", @@ -766,7 +766,7 @@ "Attributes": "", "No editor with a document that could be submitted as JCL is currently open.": "", "Submitting as JCL in document {0}": "", - "Select the Profile to use to submit the job": "", + "Select the profile to use to submit the job": "", "No profiles available": "", "Session for submitting JCL was null or undefined!": "", "Job submitted {0}": "", @@ -785,7 +785,7 @@ "Recall of data set {0} requested.": "", "Unable to gather more information": "", "Invalid paste. Copy data set(s) first.": "", - "Name of Data Set Member": "", + "Name of data set member": "", "Copying data sets is not supported.": "", "Replace": "", "The data set member already exists.\nDo you want to replace it?": "", @@ -807,10 +807,8 @@ "Hide Profile": "", "Hide profile name from tree view": "", "Change the Authentication Method": "", - "Change the authentication method": "", - "Log in to authentication service": "", "Log in to obtain a new token value": "", - "Log out of authentication service": "", + "Log out of Authentication Service": "", "Log out to invalidate and remove stored token value": "", "Profile {0} is using basic authentication. Choose a profile action.": "", "Profile {0} is using token authentication. Choose a profile action.": "", @@ -828,9 +826,9 @@ "Internal error: Tried to call a non-existing Common API in API register: {0}": "", "Reload Window": "", "The Team configuration file has been opened in the editor. Editing or removal of profiles will need to be done manually.": "", - "Choose \"Create new...\" to define or select a profile to add to the DATA SETS Explorer": "", - "Choose \"Create new...\" to define or select a profile to add to the JOBS Explorer": "", - "Choose \"Create new...\" to define or select a profile to add to the USS Explorer": "", + "Choose \"Create new...\" to define or select a profile to add to the DATA SETS tree": "", + "Choose \"Create new...\" to define or select a profile to add to the JOBS tree": "", + "Choose \"Create new...\" to define or select a profile to add to the USS tree": "", "Profile selection has been cancelled.": "", "The profile {0} has been added to the {1} tree.": "", "Profile Type": "", @@ -848,13 +846,13 @@ "Unable to log in with {0}. {1}": "", "To change the authentication": "", "To continue in current authentication": "", - "Do you wish to change the Authentication": "", - "Cannot switch to Token-based Authentication for profile {0}.": "", + "Do you wish to change the authentication": "", + "Cannot switch to token-based authentication for profile {0}.": "", "Login using token-based authentication service was successful for profile {0}.": "", - "Unable to switch to Token-based authentication for profile {0}.": "", + "Unable to switch to token-based authentication for profile {0}.": "", "Login using basic authentication was successful for profile {0}.": "", - "Unable to switch to Basic authentication for profile {0}.": "", - "Unable to Switch Authentication for profile {0}.": "", + "Unable to switch to basic authentication for profile {0}.": "", + "Unable to switch authentication for profile {0}.": "", "Logout from authentication service was successful for {0}.": "", "Unable to log out with {0}. {1}": "", "Select the location where the config file will be initialized": "", @@ -904,15 +902,15 @@ "An SSH profile will be used for issuing UNIX commands with the profile {0}.": "", "Error checking if SSH profile type required for issuing UNIX commands, setting requirement to false for profile {0}.": "", "Enter the path of the directory in order to execute the command": "", - "Select the ssh Profile.": "", - "Select the Profile to use to submit the Unix command": "", + "Select the SSH Profile.": "", + "Select the profile to use to submit the Unix command": "", "Select a Unix command to run against {0} (An option to edit will follow)": "", "Select a Unix command to run immediately against {0}": "", "Enter or update the Unix command": "", "Unix command submitted.": "", "Create a new TSO command": "", "Zowe TSO Command": "", - "Select the Profile to use to submit the TSO command": "", + "Select the profile to use to submit the TSO command": "", "Profile is invalid": "", "Select a TSO command to run against {0} (An option to edit will follow)": "", "Select a TSO command to run immediately against {0}": "", @@ -920,13 +918,13 @@ "No command entered.": "", "TSO command submitted.": "", "No account number was supplied.": "", - "Select the TSO Profile to use for account number.": "", + "Select the TSO profile to use for account number.": "", "Account Number": "", "Enter the account number for the TSO connection.": "", - "Operation Cancelled.": "", + "Operation cancelled.": "", "Create a new MVS command": "", "Zowe MVS Command": "", - "Select the Profile to use to submit the command": "", + "Select the profile to use to submit the command": "", "Select an MVS command to run against {0} (An option to edit will follow)": "", "Select an MVS command to run immediately against {0}": "", "Enter or update the MVS command": "", diff --git a/packages/zowe-explorer/package.nls.json b/packages/zowe-explorer/package.nls.json index 871ff23d65..b8997e4ebe 100644 --- a/packages/zowe-explorer/package.nls.json +++ b/packages/zowe-explorer/package.nls.json @@ -1,6 +1,6 @@ { "displayName": "Zowe Explorer", - "description": "VS Code extension, powered by Zowe CLI, that streamlines interaction with mainframe data sets, USS files, and jobs", + "description": "VS Code extension, powered by Zowe CLI, that streamlines interaction with mainframe Data Sets, USS files, and Jobs", "viewsContainers.activitybar": "Zowe Explorer", "viewsContainers.panel.tableView": "Zowe Resources", "zowe.zosconsole": "Zowe Explorer z/OS Console", @@ -21,7 +21,7 @@ "diff.useRemote": "Use Remote", "addFavorite": "Add to Favorites", "addToWorkspace": "Add to Workspace", - "removeFavProfile": "Remove profile from Favorites", + "removeFavProfile": "Remove Profile from Favorites", "addSession": "Add Profile to Data Sets View", "createDataset": "Create New Data Set", "createMember": "Create New Member", @@ -58,8 +58,8 @@ "uss.text": "Toggle Text", "jobs.search": "Search Jobs", "deleteJob": "Delete Job", - "runModifyCommand": "Issue Modify command", - "runStopCommand": "Issue Stop command", + "runModifyCommand": "Issue Modify Command", + "runStopCommand": "Issue Stop Command", "refreshJobsServer": "Refresh", "refreshAllJobs": "Refresh Jobs View", "addJobsSession": "Add Profile to Jobs View", @@ -80,9 +80,9 @@ "zowe.uss.history": "Toggle if USS favorite files persist locally", "zowe.jobs.history": "Toggle if Jobs favorite files persist locally", "zowe.jobs.confirmSubmission": "Require user confirmation before submitting a job", - "zowe.jobs.confirmSubmission.yourJobs": "Your jobs", - "zowe.jobs.confirmSubmission.otherUserJobs": "Other user jobs", - "zowe.jobs.confirmSubmission.allJobs": "All jobs", + "zowe.jobs.confirmSubmission.yourJobs": "Your Jobs", + "zowe.jobs.confirmSubmission.otherUserJobs": "Other user Jobs", + "zowe.jobs.confirmSubmission.allJobs": "All Jobs", "zowe.jobs.confirmSubmission.disabled": "Disabled", "zowe.jobs.confirmSubmission.disabled.desc": "No confirmation dialogs will be shown when submitting a job.", "zowe.jobs.confirmSubmission.yourJobs.desc": "Shows a confirmation dialog when submitting your jobs.", @@ -126,10 +126,10 @@ "all.configInit": "Initialize Team Configuration File", "copyFile": "Copy", "pasteFile": "Paste", - "jobs.sortBy": "Sort jobs...", - "jobs.filterBy": "Filter jobs...", - "ds.filterBy": "Filter PDS members...", - "ds.sortBy": "Sort PDS members...", + "jobs.sortBy": "Sort Jobs...", + "jobs.filterBy": "Filter Jobs...", + "ds.filterBy": "Filter PDS Members...", + "ds.sortBy": "Sort PDS Members...", "issueUnixCmd": "Issue Unix Command", "selectForCompare": "Select for Compare", "copyName": "Copy Name", diff --git a/packages/zowe-explorer/src/commands/MvsCommandHandler.ts b/packages/zowe-explorer/src/commands/MvsCommandHandler.ts index d0890f4be9..b4e7f96324 100644 --- a/packages/zowe-explorer/src/commands/MvsCommandHandler.ts +++ b/packages/zowe-explorer/src/commands/MvsCommandHandler.ts @@ -75,13 +75,13 @@ export class MvsCommandHandler extends ZoweCommandProvider { const profileNamesList = ProfileManagement.getRegisteredProfileNameList(Definitions.Trees.MVS); if (profileNamesList.length) { const quickPickOptions: vscode.QuickPickOptions = { - placeHolder: vscode.l10n.t("Select the Profile to use to submit the command"), + placeHolder: vscode.l10n.t("Select the profile to use to submit the command"), ignoreFocusOut: true, canPickMany: false, }; const sesName = await Gui.showQuickPick(profileNamesList, quickPickOptions); if (sesName === undefined) { - Gui.showMessage(vscode.l10n.t("Operation Cancelled")); + Gui.showMessage(vscode.l10n.t("Operation cancelled")); return; } profile = allProfiles.filter((temprofile) => temprofile.name === sesName)[0]; diff --git a/packages/zowe-explorer/src/commands/TsoCommandHandler.ts b/packages/zowe-explorer/src/commands/TsoCommandHandler.ts index a83612f748..b230ac13f2 100644 --- a/packages/zowe-explorer/src/commands/TsoCommandHandler.ts +++ b/packages/zowe-explorer/src/commands/TsoCommandHandler.ts @@ -75,13 +75,13 @@ export class TsoCommandHandler extends ZoweCommandProvider { const profileNamesList = ProfileManagement.getRegisteredProfileNameList(Definitions.Trees.MVS); if (profileNamesList.length > 0) { const quickPickOptions: vscode.QuickPickOptions = { - placeHolder: vscode.l10n.t("Select the Profile to use to submit the TSO command"), + placeHolder: vscode.l10n.t("Select the profile to use to submit the TSO command"), ignoreFocusOut: true, canPickMany: false, }; const sesName = await Gui.showQuickPick(profileNamesList, quickPickOptions); if (sesName === undefined) { - Gui.showMessage(vscode.l10n.t("Operation Cancelled")); + Gui.showMessage(vscode.l10n.t("Operation cancelled")); return; } const allProfiles = profiles.allProfiles; @@ -243,13 +243,13 @@ export class TsoCommandHandler extends ZoweCommandProvider { }); if (tsoProfileNamesList.length) { const quickPickOptions: vscode.QuickPickOptions = { - placeHolder: vscode.l10n.t("Select the TSO Profile to use for account number."), + placeHolder: vscode.l10n.t("Select the TSO profile to use for account number."), ignoreFocusOut: true, canPickMany: false, }; const sesName = await Gui.showQuickPick(tsoProfileNamesList, quickPickOptions); if (sesName === undefined) { - Gui.showMessage(vscode.l10n.t("Operation Cancelled")); + Gui.showMessage(vscode.l10n.t("Operation cancelled")); return; } tsoProfile = tsoProfiles.filter((temprofile) => temprofile.name === sesName)[0]; @@ -301,7 +301,7 @@ export class TsoCommandHandler extends ZoweCommandProvider { }; tsoParms.account = await Gui.showInputBox(InputBoxOptions); if (!tsoParms.account) { - Gui.showMessage(vscode.l10n.t("Operation Cancelled.")); + Gui.showMessage(vscode.l10n.t("Operation cancelled.")); return; } } diff --git a/packages/zowe-explorer/src/commands/UnixCommandHandler.ts b/packages/zowe-explorer/src/commands/UnixCommandHandler.ts index 6e3fbfa72a..b3dc1c45cf 100644 --- a/packages/zowe-explorer/src/commands/UnixCommandHandler.ts +++ b/packages/zowe-explorer/src/commands/UnixCommandHandler.ts @@ -47,7 +47,7 @@ export class UnixCommandHandler extends ZoweCommandProvider { private static instance: UnixCommandHandler; private serviceProf: imperative.IProfileLoaded = undefined; private unixCmdMsgs = { - opCancelledMsg: vscode.l10n.t("Operation Cancelled"), + opCancelledMsg: vscode.l10n.t("Operation cancelled"), issueCmdNotSupportedMsg: vscode.l10n.t({ message: "Issuing commands is not supported for this profile type, {0}.", args: [this.serviceProf?.type], @@ -206,7 +206,7 @@ export class UnixCommandHandler extends ZoweCommandProvider { }); if (sshProfileNamesList.length) { const quickPickOptions: vscode.QuickPickOptions = { - placeHolder: vscode.l10n.t("Select the ssh Profile."), + placeHolder: vscode.l10n.t("Select the SSH Profile."), ignoreFocusOut: true, canPickMany: false, }; @@ -278,7 +278,7 @@ export class UnixCommandHandler extends ZoweCommandProvider { if (profileNamesList.length) { if (!sshReq) { const quickPickOptions: vscode.QuickPickOptions = { - placeHolder: vscode.l10n.t("Select the Profile to use to submit the Unix command"), + placeHolder: vscode.l10n.t("Select the profile to use to submit the Unix command"), ignoreFocusOut: true, canPickMany: false, }; diff --git a/packages/zowe-explorer/src/configuration/Profiles.ts b/packages/zowe-explorer/src/configuration/Profiles.ts index d657f7f55e..f63def5ad6 100644 --- a/packages/zowe-explorer/src/configuration/Profiles.ts +++ b/packages/zowe-explorer/src/configuration/Profiles.ts @@ -62,7 +62,7 @@ export class Profiles extends ProfilesCache { public loadedProfile: imperative.IProfileLoaded; public validProfile: Validation.ValidationType = Validation.ValidationType.INVALID; private mProfileInfo: imperative.ProfileInfo; - private profilesOpCancelled = vscode.l10n.t(`Operation Cancelled`); + private profilesOpCancelled = vscode.l10n.t(`Operation cancelled`); private manualEditMsg = vscode.l10n.t( `The Team configuration file has been opened in the editor. Editing or removal of profiles will need to be done manually.` ); @@ -345,14 +345,14 @@ export class Profiles extends ProfilesCache { let addProfilePlaceholder = ""; switch (zoweFileProvider.getTreeType()) { case PersistenceSchemaEnum.Dataset: - addProfilePlaceholder = vscode.l10n.t(`Choose "Create new..." to define or select a profile to add to the DATA SETS Explorer`); + addProfilePlaceholder = vscode.l10n.t(`Choose "Create new..." to define or select a profile to add to the DATA SETS tree`); break; case PersistenceSchemaEnum.Job: - addProfilePlaceholder = vscode.l10n.t(`Choose "Create new..." to define or select a profile to add to the JOBS Explorer`); + addProfilePlaceholder = vscode.l10n.t(`Choose "Create new..." to define or select a profile to add to the JOBS tree`); break; default: // Use USS View as default for placeholder text - addProfilePlaceholder = vscode.l10n.t(`Choose "Create new..." to define or select a profile to add to the USS Explorer`); + addProfilePlaceholder = vscode.l10n.t(`Choose "Create new..." to define or select a profile to add to the USS tree`); } if (allProfiles.length > 0) { quickpick.items = [configPick, configEdit, ...items]; @@ -883,14 +883,14 @@ export class Profiles extends ProfilesCache { description: vscode.l10n.t("To continue in current authentication"), }; qp.items = [qpItemYes, qpItemNo]; - qp.placeholder = vscode.l10n.t("Do you wish to change the Authentication"); + qp.placeholder = vscode.l10n.t("Do you wish to change the authentication"); qp.activeItems = [qpItemYes]; qp.show(); const qpSelection = await Gui.resolveQuickPick(qp); qp.hide(); if (qpSelection === undefined) { - Gui.infoMessage(vscode.l10n.t("Operation Cancelled")); + Gui.infoMessage(vscode.l10n.t("Operation cancelled")); return; } if (qpSelection.label === vscode.l10n.t("No")) { @@ -904,7 +904,7 @@ export class Profiles extends ProfilesCache { loginTokenType = await zeInstance.getCommonApi(serviceProfile).getTokenTypeName(); } catch (error) { ZoweLogger.warn(error); - Gui.errorMessage(vscode.l10n.t("Cannot switch to Token-based Authentication for profile {0}.", serviceProfile.name)); + Gui.errorMessage(vscode.l10n.t("Cannot switch to token-based authentication for profile {0}.", serviceProfile.name)); return; } switch (true) { @@ -937,7 +937,7 @@ export class Profiles extends ProfilesCache { profile: { ...node.getProfile().profile, ...updBaseProfile }, }); } else { - Gui.errorMessage(vscode.l10n.t("Unable to switch to Token-based authentication for profile {0}.", serviceProfile.name)); + Gui.errorMessage(vscode.l10n.t("Unable to switch to token-based authentication for profile {0}.", serviceProfile.name)); return; } break; @@ -956,13 +956,13 @@ export class Profiles extends ProfilesCache { await this.tokenAuthClearSecureArray(serviceProfile.name, loginTokenType); ZoweExplorerApiRegister.getInstance().onProfilesUpdateEmitter.fire(Validation.EventType.UPDATE); } else { - Gui.errorMessage(vscode.l10n.t("Unable to switch to Basic authentication for profile {0}.", serviceProfile.name)); + Gui.errorMessage(vscode.l10n.t("Unable to switch to basic authentication for profile {0}.", serviceProfile.name)); return; } break; } default: { - Gui.errorMessage(vscode.l10n.t("Unable to Switch Authentication for profile {0}.", serviceProfile.name)); + Gui.errorMessage(vscode.l10n.t("Unable to switch authentication for profile {0}.", serviceProfile.name)); } } } diff --git a/packages/zowe-explorer/src/management/ProfileManagement.ts b/packages/zowe-explorer/src/management/ProfileManagement.ts index f94e57495f..0cce149ccb 100644 --- a/packages/zowe-explorer/src/management/ProfileManagement.ts +++ b/packages/zowe-explorer/src/management/ProfileManagement.ts @@ -117,18 +117,18 @@ export class ProfileManagement { public static readonly switchAuthenticationQpItems: Record = { [ProfileManagement.AuthQpLabels.switch]: { label: `$(key) ${vscode.l10n.t("Change the Authentication Method")}`, - description: vscode.l10n.t("Change the authentication method"), + description: vscode.l10n.t("Change the Authentication Method"), }, }; public static readonly tokenAuthLoginQpItem: Record = { [ProfileManagement.AuthQpLabels.login]: { - label: `$(arrow-right) ${vscode.l10n.t("Log in to authentication service")}`, + label: `$(arrow-right) ${vscode.l10n.t("Log in to Authentication Service")}`, description: vscode.l10n.t("Log in to obtain a new token value"), }, }; public static readonly tokenAuthLogoutQpItem: Record = { [ProfileManagement.AuthQpLabels.logout]: { - label: `$(arrow-left) ${vscode.l10n.t("Log out of authentication service")}`, + label: `$(arrow-left) ${vscode.l10n.t("Log out of Authentication Service")}`, description: vscode.l10n.t("Log out to invalidate and remove stored token value"), }, }; @@ -205,7 +205,7 @@ export class ProfileManagement { break; } default: { - Gui.infoMessage(vscode.l10n.t("Operation Cancelled")); + Gui.infoMessage(vscode.l10n.t("Operation cancelled")); break; } } @@ -280,7 +280,7 @@ export class ProfileManagement { private static async handleHideProfiles(node: IZoweTreeNode): Promise { const shouldHideFromAllTrees = await Profiles.handleChangeForAllTrees(node.getLabel().toString(), false); if (shouldHideFromAllTrees === undefined) { - Gui.infoMessage(vscode.l10n.t("Operation Cancelled")); + Gui.infoMessage(vscode.l10n.t("Operation cancelled")); return; } return vscode.commands.executeCommand("zowe.removeSession", node, null, shouldHideFromAllTrees); diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetActions.ts b/packages/zowe-explorer/src/trees/dataset/DatasetActions.ts index 647ea46ce8..5fe88bb40a 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetActions.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetActions.ts @@ -39,7 +39,7 @@ export class DatasetActions { dsPartitioned: vscode.l10n.t("Partitioned Data Set: Default"), dsExtended: vscode.l10n.t("Partitioned Data Set: Extended"), dsSequential: vscode.l10n.t("Sequential Data Set"), - opCancelled: vscode.l10n.t("Operation Cancelled"), + opCancelled: vscode.l10n.t("Operation cancelled"), copyingFiles: vscode.l10n.t("Copying File(s)"), profileInvalid: vscode.l10n.t("Profile is invalid, check connection details."), allocString: vscode.l10n.t("Allocate Data Set"), @@ -359,7 +359,7 @@ export class DatasetActions { vscode.l10n.t({ message: "Allocating data set like {0}.", args: [likeDSName], - comment: ["Like Data Set name"], + comment: ["Like data set name"], }) ); @@ -652,7 +652,7 @@ export class DatasetActions { public static async createMember(parent: IZoweDatasetTreeNode, datasetProvider: Types.IZoweDatasetTreeType): Promise { ZoweLogger.trace("dataset.actions.createMember called."); const options: vscode.InputBoxOptions = { - placeHolder: vscode.l10n.t("Name of Member"), + placeHolder: vscode.l10n.t("Name of member"), validateInput: (text) => { return DatasetUtils.validateMemberName(text) === true ? null : vscode.l10n.t("Enter valid member name"); }, @@ -958,7 +958,7 @@ export class DatasetActions { const profileNamesList = ProfileManagement.getRegisteredProfileNameList(Definitions.Trees.JES); if (profileNamesList.length > 1) { const quickPickOptions: vscode.QuickPickOptions = { - placeHolder: vscode.l10n.t("Select the Profile to use to submit the job"), + placeHolder: vscode.l10n.t("Select the profile to use to submit the job"), ignoreFocusOut: true, canPickMany: false, }; @@ -1465,7 +1465,7 @@ export class DatasetActions { if (node.contextValue.includes(Constants.DS_PDS_CONTEXT)) { const inputBoxOptions: vscode.InputBoxOptions = { value: beforeMemberName, - placeHolder: vscode.l10n.t("Name of Data Set Member"), + placeHolder: vscode.l10n.t("Name of data set member"), validateInput: (text) => { return DatasetUtils.validateMemberName(text) === true ? null : vscode.l10n.t("Enter valid member name"); }, diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetTemplates.ts b/packages/zowe-explorer/src/trees/dataset/DatasetTemplates.ts index 49efd90027..9166fce89e 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetTemplates.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetTemplates.ts @@ -69,7 +69,7 @@ export class DataSetTemplates { private static promptForSaveLocation(): Thenable { const qpOptions: vscode.QuickPickOptions = { - title: vscode.l10n.t("Data set template save location"), + title: vscode.l10n.t("Data Set Template Save Location"), placeHolder: vscode.l10n.t("Choose the setting location to save the data set template..."), ignoreFocusOut: true, canPickMany: false, diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetTree.ts b/packages/zowe-explorer/src/trees/dataset/DatasetTree.ts index 5e18da8f31..b11f10af35 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetTree.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetTree.ts @@ -363,7 +363,7 @@ export class DatasetTree extends ZoweTreeProvider implemen } } catch (error) { const errMessage: string = vscode.l10n.t({ - message: `Error: You have Zowe Data Set favorites that refer to a non-existent CLI profile named: {0}. + message: `Error: You have Zowe data set favorites that refer to a non-existent CLI profile named: {0}. To resolve this, you can remove {0} from the Favorites section of Zowe Explorer's Data Sets view. Would you like to do this now? {1}`, args: [profileName, SharedUtils.getAppName()], @@ -670,7 +670,7 @@ export class DatasetTree extends ZoweTreeProvider implemen let cancelled = false; if (userSelected) { const checkConfirmation = vscode.l10n.t({ - message: "This will remove all favorited Data Sets items for profile {0}. Continue?", + message: "This will remove all favorited data sets items for profile {0}. Continue?", args: [profileName], comment: ["Profile name"], }); @@ -1019,7 +1019,7 @@ export class DatasetTree extends ZoweTreeProvider implemen } } const options: vscode.InputBoxOptions = { - prompt: vscode.l10n.t("Search Data Sets: use a comma to separate multiple patterns"), + prompt: vscode.l10n.t("Search data sets: use a comma to separate multiple patterns"), value: pattern, }; // get user input @@ -1151,7 +1151,7 @@ export class DatasetTree extends ZoweTreeProvider implemen }; let afterMemberName = await Gui.showInputBox(options); if (!afterMemberName) { - Gui.showMessage(vscode.l10n.t("Rename operation cancelled.")); + Gui.showMessage(vscode.l10n.t("Rename Operation cancelled.")); return; } afterMemberName = afterMemberName.toUpperCase(); @@ -1160,7 +1160,7 @@ export class DatasetTree extends ZoweTreeProvider implemen vscode.l10n.t({ message: "Renaming data set {0}", args: [afterMemberName], - comment: ["Old Data Set name"], + comment: ["Old data set name"], }) ); if (afterMemberName && afterMemberName !== beforeMemberName) { @@ -1206,7 +1206,7 @@ export class DatasetTree extends ZoweTreeProvider implemen }; let afterDataSetName = await Gui.showInputBox(options); if (!afterDataSetName) { - Gui.showMessage(vscode.l10n.t("Rename operation cancelled.")); + Gui.showMessage(vscode.l10n.t("Rename Operation cancelled.")); return; } afterDataSetName = afterDataSetName.toUpperCase(); @@ -1215,7 +1215,7 @@ export class DatasetTree extends ZoweTreeProvider implemen vscode.l10n.t({ message: "Renaming data set {0}", args: [afterDataSetName], - comment: ["Old Data Set name"], + comment: ["Old data set name"], }) ); if (afterDataSetName && afterDataSetName !== beforeDataSetName) { diff --git a/packages/zowe-explorer/src/trees/job/JobActions.ts b/packages/zowe-explorer/src/trees/job/JobActions.ts index 7f01135a1d..9cd315fd98 100644 --- a/packages/zowe-explorer/src/trees/job/JobActions.ts +++ b/packages/zowe-explorer/src/trees/job/JobActions.ts @@ -196,7 +196,7 @@ export class JobActions { ZoweLogger.trace("job.actions.downloadSingleSpool called."); try { if (ZoweExplorerApiRegister.getJesApi(nodes[0].getProfile()).downloadSingleSpool == null) { - throw Error(vscode.l10n.t("Download Single Spool operation not implemented by extender. Please contact the extension developer(s).")); + throw Error(vscode.l10n.t("Download single spool operation not implemented by extender. Please contact the extension developer(s).")); } const dirUri = await Gui.showOpenDialog({ openLabel: vscode.l10n.t("Select"), diff --git a/packages/zowe-explorer/src/trees/job/JobTree.ts b/packages/zowe-explorer/src/trees/job/JobTree.ts index 8a950c2ffa..3c6f5787aa 100644 --- a/packages/zowe-explorer/src/trees/job/JobTree.ts +++ b/packages/zowe-explorer/src/trees/job/JobTree.ts @@ -425,7 +425,7 @@ export class JobTree extends ZoweTreeProvider implements Types } } catch (error) { const errMessage: string = vscode.l10n.t({ - message: `Error: You have Zowe Job favorites that refer to a non-existent CLI profile named: {0}. + message: `Error: You have Zowe job favorites that refer to a non-existent CLI profile named: {0}. To resolve this, you can remove {0} from the Favorites section of Zowe Explorer's Jobs view. Would you like to do this now? {1}`, args: [profileName, SharedUtils.getAppName()], @@ -579,7 +579,7 @@ export class JobTree extends ZoweTreeProvider implements Types let cancelled = false; if (userSelected) { const checkConfirmation = vscode.l10n.t({ - message: "This will remove all favorited Jobs items for profile {0}. Continue?", + message: "This will remove all favorited jobs items for profile {0}. Continue?", args: [profileName], comment: ["Profile name"], }); @@ -1055,7 +1055,7 @@ export class JobTree extends ZoweTreeProvider implements Types `$(sync~spin) ${vscode.l10n.t({ message: "Polling: {0}...", args: [path.posix.basename(node.resourceUri.path)], - comment: ["Unique Spool name"], + comment: ["Unique spool name"], })}`, Constants.STATUS_BAR_TIMEOUT_MS ); diff --git a/packages/zowe-explorer/src/trees/uss/USSActions.ts b/packages/zowe-explorer/src/trees/uss/USSActions.ts index 0f89d1be0c..ffb3ecf4ae 100644 --- a/packages/zowe-explorer/src/trees/uss/USSActions.ts +++ b/packages/zowe-explorer/src/trees/uss/USSActions.ts @@ -177,7 +177,7 @@ export class USSActions { ussFileProvider.refreshElement(node); ussFileProvider.getTreeView().reveal(node, { expand: true, focus: true }); } else { - Gui.showMessage(vscode.l10n.t("Operation Cancelled")); + Gui.showMessage(vscode.l10n.t("Operation cancelled")); } } diff --git a/packages/zowe-explorer/src/utils/AuthUtils.ts b/packages/zowe-explorer/src/utils/AuthUtils.ts index 93c7dcfbed..2aac284b44 100644 --- a/packages/zowe-explorer/src/utils/AuthUtils.ts +++ b/packages/zowe-explorer/src/utils/AuthUtils.ts @@ -71,7 +71,7 @@ export class AuthUtils { vsCodeOpts: { modal: true }, }).then(async (selection) => { if (selection !== checkCredsButton) { - Gui.showMessage(vscode.l10n.t("Operation Cancelled")); + Gui.showMessage(vscode.l10n.t("Operation cancelled")); return; } return Constants.PROFILES_CACHE.promptCredentials(label.trim(), true); diff --git a/packages/zowe-explorer/src/utils/CertificateWizard.ts b/packages/zowe-explorer/src/utils/CertificateWizard.ts index a1de63292e..265bf9d674 100644 --- a/packages/zowe-explorer/src/utils/CertificateWizard.ts +++ b/packages/zowe-explorer/src/utils/CertificateWizard.ts @@ -34,7 +34,7 @@ class DeferredPromise { } const allFiles = vscode.l10n.t("All Files"); -const userDismissed = vscode.l10n.t("User dismissed the certificate wizard."); +const userDismissed = vscode.l10n.t("User dismissed the Certificate Wizard."); export class CertificateWizard extends WebView { private opts: CertWizardOpts; @@ -80,7 +80,7 @@ export class CertificateWizard extends WebView { case "promptCertKey": { const tempCertKey = await Gui.showOpenDialog({ - title: vscode.l10n.t("Enter the path to the certificate key for authenticating the connection."), + title: vscode.l10n.t("Certificate Key for Authentication."), defaultUri: this.opts.certKey ? vscode.Uri.file(this.opts.certKey) : undefined, filters: { [vscode.l10n.t("Certificate Keys")]: ["cer", "crt", "pem", "key"], diff --git a/packages/zowe-explorer/src/utils/ProfilesUtils.ts b/packages/zowe-explorer/src/utils/ProfilesUtils.ts index a4569a3c0c..a395b51d03 100644 --- a/packages/zowe-explorer/src/utils/ProfilesUtils.ts +++ b/packages/zowe-explorer/src/utils/ProfilesUtils.ts @@ -88,10 +88,10 @@ export class ProfilesUtils { return; } else if (!settingEnabled) { this.PROFILE_SECURITY = false; - ZoweLogger.info(vscode.l10n.t(`Zowe explorer profiles are being set as unsecured.`)); + ZoweLogger.info(vscode.l10n.t(`Zowe Explorer profiles are being set as unsecured.`)); } else { this.PROFILE_SECURITY = Constants.ZOWE_CLI_SCM; - ZoweLogger.info(vscode.l10n.t(`Zowe explorer profiles are being set as secured.`)); + ZoweLogger.info(vscode.l10n.t(`Zowe Explorer profiles are being set as secured.`)); } imperative.CredentialManagerOverride.recordCredMgrInConfig(this.PROFILE_SECURITY); } @@ -448,7 +448,7 @@ export class ProfilesUtils { ).trim(); if (!profile) { - Gui.showMessage(vscode.l10n.t("Operation Cancelled")); + Gui.showMessage(vscode.l10n.t("Operation cancelled")); return; } } @@ -506,7 +506,7 @@ export class ProfilesUtils { settings = JSON.parse(fileContent); ZoweLogger.debug( vscode.l10n.t({ - message: "Reading imperative.json Credential Manager.\n {0}", + message: "Reading imperative.json credential manager.\n {0}", args: [fileContent], comment: ["File content"], }) @@ -539,7 +539,7 @@ export class ProfilesUtils { const newData = JSON.stringify(settings, null, 2); ZoweLogger.debug( vscode.l10n.t({ - message: "Updating imperative.json Credential Manager to {0}.\n{1}", + message: "Updating imperative.json credential manager to {0}.\n{1}", args: [this.PROFILE_SECURITY, newData], comment: ["Default credential override setting", "New credential override setting"], }) @@ -567,7 +567,7 @@ export class ProfilesUtils { try { await ProfilesUtils.readConfigFromDisk(true); - ZoweLogger.info(vscode.l10n.t("Zowe Profiles initialized successfully.")); + ZoweLogger.info(vscode.l10n.t("Zowe profiles initialized successfully.")); } catch (err) { if (err instanceof imperative.ImperativeError) { await AuthUtils.errorHandling(err, undefined, err.mDetails.causeErrors); @@ -584,7 +584,7 @@ export class ProfilesUtils { "Zowe V1 profiles in use.\nZowe Explorer no longer supports V1 profiles. Choose to convert existing profiles to a team configuration or create new profiles." ); ZoweLogger.warn(v1ProfileErrorMsg); - const convertButton = vscode.l10n.t("Convert Existing Profiles"); + const convertButton = vscode.l10n.t("Convert existing profiles"); const createButton = vscode.l10n.t("Create New"); const selection = await Gui.infoMessage(v1ProfileErrorMsg, { items: [convertButton, createButton], vsCodeOpts: { modal: true } }); switch (selection) { @@ -630,7 +630,7 @@ export class ProfilesUtils { if (node instanceof ZoweTreeNode) { return node.getProfile(); } - throw new Error(vscode.l10n.t("Tree Item is not a Zowe Explorer item.")); + throw new Error(vscode.l10n.t("Tree item is not a Zowe Explorer item.")); } private static async convertV1Profs(): Promise { diff --git a/packages/zowe-explorer/src/webviews/src/edit-attributes/App.tsx b/packages/zowe-explorer/src/webviews/src/edit-attributes/App.tsx index 8388d4357e..5c01400c3f 100644 --- a/packages/zowe-explorer/src/webviews/src/edit-attributes/App.tsx +++ b/packages/zowe-explorer/src/webviews/src/edit-attributes/App.tsx @@ -166,7 +166,7 @@ export function App() { return attributes.current ? (
-

{l10n.t("File properties")}

+

{l10n.t("File Properties")}

{timestamp && (

@@ -261,7 +261,7 @@ export function App() { ) : (

-

{l10n.t("File properties")}

+

{l10n.t("File Properties")}

From 085e71dccc8ceda9ffb746c20306bf1268046966 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Wed, 6 Nov 2024 10:36:42 -0500 Subject: [PATCH 3/5] refactor: Adjust "Add to Workspace" labels and fetch resources for external URLs (#3288) * refactor: add profile prefix to workspace folders Signed-off-by: Trae Yelovich * refactor: do remote lookup before opening external URL Signed-off-by: Trae Yelovich * tests: fix failing tests Signed-off-by: Trae Yelovich * test: check that executeCommand called after remote lookup Signed-off-by: Trae Yelovich * refactor: require a non-empty path before submitting uss search Signed-off-by: Trae Yelovich * chore: run l10n prepublish Signed-off-by: Trae Yelovich * run package, nls changes Signed-off-by: Billie Simmons <49491949+JillieBeanSim@users.noreply.github.com> --------- Signed-off-by: Trae Yelovich --- .../trees/shared/SharedUtils.unit.test.ts | 12 ++++++++--- .../__unit__/trees/uss/USSTree.unit.test.ts | 2 -- .../__unit__/utils/UriHandler.unit.test.ts | 20 +++++++++++++++---- packages/zowe-explorer/l10n/bundle.l10n.json | 2 +- packages/zowe-explorer/l10n/poeditor.json | 2 +- .../src/trees/shared/SharedUtils.ts | 2 +- .../zowe-explorer/src/trees/uss/USSTree.ts | 4 ++-- .../zowe-explorer/src/utils/UriHandler.ts | 18 +++++++++++++++-- 8 files changed, 46 insertions(+), 16 deletions(-) diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/shared/SharedUtils.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/shared/SharedUtils.unit.test.ts index 5230a16358..1a2b949995 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/shared/SharedUtils.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/shared/SharedUtils.unit.test.ts @@ -564,7 +564,10 @@ describe("Shared utils unit tests - function addToWorkspace", () => { }); const updateWorkspaceFoldersMock = jest.spyOn(vscode.workspace, "updateWorkspaceFolders").mockImplementation(); SharedUtils.addToWorkspace(datasetNode, null as any); - expect(updateWorkspaceFoldersMock).toHaveBeenCalledWith(0, null, { uri: datasetNode.resourceUri, name: datasetNode.label as string }); + expect(updateWorkspaceFoldersMock).toHaveBeenCalledWith(0, null, { + uri: datasetNode.resourceUri, + name: `[sestest] ${datasetNode.label as string}`, + }); }); it("adds a USS resource to the workspace", () => { const ussNode = new ZoweUSSNode({ @@ -575,7 +578,7 @@ describe("Shared utils unit tests - function addToWorkspace", () => { }); const updateWorkspaceFoldersMock = jest.spyOn(vscode.workspace, "updateWorkspaceFolders").mockImplementation(); SharedUtils.addToWorkspace(ussNode, null as any); - expect(updateWorkspaceFoldersMock).toHaveBeenCalledWith(0, null, { uri: ussNode.resourceUri, name: ussNode.label as string }); + expect(updateWorkspaceFoldersMock).toHaveBeenCalledWith(0, null, { uri: ussNode.resourceUri, name: `[sestest] ${ussNode.fullPath}` }); }); it("adds a USS session w/ fullPath to the workspace", () => { const ussNode = new ZoweUSSNode({ @@ -625,7 +628,10 @@ describe("Shared utils unit tests - function addToWorkspace", () => { const updateWorkspaceFoldersMock = jest.spyOn(vscode.workspace, "updateWorkspaceFolders").mockImplementation(); updateWorkspaceFoldersMock.mockClear(); SharedUtils.addToWorkspace(ussNode, null as any); - expect(updateWorkspaceFoldersMock).not.toHaveBeenCalledWith(0, null, { uri: ussNode.resourceUri, name: ussNode.label as string }); + expect(updateWorkspaceFoldersMock).not.toHaveBeenCalledWith(0, null, { + uri: ussNode.resourceUri, + name: `[sestest] ${ussNode.fullPath}`, + }); workspaceFolders[Symbol.dispose](); }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSTree.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSTree.unit.test.ts index e3709ba0aa..12a9831ee4 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSTree.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSTree.unit.test.ts @@ -628,8 +628,6 @@ describe("USSTree Unit Tests - Function filterPrompt", () => { globalMocks.showInputBox.mockReturnValueOnce(undefined); await globalMocks.testTree.filterPrompt(globalMocks.testTree.mSessionNodes[1]); - expect(globalMocks.showInformationMessage.mock.calls.length).toBe(1); - expect(globalMocks.showInformationMessage.mock.calls[0][0]).toBe("You must enter a path."); }); it("Tests that filter() works on a file", async () => { diff --git a/packages/zowe-explorer/__tests__/__unit__/utils/UriHandler.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/utils/UriHandler.unit.test.ts index c6a285ed36..0e4f683200 100644 --- a/packages/zowe-explorer/__tests__/__unit__/utils/UriHandler.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/utils/UriHandler.unit.test.ts @@ -11,25 +11,37 @@ import { commands, Uri } from "vscode"; import { ZoweUriHandler } from "../../../src/utils/UriHandler"; +import { DatasetFSProvider } from "../../../src/trees/dataset/DatasetFSProvider"; describe("ZoweUriHandler", () => { function getBlockMocks() { return { - executeCommand: jest.spyOn(commands, "executeCommand"), + remoteLookupForResource: jest.spyOn(DatasetFSProvider.instance, "remoteLookupForResource"), }; } it("does nothing if the parsed query does not start with a Zowe scheme", async () => { const blockMocks = getBlockMocks(); await ZoweUriHandler.getInstance().handleUri(Uri.parse("vscode://Zowe.vscode-extension-for-zowe?blah-some-unknown-query")); - expect(blockMocks.executeCommand).not.toHaveBeenCalled(); + expect(blockMocks.remoteLookupForResource).not.toHaveBeenCalled(); }); - it("calls vscode.open with the parsed URI if a Zowe resource URI was provided", async () => { + it("calls remoteLookupForResource with the parsed URI if a Zowe resource URI was provided", async () => { const blockMocks = getBlockMocks(); const uri = Uri.parse("vscode://Zowe.vscode-extension-for-zowe?zowe-ds:/lpar.zosmf/TEST.PS"); await ZoweUriHandler.getInstance().handleUri(uri); const zoweUri = Uri.parse(uri.query); - expect(blockMocks.executeCommand).toHaveBeenCalledWith("vscode.open", zoweUri, { preview: false }); + expect(blockMocks.remoteLookupForResource).toHaveBeenCalledWith(zoweUri); + }); + + it("calls remoteLookupForResource with the parsed URI if a Zowe resource URI was provided", async () => { + const blockMocks = getBlockMocks(); + blockMocks.remoteLookupForResource.mockResolvedValue({ name: "exampleEntry" } as any); + const executeCommandSpy = jest.spyOn(commands, "executeCommand"); + const uri = Uri.parse("vscode://Zowe.vscode-extension-for-zowe?zowe-ds:/lpar.zosmf/TEST.PS"); + await ZoweUriHandler.getInstance().handleUri(uri); + const zoweUri = Uri.parse(uri.query); + expect(blockMocks.remoteLookupForResource).toHaveBeenCalledWith(zoweUri); + expect(executeCommandSpy).toHaveBeenCalledWith("vscode.open", zoweUri, { preview: false }); }); }); diff --git a/packages/zowe-explorer/l10n/bundle.l10n.json b/packages/zowe-explorer/l10n/bundle.l10n.json index 8058d3142e..fa4b07fda8 100644 --- a/packages/zowe-explorer/l10n/bundle.l10n.json +++ b/packages/zowe-explorer/l10n/bundle.l10n.json @@ -284,7 +284,7 @@ "Select a filter": "Select a filter", "No selection made. Operation cancelled.": "No selection made. Operation cancelled.", "New filter": "New filter", - "You must enter a path.": "You must enter a path.", + "Please enter a valid USS path.": "Please enter a valid USS path.", "Initializing profiles with USS favorites.": "Initializing profiles with USS favorites.", "No USS favorites found.": "No USS favorites found.", "Loading profile: {0} for USS favorites/Profile name": { diff --git a/packages/zowe-explorer/l10n/poeditor.json b/packages/zowe-explorer/l10n/poeditor.json index f63ce6d2c2..8a4694c4e7 100644 --- a/packages/zowe-explorer/l10n/poeditor.json +++ b/packages/zowe-explorer/l10n/poeditor.json @@ -561,7 +561,7 @@ "Select a filter": "", "No selection made. Operation cancelled.": "", "New filter": "", - "You must enter a path.": "", + "Please enter a valid USS path.": "", "Initializing profiles with USS favorites.": "", "No USS favorites found.": "", "Loading profile: {0} for USS favorites": "", diff --git a/packages/zowe-explorer/src/trees/shared/SharedUtils.ts b/packages/zowe-explorer/src/trees/shared/SharedUtils.ts index b68958cab8..2e224167ed 100644 --- a/packages/zowe-explorer/src/trees/shared/SharedUtils.ts +++ b/packages/zowe-explorer/src/trees/shared/SharedUtils.ts @@ -329,7 +329,7 @@ export class SharedUtils { vscode.workspace.updateWorkspaceFolders(workspaceFolders?.length ?? 0, null, { uri: resourceUri, - name: isSession ? `[${item.label as string}] ${item.fullPath}` : (item.label as string), + name: `[${item.getProfileName()}] ${SharedContext.isDatasetNode(item) ? (item.label as string) : item.fullPath}`, }); } } diff --git a/packages/zowe-explorer/src/trees/uss/USSTree.ts b/packages/zowe-explorer/src/trees/uss/USSTree.ts index cc741e35a1..7d244b5d3b 100644 --- a/packages/zowe-explorer/src/trees/uss/USSTree.ts +++ b/packages/zowe-explorer/src/trees/uss/USSTree.ts @@ -744,11 +744,11 @@ export class USSTree extends ZoweTreeProvider implements Types const options: vscode.InputBoxOptions = { placeHolder: vscode.l10n.t("New filter"), value: remotepath, + validateInput: (input: string) => (input.length > 0 ? null : vscode.l10n.t("Please enter a valid USS path.")), }; // get user input remotepath = await Gui.showInputBox(options); - if (!remotepath || remotepath.length === 0) { - Gui.showMessage(vscode.l10n.t("You must enter a path.")); + if (remotepath == null) { return; } } else { diff --git a/packages/zowe-explorer/src/utils/UriHandler.ts b/packages/zowe-explorer/src/utils/UriHandler.ts index 1ba9e89115..f53554f02f 100644 --- a/packages/zowe-explorer/src/utils/UriHandler.ts +++ b/packages/zowe-explorer/src/utils/UriHandler.ts @@ -11,6 +11,9 @@ import { commands, ProviderResult, Uri, UriHandler } from "vscode"; import { ZoweScheme } from "../../../zowe-explorer-api/src"; +import { DatasetFSProvider } from "../trees/dataset/DatasetFSProvider"; +import { UssFSProvider } from "../trees/uss/UssFSProvider"; +import { ZoweLogger } from "../tools/ZoweLogger"; export class ZoweUriHandler implements UriHandler { private static instance: ZoweUriHandler = null; @@ -26,9 +29,20 @@ export class ZoweUriHandler implements UriHandler { public handleUri(uri: Uri): ProviderResult { const parsedUri = Uri.parse(uri.query); - if (!Object.values(ZoweScheme).some((scheme) => scheme === parsedUri.scheme)) { + if (uri.scheme === ZoweScheme.Jobs || !Object.values(ZoweScheme).some((scheme) => scheme === parsedUri.scheme)) { return; } - return commands.executeCommand("vscode.open", parsedUri, { preview: false }); + + const fsProvider = parsedUri.scheme === ZoweScheme.DS ? DatasetFSProvider.instance : UssFSProvider.instance; + return fsProvider + .remoteLookupForResource(parsedUri) + .then(async (_entry) => { + await commands.executeCommand("vscode.open", parsedUri, { preview: false }); + }) + .catch((err) => { + if (err instanceof Error) { + ZoweLogger.error(`Failed to open external URL: ${err.message}`); + } + }); } } From e07e839046e858be098ca4706142ac49d6d94b4f Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Wed, 6 Nov 2024 15:10:42 -0500 Subject: [PATCH 4/5] Error correlator and troubleshoot webview (#3243) * wip: ErrorCorrelator facility and work on error handling Signed-off-by: Trae Yelovich * chore: add typedoc to ErrorCorrelation; details -> summary Signed-off-by: Trae Yelovich * wip: Error prompts and webview PoC Signed-off-by: Trae Yelovich * feat: Troubleshoot webview, move PersistentVSCodeAPI Signed-off-by: Trae Yelovich * refactor: Move TipList into component file, troubleshoot format Signed-off-by: Trae Yelovich * wip: set up test cases and rename function Signed-off-by: Trae Yelovich * tests: impl ErrorCorrelator.displayError test cases Signed-off-by: Trae Yelovich * tests: ErrorCorrelator.correlateError test cases Signed-off-by: Trae Yelovich * wip: Add more error correlations; test data set error handling Signed-off-by: Trae Yelovich * wip(ErrorCorrelator): collapsible error section, Copy Details btn Signed-off-by: Trae Yelovich * copy details button, fix summary toggle state Signed-off-by: Trae Yelovich * update copied content for copy details button Signed-off-by: Trae Yelovich * feat: support template args in error summaries Signed-off-by: Trae Yelovich * wip: update AuthUtils.errorHandling to use correlator Signed-off-by: Trae Yelovich * wip: update AuthUtils.errorHandling signature and update calls Signed-off-by: Trae Yelovich * pass template args from error context, add mvs error correlation Signed-off-by: Trae Yelovich * wip: separate function to display correlation Signed-off-by: Trae Yelovich * wip: add params to AuthUtils.errorHandling for correlator Signed-off-by: Trae Yelovich * tests: Resolve failing test cases Signed-off-by: Trae Yelovich * refactor: Use API type, then profile type for narrowing Signed-off-by: Trae Yelovich * wip: Prompt for creds when opening DS Signed-off-by: Trae Yelovich * fix(api): Fix profile references being lost when cache is refreshed (#3248) * fix(api): Fix profile references being lost when cache is refreshed Signed-off-by: Timothy Johnson * fix: Pass profile instead of profile name for updating creds Signed-off-by: Trae Yelovich --------- Signed-off-by: Timothy Johnson Signed-off-by: Trae Yelovich Co-authored-by: Trae Yelovich * Revert "wip: Prompt for creds when opening DS" This reverts commit 53e95185f8c67fd9560a0b8893e43914211a7280. Signed-off-by: Trae Yelovich * refactor: Add back error correlator changes Signed-off-by: Trae Yelovich * refactor: fix tests to handle new format Signed-off-by: Trae Yelovich * tests: TroubleshootError webview class Signed-off-by: Trae Yelovich * refactor: rename TroubleshootError.setErrorData -> sendErrorData Signed-off-by: Trae Yelovich * remaining TroubleshootError cases, add log for unknown cmd Signed-off-by: Trae Yelovich * refactor: NetworkError -> CorrelatedError; cleanup class, fix type guard Signed-off-by: Trae Yelovich * impl. correlator for FSP fns; update correlator tests Signed-off-by: Trae Yelovich * tests: resolve failing test cases Signed-off-by: Trae Yelovich * refactor: throw errs instead of return; update tests Signed-off-by: Trae Yelovich * fix delete & stat error tests, run prepublish Signed-off-by: Trae Yelovich * error handling cases for stat Signed-off-by: Trae Yelovich * refactor _handleError, avoid use of await for errors Signed-off-by: Trae Yelovich * wip: add coverage to DataSet FSP Signed-off-by: Trae Yelovich * wip: more ds/uss test cases Signed-off-by: Trae Yelovich * BaseProvider._handleError test cases Signed-off-by: Trae Yelovich * jobs test cases for error handling Signed-off-by: Trae Yelovich * chore: update changelogs Signed-off-by: Trae Yelovich * expose error correlator in extender API Signed-off-by: Trae Yelovich * chore: address changelog feedback Signed-off-by: Trae Yelovich * fix circular dep Signed-off-by: Trae Yelovich * allow extenders to contribute resources for errors Signed-off-by: Trae Yelovich * handle errors when listing files in virtual workspaces Signed-off-by: Trae Yelovich * remove check for handleError mock in listFiles test Signed-off-by: Trae Yelovich * skip dialog if no correlation found, fix missing info in webview Signed-off-by: Trae Yelovich * fix tests, update logic for returning selection Signed-off-by: Trae Yelovich * offer show log opt in first dialog if correlation not found Signed-off-by: Trae Yelovich * omit profile details from log, update failing tests Signed-off-by: Trae Yelovich * restore changes to ZoweTreeNode Signed-off-by: Trae Yelovich * move HandleErrorOpts to fs/types/abstract Signed-off-by: Trae Yelovich * remove export from ErrorContext interface Signed-off-by: Trae Yelovich * revert changes to ZoweTreeNode tests Signed-off-by: Trae Yelovich * make IApiExplorerExtender.getErrorCorrelator optional Signed-off-by: Trae Yelovich * update command count, run l10n prepublish Signed-off-by: Trae Yelovich * address duplicate errors, pass profileName as template arg Signed-off-by: Trae Yelovich * resolve failing tests from changes Signed-off-by: Trae Yelovich * refactor: use optional chaining to spread templateArgs Signed-off-by: Trae Yelovich * refactor: use dsName instead of path; rm handling in autoDetectEncoding Signed-off-by: Trae Yelovich * run package Signed-off-by: Billie Simmons <49491949+JillieBeanSim@users.noreply.github.com> * fix: propagate USS listFiles error Signed-off-by: Trae Yelovich --------- Signed-off-by: Trae Yelovich Signed-off-by: Timothy Johnson Signed-off-by: Billie Simmons <49491949+JillieBeanSim@users.noreply.github.com> Co-authored-by: Timothy Johnson Co-authored-by: Billie Simmons Co-authored-by: Billie Simmons <49491949+JillieBeanSim@users.noreply.github.com> --- packages/zowe-explorer-api/CHANGELOG.md | 1 + .../__unit__/fs/BaseProvider.unit.test.ts | 30 ++ .../profiles/ProfilesCache.unit.test.ts | 15 + .../utils/ErrorCorrelator.unit.test.ts | 148 +++++++ .../src/extend/IApiExplorerExtender.ts | 8 + .../zowe-explorer-api/src/fs/BaseProvider.ts | 21 +- .../src/fs/types/abstract.ts | 12 + .../src/profiles/ProfilesCache.ts | 3 +- .../src/utils/ErrorCorrelator.ts | 360 ++++++++++++++++++ packages/zowe-explorer-api/src/utils/index.ts | 1 + packages/zowe-explorer/CHANGELOG.md | 2 + .../commands/MvsCommandHandler.unit.test.ts | 2 +- .../commands/TsoCommandHandler.unit.test.ts | 2 +- .../commands/UnixCommandHandler.unit.test.ts | 2 +- .../commands/ZoweCommandProvider.unit.test.ts | 5 +- .../configuration/Profiles.unit.test.ts | 7 +- .../ZoweExplorerExtender.unit.test.ts | 9 +- .../__tests__/__unit__/extension.unit.test.ts | 2 + .../DatasetActions.extended.unit.test.ts | 4 +- .../trees/dataset/DatasetActions.unit.test.ts | 23 +- .../dataset/DatasetFSProvider.unit.test.ts | 170 +++++++-- .../dataset/ZoweDatasetNode.unit.test.ts | 2 +- .../trees/job/JobActions.unit.test.ts | 19 +- .../trees/job/JobFSProvider.unit.test.ts | 64 +++- .../trees/uss/UssFSProvider.unit.test.ts | 106 ++++-- .../trees/uss/ZoweUSSNode.unit.test.ts | 5 +- .../__unit__/utils/ProfilesUtils.unit.test.ts | 61 ++- .../utils/TroubleshootError.unit.test.ts | 77 ++++ packages/zowe-explorer/l10n/bundle.l10n.json | 118 ++++-- packages/zowe-explorer/l10n/poeditor.json | 28 +- .../src/commands/MvsCommandHandler.ts | 6 +- .../src/commands/TsoCommandHandler.ts | 6 +- .../src/commands/UnixCommandHandler.ts | 7 +- .../src/commands/ZoweCommandProvider.ts | 5 +- .../src/configuration/Constants.ts | 2 +- .../src/configuration/Profiles.ts | 6 +- .../src/extending/ZoweExplorerExtender.ts | 9 + .../src/trees/ZoweTreeProvider.ts | 3 +- .../src/trees/dataset/DatasetActions.ts | 54 ++- .../src/trees/dataset/DatasetFSProvider.ts | 194 +++++++--- .../src/trees/dataset/DatasetTree.ts | 5 +- .../src/trees/dataset/ZoweDatasetNode.ts | 19 +- .../zowe-explorer/src/trees/job/JobActions.ts | 20 +- .../src/trees/job/JobFSProvider.ts | 100 +++-- .../zowe-explorer/src/trees/job/JobTree.ts | 4 +- .../src/trees/job/ZoweJobNode.ts | 18 +- .../src/trees/shared/SharedInit.ts | 13 + .../zowe-explorer/src/trees/uss/USSActions.ts | 14 +- .../zowe-explorer/src/trees/uss/USSTree.ts | 9 +- .../src/trees/uss/UssFSProvider.ts | 201 +++++++--- .../src/trees/uss/ZoweUSSNode.ts | 21 +- packages/zowe-explorer/src/utils/AuthUtils.ts | 122 +++--- .../zowe-explorer/src/utils/ProfilesUtils.ts | 2 +- .../src/utils/TroubleshootError.ts | 62 +++ .../components => }/PersistentVSCodeAPI.ts | 0 .../src/webviews/src/edit-history/App.tsx | 2 +- .../PersistentTable/PersistentDataPanel.tsx | 2 +- .../PersistentAddNewHistoryItemButton.tsx | 2 +- .../PersistentClearAllButton.tsx | 2 +- .../PersistentDeleteSelectedButton.tsx | 2 +- .../PersistentRefreshButton.tsx | 2 +- .../webviews/src/troubleshoot-error/App.tsx | 46 +++ .../components/ErrorInfo.tsx | 99 +++++ .../troubleshoot-error/components/TipList.tsx | 12 + .../src/troubleshoot-error/index.html | 16 + .../webviews/src/troubleshoot-error/index.tsx | 15 + 66 files changed, 1944 insertions(+), 465 deletions(-) create mode 100644 packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts create mode 100644 packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts create mode 100644 packages/zowe-explorer/__tests__/__unit__/utils/TroubleshootError.unit.test.ts create mode 100644 packages/zowe-explorer/src/utils/TroubleshootError.ts rename packages/zowe-explorer/src/webviews/src/{edit-history/components => }/PersistentVSCodeAPI.ts (100%) create mode 100644 packages/zowe-explorer/src/webviews/src/troubleshoot-error/App.tsx create mode 100644 packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx create mode 100644 packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/TipList.tsx create mode 100644 packages/zowe-explorer/src/webviews/src/troubleshoot-error/index.html create mode 100644 packages/zowe-explorer/src/webviews/src/troubleshoot-error/index.tsx diff --git a/packages/zowe-explorer-api/CHANGELOG.md b/packages/zowe-explorer-api/CHANGELOG.md index 7d048bbc7b..93ff4635c1 100644 --- a/packages/zowe-explorer-api/CHANGELOG.md +++ b/packages/zowe-explorer-api/CHANGELOG.md @@ -19,6 +19,7 @@ All notable changes to the "zowe-explorer-api" extension will be documented in t - Zowe Explorer now includes support for the [VS Code display languages](https://code.visualstudio.com/docs/getstarted/locales) French, German, Japanese, Portuguese, and Spanish. [#3239](https://github.com/zowe/zowe-explorer-vscode/pull/3239) - Localization of strings within the webviews. [#2983](https://github.com/zowe/zowe-explorer-vscode/issues/2983) +- Leverage the new error correlation facility to provide user-friendly summaries of API and network errors. Extenders can also contribute to the correlator to provide human-readable translations of error messages, as well as tips and additional resources for how to resolve the error. [#3243](https://github.com/zowe/zowe-explorer-vscode/pull/3243) ## `3.0.1` diff --git a/packages/zowe-explorer-api/__tests__/__unit__/fs/BaseProvider.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/fs/BaseProvider.unit.test.ts index d5fb25052a..cfe6930aee 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/fs/BaseProvider.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/fs/BaseProvider.unit.test.ts @@ -13,6 +13,7 @@ import * as vscode from "vscode"; import { BaseProvider, ConflictViewSelection, DirEntry, FileEntry, ZoweScheme } from "../../../src/fs"; import { Gui } from "../../../src/globals"; import { MockedProperty } from "../../../__mocks__/mockUtils"; +import { ErrorCorrelator, ZoweExplorerApiType } from "../../../src"; function getGlobalMocks(): { [key: string]: any } { return { @@ -542,6 +543,35 @@ describe("_handleConflict", () => { expect(diffOverwriteMock).toHaveBeenCalledWith(globalMocks.testFileUri); }); }); +describe("_handleError", () => { + it("calls ErrorCorrelator.displayError to display error to user - no options provided", async () => { + const displayErrorMock = jest.spyOn(ErrorCorrelator.prototype, "displayError").mockReturnValue(new Promise((res, rej) => {})); + const prov = new (BaseProvider as any)(); + prov._handleError(new Error("example")); + expect(displayErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.All, new Error("example"), { + additionalContext: undefined, + allowRetry: false, + profileType: "any", + templateArgs: undefined, + }); + }); + it("calls ErrorCorrelator.displayError to display error to user - options provided", async () => { + const displayErrorMock = jest.spyOn(ErrorCorrelator.prototype, "displayError").mockReturnValue(new Promise((res, rej) => {})); + const prov = new (BaseProvider as any)(); + prov._handleError(new Error("example"), { + additionalContext: "Failed to list data sets", + apiType: ZoweExplorerApiType.Mvs, + profileType: "zosmf", + templateArgs: {}, + }); + expect(displayErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, new Error("example"), { + additionalContext: "Failed to list data sets", + allowRetry: false, + profileType: "zosmf", + templateArgs: {}, + }); + }); +}); describe("_relocateEntry", () => { it("returns early if the entry does not exist in the file system", () => { diff --git a/packages/zowe-explorer-api/__tests__/__unit__/profiles/ProfilesCache.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/profiles/ProfilesCache.unit.test.ts index dbfd4a0cdb..fcb8af9e33 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/profiles/ProfilesCache.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/profiles/ProfilesCache.unit.test.ts @@ -317,6 +317,21 @@ describe("ProfilesCache", () => { expect(profCache.getAllTypes()).toEqual([...profileTypes, "ssh", "base"]); }); + it("should refresh profile data for existing profile and keep object reference", async () => { + const profCache = new ProfilesCache(fakeLogger as unknown as imperative.Logger); + const profInfoSpy = jest.spyOn(profCache, "getProfileInfo").mockResolvedValue(createProfInfoMock([lpar1Profile, zftpProfile])); + await profCache.refresh(fakeApiRegister as unknown as Types.IApiRegisterClient); + expect(profCache.allProfiles.length).toEqual(2); + expect(profCache.allProfiles[0]).toMatchObject(lpar1Profile); + const oldZosmfProfile = profCache.allProfiles[0]; + const newZosmfProfile = { ...lpar1Profile, profile: lpar2Profile.profile }; + profInfoSpy.mockResolvedValue(createProfInfoMock([newZosmfProfile, zftpProfile])); + await profCache.refresh(fakeApiRegister as unknown as Types.IApiRegisterClient); + expect(profCache.allProfiles.length).toEqual(2); + expect(profCache.allProfiles[0]).toMatchObject(newZosmfProfile); + expect(oldZosmfProfile.profile).toEqual(newZosmfProfile.profile); + }); + it("should refresh profile data for and merge tokens with base profile", async () => { const profCache = new ProfilesCache(fakeLogger as unknown as imperative.Logger); jest.spyOn(profCache, "getProfileInfo").mockResolvedValue( diff --git a/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts new file mode 100644 index 0000000000..43dd68a42e --- /dev/null +++ b/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts @@ -0,0 +1,148 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { ErrorCorrelator, Gui, CorrelatedError, ZoweExplorerApiType } from "../../../src/"; +import { commands } from "vscode"; + +describe("addCorrelation", () => { + it("adds a correlation for the given API and existing profile type", () => { + const fakeErrorSummary = "Example error summary for the correlator"; + ErrorCorrelator.getInstance().addCorrelation(ZoweExplorerApiType.Mvs, "zosmf", { + errorCode: "403", + summary: fakeErrorSummary, + matches: ["Specific sequence 1234 encountered"], + }); + expect( + (ErrorCorrelator.getInstance() as any).errorMatches.get(ZoweExplorerApiType.Mvs)["zosmf"].find((err) => err.summary === fakeErrorSummary) + ).not.toBe(null); + }); + it("adds a correlation for the given API and new profile type", () => { + const fakeErrorSummary = "Example error summary for the correlator"; + ErrorCorrelator.getInstance().addCorrelation(ZoweExplorerApiType.Mvs, "fake-type", { + errorCode: "403", + summary: fakeErrorSummary, + matches: ["Specific sequence 5678 encountered"], + }); + expect( + (ErrorCorrelator.getInstance() as any).errorMatches + .get(ZoweExplorerApiType.Mvs) + ["fake-type"].find((err) => err.summary === fakeErrorSummary) + ).not.toBe(null); + }); +}); + +describe("correlateError", () => { + it("correctly correlates an error in the list of error matches", () => { + expect( + ErrorCorrelator.getInstance().correlateError(ZoweExplorerApiType.Mvs, "Client is not authorized for file access.", { + profileType: "zosmf", + }) + ).toStrictEqual( + new CorrelatedError({ + correlation: { + errorCode: "500", + summary: "Insufficient write permissions for this data set. The data set may be read-only or locked.", + tips: [ + "Check that your user or group has the appropriate permissions for this data set.", + "Ensure that the data set is not opened within a mainframe editor tool.", + ], + }, + errorCode: "500", + initialError: "Client is not authorized for file access.", + }) + ); + }); + it("returns a generic CorrelatedError if no matches are available for the given profile type", () => { + expect( + ErrorCorrelator.getInstance().correlateError(ZoweExplorerApiType.Mvs, "This is the full error message", { profileType: "nonsense" }) + ).toStrictEqual(new CorrelatedError({ initialError: "This is the full error message" })); + }); + it("returns a generic CorrelatedError with the full error details if no matches are found", () => { + expect( + ErrorCorrelator.getInstance().correlateError(ZoweExplorerApiType.Mvs, "A cryptic error with no available match", { profileType: "zosmf" }) + ).toStrictEqual(new CorrelatedError({ initialError: "A cryptic error with no available match" })); + }); +}); + +describe("displayError", () => { + it("calls correlateError to get an error correlation", async () => { + const correlateErrorMock = jest + .spyOn(ErrorCorrelator.prototype, "correlateError") + .mockReturnValueOnce( + new CorrelatedError({ correlation: { summary: "Summary of network error" }, initialError: "This is the full error message" }) + ); + const errorMessageMock = jest.spyOn(Gui, "errorMessage").mockResolvedValueOnce(undefined); + await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, "This is the full error message", { profileType: "zosmf" }); + expect(correlateErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, "This is the full error message", { profileType: "zosmf" }); + expect(errorMessageMock).toHaveBeenCalledWith("Summary of network error", { items: ["More info"] }); + }); + it("presents an additional dialog when the user selects 'More info'", async () => { + const correlateErrorMock = jest + .spyOn(ErrorCorrelator.prototype, "correlateError") + .mockReturnValueOnce( + new CorrelatedError({ correlation: { summary: "Summary of network error" }, initialError: "This is the full error message" }) + ); + const errorMessageMock = jest.spyOn(Gui, "errorMessage").mockResolvedValueOnce("More info").mockResolvedValueOnce(undefined); + await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, "This is the full error message", { profileType: "zosmf" }); + expect(correlateErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, "This is the full error message", { profileType: "zosmf" }); + expect(errorMessageMock).toHaveBeenCalledWith("Summary of network error", { items: ["More info"] }); + expect(errorMessageMock).toHaveBeenCalledWith("This is the full error message", { items: ["Show log", "Troubleshoot"] }); + }); + it("opens the Zowe Explorer output channel when the user selects 'Show log'", async () => { + const correlateErrorMock = jest + .spyOn(ErrorCorrelator.prototype, "correlateError") + .mockReturnValueOnce( + new CorrelatedError({ correlation: { summary: "Summary of network error" }, initialError: "This is the full error message" }) + ); + const errorMessageMock = jest.spyOn(Gui, "errorMessage").mockResolvedValueOnce("More info").mockResolvedValueOnce("Show log"); + const executeCommandMock = jest.spyOn(commands, "executeCommand").mockImplementation(); + await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, "This is the full error message", { profileType: "zosmf" }); + expect(correlateErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, "This is the full error message", { profileType: "zosmf" }); + expect(errorMessageMock).toHaveBeenCalledWith("Summary of network error", { items: ["More info"] }); + expect(errorMessageMock).toHaveBeenCalledWith("This is the full error message", { items: ["Show log", "Troubleshoot"] }); + expect(executeCommandMock).toHaveBeenCalledWith("zowe.revealOutputChannel"); + executeCommandMock.mockRestore(); + }); + it("opens the troubleshoot webview if the user selects 'Troubleshoot'", async () => { + const error = new CorrelatedError({ + correlation: { summary: "Summary of network error" }, + initialError: "This is the full error message", + }); + const correlateErrorMock = jest.spyOn(ErrorCorrelator.getInstance(), "correlateError").mockReturnValueOnce(error); + const errorMessageMock = jest.spyOn(Gui, "errorMessage").mockResolvedValueOnce("More info").mockResolvedValueOnce("Troubleshoot"); + const executeCommandMock = jest.spyOn(commands, "executeCommand").mockImplementation(); + await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, "This is the full error message", { profileType: "zosmf" }); + expect(correlateErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, "This is the full error message", { profileType: "zosmf" }); + expect(errorMessageMock).toHaveBeenCalledWith("Summary of network error", { items: ["More info"] }); + expect(errorMessageMock).toHaveBeenCalledWith("This is the full error message", { items: ["Show log", "Troubleshoot"] }); + expect(executeCommandMock).toHaveBeenCalledWith("zowe.troubleshootError", error, error.stack); + executeCommandMock.mockRestore(); + }); +}); + +describe("displayCorrelatedError", () => { + it("returns 'Retry' for the userResponse whenever the user selects 'Retry'", async () => { + const error = new CorrelatedError({ + correlation: { summary: "Summary of network error" }, + initialError: "This is the full error message", + }); + const correlateErrorMock = jest.spyOn(ErrorCorrelator.getInstance(), "correlateError").mockReturnValueOnce(error); + const errorMessageMock = jest.spyOn(Gui, "errorMessage").mockResolvedValueOnce("Retry"); + const handledErrorInfo = await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, "This is the full error message", { + additionalContext: "Some additional context", + allowRetry: true, + profileType: "zosmf", + }); + expect(correlateErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, "This is the full error message", { profileType: "zosmf" }); + expect(errorMessageMock).toHaveBeenCalledWith("Some additional context: Summary of network error", { items: ["Retry", "More info"] }); + expect(handledErrorInfo.userResponse).toBe("Retry"); + }); +}); diff --git a/packages/zowe-explorer-api/src/extend/IApiExplorerExtender.ts b/packages/zowe-explorer-api/src/extend/IApiExplorerExtender.ts index 89e660262f..348c9e773b 100644 --- a/packages/zowe-explorer-api/src/extend/IApiExplorerExtender.ts +++ b/packages/zowe-explorer-api/src/extend/IApiExplorerExtender.ts @@ -11,6 +11,7 @@ import * as imperative from "@zowe/imperative"; import { ProfilesCache } from "../profiles/ProfilesCache"; +import { ErrorCorrelator } from "../utils/ErrorCorrelator"; /** * This interface can be used by other VS Code Extensions to access an alternative @@ -45,4 +46,11 @@ export interface IApiExplorerExtender { * or to create them automatically if it is non-existant. */ initForZowe(type: string, profileTypeConfigurations: imperative.ICommandProfileTypeConfiguration[]): void | Promise; + + /** + * Allows extenders to contribute error correlations, providing user-friendly + * summaries of API or network errors. Also gives extenders the opportunity to + * provide tips or additional resources for errors. + */ + getErrorCorrelator?(): ErrorCorrelator; } diff --git a/packages/zowe-explorer-api/src/fs/BaseProvider.ts b/packages/zowe-explorer-api/src/fs/BaseProvider.ts index 9f1206157c..f68682f2d3 100644 --- a/packages/zowe-explorer-api/src/fs/BaseProvider.ts +++ b/packages/zowe-explorer-api/src/fs/BaseProvider.ts @@ -10,11 +10,12 @@ */ import * as vscode from "vscode"; -import { DirEntry, FileEntry, IFileSystemEntry, FS_PROVIDER_DELAY, ConflictViewSelection, DeleteMetadata } from "./types"; +import { DirEntry, FileEntry, IFileSystemEntry, FS_PROVIDER_DELAY, ConflictViewSelection, DeleteMetadata, HandleErrorOpts } from "./types"; import * as path from "path"; import { FsAbstractUtils } from "./utils"; import { Gui } from "../globals/Gui"; import { ZosEncoding } from "../tree"; +import { ErrorCorrelator, ZoweExplorerApiType } from "../utils/ErrorCorrelator"; export class BaseProvider { // eslint-disable-next-line no-magic-numbers @@ -417,6 +418,24 @@ export class BaseProvider { return entry; } + protected _handleError(err: Error, opts?: HandleErrorOpts): void { + ErrorCorrelator.getInstance() + .displayError(opts?.apiType ?? ZoweExplorerApiType.All, err, { + additionalContext: opts?.additionalContext, + allowRetry: opts?.retry?.fn != null, + profileType: opts?.profileType ?? "any", + templateArgs: opts?.templateArgs, + }) + .then(async ({ userResponse }) => { + if (userResponse === "Retry" && opts?.retry?.fn != null) { + await opts.retry.fn(...(opts?.retry.args ?? [])); + } + }) + .catch(() => { + throw err; + }); + } + protected _lookupAsDirectory(uri: vscode.Uri, silent: boolean): DirEntry { const entry = this.lookup(uri, silent); if (entry != null && !FsAbstractUtils.isDirectoryEntry(entry) && !silent) { diff --git a/packages/zowe-explorer-api/src/fs/types/abstract.ts b/packages/zowe-explorer-api/src/fs/types/abstract.ts index b26c64ba59..01a71c618d 100644 --- a/packages/zowe-explorer-api/src/fs/types/abstract.ts +++ b/packages/zowe-explorer-api/src/fs/types/abstract.ts @@ -13,6 +13,7 @@ import { Duplex } from "stream"; import { IProfileLoaded } from "@zowe/imperative"; import * as vscode from "vscode"; import { ZosEncoding } from "../../tree"; +import { ZoweExplorerApiType } from "../../utils/ErrorCorrelator"; export enum ZoweScheme { DS = "zowe-ds", @@ -147,3 +148,14 @@ export type UriFsInfo = { profileName: string; profile?: IProfileLoaded; }; + +export interface HandleErrorOpts { + retry?: { + fn: (...args: any[]) => any | PromiseLike; + args?: any[]; + }; + profileType?: string; + apiType?: ZoweExplorerApiType; + templateArgs?: Record; + additionalContext?: string; +} diff --git a/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts b/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts index 6645767c64..f6a933f2b3 100644 --- a/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts +++ b/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts @@ -184,7 +184,8 @@ export class ProfilesCache { } // Step 3: Update allProfiles list - tmpAllProfiles.push(profileFix); + const existingProfile = this.allProfiles.find((tmpProf) => tmpProf.name === prof.profName && tmpProf.type === prof.profType); + tmpAllProfiles.push(existingProfile ? Object.assign(existingProfile, profileFix) : profileFix); } allProfiles.push(...tmpAllProfiles); this.profilesByType.set(type, tmpAllProfiles); diff --git a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts new file mode 100644 index 0000000000..37874cda90 --- /dev/null +++ b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts @@ -0,0 +1,360 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { Gui } from "../globals"; +import { commands } from "vscode"; +import Mustache = require("mustache"); +import { ImperativeError } from "@zowe/imperative"; + +/** + * Error match type (substring of error, or regular expression to match against error text) + */ +type ErrorMatch = string | RegExp; + +export interface ExternalResource { + href: string; + title?: string; +} + +export interface ErrorCorrelation { + /** + * An optional error code returned from the server. + * @type {string} + */ + errorCode?: string; + /** + * One or more patterns to check for within the error message. + * @type {ErrorMatch | ErrorMatch[]} + */ + matches: ErrorMatch | ErrorMatch[]; + /** + * Human-readable, brief summary of the error that was encountered. + * @type {string} + */ + summary: string; + /** + * Troubleshooting tips for end users that encounter the given error. + * @type {string[]} + */ + tips?: string[]; + /** + * Error-specific, external resources for users to help with resolution during troubleshooting. + */ + resources?: ExternalResource[]; +} + +export interface CorrelatedErrorProps { + errorCode?: string; + correlation?: Omit; + initialError: Error | string; +} + +export interface CorrelateErrorOpts { + profileType?: string; + templateArgs?: Record; +} + +export interface DisplayErrorOpts extends CorrelateErrorOpts { + additionalContext?: string; + allowRetry?: boolean; +} + +export interface DisplayCorrelatedErrorOpts extends Omit {} + +export interface HandledErrorInfo { + correlation: CorrelatedError; + userResponse: string | undefined; +} + +/** + * Representation of the given error as a correlated error (wrapper around the `Error` class). + * + * Used to cache the error info such as tips, the match that was encountered and the full error message. + */ +export class CorrelatedError { + public errorCode?: string; + public message: string; + private wasCorrelated: boolean; + + public constructor(public properties: CorrelatedErrorProps) { + this.errorCode = properties.initialError instanceof ImperativeError ? properties.initialError.errorCode : this.properties.errorCode; + this.wasCorrelated = properties.correlation != null; + + if (this.wasCorrelated) { + this.message = this.properties.correlation.summary; + } else { + this.message = this.properties.initialError instanceof Error ? this.properties.initialError.message : this.properties.initialError; + } + } + + public get correlationFound(): boolean { + return this.wasCorrelated; + } + + public get stack(): string | undefined { + return this.initial instanceof Error ? this.initial.stack : undefined; + } + + public get initial(): Error | string { + return this.properties.initialError; + } + + public asError(): Error { + const err = new Error(this.message); + err.stack = this.stack; + return err; + } + + public toString(): string { + return this.message; + } +} + +export enum ZoweExplorerApiType { + Mvs = "mvs", + Jes = "jes", + Uss = "uss", + Command = "cmd", + /* errors that match all API types */ + All = "all", +} + +export type ErrorsForApiType = Map; +export type ApiErrors = Record; + +export class ErrorCorrelator { + private static instance: ErrorCorrelator = null; + + private errorMatches: ErrorsForApiType = new Map([ + [ + ZoweExplorerApiType.Mvs, + { + zosmf: [ + { + errorCode: "500", + matches: ["Client is not authorized for file access.", /An I\/O abend was trapped\.(.+?)\n(.+?)__code=0x0913/], + summary: "Insufficient write permissions for this data set. The data set may be read-only or locked.", + tips: [ + "Check that your user or group has the appropriate permissions for this data set.", + "Ensure that the data set is not opened within a mainframe editor tool.", + ], + }, + { + matches: ["ISPF LMINIT - data set not found."], + summary: "The specified data set cannot be found. Perhaps the data set name or member name was incorrectly specified.", + tips: ["Ensure that the data set and/or member name is correct and try again."], + }, + { + matches: ["Qualifiers cannot be longer than 8 characters."], + summary: "The given data set name/pattern {{dsName}} has a qualifier longer than 8 characters.", + tips: [ + "Each qualifier in a data set can have at most 8 characters. ".concat( + "Ensure that the given name or pattern has 8 characters or less in each qualifier." + ), + ], + }, + ], + }, + ], + [ + ZoweExplorerApiType.Uss, + { + zosmf: [ + { + errorCode: "500", + matches: ["Client is not authorized for file access."], + summary: "Insufficient write permissions for this file. The file may be read-only or locked.", + tips: [ + "Check that your user or group has the appropriate permissions for this file.", + "Ensure that the file is not in use and locked by another process on the mainframe.", + "Consider using the Edit Attributes feature with this file to update its permissions.", + ], + }, + { + matches: ["File not found."], + summary: "The specified UNIX file cannot be found. Perhaps the folder or file path was incorrectly specified.", + tips: ["Ensure that the UNIX folder or file path is correct and try again."], + }, + ], + }, + ], + [ + ZoweExplorerApiType.Jes, + { + zosmf: [ + { + matches: ["No job found for reference:"], + summary: "The job modification request specified a job that does not exist.", + tips: [], + }, + { + matches: ["Submit input data does not start with a slash"], + summary: "The first character for the submitted job is invalid - expected a slash.", + tips: ["Ensure that the input data set or file contains EBCDIC data"], + }, + { + matches: ["Job input was not recognized by system as a job"], + summary: "The job was submitted without a job statement or with unrecognized (non-JCL) content.", + }, + { + errorCode: "400", + matches: ["Value of jobid query parameter is not valid"], + summary: "The given Job ID is invalid. Please verify that the job ID is correct and try again.", + }, + ], + }, + ], + [ + ZoweExplorerApiType.All, + { + any: [ + { + errorCode: "401", + matches: ["Token is not valid or expired"], + summary: + // eslint-disable-next-line max-len + "Your connection is no longer active for profile {{profileName}}. Please log in to an authentication service to restore the connection.", + }, + { + errorCode: "401", + matches: ["Username or password are not valid or expired", "All configured authentication methods failed"], + summary: + // eslint-disable-next-line max-len + "Invalid credentials for profile {{profileName}}. Please ensure the username and password are valid or this may lead to a lock-out.", + }, + ], + }, + ], + ]); + + private constructor() {} + + public static getInstance(): ErrorCorrelator { + if (!ErrorCorrelator.instance) { + ErrorCorrelator.instance = new ErrorCorrelator(); + } + + return ErrorCorrelator.instance; + } + + /** + * Adds a new error correlation to the map of error matches. + * + * @param api The API type that corresponds with the error + * @param profileType A profile type that the error occurs within + * @param correlation The correlation info (summary, tips, etc.) + */ + public addCorrelation(api: ZoweExplorerApiType, profileType: string, correlation: ErrorCorrelation): void { + const existingMatches = this.errorMatches.get(api); + this.errorMatches.set(api, { + ...(existingMatches ?? {}), + [profileType]: [...(existingMatches?.[profileType] ?? []), correlation].filter(Boolean), + }); + } + + /** + * Attempt to correlate the error details to an error contributed to the `errorMatches` map. + * + * @param api The API type where the error was encountered + * @param profileType The profile type in use + * @param errorDetails The full error details (usually `error.message`) + * @returns A matching `CorrelatedError`, or a generic `CorrelatedError` with the full error details as the summary + */ + public correlateError(api: ZoweExplorerApiType, error: string | Error, opts?: CorrelateErrorOpts): CorrelatedError { + const errorDetails = error instanceof Error ? error.message : error; + if (!this.errorMatches.has(api)) { + return new CorrelatedError({ initialError: error }); + } + + for (const apiError of [ + ...(opts?.profileType ? this.errorMatches.get(api)?.[opts.profileType] ?? [] : []), + ...(this.errorMatches.get(api)?.any ?? []), + ...this.errorMatches.get(ZoweExplorerApiType.All).any, + ]) { + for (const match of Array.isArray(apiError.matches) ? apiError.matches : [apiError.matches]) { + if (errorDetails.toString().match(match)) { + return new CorrelatedError({ + errorCode: apiError.errorCode, + initialError: error, + correlation: { + errorCode: apiError.errorCode, + summary: opts?.templateArgs ? Mustache.render(apiError.summary, opts.templateArgs) : apiError.summary, + tips: apiError.tips, + }, + }); + } + } + } + + return new CorrelatedError({ initialError: error }); + } + + /** + * Translates a detailed error message to a user-friendly summary. + * Full error details are available through the "More info" dialog option. + * + * @param api The API type where the error was encountered + * @param profileType The profile type in use + * @param errorDetails The full error details (usually `error.message`) + * @param allowRetry Whether to allow retrying the action + * @returns The user selection ("Retry" [if enabled] or "Troubleshoot") + */ + public async displayCorrelatedError(error: CorrelatedError, opts?: DisplayCorrelatedErrorOpts): Promise { + const errorCodeStr = error.properties.errorCode ? ` (Error Code ${error.properties.errorCode})` : ""; + const userSelection = await Gui.errorMessage( + `${opts?.additionalContext ? opts.additionalContext + ": " : ""}${error.message}${errorCodeStr}`.trim(), + { + items: [opts?.allowRetry ? "Retry" : undefined, ...(error.correlationFound ? ["More info"] : ["Show log", "Troubleshoot"])].filter( + Boolean + ), + } + ); + + // If the user selected "More info", show the full error details in a dialog, + // containing "Show log" and "Troubleshoot" dialog options + let nextSelection = userSelection; + if (error.correlationFound && userSelection === "More info") { + const fullErrorMsg = error.initial instanceof Error ? error.initial.message : error.initial; + nextSelection = await Gui.errorMessage(fullErrorMsg, { + items: ["Show log", "Troubleshoot"], + }); + } + + switch (nextSelection) { + // Reveal the output channel when the "Show log" option is selected + case "Show log": + return commands.executeCommand("zowe.revealOutputChannel"); + // Show the troubleshooting webview when the "Troubleshoot" option is selected + case "Troubleshoot": + return commands.executeCommand("zowe.troubleshootError", error, error.stack); + default: + return nextSelection; + } + } + + /** + * Translates a detailed error message to a user-friendly summary. + * Full error details are available through the "More info" dialog option. + * + * @param api The API type where the error was encountered + * @param profileType The profile type in use + * @param errorDetails The full error details (usually `error.message`) + * @param allowRetry Whether to allow retrying the action + * @returns The user selection ("Retry" [if enabled] or "Troubleshoot") + */ + public async displayError(api: ZoweExplorerApiType, errorDetails: string | Error, opts?: DisplayErrorOpts): Promise { + const error = this.correlateError(api, errorDetails, { profileType: opts?.profileType, templateArgs: opts?.templateArgs }); + return { + correlation: error, + userResponse: await this.displayCorrelatedError(error, { additionalContext: opts?.additionalContext, allowRetry: opts?.allowRetry }), + }; + } +} diff --git a/packages/zowe-explorer-api/src/utils/index.ts b/packages/zowe-explorer-api/src/utils/index.ts index a89e6c2f35..bb4931a173 100644 --- a/packages/zowe-explorer-api/src/utils/index.ts +++ b/packages/zowe-explorer-api/src/utils/index.ts @@ -12,6 +12,7 @@ import { IZoweTreeNode } from "../tree"; import { workspace } from "vscode"; +export * from "./ErrorCorrelator"; export * from "./Poller"; export * from "./FileManagement"; diff --git a/packages/zowe-explorer/CHANGELOG.md b/packages/zowe-explorer/CHANGELOG.md index 84ac4e381c..1cfe85d9a1 100644 --- a/packages/zowe-explorer/CHANGELOG.md +++ b/packages/zowe-explorer/CHANGELOG.md @@ -10,6 +10,8 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen - Added expired JSON web token detection for profiles in each tree view (Data Sets, USS, Jobs). When a user performs a search on a profile, they are prompted to log in if their token expired. [#3175](https://github.com/zowe/zowe-explorer-vscode/issues/3175) - Add a data set or USS resource to a virtual workspace with the new "Add to Workspace" context menu option. [#3265](https://github.com/zowe/zowe-explorer-vscode/issues/3265) - Power users and developers can now build links to efficiently open mainframe resources in Zowe Explorer. Use the **Copy External Link** option in the context menu to get the URL for a data set or USS resource, or create a link in the format `vscode://Zowe.vscode-extension-for-zowe?`. For more information on building resource URIs, see the [FileSystemProvider wiki article](https://github.com/zowe/zowe-explorer-vscode/wiki/FileSystemProvider#file-paths-vs-uris). [#3271](https://github.com/zowe/zowe-explorer-vscode/pull/3271) +- Implemented more user-friendly error messages for API or network errors within Zowe Explorer. [#3243](https://github.com/zowe/zowe-explorer-vscode/pull/3243) +- Use the "Troubleshoot" option for certain errors to obtain additional context, tips, and resources for how to resolve the errors. [#3243](https://github.com/zowe/zowe-explorer-vscode/pull/3243) ### Bug fixes diff --git a/packages/zowe-explorer/__tests__/__unit__/commands/MvsCommandHandler.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/commands/MvsCommandHandler.unit.test.ts index 4797525890..c9f4909705 100644 --- a/packages/zowe-explorer/__tests__/__unit__/commands/MvsCommandHandler.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/commands/MvsCommandHandler.unit.test.ts @@ -280,7 +280,7 @@ describe("mvsCommandActions unit testing", () => { }); expect(showInputBox.mock.calls.length).toBe(1); expect(showErrorMessage.mock.calls.length).toBe(1); - expect(showErrorMessage.mock.calls[0][0]).toEqual("Error: fake testError"); + expect(showErrorMessage.mock.calls[0][0]).toEqual("fake testError"); }); it("tests the issueMvsCommand function user escapes the quick pick box", async () => { diff --git a/packages/zowe-explorer/__tests__/__unit__/commands/TsoCommandHandler.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/commands/TsoCommandHandler.unit.test.ts index 61764f5472..43b0ea17a9 100644 --- a/packages/zowe-explorer/__tests__/__unit__/commands/TsoCommandHandler.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/commands/TsoCommandHandler.unit.test.ts @@ -279,7 +279,7 @@ describe("TsoCommandHandler unit testing", () => { }); expect(showInputBox.mock.calls.length).toBe(1); expect(showErrorMessage.mock.calls.length).toBe(1); - expect(showErrorMessage.mock.calls[0][0]).toEqual("Error: fake testError"); + expect(showErrorMessage.mock.calls[0][0]).toEqual("fake testError"); }); it("tests the issueTsoCommand function user escapes the quick pick box", async () => { diff --git a/packages/zowe-explorer/__tests__/__unit__/commands/UnixCommandHandler.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/commands/UnixCommandHandler.unit.test.ts index dc0448c977..1f0e43a500 100644 --- a/packages/zowe-explorer/__tests__/__unit__/commands/UnixCommandHandler.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/commands/UnixCommandHandler.unit.test.ts @@ -406,7 +406,7 @@ describe("UnixCommand Actions Unit Testing", () => { }); expect(showInputBox.mock.calls.length).toBe(2); expect(showErrorMessage.mock.calls.length).toBe(1); - expect(showErrorMessage.mock.calls[0][0]).toEqual("Error: fake testError"); + expect(showErrorMessage.mock.calls[0][0]).toEqual("fake testError"); }); it("If nothing is entered in the inputbox of path", async () => { diff --git a/packages/zowe-explorer/__tests__/__unit__/commands/ZoweCommandProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/commands/ZoweCommandProvider.unit.test.ts index e9f787026f..733d2a6013 100644 --- a/packages/zowe-explorer/__tests__/__unit__/commands/ZoweCommandProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/commands/ZoweCommandProvider.unit.test.ts @@ -10,7 +10,7 @@ */ import * as vscode from "vscode"; -import { ProfilesCache, ZoweTreeNode } from "@zowe/zowe-explorer-api"; +import { ProfilesCache, ZoweExplorerApiType, ZoweTreeNode } from "@zowe/zowe-explorer-api"; import { createIProfile, createISession } from "../../__mocks__/mockCreators/shared"; import { ZoweCommandProvider } from "../../../src/commands/ZoweCommandProvider"; import { Profiles } from "../../../src/configuration/Profiles"; @@ -85,7 +85,8 @@ describe("ZoweCommandProvider Unit Tests - function checkCurrentProfile", () => expect(errorHandlingSpy).toHaveBeenCalledWith( "Profile Name " + globalMocks.testProfile.name + - " is inactive. Please check if your Zowe server is active or if the URL and port in your profile is correct." + " is inactive. Please check if your Zowe server is active or if the URL and port in your profile is correct.", + { apiType: ZoweExplorerApiType.Command, profile: globalMocks.testProfile } ); }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/configuration/Profiles.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/configuration/Profiles.unit.test.ts index 3b239989bd..27d341a883 100644 --- a/packages/zowe-explorer/__tests__/__unit__/configuration/Profiles.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/configuration/Profiles.unit.test.ts @@ -891,13 +891,14 @@ describe("Profiles Unit Tests - function validateProfile", () => { throw testError; }); const errorHandlingSpy = jest.spyOn(AuthUtils, "errorHandling"); - await Profiles.getInstance().validateProfiles({ + const profile = { name: "test1", message: "", type: "", failNotFound: false, - }); - expect(errorHandlingSpy).toHaveBeenCalledWith(testError, "test1"); + }; + await Profiles.getInstance().validateProfiles(profile); + expect(errorHandlingSpy).toHaveBeenCalledWith(testError, { profile }); }); it("should return an object with profile validation status of 'unverified' from session status if validated profiles doesn't exist", async () => { createBlockMocks(); diff --git a/packages/zowe-explorer/__tests__/__unit__/extending/ZoweExplorerExtender.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/extending/ZoweExplorerExtender.unit.test.ts index ed8411f691..bf526108c3 100644 --- a/packages/zowe-explorer/__tests__/__unit__/extending/ZoweExplorerExtender.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/extending/ZoweExplorerExtender.unit.test.ts @@ -23,7 +23,7 @@ import { ZoweLocalStorage } from "../../../src/tools/ZoweLocalStorage"; import { ZoweLogger } from "../../../src/tools/ZoweLogger"; import { UssFSProvider } from "../../../src/trees/uss/UssFSProvider"; import { ProfilesUtils } from "../../../src/utils/ProfilesUtils"; -import { FileManagement, Gui, ProfilesCache } from "@zowe/zowe-explorer-api"; +import { ErrorCorrelator, FileManagement, Gui, ProfilesCache } from "@zowe/zowe-explorer-api"; import { DatasetTree } from "../../../src/trees/dataset/DatasetTree"; import { USSTree } from "../../../src/trees/uss/USSTree"; import { JobTree } from "../../../src/trees/job/JobTree"; @@ -317,4 +317,11 @@ describe("ZoweExplorerExtender unit tests", () => { addProfTypeToSchema.mockRestore(); }); }); + + describe("getErrorCorrelator", () => { + it("returns the singleton instance of ErrorCorrelator", () => { + const blockMocks = createBlockMocks(); + expect(blockMocks.instTest.getErrorCorrelator()).toBe(ErrorCorrelator.getInstance()); + }); + }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts index d48df63991..7d038d6d65 100644 --- a/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts @@ -244,6 +244,8 @@ async function createGlobalMocks() { "zowe.compareWithSelectedReadOnly", "zowe.compareFileStarted", "zowe.copyExternalLink", + "zowe.revealOutputChannel", + "zowe.troubleshootError", "zowe.placeholderCommand", ], }; diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetActions.extended.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetActions.extended.unit.test.ts index 98ffa3e517..4e8b1af22e 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetActions.extended.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetActions.extended.unit.test.ts @@ -19,6 +19,7 @@ import { SharedActions } from "../../../../src/trees/shared/SharedActions"; import { Profiles } from "../../../../src/configuration/Profiles"; import { ZoweDatasetNode } from "../../../../src/trees/dataset/ZoweDatasetNode"; import { AuthUtils } from "../../../../src/utils/AuthUtils"; +import { ZoweExplorerApiType } from "@zowe/zowe-explorer-api"; async function createGlobalMocks() { const newMocks = { @@ -241,8 +242,7 @@ describe("mvsNodeActions", () => { jest.spyOn(mockMvsApi2, "putContents").mockRejectedValue(testError); const errHandlerSpy = jest.spyOn(AuthUtils, "errorHandling").mockImplementation(); await DatasetActions.uploadDialog(node, testTree); - - expect(errHandlerSpy).toHaveBeenCalledWith(testError, "sestest"); + expect(errHandlerSpy).toHaveBeenCalledWith(testError, { apiType: ZoweExplorerApiType.Mvs, profile: globalMocks.profileOne }); }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetActions.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetActions.unit.test.ts index 87904d1ec3..80f222d3ed 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetActions.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetActions.unit.test.ts @@ -11,7 +11,7 @@ import * as vscode from "vscode"; import * as zosfiles from "@zowe/zos-files-for-zowe-sdk"; -import { Gui, imperative, Validation, Types } from "@zowe/zowe-explorer-api"; +import { Gui, imperative, Validation, Types, ZoweExplorerApiType } from "@zowe/zowe-explorer-api"; import { DatasetFSProvider } from "../../../../src/trees/dataset/DatasetFSProvider"; import { bindMvsApi, createMvsApi } from "../../../__mocks__/mockCreators/api"; import { @@ -205,7 +205,7 @@ describe("Dataset Actions Unit Tests - Function createMember", () => { }); mocked(vscode.window.showInputBox).mockResolvedValue("testMember"); - mocked(zosfiles.Upload.bufferToDataSet).mockRejectedValueOnce(Error("test")); + mocked(zosfiles.Upload.bufferToDataSet).mockRejectedValueOnce(Error("Error when uploading to data set")); try { await DatasetActions.createMember(parent, blockMocks.testDatasetTree); @@ -213,7 +213,7 @@ describe("Dataset Actions Unit Tests - Function createMember", () => { // Prevent exception from failing test } - expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("Unable to create member. Error: test"); + expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("Error when uploading to data set", { items: ["Show log", "Troubleshoot"] }); mocked(zosfiles.Upload.bufferToDataSet).mockReset(); }); it("Checking of attempt to create member without name", async () => { @@ -768,9 +768,9 @@ describe("Dataset Actions Unit Tests - Function deleteDataset", () => { }); mocked(vscode.window.showQuickPick).mockResolvedValueOnce("Delete" as any); - jest.spyOn(vscode.workspace.fs, "delete").mockRejectedValueOnce(Error("")); + jest.spyOn(vscode.workspace.fs, "delete").mockRejectedValueOnce(Error("Deletion error")); await expect(DatasetActions.deleteDataset(node, blockMocks.testDatasetTree)).rejects.toThrow(""); - expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("Error"); + expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("Deletion error", { items: ["Show log", "Troubleshoot"] }); }); it("Checking Favorite PDS dataset deletion", async () => { createGlobalMocks(); @@ -1122,9 +1122,9 @@ describe("Dataset Actions Unit Tests - Function showAttributes", () => { await expect(DatasetActions.showAttributes(node, blockMocks.testDatasetTree)).rejects.toEqual( Error("No matching names found for query: AUSER.A1557332.A996850.TEST1") ); - expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith( - "Unable to list attributes. Error: No matching names found for query: AUSER.A1557332.A996850.TEST1" - ); + expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("No matching names found for query: AUSER.A1557332.A996850.TEST1", { + items: ["Show log", "Troubleshoot"], + }); expect(mocked(vscode.window.createWebviewPanel)).not.toHaveBeenCalled(); }); }); @@ -2394,7 +2394,7 @@ describe("Dataset Actions Unit Tests - Function createFile", () => { // do nothing } - expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("Error encountered when creating data set. Error: Generic Error"); + expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("Generic Error", { items: ["Show log", "Troubleshoot"] }); expect(mocked(vscode.workspace.getConfiguration)).toHaveBeenLastCalledWith(Constants.SETTINGS_DS_DEFAULT_PS); expect(createDataSetSpy).toHaveBeenCalledWith(zosfiles.CreateDataSetTypeEnum.DATA_SET_SEQUENTIAL, "TEST", { alcunit: "CYL", @@ -2896,7 +2896,10 @@ describe("Dataset Actions Unit Tests - Function allocateLike", () => { } expect(errorHandlingSpy).toHaveBeenCalledTimes(1); - expect(errorHandlingSpy).toHaveBeenCalledWith(errorMessage, "test", "Unable to create data set."); + expect(errorHandlingSpy).toHaveBeenCalledWith( + errorMessage, + expect.objectContaining({ apiType: ZoweExplorerApiType.Mvs, scenario: "Unable to create data set." }) + ); }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts index 54eb2acf0d..fc424552a6 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts @@ -21,6 +21,7 @@ import { FsDatasetsUtils, Gui, PdsEntry, + ZoweExplorerApiType, ZoweScheme, } from "@zowe/zowe-explorer-api"; import { MockedProperty } from "../../../__mocks__/mockUtils"; @@ -248,6 +249,19 @@ describe("fetchDatasetAtUri", () => { mvsApiMock.mockRestore(); }); + it("returns null if API call fails", async () => { + const mockMvsApi = { + getContents: jest.fn().mockRejectedValue(new Error("unknown API error")), + }; + const fakePo = { ...testEntries.ps }; + const lookupAsFileMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookupAsFile").mockReturnValueOnce(fakePo); + const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValueOnce(mockMvsApi as any); + expect(await DatasetFSProvider.instance.fetchDatasetAtUri(testUris.ps, { isConflict: true })).toBe(null); + + lookupAsFileMock.mockRestore(); + mvsApiMock.mockRestore(); + }); + it("calls _updateResourceInEditor if 'editor' is specified", async () => { const contents = "dataset contents"; const mockMvsApi = { @@ -314,21 +328,24 @@ describe("readFile", () => { fetchDatasetAtUriMock.mockRestore(); }); - it("throws an error if the entry does not exist and the error is not FileNotFound", async () => { + it("calls _handleError and throws error if an unknown error occurred during lookup", async () => { const _lookupAsFileMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookupAsFile").mockImplementationOnce((uri) => { - throw FileSystemError.FileIsADirectory(uri as Uri); + throw Error("unknown fs error"); }); + const _handleErrorMock = jest.spyOn(DatasetFSProvider.instance as any, "_handleError").mockImplementation(); let err; try { await DatasetFSProvider.instance.readFile(testUris.ps); } catch (error) { err = error; - expect(err.code).toBe("FileIsADirectory"); + expect(err.message).toBe("unknown fs error"); } expect(err).toBeDefined(); expect(_lookupAsFileMock).toHaveBeenCalledWith(testUris.ps); + expect(_handleErrorMock).toHaveBeenCalled(); _lookupAsFileMock.mockRestore(); + _handleErrorMock.mockRestore(); }); it("calls fetchDatasetAtUri if the entry has not yet been accessed", async () => { @@ -436,42 +453,38 @@ describe("writeFile", () => { lookupMock.mockRestore(); }); - it("throws an error when there is an error unrelated to etag", async () => { + it("calls _handleConflict when there is an e-tag error", async () => { const mockMvsApi = { - uploadFromBuffer: jest.fn().mockImplementation(() => { - throw new Error("Unknown error on remote system"); - }), + uploadFromBuffer: jest.fn().mockRejectedValueOnce(new Error("Rest API failure with HTTP(S) status 412")), }; - const disposeMock = jest.fn(); - const setStatusBarMsg = jest.spyOn(Gui, "setStatusBarMessage").mockReturnValueOnce({ dispose: disposeMock }); const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValueOnce(mockMvsApi as any); + const statusMsgMock = jest.spyOn(Gui, "setStatusBarMessage"); const psEntry = { ...testEntries.ps, metadata: testEntries.ps.metadata } as DsEntry; const sessionEntry = { ...testEntries.session }; sessionEntry.entries.set("USER.DATA.PS", psEntry); const lookupParentDirMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(sessionEntry); const lookupMock = jest.spyOn(DatasetFSProvider.instance as any, "lookup").mockReturnValueOnce(psEntry); + const handleConflictMock = jest.spyOn(DatasetFSProvider.instance as any, "_handleConflict").mockImplementation(); const newContents = new Uint8Array([3, 6, 9]); - await expect(DatasetFSProvider.instance.writeFile(testUris.ps, newContents, { create: false, overwrite: true })).rejects.toThrow( - "Unknown error on remote system" - ); + await DatasetFSProvider.instance.writeFile(testUris.ps, newContents, { create: false, overwrite: true }); expect(lookupParentDirMock).toHaveBeenCalledWith(testUris.ps); - expect(setStatusBarMsg).toHaveBeenCalled(); + expect(statusMsgMock).toHaveBeenCalledWith("$(sync~spin) Saving data set..."); expect(mockMvsApi.uploadFromBuffer).toHaveBeenCalledWith(Buffer.from(newContents), testEntries.ps.name, { binary: false, encoding: undefined, etag: testEntries.ps.etag, returnEtag: true, }); - expect(disposeMock).toHaveBeenCalled(); - setStatusBarMsg.mockRestore(); + expect(handleConflictMock).toHaveBeenCalled(); + handleConflictMock.mockRestore(); mvsApiMock.mockRestore(); lookupMock.mockRestore(); }); - it("calls _handleConflict when there is an e-tag error", async () => { + it("calls _handleError when there is an API error", async () => { const mockMvsApi = { - uploadFromBuffer: jest.fn().mockRejectedValueOnce(new Error("Rest API failure with HTTP(S) status 412")), + uploadFromBuffer: jest.fn().mockRejectedValueOnce(new Error("Rest API failure")), }; const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValueOnce(mockMvsApi as any); const statusMsgMock = jest.spyOn(Gui, "setStatusBarMessage"); @@ -480,9 +493,9 @@ describe("writeFile", () => { sessionEntry.entries.set("USER.DATA.PS", psEntry); const lookupParentDirMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(sessionEntry); const lookupMock = jest.spyOn(DatasetFSProvider.instance as any, "lookup").mockReturnValueOnce(psEntry); - const handleConflictMock = jest.spyOn(DatasetFSProvider.instance as any, "_handleConflict").mockImplementation(); + const handleErrorMock = jest.spyOn(DatasetFSProvider.instance as any, "_handleError").mockImplementation(); const newContents = new Uint8Array([3, 6, 9]); - await DatasetFSProvider.instance.writeFile(testUris.ps, newContents, { create: false, overwrite: true }); + await expect(DatasetFSProvider.instance.writeFile(testUris.ps, newContents, { create: false, overwrite: true })).rejects.toThrow(); expect(lookupParentDirMock).toHaveBeenCalledWith(testUris.ps); expect(statusMsgMock).toHaveBeenCalledWith("$(sync~spin) Saving data set..."); @@ -492,8 +505,8 @@ describe("writeFile", () => { etag: testEntries.ps.etag, returnEtag: true, }); - expect(handleConflictMock).toHaveBeenCalled(); - handleConflictMock.mockRestore(); + expect(handleErrorMock).toHaveBeenCalled(); + handleErrorMock.mockRestore(); mvsApiMock.mockRestore(); lookupMock.mockRestore(); }); @@ -696,6 +709,27 @@ describe("stat", () => { lookupParentDirMock.mockRestore(); mvsApiMock.mockRestore(); }); + + describe("error handling", () => { + it("API response was unsuccessful for remote lookup", async () => { + const lookupMock = jest.spyOn(DatasetFSProvider.instance as any, "lookup").mockReturnValue(testEntries.ps); + const getInfoForUriMock = jest.spyOn(FsAbstractUtils, "getInfoForUri").mockReturnValue({ + isRoot: false, + slashAfterProfilePos: testUris.ps.path.indexOf("/", 1), + profileName: "sestest", + profile: testEntries.ps.metadata.profile, + }); + const exampleError = new Error("Response unsuccessful"); + const dataSetMock = jest.fn().mockRejectedValue(exampleError); + const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValue({ + dataSet: dataSetMock, + } as any); + await expect(DatasetFSProvider.instance.stat(testUris.ps)).rejects.toThrow(); + mvsApiMock.mockRestore(); + getInfoForUriMock.mockRestore(); + lookupMock.mockRestore(); + }); + }); }); describe("fetchEntriesForDataset", () => { @@ -720,6 +754,52 @@ describe("fetchEntriesForDataset", () => { expect(allMembersMock).toHaveBeenCalled(); mvsApiMock.mockRestore(); }); + it("calls _handleError in the case of an API error", async () => { + const allMembersMock = jest.fn().mockRejectedValue(new Error("API error")); + const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValue({ + allMembers: allMembersMock, + } as any); + const _handleErrorMock = jest.spyOn(DatasetFSProvider.instance as any, "_handleError").mockImplementation(); + const fakePds = Object.assign(Object.create(Object.getPrototypeOf(testEntries.pds)), testEntries.pds); + await expect( + (DatasetFSProvider.instance as any).fetchEntriesForDataset(fakePds, testUris.pds, { + isRoot: false, + slashAfterProfilePos: testUris.pds.path.indexOf("/", 1), + profileName: "sestest", + profile: testProfile, + }) + ).rejects.toThrow(); + expect(allMembersMock).toHaveBeenCalled(); + expect(_handleErrorMock).toHaveBeenCalled(); + _handleErrorMock.mockRestore(); + mvsApiMock.mockRestore(); + }); +}); + +describe("fetchEntriesForProfile", () => { + it("calls _handleError in the case of an API error", async () => { + const dataSetsMatchingPattern = jest.fn().mockRejectedValue(new Error("API error")); + const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValue({ + dataSetsMatchingPattern, + } as any); + const fakeSession = Object.assign(Object.create(Object.getPrototypeOf(testEntries.session)), testEntries.session); + const _handleErrorMock = jest.spyOn(DatasetFSProvider.instance as any, "_handleError").mockImplementation(); + const lookupAsDirMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookupAsDirectory").mockReturnValue(fakeSession); + await (DatasetFSProvider.instance as any).fetchEntriesForProfile( + testUris.session, + { + isRoot: true, + slashAfterProfilePos: testUris.pds.path.indexOf("/", 1), + profileName: "sestest", + profile: testProfile, + }, + "PUBLIC.*" + ); + expect(_handleErrorMock).toHaveBeenCalled(); + expect(lookupAsDirMock).toHaveBeenCalled(); + _handleErrorMock.mockRestore(); + mvsApiMock.mockRestore(); + }); }); describe("fetchDataset", () => { @@ -913,6 +993,18 @@ describe("fetchDataset", () => { }); }); }); + it("calls _handleError whenever an unknown filesystem error occurs", async () => { + const lookupMock = jest.spyOn(DatasetFSProvider.instance, "lookup").mockImplementation(() => { + throw new Error("unknown fs error"); + }); + await expect((DatasetFSProvider.instance as any).fetchDataset(testUris.ps, { + isRoot: false, + slashAfterProfilePos: testUris.ps.path.indexOf("/", 1), + profileName: "sestest", + profile: testProfile, + })).rejects.toThrow(); + lookupMock.mockRestore(); + }); }); describe("delete", () => { @@ -982,23 +1074,31 @@ describe("delete", () => { const fakePs = { ...testEntries.ps }; const fakeSession = { ...testEntries.session, entries: new Map() }; fakeSession.entries.set("USER.DATA.PS", fakePs); + + const sampleError = new Error("Data set does not exist on remote"); const mockMvsApi = { - deleteDataSet: jest.fn().mockRejectedValueOnce(new Error("Data set does not exist on remote")), + deleteDataSet: jest.fn().mockRejectedValueOnce(sampleError), }; const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValueOnce(mockMvsApi as any); const _lookupMock = jest.spyOn(DatasetFSProvider.instance as any, "lookup").mockReturnValueOnce(fakePs); const _fireSoonMock = jest.spyOn(DatasetFSProvider.instance as any, "_fireSoon").mockImplementation(); - const errorMsgMock = jest.spyOn(Gui, "errorMessage").mockImplementation(); + const handleErrorMock = jest.spyOn(DatasetFSProvider.instance as any, "_handleError").mockResolvedValue(undefined); jest.spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(fakeSession); - await DatasetFSProvider.instance.delete(testUris.ps, { recursive: false }); + await expect(DatasetFSProvider.instance.delete(testUris.ps, { recursive: false })).rejects.toThrow(); expect(mockMvsApi.deleteDataSet).toHaveBeenCalledWith(fakePs.name, { responseTimeout: undefined }); expect(_lookupMock).toHaveBeenCalledWith(testUris.ps, false); expect(_fireSoonMock).toHaveBeenCalled(); - expect(errorMsgMock).toHaveBeenCalledWith("Deleting /USER.DATA.PS failed due to API error: Data set does not exist on remote"); + expect(handleErrorMock).toHaveBeenCalledWith( + sampleError, + expect.objectContaining({ + additionalContext: "Failed to delete /USER.DATA.PS", + apiType: ZoweExplorerApiType.Mvs, + profileType: "zosmf", + }) + ); expect(fakeSession.entries.has(fakePs.name)).toBe(true); mvsApiMock.mockRestore(); - errorMsgMock.mockRestore(); }); }); @@ -1064,10 +1164,10 @@ describe("rename", () => { it("displays an error message when renaming fails on the remote system", async () => { const oldPds = new PdsEntry("USER.DATA.PDS"); oldPds.metadata = testEntries.pds.metadata; + const sampleError = new Error("could not upload data set"); const mockMvsApi = { - renameDataSet: jest.fn().mockRejectedValueOnce(new Error("could not upload data set")), + renameDataSet: jest.fn().mockRejectedValueOnce(sampleError), }; - const errMsgSpy = jest.spyOn(Gui, "errorMessage"); const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValueOnce(mockMvsApi as any); const _lookupMock = jest .spyOn(DatasetFSProvider.instance as any, "lookup") @@ -1075,9 +1175,19 @@ describe("rename", () => { const _lookupParentDirectoryMock = jest .spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory") .mockReturnValueOnce({ ...testEntries.session }); - await DatasetFSProvider.instance.rename(testUris.pds, testUris.pds.with({ path: "/USER.DATA.PDS2" }), { overwrite: true }); + const handleErrorMock = jest.spyOn(DatasetFSProvider.instance as any, "_handleError").mockResolvedValue(undefined); + await expect( + DatasetFSProvider.instance.rename(testUris.pds, testUris.pds.with({ path: "/USER.DATA.PDS2" }), { overwrite: true }) + ).rejects.toThrow(); expect(mockMvsApi.renameDataSet).toHaveBeenCalledWith("USER.DATA.PDS", "USER.DATA.PDS2"); - expect(errMsgSpy).toHaveBeenCalledWith("Renaming USER.DATA.PDS failed due to API error: could not upload data set"); + expect(handleErrorMock).toHaveBeenCalledWith( + sampleError, + expect.objectContaining({ + additionalContext: "Failed to rename USER.DATA.PDS", + apiType: ZoweExplorerApiType.Mvs, + profileType: "zosmf", + }) + ); _lookupMock.mockRestore(); mvsApiMock.mockRestore(); _lookupParentDirectoryMock.mockRestore(); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/ZoweDatasetNode.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/ZoweDatasetNode.unit.test.ts index 51a17ff69a..0dde39981a 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/ZoweDatasetNode.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/ZoweDatasetNode.unit.test.ts @@ -520,7 +520,7 @@ describe("ZoweDatasetNode Unit Tests - Function node.openDs()", () => { // do nothing } - expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("Error: testError"); + expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("testError", { items: ["Show log", "Troubleshoot"] }); }); it("Checking of opening for PDS Member", async () => { diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/job/JobActions.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/job/JobActions.unit.test.ts index e45170166d..914a227dcd 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/job/JobActions.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/job/JobActions.unit.test.ts @@ -41,6 +41,7 @@ import { JobActions } from "../../../../src/trees/job/JobActions"; import { DatasetActions } from "../../../../src/trees/dataset/DatasetActions"; import { Definitions } from "../../../../src/configuration/Definitions"; import { SpoolUtils } from "../../../../src/utils/SpoolUtils"; +import { AuthUtils } from "../../../../src/utils/AuthUtils"; const activeTextEditorDocument = jest.fn(); @@ -417,8 +418,14 @@ describe("Jobs Actions Unit Tests - Function downloadJcl", () => { }); it("Checking failed attempt to download Job JCL", async () => { createGlobalMocks(); - await JobActions.downloadJcl(undefined as any); - expect(mocked(Gui.errorMessage)).toHaveBeenCalled(); + const showTextDocumentMock = jest.spyOn(vscode.workspace, "openTextDocument").mockImplementationOnce(() => { + throw new Error(); + }); + const errorHandlingMock = jest.spyOn(AuthUtils, "errorHandling").mockImplementation(); + await JobActions.downloadJcl({ getProfile: jest.fn(), job: createIJobObject() } as any); + expect(showTextDocumentMock).toHaveBeenCalled(); + expect(errorHandlingMock).toHaveBeenCalled(); + errorHandlingMock.mockRestore(); }); }); @@ -1045,6 +1052,7 @@ describe("focusing on a job in the tree view", () => { const existingJobSession = createJobSessionNode(session, profile); const datasetSessionName = existingJobSession.label as string; const jobTree = createTreeView(); + const errorHandlingMock = jest.spyOn(AuthUtils, "errorHandling").mockImplementation(); const jobTreeProvider = createJobsTree(session, submittedJob, profile, jobTree); jobTreeProvider.mSessionNodes.push(existingJobSession); const testError = new Error("focusOnJob failed"); @@ -1055,8 +1063,7 @@ describe("focusing on a job in the tree view", () => { await JobActions.focusOnJob(jobTreeProvider, datasetSessionName, submittedJob.jobid); // assert expect(mocked(jobTreeProvider.refreshElement)).toHaveBeenCalledWith(existingJobSession); - expect(mocked(Gui.errorMessage)).toHaveBeenCalled(); - expect(mocked(Gui.errorMessage).mock.calls[0][0]).toContain(testError.message); + expect(errorHandlingMock).toHaveBeenCalled(); }); it("should handle error adding a new tree view session", async () => { // arrange @@ -1066,6 +1073,7 @@ describe("focusing on a job in the tree view", () => { const newJobSession = createJobSessionNode(session, profile); const datasetSessionName = newJobSession.label as string; const jobTree = createTreeView(); + const errorHandlingMock = jest.spyOn(AuthUtils, "errorHandling").mockImplementation(); const jobTreeProvider = createJobsTree(session, submittedJob, profile, jobTree); const testError = new Error("focusOnJob failed"); jest.spyOn(jobTreeProvider, "addSession").mockRejectedValueOnce(testError); @@ -1073,8 +1081,7 @@ describe("focusing on a job in the tree view", () => { await JobActions.focusOnJob(jobTreeProvider, datasetSessionName, submittedJob.jobid); // assert expect(mocked(jobTreeProvider.addSession)).toHaveBeenCalledWith({ sessionName: datasetSessionName }); - expect(mocked(Gui.errorMessage)).toHaveBeenCalled(); - expect(mocked(Gui.errorMessage).mock.calls[0][0]).toContain(testError.message); + expect(errorHandlingMock).toHaveBeenCalled(); }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/job/JobFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/job/JobFSProvider.unit.test.ts index 641b38c44c..6e49d17d9c 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/job/JobFSProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/job/JobFSProvider.unit.test.ts @@ -111,21 +111,6 @@ describe("refreshSpool", () => { }); describe("readDirectory", () => { - it("throws an error if getJobsByParameters does not exist", async () => { - const mockJesApi = {}; - const jesApiMock = jest.spyOn(ZoweExplorerApiRegister, "getJesApi").mockReturnValueOnce(mockJesApi as any); - const lookupAsDirMock = jest.spyOn(JobFSProvider.instance as any, "_lookupAsDirectory").mockReturnValueOnce({ - ...testEntries.session, - filter: { ...testEntries.session.filter, owner: "USER", prefix: "JOB*", status: "*" }, - entries: new Map(), - } as any); - await expect(JobFSProvider.instance.readDirectory(testUris.session)).rejects.toThrow( - "Failed to fetch jobs: getJobsByParameters is not implemented for this session's JES API." - ); - expect(lookupAsDirMock).toHaveBeenCalledWith(testUris.session, false); - jesApiMock.mockRestore(); - }); - it("calls getJobsByParameters to list jobs under a session", async () => { const fakeJob2 = { ...createIJobObject(), jobid: "JOB3456" }; const mockJesApi = { @@ -168,6 +153,23 @@ describe("readDirectory", () => { expect(mockJesApi.getSpoolFiles).toHaveBeenCalledWith(testEntries.job.job?.jobname, testEntries.job.job?.jobid); jesApiMock.mockRestore(); }); + + it("throws error when API error occurs", async () => { + const mockJesApi = { + getSpoolFiles: jest.fn().mockRejectedValue(new Error("Failed to fetch spools")), + }; + const jesApiMock = jest.spyOn(ZoweExplorerApiRegister, "getJesApi").mockReturnValueOnce(mockJesApi as any); + const fakeJob = new JobEntry(testEntries.job.name); + fakeJob.job = testEntries.job.job; + const _handleErrorMock = jest.spyOn(JobFSProvider.instance as any, "_handleError").mockImplementation(); + const lookupAsDirMock = jest.spyOn(JobFSProvider.instance as any, "_lookupAsDirectory").mockReturnValueOnce(fakeJob); + await expect(JobFSProvider.instance.readDirectory(testUris.job)).rejects.toThrow(); + expect(lookupAsDirMock).toHaveBeenCalledWith(testUris.job, false); + expect(mockJesApi.getSpoolFiles).toHaveBeenCalledWith(testEntries.job.job?.jobname, testEntries.job.job?.jobid); + expect(_handleErrorMock).toHaveBeenCalled(); + jesApiMock.mockRestore(); + _handleErrorMock.mockRestore(); + }); }); describe("updateFilterForUri", () => { @@ -234,6 +236,19 @@ describe("readFile", () => { lookupAsFileMock.mockRestore(); fetchSpoolAtUriMock.mockRestore(); }); + it("throws error if an error occurred while fetching spool", async () => { + const spoolEntry = { ...testEntries.spool }; + const lookupAsFileMock = jest.spyOn(JobFSProvider.instance as any, "_lookupAsFile").mockReturnValueOnce(spoolEntry); + const _handleErrorMock = jest.spyOn(JobFSProvider.instance as any, "_handleError").mockImplementation(); + const fetchSpoolAtUriMock = jest + .spyOn(JobFSProvider.instance, "fetchSpoolAtUri") + .mockRejectedValueOnce(new Error("Failed to fetch contents for spool")); + await expect(JobFSProvider.instance.readFile(testUris.spool)).rejects.toThrow(); + expect(_handleErrorMock).toHaveBeenCalled(); + _handleErrorMock.mockRestore(); + lookupAsFileMock.mockRestore(); + fetchSpoolAtUriMock.mockRestore(); + }); }); describe("writeFile", () => { @@ -329,6 +344,25 @@ describe("delete", () => { lookupMock.mockRestore(); lookupParentDirMock.mockRestore(); }); + it("throws an error if an API error occurs during deletion", async () => { + const mockUssApi = { + deleteJob: jest.fn().mockRejectedValue(new Error("Failed to delete job")), + }; + const ussApiMock = jest.spyOn(ZoweExplorerApiRegister, "getJesApi").mockReturnValueOnce(mockUssApi as any); + const fakeJob = new JobEntry(testEntries.job.name); + fakeJob.job = testEntries.job.job; + const lookupMock = jest.spyOn(JobFSProvider.instance as any, "lookup").mockReturnValueOnce(fakeJob); + const lookupParentDirMock = jest + .spyOn(JobFSProvider.instance as any, "_lookupParentDirectory") + .mockReturnValueOnce({ ...testEntries.session }); + await expect(JobFSProvider.instance.delete(testUris.job, { recursive: true, deleteRemote: true })).rejects.toThrow(); + const jobInfo = testEntries.job.job; + expect(jobInfo).not.toBeUndefined(); + expect(mockUssApi.deleteJob).toHaveBeenCalledWith(jobInfo?.jobname || "TESTJOB", jobInfo?.jobid || "JOB12345"); + ussApiMock.mockRestore(); + lookupMock.mockRestore(); + lookupParentDirMock.mockRestore(); + }); it("does not delete a spool from the FSP and remote file system", async () => { const mockUssApi = { diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts index 7e53ac1699..7d99bf286c 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts @@ -10,7 +10,7 @@ */ import { Disposable, FilePermission, FileSystemError, FileType, TextEditor, Uri, workspace } from "vscode"; -import { BaseProvider, DirEntry, FileEntry, Gui, UssDirectory, UssFile, ZoweScheme } from "@zowe/zowe-explorer-api"; +import { BaseProvider, DirEntry, FileEntry, Gui, UssDirectory, UssFile, ZoweExplorerApiType, ZoweScheme } from "@zowe/zowe-explorer-api"; import { Profiles } from "../../../../src/configuration/Profiles"; import { createIProfile } from "../../../__mocks__/mockCreators/shared"; import { ZoweExplorerApiRegister } from "../../../../src/extending/ZoweExplorerApiRegister"; @@ -139,20 +139,22 @@ describe("move", () => { expect(await UssFSProvider.instance.move(testUris.file, newUri)).toBe(false); expect(errorMsgMock).toHaveBeenCalledWith("The 'move' function is not implemented for this USS API."); }); + it("throws an error if the API request failed", async () => { + getInfoFromUriMock.mockReturnValueOnce({ + // info for new URI + path: "/aFile2.txt", + profile: testProfile, + }); + const move = jest.fn().mockRejectedValue(new Error("error during move")); + const handleErrorMock = jest.spyOn(UssFSProvider.instance as any, "_handleError").mockImplementation(); + jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce({ move } as any); + await expect(UssFSProvider.instance.move(testUris.file, newUri)).rejects.toThrow(); + expect(handleErrorMock).toHaveBeenCalled(); + handleErrorMock.mockRestore(); + }); }); describe("listFiles", () => { - it("throws an error when called with a URI with an empty path", async () => { - await expect( - UssFSProvider.instance.listFiles( - testProfile, - Uri.from({ - scheme: ZoweScheme.USS, - path: "", - }) - ) - ).rejects.toThrow("Could not list USS files: Empty path provided in URI"); - }); it("removes '.', '..', and '...' from IZosFilesResponse items when successful", async () => { jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce({ fileList: jest.fn().mockResolvedValueOnce({ @@ -192,6 +194,12 @@ describe("listFiles", () => { }, }); }); + it("returns an unsuccessful response if an error occurred", async () => { + jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce({ + fileList: jest.fn().mockRejectedValue(new Error("error listing files")), + } as any); + await expect(UssFSProvider.instance.listFiles(testProfile, testUris.folder)).rejects.toThrow(); + }); }); describe("fetchEntries", () => { @@ -322,6 +330,23 @@ describe("fetchFileAtUri", () => { expect(fileEntry.data?.byteLength).toBe(exampleData.length); autoDetectEncodingMock.mockRestore(); }); + it("throws an error if it failed to fetch contents", async () => { + const fileEntry = { ...testEntries.file }; + const lookupAsFileMock = jest.spyOn((UssFSProvider as any).prototype, "_lookupAsFile").mockReturnValueOnce(fileEntry); + const autoDetectEncodingMock = jest.spyOn(UssFSProvider.instance, "autoDetectEncoding").mockImplementation(); + jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce({ + getContents: jest.fn().mockRejectedValue(new Error("error retrieving contents")), + } as any); + + const _handleErrorMock = jest.spyOn(UssFSProvider.instance as any, "_handleError").mockImplementation(); + await expect(UssFSProvider.instance.fetchFileAtUri(testUris.file)).rejects.toThrow(); + + expect(lookupAsFileMock).toHaveBeenCalledWith(testUris.file); + expect(autoDetectEncodingMock).toHaveBeenCalledWith(fileEntry); + expect(_handleErrorMock).toHaveBeenCalled(); + autoDetectEncodingMock.mockRestore(); + _handleErrorMock.mockRestore(); + }); it("calls getContents to get the data for a file entry with encoding", async () => { const fileEntry = { ...testEntries.file }; const lookupAsFileMock = jest.spyOn((UssFSProvider as any).prototype, "_lookupAsFile").mockReturnValueOnce(fileEntry); @@ -428,6 +453,17 @@ describe("autoDetectEncoding", () => { } as any); }); + it("throws error if getTag call fails", async () => { + getTagMock.mockRejectedValueOnce(new Error("error fetching tag")); + const testEntry = new UssFile("testFile"); + testEntry.metadata = { + path: "/testFile", + profile: testProfile, + }; + await expect(UssFSProvider.instance.autoDetectEncoding(testEntry)).rejects.toThrow(); + expect(getTagMock).toHaveBeenCalledTimes(1); + }); + it("sets encoding if file tagged as binary", async () => { getTagMock.mockResolvedValueOnce("binary"); const testEntry = new UssFile("testFile"); @@ -643,11 +679,12 @@ describe("writeFile", () => { ussApiMock.mockRestore(); }); - it("throws an error when there is an error unrelated to etag", async () => { + it("throws an error when an unknown API error occurs", async () => { const mockUssApi = { - uploadFromBuffer: jest.fn().mockRejectedValueOnce(new Error("Unknown error on remote system")), + uploadFromBuffer: jest.fn().mockRejectedValueOnce(new Error("Rest API failure")), }; const ussApiMock = jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce(mockUssApi as any); + const statusMsgMock = jest.spyOn(Gui, "setStatusBarMessage"); const folder = { ...testEntries.folder, entries: new Map([[testEntries.file.name, { ...testEntries.file }]]), @@ -655,11 +692,22 @@ describe("writeFile", () => { const lookupParentDirMock = jest.spyOn(UssFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(folder); const autoDetectEncodingMock = jest.spyOn(UssFSProvider.instance, "autoDetectEncoding").mockResolvedValue(undefined); const newContents = new Uint8Array([3, 6, 9]); - await expect(UssFSProvider.instance.writeFile(testUris.file, newContents, { create: false, overwrite: true })).rejects.toThrow( - "Unknown error on remote system" - ); + const handleConflictMock = jest.spyOn(UssFSProvider.instance as any, "_handleConflict").mockImplementation(); + const _handleErrorMock = jest.spyOn(UssFSProvider.instance as any, "_handleError").mockImplementation(); + await expect(UssFSProvider.instance.writeFile(testUris.file, newContents, { create: false, overwrite: true })).rejects.toThrow(); - lookupParentDirMock.mockRestore(); + expect(lookupParentDirMock).toHaveBeenCalledWith(testUris.file); + expect(statusMsgMock).toHaveBeenCalledWith("$(sync~spin) Saving USS file..."); + expect(mockUssApi.uploadFromBuffer).toHaveBeenCalledWith(Buffer.from(newContents), testEntries.file.metadata.path, { + binary: false, + encoding: undefined, + etag: testEntries.file.etag, + returnEtag: true, + }); + expect(handleConflictMock).not.toHaveBeenCalled(); + expect(_handleErrorMock).toHaveBeenCalled(); + handleConflictMock.mockRestore(); + _handleErrorMock.mockRestore(); ussApiMock.mockRestore(); autoDetectEncodingMock.mockRestore(); }); @@ -894,11 +942,13 @@ describe("rename", () => { }; (UssFSProvider.instance as any).root.entries.set("sestest", sessionEntry); - await UssFSProvider.instance.rename(testUris.folder, testUris.folder.with({ path: "/sestest/aFolder2" }), { overwrite: true }); + await expect( + UssFSProvider.instance.rename(testUris.folder, testUris.folder.with({ path: "/sestest/aFolder2" }), { overwrite: true }) + ).rejects.toThrow(); expect(mockUssApi.rename).toHaveBeenCalledWith("/aFolder", "/aFolder2"); expect(folderEntry.metadata.path).toBe("/aFolder"); expect(sessionEntry.entries.has("aFolder2")).toBe(false); - expect(errMsgSpy).toHaveBeenCalledWith("Renaming /aFolder failed due to API error: could not upload file"); + expect(errMsgSpy).toHaveBeenCalledWith("Failed to rename /aFolder: could not upload file", { items: ["Retry", "Show log", "Troubleshoot"] }); lookupMock.mockRestore(); ussApiMock.mockRestore(); @@ -933,15 +983,23 @@ describe("delete", () => { parent: sesEntry, parentUri: Uri.from({ scheme: ZoweScheme.USS, path: "/sestest" }), }); - const errorMsgMock = jest.spyOn(Gui, "errorMessage").mockResolvedValueOnce(undefined); - const deleteMock = jest.fn().mockRejectedValueOnce(new Error("insufficient permissions")); + const exampleError = new Error("insufficient permissions"); + const deleteMock = jest.fn().mockRejectedValueOnce(exampleError); jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce({ delete: deleteMock, } as any); - await UssFSProvider.instance.delete(testUris.file, { recursive: false }); + const handleErrorMock = jest.spyOn((BaseProvider as any).prototype, "_handleError"); + await expect(UssFSProvider.instance.delete(testUris.file, { recursive: false })).rejects.toThrow(); expect(getDelInfoMock).toHaveBeenCalledWith(testUris.file); expect(deleteMock).toHaveBeenCalledWith(testEntries.file.metadata.path, false); - expect(errorMsgMock).toHaveBeenCalledWith("Deleting /aFile.txt failed due to API error: insufficient permissions"); + expect(handleErrorMock).toHaveBeenCalledWith( + exampleError, + expect.objectContaining({ + additionalContext: "Failed to delete /aFile.txt", + apiType: ZoweExplorerApiType.Uss, + profileType: testEntries.file.metadata.profile.type, + }) + ); expect(sesEntry.entries.has("aFile.txt")).toBe(true); expect(sesEntry.size).toBe(1); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/uss/ZoweUSSNode.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/uss/ZoweUSSNode.unit.test.ts index 56f052c5b2..1fdd7d247d 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/uss/ZoweUSSNode.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/uss/ZoweUSSNode.unit.test.ts @@ -446,7 +446,6 @@ describe("ZoweUSSNode Unit Tests - Function node.rename()", () => { const renameMock = jest.spyOn(vscode.workspace.fs, "rename").mockRejectedValueOnce(new Error("Rename error: file is busy")); await blockMocks.ussDir.rename(newFullPath); - expect(errMessageMock).toHaveBeenCalledWith("Rename error: file is busy"); errMessageMock.mockRestore(); renameMock.mockRestore(); }); @@ -859,9 +858,7 @@ describe("ZoweUSSNode Unit Tests - Function node.getChildren()", () => { const response = await blockMocks.childNode.getChildren(); expect(response).toEqual([]); expect(globalMocks.showErrorMessage.mock.calls.length).toEqual(1); - expect(globalMocks.showErrorMessage.mock.calls[0][0]).toEqual( - "Retrieving response from USS list API Error: Throwing an error to check error handling for unit tests!" - ); + expect(globalMocks.showErrorMessage.mock.calls[0][0]).toEqual("Throwing an error to check error handling for unit tests!"); }); it("Tests that when passing a session node that is not dirty the node.getChildren() method is exited early", async () => { 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 80be2384c8..c7e8b9af01 100644 --- a/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts @@ -91,12 +91,11 @@ describe("ProfilesUtils unit tests", () => { it("should log error details", async () => { createBlockMocks(); const errorDetails = new Error("i haz error"); - const label = "test"; - const moreInfo = "Task failed successfully"; - await AuthUtils.errorHandling(errorDetails, label, moreInfo); - expect(Gui.errorMessage).toHaveBeenCalledWith(moreInfo + ` Error: ${errorDetails.message}`); + const scenario = "Task failed successfully"; + await AuthUtils.errorHandling(errorDetails, { scenario }); + expect(Gui.errorMessage).toHaveBeenCalledWith(errorDetails.message, { items: ["Show log", "Troubleshoot"] }); expect(ZoweLogger.error).toHaveBeenCalledWith( - `${errorDetails.toString()}\n` + util.inspect({ errorDetails, label, moreInfo }, { depth: null }) + `${errorDetails.toString()}\n` + util.inspect({ errorDetails, ...{ scenario, profile: undefined } }, { depth: null }) ); }); @@ -108,13 +107,12 @@ describe("ProfilesUtils unit tests", () => { msg: "Circular reference", causeErrors: errorJson, }); - const label = "test"; - const moreInfo = "Task failed successfully"; - await AuthUtils.errorHandling(errorDetails, label, moreInfo as unknown as string); + const scenario = "Task failed successfully"; + await AuthUtils.errorHandling(errorDetails, { scenario }); // eslint-disable-next-line @typescript-eslint/restrict-plus-operands - expect(Gui.errorMessage).toHaveBeenCalledWith((`${moreInfo} ` + errorDetails) as any); + expect(Gui.errorMessage).toHaveBeenCalledWith(errorDetails.message, { items: ["Show log", "Troubleshoot"] }); expect(ZoweLogger.error).toHaveBeenCalledWith( - `Error: ${errorDetails.message}\n` + util.inspect({ errorDetails, label, moreInfo }, { depth: null }) + `Error: ${errorDetails.message}\n` + util.inspect({ errorDetails, ...{ scenario, profile: undefined } }, { depth: null }) ); }); @@ -123,9 +121,8 @@ describe("ProfilesUtils unit tests", () => { msg: "Invalid hostname", errorCode: 404 as unknown as string, }); - const label = "test"; - const moreInfo = "Task failed successfully"; - const spyOpenConfigFile = jest.fn(); + const scenario = "Task failed successfully"; + const openConfigForMissingHostnameMock = jest.spyOn(AuthUtils, "openConfigForMissingHostname"); Object.defineProperty(Constants, "PROFILES_CACHE", { value: { getProfileInfo: () => ({ @@ -139,12 +136,12 @@ describe("ProfilesUtils unit tests", () => { }, ], }), - openConfigFile: spyOpenConfigFile, + openConfigFile: jest.fn(), }, configurable: true, }); - await AuthUtils.errorHandling(errorDetails, label, moreInfo); - expect(spyOpenConfigFile).toHaveBeenCalledTimes(1); + await AuthUtils.errorHandling(errorDetails, { scenario }); + expect(openConfigForMissingHostnameMock).toHaveBeenCalled(); }); it("should handle error for invalid credentials and prompt for authentication", async () => { @@ -153,21 +150,21 @@ describe("ProfilesUtils unit tests", () => { errorCode: 401 as unknown as string, additionalDetails: "Authentication is not valid or expired.", }); - const label = "test"; - const moreInfo = "Task failed successfully"; + const scenario = "Task failed successfully"; const showMessageSpy = jest.spyOn(Gui, "errorMessage").mockImplementation(() => Promise.resolve("Update Credentials")); const promptCredsSpy = jest.fn(); + const profile = { type: "zosmf" } as any; Object.defineProperty(Constants, "PROFILES_CACHE", { value: { promptCredentials: promptCredsSpy, getProfileInfo: profileInfoMock, - getLoadedProfConfig: () => ({ type: "zosmf" }), + getLoadedProfConfig: () => profile, getDefaultProfile: () => ({}), getSecurePropsForProfile: () => [], }, configurable: true, }); - await AuthUtils.errorHandling(errorDetails, label, moreInfo); + await AuthUtils.errorHandling(errorDetails, { profile, scenario }); expect(showMessageSpy).toHaveBeenCalledTimes(1); expect(promptCredsSpy).toHaveBeenCalledTimes(1); showMessageSpy.mockClear(); @@ -176,25 +173,25 @@ describe("ProfilesUtils unit tests", () => { it("should handle token error and proceed to login", async () => { const errorDetails = new imperative.ImperativeError({ msg: "Invalid credentials", - errorCode: 401 as unknown as string, + errorCode: "401", additionalDetails: "Token is not valid or expired.", }); - const label = "test"; - const moreInfo = "Task failed successfully"; + const scenario = "Task failed successfully"; const showErrorSpy = jest.spyOn(Gui, "errorMessage"); const showMessageSpy = jest.spyOn(Gui, "showMessage").mockImplementation(() => Promise.resolve("selection")); const ssoLoginSpy = jest.fn(); + const profile = { type: "zosmf" } as any; Object.defineProperty(Constants, "PROFILES_CACHE", { value: { getProfileInfo: profileInfoMock, - getLoadedProfConfig: () => ({ type: "zosmf" }), + getLoadedProfConfig: () => profile, getDefaultProfile: () => ({}), getSecurePropsForProfile: () => ["tokenValue"], ssoLogin: ssoLoginSpy, }, configurable: true, }); - await AuthUtils.errorHandling(errorDetails, label, moreInfo); + await AuthUtils.errorHandling(errorDetails, { profile, scenario }); expect(showMessageSpy).toHaveBeenCalledTimes(1); expect(ssoLoginSpy).toHaveBeenCalledTimes(1); expect(showErrorSpy).not.toHaveBeenCalled(); @@ -205,10 +202,9 @@ describe("ProfilesUtils unit tests", () => { it("should handle credential error and no selection made for update", async () => { const errorDetails = new imperative.ImperativeError({ msg: "Invalid credentials", - errorCode: String(401), - additionalDetails: "Authentication failed.", + errorCode: "401", + additionalDetails: "All configured authentication methods failed", }); - const label = "test"; const moreInfo = "Task failed successfully"; Object.defineProperty(vscode, "env", { value: { @@ -219,20 +215,21 @@ describe("ProfilesUtils unit tests", () => { const showErrorSpy = jest.spyOn(Gui, "errorMessage").mockResolvedValue(undefined); const showMsgSpy = jest.spyOn(Gui, "showMessage"); const promptCredentialsSpy = jest.fn(); + const profile = { type: "zosmf" } as any; Object.defineProperty(Constants, "PROFILES_CACHE", { value: { promptCredentials: promptCredentialsSpy, getProfileInfo: profileInfoMock, - getLoadedProfConfig: () => ({ type: "zosmf" }), + getLoadedProfConfig: () => profile, getDefaultProfile: () => ({}), getSecurePropsForProfile: () => [], }, configurable: true, }); - await AuthUtils.errorHandling(errorDetails, label, moreInfo); + await AuthUtils.errorHandling(errorDetails, { profile, scenario: moreInfo }); expect(showErrorSpy).toHaveBeenCalledTimes(1); expect(promptCredentialsSpy).not.toHaveBeenCalled(); - expect(showMsgSpy).toHaveBeenCalledWith("Operation cancelled"); + expect(showMsgSpy).not.toHaveBeenCalledWith("Operation Cancelled"); showErrorSpy.mockClear(); showMsgSpy.mockClear(); promptCredentialsSpy.mockClear(); @@ -574,7 +571,7 @@ describe("ProfilesUtils unit tests", () => { await ProfilesUtils.initializeZoweProfiles((msg) => ZoweExplorerExtender.showZoweConfigError(msg)); expect(initZoweFolderSpy).toHaveBeenCalledTimes(1); expect(readConfigFromDiskSpy).toHaveBeenCalledTimes(1); - expect(Gui.errorMessage).toHaveBeenCalledWith(expect.stringContaining(testError.message)); + expect(Gui.errorMessage).toHaveBeenCalledWith(testError.message, { items: ["Show log", "Troubleshoot"] }); }); it("should handle JSON parse error thrown on read config from disk", async () => { diff --git a/packages/zowe-explorer/__tests__/__unit__/utils/TroubleshootError.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/utils/TroubleshootError.unit.test.ts new file mode 100644 index 0000000000..a65d96bfd3 --- /dev/null +++ b/packages/zowe-explorer/__tests__/__unit__/utils/TroubleshootError.unit.test.ts @@ -0,0 +1,77 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { env, ExtensionContext } from "vscode"; +import { TroubleshootError } from "../../../src/utils/TroubleshootError"; +import { CorrelatedError } from "@zowe/zowe-explorer-api"; +import { ZoweLogger } from "../../../src/tools/ZoweLogger"; +import { MockedProperty } from "../../__mocks__/mockUtils"; + +describe("TroubleshootError", () => { + function getGlobalMocks() { + const context = { + extensionPath: "/a/b/c/zowe-explorer", + subscriptions: [], + } as unknown as ExtensionContext; + const error = new Error("test error"); + error.stack = "test stack trace"; + const correlatedError = new CorrelatedError({ initialError: error }); + const troubleshootError = new TroubleshootError(context, { error: correlatedError, stackTrace: "test stack trace" }); + + return { + context, + error, + correlatedError, + troubleshootError, + }; + } + describe("onDidReceiveMessage", () => { + it("handles copy command for error with stack trace", async () => { + const { error, troubleshootError } = getGlobalMocks(); + const writeTextMock = jest.spyOn(env.clipboard, "writeText").mockImplementation(); + await troubleshootError.onDidReceiveMessage({ command: "copy" }); + expect(writeTextMock).toHaveBeenCalledWith(`Error details:\n${error.message}\nStack trace:\n${error.stack?.replace(/(.+?)\n/, "")}`); + }); + + it("handles copy command for error without stack trace", async () => { + const { error, troubleshootError } = getGlobalMocks(); + const errorProp = new MockedProperty(error, "stack", { value: undefined }); + const writeTextMock = jest.spyOn(env.clipboard, "writeText").mockImplementation(); + await troubleshootError.onDidReceiveMessage({ command: "copy" }); + expect(writeTextMock).toHaveBeenCalledWith(`Error details:\n${error.message}`); + errorProp[Symbol.dispose](); + }); + + it("handles ready command", async () => { + const { troubleshootError } = getGlobalMocks(); + const sendErrorDataSpy = jest.spyOn(troubleshootError, "sendErrorData"); + await troubleshootError.onDidReceiveMessage({ command: "ready" }); + expect(sendErrorDataSpy).toHaveBeenCalledWith(troubleshootError.errorData); + }); + + it("handles an unrecognized command", async () => { + const { troubleshootError } = getGlobalMocks(); + const debugSpy = jest.spyOn(ZoweLogger, "debug"); + await troubleshootError.onDidReceiveMessage({ command: "unknown" }); + expect(debugSpy).toHaveBeenCalledWith("[TroubleshootError] Unknown command: unknown"); + }); + }); + + describe("sendErrorData", () => { + it("sends error data to the webview", async () => { + const { correlatedError, troubleshootError } = getGlobalMocks(); + const postMessageSpy = jest.spyOn(troubleshootError.panel.webview, "postMessage"); + const data = { error: correlatedError, stackTrace: correlatedError.stack }; + await troubleshootError.sendErrorData(data); + expect(postMessageSpy).toHaveBeenCalledWith(data); + }); + }); +}); diff --git a/packages/zowe-explorer/l10n/bundle.l10n.json b/packages/zowe-explorer/l10n/bundle.l10n.json index fa4b07fda8..4e88316430 100644 --- a/packages/zowe-explorer/l10n/bundle.l10n.json +++ b/packages/zowe-explorer/l10n/bundle.l10n.json @@ -47,6 +47,7 @@ "Certificate Key File": "Certificate Key File", "Submit": "Submit", "Cancel": "Cancel", + "Troubleshoot Error": "Troubleshoot Error", "Zowe Explorer profiles are being set as unsecured.": "Zowe Explorer profiles are being set as unsecured.", "Zowe Explorer profiles are being set as secured.": "Zowe Explorer profiles are being set as secured.", "Custom credential manager failed to activate": "Custom credential manager failed to activate", @@ -158,14 +159,8 @@ "Certificate Key for Authentication.": "Certificate Key for Authentication.", "Certificate Keys": "Certificate Keys", "Select Certificate Key": "Select Certificate Key", - "Required parameter 'host' must not be blank.": "Required parameter 'host' must not be blank.", - "Invalid Credentials for profile '{0}'. Please ensure the username and password are valid or this may lead to a lock-out./Label": { - "message": "Invalid Credentials for profile '{0}'. Please ensure the username and password are valid or this may lead to a lock-out.", - "comment": [ - "Label" - ] - }, "Update Credentials": "Update Credentials", + "Required parameter 'host' must not be blank.": "Required parameter 'host' must not be blank.", "Your connection is no longer active for profile '{0}'. Please log in to an authentication service to restore the connection./Profile name": { "message": "Your connection is no longer active for profile '{0}'. Please log in to an authentication service to restore the connection.", "comment": [ @@ -208,32 +203,6 @@ "Profile auth error": "Profile auth error", "Profile is not authenticated, please log in to continue": "Profile is not authenticated, please log in to continue", "Retrieving response from USS list API": "Retrieving response from USS list API", - "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.", - "Saving USS file...": "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": [ @@ -304,6 +273,48 @@ "initializeUSSFavorites.error.buttonRemove": "initializeUSSFavorites.error.buttonRemove", "File does not exist. It may have been deleted.": "File does not exist. It may have been deleted.", "Pulling from Mainframe...": "Pulling from Mainframe...", + "The 'move' function is not implemented for this USS API.": "The 'move' function is not implemented for this USS API.", + "Failed to move {0}/File path": { + "message": "Failed to move {0}", + "comment": [ + "File path" + ] + }, + "Failed to get contents for {0}/File path": { + "message": "Failed to get contents for {0}", + "comment": [ + "File path" + ] + }, + "Profile does not exist for this file.": "Profile does not exist for this file.", + "Saving USS file...": "Saving USS file...", + "Failed to rename {0}/File path": { + "message": "Failed to rename {0}", + "comment": [ + "File path" + ] + }, + "Failed to delete {0}/File name": { + "message": "Failed to delete {0}", + "comment": [ + "File name" + ] + }, + "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" + ] + }, + "Failed to copy {0} to {1}/Source pathDestination path": { + "message": "Failed to copy {0} to {1}", + "comment": [ + "Source path", + "Destination path" + ] + }, "{0} location/Node type": { "message": "{0} location", "comment": [ @@ -495,7 +506,18 @@ "Phase Name": "Phase Name", "Error Details": "Error Details", "Fetching spool file...": "Fetching spool file...", - "Failed to fetch jobs: getJobsByParameters is not implemented for this session's JES API.": "Failed to fetch jobs: getJobsByParameters is not implemented for this session's JES API.", + "Failed to get contents for {0}/Spool name": { + "message": "Failed to get contents for {0}", + "comment": [ + "Spool name" + ] + }, + "Failed to delete job {0}/Job name": { + "message": "Failed to delete job {0}", + "comment": [ + "Job name" + ] + }, "Are you sure you want to delete the following item?\nThis will permanently remove the following job from your system.\n\n{0}/Job name": { "message": "Are you sure you want to delete the following item?\nThis will permanently remove the following job from your system.\n\n{0}", "comment": [ @@ -684,19 +706,31 @@ "Choose the setting location to save the data set template...": "Choose the setting location to save the data set template...", "Save as User setting": "Save as User setting", "Save as Workspace setting": "Save as Workspace setting", + "Failed to list datasets": "Failed to list datasets", + "Failed to list dataset members": "Failed to list dataset members", + "Failed to read {0}/File path": { + "message": "Failed to read {0}", + "comment": [ + "File path" + ] + }, "Saving data set...": "Saving data set...", - "Deleting {0} failed due to API error: {1}/File pathError message": { - "message": "Deleting {0} failed due to API error: {1}", + "Failed to save {0}/Data set name": { + "message": "Failed to save {0}", "comment": [ - "File path", - "Error message" + "Data set name" ] }, - "Renaming {0} failed due to API error: {1}/File nameError message": { - "message": "Renaming {0} failed due to API error: {1}", + "Failed to delete {0}/File path": { + "message": "Failed to delete {0}", "comment": [ - "File name", - "Error message" + "File path" + ] + }, + "Failed to rename {0}/Data set name": { + "message": "Failed to rename {0}", + "comment": [ + "Data set name" ] }, "Partitioned Data Set: Binary": "Partitioned Data Set: Binary", diff --git a/packages/zowe-explorer/l10n/poeditor.json b/packages/zowe-explorer/l10n/poeditor.json index 8a4694c4e7..e715a39d2f 100644 --- a/packages/zowe-explorer/l10n/poeditor.json +++ b/packages/zowe-explorer/l10n/poeditor.json @@ -464,6 +464,7 @@ "Certificate Key File": "", "Submit": "", "Cancel": "", + "Troubleshoot Error": "", "Zowe Explorer profiles are being set as unsecured.": "", "Zowe Explorer profiles are being set as secured.": "", "Custom credential manager failed to activate": "", @@ -514,9 +515,8 @@ "Certificate Key for Authentication.": "", "Certificate Keys": "", "Select Certificate Key": "", - "Required parameter 'host' must not be blank.": "", - "Invalid Credentials for profile '{0}'. Please ensure the username and password are valid or this may lead to a lock-out.": "", "Update Credentials": "", + "Required parameter 'host' must not be blank.": "", "Your connection is no longer active for profile '{0}'. Please log in to an authentication service to restore the connection.": "", "Profile Name {0} is inactive. Please check if your Zowe server is active or if the URL and port in your profile is correct.": "", "Use the search button to list USS files": "", @@ -534,14 +534,6 @@ "Profile auth error": "", "Profile is not authenticated, please log in to continue": "", "Retrieving response from USS list API": "", - "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.": "", - "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": "", @@ -570,6 +562,16 @@ "initializeUSSFavorites.error.buttonRemove": "", "File does not exist. It may have been deleted.": "", "Pulling from Mainframe...": "", + "The 'move' function is not implemented for this USS API.": "", + "Failed to move {0}": "", + "Failed to get contents for {0}": "", + "Profile does not exist for this file.": "", + "Saving USS file...": "", + "Failed to rename {0}": "", + "Failed to delete {0}": "", + "No error details given": "", + "Error fetching destination {0} for paste action: {1}": "", + "Failed to copy {0} to {1}": "", "{0} location": "", "Choose a location to create the {0}": "", "Name of file or directory": "", @@ -663,7 +665,7 @@ "Phase Name": "", "Error Details": "", "Fetching spool file...": "", - "Failed to fetch jobs: getJobsByParameters is not implemented for this session's JES API.": "", + "Failed to delete job {0}": "", "Are you sure you want to delete the following item?\nThis will permanently remove the following job from your system.\n\n{0}": "", "Job {0} was deleted.": "", "Are you sure you want to delete the following {0} items?\nThis will permanently remove the following jobs from your system.\n\n{1}": "", @@ -724,7 +726,11 @@ "Choose the setting location to save the data set template...": "", "Save as User setting": "", "Save as Workspace setting": "", + "Failed to list datasets": "", + "Failed to list dataset members": "", + "Failed to read {0}": "", "Saving data set...": "", + "Failed to save {0}": "", "Partitioned Data Set: Binary": "", "Partitioned Data Set: C": "", "Partitioned Data Set: Classic": "", diff --git a/packages/zowe-explorer/src/commands/MvsCommandHandler.ts b/packages/zowe-explorer/src/commands/MvsCommandHandler.ts index b4e7f96324..cccb993c35 100644 --- a/packages/zowe-explorer/src/commands/MvsCommandHandler.ts +++ b/packages/zowe-explorer/src/commands/MvsCommandHandler.ts @@ -10,7 +10,7 @@ */ import * as vscode from "vscode"; -import { Validation, imperative, IZoweTreeNode, Gui } from "@zowe/zowe-explorer-api"; +import { Validation, imperative, IZoweTreeNode, Gui, ZoweExplorerApiType } from "@zowe/zowe-explorer-api"; import { ZoweCommandProvider } from "./ZoweCommandProvider"; import { ZoweLogger } from "../tools/ZoweLogger"; import { Profiles } from "../configuration/Profiles"; @@ -126,7 +126,7 @@ export class MvsCommandHandler extends ZoweCommandProvider { }) ); } else { - await AuthUtils.errorHandling(error, profile.name); + await AuthUtils.errorHandling(error, { apiType: ZoweExplorerApiType.Command, profile }); } } } @@ -215,7 +215,7 @@ export class MvsCommandHandler extends ZoweCommandProvider { } } } catch (error) { - await AuthUtils.errorHandling(error, profile.name); + await AuthUtils.errorHandling(error, { apiType: ZoweExplorerApiType.Command, profile }); } this.history.addSearchHistory(command); } diff --git a/packages/zowe-explorer/src/commands/TsoCommandHandler.ts b/packages/zowe-explorer/src/commands/TsoCommandHandler.ts index b230ac13f2..56c8815da9 100644 --- a/packages/zowe-explorer/src/commands/TsoCommandHandler.ts +++ b/packages/zowe-explorer/src/commands/TsoCommandHandler.ts @@ -11,7 +11,7 @@ import * as vscode from "vscode"; import * as zostso from "@zowe/zos-tso-for-zowe-sdk"; -import { Gui, Validation, imperative, IZoweTreeNode } from "@zowe/zowe-explorer-api"; +import { Gui, Validation, imperative, IZoweTreeNode, ZoweExplorerApiType } from "@zowe/zowe-explorer-api"; import { ZoweCommandProvider } from "./ZoweCommandProvider"; import { ZoweLogger } from "../tools/ZoweLogger"; import { Profiles } from "../configuration/Profiles"; @@ -134,7 +134,7 @@ export class TsoCommandHandler extends ZoweCommandProvider { }) ); } else { - await AuthUtils.errorHandling(error, profile.name); + await AuthUtils.errorHandling(error, { apiType: ZoweExplorerApiType.Command, profile }); } } } @@ -229,7 +229,7 @@ export class TsoCommandHandler extends ZoweCommandProvider { ZoweLogger.error(message); Gui.errorMessage(message); } else { - await AuthUtils.errorHandling(error, profile.name); + await AuthUtils.errorHandling(error, { apiType: ZoweExplorerApiType.Command, profile }); } } } diff --git a/packages/zowe-explorer/src/commands/UnixCommandHandler.ts b/packages/zowe-explorer/src/commands/UnixCommandHandler.ts index b3dc1c45cf..51726100e5 100644 --- a/packages/zowe-explorer/src/commands/UnixCommandHandler.ts +++ b/packages/zowe-explorer/src/commands/UnixCommandHandler.ts @@ -12,7 +12,7 @@ import * as vscode from "vscode"; import * as zosuss from "@zowe/zos-uss-for-zowe-sdk"; import { ZoweCommandProvider } from "./ZoweCommandProvider"; -import { Gui, IZoweTreeNode, imperative } from "@zowe/zowe-explorer-api"; +import { Gui, IZoweTreeNode, ZoweExplorerApiType, imperative } from "@zowe/zowe-explorer-api"; import { Profiles } from "../configuration/Profiles"; import { ZoweExplorerApiRegister } from "../extending/ZoweExplorerApiRegister"; import { ZoweLogger } from "../tools/ZoweLogger"; @@ -165,7 +165,7 @@ export class UnixCommandHandler extends ZoweCommandProvider { }) ); } else { - await AuthUtils.errorHandling(error, this.serviceProf.name); + await AuthUtils.errorHandling(error, { apiType: ZoweExplorerApiType.Command, profile: this.serviceProf }); } } } @@ -354,7 +354,6 @@ export class UnixCommandHandler extends ZoweCommandProvider { private async issueCommand(profile: imperative.IProfileLoaded, command: string, cwd: string): Promise { ZoweLogger.trace("UnixCommandHandler.issueCommand called."); - const profName = this.sshProfile !== undefined ? this.sshProfile.name : profile.name; try { if (command) { const user: string = profile.profile.user; @@ -379,7 +378,7 @@ export class UnixCommandHandler extends ZoweCommandProvider { this.serviceProf = undefined; } } catch (error) { - await AuthUtils.errorHandling(error, profName); + await AuthUtils.errorHandling(error, { apiType: ZoweExplorerApiType.Command, profile }); } } } diff --git a/packages/zowe-explorer/src/commands/ZoweCommandProvider.ts b/packages/zowe-explorer/src/commands/ZoweCommandProvider.ts index d2ac576dc8..7db19f3cf8 100644 --- a/packages/zowe-explorer/src/commands/ZoweCommandProvider.ts +++ b/packages/zowe-explorer/src/commands/ZoweCommandProvider.ts @@ -10,7 +10,7 @@ */ import * as vscode from "vscode"; -import { IZoweTreeNode, PersistenceSchemaEnum, Validation } from "@zowe/zowe-explorer-api"; +import { IZoweTreeNode, PersistenceSchemaEnum, Validation, ZoweExplorerApiType } from "@zowe/zowe-explorer-api"; import { ZowePersistentFilters } from "../tools/ZowePersistentFilters"; import { ZoweLogger } from "../tools/ZoweLogger"; import { SharedContext } from "../trees/shared/SharedContext"; @@ -75,7 +75,8 @@ export class ZoweCommandProvider { "Profile Name {0} is inactive. Please check if your Zowe server is active or if the URL and port in your profile is correct.", args: [profile.name], comment: ["Profile name"], - }) + }), + { apiType: ZoweExplorerApiType.Command, profile } ); } else if (profileStatus.status === "active") { if ( diff --git a/packages/zowe-explorer/src/configuration/Constants.ts b/packages/zowe-explorer/src/configuration/Constants.ts index 33e6406262..e584d01e9c 100644 --- a/packages/zowe-explorer/src/configuration/Constants.ts +++ b/packages/zowe-explorer/src/configuration/Constants.ts @@ -16,7 +16,7 @@ import { imperative, PersistenceSchemaEnum } from "@zowe/zowe-explorer-api"; import type { Profiles } from "./Profiles"; export class Constants { - public static readonly COMMAND_COUNT = 101; + public static readonly COMMAND_COUNT = 103; public static readonly MAX_SEARCH_HISTORY = 5; public static readonly MAX_FILE_HISTORY = 10; public static readonly MS_PER_SEC = 1000; diff --git a/packages/zowe-explorer/src/configuration/Profiles.ts b/packages/zowe-explorer/src/configuration/Profiles.ts index f63def5ad6..aaec1446c6 100644 --- a/packages/zowe-explorer/src/configuration/Profiles.ts +++ b/packages/zowe-explorer/src/configuration/Profiles.ts @@ -110,7 +110,7 @@ export class Profiles extends ProfilesCache { await Profiles.getInstance().ssoLogin(null, theProfile.name); theProfile = Profiles.getInstance().loadNamedProfile(theProfile.name); } catch (error) { - await AuthUtils.errorHandling(error, theProfile.name, error.message); + await AuthUtils.errorHandling(error, { profile: theProfile }); return profileStatus; } } else if (!usingTokenAuth && (!theProfile.profile.user || !theProfile.profile.password)) { @@ -123,7 +123,7 @@ export class Profiles extends ProfilesCache { try { values = await Profiles.getInstance().promptCredentials(theProfile); } catch (error) { - await AuthUtils.errorHandling(error, theProfile.name, error.message); + await AuthUtils.errorHandling(error, { profile: theProfile }); return profileStatus; } if (values) { @@ -748,7 +748,7 @@ export class Profiles extends ProfilesCache { comment: [`The profile name`], }) ); - await AuthUtils.errorHandling(error, theProfile.name); + await AuthUtils.errorHandling(error, { profile: theProfile }); filteredProfile = { status: "inactive", name: theProfile.name, diff --git a/packages/zowe-explorer/src/extending/ZoweExplorerExtender.ts b/packages/zowe-explorer/src/extending/ZoweExplorerExtender.ts index 0a947aaa4b..9e32d2ddf3 100644 --- a/packages/zowe-explorer/src/extending/ZoweExplorerExtender.ts +++ b/packages/zowe-explorer/src/extending/ZoweExplorerExtender.ts @@ -23,6 +23,7 @@ import { IZoweExplorerTreeApi, imperative, ZoweVsCodeExtension, + ErrorCorrelator, } from "@zowe/zowe-explorer-api"; import { Constants } from "../configuration/Constants"; import { ProfilesUtils } from "../utils/ProfilesUtils"; @@ -139,6 +140,14 @@ export class ZoweExplorerExtender implements IApiExplorerExtender, IZoweExplorer public jobsProvider?: Types.IZoweJobTreeType ) {} + /** + * @implements The {@link IApiExplorerExtender.getErrorCorrelator} function + * @returns Singleton instance of the error correlator + */ + public getErrorCorrelator(): ErrorCorrelator { + return ErrorCorrelator.getInstance(); + } + /** * * @implements IApiExplorerExtender.initForZowe() diff --git a/packages/zowe-explorer/src/trees/ZoweTreeProvider.ts b/packages/zowe-explorer/src/trees/ZoweTreeProvider.ts index ea2ddbd1ed..9c6ee197ee 100644 --- a/packages/zowe-explorer/src/trees/ZoweTreeProvider.ts +++ b/packages/zowe-explorer/src/trees/ZoweTreeProvider.ts @@ -241,7 +241,8 @@ export class ZoweTreeProvider { "Profile Name {0} is inactive. Please check if your Zowe server is active or if the URL and port in your profile is correct.", args: [profile.name], comment: ["Profile name"], - }) + }), + { profile } ); } else if (profileStatus.status === "active") { if ( diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetActions.ts b/packages/zowe-explorer/src/trees/dataset/DatasetActions.ts index 5fe88bb40a..7a5488bce7 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetActions.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetActions.ts @@ -12,7 +12,7 @@ import * as vscode from "vscode"; import * as zosfiles from "@zowe/zos-files-for-zowe-sdk"; import * as path from "path"; -import { Gui, imperative, IZoweDatasetTreeNode, Validation, Types, FsAbstractUtils, ZoweScheme } from "@zowe/zowe-explorer-api"; +import { Gui, imperative, IZoweDatasetTreeNode, Validation, Types, FsAbstractUtils, ZoweScheme, ZoweExplorerApiType } from "@zowe/zowe-explorer-api"; import { ZoweDatasetNode } from "./ZoweDatasetNode"; import { DatasetUtils } from "./DatasetUtils"; import { DatasetFSProvider } from "./DatasetFSProvider"; @@ -246,7 +246,7 @@ export class DatasetActions { const errorMsg = vscode.l10n.t("Error encountered when creating data set."); ZoweLogger.error(errorMsg + JSON.stringify(err)); if (err instanceof Error) { - await AuthUtils.errorHandling(err, node.getProfileName(), errorMsg); + await AuthUtils.errorHandling(err, { apiType: ZoweExplorerApiType.Mvs, profile: node.getProfile(), scenario: errorMsg }); } throw new Error(err); } @@ -382,7 +382,12 @@ export class DatasetActions { await ZoweExplorerApiRegister.getMvsApi(profile).allocateLikeDataSet(newDSName.toUpperCase(), likeDSName); } catch (err) { if (err instanceof Error) { - await AuthUtils.errorHandling(err, newDSName, vscode.l10n.t("Unable to create data set.")); + await AuthUtils.errorHandling(err, { + apiType: ZoweExplorerApiType.Mvs, + profile, + dsName: newDSName, + scenario: vscode.l10n.t("Unable to create data set."), + }); } throw err; } @@ -438,7 +443,10 @@ export class DatasetActions { Gui.reportProgress(progress, value.length, index, "Uploading"); const response = await DatasetActions.uploadFile(node, item.fsPath); if (!response?.success) { - await AuthUtils.errorHandling(response?.commandResponse, node.getProfileName(), response?.commandResponse); + await AuthUtils.errorHandling(response?.commandResponse, { + apiType: ZoweExplorerApiType.Mvs, + profile: node.getProfile(), + }); break; } index++; @@ -476,7 +484,7 @@ export class DatasetActions { responseTimeout: prof.profile?.responseTimeout, }); } catch (e) { - await AuthUtils.errorHandling(e, node.getProfileName()); + await AuthUtils.errorHandling(e, { apiType: ZoweExplorerApiType.Mvs, profile: node.getProfile() }); } } @@ -674,7 +682,11 @@ export class DatasetActions { }); } catch (err) { if (err instanceof Error) { - await AuthUtils.errorHandling(err, label, vscode.l10n.t("Unable to create member.")); + await AuthUtils.errorHandling(err, { + apiType: ZoweExplorerApiType.Mvs, + parentDsName: label, + scenario: vscode.l10n.t("Unable to create member."), + }); } throw err; } @@ -878,7 +890,11 @@ export class DatasetActions { } } catch (err) { if (err instanceof Error) { - await AuthUtils.errorHandling(err, node.getProfileName(), vscode.l10n.t("Unable to list attributes.")); + await AuthUtils.errorHandling(err, { + apiType: ZoweExplorerApiType.Mvs, + profile: node.getProfile(), + scenario: vscode.l10n.t("Unable to list attributes."), + }); } throw err; } @@ -1012,7 +1028,11 @@ export class DatasetActions { ); } catch (error) { if (error instanceof Error) { - await AuthUtils.errorHandling(error, sessProfileName, vscode.l10n.t("Job submission failed.")); + await AuthUtils.errorHandling(error, { + apiType: ZoweExplorerApiType.Mvs, + profile: sessProfile, + scenario: vscode.l10n.t("Job submission failed."), + }); } } } else { @@ -1135,7 +1155,11 @@ export class DatasetActions { ); } catch (error) { if (error instanceof Error) { - await AuthUtils.errorHandling(error, sesName, vscode.l10n.t("Job submission failed.")); + await AuthUtils.errorHandling(error, { + apiType: ZoweExplorerApiType.Mvs, + profile: sessProfile, + scenario: vscode.l10n.t("Job submission failed."), + }); } } } @@ -1192,7 +1216,7 @@ export class DatasetActions { }) ); } else { - await AuthUtils.errorHandling(err, node.getProfileName()); + await AuthUtils.errorHandling(err, { apiType: ZoweExplorerApiType.Mvs, profile: node.getProfile() }); } throw err; } @@ -1276,7 +1300,7 @@ export class DatasetActions { }) ); } else { - await AuthUtils.errorHandling(err, node.getProfileName()); + await AuthUtils.errorHandling(err, { apiType: ZoweExplorerApiType.Mvs, profile: node.getProfile() }); } } } @@ -1293,7 +1317,7 @@ export class DatasetActions { await node.getChildren(); datasetProvider.refreshElement(node); } catch (err) { - await AuthUtils.errorHandling(err, node.getProfileName()); + await AuthUtils.errorHandling(err, { apiType: ZoweExplorerApiType.Mvs, profile: node.getProfile() }); } } @@ -1712,7 +1736,11 @@ export class DatasetActions { } } catch (error) { if (error instanceof Error) { - await AuthUtils.errorHandling(error, DatasetUtils.getNodeLabels(node).dataSetName, vscode.l10n.t("Unable to copy data set.")); + await AuthUtils.errorHandling(error, { + apiType: ZoweExplorerApiType.Mvs, + dsName: DatasetUtils.getNodeLabels(node).dataSetName, + scenario: vscode.l10n.t("Unable to copy data set."), + }); } } } diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts index f42617f2a7..3c87466294 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts @@ -26,6 +26,7 @@ import { ZoweScheme, UriFsInfo, FileEntry, + ZoweExplorerApiType, } from "@zowe/zowe-explorer-api"; import { IZosFilesResponse } from "@zowe/zos-files-for-zowe-sdk"; import { Profiles } from "../../configuration/Profiles"; @@ -90,13 +91,20 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem // Locate the resource using the profile in the given URI. let resp; const isPdsMember = !FsDatasetsUtils.isPdsEntry(entry) && (entry as DsEntry).isMember; - if (isPdsMember) { - // PDS member - const pds = this._lookupParentDirectory(uri); - resp = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).allMembers(pds.name, { attributes: true }); - } else { - // Data Set - resp = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).dataSet(path.parse(uri.path).name, { attributes: true }); + try { + if (isPdsMember) { + // PDS member + const pds = this._lookupParentDirectory(uri); + resp = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).allMembers(pds.name, { attributes: true }); + } else { + // Data Set + resp = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).dataSet(path.parse(uri.path).name, { attributes: true }); + } + } catch (err) { + if (err instanceof Error) { + ZoweLogger.error(err.message); + } + throw err; } // Attempt to parse a successful API response and update the data set's cached stats. @@ -131,14 +139,26 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem ), ]; - if (mvsApi.dataSetsMatchingPattern) { - datasetResponses.push(await mvsApi.dataSetsMatchingPattern(dsPatterns)); - } else { - for (const dsp of dsPatterns) { - datasetResponses.push(await mvsApi.dataSet(dsp)); + try { + if (mvsApi.dataSetsMatchingPattern) { + datasetResponses.push(await mvsApi.dataSetsMatchingPattern(dsPatterns)); + } else { + for (const dsp of dsPatterns) { + datasetResponses.push(await mvsApi.dataSet(dsp)); + } } + } catch (err) { + this._handleError(err, { + additionalContext: vscode.l10n.t("Failed to list datasets"), + retry: { + fn: this.fetchEntriesForProfile.bind(this), + args: [uri, uriInfo, pattern], + }, + apiType: ZoweExplorerApiType.Mvs, + profileType: uriInfo.profile?.type, + templateArgs: { profileName: uriInfo.profileName }, + }); } - for (const resp of datasetResponses) { for (const ds of resp.apiResponse?.items ?? resp.apiResponse ?? []) { let tempEntry = profileEntry.entries.get(ds.dsname); @@ -169,7 +189,22 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem } private async fetchEntriesForDataset(entry: PdsEntry, uri: vscode.Uri, uriInfo: UriFsInfo): Promise { - const members = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).allMembers(path.posix.basename(uri.path)); + let members: IZosFilesResponse; + try { + members = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).allMembers(path.posix.basename(uri.path)); + } catch (err) { + this._handleError(err, { + additionalContext: vscode.l10n.t("Failed to list dataset members"), + retry: { + fn: this.fetchEntriesForDataset.bind(this), + args: [entry, uri, uriInfo], + }, + apiType: ZoweExplorerApiType.Mvs, + profileType: uriInfo.profile?.type, + templateArgs: { profileName: uriInfo.profileName }, + }); + throw err; + } const pdsExtension = DatasetUtils.getExtension(entry.name); for (const ds of members.apiResponse?.items || []) { @@ -188,11 +223,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem try { entry = this.lookup(uri, false) as PdsEntry | DsEntry; } catch (err) { - if (!(err instanceof vscode.FileSystemError)) { - throw err; - } - - if (err.code !== "FileNotFound") { + if (!(err instanceof vscode.FileSystemError) || err.code !== "FileNotFound") { throw err; } } @@ -203,26 +234,33 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem const uriPath = uri.path.substring(uriInfo.slashAfterProfilePos + 1).split("/"); const pdsMember = uriPath.length === 2; if (!entryExists) { - if (pdsMember) { - const resp = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).allMembers(uriPath[0]); - entryIsDir = false; - const memberName = path.parse(uriPath[1]).name; - if ( - !resp.success || - resp.apiResponse?.items?.length < 1 || - !resp.apiResponse.items.find((respItem) => respItem.member === memberName) - ) { - throw vscode.FileSystemError.FileNotFound(uri); - } - } else { - const resp = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).dataSet(uriPath[0], { - attributes: true, - }); - if (resp.success && resp.apiResponse?.items?.length > 0) { - entryIsDir = resp.apiResponse.items[0].dsorg?.startsWith("PO"); + try { + if (pdsMember) { + const resp = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).allMembers(uriPath[0]); + entryIsDir = false; + const memberName = path.parse(uriPath[1]).name; + if ( + !resp.success || + resp.apiResponse?.items?.length < 1 || + !resp.apiResponse.items.find((respItem) => respItem.member === memberName) + ) { + throw vscode.FileSystemError.FileNotFound(uri); + } } else { - throw vscode.FileSystemError.FileNotFound(uri); + const resp = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).dataSet(uriPath[0], { + attributes: true, + }); + if (resp.success && resp.apiResponse?.items?.length > 0) { + entryIsDir = resp.apiResponse.items[0].dsorg?.startsWith("PO"); + } else { + throw vscode.FileSystemError.FileNotFound(uri); + } + } + } catch (err) { + if (err instanceof Error) { + ZoweLogger.error(err.message); } + throw err; } } if (entryIsDir) { @@ -298,7 +336,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem } /** - * Creates a directory entry in the provider at the given URI. + * Creates a local directory entry in the provider at the given URI. * @param uri The URI that represents a new directory path */ public createDirectory(uri: vscode.Uri): void { @@ -370,7 +408,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem parentDir.entries.set(dsname, ds); dsEntry = parentDir.entries.get(dsname) as DsEntry; } - //update entry's contents, attributes + if (options?.isConflict) { dsEntry.conflictData = { contents: data, @@ -394,6 +432,9 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem } catch (error) { //Response will error if the file is not found //Callers of fetchDatasetAtUri() do not expect it to throw an error + if (error instanceof Error) { + ZoweLogger.error(error.message); + } return null; } } @@ -411,6 +452,21 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem ds = this._lookupAsFile(uri) as DsEntry; } catch (err) { if (!(err instanceof vscode.FileSystemError) || err.code !== "FileNotFound") { + const uriInfo = this._getInfoFromUri(uri); + this._handleError(err, { + additionalContext: vscode.l10n.t({ + message: "Failed to read {0}", + args: [uri.path], + comment: ["File path"], + }), + apiType: ZoweExplorerApiType.Mvs, + profileType: uriInfo.profile?.type, + retry: { + fn: this.readFile.bind(this), + args: [uri], + }, + templateArgs: { profileName: uriInfo.profile?.name ?? "" }, + }); throw err; } } @@ -543,6 +599,20 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem } } catch (err) { if (!err.message.includes("Rest API failure with HTTP(S) status 412")) { + this._handleError(err, { + additionalContext: vscode.l10n.t({ + message: "Failed to save {0}", + args: [(entry.metadata as DsEntryMetadata).dsName], + comment: ["Data set name"], + }), + apiType: ZoweExplorerApiType.Mvs, + profileType: entry.metadata.profile.type, + retry: { + fn: this.writeFile.bind(this), + args: [uri, content, options], + }, + templateArgs: { profileName: entry.metadata.profile.name ?? "" }, + }); throw err; } @@ -588,14 +658,21 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem responseTimeout: entry.metadata.profile.profile?.responseTimeout, }); } catch (err) { - await Gui.errorMessage( - vscode.l10n.t({ - message: "Deleting {0} failed due to API error: {1}", - args: [entry.metadata.path, err.message], - comment: ["File path", "Error message"], - }) - ); - return; + this._handleError(err, { + additionalContext: vscode.l10n.t({ + message: "Failed to delete {0}", + args: [entry.metadata.path], + comment: ["File path"], + }), + apiType: ZoweExplorerApiType.Mvs, + profileType: entry.metadata.profile.type, + retry: { + fn: this.delete.bind(this), + args: [uri, _options], + }, + templateArgs: { profileName: entry.metadata.profile?.name ?? "" }, + }); + throw err; } parent.entries.delete(entry.name); @@ -628,14 +705,21 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem ); } } catch (err) { - await Gui.errorMessage( - vscode.l10n.t({ - message: "Renaming {0} failed due to API error: {1}", - args: [oldName, err.message], - comment: ["File name", "Error message"], - }) - ); - return; + this._handleError(err, { + additionalContext: vscode.l10n.t({ + message: "Failed to rename {0}", + args: [oldName], + comment: ["Data set name"], + }), + apiType: ZoweExplorerApiType.Mvs, + profileType: entry.metadata.profile.type, + retry: { + fn: this.rename.bind(this), + args: [oldUri, newUri, options], + }, + templateArgs: { profileName: entry.metadata.profile?.name ?? "" }, + }); + throw err; } parentDir.entries.delete(entry.name); diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetTree.ts b/packages/zowe-explorer/src/trees/dataset/DatasetTree.ts index b11f10af35..9be3c67fb9 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetTree.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetTree.ts @@ -24,6 +24,7 @@ import { ZosEncoding, FsAbstractUtils, DatasetMatch, + ZoweExplorerApiType, } from "@zowe/zowe-explorer-api"; import { ZoweDatasetNode } from "./ZoweDatasetNode"; import { DatasetFSProvider } from "./DatasetFSProvider"; @@ -432,7 +433,7 @@ export class DatasetTree extends ZoweTreeProvider implemen if (err.toString().includes("hostname")) { ZoweLogger.error(err); } else { - await AuthUtils.errorHandling(err, profile.name); + await AuthUtils.errorHandling(err, { apiType: ZoweExplorerApiType.Mvs, profile }); } } // Creates ZoweDatasetNode to track new session and pushes it to mSessionNodes @@ -1058,7 +1059,7 @@ export class DatasetTree extends ZoweTreeProvider implemen response = await this.getChildren(sessionNode); }); } catch (err) { - await AuthUtils.errorHandling(err, String(node.label)); + await AuthUtils.errorHandling(err, { apiType: ZoweExplorerApiType.Mvs, profile: node.getProfile() }); } if (response.length === 0) { return; diff --git a/packages/zowe-explorer/src/trees/dataset/ZoweDatasetNode.ts b/packages/zowe-explorer/src/trees/dataset/ZoweDatasetNode.ts index 355c9a5397..ffb41db3c2 100644 --- a/packages/zowe-explorer/src/trees/dataset/ZoweDatasetNode.ts +++ b/packages/zowe-explorer/src/trees/dataset/ZoweDatasetNode.ts @@ -26,6 +26,7 @@ import { ZoweScheme, PdsEntry, FsDatasetsUtils, + ZoweExplorerApiType, } from "@zowe/zowe-explorer-api"; import { DatasetFSProvider } from "./DatasetFSProvider"; import { SharedUtils } from "../shared/SharedUtils"; @@ -193,10 +194,6 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod return dsEntry.stats; } - public setProfileToChoice(profile: imperative.IProfileLoaded): void { - super.setProfileToChoice(profile, DatasetFSProvider.instance); - } - /** * Retrieves child nodes of this ZoweDatasetNode * @@ -239,7 +236,11 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod // Throws reject if the Zowe command does not throw an error but does not succeed // The dataSetsMatchingPattern API may return success=false and apiResponse=[] when no data sets found if (!response.success && !(Array.isArray(response.apiResponse) && response.apiResponse.length === 0)) { - await AuthUtils.errorHandling(vscode.l10n.t("The response from Zowe CLI was not successful")); + await AuthUtils.errorHandling(new imperative.ImperativeError({ msg: response.commandResponse }), { + apiType: ZoweExplorerApiType.Mvs, + profile: cachedProfile, + scenario: vscode.l10n.t("The response from Zowe CLI was not successful"), + }); return []; } @@ -589,7 +590,11 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod responses.push(await ZoweExplorerApiRegister.getMvsApi(profile).allMembers(this.label as string, options)); } } catch (error) { - const updated = await AuthUtils.errorHandling(error, this.getProfileName(), vscode.l10n.t("Retrieving response from MVS list API")); + const updated = await AuthUtils.errorHandling(error, { + apiType: ZoweExplorerApiType.Mvs, + profile: this.getProfile(), + scenario: vscode.l10n.t("Retrieving response from MVS list API"), + }); AuthUtils.syncSessionNode((prof) => ZoweExplorerApiRegister.getMvsApi(prof), this.getSessionNode(), updated && this); return; } @@ -627,7 +632,7 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod datasetProvider.addFileHistory(`[${this.getProfileName()}]: ${this.label as string}`); } } catch (err) { - await AuthUtils.errorHandling(err, this.getProfileName()); + await AuthUtils.errorHandling(err, { apiType: ZoweExplorerApiType.Mvs, profile: this.getProfile() }); throw err; } } diff --git a/packages/zowe-explorer/src/trees/job/JobActions.ts b/packages/zowe-explorer/src/trees/job/JobActions.ts index 9cd315fd98..3d47b6016f 100644 --- a/packages/zowe-explorer/src/trees/job/JobActions.ts +++ b/packages/zowe-explorer/src/trees/job/JobActions.ts @@ -11,7 +11,7 @@ import * as vscode from "vscode"; import * as zosjobs from "@zowe/zos-jobs-for-zowe-sdk"; -import { Gui, IZoweJobTreeNode, Sorting, Types } from "@zowe/zowe-explorer-api"; +import { Gui, IZoweJobTreeNode, Sorting, Types, ZoweExplorerApiType } from "@zowe/zowe-explorer-api"; import { ZoweJobNode } from "./ZoweJobNode"; import { JobTree } from "./JobTree"; import { JobUtils } from "./JobUtils"; @@ -54,7 +54,7 @@ export class JobActions { }) ); } catch (error) { - await AuthUtils.errorHandling(error, job.getProfile().name); + await AuthUtils.errorHandling(error, { apiType: ZoweExplorerApiType.Jes, profile: job.getProfile() }); } } @@ -120,7 +120,7 @@ export class JobActions { if (deletionErrors.length) { const errorMessages = deletionErrors.map((error) => error.message).join(", "); const userMessage = `There were errors during jobs deletion: ${errorMessages}`; - await AuthUtils.errorHandling(userMessage); + await AuthUtils.errorHandling(userMessage, { apiType: ZoweExplorerApiType.Jes }); } } @@ -137,7 +137,7 @@ export class JobActions { try { await jobsProvider.addSession({ sessionName: sessionName.trim() }); } catch (error) { - await AuthUtils.errorHandling(error); + await AuthUtils.errorHandling(error, { apiType: ZoweExplorerApiType.Jes, profile: sessionName }); return; } sessionNode = jobsProvider.mSessionNodes.find((jobNode) => jobNode.label.toString().trim() === sessionName.trim()); @@ -145,7 +145,7 @@ export class JobActions { try { jobsProvider.refreshElement(sessionNode); } catch (error) { - await AuthUtils.errorHandling(error); + await AuthUtils.errorHandling(error, { apiType: ZoweExplorerApiType.Jes, profile: sessionName }); return; } sessionNode.searchId = jobId; @@ -183,7 +183,7 @@ export class JobActions { } } } catch (error) { - await AuthUtils.errorHandling(error); + await AuthUtils.errorHandling(error, { apiType: ZoweExplorerApiType.Jes }); } } @@ -227,7 +227,7 @@ export class JobActions { } } } catch (error) { - await AuthUtils.errorHandling(error); + await AuthUtils.errorHandling(error, { apiType: ZoweExplorerApiType.Jes }); } } @@ -270,7 +270,7 @@ export class JobActions { const jclDoc = await vscode.workspace.openTextDocument({ language: "jcl", content: jobJcl }); await Gui.showTextDocument(jclDoc, { preview: false }); } catch (error) { - await AuthUtils.errorHandling(error); + await AuthUtils.errorHandling(error, { apiType: ZoweExplorerApiType.Jes, profile: job.getProfile() }); } } @@ -307,7 +307,7 @@ export class JobActions { vscode.l10n.t("jobActions.modifyCommand.apiNonExisting", "Not implemented yet for profile of type: ") + job.getProfile().type ); } else { - await AuthUtils.errorHandling(error, job.getProfile().name); + await AuthUtils.errorHandling(error, { apiType: ZoweExplorerApiType.Command, profile: job.getProfile() }); } } } @@ -343,7 +343,7 @@ export class JobActions { }) ); } else { - await AuthUtils.errorHandling(error, job.getProfile().name); + await AuthUtils.errorHandling(error, { apiType: ZoweExplorerApiType.Command, profile: job.getProfile() }); } } } diff --git a/packages/zowe-explorer/src/trees/job/JobFSProvider.ts b/packages/zowe-explorer/src/trees/job/JobFSProvider.ts index fc5dfaf2d6..441cf1b106 100644 --- a/packages/zowe-explorer/src/trees/job/JobFSProvider.ts +++ b/packages/zowe-explorer/src/trees/job/JobFSProvider.ts @@ -25,6 +25,7 @@ import { ZoweScheme, FsJobsUtils, FsAbstractUtils, + ZoweExplorerApiType, } from "@zowe/zowe-explorer-api"; import { IJob, IJobFile } from "@zowe/zos-jobs-for-zowe-sdk"; import { Profiles } from "../../configuration/Profiles"; @@ -89,33 +90,42 @@ export class JobFSProvider extends BaseProvider implements vscode.FileSystemProv const results: [string, vscode.FileType][] = []; const jesApi = ZoweExplorerApiRegister.getJesApi(uriInfo.profile); - if (FsAbstractUtils.isFilterEntry(fsEntry)) { - if (!jesApi.getJobsByParameters) { - throw new Error(vscode.l10n.t("Failed to fetch jobs: getJobsByParameters is not implemented for this session's JES API.")); - } - - const jobFiles = await jesApi.getJobsByParameters({ - owner: fsEntry.filter["owner"] ?? "*", - status: fsEntry.filter["status"] ?? "*", - prefix: fsEntry.filter["prefix"] ?? "*", - }); - for (const job of jobFiles) { - if (!fsEntry.entries.has(job.jobid)) { - const newJob = new JobEntry(job.jobid); - newJob.job = job; - fsEntry.entries.set(job.jobid, newJob); + try { + if (FsAbstractUtils.isFilterEntry(fsEntry)) { + const jobFiles = await jesApi.getJobsByParameters({ + owner: fsEntry.filter["owner"] ?? "*", + status: fsEntry.filter["status"] ?? "*", + prefix: fsEntry.filter["prefix"] ?? "*", + }); + for (const job of jobFiles) { + if (!fsEntry.entries.has(job.jobid)) { + const newJob = new JobEntry(job.jobid); + newJob.job = job; + fsEntry.entries.set(job.jobid, newJob); + } } - } - } else if (FsJobsUtils.isJobEntry(fsEntry)) { - const spoolFiles = await jesApi.getSpoolFiles(fsEntry.job.jobname, fsEntry.job.jobid); - for (const spool of spoolFiles) { - const spoolName = FsJobsUtils.buildUniqueSpoolName(spool); - if (!fsEntry.entries.has(spoolName)) { - const newSpool = new SpoolEntry(spoolName); - newSpool.spool = spool; - fsEntry.entries.set(spoolName, newSpool); + } else if (FsJobsUtils.isJobEntry(fsEntry)) { + const spoolFiles = await jesApi.getSpoolFiles(fsEntry.job.jobname, fsEntry.job.jobid); + for (const spool of spoolFiles) { + const spoolName = FsJobsUtils.buildUniqueSpoolName(spool); + if (!fsEntry.entries.has(spoolName)) { + const newSpool = new SpoolEntry(spoolName); + newSpool.spool = spool; + fsEntry.entries.set(spoolName, newSpool); + } } } + } catch (err) { + this._handleError(err, { + apiType: ZoweExplorerApiType.Jes, + profileType: uriInfo.profile?.type, + retry: { + fn: this.readDirectory.bind(this), + args: [uri], + }, + templateArgs: { profileName: uriInfo.profile?.name ?? "" }, + }); + throw err; } for (const entry of fsEntry.entries) { @@ -192,7 +202,6 @@ export class JobFSProvider extends BaseProvider implements vscode.FileSystemProv const bufBuilder = new BufferBuilder(); const jesApi = ZoweExplorerApiRegister.getJesApi(spoolEntry.metadata.profile); - if (jesApi.downloadSingleSpool) { await jesApi.downloadSingleSpool({ jobFile: spoolEntry.spool, @@ -222,7 +231,25 @@ export class JobFSProvider extends BaseProvider implements vscode.FileSystemProv public async readFile(uri: vscode.Uri): Promise { const spoolEntry = this._lookupAsFile(uri) as SpoolEntry; if (!spoolEntry.wasAccessed) { - await this.fetchSpoolAtUri(uri); + try { + await this.fetchSpoolAtUri(uri); + } catch (err) { + this._handleError(err, { + additionalContext: vscode.l10n.t({ + message: "Failed to get contents for {0}", + args: [spoolEntry.name], + comment: "Spool name", + }), + apiType: ZoweExplorerApiType.Jes, + profileType: spoolEntry.metadata.profile.type, + retry: { + fn: this.readFile.bind(this), + args: [uri], + }, + templateArgs: { profileName: spoolEntry.metadata.profile?.name ?? "" }, + }); + throw err; + } spoolEntry.wasAccessed = true; } @@ -289,9 +316,26 @@ export class JobFSProvider extends BaseProvider implements vscode.FileSystemProv } const parent = this._lookupParentDirectory(uri, false); - const profInfo = FsAbstractUtils.getInfoForUri(uri, Profiles.getInstance()); - await ZoweExplorerApiRegister.getJesApi(profInfo.profile).deleteJob(entry.job.jobname, entry.job.jobid); + try { + await ZoweExplorerApiRegister.getJesApi(profInfo.profile).deleteJob(entry.job.jobname, entry.job.jobid); + } catch (err) { + this._handleError(err, { + additionalContext: vscode.l10n.t({ + message: "Failed to delete job {0}", + args: [entry.job.jobname], + comment: "Job name", + }), + apiType: ZoweExplorerApiType.Jes, + profileType: profInfo.profile.type, + retry: { + fn: this.delete.bind(this), + args: [uri, options], + }, + templateArgs: { profileName: profInfo.profile.name ?? "" }, + }); + throw err; + } parent.entries.delete(entry.name); this._fireSoon({ type: vscode.FileChangeType.Deleted, uri }); } diff --git a/packages/zowe-explorer/src/trees/job/JobTree.ts b/packages/zowe-explorer/src/trees/job/JobTree.ts index 3c6f5787aa..377ae27ead 100644 --- a/packages/zowe-explorer/src/trees/job/JobTree.ts +++ b/packages/zowe-explorer/src/trees/job/JobTree.ts @@ -12,7 +12,7 @@ import * as vscode from "vscode"; import * as path from "path"; import { IJob } from "@zowe/zos-jobs-for-zowe-sdk"; -import { Gui, Validation, imperative, IZoweJobTreeNode, PersistenceSchemaEnum, Poller, Types } from "@zowe/zowe-explorer-api"; +import { Gui, Validation, imperative, IZoweJobTreeNode, PersistenceSchemaEnum, Poller, Types, ZoweExplorerApiType } from "@zowe/zowe-explorer-api"; import { ZoweJobNode } from "./ZoweJobNode"; import { JobFSProvider } from "./JobFSProvider"; import { JobUtils } from "./JobUtils"; @@ -193,7 +193,7 @@ export class JobTree extends ZoweTreeProvider implements Types if (err.toString().includes("hostname")) { ZoweLogger.error(err); } else { - await AuthUtils.errorHandling(err, profile.name); + await AuthUtils.errorHandling(err, { apiType: ZoweExplorerApiType.Jes, profile }); } } // Creates ZoweNode to track new session and pushes it to mSessionNodes diff --git a/packages/zowe-explorer/src/trees/job/ZoweJobNode.ts b/packages/zowe-explorer/src/trees/job/ZoweJobNode.ts index 4d4f7dfc86..4072a6ee77 100644 --- a/packages/zowe-explorer/src/trees/job/ZoweJobNode.ts +++ b/packages/zowe-explorer/src/trees/job/ZoweJobNode.ts @@ -12,7 +12,7 @@ import * as vscode from "vscode"; import * as zosjobs from "@zowe/zos-jobs-for-zowe-sdk"; import * as path from "path"; -import { FsJobsUtils, imperative, IZoweJobTreeNode, Sorting, ZoweScheme, ZoweTreeNode } from "@zowe/zowe-explorer-api"; +import { FsJobsUtils, imperative, IZoweJobTreeNode, Sorting, ZoweExplorerApiType, ZoweScheme, ZoweTreeNode } from "@zowe/zowe-explorer-api"; import { JobFSProvider } from "./JobFSProvider"; import { JobUtils } from "./JobUtils"; import { Constants } from "../../configuration/Constants"; @@ -262,10 +262,6 @@ export class ZoweJobNode extends ZoweTreeNode implements IZoweJobTreeNode { return this.children; } - public setProfileToChoice(profile: imperative.IProfileLoaded): void { - super.setProfileToChoice(profile, JobFSProvider.instance); - } - public static sortJobs(sortOpts: Sorting.NodeSort): (x: IZoweJobTreeNode, y: IZoweJobTreeNode) => number { return (x, y) => { const sortLessThan = sortOpts.direction == Sorting.SortDirection.Ascending ? -1 : 1; @@ -396,7 +392,11 @@ export class ZoweJobNode extends ZoweTreeNode implements IZoweJobTreeNode { }, []); } } catch (error) { - const updated = await AuthUtils.errorHandling(error, this.getProfileName(), vscode.l10n.t("Retrieving response from JES list API")); + const updated = await AuthUtils.errorHandling(error, { + apiType: ZoweExplorerApiType.Jes, + profile: this.getProfile(), + scenario: vscode.l10n.t("Retrieving response from JES list API"), + }); AuthUtils.syncSessionNode((profile) => ZoweExplorerApiRegister.getJesApi(profile), this.getSessionNode(), updated && this); return; } @@ -413,7 +413,11 @@ export class ZoweJobNode extends ZoweTreeNode implements IZoweJobTreeNode { // see an issue #845 for the details spools = spools.filter((item) => !(item.id === undefined && item.ddname === undefined && item.stepname === undefined)); } catch (error) { - const updated = await AuthUtils.errorHandling(error, this.getProfileName(), vscode.l10n.t("Retrieving response from JES list API")); + const updated = await AuthUtils.errorHandling(error, { + apiType: ZoweExplorerApiType.Jes, + profile: this.getProfile(), + scenario: vscode.l10n.t("Retrieving response from JES list API"), + }); AuthUtils.syncSessionNode((profile) => ZoweExplorerApiRegister.getJesApi(profile), this.getSessionNode(), updated && this); return; } diff --git a/packages/zowe-explorer/src/trees/shared/SharedInit.ts b/packages/zowe-explorer/src/trees/shared/SharedInit.ts index f275ef658e..dafe3d0f28 100644 --- a/packages/zowe-explorer/src/trees/shared/SharedInit.ts +++ b/packages/zowe-explorer/src/trees/shared/SharedInit.ts @@ -13,6 +13,7 @@ import * as vscode from "vscode"; import { FileManagement, Gui, + CorrelatedError, IZoweTree, IZoweTreeNode, TableViewProvider, @@ -47,6 +48,7 @@ import { TreeViewUtils } from "../../utils/TreeViewUtils"; import { CertificateWizard } from "../../utils/CertificateWizard"; import { ZosConsoleViewProvider } from "../../zosconsole/ZosConsolePanel"; import { ZoweUriHandler } from "../../utils/UriHandler"; +import { TroubleshootError } from "../../utils/TroubleshootError"; export class SharedInit { private static originalEmitZoweEvent: typeof imperative.EventProcessor.prototype.emitEvent; @@ -281,6 +283,17 @@ export class SharedInit { context.subscriptions.push( vscode.commands.registerCommand("zowe.copyExternalLink", (node: IZoweTreeNode) => SharedUtils.copyExternalLink(context, node)) ); + context.subscriptions.push( + vscode.commands.registerCommand("zowe.revealOutputChannel", (): void => { + ZoweLogger.zeOutputChannel.show(); + }) + ); + context.subscriptions.push( + vscode.commands.registerCommand( + "zowe.troubleshootError", + (error: CorrelatedError, stackTrace?: string) => new TroubleshootError(context, { error, stackTrace }) + ) + ); context.subscriptions.push(vscode.window.registerUriHandler(ZoweUriHandler.getInstance())); context.subscriptions.push( vscode.commands.registerCommand("zowe.placeholderCommand", () => { diff --git a/packages/zowe-explorer/src/trees/uss/USSActions.ts b/packages/zowe-explorer/src/trees/uss/USSActions.ts index ffb3ecf4ae..ed6e4aad93 100644 --- a/packages/zowe-explorer/src/trees/uss/USSActions.ts +++ b/packages/zowe-explorer/src/trees/uss/USSActions.ts @@ -13,7 +13,7 @@ import * as vscode from "vscode"; import * as fs from "fs"; import * as path from "path"; import * as zosfiles from "@zowe/zos-files-for-zowe-sdk"; -import { Gui, imperative, IZoweUSSTreeNode, Types } from "@zowe/zowe-explorer-api"; +import { Gui, imperative, IZoweUSSTreeNode, Types, ZoweExplorerApiType } from "@zowe/zowe-explorer-api"; import { isBinaryFileSync } from "isbinaryfile"; import { USSAttributeView } from "./USSAttributeView"; import { USSFileStructure } from "./USSFileStructure"; @@ -99,7 +99,11 @@ export class USSActions { } } catch (err) { if (err instanceof Error) { - await AuthUtils.errorHandling(err, node.getProfileName(), vscode.l10n.t("Unable to create node:")); + await AuthUtils.errorHandling(err, { + apiType: ZoweExplorerApiType.Uss, + profile: node.getProfile(), + scenario: vscode.l10n.t("Unable to create node:"), + }); } throw err; } @@ -117,7 +121,7 @@ export class USSActions { await node.getChildren(); ussFileProvider.refreshElement(node); } catch (err) { - await AuthUtils.errorHandling(err, node.getProfileName()); + await AuthUtils.errorHandling(err, { apiType: ZoweExplorerApiType.Uss, profile: node.getProfile() }); } } @@ -188,7 +192,7 @@ export class USSActions { const ussName = `${node.fullPath}/${localFileName}`; await ZoweExplorerApiRegister.getUssApi(node.getProfile()).putContent(filePath, ussName, { binary: true }); } catch (e) { - await AuthUtils.errorHandling(e, node.getProfileName()); + await AuthUtils.errorHandling(e, { apiType: ZoweExplorerApiType.Uss, profile: node.getProfile() }); } } @@ -213,7 +217,7 @@ export class USSActions { } await ZoweExplorerApiRegister.getUssApi(prof).putContent(doc.fileName, ussName, options); } catch (e) { - await AuthUtils.errorHandling(e, node.getProfileName()); + await AuthUtils.errorHandling(e, { apiType: ZoweExplorerApiType.Uss, profile: node.getProfile() }); } } diff --git a/packages/zowe-explorer/src/trees/uss/USSTree.ts b/packages/zowe-explorer/src/trees/uss/USSTree.ts index 7d244b5d3b..a7e74cc951 100644 --- a/packages/zowe-explorer/src/trees/uss/USSTree.ts +++ b/packages/zowe-explorer/src/trees/uss/USSTree.ts @@ -20,6 +20,7 @@ import { Types, Validation, ZosEncoding, + ZoweExplorerApiType, ZoweScheme, } from "@zowe/zowe-explorer-api"; import { UssFSProvider } from "./UssFSProvider"; @@ -333,7 +334,11 @@ export class USSTree extends ZoweTreeProvider implements Types this.updateFavorites(); } catch (err) { if (err instanceof Error) { - await AuthUtils.errorHandling(err, originalNode.getProfileName(), vscode.l10n.t("Unable to rename node:")); + await AuthUtils.errorHandling(err, { + apiType: ZoweExplorerApiType.Uss, + profile: originalNode.getProfile(), + scenario: vscode.l10n.t("Unable to rename node:"), + }); } throw err; } @@ -486,7 +491,7 @@ export class USSTree extends ZoweTreeProvider implements Types if (err.toString().includes("hostname")) { ZoweLogger.error(err); } else { - await AuthUtils.errorHandling(err, profile.name); + await AuthUtils.errorHandling(err, { apiType: ZoweExplorerApiType.Uss, profile }); } } // Creates ZoweNode to track new session and pushes it to mSessionNodes diff --git a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts index 6938072482..69e6daaa77 100644 --- a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts +++ b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts @@ -23,6 +23,7 @@ import { ZosEncoding, ZoweScheme, UriFsInfo, + ZoweExplorerApiType, } from "@zowe/zowe-explorer-api"; import { IZosFilesResponse } from "@zowe/zos-files-for-zowe-sdk"; import { USSFileStructure } from "./USSFileStructure"; @@ -120,7 +121,21 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv const oldInfo = this._getInfoFromUri(oldUri); - await ussApi.move(oldInfo.path, info.path); + try { + await ussApi.move(oldInfo.path, info.path); + } catch (err) { + this._handleError(err, { + additionalContext: vscode.l10n.t({ message: "Failed to move {0}", args: [oldInfo.path], comment: "File path" }), + apiType: ZoweExplorerApiType.Uss, + retry: { + fn: this.move.bind(this), + args: [oldUri, newUri], + }, + profileType: info.profile.type, + templateArgs: { profileName: info.profile.name ?? "" }, + }); + throw err; + } await this._relocateEntry(oldUri, newUri, info.path); return true; } @@ -128,15 +143,18 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv public async listFiles(profile: imperative.IProfileLoaded, uri: vscode.Uri, keepRelative: boolean = false): Promise { const queryParams = new URLSearchParams(uri.query); const ussPath = queryParams.has("searchPath") ? queryParams.get("searchPath") : uri.path.substring(uri.path.indexOf("/", 1)); - if (ussPath.length === 0) { - throw new imperative.ImperativeError({ - msg: vscode.l10n.t("Could not list USS files: Empty path provided in URI"), - }); - } - const response = await ZoweExplorerApiRegister.getUssApi(profile).fileList(ussPath); - // If request was successful, create directories for the path if it doesn't exist - if (response.success && !keepRelative && response.apiResponse.items?.[0]?.mode?.startsWith("d") && !this.exists(uri)) { - await vscode.workspace.fs.createDirectory(uri); + let response: IZosFilesResponse; + try { + response = await ZoweExplorerApiRegister.getUssApi(profile).fileList(ussPath); + // If request was successful, create directories for the path if it doesn't exist + if (response.success && !keepRelative && response.apiResponse.items?.[0]?.mode?.startsWith("d") && !this.exists(uri)) { + await vscode.workspace.fs.createDirectory(uri); + } + } catch (err) { + if (err instanceof Error) { + ZoweLogger.error(err.message); + } + throw err; } return { @@ -181,7 +199,7 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv } const fileList = entryExists ? await this.listFiles(entry.metadata.profile, uri) : resp; - for (const item of fileList.apiResponse.items) { + for (const item of fileList.apiResponse?.items ?? []) { const itemName = item.name as string; const isDirectory = item.mode?.startsWith("d") ?? false; @@ -262,13 +280,33 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv const metadata = file.metadata; await this.autoDetectEncoding(file as UssFile); const profileEncoding = file.encoding ? null : file.metadata.profile.profile?.encoding; - const resp = await ZoweExplorerApiRegister.getUssApi(metadata.profile).getContents(filePath, { - binary: file.encoding?.kind === "binary", - encoding: file.encoding?.kind === "other" ? file.encoding.codepage : profileEncoding, - responseTimeout: metadata.profile.profile?.responseTimeout, - returnEtag: true, - stream: bufBuilder, - }); + + let resp: IZosFilesResponse; + try { + resp = await ZoweExplorerApiRegister.getUssApi(metadata.profile).getContents(filePath, { + binary: file.encoding?.kind === "binary", + encoding: file.encoding?.kind === "other" ? file.encoding.codepage : profileEncoding, + responseTimeout: metadata.profile.profile?.responseTimeout, + returnEtag: true, + stream: bufBuilder, + }); + } catch (err) { + this._handleError(err, { + additionalContext: vscode.l10n.t({ + message: "Failed to get contents for {0}", + args: [filePath], + comment: ["File path"], + }), + retry: { + fn: this.fetchFileAtUri.bind(this), + args: [uri, options], + }, + apiType: ZoweExplorerApiType.Uss, + profileType: metadata.profile.type, + templateArgs: { profileName: metadata.profile.name }, + }); + throw err; + } const data: Uint8Array = bufBuilder.read() ?? new Uint8Array(); if (options?.isConflict) { @@ -465,6 +503,15 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv } catch (err) { if (!err.message.includes("Rest API failure with HTTP(S) status 412")) { // Some unknown error happened, don't update the entry + this._handleError(err, { + apiType: ZoweExplorerApiType.Uss, + retry: { + fn: this.writeFile.bind(this), + args: [uri, content, options], + }, + profileType: parentDir.metadata.profile.type, + templateArgs: { profileName: parentDir.metadata.profile.name ?? "" }, + }); throw err; } @@ -518,14 +565,21 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv try { await ZoweExplorerApiRegister.getUssApi(entry.metadata.profile).rename(entry.metadata.path, newPath); } catch (err) { - await Gui.errorMessage( - vscode.l10n.t({ - message: "Renaming {0} failed due to API error: {1}", - args: [entry.metadata.path, err.message], - comment: ["File path", "Error message"], - }) - ); - return; + this._handleError(err, { + additionalContext: vscode.l10n.t({ + message: "Failed to rename {0}", + args: [entry.metadata.path], + comment: ["File path"], + }), + retry: { + fn: this.rename.bind(this), + args: [oldUri, newUri, options], + }, + apiType: ZoweExplorerApiType.Uss, + profileType: entry.metadata.profile?.type, + templateArgs: { profileName: entry.metadata.profile?.name ?? "" }, + }); + throw err; } parentDir.entries.delete(entry.name); @@ -554,14 +608,21 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv entryToDelete instanceof UssDirectory ); } catch (err) { - await Gui.errorMessage( - vscode.l10n.t({ - message: "Deleting {0} failed due to API error: {1}", - args: [entryToDelete.metadata.path, err.message], - comment: ["File name", "Error message"], - }) - ); - return; + this._handleError(err, { + additionalContext: vscode.l10n.t({ + message: "Failed to delete {0}", + args: [entryToDelete.metadata.path], + comment: ["File name"], + }), + retry: { + fn: this.delete.bind(this), + args: [uri, _options], + }, + apiType: ZoweExplorerApiType.Uss, + profileType: parent.metadata.profile.type, + templateArgs: { profileName: parent.metadata.profile.name ?? "" }, + }); + throw err; } parent.entries.delete(entryToDelete.name); @@ -636,35 +697,53 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv const fileName = this.buildFileName(fileList, path.basename(sourceInfo.path)); const outputPath = path.posix.join(destInfo.path, fileName); - if (hasCopyApi && sourceInfo.profile.profile === destInfo.profile.profile) { - await api.copy(outputPath, { - from: sourceInfo.path, - recursive: options.tree.type === USSFileStructure.UssFileType.Directory, - overwrite: options.overwrite ?? true, - }); - } else if (options.tree.type === USSFileStructure.UssFileType.Directory) { - // Not all APIs respect the recursive option, so it's best to - // create a directory and copy recursively to avoid missing any files/folders - await api.create(outputPath, "directory"); - if (options.tree.children) { - for (const child of options.tree.children) { - await this.copyTree( - child.localUri, - vscode.Uri.from({ - scheme: ZoweScheme.USS, - path: path.posix.join(destInfo.profile.name, outputPath, child.baseName), - }), - { ...options, tree: child } - ); + try { + if (hasCopyApi && sourceInfo.profile.profile === destInfo.profile.profile) { + await api.copy(outputPath, { + from: sourceInfo.path, + recursive: options.tree.type === USSFileStructure.UssFileType.Directory, + overwrite: options.overwrite ?? true, + }); + } else if (options.tree.type === USSFileStructure.UssFileType.Directory) { + // Not all APIs respect the recursive option, so it's best to + // create a directory and copy recursively to avoid missing any files/folders + await api.create(outputPath, "directory"); + if (options.tree.children) { + for (const child of options.tree.children) { + await this.copyTree( + child.localUri, + vscode.Uri.from({ + scheme: ZoweScheme.USS, + path: path.posix.join(destInfo.profile.name, outputPath, child.baseName), + }), + { ...options, tree: child } + ); + } } + } else { + const fileEntry = this.lookup(source); + if (!fileEntry.wasAccessed) { + // must fetch contents of file first before pasting in new path + await this.readFile(source); + } + await api.uploadFromBuffer(Buffer.from(fileEntry.data), outputPath); } - } else { - const fileEntry = this.lookup(source); - if (!fileEntry.wasAccessed) { - // must fetch contents of file first before pasting in new path - await this.readFile(source); - } - await api.uploadFromBuffer(Buffer.from(fileEntry.data), outputPath); + } catch (err) { + this._handleError(err, { + additionalContext: vscode.l10n.t({ + message: "Failed to copy {0} to {1}", + args: [source.path, destination.path, err.message], + comment: ["Source path", "Destination path"], + }), + retry: { + fn: this.copyTree.bind(this), + args: [source, destination, options], + }, + apiType: ZoweExplorerApiType.Uss, + profileType: destInfo.profile.type, + templateArgs: { profileName: destInfo.profile.name ?? "" }, + }); + throw err; } } diff --git a/packages/zowe-explorer/src/trees/uss/ZoweUSSNode.ts b/packages/zowe-explorer/src/trees/uss/ZoweUSSNode.ts index 364532e9a5..a50d6c341e 100644 --- a/packages/zowe-explorer/src/trees/uss/ZoweUSSNode.ts +++ b/packages/zowe-explorer/src/trees/uss/ZoweUSSNode.ts @@ -25,6 +25,7 @@ import { UssDirectory, FsAbstractUtils, MainframeInteraction, + ZoweExplorerApiType, } from "@zowe/zowe-explorer-api"; import { USSUtils } from "./USSUtils"; import { Constants } from "../../configuration/Constants"; @@ -160,10 +161,6 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { ussEntry.attributes = { ...ussEntry.attributes, ...attributes }; } - public setProfileToChoice(profile: imperative.IProfileLoaded): void { - super.setProfileToChoice(profile, UssFSProvider.instance); - } - public get onUpdate(): vscode.Event { return this.onUpdateEmitter.event; } @@ -540,7 +537,7 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { ussFileProvider.getTreeView().reveal(this, { select: true, focus: true, expand: false }); } } catch (err) { - await AuthUtils.errorHandling(err, this.getProfileName()); + await AuthUtils.errorHandling(err, { apiType: ZoweExplorerApiType.Uss, profile: this.getProfile() }); throw err; } } @@ -581,7 +578,7 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { }) ); } else { - await AuthUtils.errorHandling(err, this.getProfileName()); + await AuthUtils.errorHandling(err, { apiType: ZoweExplorerApiType.Uss, profile: this.getProfile() }); } } } @@ -654,7 +651,11 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { ); } } catch (error) { - await AuthUtils.errorHandling(error, this.label.toString(), vscode.l10n.t("Error uploading files")); + await AuthUtils.errorHandling(error, { + apiType: ZoweExplorerApiType.Uss, + profile: this.getProfile(), + scenario: vscode.l10n.t("Error uploading files"), + }); } } @@ -680,7 +681,11 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { return await UssFSProvider.instance.listFiles(profile, this.resourceUri); } } catch (error) { - const updated = await AuthUtils.errorHandling(error, this.getProfileName(), vscode.l10n.t("Retrieving response from USS list API")); + const updated = await AuthUtils.errorHandling(error, { + apiType: ZoweExplorerApiType.Uss, + profile: this.getProfile(), + scenario: vscode.l10n.t("Retrieving response from USS list API"), + }); AuthUtils.syncSessionNode((prof) => ZoweExplorerApiRegister.getUssApi(prof), this.getSessionNode(), updated && this); return { success: false, commandResponse: null }; } diff --git a/packages/zowe-explorer/src/utils/AuthUtils.ts b/packages/zowe-explorer/src/utils/AuthUtils.ts index 2aac284b44..d648b1a13e 100644 --- a/packages/zowe-explorer/src/utils/AuthUtils.ts +++ b/packages/zowe-explorer/src/utils/AuthUtils.ts @@ -11,84 +11,100 @@ import * as util from "util"; import * as vscode from "vscode"; -import { imperative, Gui, MainframeInteraction, IZoweTreeNode } from "@zowe/zowe-explorer-api"; +import { imperative, Gui, MainframeInteraction, IZoweTreeNode, ErrorCorrelator, ZoweExplorerApiType, CorrelatedError } from "@zowe/zowe-explorer-api"; import { Constants } from "../configuration/Constants"; import { ZoweLogger } from "../tools/ZoweLogger"; import { SharedTreeProviders } from "../trees/shared/SharedTreeProviders"; +interface ErrorContext { + apiType?: ZoweExplorerApiType; + profile?: string | imperative.IProfileLoaded; + scenario?: string; + [key: string]: any; +} + export class AuthUtils { + public static async promptForAuthentication( + imperativeError: imperative.ImperativeError, + correlation: CorrelatedError, + profile: imperative.IProfileLoaded + ): Promise { + if (imperativeError.mDetails.additionalDetails) { + const tokenError: string = imperativeError.mDetails.additionalDetails; + const isTokenAuth = await AuthUtils.isUsingTokenAuth(profile.name); + + if (tokenError.includes("Token is not valid or expired.") || isTokenAuth) { + const message = vscode.l10n.t("Log in to Authentication Service"); + const success = Gui.showMessage(correlation.message, { items: [message] }).then(async (selection) => { + if (selection) { + return Constants.PROFILES_CACHE.ssoLogin(null, profile.name); + } + }); + return success; + } + } + const checkCredsButton = vscode.l10n.t("Update Credentials"); + const creds = await Gui.errorMessage(correlation.message, { + items: [checkCredsButton], + vsCodeOpts: { modal: true }, + }).then(async (selection) => { + if (selection !== checkCredsButton) { + return; + } + return Constants.PROFILES_CACHE.promptCredentials(profile, true); + }); + return creds != null ? true : false; + } + + public static async openConfigForMissingHostname(profile: imperative.IProfileLoaded): Promise { + const mProfileInfo = await Constants.PROFILES_CACHE.getProfileInfo(); + Gui.errorMessage(vscode.l10n.t("Required parameter 'host' must not be blank.")); + const profAllAttrs = mProfileInfo.getAllProfiles(); + for (const prof of profAllAttrs) { + if (prof.profName === profile?.name) { + const filePath = prof.profLoc.osLoc[0]; + await Constants.PROFILES_CACHE.openConfigFile(filePath); + } + } + } + /************************************************************************************************************* * Error Handling * @param {errorDetails} - string or error object * @param {label} - additional information such as profile name, credentials, messageID etc * @param {moreInfo} - additional/customized error messages *************************************************************************************************************/ - public static async errorHandling(errorDetails: Error | string, label?: string, moreInfo?: string): Promise { + public static async errorHandling(errorDetails: Error | string, moreInfo?: ErrorContext): Promise { // Use util.inspect instead of JSON.stringify to handle circular references // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - ZoweLogger.error(`${errorDetails.toString()}\n` + util.inspect({ errorDetails, label, moreInfo }, { depth: null })); + ZoweLogger.error(`${errorDetails.toString()}\n` + util.inspect({ errorDetails, ...{ ...moreInfo, profile: undefined } }, { depth: null })); + + const profile = typeof moreInfo.profile === "string" ? Constants.PROFILES_CACHE.loadNamedProfile(moreInfo.profile) : moreInfo?.profile; + const correlation = ErrorCorrelator.getInstance().correlateError(moreInfo?.apiType ?? ZoweExplorerApiType.All, errorDetails, { + profileType: profile?.type, + ...Object.keys(moreInfo).reduce((all, k) => (typeof moreInfo[k] === "string" ? { ...all, [k]: moreInfo[k] } : all), {}), + templateArgs: { profileName: profile?.name ?? "", ...moreInfo?.templateArgs }, + }); if (typeof errorDetails !== "string" && (errorDetails as imperative.ImperativeError)?.mDetails !== undefined) { const imperativeError: imperative.ImperativeError = errorDetails as imperative.ImperativeError; const httpErrorCode = Number(imperativeError.mDetails.errorCode); // open config file for missing hostname error if (imperativeError.toString().includes("hostname")) { - const mProfileInfo = await Constants.PROFILES_CACHE.getProfileInfo(); - Gui.errorMessage(vscode.l10n.t("Required parameter 'host' must not be blank.")); - const profAllAttrs = mProfileInfo.getAllProfiles(); - for (const prof of profAllAttrs) { - if (prof.profName === label.trim()) { - const filePath = prof.profLoc.osLoc[0]; - await Constants.PROFILES_CACHE.openConfigFile(filePath); - return false; - } - } + await AuthUtils.openConfigForMissingHostname(profile); + return false; } else if ( - httpErrorCode === imperative.RestConstants.HTTP_STATUS_401 || - imperativeError.message.includes("All configured authentication methods failed") + profile != null && + (httpErrorCode === imperative.RestConstants.HTTP_STATUS_401 || + imperativeError.message.includes("All configured authentication methods failed")) ) { - const errMsg = vscode.l10n.t({ - message: - "Invalid Credentials for profile '{0}'. Please ensure the username and password are valid or this may lead to a lock-out.", - args: [label], - comment: ["Label"], - }); - if (label.includes("[")) { - label = label.substring(0, label.indexOf(" [")).trim(); - } - - if (imperativeError.mDetails.additionalDetails) { - const tokenError: string = imperativeError.mDetails.additionalDetails; - const isTokenAuth = await AuthUtils.isUsingTokenAuth(label); - - if (tokenError.includes("Token is not valid or expired.") || isTokenAuth) { - AuthUtils.promptUserForSsoLogin(label); - return; - } - } - const checkCredsButton = vscode.l10n.t("Update Credentials"); - const creds = await Gui.errorMessage(errMsg, { - items: [checkCredsButton], - vsCodeOpts: { modal: true }, - }).then(async (selection) => { - if (selection !== checkCredsButton) { - Gui.showMessage(vscode.l10n.t("Operation cancelled")); - return; - } - return Constants.PROFILES_CACHE.promptCredentials(label.trim(), true); - }); - return creds != null ? true : false; + return AuthUtils.promptForAuthentication(imperativeError, correlation, profile); } } if (errorDetails.toString().includes("Could not find profile")) { return false; } - if (moreInfo === undefined) { - moreInfo = errorDetails.toString().includes("Error") ? "" : "Error: "; - } else { - moreInfo += " "; - } - // Try to keep message readable since VS Code doesn't support newlines in error messages - Gui.errorMessage(moreInfo + errorDetails.toString().replace(/\n/g, " | ")); + + await ErrorCorrelator.getInstance().displayCorrelatedError(correlation, { templateArgs: { profileName: profile?.name ?? "" } }); return false; } diff --git a/packages/zowe-explorer/src/utils/ProfilesUtils.ts b/packages/zowe-explorer/src/utils/ProfilesUtils.ts index a395b51d03..a54adae25a 100644 --- a/packages/zowe-explorer/src/utils/ProfilesUtils.ts +++ b/packages/zowe-explorer/src/utils/ProfilesUtils.ts @@ -570,7 +570,7 @@ export class ProfilesUtils { ZoweLogger.info(vscode.l10n.t("Zowe profiles initialized successfully.")); } catch (err) { if (err instanceof imperative.ImperativeError) { - await AuthUtils.errorHandling(err, undefined, err.mDetails.causeErrors); + await AuthUtils.errorHandling(err, { scenario: err.mDetails.causeErrors }); } else { ZoweLogger.error(err); errorCallback(err.message); diff --git a/packages/zowe-explorer/src/utils/TroubleshootError.ts b/packages/zowe-explorer/src/utils/TroubleshootError.ts new file mode 100644 index 0000000000..b9e2c2c60c --- /dev/null +++ b/packages/zowe-explorer/src/utils/TroubleshootError.ts @@ -0,0 +1,62 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { CorrelatedError, WebView } from "@zowe/zowe-explorer-api"; +import { env, ExtensionContext, l10n } from "vscode"; +import { ZoweLogger } from "../tools/ZoweLogger"; + +type TroubleshootData = { + error: CorrelatedError; + stackTrace?: string; +}; + +export class TroubleshootError extends WebView { + public constructor(context: ExtensionContext, public errorData: TroubleshootData) { + super(l10n.t("Troubleshoot Error"), "troubleshoot-error", context, { + onDidReceiveMessage: (message: object) => this.onDidReceiveMessage(message), + }); + } + + public async onDidReceiveMessage(message: object): Promise { + if (!("command" in message)) { + return; + } + + switch (message.command) { + case "ready": + await this.sendErrorData(this.errorData); + break; + case "copy": + await env.clipboard.writeText( + this.errorData.error.stack + ? `Error details:\n${this.errorData.error.message}\nStack trace:\n${this.errorData.error.stack.replace(/(.+?)\n/, "")}` + : `Error details:\n${this.errorData.error.message}` + ); + break; + default: + ZoweLogger.debug(`[TroubleshootError] Unknown command: ${message.command as string}`); + break; + } + } + + /** + * Propagate error data to the webview + * + * @param errorData Error and stack trace + * @returns Whether Zowe Explorer successfully sent the data to the webview + */ + public async sendErrorData(errorData: TroubleshootData): Promise { + return this.panel.webview.postMessage({ + error: errorData.error, + stackTrace: errorData.stackTrace, + }); + } +} diff --git a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentVSCodeAPI.ts b/packages/zowe-explorer/src/webviews/src/PersistentVSCodeAPI.ts similarity index 100% rename from packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentVSCodeAPI.ts rename to packages/zowe-explorer/src/webviews/src/PersistentVSCodeAPI.ts diff --git a/packages/zowe-explorer/src/webviews/src/edit-history/App.tsx b/packages/zowe-explorer/src/webviews/src/edit-history/App.tsx index 859cfa36a3..a28fe785ae 100644 --- a/packages/zowe-explorer/src/webviews/src/edit-history/App.tsx +++ b/packages/zowe-explorer/src/webviews/src/edit-history/App.tsx @@ -14,7 +14,7 @@ import { VSCodeDivider, VSCodePanels, VSCodePanelTab } from "@vscode/webview-ui- import { JSXInternal } from "preact/src/jsx"; import { isSecureOrigin } from "../utils"; import PersistentDataPanel from "./components/PersistentTable/PersistentDataPanel"; -import PersistentVSCodeAPI from "./components/PersistentVSCodeAPI"; +import PersistentVSCodeAPI from "../PersistentVSCodeAPI"; import PersistentManagerHeader from "./components/PersistentManagerHeader/PersistentManagerHeader"; import * as l10n from "@vscode/l10n"; diff --git a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentTable/PersistentDataPanel.tsx b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentTable/PersistentDataPanel.tsx index 78a41ae7bc..b3bef8545b 100644 --- a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentTable/PersistentDataPanel.tsx +++ b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentTable/PersistentDataPanel.tsx @@ -18,7 +18,7 @@ import { panelId } from "../../types"; import PersistentToolBar from "../PersistentToolBar/PersistentToolBar"; import PersistentTableData from "./PersistentTableData"; import PersistentDataGridHeaders from "./PersistentDataGridHeaders"; -import PersistentVSCodeAPI from "../PersistentVSCodeAPI"; +import PersistentVSCodeAPI from "../../../PersistentVSCodeAPI"; export default function PersistentDataPanel({ type }: Readonly<{ type: Readonly }>): JSXInternal.Element { const [data, setData] = useState<{ [type: string]: { [property: string]: string[] } }>({ ds: {}, uss: {}, jobs: {} }); diff --git a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentAddNewHistoryItemButton.tsx b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentAddNewHistoryItemButton.tsx index 7f25e39aa4..9f7a0f637f 100644 --- a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentAddNewHistoryItemButton.tsx +++ b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentAddNewHistoryItemButton.tsx @@ -12,7 +12,7 @@ import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; import { JSXInternal } from "preact/src/jsx"; import { useDataPanelContext } from "../PersistentUtils"; -import PersistentVSCodeAPI from "../PersistentVSCodeAPI"; +import PersistentVSCodeAPI from "../../../PersistentVSCodeAPI"; import * as l10n from "@vscode/l10n"; export default function PersistentAddNewHistoryItemButton(): JSXInternal.Element { diff --git a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentClearAllButton.tsx b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentClearAllButton.tsx index 3e5ae65577..366d5a274a 100644 --- a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentClearAllButton.tsx +++ b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentClearAllButton.tsx @@ -12,7 +12,7 @@ import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; import { JSXInternal } from "preact/src/jsx"; import { useDataPanelContext } from "../PersistentUtils"; -import PersistentVSCodeAPI from "../PersistentVSCodeAPI"; +import PersistentVSCodeAPI from "../../../PersistentVSCodeAPI"; import * as l10n from "@vscode/l10n"; export default function PersistentClearAllButton(): JSXInternal.Element { diff --git a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentDeleteSelectedButton.tsx b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentDeleteSelectedButton.tsx index efe38ea4ac..b7bb242a7b 100644 --- a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentDeleteSelectedButton.tsx +++ b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentDeleteSelectedButton.tsx @@ -1,7 +1,7 @@ import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; import { JSXInternal } from "preact/src/jsx"; import { useDataPanelContext } from "../PersistentUtils"; -import PersistentVSCodeAPI from "../PersistentVSCodeAPI"; +import PersistentVSCodeAPI from "../../../PersistentVSCodeAPI"; import * as l10n from "@vscode/l10n"; export default function PersistentDeleteSelectedButton(): JSXInternal.Element { diff --git a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentRefreshButton.tsx b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentRefreshButton.tsx index 1502733099..e2b8fa3c31 100644 --- a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentRefreshButton.tsx +++ b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentRefreshButton.tsx @@ -12,7 +12,7 @@ import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; import { JSXInternal } from "preact/src/jsx"; import { useDataPanelContext } from "../PersistentUtils"; -import PersistentVSCodeAPI from "../PersistentVSCodeAPI"; +import PersistentVSCodeAPI from "../../../PersistentVSCodeAPI"; import * as l10n from "@vscode/l10n"; export default function PersistentRefreshButton(): JSXInternal.Element { diff --git a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/App.tsx b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/App.tsx new file mode 100644 index 0000000000..fcc36e2497 --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/App.tsx @@ -0,0 +1,46 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { useEffect, useState } from "preact/hooks"; +import { JSXInternal } from "preact/src/jsx"; +import { isSecureOrigin } from "../utils"; +import { ErrorInfo, ErrorInfoProps, isCorrelatedError } from "./components/ErrorInfo"; +import PersistentVSCodeAPI from "../PersistentVSCodeAPI"; + +export function App(): JSXInternal.Element { + const [errorInfo, setErrorInfo] = useState(); + + useEffect(() => { + window.addEventListener("message", (event) => { + if (!isSecureOrigin(event.origin)) { + return; + } + + if (!event.data) { + return; + } + + const errorInfo = event.data["error"]; + + if (isCorrelatedError(errorInfo)) { + setErrorInfo({ error: errorInfo, stackTrace: event.data?.stackTrace }); + } + }); + PersistentVSCodeAPI.getVSCodeAPI().postMessage({ command: "ready" }); + }, []); + + return ( +
+

Troubleshooting

+ {errorInfo ? : null} +
+ ); +} diff --git a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx new file mode 100644 index 0000000000..dc2cc66f2b --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx @@ -0,0 +1,99 @@ +import { VSCodeButton, VSCodeDivider, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react"; +import { CorrelatedError } from "@zowe/zowe-explorer-api"; +import { TipList } from "./TipList"; +import { useState } from "preact/hooks"; +import PersistentVSCodeAPI from "../../PersistentVSCodeAPI"; + +export type ErrorInfoProps = { + error: CorrelatedError; + stackTrace?: string; +}; + +export const isCorrelatedError = (val: any): val is CorrelatedError => { + return val?.["properties"] != null && val.properties["initialError"] != null; +}; + +export const ErrorInfo = ({ error, stackTrace }: ErrorInfoProps) => { + const [errorDisplayed, setErrorDisplayed] = useState(false); + return ( +
+

Error details

+

+ Code: + {error.errorCode ?? "Not available"} +

+

+ Description: + {error.message} +

+
+ setErrorDisplayed((prev) => !prev)} + > + + + {errorDisplayed ? ( + + ) : ( + + )} +   Full error summary + + + { + e.stopImmediatePropagation(); + PersistentVSCodeAPI.getVSCodeAPI().postMessage({ + command: "copy", + }); + }} + > + Copy details + + + + + +
+ + {error.properties.correlation?.tips ? ( + <> + + + + ) : null} +

Additional resources

+ +
+ ); +}; diff --git a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/TipList.tsx b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/TipList.tsx new file mode 100644 index 0000000000..bf8ce815dd --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/TipList.tsx @@ -0,0 +1,12 @@ +export const TipList = ({ tips }: { tips: string[] }) => { + return ( +
+

Tips

+
    + {tips.map((tip) => ( +
  • {tip}
  • + ))} +
+
+ ); +}; diff --git a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/index.html b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/index.html new file mode 100644 index 0000000000..9beb19e39c --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/index.html @@ -0,0 +1,16 @@ + + + + + + + + Troubleshoot Error + + +
+ + + diff --git a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/index.tsx b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/index.tsx new file mode 100644 index 0000000000..748009dcd9 --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/index.tsx @@ -0,0 +1,15 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { render } from "preact"; +import { App } from "./App"; + +render(, document.getElementById("webviewRoot")!); From 6784053629d303f9bd03c4450d7ecbaa08927631 Mon Sep 17 00:00:00 2001 From: Peter Haumer <4391934+phaumer@users.noreply.github.com> Date: Tue, 12 Nov 2024 08:07:11 -0800 Subject: [PATCH 5/5] Change default credentials manager check and not-found dialog (#3297) * updates to webpack and package.json for dev mode Signed-off-by: Billie Simmons <49491949+JillieBeanSim@users.noreply.github.com> * Remove endless startup loop when default CM cannot be loaded Signed-off-by: Peter Haumer <4391934+phaumer@users.noreply.github.com> * Initial set of test updates Signed-off-by: Peter Haumer <4391934+phaumer@users.noreply.github.com> * Merge branch 'main' into change-credentials-manager-check Signed-off-by: Peter Haumer <4391934+phaumer@users.noreply.github.com> * Convenience launch to only run currently open test Signed-off-by: Peter Haumer <4391934+phaumer@users.noreply.github.com> * Fixed ProfileUtils tests Signed-off-by: Peter Haumer <4391934+phaumer@users.noreply.github.com> * Clean up package files Signed-off-by: Peter Haumer <4391934+phaumer@users.noreply.github.com> * Updated changelog Signed-off-by: Peter Haumer <4391934+phaumer@users.noreply.github.com> * Reworded info message Signed-off-by: Peter Haumer <4391934+phaumer@users.noreply.github.com> * Update CHANGELOG.md Signed-off-by: Fernando Rijo Cedeno <37381190+zFernand0@users.noreply.github.com> * Added detail to info message Signed-off-by: Peter Haumer <4391934+phaumer@users.noreply.github.com> * Updated resource files Signed-off-by: Peter Haumer <4391934+phaumer@users.noreply.github.com> --------- Signed-off-by: Billie Simmons <49491949+JillieBeanSim@users.noreply.github.com> Signed-off-by: Peter Haumer <4391934+phaumer@users.noreply.github.com> Signed-off-by: Fernando Rijo Cedeno <37381190+zFernand0@users.noreply.github.com> Co-authored-by: Billie Simmons <49491949+JillieBeanSim@users.noreply.github.com> Co-authored-by: Fernando Rijo Cedeno <37381190+zFernand0@users.noreply.github.com> --- .vscode/launch.json | 9 ++ packages/zowe-explorer/CHANGELOG.md | 1 + .../__unit__/utils/ProfilesUtils.unit.test.ts | 49 ++++------ packages/zowe-explorer/l10n/bundle.l10n.json | 91 +++++++++---------- packages/zowe-explorer/l10n/poeditor.json | 27 +++--- .../zowe-explorer/src/utils/ProfilesUtils.ts | 88 +++++++++++------- 6 files changed, 142 insertions(+), 123 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 098a5ca927..772a2e5584 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -37,6 +37,15 @@ "console": "integratedTerminal", "sourceMaps": true }, + { + "type": "node", + "name": "Current Unit Tests (Jest)", + "request": "launch", + "runtimeArgs": ["--inspect-brk", "${workspaceFolder}/node_modules/jest/bin/jest", "-i", "${fileBasenameNoExtension}"], + "cwd": "${workspaceFolder}/packages/zowe-explorer", + "console": "integratedTerminal", + "sourceMaps": true + }, { "type": "node", "name": "API Unit Tests (Jest)", diff --git a/packages/zowe-explorer/CHANGELOG.md b/packages/zowe-explorer/CHANGELOG.md index 1cfe85d9a1..f030aceae6 100644 --- a/packages/zowe-explorer/CHANGELOG.md +++ b/packages/zowe-explorer/CHANGELOG.md @@ -23,6 +23,7 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen - Fixed issue where Zowe Explorer would present the "No configs detected" notification when initialized in a workspace without a Zowe team configuration. [#3280](https://github.com/zowe/zowe-explorer-vscode/issues/3280) - Reduced the number of MVS API calls performed by `vscode.workspace.fs.readFile` when fetching the contents of a data set entry. [#3278](https://github.com/zowe/zowe-explorer-vscode/issues/3278) - Fixed an issue to review inconsistent capitalization across translation strings. [#2935](https://github.com/zowe/zowe-explorer-vscode/issues/2935) +- Updated the test for the default credential manager for better compatibility with Cloud-based platforms such as Eclipse Che and Red Hat OpenShift Dev Spaces. [#3297](https://github.com/zowe/zowe-explorer-vscode/pull/3297) ## `3.0.2` 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 c7e8b9af01..41fd8274df 100644 --- a/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts @@ -65,6 +65,7 @@ describe("ProfilesUtils unit tests", () => { Object.defineProperty(ZoweLogger, "info", { value: jest.fn(), configurable: true }); Object.defineProperty(SettingsConfig, "getDirectValue", { value: newMocks.mockGetDirectValue, configurable: true }); Object.defineProperty(ProfilesUtils, "PROFILE_SECURITY", { value: Constants.ZOWE_CLI_SCM, configurable: true }); + Object.defineProperty(ProfilesUtils, "checkDefaultCredentialManager", { value: jest.fn(), configurable: true }); return newMocks; } @@ -457,6 +458,7 @@ describe("ProfilesUtils unit tests", () => { describe("initializeZoweFolder", () => { it("should create directories and files that do not exist", async () => { const blockMocks = createBlockMocks(); + jest.spyOn(ProfilesUtils, "checkDefaultCredentialManager").mockReturnValue(true); blockMocks.mockGetDirectValue.mockReturnValue(true); blockMocks.mockExistsSync.mockReturnValue(false); jest.spyOn(fs, "readFileSync").mockReturnValue(Buffer.from(JSON.stringify({ overrides: { credentialManager: "@zowe/cli" } }), "utf-8")); @@ -469,6 +471,7 @@ describe("ProfilesUtils unit tests", () => { it("should skip creating directories and files that already exist", async () => { const blockMocks = createBlockMocks(); + jest.spyOn(ProfilesUtils, "checkDefaultCredentialManager").mockReturnValue(true); jest.spyOn(ProfilesUtils, "getCredentialManagerOverride").mockReturnValue("@zowe/cli"); blockMocks.mockGetDirectValue.mockReturnValue("@zowe/cli"); blockMocks.mockExistsSync.mockReturnValue(true); @@ -566,7 +569,7 @@ describe("ProfilesUtils unit tests", () => { it("should handle Imperative error thrown on read config from disk", async () => { const testError = new imperative.ImperativeError({ msg: "readConfigFromDisk failed" }); - const initZoweFolderSpy = jest.spyOn(ProfilesUtils, "initializeZoweFolder").mockReturnValueOnce(); + const initZoweFolderSpy = jest.spyOn(ProfilesUtils, "initializeZoweFolder").mockResolvedValueOnce(); const readConfigFromDiskSpy = jest.spyOn(ProfilesUtils, "readConfigFromDisk").mockRejectedValueOnce(testError); await ProfilesUtils.initializeZoweProfiles((msg) => ZoweExplorerExtender.showZoweConfigError(msg)); expect(initZoweFolderSpy).toHaveBeenCalledTimes(1); @@ -576,7 +579,7 @@ describe("ProfilesUtils unit tests", () => { it("should handle JSON parse error thrown on read config from disk", async () => { const testError = new Error("readConfigFromDisk failed"); - const initZoweFolderSpy = jest.spyOn(ProfilesUtils, "initializeZoweFolder").mockReturnValueOnce(); + const initZoweFolderSpy = jest.spyOn(ProfilesUtils, "initializeZoweFolder").mockResolvedValueOnce(); const readConfigFromDiskSpy = jest.spyOn(ProfilesUtils, "readConfigFromDisk").mockRejectedValueOnce(testError); const showZoweConfigErrorSpy = jest.spyOn(ZoweExplorerExtender, "showZoweConfigError").mockReturnValueOnce(); await ProfilesUtils.initializeZoweProfiles((msg) => ZoweExplorerExtender.showZoweConfigError(msg)); @@ -649,6 +652,7 @@ describe("ProfilesUtils unit tests", () => { it("should update the credential manager setting if secure value is true", () => { jest.spyOn(SettingsConfig, "isConfigSettingSetByUser").mockReturnValue(false); jest.spyOn(SettingsConfig, "getDirectValue").mockReturnValueOnce(true); + jest.spyOn(ProfilesUtils, "checkDefaultCredentialManager").mockReturnValue(true); const loggerInfoSpy = jest.spyOn(ZoweLogger, "info"); const recordCredMgrInConfigSpy = jest.spyOn(imperative.CredentialManagerOverride, "recordCredMgrInConfig"); ProfilesUtils.updateCredentialManagerSetting(); @@ -679,7 +683,8 @@ describe("ProfilesUtils unit tests", () => { let getCredentialManagerMapSpy: jest.SpyInstance; let setupCustomCredentialManagerSpy: jest.SpyInstance; let readProfilesFromDiskSpy: jest.SpyInstance; - let promptAndDisableCredentialManagementSpy: jest.SpyInstance; + let disableCredentialManagementSpy: jest.SpyInstance; + let checkDefaultCredentialManagerSpy: jest.SpyInstance; beforeEach(() => { jest.clearAllMocks(); @@ -692,7 +697,8 @@ describe("ProfilesUtils unit tests", () => { getCredentialManagerMapSpy = jest.spyOn(ProfilesUtils, "getCredentialManagerMap"); setupCustomCredentialManagerSpy = jest.spyOn(ProfilesUtils, "setupCustomCredentialManager"); readProfilesFromDiskSpy = jest.spyOn(imperative.ProfileInfo.prototype, "readProfilesFromDisk"); - promptAndDisableCredentialManagementSpy = jest.spyOn(ProfilesUtils, "promptAndDisableCredentialManagement"); + disableCredentialManagementSpy = jest.spyOn(ProfilesUtils, "disableCredentialManagement"); + checkDefaultCredentialManagerSpy = jest.spyOn(ProfilesUtils, "checkDefaultCredentialManager"); }); it("should retrieve the custom credential manager", async () => { @@ -720,10 +726,11 @@ describe("ProfilesUtils unit tests", () => { await expect(ProfilesUtils.getProfileInfo()).resolves.toEqual({}); }); - it("should retrieve the default credential manager and prompt to disable credential management if environment not supported", async () => { + it("should throw exception of readProfilesFromDiskSpy fails", async () => { const expectedErrMsg = // eslint-disable-next-line max-len "Failed to load credential manager. This may be related to Zowe Explorer being unable to use the default credential manager in a browser based environment."; + checkDefaultCredentialManagerSpy.mockReturnValue(false); getDirectValueSpy.mockReturnValueOnce(false); getCredentialManagerOverrideSpy.mockReturnValue("@zowe/cli"); isVSCodeCredentialPluginInstalledSpy.mockReturnValueOnce(false); @@ -741,11 +748,11 @@ describe("ProfilesUtils unit tests", () => { throw err; }); await expect(ProfilesUtils.getProfileInfo()).rejects.toThrow(expectedErrMsg); - expect(promptAndDisableCredentialManagementSpy).toHaveBeenCalledTimes(1); }); it("should ignore error if it is not an instance of ProfInfoErr", async () => { const expectedErrorMsg = "Another error unrelated to credential management"; + checkDefaultCredentialManagerSpy.mockReturnValue(true); getDirectValueSpy.mockReturnValueOnce(false); getCredentialManagerOverrideSpy.mockReturnValue("@zowe/cli"); isVSCodeCredentialPluginInstalledSpy.mockReturnValueOnce(false); @@ -756,7 +763,7 @@ describe("ProfilesUtils unit tests", () => { throw new Error(expectedErrorMsg); }); await expect(ProfilesUtils.getProfileInfo()).resolves.not.toThrow(); - expect(promptAndDisableCredentialManagementSpy).toHaveBeenCalledTimes(0); + expect(disableCredentialManagementSpy).toHaveBeenCalledTimes(0); }); }); @@ -960,10 +967,11 @@ describe("ProfilesUtils unit tests", () => { }); }); - describe("promptAndDisableCredentialManagement", () => { + describe("disableCredentialManagement", () => { let setDirectValueSpy: jest.SpyInstance; let warningMessageSpy: jest.SpyInstance; let executeCommandSpy: jest.SpyInstance; + let getDirectValueSpy: jest.SpyInstance; beforeEach(() => { jest.clearAllMocks(); @@ -972,33 +980,16 @@ describe("ProfilesUtils unit tests", () => { setDirectValueSpy = jest.spyOn(SettingsConfig, "setDirectValue"); warningMessageSpy = jest.spyOn(Gui, "warningMessage"); executeCommandSpy = jest.spyOn(vscode.commands, "executeCommand"); + getDirectValueSpy = jest.spyOn(SettingsConfig, "getDirectValue"); }); - it("should prompt whether to disable credential management, and disable globally if 'Yes, globally' selected", async () => { + it("should show warning that credential management was disabled", async () => { warningMessageSpy.mockResolvedValue("Yes, globally"); - await expect(ProfilesUtils.promptAndDisableCredentialManagement()).resolves.not.toThrow(); + getDirectValueSpy.mockReturnValueOnce(true); + await expect(ProfilesUtils.disableCredentialManagement()).resolves.not.toThrow(); expect(setDirectValueSpy).toHaveBeenCalledWith(Constants.SETTINGS_SECURE_CREDENTIALS_ENABLED, false, vscode.ConfigurationTarget.Global); expect(executeCommandSpy).toHaveBeenCalledWith("workbench.action.reloadWindow"); }); - - it("should prompt whether to disable credential management, and disable on workspace if 'Only for this workspace' selected", async () => { - warningMessageSpy.mockResolvedValue("Only for this workspace"); - await expect(ProfilesUtils.promptAndDisableCredentialManagement()).resolves.not.toThrow(); - expect(setDirectValueSpy).toHaveBeenCalledWith( - Constants.SETTINGS_SECURE_CREDENTIALS_ENABLED, - false, - vscode.ConfigurationTarget.Workspace - ); - expect(executeCommandSpy).toHaveBeenCalledWith("workbench.action.reloadWindow"); - }); - - it("should prompt whether to disable credential management, and throw error if 'No'", async () => { - warningMessageSpy.mockResolvedValue("No"); - await expect(ProfilesUtils.promptAndDisableCredentialManagement()).rejects.toThrow( - // eslint-disable-next-line max-len - "Failed to load credential manager. This may be related to Zowe Explorer being unable to use the default credential manager in a browser based environment." - ); - }); }); describe("v1ProfileOptions", () => { diff --git a/packages/zowe-explorer/l10n/bundle.l10n.json b/packages/zowe-explorer/l10n/bundle.l10n.json index 4e88316430..8cf8918fea 100644 --- a/packages/zowe-explorer/l10n/bundle.l10n.json +++ b/packages/zowe-explorer/l10n/bundle.l10n.json @@ -57,10 +57,9 @@ "Credential manager display name" ] }, - "Yes, globally": "Yes, globally", - "Only for this workspace": "Only for this workspace", - "Zowe Explorer failed to activate since the default credential manager is not supported in your environment.": "Zowe Explorer failed to activate since the default credential manager is not supported in your environment.", - "Do you wish to disable credential management? (VS Code window reload will be triggered)": "Do you wish to disable credential management? (VS Code window reload will be triggered)", + "Zowe Explorer's default credential manager is not supported in your environment. Consider installing a custom solution for your platform. Click Reload to start Zowe Explorer without a credential manager.": "Zowe Explorer's default credential manager is not supported in your environment. Consider installing a custom solution for your platform. Click Reload to start Zowe Explorer without a credential manager.", + "Reload window": "Reload window", + "Default Zowe credentials manager not found on current platform. This is typically the case when running in container-based environments or Linux systems that miss required security libraries or user permissions.": "Default Zowe credentials manager not found on current platform. This is typically the case when running in container-based environments or Linux systems that miss required security libraries or user permissions.", "No custom credential managers found, using the default instead.": "No custom credential managers found, using the default instead.", "Custom credential manager {0} found/Credential manager display name": { "message": "Custom credential manager {0} found", @@ -203,6 +202,48 @@ "Profile auth error": "Profile auth error", "Profile is not authenticated, please log in to continue": "Profile is not authenticated, please log in to continue", "Retrieving response from USS list API": "Retrieving response from USS list API", + "The 'move' function is not implemented for this USS API.": "The 'move' function is not implemented for this USS API.", + "Failed to move {0}/File path": { + "message": "Failed to move {0}", + "comment": [ + "File path" + ] + }, + "Failed to get contents for {0}/File path": { + "message": "Failed to get contents for {0}", + "comment": [ + "File path" + ] + }, + "Profile does not exist for this file.": "Profile does not exist for this file.", + "Saving USS file...": "Saving USS file...", + "Failed to rename {0}/File path": { + "message": "Failed to rename {0}", + "comment": [ + "File path" + ] + }, + "Failed to delete {0}/File name": { + "message": "Failed to delete {0}", + "comment": [ + "File name" + ] + }, + "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" + ] + }, + "Failed to copy {0} to {1}/Source pathDestination path": { + "message": "Failed to copy {0} to {1}", + "comment": [ + "Source path", + "Destination path" + ] + }, "Downloaded: {0}/Download time": { "message": "Downloaded: {0}", "comment": [ @@ -273,48 +314,6 @@ "initializeUSSFavorites.error.buttonRemove": "initializeUSSFavorites.error.buttonRemove", "File does not exist. It may have been deleted.": "File does not exist. It may have been deleted.", "Pulling from Mainframe...": "Pulling from Mainframe...", - "The 'move' function is not implemented for this USS API.": "The 'move' function is not implemented for this USS API.", - "Failed to move {0}/File path": { - "message": "Failed to move {0}", - "comment": [ - "File path" - ] - }, - "Failed to get contents for {0}/File path": { - "message": "Failed to get contents for {0}", - "comment": [ - "File path" - ] - }, - "Profile does not exist for this file.": "Profile does not exist for this file.", - "Saving USS file...": "Saving USS file...", - "Failed to rename {0}/File path": { - "message": "Failed to rename {0}", - "comment": [ - "File path" - ] - }, - "Failed to delete {0}/File name": { - "message": "Failed to delete {0}", - "comment": [ - "File name" - ] - }, - "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" - ] - }, - "Failed to copy {0} to {1}/Source pathDestination path": { - "message": "Failed to copy {0} to {1}", - "comment": [ - "Source path", - "Destination path" - ] - }, "{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 e715a39d2f..618dc6cd12 100644 --- a/packages/zowe-explorer/l10n/poeditor.json +++ b/packages/zowe-explorer/l10n/poeditor.json @@ -469,10 +469,9 @@ "Zowe Explorer profiles are being set as secured.": "", "Custom credential manager failed to activate": "", "Custom credential manager {0} found, attempting to activate.": "", - "Yes, globally": "", - "Only for this workspace": "", - "Zowe Explorer failed to activate since the default credential manager is not supported in your environment.": "", - "Do you wish to disable credential management? (VS Code window reload will be triggered)": "", + "Zowe Explorer's default credential manager is not supported in your environment. Consider installing a custom solution for your platform. Click Reload to start Zowe Explorer without a credential manager.": "", + "Reload window": "", + "Default Zowe credentials manager not found on current platform. This is typically the case when running in container-based environments or Linux systems that miss required security libraries or user permissions.": "", "No custom credential managers found, using the default instead.": "", "Custom credential manager {0} found": "", "Do you wish to use this credential manager instead?": "", @@ -534,6 +533,16 @@ "Profile auth error": "", "Profile is not authenticated, please log in to continue": "", "Retrieving response from USS list API": "", + "The 'move' function is not implemented for this USS API.": "", + "Failed to move {0}": "", + "Failed to get contents for {0}": "", + "Profile does not exist for this file.": "", + "Saving USS file...": "", + "Failed to rename {0}": "", + "Failed to delete {0}": "", + "No error details given": "", + "Error fetching destination {0} for paste action: {1}": "", + "Failed to copy {0} to {1}": "", "Downloaded: {0}": "", "Encoding: {0}": "", "Binary": "", @@ -562,16 +571,6 @@ "initializeUSSFavorites.error.buttonRemove": "", "File does not exist. It may have been deleted.": "", "Pulling from Mainframe...": "", - "The 'move' function is not implemented for this USS API.": "", - "Failed to move {0}": "", - "Failed to get contents for {0}": "", - "Profile does not exist for this file.": "", - "Saving USS file...": "", - "Failed to rename {0}": "", - "Failed to delete {0}": "", - "No error details given": "", - "Error fetching destination {0} for paste action: {1}": "", - "Failed to copy {0} to {1}": "", "{0} location": "", "Choose a location to create the {0}": "", "Name of file or directory": "", diff --git a/packages/zowe-explorer/src/utils/ProfilesUtils.ts b/packages/zowe-explorer/src/utils/ProfilesUtils.ts index a54adae25a..5abc97cfe5 100644 --- a/packages/zowe-explorer/src/utils/ProfilesUtils.ts +++ b/packages/zowe-explorer/src/utils/ProfilesUtils.ts @@ -28,7 +28,7 @@ export enum ProfilesConvertStatus { export class ProfilesUtils { public static PROFILE_SECURITY: string | boolean = Constants.ZOWE_CLI_SCM; - private static noConfigDialogShown: boolean = false; + private static noConfigDialogShown = false; /** * Check if the credential manager's vsix is installed for use @@ -82,18 +82,22 @@ export class ProfilesUtils { */ public static updateCredentialManagerSetting(credentialManager?: string | false): void { ZoweLogger.trace("ProfilesUtils.updateCredentialManagerSetting called."); + const currentProfileSecurity = this.PROFILE_SECURITY; const settingEnabled: boolean = SettingsConfig.getDirectValue(Constants.SETTINGS_SECURE_CREDENTIALS_ENABLED, true); + const defaultCredentialManagerFound = this.checkDefaultCredentialManager(); if (settingEnabled && credentialManager) { this.PROFILE_SECURITY = credentialManager; return; - } else if (!settingEnabled) { + } else if (!settingEnabled || !defaultCredentialManagerFound) { this.PROFILE_SECURITY = false; ZoweLogger.info(vscode.l10n.t(`Zowe Explorer profiles are being set as unsecured.`)); } else { this.PROFILE_SECURITY = Constants.ZOWE_CLI_SCM; ZoweLogger.info(vscode.l10n.t(`Zowe Explorer profiles are being set as secured.`)); } - imperative.CredentialManagerOverride.recordCredMgrInConfig(this.PROFILE_SECURITY); + if (currentProfileSecurity !== this.PROFILE_SECURITY) { + imperative.CredentialManagerOverride.recordCredMgrInConfig(this.PROFILE_SECURITY); + } } /** @@ -149,35 +153,38 @@ export class ProfilesUtils { * Prompt whether to disable credential management setting * * This will disable credential management on all settings - * scopes since order presedence can be hard to predict based on the user's setup + * scopes since order precedence can be hard to predict based on the user's setup */ - public static async promptAndDisableCredentialManagement(): Promise { - ZoweLogger.trace("ProfilesUtils.promptAndDisableCredentialManagement called."); - const noButton = vscode.l10n.t("No"); - const yesGloballyButton = vscode.l10n.t("Yes, globally"); - const yesWorkspaceButton = vscode.l10n.t("Only for this workspace"); - const response = await Gui.warningMessage( - vscode.l10n.t("Zowe Explorer failed to activate since the default credential manager is not supported in your environment."), - { - items: [noButton, yesGloballyButton, yesWorkspaceButton], - vsCodeOpts: { - modal: true, - detail: vscode.l10n.t("Do you wish to disable credential management? (VS Code window reload will be triggered)"), - }, - } - ); - if (response === yesGloballyButton || response === yesWorkspaceButton) { - const scope = response === yesGloballyButton ? vscode.ConfigurationTarget.Global : vscode.ConfigurationTarget.Workspace; - await SettingsConfig.setDirectValue(Constants.SETTINGS_SECURE_CREDENTIALS_ENABLED, false, scope); - await vscode.commands.executeCommand("workbench.action.reloadWindow"); - } else { - throw new imperative.ImperativeError({ - msg: vscode.l10n.t( - // eslint-disable-next-line max-len - "Failed to load credential manager. This may be related to Zowe Explorer being unable to use the default credential manager in a browser based environment." + public static async disableCredentialManagement(): Promise { + ZoweLogger.trace("ProfilesUtils.disableCredentialManagement called."); + const settingEnabled: boolean = SettingsConfig.getDirectValue(Constants.SETTINGS_SECURE_CREDENTIALS_ENABLED, true); + if (settingEnabled) { + this.PROFILE_SECURITY = false; + await SettingsConfig.setDirectValue(Constants.SETTINGS_SECURE_CREDENTIALS_ENABLED, false, vscode.ConfigurationTarget.Global); + await Gui.infoMessage( + vscode.l10n.t( + "Zowe Explorer's default credential manager is not supported in your environment. Consider installing a custom solution for your platform. Click Reload to start Zowe Explorer without a credential manager." ), - }); + { + items: [vscode.l10n.t("Reload window")], + } + ); + await vscode.commands.executeCommand("workbench.action.reloadWindow"); + } + } + + public static checkDefaultCredentialManager(): boolean { + try { + ProfilesCache.requireKeyring(); + } catch (_error) { + ZoweLogger.info( + vscode.l10n.t( + "Default Zowe credentials manager not found on current platform. This is typically the case when running in container-based environments or Linux systems that miss required security libraries or user permissions." + ) + ); + return false; } + return true; } /** @@ -204,7 +211,7 @@ export class ProfilesUtils { return profileInfo; } catch (err) { if (err instanceof imperative.ProfInfoErr && err.errorCode === imperative.ProfInfoErr.LOAD_CRED_MGR_FAILED) { - await ProfilesUtils.promptAndDisableCredentialManagement(); + await ProfilesUtils.disableCredentialManagement(); } if (err instanceof Error) { ZoweLogger.error(err.message); @@ -306,8 +313,8 @@ export class ProfilesUtils { */ public static async getProfileInfo(): Promise { ZoweLogger.trace("ProfilesUtils.getProfileInfo called."); - const hasSecureCredentialManagerEnabled: boolean = SettingsConfig.getDirectValue(Constants.SETTINGS_SECURE_CREDENTIALS_ENABLED); + const hasSecureCredentialManagerEnabled: boolean = this.checkDefaultCredentialManager(); if (hasSecureCredentialManagerEnabled) { const shouldCheckForCustomCredentialManagers = SettingsConfig.getDirectValue( Constants.SETTINGS_CHECK_FOR_CUSTOM_CREDENTIAL_MANAGERS @@ -327,9 +334,17 @@ export class ProfilesUtils { if (credentialManagerMap && isVSCodeCredentialPluginInstalled) { return this.setupCustomCredentialManager(credentialManagerMap); } + return this.setupDefaultCredentialManager(); } - return this.setupDefaultCredentialManager(); + const profileInfo = new imperative.ProfileInfo("zowe", {}); + const workspacePath = ZoweVsCodeExtension.workspaceRoot?.uri.fsPath; + // Trigger initialize() function of credential manager to throw an error early if failed to load + await profileInfo.readProfilesFromDisk({ + homeDir: FileManagement.getZoweDir(), + projectDir: workspacePath ? FileManagement.getFullPath(workspacePath) : undefined, + }); + return profileInfo; } public static async readConfigFromDisk(warnForMissingSchema?: boolean): Promise { @@ -466,7 +481,7 @@ export class ProfilesUtils { } } - public static initializeZoweFolder(): void { + public static async initializeZoweFolder(): Promise { ZoweLogger.trace("ProfilesUtils.initializeZoweFolder called."); // Ensure that ~/.zowe folder exists const zoweDir = FileManagement.getZoweDir(); @@ -477,6 +492,11 @@ export class ProfilesUtils { if (!fs.existsSync(settingsPath)) { fs.mkdirSync(settingsPath); } + + if (!this.checkDefaultCredentialManager()) { + await this.disableCredentialManagement(); + } + ProfilesUtils.writeOverridesFile(); // set global variable of security value to existing override // this will later get reverted to default in getProfilesInfo.ts if user chooses to @@ -553,7 +573,7 @@ export class ProfilesUtils { public static async initializeZoweProfiles(errorCallback: (msg: string) => unknown): Promise { ZoweLogger.trace("ProfilesUtils.initializeZoweProfiles called."); try { - ProfilesUtils.initializeZoweFolder(); + await ProfilesUtils.initializeZoweFolder(); } catch (err) { ZoweLogger.error(err); Gui.errorMessage(