From 080a8d9ab8302051c30c4a7bf5c1489b82615a4b Mon Sep 17 00:00:00 2001 From: "Andrew W. Harn" Date: Mon, 16 Dec 2024 10:19:03 -0500 Subject: [PATCH] Add ability to view Job Spool with encoding (#3361) * Make initial changes, not working Signed-off-by: Andrew W. Harn * Fix the implementation and get it working Signed-off-by: Andrew W. Harn * Add the first set of tests Signed-off-by: Andrew W. Harn * Add more tests Signed-off-by: Andrew W. Harn * Add more tests Signed-off-by: Andrew W. Harn * Fix logic to prevent same name nodes on different profiles from sharing an encoding value. Signed-off-by: Andrew W. Harn * Add changelog Signed-off-by: Andrew W. Harn * Update API changelog Signed-off-by: Andrew W. Harn * Make changes to match async compatibility Signed-off-by: Andrew W. Harn --------- Signed-off-by: Andrew W. Harn --- packages/zowe-explorer-api/CHANGELOG.md | 1 + .../src/tree/IZoweTreeNode.ts | 31 ++ packages/zowe-explorer/CHANGELOG.md | 1 + .../trees/job/JobFSProvider.unit.test.ts | 42 +- .../__unit__/trees/job/JobTree.unit.test.ts | 146 ++++- .../trees/job/ZoweJobNode.unit.test.ts | 151 +++++- .../trees/shared/SharedUtils.unit.test.ts | 512 +++++++++++++++++- packages/zowe-explorer/package.json | 5 + .../src/trees/job/JobFSProvider.ts | 19 +- .../zowe-explorer/src/trees/job/JobTree.ts | 28 +- .../src/trees/job/ZoweJobNode.ts | 62 ++- .../src/trees/shared/SharedUtils.ts | 7 +- 12 files changed, 992 insertions(+), 13 deletions(-) diff --git a/packages/zowe-explorer-api/CHANGELOG.md b/packages/zowe-explorer-api/CHANGELOG.md index c72938d7b8..2b5ff142d3 100644 --- a/packages/zowe-explorer-api/CHANGELOG.md +++ b/packages/zowe-explorer-api/CHANGELOG.md @@ -11,6 +11,7 @@ All notable changes to the "zowe-explorer-api" extension will be documented in t - Added support for extenders to obtain an updated Session that will includes VS Code proxy settings values if set, `getProfileSessionWithVscProxy`. [#3010](https://github.com/zowe/zowe-explorer-vscode/issues/3010) - Added support for VS Code proxy settings with zosmf profile types. [#3010](https://github.com/zowe/zowe-explorer-vscode/issues/3010) - Added optional `getLocalStorage` function to the `IApiExplorerExtender` interface to expose local storage access to Zowe Explorer extenders. [#3180](https://github.com/zowe/zowe-explorer-vscode/issues/3180) +- Added optional `setEncoding`, `getEncoding`, and `getEncodingInMap` functions to the `IZoweJobTreeNode` interface. [#3361](https://github.com/zowe/zowe-explorer-vscode/pull/3361) ### Bug fixes diff --git a/packages/zowe-explorer-api/src/tree/IZoweTreeNode.ts b/packages/zowe-explorer-api/src/tree/IZoweTreeNode.ts index 392ad02020..c21718f2cb 100644 --- a/packages/zowe-explorer-api/src/tree/IZoweTreeNode.ts +++ b/packages/zowe-explorer-api/src/tree/IZoweTreeNode.ts @@ -503,4 +503,35 @@ export interface IZoweJobTreeNode extends IZoweTreeNode { * @returns {Promise} */ getChildren(): Promise; + + /** + * Sets the encoding of the job node + * + * @returns {void | PromiseLike} + */ + setEncoding?(encoding: ZosEncoding): void | PromiseLike; + + /** + * Gets the encoding of the job node + * + * @returns {ZosEncoding | PromiseLike} + */ + getEncoding?(): ZosEncoding | PromiseLike; + + /** + * Gets the encoding of a job node given a path + * + * @param {string} uriPath the path of the node + * @returns {ZosEncoding | PromiseLike} + */ + getEncodingInMap?(uriPath: string): ZosEncoding | PromiseLike; + + /** + * Updates the encoding of a job node given a path + * + * @param {string} uriPath the path of the node + * @param {ZosEncoding} encoding the encoding to apply + * @returns {void | PromiseLike} + */ + updateEncodingInMap?(uriPath: string, encoding: ZosEncoding): void | PromiseLike; } diff --git a/packages/zowe-explorer/CHANGELOG.md b/packages/zowe-explorer/CHANGELOG.md index 0660d13383..f13e95face 100644 --- a/packages/zowe-explorer/CHANGELOG.md +++ b/packages/zowe-explorer/CHANGELOG.md @@ -17,6 +17,7 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen - Allow extenders to add context menu actions to a top level node, i.e. data sets, USS, Jobs, by encoding the profile type in the context value. [#3309](https://github.com/zowe/zowe-explorer-vscode/pull/3309) - You can now add multiple partitioned data sets or USS directories to your workspace at once using the "Add to Workspace" feature. [#3324](https://github.com/zowe/zowe-explorer-vscode/issues/3324) - Exposed read and write access to local storage keys for Zowe Explorer extenders. [#3180](https://github.com/zowe/zowe-explorer-vscode/issues/3180) +- Added `Open with Encoding` to the context menu of Job Spool files. [#1941](https://github.com/zowe/zowe-explorer-vscode/issues/1941) ### Bug fixes 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 e04d77e583..f32fa0662a 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 @@ -43,9 +43,7 @@ const testEntries = { profile: testProfile, path: "/TESTJOB(JOB1234) - ACTIVE/JES2.JESMSGLG.2", }, - spool: { - id: "SOMEID", - } as any, + spool: createIJobFile(), } as SpoolEntry, session: { ...new FilterEntry("sestest"), @@ -226,6 +224,44 @@ describe("fetchSpoolAtUri", () => { lookupAsFileMock.mockRestore(); }); + it("fetches the spool contents for a given URI - downloadSingleSpool w/ binary encoding", async () => { + const lookupAsFileMock = jest + .spyOn(JobFSProvider.instance as any, "_lookupAsFile") + .mockReturnValueOnce({ ...testEntries.spool, data: new Uint8Array(), encoding: { kind: "binary" } }); + const newData = "spool contents"; + const mockJesApi = { + downloadSingleSpool: jest.fn((opts) => { + opts.stream.write(newData); + }), + }; + const jesApiMock = jest.spyOn(ZoweExplorerApiRegister, "getJesApi").mockReturnValueOnce(mockJesApi as any); + const entry = await JobFSProvider.instance.fetchSpoolAtUri(testUris.spool); + expect(mockJesApi.downloadSingleSpool).toHaveBeenCalledWith(expect.objectContaining({ jobFile: testEntries.spool.spool, binary: true })); + expect(entry.data.toString()).toStrictEqual(newData.toString()); + jesApiMock.mockRestore(); + lookupAsFileMock.mockRestore(); + }); + + it("fetches the spool contents for a given URI - downloadSingleSpool w/ other encoding", async () => { + const lookupAsFileMock = jest + .spyOn(JobFSProvider.instance as any, "_lookupAsFile") + .mockReturnValueOnce({ ...testEntries.spool, data: new Uint8Array(), encoding: { kind: "other", codepage: "IBM-1147" } }); + const newData = "spool contents"; + const mockJesApi = { + downloadSingleSpool: jest.fn((opts) => { + opts.stream.write(newData); + }), + }; + const jesApiMock = jest.spyOn(ZoweExplorerApiRegister, "getJesApi").mockReturnValueOnce(mockJesApi as any); + const entry = await JobFSProvider.instance.fetchSpoolAtUri(testUris.spool); + expect(mockJesApi.downloadSingleSpool).toHaveBeenCalledWith( + expect.objectContaining({ jobFile: testEntries.spool.spool, encoding: "IBM-1147", binary: false }) + ); + expect(entry.data.toString()).toStrictEqual(newData.toString()); + jesApiMock.mockRestore(); + lookupAsFileMock.mockRestore(); + }); + it("fetches the spool contents for a given URI - getSpoolContentById", async () => { const lookupAsFileMock = jest .spyOn(JobFSProvider.instance as any, "_lookupAsFile") diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/job/JobTree.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/job/JobTree.unit.test.ts index 319f70310d..caf65e80bd 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/job/JobTree.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/job/JobTree.unit.test.ts @@ -11,7 +11,7 @@ import * as vscode from "vscode"; import * as zosjobs from "@zowe/zos-jobs-for-zowe-sdk"; -import { Gui, imperative, IZoweJobTreeNode, ProfilesCache, Validation, Poller } from "@zowe/zowe-explorer-api"; +import { Gui, imperative, IZoweJobTreeNode, ProfilesCache, Validation, Poller, ZosEncoding } from "@zowe/zowe-explorer-api"; import { createIJobFile, createIJobObject, createJobFavoritesNode, createJobSessionNode, MockJobDetail } from "../../../__mocks__/mockCreators/jobs"; import { createIProfile, @@ -1167,3 +1167,147 @@ describe("ZosJobsProvider Unit Test - Filter Jobs", () => { expect(filterJobsSpy).toHaveBeenCalledWith(node1); }); }); + +describe("openWithEncoding", () => { + beforeEach(async () => { + await createGlobalMocks(); + jest.restoreAllMocks(); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + it("should open a Job Spool file with an encoding (binary, prompted)", async () => { + const testTree = new JobTree(); + + const spoolNode = new ZoweSpoolNode({ label: "SPOOL", collapsibleState: vscode.TreeItemCollapsibleState.None, spool: createIJobFile() }); + + const encoding: ZosEncoding = { kind: "binary" }; + + const promptMock = jest.spyOn(SharedUtils, "promptForEncoding").mockResolvedValue(encoding); + const executeCommandMock = jest.spyOn(vscode.commands, "executeCommand").mockImplementation(); + const fetchSpoolAtUriMock = jest.spyOn(JobFSProvider.instance, "fetchSpoolAtUri").mockImplementation(); + const setEncodingSpy = jest.spyOn(spoolNode, "setEncoding").mockImplementation(); + + await testTree.openWithEncoding(spoolNode); + expect(promptMock).toHaveBeenCalledWith(spoolNode); + expect(promptMock).toHaveBeenCalledTimes(1); + expect(setEncodingSpy).toHaveBeenCalledWith(encoding); + expect(fetchSpoolAtUriMock).toHaveBeenCalledTimes(1); + expect(executeCommandMock).toHaveBeenCalledTimes(1); + }); + + it("should open a Job Spool file with an encoding (binary, provided)", async () => { + const testTree = new JobTree(); + + const spoolNode = new ZoweSpoolNode({ label: "SPOOL", collapsibleState: vscode.TreeItemCollapsibleState.None, spool: createIJobFile() }); + + const encoding: ZosEncoding = { kind: "binary" }; + + const promptMock = jest.spyOn(SharedUtils, "promptForEncoding"); + const executeCommandMock = jest.spyOn(vscode.commands, "executeCommand").mockImplementation(); + const setEncodingSpy = jest.spyOn(spoolNode, "setEncoding").mockImplementation(); + const fetchSpoolAtUriMock = jest.spyOn(JobFSProvider.instance, "fetchSpoolAtUri").mockImplementation(); + + await testTree.openWithEncoding(spoolNode, encoding); + expect(promptMock).toHaveBeenCalledTimes(0); + expect(setEncodingSpy).toHaveBeenCalledWith(encoding); + expect(fetchSpoolAtUriMock).toHaveBeenCalledTimes(1); + expect(executeCommandMock).toHaveBeenCalledTimes(1); + }); + + it("should open a Job Spool file with an encoding (ascii, prompted)", async () => { + const testTree = new JobTree(); + + const spoolNode = new ZoweSpoolNode({ label: "SPOOL", collapsibleState: vscode.TreeItemCollapsibleState.None, spool: createIJobFile() }); + + const encoding: ZosEncoding = { kind: "text" }; + + const promptMock = jest.spyOn(SharedUtils, "promptForEncoding").mockResolvedValue(encoding); + const executeCommandMock = jest.spyOn(vscode.commands, "executeCommand").mockImplementation(); + const fetchSpoolAtUriMock = jest.spyOn(JobFSProvider.instance, "fetchSpoolAtUri").mockImplementation(); + const setEncodingSpy = jest.spyOn(spoolNode, "setEncoding").mockImplementation(); + + await testTree.openWithEncoding(spoolNode); + expect(promptMock).toHaveBeenCalledWith(spoolNode); + expect(promptMock).toHaveBeenCalledTimes(1); + expect(setEncodingSpy).toHaveBeenCalledWith(encoding); + expect(fetchSpoolAtUriMock).toHaveBeenCalledTimes(1); + expect(executeCommandMock).toHaveBeenCalledTimes(1); + }); + + it("should open a Job Spool file with an encoding (ascii, provided)", async () => { + const testTree = new JobTree(); + + const spoolNode = new ZoweSpoolNode({ label: "SPOOL", collapsibleState: vscode.TreeItemCollapsibleState.None, spool: createIJobFile() }); + + const encoding: ZosEncoding = { kind: "text" }; + + const promptMock = jest.spyOn(SharedUtils, "promptForEncoding"); + const executeCommandMock = jest.spyOn(vscode.commands, "executeCommand").mockImplementation(); + const setEncodingSpy = jest.spyOn(spoolNode, "setEncoding").mockImplementation(); + const fetchSpoolAtUriMock = jest.spyOn(JobFSProvider.instance, "fetchSpoolAtUri").mockImplementation(); + + await testTree.openWithEncoding(spoolNode, encoding); + expect(promptMock).toHaveBeenCalledTimes(0); + expect(setEncodingSpy).toHaveBeenCalledWith(encoding); + expect(fetchSpoolAtUriMock).toHaveBeenCalledTimes(1); + expect(executeCommandMock).toHaveBeenCalledTimes(1); + }); + + it("should open a Job Spool file with an encoding (other, prompted)", async () => { + const testTree = new JobTree(); + + const spoolNode = new ZoweSpoolNode({ label: "SPOOL", collapsibleState: vscode.TreeItemCollapsibleState.None, spool: createIJobFile() }); + + const encoding: ZosEncoding = { kind: "other", codepage: "IBM-1147" }; + + const promptMock = jest.spyOn(SharedUtils, "promptForEncoding").mockResolvedValue(encoding); + const executeCommandMock = jest.spyOn(vscode.commands, "executeCommand").mockImplementation(); + const fetchSpoolAtUriMock = jest.spyOn(JobFSProvider.instance, "fetchSpoolAtUri").mockImplementation(); + const setEncodingSpy = jest.spyOn(spoolNode, "setEncoding").mockImplementation(); + + await testTree.openWithEncoding(spoolNode); + expect(promptMock).toHaveBeenCalledWith(spoolNode); + expect(promptMock).toHaveBeenCalledTimes(1); + expect(setEncodingSpy).toHaveBeenCalledWith(encoding); + expect(fetchSpoolAtUriMock).toHaveBeenCalledTimes(1); + expect(executeCommandMock).toHaveBeenCalledTimes(1); + }); + + it("should open a Job Spool file with an encoding (other, provided)", async () => { + const testTree = new JobTree(); + + const spoolNode = new ZoweSpoolNode({ label: "SPOOL", collapsibleState: vscode.TreeItemCollapsibleState.None, spool: createIJobFile() }); + + const encoding: ZosEncoding = { kind: "other", codepage: "IBM-1147" }; + + const promptMock = jest.spyOn(SharedUtils, "promptForEncoding"); + const executeCommandMock = jest.spyOn(vscode.commands, "executeCommand").mockImplementation(); + const setEncodingSpy = jest.spyOn(spoolNode, "setEncoding").mockImplementation(); + const fetchSpoolAtUriMock = jest.spyOn(JobFSProvider.instance, "fetchSpoolAtUri").mockImplementation(); + + await testTree.openWithEncoding(spoolNode, encoding); + expect(promptMock).toHaveBeenCalledTimes(0); + expect(setEncodingSpy).toHaveBeenCalledWith(encoding); + expect(fetchSpoolAtUriMock).toHaveBeenCalledTimes(1); + expect(executeCommandMock).toHaveBeenCalledTimes(1); + }); + + it("should open a Job Spool file with an encoding (undefined, prompted)", async () => { + const testTree = new JobTree(); + + const spoolNode = new ZoweSpoolNode({ label: "SPOOL", collapsibleState: vscode.TreeItemCollapsibleState.None, spool: createIJobFile() }); + + const promptMock = jest.spyOn(SharedUtils, "promptForEncoding").mockResolvedValue(undefined); + const executeCommandMock = jest.spyOn(vscode.commands, "executeCommand").mockImplementation(); + const fetchSpoolAtUriMock = jest.spyOn(JobFSProvider.instance, "fetchSpoolAtUri").mockImplementation(); + const setEncodingSpy = jest.spyOn(spoolNode, "setEncoding").mockImplementation(); + + await testTree.openWithEncoding(spoolNode); + expect(promptMock).toHaveBeenCalledWith(spoolNode); + expect(promptMock).toHaveBeenCalledTimes(1); + expect(setEncodingSpy).toHaveBeenCalledTimes(0); + expect(fetchSpoolAtUriMock).toHaveBeenCalledTimes(0); + expect(executeCommandMock).toHaveBeenCalledTimes(0); + }); +}); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/job/ZoweJobNode.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/job/ZoweJobNode.unit.test.ts index 96fae712cb..056c64b2a9 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/job/ZoweJobNode.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/job/ZoweJobNode.unit.test.ts @@ -13,7 +13,7 @@ jest.mock("@zowe/zos-jobs-for-zowe-sdk"); import * as vscode from "vscode"; import * as zosjobs from "@zowe/zos-jobs-for-zowe-sdk"; import * as zosmf from "@zowe/zosmf-for-zowe-sdk"; -import { createIJobFile, createIJobObject, createJobSessionNode } from "../../../__mocks__/mockCreators/jobs"; +import { createIJobFile, createIJobObject, createJobNode, createJobSessionNode } from "../../../__mocks__/mockCreators/jobs"; import { imperative, IZoweJobTreeNode, ProfilesCache, Gui, Sorting } from "@zowe/zowe-explorer-api"; import { TreeViewUtils } from "../../../../src/utils/TreeViewUtils"; import { @@ -34,6 +34,7 @@ import { ZoweJobNode, ZoweSpoolNode } from "../../../../src/trees/job/ZoweJobNod import { SharedContext } from "../../../../src/trees/shared/SharedContext"; import { SharedTreeProviders } from "../../../../src/trees/shared/SharedTreeProviders"; import { JobInit } from "../../../../src/trees/job/JobInit"; +import { ZoweLogger } from "../../../../src/tools/ZoweLogger"; async function createGlobalMocks() { const globalMocks = { @@ -813,6 +814,154 @@ describe("ZoweJobNode unit tests - Function saveSearch", () => { }); }); +describe("ZoweJobNode unit tests - Function getEncodingInMap", () => { + it("should get the encoding in the map", async () => { + const globalMocks = await createGlobalMocks(); + JobFSProvider.instance.encodingMap["fakePath"] = { kind: "text" }; + const encoding = globalMocks.testJobNode.getEncodingInMap("fakePath"); + expect(encoding).toEqual({ kind: "text" }); + }); +}); + +describe("ZoweJobNode unit tests - Function updateEncodingInMap", () => { + it("should update the encoding in the map", async () => { + const globalMocks = await createGlobalMocks(); + globalMocks.testJobNode.updateEncodingInMap("fakePath", { kind: "binary" }); + expect(JobFSProvider.instance.encodingMap["fakePath"]).toEqual({ kind: "binary" }); + }); +}); + +describe("ZoweJobNode unit tests - Function getEncoding", () => { + it("should update the encoding of the node", () => { + const testNode = createJobNode(createJobSessionNode(createISession(), createIProfile()), createIProfile()); + const getEncodingForFileSpy = jest + .spyOn(JobFSProvider.instance, "getEncodingForFile") + .mockReturnValue({ kind: "other", codepage: "IBM-1147" }); + const encoding = testNode.getEncoding(); + expect(getEncodingForFileSpy).toHaveBeenCalledTimes(1); + expect(getEncodingForFileSpy).toHaveBeenCalledWith(testNode.resourceUri); + expect(encoding).toEqual({ kind: "other", codepage: "IBM-1147" }); + }); +}); + +describe("ZoweJobNode unit tests - Function setEncoding", () => { + const zoweLoggerTraceSpy = jest.spyOn(ZoweLogger, "trace").mockImplementation(); + const setEncodingForFileSpy = jest.spyOn(JobFSProvider.instance, "setEncodingForFile").mockImplementation(); + const existsSpy = jest.spyOn(JobFSProvider.instance, "exists"); + + beforeEach(() => { + jest.clearAllMocks(); + JobFSProvider.instance.encodingMap = {}; + }); + + afterAll(() => { + zoweLoggerTraceSpy.mockRestore(); + setEncodingForFileSpy.mockRestore(); + existsSpy.mockRestore(); + }); + + it("should error if set encoding is called on a non-spool node", () => { + const testNode = createJobNode(createJobSessionNode(createISession(), createIProfile()), createIProfile()); + const updateEncodingSpy = jest.spyOn(testNode, "updateEncodingInMap"); + + testNode.dirty = false; + + let e: Error; + try { + testNode.setEncoding({ kind: "text" }); + } catch (err) { + e = err; + } + + expect(e).toBeDefined(); + expect(e.message).toEqual("Cannot set encoding for node with context job"); + expect(existsSpy).not.toHaveBeenCalled(); + expect(setEncodingForFileSpy).not.toHaveBeenCalled(); + expect(updateEncodingSpy).not.toHaveBeenCalled(); + expect(testNode.dirty).toEqual(false); + }); + + it("should error if the resource does not exist", () => { + const testNode = new ZoweSpoolNode({ label: "SPOOL", collapsibleState: vscode.TreeItemCollapsibleState.None, spool: createIJobFile() }); + const updateEncodingSpy = jest.spyOn(testNode, "updateEncodingInMap"); + testNode.dirty = false; + + existsSpy.mockReturnValueOnce(false); + + let e: Error; + try { + testNode.setEncoding({ kind: "text" }); + } catch (err) { + e = err; + } + + expect(e).toBeDefined(); + expect(e.message).toEqual("Cannot set encoding for non-existent node"); + expect(existsSpy).toHaveBeenCalledWith(testNode.resourceUri); + expect(setEncodingForFileSpy).not.toHaveBeenCalled(); + expect(updateEncodingSpy).not.toHaveBeenCalled(); + expect(testNode.dirty).toEqual(false); + }); + + it("should delete a null encoding from the provider", () => { + const testParentNode = createJobNode(createJobSessionNode(createISession(), createIProfile()), createIProfile()); + const testNode = new ZoweSpoolNode({ + label: "SPOOL", + collapsibleState: vscode.TreeItemCollapsibleState.None, + spool: createIJobFile(), + parentNode: testParentNode, + }); + const updateEncodingSpy = jest.spyOn(testNode, "updateEncodingInMap").mockImplementation(); + const nodePath = testNode.resourceUri.path; + + testNode.dirty = false; + JobFSProvider.instance.encodingMap[nodePath] = { kind: "text" }; + existsSpy.mockReturnValueOnce(true); + + let e: Error; + try { + testNode.setEncoding(null); + } catch (err) { + e = err; + } + + expect(e).not.toBeDefined(); + expect(existsSpy).toHaveBeenCalledWith(testNode.resourceUri); + expect(setEncodingForFileSpy).toHaveBeenCalledWith(testNode.resourceUri, null); + expect(updateEncodingSpy).not.toHaveBeenCalled(); + expect(JobFSProvider.instance.encodingMap[nodePath]).not.toBeDefined(); + expect(testNode.dirty).toEqual(true); + }); + + it("should update the encoding in the provider map", () => { + const testParentNode = createJobNode(createJobSessionNode(createISession(), createIProfile()), createIProfile()); + const testNode = new ZoweSpoolNode({ + label: "SPOOL", + collapsibleState: vscode.TreeItemCollapsibleState.None, + spool: createIJobFile(), + parentNode: testParentNode, + }); + const nodePath = testNode.resourceUri.path; + const updateEncodingSpy = jest.spyOn(testNode, "updateEncodingInMap").mockImplementation(); + + testNode.dirty = false; + existsSpy.mockReturnValueOnce(true); + + let e: Error; + try { + testNode.setEncoding({ kind: "binary" }); + } catch (err) { + e = err; + } + + expect(e).not.toBeDefined(); + expect(existsSpy).toHaveBeenCalledWith(testNode.resourceUri); + expect(setEncodingForFileSpy).toHaveBeenCalledWith(testNode.resourceUri, { kind: "binary" }); + expect(updateEncodingSpy).toHaveBeenCalledWith(nodePath, { kind: "binary" }); + expect(testNode.dirty).toEqual(true); + }); +}); + describe("ZosJobsProvider - Function searchPrompt", () => { it("should exit if searchCriteria is undefined", async () => { const globalMocks = await createGlobalMocks(); 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 8e0bbd2444..a6857a1a57 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 @@ -12,7 +12,7 @@ import * as vscode from "vscode"; import { createIProfile, createISession, createInstanceOfProfile } from "../../../__mocks__/mockCreators/shared"; import { createDatasetSessionNode } from "../../../__mocks__/mockCreators/datasets"; -import { createUSSNode } from "../../../__mocks__/mockCreators/uss"; +import { createUSSNode, createUSSSessionNode } from "../../../__mocks__/mockCreators/uss"; import { UssFSProvider } from "../../../../src/trees/uss/UssFSProvider"; import { imperative, ProfilesCache, Gui, ZosEncoding, BaseProvider } from "@zowe/zowe-explorer-api"; import { Constants } from "../../../../src/configuration/Constants"; @@ -21,12 +21,14 @@ import { ZoweLocalStorage } from "../../../../src/tools/ZoweLocalStorage"; import { ZoweLogger } from "../../../../src/tools/ZoweLogger"; import { DatasetFSProvider } from "../../../../src/trees/dataset/DatasetFSProvider"; import { ZoweDatasetNode } from "../../../../src/trees/dataset/ZoweDatasetNode"; -import { ZoweJobNode } from "../../../../src/trees/job/ZoweJobNode"; +import { ZoweJobNode, ZoweSpoolNode } from "../../../../src/trees/job/ZoweJobNode"; import { SharedUtils } from "../../../../src/trees/shared/SharedUtils"; import { ZoweUSSNode } from "../../../../src/trees/uss/ZoweUSSNode"; import { AuthUtils } from "../../../../src/utils/AuthUtils"; import { SharedTreeProviders } from "../../../../src/trees/shared/SharedTreeProviders"; import { MockedProperty } from "../../../__mocks__/mockUtils"; +import { createIJobFile, createJobSessionNode } from "../../../__mocks__/mockCreators/jobs"; +import { JobFSProvider } from "../../../../src/trees/job/JobFSProvider"; function createGlobalMocks() { const newMocks = { @@ -536,6 +538,512 @@ describe("Shared utils unit tests - function promptForEncoding", () => { expect(blockMocks.showQuickPick).toHaveBeenCalled(); expect(blockMocks.showQuickPick.mock.calls[0][1]).toEqual(expect.objectContaining({ placeHolder: "Current encoding is IBM-1047" })); }); + + it("remembers cached encoding for spool node", async () => { + const blockMocks = createBlockMocks(); + const sessionNode = createJobSessionNode(blockMocks.session, blockMocks.profile); + const jobNode = new ZoweJobNode({ + label: "TESTPS", + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + session: blockMocks.session, + profile: blockMocks.profile, + parentNode: sessionNode, + }); + const spoolNode = new ZoweSpoolNode({ + label: "SPOOL", + collapsibleState: vscode.TreeItemCollapsibleState.None, + spool: createIJobFile(), + parentNode: jobNode, + }); + JobFSProvider.instance.encodingMap[spoolNode.resourceUri.path] = { kind: "text" }; + blockMocks.getEncodingForFile.mockReturnValueOnce(undefined); + await SharedUtils.promptForEncoding(spoolNode); + expect(blockMocks.showQuickPick).toHaveBeenCalled(); + expect(blockMocks.showQuickPick.mock.calls[0][1]).toEqual(expect.objectContaining({ placeHolder: "Current encoding is EBCDIC" })); + }); + + it("prompts for text encoding for Spool file", async () => { + const blockMocks = createBlockMocks(); + const sessionNode = createJobSessionNode(blockMocks.session, blockMocks.profile); + const jobNode = new ZoweJobNode({ + label: "TESTPS", + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + session: blockMocks.session, + profile: blockMocks.profile, + parentNode: sessionNode, + }); + const node = new ZoweSpoolNode({ + label: "testFile", + collapsibleState: vscode.TreeItemCollapsibleState.None, + session: blockMocks.session, + profile: blockMocks.profile, + parentNode: jobNode, + spool: createIJobFile(), + }); + + blockMocks.showQuickPick.mockImplementationOnce(async (items) => items[0]); + blockMocks.getEncodingForFile.mockReturnValueOnce(undefined); + const encoding = await SharedUtils.promptForEncoding(node); + expect(blockMocks.showQuickPick).toHaveBeenCalled(); + expect(encoding).toEqual(textEncoding); + }); + + it("prompts for binary encoding for Spool file", async () => { + const blockMocks = createBlockMocks(); + const sessionNode = createJobSessionNode(blockMocks.session, blockMocks.profile); + const jobNode = new ZoweJobNode({ + label: "TESTPS", + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + session: blockMocks.session, + profile: blockMocks.profile, + parentNode: sessionNode, + }); + const node = new ZoweSpoolNode({ + label: "testFile", + collapsibleState: vscode.TreeItemCollapsibleState.None, + session: blockMocks.session, + profile: blockMocks.profile, + parentNode: jobNode, + spool: createIJobFile(), + }); + + blockMocks.showQuickPick.mockImplementationOnce(async (items) => items[1]); + const encoding = await SharedUtils.promptForEncoding(node); + expect(blockMocks.showQuickPick).toHaveBeenCalled(); + expect(encoding).toEqual(binaryEncoding); + }); + + it("prompts for other encoding for Spool file and returns codepage", async () => { + const blockMocks = createBlockMocks(); + const sessionNode = createJobSessionNode(blockMocks.session, blockMocks.profile); + const jobNode = new ZoweJobNode({ + label: "TESTPS", + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + session: blockMocks.session, + profile: blockMocks.profile, + parentNode: sessionNode, + }); + const node = new ZoweSpoolNode({ + label: "testFile", + collapsibleState: vscode.TreeItemCollapsibleState.None, + session: blockMocks.session, + profile: blockMocks.profile, + parentNode: jobNode, + spool: createIJobFile(), + }); + + blockMocks.showQuickPick.mockImplementationOnce(async (items) => items[2]); + blockMocks.showInputBox.mockResolvedValueOnce("IBM-1047"); + const encoding = await SharedUtils.promptForEncoding(node); + expect(blockMocks.showQuickPick).toHaveBeenCalled(); + expect(blockMocks.showInputBox).toHaveBeenCalled(); + expect(encoding).toEqual(otherEncoding); + }); + + it("prompts for other encoding for Spool file and returns undefined", async () => { + const blockMocks = createBlockMocks(); + const sessionNode = createJobSessionNode(blockMocks.session, blockMocks.profile); + const jobNode = new ZoweJobNode({ + label: "TESTPS", + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + session: blockMocks.session, + profile: blockMocks.profile, + parentNode: sessionNode, + }); + const node = new ZoweSpoolNode({ + label: "testFile", + collapsibleState: vscode.TreeItemCollapsibleState.None, + session: blockMocks.session, + profile: blockMocks.profile, + parentNode: jobNode, + spool: createIJobFile(), + }); + + blockMocks.showQuickPick.mockImplementationOnce(async (items) => items[2]); + blockMocks.showInputBox.mockResolvedValueOnce(undefined); + const encoding = await SharedUtils.promptForEncoding(node); + expect(blockMocks.showQuickPick).toHaveBeenCalled(); + expect(blockMocks.showInputBox).toHaveBeenCalled(); + expect(encoding).toBeUndefined(); + }); +}); + +describe("Shared utils unit tests - function getCachedEncoding", () => { + const mockSession = createISession(); + const mockProfile = createIProfile(); + describe("Spool nodes", () => { + it("correctly returns the cached encoding for binary", async () => { + const sessionNode = createJobSessionNode(mockSession, mockProfile); + const jobNode = new ZoweJobNode({ + label: "TESTPS", + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + session: mockSession, + profile: mockProfile, + parentNode: sessionNode, + }); + const node = new ZoweSpoolNode({ + label: "testFile", + collapsibleState: vscode.TreeItemCollapsibleState.None, + session: mockSession, + profile: mockProfile, + parentNode: jobNode, + spool: createIJobFile(), + }); + const encoding = { kind: "binary" }; + + const encodingMapSpy = jest.spyOn(node, "getEncodingInMap").mockResolvedValue(encoding); + const response = await SharedUtils.getCachedEncoding(node); + + expect(encodingMapSpy).toHaveBeenCalledWith(node.resourceUri.path); + expect(response).toEqual(encoding.kind); + }); + + it("correctly returns the cached encoding for text", async () => { + const sessionNode = createJobSessionNode(mockSession, mockProfile); + const jobNode = new ZoweJobNode({ + label: "TESTPS", + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + session: mockSession, + profile: mockProfile, + parentNode: sessionNode, + }); + const node = new ZoweSpoolNode({ + label: "testFile", + collapsibleState: vscode.TreeItemCollapsibleState.None, + session: mockSession, + profile: mockProfile, + parentNode: jobNode, + spool: createIJobFile(), + }); + const encoding = { kind: "text" }; + + const encodingMapSpy = jest.spyOn(node, "getEncodingInMap").mockResolvedValue(encoding); + const response = await SharedUtils.getCachedEncoding(node); + + expect(encodingMapSpy).toHaveBeenCalledWith(node.resourceUri.path); + expect(response).toEqual(encoding.kind); + }); + + it("correctly returns the cached encoding for other", async () => { + const sessionNode = createJobSessionNode(mockSession, mockProfile); + const jobNode = new ZoweJobNode({ + label: "TESTPS", + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + session: mockSession, + profile: mockProfile, + parentNode: sessionNode, + }); + const node = new ZoweSpoolNode({ + label: "testFile", + collapsibleState: vscode.TreeItemCollapsibleState.None, + session: mockSession, + profile: mockProfile, + parentNode: jobNode, + spool: createIJobFile(), + }); + const encoding = { kind: "other", codepage: "IBM-1147" }; + + const encodingMapSpy = jest.spyOn(node, "getEncodingInMap").mockResolvedValue(encoding); + const response = await SharedUtils.getCachedEncoding(node); + + expect(encodingMapSpy).toHaveBeenCalledWith(node.resourceUri.path); + expect(response).toEqual(encoding.codepage); + }); + + it("correctly returns the cached encoding for undefined", async () => { + const sessionNode = createJobSessionNode(mockSession, mockProfile); + const jobNode = new ZoweJobNode({ + label: "TESTPS", + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + session: mockSession, + profile: mockProfile, + parentNode: sessionNode, + }); + const node = new ZoweSpoolNode({ + label: "testFile", + collapsibleState: vscode.TreeItemCollapsibleState.None, + session: mockSession, + profile: mockProfile, + parentNode: jobNode, + spool: createIJobFile(), + }); + const encoding = undefined; + + const encodingMapSpy = jest.spyOn(node, "getEncodingInMap").mockResolvedValue(encoding); + const response = await SharedUtils.getCachedEncoding(node); + + expect(encodingMapSpy).toHaveBeenCalledWith(node.resourceUri.path); + expect(response).toEqual(encoding); + }); + }); + + describe("USS nodes", () => { + it("correctly returns the cached encoding for binary", async () => { + const sessionNode = createUSSSessionNode(mockSession, mockProfile); + const ussNode = new ZoweUSSNode({ + label: "TEST.PS", + collapsibleState: vscode.TreeItemCollapsibleState.None, + contextOverride: Constants.USS_BINARY_FILE_CONTEXT, + session: mockSession, + profile: mockProfile, + parentNode: sessionNode, + }); + const encoding = { kind: "binary" }; + + const encodingMapSpy = jest.spyOn(ussNode, "getEncodingInMap").mockResolvedValue(encoding); + const response = await SharedUtils.getCachedEncoding(ussNode); + + expect(encodingMapSpy).toHaveBeenCalledWith(ussNode.fullPath); + expect(response).toEqual(encoding.kind); + }); + + it("correctly returns the cached encoding for text", async () => { + const sessionNode = createUSSSessionNode(mockSession, mockProfile); + const ussNode = new ZoweUSSNode({ + label: "TEST.PS", + collapsibleState: vscode.TreeItemCollapsibleState.None, + contextOverride: Constants.USS_TEXT_FILE_CONTEXT, + session: mockSession, + profile: mockProfile, + parentNode: sessionNode, + }); + const encoding = { kind: "text" }; + + const encodingMapSpy = jest.spyOn(ussNode, "getEncodingInMap").mockResolvedValue(encoding); + const response = await SharedUtils.getCachedEncoding(ussNode); + + expect(encodingMapSpy).toHaveBeenCalledWith(ussNode.fullPath); + expect(response).toEqual(encoding.kind); + }); + + it("correctly returns the cached encoding for other", async () => { + const sessionNode = createUSSSessionNode(mockSession, mockProfile); + const ussNode = new ZoweUSSNode({ + label: "TEST.PS", + collapsibleState: vscode.TreeItemCollapsibleState.None, + contextOverride: Constants.USS_TEXT_FILE_CONTEXT, + session: mockSession, + profile: mockProfile, + parentNode: sessionNode, + }); + const encoding = { kind: "other", codepage: "IBM-1147" }; + + const encodingMapSpy = jest.spyOn(ussNode, "getEncodingInMap").mockResolvedValue(encoding); + const response = await SharedUtils.getCachedEncoding(ussNode); + + expect(encodingMapSpy).toHaveBeenCalledWith(ussNode.fullPath); + expect(response).toEqual(encoding.codepage); + }); + + it("correctly returns the cached encoding for undefined", async () => { + const sessionNode = createUSSSessionNode(mockSession, mockProfile); + const ussNode = new ZoweUSSNode({ + label: "TEST.PS", + collapsibleState: vscode.TreeItemCollapsibleState.None, + contextOverride: Constants.USS_TEXT_FILE_CONTEXT, + session: mockSession, + profile: mockProfile, + parentNode: sessionNode, + }); + const encoding = undefined; + + const encodingMapSpy = jest.spyOn(ussNode, "getEncodingInMap").mockResolvedValue(encoding); + const response = await SharedUtils.getCachedEncoding(ussNode); + + expect(encodingMapSpy).toHaveBeenCalledWith(ussNode.fullPath); + expect(response).toEqual(encoding); + }); + }); + + describe("Dataset nodes", () => { + describe("Sequential", () => { + it("correctly returns the cached encoding for binary", async () => { + const sessionNode = createDatasetSessionNode(mockSession, mockProfile); + const dsNode = new ZoweDatasetNode({ + label: "TEST.PS", + collapsibleState: vscode.TreeItemCollapsibleState.None, + contextOverride: Constants.DS_DS_BINARY_CONTEXT, + session: mockSession, + profile: mockProfile, + parentNode: sessionNode, + }); + const encoding = { kind: "binary" }; + + const encodingMapSpy = jest.spyOn(dsNode, "getEncodingInMap").mockResolvedValue(encoding); + const response = await SharedUtils.getCachedEncoding(dsNode); + + expect(encodingMapSpy).toHaveBeenCalledWith(dsNode.label); + expect(response).toEqual(encoding.kind); + }); + + it("correctly returns the cached encoding for text", async () => { + const sessionNode = createDatasetSessionNode(mockSession, mockProfile); + const dsNode = new ZoweDatasetNode({ + label: "TEST.PS", + collapsibleState: vscode.TreeItemCollapsibleState.None, + contextOverride: Constants.DS_DS_CONTEXT, + session: mockSession, + profile: mockProfile, + parentNode: sessionNode, + }); + const encoding = { kind: "test" }; + + const encodingMapSpy = jest.spyOn(dsNode, "getEncodingInMap").mockResolvedValue(encoding); + const response = await SharedUtils.getCachedEncoding(dsNode); + + expect(encodingMapSpy).toHaveBeenCalledWith(dsNode.label); + expect(response).toEqual(encoding.kind); + }); + + it("correctly returns the cached encoding for other", async () => { + const sessionNode = createDatasetSessionNode(mockSession, mockProfile); + const dsNode = new ZoweDatasetNode({ + label: "TEST.PS", + collapsibleState: vscode.TreeItemCollapsibleState.None, + contextOverride: Constants.DS_DS_CONTEXT, + session: mockSession, + profile: mockProfile, + parentNode: sessionNode, + }); + const encoding = { kind: "other", codepage: "IBM-1147" }; + + const encodingMapSpy = jest.spyOn(dsNode, "getEncodingInMap").mockResolvedValue(encoding); + const response = await SharedUtils.getCachedEncoding(dsNode); + + expect(encodingMapSpy).toHaveBeenCalledWith(dsNode.label); + expect(response).toEqual(encoding.codepage); + }); + + it("correctly returns the cached encoding for undefined", async () => { + const sessionNode = createDatasetSessionNode(mockSession, mockProfile); + const dsNode = new ZoweDatasetNode({ + label: "TEST.PS", + collapsibleState: vscode.TreeItemCollapsibleState.None, + contextOverride: Constants.DS_DS_CONTEXT, + session: mockSession, + profile: mockProfile, + parentNode: sessionNode, + }); + const encoding = undefined; + + const encodingMapSpy = jest.spyOn(dsNode, "getEncodingInMap").mockResolvedValue(encoding); + const response = await SharedUtils.getCachedEncoding(dsNode); + + expect(encodingMapSpy).toHaveBeenCalledWith(dsNode.label); + expect(response).toEqual(encoding); + }); + }); + + describe("Partitioned", () => { + it("correctly returns the cached encoding for binary", async () => { + const sessionNode = createDatasetSessionNode(mockSession, mockProfile); + const dsNode = new ZoweDatasetNode({ + label: "TEST.PDS", + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + contextOverride: Constants.DS_PDS_CONTEXT, + session: mockSession, + profile: mockProfile, + parentNode: sessionNode, + }); + const memNode = new ZoweDatasetNode({ + label: "TESTMEM", + collapsibleState: vscode.TreeItemCollapsibleState.None, + contextOverride: Constants.DS_MEMBER_BINARY_CONTEXT, + session: mockSession, + profile: mockProfile, + parentNode: dsNode, + }); + const encoding = { kind: "binary" }; + + const encodingMapSpy = jest.spyOn(memNode, "getEncodingInMap").mockResolvedValue(encoding); + const response = await SharedUtils.getCachedEncoding(memNode); + + expect(encodingMapSpy).toHaveBeenCalledWith(`${dsNode.label as string}(${memNode.label as string})`); + expect(response).toEqual(encoding.kind); + }); + + it("correctly returns the cached encoding for text", async () => { + const sessionNode = createDatasetSessionNode(mockSession, mockProfile); + const dsNode = new ZoweDatasetNode({ + label: "TEST.PDS", + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + contextOverride: Constants.DS_PDS_CONTEXT, + session: mockSession, + profile: mockProfile, + parentNode: sessionNode, + }); + const memNode = new ZoweDatasetNode({ + label: "TESTMEM", + collapsibleState: vscode.TreeItemCollapsibleState.None, + contextOverride: Constants.DS_MEMBER_CONTEXT, + session: mockSession, + profile: mockProfile, + parentNode: dsNode, + }); + const encoding = { kind: "text" }; + + const encodingMapSpy = jest.spyOn(memNode, "getEncodingInMap").mockResolvedValue(encoding); + const response = await SharedUtils.getCachedEncoding(memNode); + + expect(encodingMapSpy).toHaveBeenCalledWith(`${dsNode.label as string}(${memNode.label as string})`); + expect(response).toEqual(encoding.kind); + }); + + it("correctly returns the cached encoding for other", async () => { + const sessionNode = createDatasetSessionNode(mockSession, mockProfile); + const dsNode = new ZoweDatasetNode({ + label: "TEST.PDS", + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + contextOverride: Constants.DS_PDS_CONTEXT, + session: mockSession, + profile: mockProfile, + parentNode: sessionNode, + }); + const memNode = new ZoweDatasetNode({ + label: "TESTMEM", + collapsibleState: vscode.TreeItemCollapsibleState.None, + contextOverride: Constants.DS_MEMBER_CONTEXT, + session: mockSession, + profile: mockProfile, + parentNode: dsNode, + }); + const encoding = { kind: "other", codepage: "IBM-1147" }; + + const encodingMapSpy = jest.spyOn(memNode, "getEncodingInMap").mockResolvedValue(encoding); + const response = await SharedUtils.getCachedEncoding(memNode); + + expect(encodingMapSpy).toHaveBeenCalledWith(`${dsNode.label as string}(${memNode.label as string})`); + expect(response).toEqual(encoding.codepage); + }); + + it("correctly returns the cached encoding for undefined", async () => { + const sessionNode = createDatasetSessionNode(mockSession, mockProfile); + const dsNode = new ZoweDatasetNode({ + label: "TEST.PDS", + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + contextOverride: Constants.DS_PDS_CONTEXT, + session: mockSession, + profile: mockProfile, + parentNode: sessionNode, + }); + const memNode = new ZoweDatasetNode({ + label: "TESTMEM", + collapsibleState: vscode.TreeItemCollapsibleState.None, + contextOverride: Constants.DS_MEMBER_CONTEXT, + session: mockSession, + profile: mockProfile, + parentNode: dsNode, + }); + const encoding = undefined; + + const encodingMapSpy = jest.spyOn(memNode, "getEncodingInMap").mockResolvedValue(encoding); + const response = await SharedUtils.getCachedEncoding(memNode); + + expect(encodingMapSpy).toHaveBeenCalledWith(`${dsNode.label as string}(${memNode.label as string})`); + expect(response).toEqual(encoding); + }); + }); + }); }); describe("Shared utils unit tests - function parseFavorites", () => { diff --git a/packages/zowe-explorer/package.json b/packages/zowe-explorer/package.json index acb38806ec..9310a620f8 100644 --- a/packages/zowe-explorer/package.json +++ b/packages/zowe-explorer/package.json @@ -1204,6 +1204,11 @@ "command": "zowe.issueTsoCmd", "group": "000_zowe_jobsMainframeInteraction@3" }, + { + "when": "view == zowe.jobs.explorer && viewItem =~ /^(spool.*)/", + "command": "zowe.openWithEncoding", + "group": "000_zowe_jobsMainframeInteraction@4" + }, { "when": "view == zowe.jobs.explorer && viewItem =~ /^(?!.*_fav.*)job.*/", "command": "zowe.jobs.refreshJob", diff --git a/packages/zowe-explorer/src/trees/job/JobFSProvider.ts b/packages/zowe-explorer/src/trees/job/JobFSProvider.ts index ec428e9c43..2aee3ed163 100644 --- a/packages/zowe-explorer/src/trees/job/JobFSProvider.ts +++ b/packages/zowe-explorer/src/trees/job/JobFSProvider.ts @@ -26,8 +26,9 @@ import { FsJobsUtils, FsAbstractUtils, ZoweExplorerApiType, + ZosEncoding, } from "@zowe/zowe-explorer-api"; -import { IJob, IJobFile } from "@zowe/zos-jobs-for-zowe-sdk"; +import { IDownloadSpoolContentParms, IJob, IJobFile } from "@zowe/zos-jobs-for-zowe-sdk"; import { Profiles } from "../../configuration/Profiles"; import { ZoweExplorerApiRegister } from "../../extending/ZoweExplorerApiRegister"; import { SharedContext } from "../shared/SharedContext"; @@ -35,12 +36,15 @@ import { AuthUtils } from "../../utils/AuthUtils"; export class JobFSProvider extends BaseProvider implements vscode.FileSystemProvider { private static _instance: JobFSProvider; + private constructor() { super(); ZoweExplorerApiRegister.addFileSystemEvent(ZoweScheme.Jobs, this.onDidChangeFile); this.root = new DirEntry(""); } + public encodingMap: Record = {}; + public static get instance(): JobFSProvider { if (!JobFSProvider._instance) { JobFSProvider._instance = new JobFSProvider(); @@ -205,10 +209,19 @@ export class JobFSProvider extends BaseProvider implements vscode.FileSystemProv const jesApi = ZoweExplorerApiRegister.getJesApi(spoolEntry.metadata.profile); try { if (jesApi.downloadSingleSpool) { - await jesApi.downloadSingleSpool({ + const spoolDownloadObject: IDownloadSpoolContentParms = { jobFile: spoolEntry.spool, stream: bufBuilder, - }); + }; + + // Handle encoding and binary options + if (spoolEntry.encoding) { + spoolDownloadObject.binary = spoolEntry.encoding.kind === "binary"; + if (spoolEntry.encoding.kind === "other") { + spoolDownloadObject.encoding = spoolEntry.encoding.codepage; + } + } + await jesApi.downloadSingleSpool(spoolDownloadObject); } else { const jobEntry = this._lookupParentDirectory(uri) as JobEntry; bufBuilder.write(await jesApi.getSpoolContentById(jobEntry.job.jobname, jobEntry.job.jobid, spoolEntry.spool.id)); diff --git a/packages/zowe-explorer/src/trees/job/JobTree.ts b/packages/zowe-explorer/src/trees/job/JobTree.ts index 47b02e398d..54d1f9a290 100644 --- a/packages/zowe-explorer/src/trees/job/JobTree.ts +++ b/packages/zowe-explorer/src/trees/job/JobTree.ts @@ -12,7 +12,17 @@ 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, ZoweExplorerApiType } from "@zowe/zowe-explorer-api"; +import { + Gui, + Validation, + imperative, + IZoweJobTreeNode, + PersistenceSchemaEnum, + Poller, + Types, + ZoweExplorerApiType, + ZosEncoding, +} from "@zowe/zowe-explorer-api"; import { ZoweJobNode } from "./ZoweJobNode"; import { JobFSProvider } from "./JobFSProvider"; import { JobUtils } from "./JobUtils"; @@ -1162,6 +1172,22 @@ export class JobTree extends ZoweTreeProvider implements Types inputBox.show(); return inputBox; } + + /** + * Opens the spool file with a particular encoding + * @param {IZoweJobTreeNode} node The Job Tree Node to open with encoding + * @param {ZosEncoding} encoding The encoding to use to open the Job Tree Node + */ + + public async openWithEncoding(node: IZoweJobTreeNode, encoding?: ZosEncoding): Promise { + encoding ??= await SharedUtils.promptForEncoding(node); + if (encoding !== undefined) { + // Set the encoding, fetch the new contents with the encoding, and open the spool file. + await node.setEncoding(encoding); + await JobFSProvider.instance.fetchSpoolAtUri(node.resourceUri); + await vscode.commands.executeCommand("vscode.open", node.resourceUri); + } + } } /** diff --git a/packages/zowe-explorer/src/trees/job/ZoweJobNode.ts b/packages/zowe-explorer/src/trees/job/ZoweJobNode.ts index 4072a6ee77..abb52708f0 100644 --- a/packages/zowe-explorer/src/trees/job/ZoweJobNode.ts +++ b/packages/zowe-explorer/src/trees/job/ZoweJobNode.ts @@ -12,7 +12,16 @@ 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, ZoweExplorerApiType, ZoweScheme, ZoweTreeNode } from "@zowe/zowe-explorer-api"; +import { + FsJobsUtils, + imperative, + IZoweJobTreeNode, + Sorting, + ZosEncoding, + ZoweExplorerApiType, + ZoweScheme, + ZoweTreeNode, +} from "@zowe/zowe-explorer-api"; import { JobFSProvider } from "./JobFSProvider"; import { JobUtils } from "./JobUtils"; import { Constants } from "../../configuration/Constants"; @@ -305,6 +314,57 @@ export class ZoweJobNode extends ZoweTreeNode implements IZoweJobTreeNode { return this.session ? this : this.getParent()?.getSessionNode() ?? this; } + /** + * Get the encoding from the JobFSProvider encoding map for a URI + * @param {string} uriPath The URI to look up in the encoding map + * @returns {ZosEncoding} + */ + public getEncodingInMap(uriPath: string): ZosEncoding { + return JobFSProvider.instance.encodingMap[uriPath]; + } + + /** + * Update the encoding for a URI in the JobFSProvider encoding map + * @param {string} uriPath The URI to update in the encoding map + * @param {ZosEncoding} encoding The encoding to associate with the URI + */ + public updateEncodingInMap(uriPath: string, encoding: ZosEncoding): void { + JobFSProvider.instance.encodingMap[uriPath] = encoding; + } + + /** + * Get the encoding for a particular Job node + * @returns {ZosEncoding} + */ + public getEncoding(): ZosEncoding { + return JobFSProvider.instance.getEncodingForFile(this.resourceUri); + } + + /** + * Update the encoding for a particular Job node + * @param {ZosEncoding} encoding The encoding to use for the Job node + */ + public setEncoding(encoding: ZosEncoding): void { + ZoweLogger.trace("ZoweJobNode.setEncoding called."); + if (!this.contextValue.startsWith(Constants.JOBS_SPOOL_CONTEXT)) { + throw new Error(`Cannot set encoding for node with context ${this.contextValue}`); + } + + if (JobFSProvider.instance.exists(this.resourceUri)) { + JobFSProvider.instance.setEncodingForFile(this.resourceUri, encoding); + } else { + throw new Error(`Cannot set encoding for non-existent node`); + } + + if (encoding != null) { + this.updateEncodingInMap(this.resourceUri.path, encoding); + } else { + delete JobFSProvider.instance.encodingMap[this.resourceUri.path]; + } + + this.dirty = true; + } + public set owner(newOwner: string) { if (newOwner !== undefined) { if (newOwner.length === 0) { diff --git a/packages/zowe-explorer/src/trees/shared/SharedUtils.ts b/packages/zowe-explorer/src/trees/shared/SharedUtils.ts index ef190bc4f5..0c2f6b64d1 100644 --- a/packages/zowe-explorer/src/trees/shared/SharedUtils.ts +++ b/packages/zowe-explorer/src/trees/shared/SharedUtils.ts @@ -144,6 +144,8 @@ export class SharedUtils { let cachedEncoding: ZosEncoding; if (SharedUtils.isZoweUSSTreeNode(node)) { cachedEncoding = await node.getEncodingInMap(node.fullPath); + } else if (SharedUtils.isZoweJobTreeNode(node)) { + cachedEncoding = await node.getEncodingInMap(node.resourceUri.path); } else { const isMemberNode = node.contextValue.startsWith(Constants.DS_MEMBER_CONTEXT); const dsKey = isMemberNode ? `${node.getParent().label as string}(${node.label as string})` : (node.label as string); @@ -190,7 +192,10 @@ export class SharedUtils { .filter(Boolean); } - public static async promptForEncoding(node: IZoweDatasetTreeNode | IZoweUSSTreeNode, taggedEncoding?: string): Promise { + public static async promptForEncoding( + node: IZoweDatasetTreeNode | IZoweUSSTreeNode | IZoweJobTreeNode, + taggedEncoding?: string + ): Promise { const ebcdicItem: vscode.QuickPickItem = { label: vscode.l10n.t("EBCDIC"), description: vscode.l10n.t("z/OS default codepage"),