diff --git a/.github/workflows/zowe-explorer-samples.yml b/.github/workflows/zowe-explorer-samples.yml index 5def721972..b91dd6029f 100644 --- a/.github/workflows/zowe-explorer-samples.yml +++ b/.github/workflows/zowe-explorer-samples.yml @@ -41,5 +41,8 @@ jobs: # install pnpm - run: npm install -g pnpm + - run: pnpm config set network-timeout 60000 && pnpm i + - run: pnpm --filter "zowe-explorer-api" build - run: for dir in samples/*; do pushd $dir && pnpm --ignore-workspace i && pnpm run compile && popd; done + shell: bash diff --git a/package.json b/package.json index 5b8bd22a31..86f6c7a860 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "private": true, "workspaces": [ "packages/*", - "packages/zowe-explorer/webviews/*" + "packages/zowe-explorer/src/webviews" ], "engines": { "vscode": "^1.79.0" @@ -62,8 +62,8 @@ "madge": "pnpm -r run madge", "pretty": "prettier --write .", "build": "pnpm -r run build", - "test": "pnpm -r --reporter append-only run test", - "test:parallel": "pnpm -r --parallel run test", + "test": "pnpm -r --sequential --color --reporter append-only run test", + "test:parallel": "pnpm -r --parallel --color run test", "package": "pnpm run -r package", "pretest:integration": "ts-node ./scripts/create-env.ts", "posttest:integration": "ts-node ./scripts/clean-env.ts", diff --git a/packages/eslint-plugin-zowe-explorer/CHANGELOG.md b/packages/eslint-plugin-zowe-explorer/CHANGELOG.md index 99d099db92..75dbbc9027 100644 --- a/packages/eslint-plugin-zowe-explorer/CHANGELOG.md +++ b/packages/eslint-plugin-zowe-explorer/CHANGELOG.md @@ -7,46 +7,36 @@ All notable changes to the "eslint-plugin-zowe-explorer" package will be documen - Added placeholder `madge` script to `package.json` for workspace script to succeed. - Migrated to new package manager PNPM from Yarn. -## `2.9.1` +## `2.11.2` ### New features and enhancements ### Bug fixes -## `2.11.0` +## `2.11.1` ### New features and enhancements ### Bug fixes -## `2.10.0` +## `2.12.0` -### New features and enhancements +## `2.11.2` -### Bug fixes +## `2.11.1` -## `2.9.2` +## `2.11.0` -### New features and enhancements +## `2.10.0` -### Bug fixes +## `2.9.2` ## `2.9.1` -### New features and enhancements - -### Bug fixes - ## `2.9.0` -### New features and enhancements - -### Bug fixes - ## `2.8.1` -### New features and enhancements - ### Bug fixes - Added `no-floating-promises` rule that ignores floating thenables without a `.catch` method. [#2291](https://github.com/zowe/vscode-extension-for-zowe/issues/2291) @@ -57,14 +47,8 @@ All notable changes to the "eslint-plugin-zowe-explorer" package will be documen - Updated linter rules and addressed linter errors throughout the codebase. [#2184](https://github.com/zowe/vscode-extension-for-zowe/issues/2184) -### Bug fixes - ## `2.7.0` -### New features and enhancements - -### Bug fixes - ## `2.6.0` ### New features and enhancements diff --git a/packages/zowe-explorer-api/CHANGELOG.md b/packages/zowe-explorer-api/CHANGELOG.md index f700b39fde..a21e91ff60 100644 --- a/packages/zowe-explorer-api/CHANGELOG.md +++ b/packages/zowe-explorer-api/CHANGELOG.md @@ -11,6 +11,25 @@ All notable changes to the "zowe-explorer-api" extension will be documented in t - Added `madge` script in `package.json` to track circular dependencies. [#2148](https://github.com/zowe/vscode-extension-for-zowe/issues/2148) - Migrated to new package manager PNPM from Yarn. +## `2.12.0` + +### New features and enhancements + +- Added optional `getTag` function to `ZoweExplorerAPI.IUss` for getting the tag of a file on USS. +- Added new API {ZE Extender MetaData} to allow extenders to have the metadata of registered extenders to aid in team configuration file creation from a view that isn't Zowe Explorer's. [#2394](https://github.com/zowe/vscode-extension-for-zowe/issues/2394) +- Add `sort` and `filter` optional variables for storing sort/filter options alongside tree nodes. [#2420](https://github.com/zowe/vscode-extension-for-zowe/issues/2420) +- Add `stats` optional variable for storing dataset stats (such as user, modified date, etc.) +- Add option enums and types for sorting, filtering and sort direction in tree nodes. [#2420](https://github.com/zowe/vscode-extension-for-zowe/issues/2420) +- Added option for retaining context when generating webviews in Webview API + +## `2.11.2` + +### Bug fixes + +- Bundle Zowe Secrets for issues seen by extenders that use the ProfilesCache for profile management. [#2512](https://github.com/zowe/vscode-extension-for-zowe/issues/2512) + +## `2.11.1` + ## `2.11.0` ### New features and enhancements @@ -32,10 +51,6 @@ All notable changes to the "zowe-explorer-api" extension will be documented in t ## `2.9.2` -### New features and enhancements - -### Bug fixes - ## `2.9.1` ### New features and enhancements diff --git a/packages/zowe-explorer-api/__tests__/__unit__/globals/Gui.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/globals/Gui.unit.test.ts index 28a11d19cc..a8382a571a 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/globals/Gui.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/globals/Gui.unit.test.ts @@ -22,7 +22,7 @@ function createGlobalMocks() { showWarningMessage: jest.fn(), createOutputChannel: jest.fn(), createQuickPick: jest.fn(), - createTreeView: jest.fn(), + createTreeView: jest.fn().mockReturnValue({ onDidCollapseElement: jest.fn() }), createWebviewPanel: jest.fn(), withProgress: jest.fn(), showTextDocument: jest.fn(), 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 d18ff9e596..bba6081f0b 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 @@ -85,6 +85,17 @@ const baseProfileWithToken = { tokenValue: "baseToken", }, }; +const profilemetadata: zowe.imperative.ICommandProfileTypeConfiguration[] = [ + { + type: "acme", + schema: { + type: "object", + title: "acme profile1", + description: "A profile to execute commands", + properties: {}, + }, + }, +]; function createProfInfoMock(profiles: Partial[]): zowe.imperative.ProfileInfo { return { @@ -154,6 +165,20 @@ describe("ProfilesCache", () => { expect(Object.keys(keyring).length).toBe(5); }); + it("addToConfigArray should set the profileTypeConfigurations array", () => { + const profCache = new ProfilesCache(fakeLogger as unknown as zowe.imperative.Logger); + profilemetadata.push(profilemetadata[0]); + profCache.addToConfigArray(profilemetadata); + expect(profCache.profileTypeConfigurations).toEqual(profilemetadata.filter((a, index) => index == 0)); + }); + + it("getConfigArray should return the data of profileTypeConfigurations Array", () => { + const profCache = new ProfilesCache(fakeLogger as unknown as zowe.imperative.Logger); + profCache.profileTypeConfigurations = profilemetadata; + const res = profCache.getConfigArray(); + expect(res).toEqual(profilemetadata); + }); + it("loadNamedProfile should find profiles by name and type", () => { const profCache = new ProfilesCache(fakeLogger as unknown as zowe.imperative.Logger); profCache.allProfiles = [lpar1Profile as zowe.imperative.IProfileLoaded, zftpProfile as zowe.imperative.IProfileLoaded]; 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 96d52b616a..d6fd650f36 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 @@ -279,6 +279,30 @@ describe("ZosmfUssApi", () => { expect(logoutSpy).toHaveBeenCalledWith(fakeSession); }); + it("should retrieve the tag of a file", async () => { + const zosmfApi = new ZosmfUssApi(); + jest.spyOn(JSON, "parse").mockReturnValue({ + stdout: ["-t UTF-8 tesfile.txt"], + }); + + Object.defineProperty(zowe.Utilities, "putUSSPayload", { + value: () => Buffer.from(""), + configurable: true, + }); + await expect(zosmfApi.getTag("testfile.txt")).resolves.toEqual("UTF-8"); + }); + + it("should update the tag attribute when passed in", async () => { + const zosmfApi = new ZosmfUssApi(); + const changeTagSpy = jest.fn(); + Object.defineProperty(zowe.Utilities, "putUSSPayload", { + value: changeTagSpy, + configurable: true, + }); + await expect(zosmfApi.updateAttributes("/test/path", { tag: "utf-8" })).resolves.not.toThrow(); + expect(changeTagSpy).toBeCalledTimes(1); + }); + const ussApis: ITestApi[] = [ { name: "isFileTagBinOrAscii", diff --git a/packages/zowe-explorer-api/package.json b/packages/zowe-explorer-api/package.json index d69e49ba40..a60d346bda 100644 --- a/packages/zowe-explorer-api/package.json +++ b/packages/zowe-explorer-api/package.json @@ -13,7 +13,8 @@ "lib" ], "devDependencies": { - "@types/semver": "^7.5.0" + "@types/semver": "^7.5.0", + "copyfiles": "^2.4.1" }, "dependencies": { "@types/vscode": "^1.53.2", @@ -23,7 +24,7 @@ "semver": "^7.5.3" }, "scripts": { - "build": "pnpm check-cli && pnpm clean && pnpm madge && tsc -p ./ && pnpm license", + "build": "pnpm check-cli && pnpm copy-secrets && pnpm clean && pnpm madge && tsc -p ./ && pnpm license", "test:unit": "jest \".*__tests__.*\\.unit\\.test\\.ts\" --coverage", "test": "pnpm test:unit", "lint": "concurrently -n \"_eslint_,prettier\" \"eslint .\" \"prettier --write . && prettier --check .\"", @@ -34,7 +35,8 @@ "clean": "rimraf lib", "fresh-clone": "pnpm clean && (rimraf node_modules || true)", "license": "node ../../scripts/updateLicenses.js", - "package": "pnpm build && pnpm pack && node ../../scripts/mv-pack.js zowe-zowe-explorer-api tgz" + "package": "pnpm build && pnpm pack && node ../../scripts/mv-pack.js zowe-zowe-explorer-api tgz", + "copy-secrets": "copyfiles -f ../../node_modules/@zowe/secrets-for-zowe-sdk/prebuilds/*.node ./prebuilds" }, "jest": { "moduleFileExtensions": [ diff --git a/packages/zowe-explorer-api/src/extend/interfaces.ts b/packages/zowe-explorer-api/src/extend/interfaces.ts index 5e158539dd..0e70fa25b9 100644 --- a/packages/zowe-explorer-api/src/extend/interfaces.ts +++ b/packages/zowe-explorer-api/src/extend/interfaces.ts @@ -172,6 +172,14 @@ export interface IUss extends ICommon { * @returns {Promise} */ rename(currentUssPath: string, newUssPath: string): Promise; + + /** + * Get the tag of a USS file + * + * @param {string} ussPath + * @returns {Promise} + */ + getTag?(ussPath: string): Promise; } /** diff --git a/packages/zowe-explorer-api/src/index.ts b/packages/zowe-explorer-api/src/index.ts index 182f051a03..fa233ad89e 100644 --- a/packages/zowe-explorer-api/src/index.ts +++ b/packages/zowe-explorer-api/src/index.ts @@ -17,7 +17,13 @@ export * from "./profiles/ProfilesCache"; export * from "./profiles/ZoweExplorerZosmfApi"; export * from "./security/KeytarApi"; export * from "./security/KeytarCredentialManager"; -export * from "./tree"; +export * from "./security/KeytarApi"; +export * from "./security/KeytarCredentialManager"; +export * from "./tree/ZoweExplorerTreeApi"; +export * from "./tree/ZoweTreeNode"; +export * from "./tree/IZoweTree"; +export * from "./tree/IZoweTreeNode"; +export * from "./tree/sorting"; export * from "./utils"; export * from "./vscode/ZoweVsCodeExtension"; export * from "./vscode/ui"; diff --git a/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts b/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts index 64dd2c54f6..30ef86b430 100644 --- a/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts +++ b/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts @@ -69,6 +69,7 @@ export class ProfilesCache { public profilesForValidation: IProfileValidation[] = []; public profilesValidationSetting: IValidationSetting[] = []; public allProfiles: zowe.imperative.IProfileLoaded[] = []; + public profileTypeConfigurations: zowe.imperative.ICommandProfileTypeConfiguration[] = []; protected allTypes: string[]; protected allExternalTypes = new Set(); protected profilesByType = new Map(); @@ -83,6 +84,21 @@ export class ProfilesCache { return require("@zowe/secrets-for-zowe-sdk").keyring; } + public addToConfigArray(extendermetadata: zowe.imperative.ICommandProfileTypeConfiguration[]): void { + extendermetadata?.forEach((item) => { + const index = this.profileTypeConfigurations.findIndex((ele) => ele.type == item.type); + if (index !== -1) { + this.profileTypeConfigurations[index] = item; + } else { + this.profileTypeConfigurations.push(item); + } + }); + } + + public getConfigArray(): zowe.imperative.ICommandProfileTypeConfiguration[] { + return this.profileTypeConfigurations; + } + public async getProfileInfo(_envTheia = false): Promise { const mProfileInfo = new zowe.imperative.ProfileInfo("zowe", { // eslint-disable-next-line @typescript-eslint/no-unsafe-return diff --git a/packages/zowe-explorer-api/src/profiles/ZoweExplorerZosmfApi.ts b/packages/zowe-explorer-api/src/profiles/ZoweExplorerZosmfApi.ts index a5444d5faf..c64e717f4b 100644 --- a/packages/zowe-explorer-api/src/profiles/ZoweExplorerZosmfApi.ts +++ b/packages/zowe-explorer-api/src/profiles/ZoweExplorerZosmfApi.ts @@ -127,6 +127,14 @@ export class ZosmfUssApi extends ZosmfApiCommon implements IUss { public async updateAttributes(ussPath: string, attributes: Partial): Promise { try { + if (attributes.tag) { + await zowe.Utilities.putUSSPayload(this.getSession(), ussPath, { + request: "chtag", + action: "set", + type: "text", + codeset: attributes.tag !== null ? attributes.tag.toString() : attributes.tag, + }); + } if ((attributes.group || attributes.gid) && (attributes.owner || attributes.uid)) { await zowe.Utilities.putUSSPayload(this.getSession(), ussPath, { request: "chown", @@ -141,7 +149,6 @@ export class ZosmfUssApi extends ZosmfApiCommon implements IUss { recursive: true, }); } - if (attributes.perms) { await zowe.Utilities.putUSSPayload(this.getSession(), ussPath, { request: "chmod", @@ -185,6 +192,14 @@ export class ZosmfUssApi extends ZosmfApiCommon implements IUss { apiResponse: result, }; } + + public async getTag(ussPath: string): Promise { + const response = await zowe.Utilities.putUSSPayload(this.getSession(), ussPath, { + request: "chtag", + action: "list", + }); + return JSON.parse(response.toString()).stdout[0].split(" ")[1] as string; + } } /** diff --git a/packages/zowe-explorer-api/src/tree/IZoweTreeNode.ts b/packages/zowe-explorer-api/src/tree/IZoweTreeNode.ts index d699e2189e..ff0b54606e 100644 --- a/packages/zowe-explorer-api/src/tree/IZoweTreeNode.ts +++ b/packages/zowe-explorer-api/src/tree/IZoweTreeNode.ts @@ -10,7 +10,12 @@ */ import * as vscode from "vscode"; -import { imperative } from "@zowe/cli"; +import { IJob, imperative } from "@zowe/cli"; +import { FileAttributes } from "../utils/files"; +import { DatasetFilter, NodeSort } from "./sorting"; +import { IZoweUSSTreeType } from "."; + +export type IZoweNodeType = IZoweDatasetTreeNode | IZoweUSSTreeNode | IZoweJobTreeNode; export enum NodeAction { Download = "download", @@ -74,6 +79,10 @@ export interface IZoweTreeNode { * whether the node was double-clicked */ wasDoubleClicked?: boolean; + /** + * Sorting method for this node's children + */ + sort?: NodeSort; /** * Retrieves the node label */ @@ -115,3 +124,231 @@ export interface IZoweTreeNode { */ setSessionToChoice(sessionObj: imperative.Session): void; } + +export type DatasetStats = { + user: string; + // built from "m4date", "mtime" and "msec" variables from z/OSMF API response + modifiedDate: Date; +}; + +/** + * Extended interface for Zowe Dataset tree nodes. + * + * @export + * @interface export interface IZoweDatasetTreeNode extends IZoweTreeNode { + */ +export interface IZoweDatasetTreeNode extends IZoweTreeNode { + /** + * Search criteria for a Dataset search + */ + pattern?: string; + /** + * Search criteria for a Dataset member search + */ + memberPattern?: string; + /** + * Additional statistics about this data set + */ + stats?: Partial; + /** + * Filter method for this data set's children + */ + filter?: DatasetFilter; + /** + * Retrieves child nodes of this IZoweDatasetTreeNode + * + * @returns {Promise} + */ + getChildren(): Promise; + /** + * Retrieves the etag value for the file + * + * @returns {string} + */ + getEtag?(): string; + /** + * Sets the etag value for the file + * + * @param {string} + */ + setEtag?(etag: string); +} + +/** + * Extended interface for Zowe USS tree nodes. + * + * @export + * @interface export interface IZoweUSSTreeNode extends IZoweTreeNode { + */ +export interface IZoweUSSTreeNode extends IZoweTreeNode { + /** + * Retrieves an abridged for of the label + */ + shortLabel?: string; + /** + * List of child nodes downloaded in binary format + */ + binaryFiles?: Record; + /** + * Binary indicator. Default false (text) + */ + binary?: boolean; + /** + * Specific profile name in use with this node + */ + mProfileName?: string; + + /** + * File attributes + */ + attributes?: FileAttributes; + /** + * Event that fires whenever an existing node is updated. + */ + onUpdateEmitter?: vscode.EventEmitter; + /** + * Event that fires whenever an existing node is updated. + */ + onUpdate?: vscode.Event; + /** + * Retrieves child nodes of this IZoweUSSTreeNode + * + * @returns {Promise} + */ + getChildren(): Promise; + /** + * Retrieves the etag value for the file + * + * @returns {string} + */ + getEtag?(): string; + /** + * Sets the etag value for the file + * + * @param {string} + */ + setEtag?(etag: string); + /** + * Renaming a USS Node. This could be a Favorite Node + * + * @param {string} newNamePath + */ + rename?(newNamePath: string); + /** + * Specifies the field as binary + * @param binary true is a binary file otherwise false + */ + setBinary?(binary: boolean); + // /** + // * Opens the text document + // * @return vscode.TextDocument + // */ + // getOpenedDocumentInstance?(): vscode.TextDocument; + /** + * Downloads and displays a file in a text editor view + * + * @param download Download the file default false + * @param preview the file, true or false + * @param ussFileProvider the tree provider + */ + openUSS?(download: boolean, previewFile: boolean, ussFileProvider: IZoweUSSTreeType); + /** + * Returns the local file path for the ZoweUSSNode + * + */ + getUSSDocumentFilePath?(): string; + /** + * Refreshes the node with current mainframe data + * + */ + refreshUSS?(); + /** + * + * @param ussFileProvider Deletes the USS tree node + * @param filePath + * @param cancelled optional + */ + deleteUSSNode?(ussFileProvider: IZoweUSSTreeType, filePath: string, cancelled?: boolean); + /** + * Process for renaming a USS Node. This could be a Favorite Node + * + * @param {USSTree} ussFileProvider + * @param {string} filePath + */ + renameUSSNode?(ussFileProvider: IZoweUSSTreeType, filePath: string); + /** + * Refreshes node and reopens it. + * @param hasClosedInstance + * @deprecated Use reopen instead. Will be removed by version 2.0. + */ + refreshAndReopen?(hasClosedInstance?: boolean); + /** + * Reopens a file if it was closed (e.g. while it was being renamed). + * @param hasClosedInstance + */ + reopen?(hasClosedInstance?: boolean); + /** + * Adds a search node to the USS favorites list + * + * @param {USSTree} ussFileProvider + */ + saveSearch?(ussFileProvider: IZoweUSSTreeType); + /** + * uploads selected uss node(s) to from clipboard to mainframe + * @deprecated in favor of `pasteUssTree` + */ + copyUssFile?(); + + /** + * Uploads a tree of USS file(s)/folder(s) to mainframe + */ + pasteUssTree?(); +} + +/** + * Extended interface for Zowe Job tree nodes. + * + * @export + * @interface export interface IZoweJobTreeNode extends IZoweTreeNode { + */ +export interface IZoweJobTreeNode extends IZoweTreeNode { + /** + * Use Job-specific tree node for children. + */ + children?: IZoweJobTreeNode[]; + /** + * Standard job response document + * Represents the attributes and status of a z/OS batch job + * @interface IJob + */ + job?: IJob; + /** + * Search criteria for a Job search + */ + searchId?: string; + /** + * Job Prefix i.e "MYJOB" + * Attribute of Job query + */ + prefix?: string; + /** + * Job Owner i.e "MYID" + * Attribute of Job query + */ + owner?: string; + /** + * Job Status i.e "ACTIVE" + * Attribute of Job query + */ + status?: string; + /** + * Returns whether the job node is a filtered search + */ + filtered?: boolean; + /** + * Retrieves child nodes of this IZoweJobTreeNode + * + * @returns {Promise} + */ + getChildren(): Promise; +} diff --git a/packages/zowe-explorer-api/src/tree/index.ts b/packages/zowe-explorer-api/src/tree/index.ts index 1e2529d462..098aec95ad 100644 --- a/packages/zowe-explorer-api/src/tree/index.ts +++ b/packages/zowe-explorer-api/src/tree/index.ts @@ -12,13 +12,13 @@ import { IZoweUSSTreeNode } from "./IZoweUSSTreeNode"; import { IZoweDatasetTreeNode } from "./IZoweDatasetTreeNode"; import { IZoweJobTreeNode } from "./IZoweJobTreeNode"; +import { IZoweTree } from "./IZoweTree"; export type IZoweNodeType = IZoweDatasetTreeNode | IZoweUSSTreeNode | IZoweJobTreeNode; +export type IZoweUSSTreeType = IZoweTree; +export * from "./sorting"; export * from "./ZoweExplorerTreeApi"; export * from "./ZoweTreeNode"; export * from "./IZoweTree"; export * from "./IZoweTreeNode"; -export * from "./IZoweDatasetTreeNode"; -export * from "./IZoweJobTreeNode"; -export * from "./IZoweUSSTreeNode"; diff --git a/packages/zowe-explorer-api/src/tree/sorting.ts b/packages/zowe-explorer-api/src/tree/sorting.ts new file mode 100644 index 0000000000..cefcf68eb0 --- /dev/null +++ b/packages/zowe-explorer-api/src/tree/sorting.ts @@ -0,0 +1,43 @@ +/** + * 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. + * + */ + +export enum DatasetSortOpts { + Name, + LastModified, + UserId, +} + +export enum SortDirection { + Ascending, + Descending, +} + +export enum DatasetFilterOpts { + LastModified, + UserId, +} + +export type DatasetFilter = { + method: DatasetFilterOpts; + value: string; +}; + +export type NodeSort = { + method: DatasetSortOpts | JobSortOpts; + direction: SortDirection; +}; + +export enum JobSortOpts { + Id, + DateSubmitted, + Name, + ReturnCode, +} diff --git a/packages/zowe-explorer-api/src/utils/files.ts b/packages/zowe-explorer-api/src/utils/files.ts index 76b1026ff2..cb95337463 100644 --- a/packages/zowe-explorer-api/src/utils/files.ts +++ b/packages/zowe-explorer-api/src/utils/files.ts @@ -21,6 +21,7 @@ export type FileAttributes = { owner: string; uid: number; perms: string; + tag?: string; }; export function permStringToOctal(perms: string): number { diff --git a/packages/zowe-explorer-api/src/vscode/ui/WebView.ts b/packages/zowe-explorer-api/src/vscode/ui/WebView.ts index 1f88b2c994..a0f18ba622 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/WebView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/WebView.ts @@ -36,14 +36,20 @@ export class WebView { /** * Constructs a webview for use with bundled assets. - * The webview entrypoint must be located at /dist/assets/index.js. + * The webview entrypoint must be located at src//dist//index.js. * * @param title The title for the new webview - * @param dirName The directory name (relative to the "webviews" folder in the extension root) with a valid entrypoint (see above). + * @param webviewName The webview name, the same name given to the directory of your webview in the webviews/src directory. * @param context The VSCode extension context * @param onDidReceiveMessage Event callback: called when messages are received from the webview */ - public constructor(title: string, dirName: string, context: ExtensionContext, onDidReceiveMessage?: (message: object) => void | Promise) { + public constructor( + title: string, + webviewName: string, + context: ExtensionContext, + onDidReceiveMessage?: (message: object) => void | Promise, + retainContext?: boolean + ) { this.disposables = []; // Generate random nonce for loading the bundled script @@ -52,13 +58,14 @@ export class WebView { // Build URIs for the webview directory and get the paths as VScode resources this.uris.disk = { - build: Uri.file(joinPath(context.extensionPath, "webviews", dirName)), - script: Uri.file(joinPath(context.extensionPath, "webviews", dirName, "dist", "assets", "index.js")), + build: Uri.file(joinPath(context.extensionPath, "src", "webviews")), + script: Uri.file(joinPath(context.extensionPath, "src", "webviews", "dist", webviewName, `${webviewName}.js`)), }; this.panel = window.createWebviewPanel("ZEAPIWebview", this.title, ViewColumn.Beside, { enableScripts: true, localResourceRoots: [this.uris.disk.build], + retainContextWhenHidden: retainContext ?? false, }); // Associate URI resources with webview diff --git a/packages/zowe-explorer-ftp-extension/CHANGELOG.md b/packages/zowe-explorer-ftp-extension/CHANGELOG.md index 2c1fe0c876..fca1b65158 100644 --- a/packages/zowe-explorer-ftp-extension/CHANGELOG.md +++ b/packages/zowe-explorer-ftp-extension/CHANGELOG.md @@ -17,6 +17,24 @@ All notable changes to the "zowe-explorer-ftp-extension" extension will be docum - Added `madge` script in `package.json` to track circular dependencies. [#2148](https://github.com/zowe/vscode-extension-for-zowe/issues/2148) - Migrated to new package manager PNPM from Yarn. +- Fixed ECONNRESET error when trying to upload or create an empty data set member. [#2350](https://github.com/zowe/vscode-extension-for-zowe/issues/2350) + +## `2.12.0` + +### Bug fixes + +- Fixed ECONNRESET error when trying to upload or create an empty data set member. [#2350](https://github.com/zowe/vscode-extension-for-zowe/issues/2350) +- Fixed issue where temporary files for e-tag comparison were not deleted after use. +- Fixed issue where another connection attempt was made inside `putContents` (in `getContentsTag`) even though a connection was already active. + +## `2.11.2` + +### Bug fixes + +- Update Zowe Explorer API dependency to pick up latest fixes for Zowe Secrets. [#2512](https://github.com/zowe/vscode-extension-for-zowe/issues/2512) + +## `2.11.1` + ## `2.11.0` ### Bug fixes @@ -36,10 +54,6 @@ All notable changes to the "zowe-explorer-ftp-extension" extension will be docum ## `2.9.2` -### New features and enhancements - -### Bug fixes - ## `2.9.1` ### Bug fixes diff --git a/packages/zowe-explorer-ftp-extension/__tests__/__unit__/Mvs/ZoweExplorerFtpMvsApi.unit.test.ts b/packages/zowe-explorer-ftp-extension/__tests__/__unit__/Mvs/ZoweExplorerFtpMvsApi.unit.test.ts index 966c27bbaa..773ea9e896 100644 --- a/packages/zowe-explorer-ftp-extension/__tests__/__unit__/Mvs/ZoweExplorerFtpMvsApi.unit.test.ts +++ b/packages/zowe-explorer-ftp-extension/__tests__/__unit__/Mvs/ZoweExplorerFtpMvsApi.unit.test.ts @@ -36,7 +36,7 @@ const MvsApi = new FtpMvsApi(); describe("FtpMvsApi", () => { beforeEach(() => { - MvsApi.checkedProfile = jest.fn().mockReturnValue({ message: "success", type: "zftp", failNotFound: false }); + MvsApi.checkedProfile = jest.fn().mockReturnValue({ message: "success", type: "zftp", profile: { secureFtp: false }, failNotFound: false }); MvsApi.ftpClient = jest.fn().mockReturnValue({ host: "", user: "", password: "", port: "" }); MvsApi.releaseConnection = jest.fn(); globals.SESSION_MAP.get = jest.fn().mockReturnValue({ mvsListConnection: { connected: true } }); @@ -100,6 +100,8 @@ describe("FtpMvsApi", () => { it("should upload content to dataset.", async () => { const localFile = tmp.tmpNameSync({ tmpdir: "/tmp" }); + const tmpNameSyncSpy = jest.spyOn(tmp, "tmpNameSync"); + const rmSyncSpy = jest.spyOn(fs, "rmSync"); fs.writeFileSync(localFile, "hello"); const response = TestUtils.getSingleLineStream(); @@ -113,7 +115,7 @@ describe("FtpMvsApi", () => { dataSetName: " (IBMUSER).DS2", options: { encoding: "", returnEtag: true, etag: "utf8" }, }; - jest.spyOn(MvsApi as any, "getContentsTag").mockReturnValue(undefined); + jest.spyOn(MvsApi as any, "getContents").mockResolvedValueOnce({ apiResponse: { etag: "utf8" } }); jest.spyOn(fs, "readFileSync").mockReturnValue("test"); jest.spyOn(Gui, "warningMessage").mockImplementation(); const result = await MvsApi.putContents(mockParams.inputFilePath, mockParams.dataSetName, mockParams.options); @@ -121,6 +123,83 @@ describe("FtpMvsApi", () => { expect(DataSetUtils.listDataSets).toBeCalledTimes(1); expect(DataSetUtils.uploadDataSet).toBeCalledTimes(1); expect(MvsApi.releaseConnection).toBeCalled(); + // check that correct function is called from node-tmp + expect(tmpNameSyncSpy).toHaveBeenCalled(); + expect(rmSyncSpy).toHaveBeenCalled(); + }); + + it("should upload single space to dataset when secureFtp is true and contents are empty", async () => { + const localFile = tmp.tmpNameSync({ tmpdir: "/tmp" }); + + fs.writeFileSync(localFile, ""); + const response = TestUtils.getSingleLineStream(); + DataSetUtils.listDataSets = jest.fn().mockReturnValue([{ dsname: "USER.EMPTYDS", dsorg: "PS", lrecl: 2 }]); + const uploadDataSetMock = jest.fn().mockReturnValue(response); + DataSetUtils.uploadDataSet = uploadDataSetMock; + jest.spyOn(MvsApi, "getContents").mockResolvedValue({ apiResponse: { etag: "123" } } as any); + + const mockParams = { + inputFilePath: localFile, + dataSetName: "USER.EMPTYDS", + options: { encoding: "", returnEtag: true, etag: "utf8" }, + }; + jest.spyOn(MvsApi, "checkedProfile").mockReturnValueOnce({ + type: "zftp", + message: "", + profile: { + secureFtp: true, + }, + failNotFound: false, + }); + + jest.spyOn(MvsApi as any, "getContentsTag").mockReturnValue(undefined); + jest.spyOn(fs, "readFileSync").mockReturnValue(""); + await MvsApi.putContents(mockParams.inputFilePath, mockParams.dataSetName, mockParams.options); + expect(DataSetUtils.uploadDataSet).toHaveBeenCalledWith({ host: "", password: "", port: "", user: "" }, "USER.EMPTYDS", { + content: " ", + encoding: "", + transferType: "ascii", + }); + // ensure options object at runtime does not have localFile + expect(Object.keys(uploadDataSetMock.mock.calls[0][2]).find((k) => k === "localFile")).toBe(undefined); + expect(MvsApi.releaseConnection).toBeCalled(); + }); + + it("should upload single space to dataset when secureFtp is true and contents are empty", async () => { + const localFile = tmp.tmpNameSync({ tmpdir: "/tmp" }); + + fs.writeFileSync(localFile, ""); + const response = TestUtils.getSingleLineStream(); + DataSetUtils.listDataSets = jest.fn().mockReturnValue([{ dsname: "USER.EMPTYDS", dsorg: "PS", lrecl: 2 }]); + const uploadDataSetMock = jest.fn().mockReturnValue(response); + DataSetUtils.uploadDataSet = uploadDataSetMock; + jest.spyOn(MvsApi, "getContents").mockResolvedValue({ apiResponse: { etag: "123" } } as any); + + const mockParams = { + inputFilePath: localFile, + dataSetName: "USER.EMPTYDS", + options: { encoding: "", returnEtag: true, etag: "utf8" }, + }; + jest.spyOn(MvsApi, "checkedProfile").mockReturnValueOnce({ + type: "zftp", + message: "", + profile: { + secureFtp: true, + }, + failNotFound: false, + }); + + jest.spyOn(MvsApi as any, "getContentsTag").mockReturnValue(undefined); + jest.spyOn(fs, "readFileSync").mockReturnValue(""); + await MvsApi.putContents(mockParams.inputFilePath, mockParams.dataSetName, mockParams.options); + expect(DataSetUtils.uploadDataSet).toHaveBeenCalledWith({ host: "", password: "", port: "", user: "" }, "USER.EMPTYDS", { + content: " ", + encoding: "", + transferType: "ascii", + }); + // ensure options object at runtime does not have localFile + expect(Object.keys(uploadDataSetMock.mock.calls[0][2]).find((k) => k === "localFile")).toBe(undefined); + expect(MvsApi.releaseConnection).toBeCalled(); }); it("should create dataset.", async () => { diff --git a/packages/zowe-explorer-ftp-extension/__tests__/__unit__/Uss/ZoweExplorerFtpUssApi.unit.test.ts b/packages/zowe-explorer-ftp-extension/__tests__/__unit__/Uss/ZoweExplorerFtpUssApi.unit.test.ts index 02538ce5e3..94e2babe54 100644 --- a/packages/zowe-explorer-ftp-extension/__tests__/__unit__/Uss/ZoweExplorerFtpUssApi.unit.test.ts +++ b/packages/zowe-explorer-ftp-extension/__tests__/__unit__/Uss/ZoweExplorerFtpUssApi.unit.test.ts @@ -96,6 +96,8 @@ describe("FtpUssApi", () => { const localFile = tmp.tmpNameSync({ tmpdir: "/tmp" }); const response = TestUtils.getSingleLineStream(); UssUtils.uploadFile = jest.fn().mockReturnValue(response); + const tmpNameSyncSpy = jest.spyOn(tmp, "tmpNameSync"); + const rmSyncSpy = jest.spyOn(fs, "rmSync"); jest.spyOn(UssApi, "getContents").mockResolvedValue({ apiResponse: { etag: "test" } } as any); const mockParams = { inputFilePath: localFile, @@ -115,6 +117,9 @@ describe("FtpUssApi", () => { expect(UssUtils.downloadFile).toBeCalledTimes(1); expect(UssUtils.uploadFile).toBeCalledTimes(1); expect(UssApi.releaseConnection).toBeCalled(); + // check that correct function is called from node-tmp + expect(tmpNameSyncSpy).toHaveBeenCalled(); + expect(rmSyncSpy).toHaveBeenCalled(); }); it("should upload uss directory.", async () => { diff --git a/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpMvsApi.ts b/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpMvsApi.ts index 4f12c9e8b1..63d0703d14 100644 --- a/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpMvsApi.ts +++ b/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpMvsApi.ts @@ -112,11 +112,6 @@ export class FtpMvsApi extends AbstractFtpApi implements IMvs { } public async putContents(inputFilePath: string, dataSetName: string, options: IUploadOptions): Promise { - const transferOptions = { - transferType: options.binary ? TRANSFER_TYPE_BINARY : TRANSFER_TYPE_ASCII, - localFile: inputFilePath, - encoding: options.encoding, - }; const file = path.basename(inputFilePath).replace(/[^a-z0-9]+/gi, ""); const member = file.substr(0, MAX_MEMBER_NAME_LEN); let targetDataset: string; @@ -135,24 +130,37 @@ export class FtpMvsApi extends AbstractFtpApi implements IMvs { targetDataset = dataSetName + "(" + member + ")"; } const result = this.getDefaultResponse(); + const profile = this.checkedProfile(); + + // Save-Save with FTP requires loading the file first + // (moved this block above connection request so only one connection is active at a time) + if (options.returnEtag && options.etag) { + const contentsTag = await this.getContentsTag(dataSetName); + if (contentsTag && contentsTag !== options.etag) { + result.success = false; + result.commandResponse = "Rest API failure with HTTP(S) status 412 Save conflict."; + return result; + } + } let connection; try { - connection = await this.ftpClient(this.checkedProfile()); + connection = await this.ftpClient(profile); if (!connection) { globals.LOGGER.logImperativeMessage(result.commandResponse, MessageSeverity.ERROR); throw new Error(result.commandResponse); } - // Save-Save with FTP requires loading the file first - if (options.returnEtag && options.etag) { - const contentsTag = await this.getContentsTag(dataSetName); - if (contentsTag && contentsTag !== options.etag) { - result.success = false; - result.commandResponse = "Rest API failure with HTTP(S) status 412 Save conflict."; - return result; - } - } const lrecl: number = dsAtrribute.apiResponse.items[0].lrecl; const data = fs.readFileSync(inputFilePath, { encoding: "utf8" }); + const transferOptions: Record = { + transferType: options.binary ? TRANSFER_TYPE_BINARY : TRANSFER_TYPE_ASCII, + localFile: inputFilePath, + encoding: options.encoding, + }; + if (profile.profile.secureFtp && data === "") { + // substitute single space for empty DS contents when saving (avoids FTPS error) + transferOptions.content = " "; + delete transferOptions.localFile; + } const lines = data.split(/\r?\n/); const foundIndex = lines.findIndex((line) => line.length > lrecl); if (foundIndex !== -1) { @@ -172,6 +180,9 @@ export class FtpMvsApi extends AbstractFtpApi implements IMvs { await DataSetUtils.uploadDataSet(connection, targetDataset, transferOptions); result.success = true; if (options.returnEtag) { + // release this connection instance because a new one will be made with getContentsTag + this.releaseConnection(connection); + connection = null; const contentsTag = await this.getContentsTag(dataSetName); result.apiResponse = [ { @@ -242,15 +253,17 @@ export class FtpMvsApi extends AbstractFtpApi implements IMvs { } public async createDataSetMember(dataSetName: string, options?: IUploadOptions): Promise { + const profile = this.checkedProfile(); const transferOptions = { - transferType: options ? TRANSFER_TYPE_BINARY : TRANSFER_TYPE_ASCII, - content: "", + transferType: options.binary ? TRANSFER_TYPE_BINARY : TRANSFER_TYPE_ASCII, + // we have to provide a single space for content over FTPS, or it will fail to upload + content: profile.profile.secureFtp ? " " : "", encoding: options.encoding, }; const result = this.getDefaultResponse(); let connection; try { - connection = await this.ftpClient(this.checkedProfile()); + connection = await this.ftpClient(profile); if (!connection) { throw new Error(result.commandResponse); } @@ -359,6 +372,7 @@ export class FtpMvsApi extends AbstractFtpApi implements IMvs { }; const loadResult = await this.getContents(dataSetName, options); const etag: string = loadResult.apiResponse.etag; + fs.rmSync(tmpFileName, { force: true }); return etag; } private getDefaultResponse(): zowe.IZosFilesResponse { diff --git a/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpUssApi.ts b/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpUssApi.ts index e9cd731760..3804036969 100644 --- a/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpUssApi.ts +++ b/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpUssApi.ts @@ -126,23 +126,27 @@ export class FtpUssApi extends AbstractFtpApi implements IUss { localFile: inputFilePath, }; const result = this.getDefaultResponse(); + // Save-Save with FTP requires loading the file first + // (moved this block above connection request so only one connection is active at a time) + if (returnEtag && etag) { + const contentsTag = await this.getContentsTag(ussFilePath); + if (contentsTag && contentsTag !== etag) { + throw new Error("Rest API failure with HTTP(S) status 412 Save conflict."); + } + } let connection; try { connection = await this.ftpClient(this.checkedProfile()); if (!connection) { throw new Error(result.commandResponse); } - // Save-Save with FTP requires loading the file first - if (returnEtag && etag) { - const contentsTag = await this.getContentsTag(ussFilePath); - if (contentsTag && contentsTag !== etag) { - throw new Error("Rest API failure with HTTP(S) status 412 Save conflict."); - } - } await UssUtils.uploadFile(connection, ussFilePath, transferOptions); result.success = true; if (returnEtag) { + // release this connection instance because a new one will be made with getContentsTag + this.releaseConnection(connection); + connection = null; const contentsTag = await this.getContentsTag(ussFilePath); result.apiResponse.etag = contentsTag; } @@ -274,6 +278,7 @@ export class FtpUssApi extends AbstractFtpApi implements IUss { }; const loadResult = await this.getContents(ussFilePath, options); const etag: string = loadResult.apiResponse.etag; + fs.rmSync(tmpFileName, { force: true }); return etag; } diff --git a/packages/zowe-explorer/.prettierignore b/packages/zowe-explorer/.prettierignore index 524eab72be..7a0ab09158 100644 --- a/packages/zowe-explorer/.prettierignore +++ b/packages/zowe-explorer/.prettierignore @@ -1,4 +1,4 @@ out results scripts -webviews/**/dist \ No newline at end of file +src/webviews/dist diff --git a/packages/zowe-explorer/.vscodeignore b/packages/zowe-explorer/.vscodeignore index 931447bc41..0a539b22c5 100644 --- a/packages/zowe-explorer/.vscodeignore +++ b/packages/zowe-explorer/.vscodeignore @@ -8,7 +8,7 @@ !resources/**/*.png !resources/**/*.svg !prebuilds/** -!webviews/*/dist/ +!src/webviews/dist/** !CHANGELOG.md !LICENSE diff --git a/packages/zowe-explorer/CHANGELOG.md b/packages/zowe-explorer/CHANGELOG.md index 6a190c3868..ab84254092 100644 --- a/packages/zowe-explorer/CHANGELOG.md +++ b/packages/zowe-explorer/CHANGELOG.md @@ -22,6 +22,45 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen - Added `madge` script in `package.json` to track circular dependencies. [#2148](https://github.com/zowe/vscode-extension-for-zowe/issues/2148) - Migrated to new package manager PNPM from Yarn. +### Bug fixes + +## `2.12.0` + +### New features and enhancements + +- Added "Sort Jobs" feature in Jobs tree view: accessible via sort icon or right-clicking on session node. [#2257](https://github.com/zowe/vscode-extension-for-zowe/issues/2257) +- Introduce a new user interface for managing profiles via right-click action "Manage Profile". +- Added new edit feature on `Edit Attributes` view for changing file tags on USS. [#2113](https://github.com/zowe/vscode-extension-for-zowe/issues/2113) +- Added new API {ZE Extender MetaData} to allow extenders to have the metadata of registered extenders to aid in team configuration file creation from a view that isn't Zowe Explorer's. [#2394](https://github.com/zowe/vscode-extension-for-zowe/issues/2394) +- Added ability to install extension from VS Code marketplace if custom credential manager extension is missing after defining it on `imperative.json`. [#2381](https://github.com/zowe/vscode-extension-for-zowe/issues/2381) +- Added new right-click action for `Submit as JCL` for local files in the VS Code file explorer as well as files opened in the VS Code text editor. [#2475](https://github.com/zowe/vscode-extension-for-zowe/issues/2475) +- Added "Sort PDS members" feature in Data Sets tree view: accessible via sort icon on session node, or by right-clicking a PDS or session. [#2420](https://github.com/zowe/vscode-extension-for-zowe/issues/2420) +- Added "Filter PDS members" feature in Data Sets tree view: accessible via filter icon on session node, or by right-clicking a PDS or session. [#2420](https://github.com/zowe/vscode-extension-for-zowe/issues/2420) +- Added descriptions to data set nodes if filtering and/or sorting is enabled (where applicable). +- Added webview for editing persistent items on Zowe Explorer. [#2488](https://github.com/zowe/vscode-extension-for-zowe/issues/2488) + +### Bug fixes + +- Fixed submitting local JCL using command pallet option `Zowe Explorer: Submit as JCL` by adding a check for chosen profile returned to continue the action. [#1625](https://github.com/zowe/vscode-extension-for-zowe/issues/1625) +- Fixed conflict resolution being skipped if local and remote file have different contents but are the same size. [#2496](https://github.com/zowe/vscode-extension-for-zowe/issues/2496) +- Fixed issue with token based auth for unsecure profiles in Zowe Explorer. [#2518](https://github.com/zowe/vscode-extension-for-zowe/issues/2518) + +## `2.11.2` + +### Bug fixes + +- Update Zowe Explorer API dependency to pick up latest fixes for Zowe Secrets. [#2512](https://github.com/zowe/vscode-extension-for-zowe/issues/2512) + +## `2.11.1` + +### Bug fixes + +- Fixed issue where USS nodes were not removed from tree during deletion. [#2479](https://github.com/zowe/vscode-extension-for-zowe/issues/2479) +- Fixed issue where new USS nodes from a paste operation were not shown in tree until refreshed. [#2479](https://github.com/zowe/vscode-extension-for-zowe/issues/2479) +- Fixed issue where the "Delete Job" action showed a successful deletion message, even if the API returned an error. +- USS directories, PDS nodes, job nodes and session nodes now update with their respective "collapsed icon" when collapsed. +- Fixed bug where the list of datasets from a filter search was not re-sorted after a new data set was created in Zowe Explorer. [#2473](https://github.com/zowe/vscode-extension-for-zowe/issues/2473) + ## `2.11.0` ### New features and enhancements diff --git a/packages/zowe-explorer/__mocks__/mockCreators/datasets.ts b/packages/zowe-explorer/__mocks__/mockCreators/datasets.ts index 18a2a5cda1..41ee8758f2 100644 --- a/packages/zowe-explorer/__mocks__/mockCreators/datasets.ts +++ b/packages/zowe-explorer/__mocks__/mockCreators/datasets.ts @@ -43,6 +43,11 @@ export function createDatasetTree(sessionNode: ZoweDatasetNode, treeView: any, f addFavorite: jest.fn(), getSearchHistory: jest.fn(), getFileHistory: jest.fn(), + getSessions: jest.fn(), + getFavorites: jest.fn(), + removeSearchHistory: jest.fn(), + resetSearchHistory: jest.fn(), + resetFileHistory: jest.fn(), refresh: jest.fn(), refreshElement: jest.fn(), checkCurrentProfile: jest.fn(), @@ -78,6 +83,7 @@ export function createDatasetTree(sessionNode: ZoweDatasetNode, treeView: any, f testDatasetTree.removeFileHistory.mockImplementation((badFile) => testDatasetTree.mFileHistory.splice(testDatasetTree.mFileHistory.indexOf(badFile), 1) ); + testDatasetTree.getSearchHistory.mockImplementation(); testDatasetTree.getFileHistory.mockImplementation(() => testDatasetTree.mFileHistory); testDatasetTree.deleteSession.mockImplementation((badSession) => removeNodeFromArray(badSession, testDatasetTree.mSessionNodes)); testDatasetTree.removeFavorite.mockImplementation((badFavorite) => removeNodeFromArray(badFavorite, testDatasetTree.mFavorites)); diff --git a/packages/zowe-explorer/__mocks__/mockCreators/jobs.ts b/packages/zowe-explorer/__mocks__/mockCreators/jobs.ts index a3f1503b22..d223f5951f 100644 --- a/packages/zowe-explorer/__mocks__/mockCreators/jobs.ts +++ b/packages/zowe-explorer/__mocks__/mockCreators/jobs.ts @@ -86,6 +86,12 @@ export function createJobsTree(session: imperative.Session, iJob: IJob, profile: dispose: jest.fn(), }; }), + getSessions: jest.fn(), + getFavorites: jest.fn(), + getSearchHistory: jest.fn(), + removeSearchHistory: jest.fn(), + resetSearchHistory: jest.fn(), + resetFileHistory: jest.fn(), deleteSession: jest.fn(), addFavorite: jest.fn(), removeFavorite: jest.fn(), @@ -105,6 +111,7 @@ export function createJobsTree(session: imperative.Session, iJob: IJob, profile: testJobsTree.addFavorite.mockImplementation((newFavorite) => { testJobsTree.mFavorites.push(newFavorite); }); + testJobsTree.getSearchHistory.mockImplementation(); testJobsTree.deleteSession.mockImplementation((badSession) => removeNodeFromArray(badSession, testJobsTree.mSessionNodes)); testJobsTree.removeFavorite.mockImplementation((badFavorite) => removeNodeFromArray(badFavorite, testJobsTree.mFavorites)); testJobsTree.removeFavProfile.mockImplementation((badFavProfileName) => { diff --git a/packages/zowe-explorer/__mocks__/mockCreators/shared.ts b/packages/zowe-explorer/__mocks__/mockCreators/shared.ts index 6c226c620f..e46e054cd3 100644 --- a/packages/zowe-explorer/__mocks__/mockCreators/shared.ts +++ b/packages/zowe-explorer/__mocks__/mockCreators/shared.ts @@ -218,6 +218,40 @@ export function createValidIProfile(): imperative.IProfileLoaded { }; } +export function createTokenAuthIProfile(): imperative.IProfileLoaded { + return { + name: "sestest", + profile: { + type: "zosmf", + host: "test", + port: 1443, + rejectUnauthorized: false, + tokenType: "apimlAuthenticationToken", + tokenValue: "stringofletters", + name: "testName", + }, + type: "zosmf", + message: "", + failNotFound: false, + }; +} + +export function createNoAuthIProfile(): imperative.IProfileLoaded { + return { + name: "sestest", + profile: { + type: "zosmf", + host: null, + port: 1443, + rejectUnauthorized: false, + name: "testName", + }, + type: "zosmf", + message: "", + failNotFound: false, + }; +} + export function createAltTypeIProfile(): imperative.IProfileLoaded { return { name: "altTypeProfile", @@ -259,12 +293,12 @@ export function createTextDocument(name: string, sessionNode?: ZoweDatasetNode | isDirty: null, isClosed: null, save: null, - eol: null, + eol: 1, lineCount: null, lineAt: null, offsetAt: null, - positionAt: null, - getText: jest.fn(), + positionAt: jest.fn(), + getText: jest.fn().mockReturnValue(""), getWordRangeAtPosition: null, validateRange: null, validatePosition: null, diff --git a/packages/zowe-explorer/__mocks__/mockCreators/uss.ts b/packages/zowe-explorer/__mocks__/mockCreators/uss.ts index 422f0da7eb..1d5c327c06 100644 --- a/packages/zowe-explorer/__mocks__/mockCreators/uss.ts +++ b/packages/zowe-explorer/__mocks__/mockCreators/uss.ts @@ -24,6 +24,12 @@ export function createUSSTree(favoriteNodes: ZoweUSSNode[], sessionNodes: ZoweUS newTree.mSessionNodes = [...sessionNodes]; newTree.mFavorites = favoriteNodes; newTree.addSession = jest.fn(); + newTree.getSessions = jest.fn(); + newTree.getFavorites = jest.fn(); + newTree.getSearchHistory = jest.fn(); + newTree.removeSearchHistory = jest.fn(); + newTree.resetSearchHistory = jest.fn(); + newTree.resetFileHistory = jest.fn(); newTree.refresh = jest.fn(); newTree.checkCurrentProfile = jest.fn(); newTree.refreshElement = jest.fn(); diff --git a/packages/zowe-explorer/__mocks__/vscode.ts b/packages/zowe-explorer/__mocks__/vscode.ts index b3c3af452d..75824e2b33 100644 --- a/packages/zowe-explorer/__mocks__/vscode.ts +++ b/packages/zowe-explorer/__mocks__/vscode.ts @@ -144,7 +144,12 @@ export namespace extensions { }; } } - +export interface TreeViewExpansionEvent { + /** + * Element that is expanded or collapsed. + */ + readonly element: T; +} export interface TreeView { /** * An optional human-readable message that will be rendered in the view. @@ -177,6 +182,8 @@ export interface TreeView { * **NOTE:** The {@link TreeDataProvider} that the `TreeView` {@link window.createTreeView is registered with} with must implement {@link TreeDataProvider.getParent getParent} method to access this API. */ reveal(element: T, options?: { select?: boolean; focus?: boolean; expand?: boolean | number }): Thenable; + + onDidCollapseElement: Event>; } export class FileDecoration { diff --git a/packages/zowe-explorer/__tests__/__theia__/mockFiles/settings.json b/packages/zowe-explorer/__tests__/__theia__/mockFiles/settings.json index 8c61d7946f..4a203675de 100644 --- a/packages/zowe-explorer/__tests__/__theia__/mockFiles/settings.json +++ b/packages/zowe-explorer/__tests__/__theia__/mockFiles/settings.json @@ -23,5 +23,6 @@ "fileHistory": [], "searchHistory": [] }, - "zowe.security.secureCredentialsEnabled": false + "zowe.security.secureCredentialsEnabled": false, + "zowe.security.checkForCustomCredentialManagers": false } diff --git a/packages/zowe-explorer/__tests__/__theia__/theia/Locators.ts b/packages/zowe-explorer/__tests__/__theia__/theia/Locators.ts index 089c6386bc..5e3eb996bc 100644 --- a/packages/zowe-explorer/__tests__/__theia__/theia/Locators.ts +++ b/packages/zowe-explorer/__tests__/__theia__/theia/Locators.ts @@ -28,7 +28,7 @@ export const DatasetsLocators = { favoriteProfileInDatasetXpath: "(//div[contains(@id,'Favorites') and contains(@id,'TestSeleniumProfile')])", addToFavoriteOptionXpath: "//li[@data-command='zowe.ds.saveSearch']", removeFavoriteProfileFromDatasetsOptionXpath: "//li[@data-command='zowe.ds.removeFavProfile']", - deleteProfileFromDatasetsXpath: "(//li[@data-command='zowe.ds.deleteProfile'])", + manageProfileFromDatasetsXpath: "(//li[@data-command='zowe.profileManagement'])", }; export const UssLocators = { @@ -44,6 +44,7 @@ export const UssLocators = { addToFavoriteOptionXpath: "//li[@data-command='zowe.uss.addFavorite']", removeFavoriteProfileFromUssOptionXpath: "//li[@data-command='zowe.uss.removeFavProfile']", hideProfileFromUssOptionXpath: "//li[@data-command='zowe.uss.removeSession']", + manageProfileFromUnixXpath: "(//li[@data-command='zowe.profileManagement'])", }; export const JobsLocators = { @@ -60,6 +61,7 @@ export const JobsLocators = { removeFavoriteProfileFromJobsOptionXpath: "//li[@data-command='zowe.jobs.removeFavProfile']", hideProfileFromJobsOptionXpath: "//li[@data-command='zowe.jobs.removeJobsSession']", secondJobsProfileBeforeHidingXpath: "(//div[contains(@id,'TestSeleniumProfile')])[2]", + manageProfileFromJobsXpath: "(//li[@data-command='zowe.profileManagement'])", }; export const TheiaNotificationMessages = { diff --git a/packages/zowe-explorer/__tests__/__theia__/theia/extension.theiaChrome.ts b/packages/zowe-explorer/__tests__/__theia__/theia/extension.theiaChrome.ts index d9bd97df36..239aadb46a 100644 --- a/packages/zowe-explorer/__tests__/__theia__/theia/extension.theiaChrome.ts +++ b/packages/zowe-explorer/__tests__/__theia__/theia/extension.theiaChrome.ts @@ -138,13 +138,21 @@ export async function addProfileToFavoritesInJobs() { export async function hideProfileInUss() { const hideProfileFromUss = await driverChrome.wait(until.elementLocated(By.xpath(UssLocators.secondUssProfileXpath)), WAITTIME); await driverChrome.actions().click(hideProfileFromUss, Button.RIGHT).perform(); - await driverChrome.wait(until.elementLocated(By.xpath(UssLocators.hideProfileFromUssOptionXpath)), WAITTIME).click(); + driverChrome.wait(until.elementLocated(By.xpath(UssLocators.manageProfileFromUnixXpath)), WAITTIME).click(); + await driverChrome.sleep(SHORTSLEEPTIME); + const manageProfile = driverChrome.wait(until.elementLocated(By.xpath(UssLocators.emptyInputBoxXpath)), WAITTIME); + manageProfile.sendKeys("Hide Profile"); + manageProfile.sendKeys(Key.ENTER); } export async function hideProfileInJobs() { const hideProfileFromJobs = await driverChrome.wait(until.elementLocated(By.xpath(JobsLocators.secondJobsProfileBeforeHidingXpath)), WAITTIME); await driverChrome.actions().click(hideProfileFromJobs, Button.RIGHT).perform(); - await driverChrome.wait(until.elementLocated(By.xpath(JobsLocators.hideProfileFromJobsOptionXpath)), WAITTIME).click(); + driverChrome.wait(until.elementLocated(By.xpath(JobsLocators.manageProfileFromJobsXpath)), WAITTIME).click(); + await driverChrome.sleep(SHORTSLEEPTIME); + const manageProfile = driverChrome.wait(until.elementLocated(By.xpath(JobsLocators.emptyInputBoxXpath)), WAITTIME); + manageProfile.sendKeys("Hide Profile"); + manageProfile.sendKeys(Key.ENTER); } export async function verifyProfileIsHideInUss() { @@ -170,23 +178,29 @@ export async function verifyProfileIsHideInJobs() { export async function deleteDefaultProfileInDatasets() { const profileName = await driverChrome.wait(until.elementLocated(By.xpath(DatasetsLocators.defaultDatasetsProfileXpath)), WAITTIME); await driverChrome.actions().click(profileName, Button.RIGHT).perform(); - await driverChrome.wait(until.elementLocated(By.xpath(DatasetsLocators.deleteProfileFromDatasetsXpath)), WAITTIME).click(); + await driverChrome.wait(until.elementLocated(By.xpath(DatasetsLocators.manageProfileFromDatasetsXpath)), WAITTIME).click(); + await driverChrome.sleep(SHORTSLEEPTIME); + const manageProfile = driverChrome.wait(until.elementLocated(By.xpath(DatasetsLocators.emptyInputBoxXpath)), WAITTIME); + manageProfile.sendKeys("Delete Profile"); + manageProfile.sendKeys(Key.ENTER); await driverChrome.sleep(SHORTSLEEPTIME); const deleteProfile = driverChrome.wait(until.elementLocated(By.xpath(DatasetsLocators.emptyInputBoxXpath)), WAITTIME); deleteProfile.sendKeys("Delete"); deleteProfile.sendKeys(Key.ENTER); - return; } export async function deleteProfileInDatasets() { const favprofile = await driverChrome.wait(until.elementLocated(By.xpath(DatasetsLocators.secondDatasetProfileXpath)), WAITTIME); await driverChrome.actions().click(favprofile, Button.RIGHT).perform(); - await driverChrome.wait(until.elementLocated(By.xpath(DatasetsLocators.deleteProfileFromDatasetsXpath)), WAITTIME).click(); + await driverChrome.wait(until.elementLocated(By.xpath(DatasetsLocators.manageProfileFromDatasetsXpath)), WAITTIME).click(); + await driverChrome.sleep(SHORTSLEEPTIME); + const manageProfile = driverChrome.wait(until.elementLocated(By.xpath(DatasetsLocators.emptyInputBoxXpath)), WAITTIME); + manageProfile.sendKeys("Delete Profile"); + manageProfile.sendKeys(Key.ENTER); await driverChrome.sleep(SHORTSLEEPTIME); const deleteProfile = driverChrome.wait(until.elementLocated(By.xpath(DatasetsLocators.emptyInputBoxXpath)), WAITTIME); deleteProfile.sendKeys("Delete"); deleteProfile.sendKeys(Key.ENTER); - return; } export async function verifyRemovedFavoriteProfileInDatasets() { diff --git a/packages/zowe-explorer/__tests__/__unit__/PersistentFilters.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/PersistentFilters.unit.test.ts index 9736ba9176..26ca3fcddf 100644 --- a/packages/zowe-explorer/__tests__/__unit__/PersistentFilters.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/PersistentFilters.unit.test.ts @@ -96,4 +96,12 @@ describe("PersistentFilters Unit Test", () => { expect(pf.getDsTemplates()).toEqual([mockTemplate]); }); }); + describe("removeSearchHistory", () => { + it("should remove the specified item from the persistent object", () => { + const pf: PersistentFilters = new PersistentFilters("test", 2, 2); + pf["mSearchHistory"] = ["test1", "test2"]; + pf.removeSearchHistory("test1"); + expect(pf.getSearchHistory().length).toEqual(1); + }); + }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/Profiles.extended.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/Profiles.extended.unit.test.ts index 502f97a688..f939c37f16 100644 --- a/packages/zowe-explorer/__tests__/__unit__/Profiles.extended.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/Profiles.extended.unit.test.ts @@ -39,6 +39,7 @@ import { SettingsConfig } from "../../src/utils/SettingsConfig"; import { ZoweLogger } from "../../src/utils/LoggerUtils"; import { ZoweLocalStorage } from "../../src/utils/ZoweLocalStorage"; jest.mock("../../src/utils/LoggerUtils"); +import { TreeProviders } from "../../src/shared/TreeProviders"; jest.mock("child_process"); jest.mock("fs"); @@ -112,7 +113,14 @@ async function createGlobalMocks() { configurable: true, }); Object.defineProperty(globals, "ISTHEIA", { get: () => false, configurable: true }); - Object.defineProperty(vscode.window, "createTreeView", { value: jest.fn(), configurable: true }); + Object.defineProperty(vscode.window, "createTreeView", { + value: jest.fn().mockReturnValue({ onDidCollapseElement: jest.fn() }), + configurable: true, + }); + Object.defineProperty(vscode.workspace, "getConfiguration", { + value: newMocks.mockGetConfiguration, + configurable: true, + }); Object.defineProperty(vscode, "ConfigurationTarget", { value: newMocks.mockConfigurationTarget, configurable: true, @@ -905,7 +913,7 @@ describe("Profiles Unit Tests - function checkCurrentProfile", () => { it("should throw an error if using token auth and is logged out or has expired token", async () => { const globalMocks = await createGlobalMocks(); jest.spyOn(utils, "errorHandling").mockImplementation(); - jest.spyOn(utils, "isUsingTokenAuth").mockResolvedValue(true); + jest.spyOn(utils.ProfilesUtils, "isUsingTokenAuth").mockResolvedValue(true); setupProfilesCheck(globalMocks); await expect(Profiles.getInstance().checkCurrentProfile(globalMocks.testProfile)).resolves.toEqual({ name: "sestest", status: "unverified" }); }); @@ -1021,6 +1029,7 @@ describe("Profiles Unit Tests - function ssoLogin", () => { ], configurable: true, }); + Object.defineProperty(utils.ProfilesUtils, "isProfileUsingBasicAuth", { value: jest.fn(), configurable: true }); jest.spyOn(Gui, "showMessage").mockImplementation(); }); it("should perform an SSOLogin successfully while fetching the base profile", async () => { @@ -1089,6 +1098,14 @@ describe("Profiles Unit Tests - function ssoLogout", () => { jest.spyOn(Gui, "showMessage").mockImplementation(); }); it("should logout successfully and refresh zowe explorer", async () => { + const mockTreeProvider = { + mSessionNodes: [testNode], + flipState: jest.fn(), + refreshElement: jest.fn(), + } as any; + jest.spyOn(TreeProviders, "ds", "get").mockReturnValue(mockTreeProvider); + jest.spyOn(TreeProviders, "uss", "get").mockReturnValue(mockTreeProvider); + jest.spyOn(TreeProviders, "job", "get").mockReturnValue(mockTreeProvider); const getTokenTypeNameMock = jest.fn(); const logoutMock = jest.fn(); jest.spyOn(ZoweExplorerApiRegister.getInstance(), "getCommonApi").mockImplementation(() => ({ @@ -1227,3 +1244,67 @@ describe("Profiles Unit Tests - function getSecurePropsForProfile", () => { await expect(Profiles.getInstance().getSecurePropsForProfile(globalMocks.testProfile.name ?? "")).resolves.toEqual(["tokenValue"]); }); }); + +describe("Profiles Unit Tests - function clearFilterFromAllTrees", () => { + afterEach(() => { + jest.clearAllMocks(); + jest.resetModules(); + jest.restoreAllMocks(); + }); + + it("should fail to clear filter if no session nodes are available", async () => { + const globalMocks = await createGlobalMocks(); + const testNode = new (ZoweTreeNode as any)( + "fake", + vscode.TreeItemCollapsibleState.None, + undefined, + globalMocks.testSession, + globalMocks.testProfile + ); + + const flipStateSpy = jest.fn(); + const refreshElementSpy = jest.fn(); + + const mockTreeProvider = { + mSessionNodes: [], + flipState: flipStateSpy, + refreshElement: refreshElementSpy, + } as any; + jest.spyOn(TreeProviders, "ds", "get").mockReturnValue(mockTreeProvider); + jest.spyOn(TreeProviders, "uss", "get").mockReturnValue(mockTreeProvider); + jest.spyOn(TreeProviders, "job", "get").mockReturnValue(mockTreeProvider); + + expect(Profiles.getInstance().clearFilterFromAllTrees(testNode)); + expect(flipStateSpy).toBeCalledTimes(0); + expect(refreshElementSpy).toBeCalledTimes(0); + }); + + it("should fail to clear filters if the session node is not listed in the tree", async () => { + const globalMocks = await createGlobalMocks(); + const testNode = new (ZoweTreeNode as any)( + "fake", + vscode.TreeItemCollapsibleState.None, + undefined, + globalMocks.testSession, + globalMocks.testProfile + ); + + const flipStateSpy = jest.fn(); + const refreshElementSpy = jest.fn(); + const getProfileSpy = jest.fn(() => ({ name: "test" })); + + const mockTreeProvider = { + mSessionNodes: [{ getProfile: getProfileSpy }], + flipState: flipStateSpy, + refreshElement: refreshElementSpy, + } as any; + jest.spyOn(TreeProviders, "ds", "get").mockReturnValue(mockTreeProvider); + jest.spyOn(TreeProviders, "uss", "get").mockReturnValue(mockTreeProvider); + jest.spyOn(TreeProviders, "job", "get").mockReturnValue(mockTreeProvider); + + expect(Profiles.getInstance().clearFilterFromAllTrees(testNode)); + expect(flipStateSpy).toBeCalledTimes(0); + expect(refreshElementSpy).toBeCalledTimes(0); + expect(getProfileSpy).toBeCalledTimes(3); + }); +}); diff --git a/packages/zowe-explorer/__tests__/__unit__/ZoweExplorerExtender.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/ZoweExplorerExtender.unit.test.ts index 69c7d29788..34491edf7e 100644 --- a/packages/zowe-explorer/__tests__/__unit__/ZoweExplorerExtender.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/ZoweExplorerExtender.unit.test.ts @@ -54,7 +54,10 @@ describe("ZoweExplorerExtender unit tests", () => { }) .mockReturnValue(newMocks.profiles), }); - Object.defineProperty(vscode.window, "createTreeView", { value: jest.fn(), configurable: true }); + Object.defineProperty(vscode.window, "createTreeView", { + value: jest.fn().mockReturnValue({ onDidCollapseElement: jest.fn() }), + configurable: true, + }); Object.defineProperty(vscode.window, "showErrorMessage", { value: newMocks.mockErrorMessage, configurable: true, @@ -71,7 +74,22 @@ describe("ZoweExplorerExtender unit tests", () => { }, configurable: true, }); - + Object.defineProperty(Profiles.getInstance(), "addToConfigArray", { + value: jest.fn(), + configurable: true, + }); + Object.defineProperty(ZoweLogger, "error", { + value: jest.fn(), + configurable: true, + }); + Object.defineProperty(ZoweLogger, "trace", { + value: jest.fn(), + configurable: true, + }); + Object.defineProperty(Profiles.getInstance(), "addToConfigArray", { + value: jest.fn(), + configurable: true, + }); return newMocks; } diff --git a/packages/zowe-explorer/__tests__/__unit__/ZoweNode.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/ZoweNode.unit.test.ts index 89a1b6b0a0..acd6ac2ef3 100644 --- a/packages/zowe-explorer/__tests__/__unit__/ZoweNode.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/ZoweNode.unit.test.ts @@ -18,6 +18,7 @@ import { List, imperative } from "@zowe/cli"; import { Profiles } from "../../src/Profiles"; import * as globals from "../../src/globals"; import { ZoweLogger } from "../../src/utils/LoggerUtils"; +import { DatasetSortOpts, SortDirection } from "@zowe/zowe-explorer-api"; describe("Unit Tests (Jest)", () => { // Globals @@ -234,7 +235,6 @@ describe("Unit Tests (Jest)", () => { undefined, profileOne ); - infoChild.id = "root.Use the search button to display data sets"; rootNode.contextValue = globals.DS_SESSION_CONTEXT; rootNode.dirty = false; await expect(await rootNode.getChildren()).toEqual([infoChild]); @@ -255,7 +255,6 @@ describe("Unit Tests (Jest)", () => { undefined, profileOne ); - infoChild.id = "root.Use the search button to display data sets"; rootNode.contextValue = globals.DS_SESSION_CONTEXT; await expect(await rootNode.getChildren()).toEqual([infoChild]); }); @@ -352,11 +351,16 @@ describe("Unit Tests (Jest)", () => { }; }), }); + const sessionNode = { + getSessionNode: jest.fn(), + sort: { method: DatasetSortOpts.Name, direction: SortDirection.Ascending }, + } as unknown as ZoweDatasetNode; + const getSessionNodeSpy = jest.spyOn(ZoweDatasetNode.prototype, "getSessionNode").mockReturnValue(sessionNode); // Creating a rootNode const pds = new ZoweDatasetNode( "[root]: something", vscode.TreeItemCollapsibleState.Collapsed, - { getSessionNode: jest.fn() } as unknown as ZoweDatasetNode, + sessionNode, session, undefined, undefined, @@ -379,6 +383,7 @@ describe("Unit Tests (Jest)", () => { expect(pdsChildren[0].contextValue).toEqual(globals.DS_FILE_ERROR_CONTEXT); expect(pdsChildren[1].label).toEqual("GOODMEM1"); expect(pdsChildren[1].contextValue).toEqual(globals.DS_MEMBER_CONTEXT); + getSessionNodeSpy.mockRestore(); }); /************************************************************************************************************* diff --git a/packages/zowe-explorer/__tests__/__unit__/abstract/ZoweSaveQueue.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/abstract/ZoweSaveQueue.unit.test.ts index ca818d779b..31f0eae796 100644 --- a/packages/zowe-explorer/__tests__/__unit__/abstract/ZoweSaveQueue.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/abstract/ZoweSaveQueue.unit.test.ts @@ -30,6 +30,7 @@ describe("ZoweSaveQueue - unit tests", () => { }, configurable: true, }); + jest.spyOn(Gui, "createTreeView").mockReturnValue({ onDidCollapseElement: jest.fn() } as any); const globalMocks = { errorMessageSpy: jest.spyOn(Gui, "errorMessage"), markDocumentUnsavedSpy: jest.spyOn(workspaceUtils, "markDocumentUnsaved"), diff --git a/packages/zowe-explorer/__tests__/__unit__/abstract/TreeProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/abstract/ZoweTreeProvider.unit.test.ts similarity index 99% rename from packages/zowe-explorer/__tests__/__unit__/abstract/TreeProvider.unit.test.ts rename to packages/zowe-explorer/__tests__/__unit__/abstract/ZoweTreeProvider.unit.test.ts index 35efcd14c4..b6834bbad8 100644 --- a/packages/zowe-explorer/__tests__/__unit__/abstract/TreeProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/abstract/ZoweTreeProvider.unit.test.ts @@ -48,7 +48,7 @@ async function createGlobalMocks() { mockLoadNamedProfile: jest.fn(), mockDefaultProfile: jest.fn(), withProgress: jest.fn(), - createTreeView: jest.fn(), + createTreeView: jest.fn().mockReturnValue({ onDidCollapseElement: jest.fn() }), mockAffects: jest.fn(), mockEditSession: jest.fn(), mockCheckCurrentProfile: jest.fn(), diff --git a/packages/zowe-explorer/__tests__/__unit__/api/ZoweExplorerZosmfApi.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/api/ZoweExplorerZosmfApi.unit.test.ts index 7f671cb9ff..b5c1879d77 100644 --- a/packages/zowe-explorer/__tests__/__unit__/api/ZoweExplorerZosmfApi.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/api/ZoweExplorerZosmfApi.unit.test.ts @@ -100,4 +100,31 @@ describe("Zosmf API tests", () => { Promise.resolve(zowe.Download.ussFile(api.getSession(), "/some/input/path", {})) ); }); + + it("should update the tag attribute of a USS file if a new change is made", async () => { + const api = new ZosmfUssApi(); + const changeTagSpy = jest.fn(); + Object.defineProperty(zowe, "Utilities", { + value: { + putUSSPayload: changeTagSpy, + }, + configurable: true, + }); + await expect(api.updateAttributes("/test/path", { tag: "utf-8" })).resolves.not.toThrow(); + expect(changeTagSpy).toBeCalledTimes(1); + }); + + it("should get the tag of a file successfully", async () => { + const api = new ZosmfUssApi(); + jest.spyOn(JSON, "parse").mockReturnValue({ + stdout: ["-t UTF-8 tesfile.txt"], + }); + Object.defineProperty(zowe, "Utilities", { + value: { + putUSSPayload: () => Buffer.from(""), + }, + configurable: true, + }); + await expect(api.getTag("testfile.txt")).resolves.toEqual("UTF-8"); + }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/dataset/DatasetTree.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/dataset/DatasetTree.unit.test.ts index 907ca4642d..15910257bd 100644 --- a/packages/zowe-explorer/__tests__/__unit__/dataset/DatasetTree.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/dataset/DatasetTree.unit.test.ts @@ -15,7 +15,15 @@ import * as fs from "fs"; import * as zowe from "@zowe/cli"; import { DatasetTree } from "../../../src/dataset/DatasetTree"; import { ZoweDatasetNode } from "../../../src/dataset/ZoweDatasetNode"; -import { Gui, IZoweDatasetTreeNode, ProfilesCache, ValidProfileEnum } from "@zowe/zowe-explorer-api"; +import { + DatasetFilterOpts, + DatasetSortOpts, + Gui, + IZoweDatasetTreeNode, + ProfilesCache, + SortDirection, + ValidProfileEnum, +} from "@zowe/zowe-explorer-api"; import { ZoweExplorerApiRegister } from "../../../src/ZoweExplorerApiRegister"; import { Profiles } from "../../../src/Profiles"; import * as utils from "../../../src/utils/ProfilesUtils"; @@ -60,10 +68,22 @@ function createGlobalMocks() { globalMocks.mockProfileInstance = createInstanceOfProfile(globalMocks.testProfileLoaded); - Object.defineProperty(vscode.window, "createTreeView", { value: jest.fn(), configurable: true }); + Object.defineProperty(ZoweLocalStorage, "storage", { + value: { + get: () => ({ persistence: true, favorites: [], history: [], sessions: ["zosmf"], searchHistory: [], fileHistory: [] }), + update: jest.fn(), + keys: () => [], + }, + configurable: true, + }); + Object.defineProperty(vscode.window, "createTreeView", { + value: jest.fn().mockReturnValue({ onDidCollapseElement: jest.fn() }), + configurable: true, + }); Object.defineProperty(Gui, "showMessage", { value: jest.fn(), configurable: true }); Object.defineProperty(Gui, "setStatusBarMessage", { value: jest.fn().mockReturnValue({ dispose: jest.fn() }), configurable: true }); Object.defineProperty(vscode.window, "showTextDocument", { value: jest.fn(), configurable: true }); + Object.defineProperty(vscode.workspace, "getConfiguration", { value: jest.fn(), configurable: true }); Object.defineProperty(vscode.workspace, "openTextDocument", { value: jest.fn(), configurable: true }); Object.defineProperty(Profiles, "getInstance", { value: jest.fn().mockReturnValue(globalMocks.mockProfileInstance), @@ -77,6 +97,9 @@ function createGlobalMocks() { Object.defineProperty(zowe.Rename, "dataSetMember", { value: jest.fn(), configurable: true }); Object.defineProperty(zowe, "Download", { value: jest.fn(), configurable: true }); Object.defineProperty(globals, "ISTHEIA", { get: globalMocks.isTheia, configurable: true }); + Object.defineProperty(globals, "LOG", { value: jest.fn(), configurable: true }); + Object.defineProperty(globals.LOG, "debug", { value: jest.fn(), configurable: true }); + Object.defineProperty(globals.LOG, "error", { value: jest.fn(), configurable: true }); Object.defineProperty(fs, "unlinkSync", { value: jest.fn(), configurable: true }); Object.defineProperty(fs, "existsSync", { value: jest.fn(), configurable: true }); Object.defineProperty(vscode.commands, "executeCommand", { value: jest.fn(), configurable: true }); @@ -142,18 +165,11 @@ function createGlobalMocks() { configurable: true, }); Object.defineProperty(Gui, "errorMessage", { value: jest.fn(), configurable: true }); - Object.defineProperty(ZoweLocalStorage, "storage", { - value: { - get: jest.fn(() => ({ persistence: true })), - update: jest.fn(), - keys: jest.fn(), - }, - configurable: true, - }); - Object.defineProperty(utils.ProfilesUtils, "usingTeamConfig", { - value: jest.fn().mockReturnValue(true), - configurable: true, - }); + Object.defineProperty(ZoweLogger, "error", { value: jest.fn(), configurable: true }); + Object.defineProperty(ZoweLogger, "debug", { value: jest.fn(), configurable: true }); + Object.defineProperty(ZoweLogger, "warn", { value: jest.fn(), configurable: true }); + Object.defineProperty(ZoweLogger, "info", { value: jest.fn(), configurable: true }); + Object.defineProperty(ZoweLogger, "trace", { value: jest.fn(), configurable: true }); return globalMocks; } @@ -527,7 +543,7 @@ describe("Dataset Tree Unit Tests - Function getChildren", () => { await testTree.getChildren(favProfileNode); - expect(loadProfilesForFavoritesSpy).toHaveBeenCalledWith(favProfileNode); + expect(loadProfilesForFavoritesSpy).toHaveBeenCalledWith(log, favProfileNode); }); it("Checking function for PDS Dataset node", async () => { createGlobalMocks(); @@ -637,7 +653,7 @@ describe("Dataset Tree Unit Tests - Function loadProfilesForFavorites", () => { }), }); - await testTree.loadProfilesForFavorites(favProfileNode); + await testTree.loadProfilesForFavorites(blockMocks.log, favProfileNode); const resultFavProfileNode = testTree.mFavorites[0]; expect(resultFavProfileNode).toEqual(expectedFavProfileNode); @@ -670,7 +686,7 @@ describe("Dataset Tree Unit Tests - Function loadProfilesForFavorites", () => { }), }); mocked(Gui.errorMessage).mockResolvedValueOnce("Remove"); - await testTree.loadProfilesForFavorites(favProfileNode); + await testTree.loadProfilesForFavorites(blockMocks.log, favProfileNode); expect(showErrorMessageSpy).toBeCalledTimes(1); showErrorMessageSpy.mockClear(); }); @@ -708,7 +724,7 @@ describe("Dataset Tree Unit Tests - Function loadProfilesForFavorites", () => { blockMocks.imperativeProfile ); - await testTree.loadProfilesForFavorites(favProfileNode); + await testTree.loadProfilesForFavorites(blockMocks.log, favProfileNode); const resultFavPdsNode = testTree.mFavorites[0].children[0]; expect(resultFavPdsNode).toEqual(expectedFavPdsNode); @@ -748,7 +764,7 @@ describe("Dataset Tree Unit Tests - Function loadProfilesForFavorites", () => { blockMocks.imperativeProfile ); - await testTree.loadProfilesForFavorites(favProfileNode); + await testTree.loadProfilesForFavorites(blockMocks.log, favProfileNode); const resultFavPdsNode = testTree.mFavorites[0].children[0]; expect(resultFavPdsNode).toEqual(expectedFavPdsNode); @@ -1762,6 +1778,43 @@ describe("Dataset Tree Unit Tests - Function datasetFilterPrompt", () => { expect(await testTree.datasetFilterPrompt(testTree.mSessionNodes[1])).not.toBeDefined(); }); + + it("updates stats with modified date and user ID if provided in API", async () => { + const globalMocks = createGlobalMocks(); + const blockMocks = await createBlockMocks(globalMocks); + + const testTree = new DatasetTree(); + testTree.mSessionNodes.push(blockMocks.datasetSessionNode); + const newNode = new ZoweDatasetNode("TEST.PDS", vscode.TreeItemCollapsibleState.Collapsed, testTree.mSessionNodes[1], blockMocks.session); + testTree.mSessionNodes[1].children = [newNode]; + const updateStatsSpy = jest.spyOn(ZoweDatasetNode.prototype, "updateStats"); + const getDatasetsSpy = jest.spyOn((ZoweDatasetNode as any).prototype, "getDatasets"); + getDatasetsSpy.mockResolvedValueOnce([ + { + success: true, + commandResponse: null, + apiResponse: { + items: [ + { + m4date: "2023-10-31", + mtime: "12:00", + msec: "30", + member: "HI", + user: "SOMEUSR", + }, + { + changed: "2023-10-31 03:00:00", + member: "BYE", + id: "SOMEUSR", + }, + ], + }, + }, + ]); + await testTree.mSessionNodes[1].children[0].getChildren(); + + expect(updateStatsSpy).toHaveBeenCalled(); + }); }); describe("Dataset Tree Unit Tests - Function editSession", () => { async function createBlockMocks() { @@ -1859,7 +1912,7 @@ describe("Dataset Tree Unit Tests - Function onDidConfiguration", () => { const imperativeProfile = createIProfile(); const treeView = createTreeView(); const datasetSessionNode = createDatasetSessionNode(session, imperativeProfile); - const workspaceConfiguration = jest.spyOn(vscode.workspace, "getConfiguration").mockReturnValueOnce(createWorkspaceConfiguration()); + const workspaceConfiguration = createWorkspaceConfiguration(); return { session, @@ -1873,6 +1926,7 @@ describe("Dataset Tree Unit Tests - Function onDidConfiguration", () => { createGlobalMocks(); const blockMocks = createBlockMocks(); + mocked(vscode.workspace.getConfiguration).mockReturnValue(blockMocks.workspaceConfiguration); mocked(vscode.window.createTreeView).mockReturnValueOnce(blockMocks.treeView); const testTree = new DatasetTree(); const event = { @@ -1883,7 +1937,7 @@ describe("Dataset Tree Unit Tests - Function onDidConfiguration", () => { await testTree.onDidChangeConfiguration(event); - expect(mocked(vscode.workspace.getConfiguration)).toHaveBeenCalled(); + expect(mocked(vscode.workspace.getConfiguration)).toBeCalledTimes(2); }); }); describe("Dataset Tree Unit Tests - Function renameNode", () => { @@ -2696,6 +2750,308 @@ describe("Dataset Tree Unit Tests - Function initializeFavorites", () => { readFavorites: () => ["[SAMPLE]: SAMPLE.{session}", "*SAMPLE", "SAMPLE*"], }, }); - expect(() => testTree.initializeFavorites()).not.toThrow(); + expect(() => testTree.initializeFavorites(log)).not.toThrow(); + }); +}); +describe("Dataset Tree Unit Tests - Sorting and Filtering operations", () => { + createGlobalMocks(); + mocked(vscode.window.createTreeView).mockReturnValueOnce(createTreeView()); + const tree = new DatasetTree(); + const nodesForSuite = (): Record => { + const session = new ZoweDatasetNode("testSession", vscode.TreeItemCollapsibleState.Collapsed, null, createISession()); + session.contextValue = globals.DS_SESSION_CONTEXT; + const pds = new ZoweDatasetNode("testPds", vscode.TreeItemCollapsibleState.Collapsed, session, createISession()); + pds.contextValue = globals.DS_PDS_CONTEXT; + + const nodeA = new ZoweDatasetNode("A", vscode.TreeItemCollapsibleState.Collapsed, pds, createISession()); + nodeA.stats = { user: "someUser", modifiedDate: new Date() }; + const nodeB = new ZoweDatasetNode("B", vscode.TreeItemCollapsibleState.Collapsed, pds, createISession()); + nodeB.stats = { user: "anotherUser", modifiedDate: new Date("2022-01-01T12:00:00") }; + const nodeC = new ZoweDatasetNode("C", vscode.TreeItemCollapsibleState.Collapsed, pds, createISession()); + nodeC.stats = { user: "someUser", modifiedDate: new Date("2022-03-15T16:30:00") }; + pds.children = [nodeA, nodeB, nodeC]; + pds.sort = { + method: DatasetSortOpts.Name, + direction: SortDirection.Ascending, + }; + session.children = [pds]; + + return { + session, + pds, + }; + }; + + const getBlockMocks = (): Record => ({ + nodeDataChanged: jest.spyOn(DatasetTree.prototype, "nodeDataChanged"), + refreshElement: jest.spyOn(DatasetTree.prototype, "refreshElement"), + showQuickPick: jest.spyOn(Gui, "showQuickPick"), + showInputBox: jest.spyOn(Gui, "showInputBox"), + }); + + afterEach(() => { + const mocks = getBlockMocks(); + for (const mock of Object.values(mocks)) { + mock.mockClear(); + } + }); + + afterAll(() => { + const mocks = getBlockMocks(); + for (const mock of Object.values(mocks)) { + mock.mockRestore(); + } + }); + + describe("sortBy & sortPdsMembersDialog", () => { + // for sorting, we shouldn't need to refresh since all nodes + // should be intact, just in a different order + it("does nothing if no children exist", async () => { + const mocks = getBlockMocks(); + const nodes = nodesForSuite(); + // case 1: called on PDS node + mocks.showQuickPick.mockResolvedValueOnce({ label: "$(case-sensitive) Name (default)" }); + nodes.pds.children = []; + await tree.sortPdsMembersDialog(nodes.pds); + expect(mocks.nodeDataChanged).not.toHaveBeenCalled(); + + // case 2: called on session node + mocks.showQuickPick.mockResolvedValueOnce({ label: "$(case-sensitive) Name (default)" }); + nodes.session.children = []; + await tree.sortPdsMembersDialog(nodes.session); + expect(mocks.nodeDataChanged).not.toHaveBeenCalled(); + }); + + it("sorts by name", async () => { + const mocks = getBlockMocks(); + const nodes = nodesForSuite(); + mocks.showQuickPick.mockResolvedValueOnce({ label: "$(case-sensitive) Name (default)" }); + await tree.sortPdsMembersDialog(nodes.pds); + expect(mocks.nodeDataChanged).toHaveBeenCalled(); + expect(mocks.refreshElement).not.toHaveBeenCalled(); + expect(nodes.pds.children?.map((c: IZoweDatasetTreeNode) => c.label)).toStrictEqual(["A", "B", "C"]); + expect(nodes.pds.children?.reduce((val, cur) => val + (cur.description as string), "")).toBe(""); + }); + + it("sorts by last modified date", async () => { + const mocks = getBlockMocks(); + const nodes = nodesForSuite(); + mocks.showQuickPick.mockResolvedValueOnce({ label: "$(calendar) Date Modified" }); + await tree.sortPdsMembersDialog(nodes.pds); + expect(mocks.nodeDataChanged).toHaveBeenCalled(); + expect(mocks.refreshElement).not.toHaveBeenCalled(); + expect(nodes.pds.children?.map((c: IZoweDatasetTreeNode) => c.label)).toStrictEqual(["B", "C", "A"]); + }); + + it("sorts by last modified date: handling 2 nodes with same date", async () => { + const mocks = getBlockMocks(); + const nodes = nodesForSuite(); + mocks.showQuickPick.mockResolvedValueOnce({ label: "$(fold) Sort Direction" }); + mocks.showQuickPick.mockResolvedValueOnce({ label: "Descending" }); + mocks.showQuickPick.mockResolvedValueOnce({ label: "$(calendar) Date Modified" }); + // insert node with same date modified + const nodeD = new ZoweDatasetNode("D", vscode.TreeItemCollapsibleState.Collapsed, nodes.pds, createISession()); + nodeD.stats = { user: "someUser", modifiedDate: new Date("2022-03-15T16:30:00") }; + nodes.pds.children = [...(nodes.pds.children ?? []), nodeD]; + await tree.sortPdsMembersDialog(nodes.pds); + expect(mocks.nodeDataChanged).toHaveBeenCalled(); + expect(mocks.refreshElement).not.toHaveBeenCalled(); + expect(nodes.pds.children?.map((c: IZoweDatasetTreeNode) => c.label)).toStrictEqual(["A", "D", "C", "B"]); + }); + + it("sorts by user ID", async () => { + const mocks = getBlockMocks(); + const nodes = nodesForSuite(); + mocks.showQuickPick.mockResolvedValueOnce({ label: "$(account) User ID" }); + await tree.sortPdsMembersDialog(nodes.pds); + expect(mocks.nodeDataChanged).toHaveBeenCalled(); + expect(mocks.refreshElement).not.toHaveBeenCalled(); + expect(nodes.pds.children?.map((c: IZoweDatasetTreeNode) => c.label)).toStrictEqual(["B", "A", "C"]); + }); + + it("returns to sort selection dialog when sort direction selection is canceled", async () => { + const sortPdsMembersDialog = jest.spyOn(tree, "sortPdsMembersDialog"); + const mocks = getBlockMocks(); + const nodes = nodesForSuite(); + mocks.showQuickPick.mockResolvedValueOnce({ label: "$(fold) Sort Direction" }); + mocks.showQuickPick.mockResolvedValueOnce(undefined); + await tree.sortPdsMembersDialog(nodes.pds); + expect(mocks.nodeDataChanged).not.toHaveBeenCalled(); + expect(mocks.refreshElement).not.toHaveBeenCalled(); + expect(sortPdsMembersDialog).toHaveBeenCalledTimes(2); + }); + + it("sorting by session: descriptions are reset when sorted by name", async () => { + const mocks = getBlockMocks(); + const nodes = nodesForSuite(); + mocks.showQuickPick.mockResolvedValueOnce({ label: "$(case-sensitive) Name (default)" }); + await tree.sortPdsMembersDialog(nodes.session); + expect(mocks.nodeDataChanged).toHaveBeenCalled(); + expect(mocks.refreshElement).not.toHaveBeenCalled(); + expect(nodes.pds.children?.map((c: IZoweDatasetTreeNode) => c.label)).toStrictEqual(["A", "B", "C"]); + expect(nodes.pds.children?.reduce((val, cur) => val + (cur.description as string), "")).toBe(""); + }); + }); + + describe("filterBy & filterPdsMembersDialog", () => { + afterEach(() => { + const mocks = getBlockMocks(); + for (const mock of Object.values(mocks)) { + mock.mockReset(); + } + }); + + afterAll(() => { + const mocks = getBlockMocks(); + for (const mock of Object.values(mocks)) { + mock.mockRestore(); + } + }); + + it("calls refreshElement if PDS children were removed from a previous filter", async () => { + const mocks = getBlockMocks(); + const nodes = nodesForSuite(); + mocks.showQuickPick.mockResolvedValueOnce("$(calendar) Date Modified" as any); + mocks.showInputBox.mockResolvedValueOnce("2022-01-01"); + + nodes.pds.filter = { method: DatasetFilterOpts.UserId, value: "invalidUserId" }; + nodes.pds.children = []; + await tree.filterPdsMembersDialog(nodes.pds); + // nodeDataChanged called once to show new description + expect(mocks.nodeDataChanged).toHaveBeenCalledWith(nodes.pds); + expect(mocks.refreshElement).toHaveBeenCalledWith(nodes.pds); + }); + + it("returns to filter selection dialog when filter entry is canceled", async () => { + const filterPdsMembersSpy = jest.spyOn(tree, "filterPdsMembersDialog"); + const mocks = getBlockMocks(); + const nodes = nodesForSuite(); + mocks.showQuickPick.mockResolvedValueOnce("$(calendar) Date Modified" as any); + mocks.showInputBox.mockResolvedValueOnce(undefined); + await tree.filterPdsMembersDialog(nodes.pds); + expect(mocks.nodeDataChanged).not.toHaveBeenCalled(); + expect(mocks.refreshElement).not.toHaveBeenCalled(); + expect(filterPdsMembersSpy).toHaveBeenCalledTimes(2); + }); + + it("filters single PDS by last modified date", async () => { + const mocks = getBlockMocks(); + const nodes = nodesForSuite(); + mocks.showQuickPick.mockResolvedValueOnce("$(calendar) Date Modified" as any); + mocks.showInputBox.mockResolvedValueOnce("2022-03-15"); + await tree.filterPdsMembersDialog(nodes.pds); + expect(mocks.nodeDataChanged).toHaveBeenCalled(); + expect(mocks.refreshElement).not.toHaveBeenCalled(); + expect(nodes.pds.children?.map((c: IZoweDatasetTreeNode) => c.label)).toStrictEqual(["C"]); + }); + + it("filters single PDS by user ID", async () => { + const mocks = getBlockMocks(); + const nodes = nodesForSuite(); + mocks.showQuickPick.mockResolvedValueOnce("$(account) User ID" as any); + mocks.showInputBox.mockResolvedValueOnce("anotherUser"); + await tree.filterPdsMembersDialog(nodes.pds); + expect(mocks.nodeDataChanged).toHaveBeenCalled(); + expect(mocks.refreshElement).not.toHaveBeenCalled(); + expect(nodes.pds.children?.map((c: IZoweDatasetTreeNode) => c.label)).toStrictEqual(["B"]); + }); + + it("filters PDS members using the session node filter", async () => { + const mocks = getBlockMocks(); + const nodes = nodesForSuite(); + const uidString = "$(account) User ID" as any; + const anotherUser = "anotherUser"; + mocks.showQuickPick.mockResolvedValueOnce(uidString).mockResolvedValueOnce(uidString); + mocks.showInputBox.mockResolvedValueOnce(anotherUser).mockResolvedValueOnce(anotherUser); + + // case 1: old filter was set on session, just refresh PDS to use new filter + nodes.session.filter = { + method: DatasetFilterOpts.LastModified, + value: "2020-01-01", + }; + await tree.filterPdsMembersDialog(nodes.session); + expect(mocks.refreshElement).toHaveBeenCalled(); + + // case 2: no old filter present, PDS has children to be filtered + nodes.session.filter = undefined; + await tree.filterPdsMembersDialog(nodes.session); + expect(mocks.nodeDataChanged).toHaveBeenCalled(); + }); + + it("clears filter for a PDS when selected in dialog", async () => { + const mocks = getBlockMocks(); + const nodes = nodesForSuite(); + const resp = "$(clear-all) Clear filter for PDS" as any; + mocks.showQuickPick.mockResolvedValueOnce(resp); + const updateFilterForNode = jest.spyOn(DatasetTree.prototype, "updateFilterForNode"); + await tree.filterPdsMembersDialog(nodes.pds); + expect(mocks.refreshElement).not.toHaveBeenCalled(); + expect(updateFilterForNode).toHaveBeenCalledWith(nodes.pds, null, false); + }); + }); + + describe("removeSearchHistory", () => { + it("removes the search item passed in from the current history", () => { + tree.addSearchHistory("test"); + expect(tree["mHistory"]["mSearchHistory"].length).toEqual(1); + tree.removeSearchHistory("test"); + expect(tree["mHistory"]["mSearchHistory"].length).toEqual(0); + }); + }); + + describe("resetSearchHistory", () => { + it("clears the entire search history", () => { + tree.addSearchHistory("test1"); + tree.addSearchHistory("test2"); + tree.addSearchHistory("test3"); + tree.addSearchHistory("test4"); + expect(tree["mHistory"]["mSearchHistory"].length).toEqual(4); + tree.resetSearchHistory(); + expect(tree["mHistory"]["mSearchHistory"].length).toEqual(0); + }); + }); + + describe("resetFileHistory", () => { + it("clears the entire file history", () => { + tree.addFileHistory("test1"); + tree.addFileHistory("test2"); + tree.addFileHistory("test3"); + tree.addFileHistory("test4"); + expect(tree["mHistory"]["mFileHistory"].length).toEqual(4); + tree.resetFileHistory(); + expect(tree["mHistory"]["mFileHistory"].length).toEqual(0); + }); + }); + + describe("addDsTemplate", () => { + it("adds a new DS template to the persistent object", () => { + tree.addDsTemplate({ test: "test" } as any); + expect(tree["mHistory"]["mDsTemplates"].length).toEqual(1); + }); + }); + + describe("getSessions", () => { + it("gets all the available sessions from persistent object", () => { + tree["mHistory"]["mSessions"] = ["sestest"]; + expect(tree.getSessions()).toEqual(["sestest"]); + }); + }); + + describe("getDsTemplates", () => { + it("gets all the DS templates from persistent object", () => { + jest.spyOn(vscode.workspace, "getConfiguration").mockReturnValue({ + get: () => ["test1", "test2", "test3"], + } as any); + expect(tree.getDsTemplates()).toEqual(["test1", "test2", "test3"]); + }); + }); + + describe("getFavorites", () => { + it("gets all the favorites from persistent object", () => { + jest.spyOn(ZoweLocalStorage, "getValue").mockReturnValue({ + favorites: ["test1", "test2", "test3"], + }); + expect(tree.getFavorites()).toEqual(["test1", "test2", "test3"]); + }); }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/dataset/actions.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/dataset/actions.unit.test.ts index 33eb2b005a..6462414f16 100644 --- a/packages/zowe-explorer/__tests__/__unit__/dataset/actions.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/dataset/actions.unit.test.ts @@ -1446,6 +1446,71 @@ describe("Dataset Actions Unit Tests - Function saveFile", () => { logSpy.mockClear(); commandSpy.mockClear(); }); + + it("Checking common dataset saving failed due to conflict with server version when file size has not changed", async () => { + globals.defineGlobals(""); + createGlobalMocks(); + const blockMocks = createBlockMocks(); + const node = new ZoweDatasetNode( + "HLQ.TEST.AFILE", + vscode.TreeItemCollapsibleState.None, + blockMocks.datasetSessionNode, + null, + undefined, + undefined, + blockMocks.imperativeProfile + ); + blockMocks.datasetSessionNode.children.push(node); + + mocked(sharedUtils.concatChildNodes).mockReturnValueOnce([node]); + blockMocks.testDatasetTree.getChildren.mockReturnValueOnce([blockMocks.datasetSessionNode]); + mocked(zowe.List.dataSet).mockResolvedValue({ + success: true, + commandResponse: "", + apiResponse: { + items: [{ dsname: "HLQ.TEST.AFILE" }], + }, + }); + mocked(zowe.Upload.pathToDataSet).mockResolvedValueOnce({ + success: false, + commandResponse: "Rest API failure with HTTP(S) status 412", + apiResponse: [], + }); + + mocked(vscode.window.withProgress).mockImplementation((progLocation, callback) => { + return callback(); + }); + const profile = blockMocks.imperativeProfile; + profile.profile.encoding = 1047; + blockMocks.profileInstance.loadNamedProfile.mockReturnValueOnce(blockMocks.imperativeProfile); + mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); + Object.defineProperty(wsUtils, "markDocumentUnsaved", { + value: jest.fn(), + configurable: true, + }); + Object.defineProperty(context, "isTypeUssTreeNode", { + value: jest.fn().mockReturnValueOnce(false), + configurable: true, + }); + Object.defineProperty(ZoweExplorerApiRegister.getMvsApi, "getContents", { + value: jest.fn(), + configurable: true, + }); + + const testDocument = createTextDocument("HLQ.TEST.AFILE", blockMocks.datasetSessionNode); + (testDocument as any).fileName = path.join(globals.DS_DIR, testDocument.fileName); + const logSpy = jest.spyOn(ZoweLogger, "warn"); + const commandSpy = jest.spyOn(vscode.commands, "executeCommand"); + jest.spyOn(fs, "statSync").mockReturnValueOnce({ size: 0 } as any); + + await dsActions.saveFile(testDocument, blockMocks.testDatasetTree); + + expect(logSpy).toBeCalledWith("Remote file has changed. Presenting with way to resolve file."); + expect(mocked(sharedUtils.concatChildNodes)).toBeCalled(); + expect(commandSpy).toBeCalledWith("workbench.files.action.compareWithSaved"); + logSpy.mockClear(); + commandSpy.mockClear(); + }); }); describe("Dataset Actions Unit Tests - Function showAttributes", () => { diff --git a/packages/zowe-explorer/__tests__/__unit__/dataset/init.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/dataset/init.unit.test.ts index d3c5eca4ec..fc418dae1f 100644 --- a/packages/zowe-explorer/__tests__/__unit__/dataset/init.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/dataset/init.unit.test.ts @@ -45,6 +45,8 @@ describe("Test src/dataset/extension", () => { onDidChangeConfiguration: jest.fn(), getTreeView: jest.fn(), refreshElement: jest.fn(), + sortPdsMembersDialog: jest.fn(), + filterPdsMembersDialog: jest.fn(), }; const commands: IJestIt[] = [ { @@ -156,7 +158,7 @@ describe("Test src/dataset/extension", () => { }, { name: "zowe.ds.submitJcl", - mock: [{ spy: jest.spyOn(dsActions, "submitJcl"), arg: [dsProvider] }], + mock: [{ spy: jest.spyOn(dsActions, "submitJcl"), arg: [dsProvider, test.value] }], }, { name: "zowe.ds.submitMember", @@ -250,6 +252,14 @@ describe("Test src/dataset/extension", () => { name: "zowe.ds.ssoLogout", mock: [{ spy: jest.spyOn(dsProvider, "ssoLogout"), arg: [test.value] }], }, + { + name: "zowe.ds.sortBy", + mock: [{ spy: jest.spyOn(dsProvider, "sortPdsMembersDialog"), arg: [test.value] }], + }, + { + name: "zowe.ds.filterBy", + mock: [{ spy: jest.spyOn(dsProvider, "filterPdsMembersDialog"), arg: [test.value] }], + }, { name: "onDidChangeConfiguration", mock: [{ spy: jest.spyOn(dsProvider, "onDidChangeConfiguration"), arg: [test.value] }], diff --git a/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts index 16c7ac0724..205c65fdef 100644 --- a/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts @@ -28,10 +28,10 @@ import { SettingsConfig } from "../../src/utils/SettingsConfig"; import { ZoweExplorerExtender } from "../../src/ZoweExplorerExtender"; import { DatasetTree } from "../../src/dataset/DatasetTree"; import { USSTree } from "../../src/uss/USSTree"; -import { ZoweLogger } from "../../src/utils/LoggerUtils"; import { ZoweSaveQueue } from "../../src/abstract/ZoweSaveQueue"; import { ZoweLocalStorage } from "../../src/utils/ZoweLocalStorage"; jest.mock("../../src/utils/LoggerUtils"); +import { ProfilesUtils } from "../../src/utils/ProfilesUtils"; jest.mock("vscode"); jest.mock("fs"); @@ -47,7 +47,7 @@ async function createGlobalMocks() { mockMoveSync: jest.fn(), mockGetAllProfileNames: jest.fn(), mockReveal: jest.fn(), - mockCreateTreeView: jest.fn(), + mockCreateTreeView: jest.fn().mockReturnValue({ onDidCollapseElement: jest.fn() }), mockExecuteCommand: jest.fn(), mockRegisterCommand: jest.fn(), mockOnDidSaveTextDocument: jest.fn(), @@ -141,6 +141,7 @@ async function createGlobalMocks() { appName: vscode.env.appName, uriScheme: vscode.env.uriScheme, expectedCommands: [ + "zowe.updateSecureCredentials", "zowe.extRefresh", "zowe.all.config.init", "zowe.ds.addSession", @@ -178,6 +179,8 @@ async function createGlobalMocks() { "zowe.ds.enableValidation", "zowe.ds.ssoLogin", "zowe.ds.ssoLogout", + "zowe.ds.sortBy", + "zowe.ds.filterBy", "zowe.uss.addFavorite", "zowe.uss.removeFavorite", "zowe.uss.addSession", @@ -240,9 +243,11 @@ async function createGlobalMocks() { "zowe.jobs.startPolling", "zowe.jobs.stopPolling", "zowe.jobs.cancelJob", + "zowe.jobs.sortBy", "zowe.manualPoll", - "zowe.updateSecureCredentials", + "zowe.editHistory", "zowe.promptCredentials", + "zowe.profileManagement", "zowe.openRecentMember", "zowe.searchInAllLoadedItems", "zowe.ds.deleteProfile", @@ -454,6 +459,7 @@ describe("Extension Unit Tests", () => { let globalMocks; beforeAll(async () => { globalMocks = await createGlobalMocks(); + jest.spyOn(fs, "readFileSync").mockReturnValue(Buffer.from(JSON.stringify({ overrides: { credentialManager: "@zowe/cli" } }), "utf-8")); Object.defineProperty(zowe.imperative, "ProfileInfo", { value: globalMocks.mockImperativeProfileInfo, configurable: true, @@ -574,6 +580,7 @@ describe("Extension Unit Tests", () => { describe("Extension Unit Tests - THEIA", () => { it("Tests that activate() works correctly for Theia", async () => { const globalMocks = await createGlobalMocks(); + jest.spyOn(ProfilesUtils, "getCredentialManagerOverride").mockReturnValueOnce("@zowe/cli"); Object.defineProperty(vscode.env, "appName", { value: "Eclipse Theia" }); Object.defineProperty(vscode.env, "uriScheme", { value: "theia" }); diff --git a/packages/zowe-explorer/__tests__/__unit__/generators/icons.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/generators/icons.unit.test.ts index 506ddc5dc4..80885d024c 100644 --- a/packages/zowe-explorer/__tests__/__unit__/generators/icons.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/generators/icons.unit.test.ts @@ -20,7 +20,7 @@ import { ZoweLocalStorage } from "../../../src/utils/ZoweLocalStorage"; describe("Checking icon generator's basics", () => { const setGlobalMocks = () => { - const createTreeView = jest.fn(); + const createTreeView = jest.fn().mockReturnValue({ onDidCollapseElement: jest.fn() }); const getConfiguration = jest.fn(); Object.defineProperty(vscode.window, "createTreeView", { value: createTreeView }); diff --git a/packages/zowe-explorer/__tests__/__unit__/generators/messages.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/generators/messages.unit.test.ts index 74a7fffcf3..eecf2c01b7 100644 --- a/packages/zowe-explorer/__tests__/__unit__/generators/messages.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/generators/messages.unit.test.ts @@ -20,7 +20,7 @@ jest.mock("vscode"); describe("Checking message generator's basics", () => { const setGlobalMocks = () => { - const createTreeView = jest.fn(); + const createTreeView = jest.fn().mockReturnValue({ onDidCollapseElement: jest.fn() }); const getConfiguration = jest.fn(); Object.defineProperty(vscode.window, "createTreeView", { value: createTreeView }); Object.defineProperty(ZoweLocalStorage, "storage", { diff --git a/packages/zowe-explorer/__tests__/__unit__/job/ZosJobsProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/job/ZosJobsProvider.unit.test.ts index a864897338..eea26f20c3 100644 --- a/packages/zowe-explorer/__tests__/__unit__/job/ZosJobsProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/job/ZosJobsProvider.unit.test.ts @@ -32,6 +32,7 @@ import { getIconByNode } from "../../../src/generators/icons"; import { createJesApi } from "../../../__mocks__/mockCreators/api"; import * as sessUtils from "../../../src/utils/SessionUtils"; import { jobStringValidator } from "../../../src/shared/utils"; +import { ZoweLogger } from "../../../src/utils/LoggerUtils"; import { Poller } from "@zowe/zowe-explorer-api/src/utils"; import { SettingsConfig } from "../../../src/utils/SettingsConfig"; import { ZoweLocalStorage } from "../../../src/utils/ZoweLocalStorage"; @@ -45,7 +46,7 @@ async function createGlobalMocks() { mockGetJob: jest.fn(), mockRefresh: jest.fn(), mockAffectsConfig: jest.fn(), - createTreeView: jest.fn(), + createTreeView: jest.fn().mockReturnValue({ onDidCollapseElement: jest.fn() }), mockGetSpoolFiles: jest.fn(), mockDeleteJobs: jest.fn(), mockShowInputBox: jest.fn(), @@ -85,7 +86,7 @@ async function createGlobalMocks() { }; }), }; - + jest.spyOn(Gui, "createTreeView").mockImplementation(globalMocks.createTreeView); Object.defineProperty(ProfilesCache, "getConfigInstance", { value: jest.fn(() => { return { @@ -130,6 +131,10 @@ async function createGlobalMocks() { Object.defineProperty(vscode.window, "showQuickPick", { value: globalMocks.mockShowQuickPick, configurable: true }); Object.defineProperty(vscode, "ConfigurationTarget", { value: globalMocks.enums, configurable: true }); Object.defineProperty(vscode.window, "showInputBox", { value: globalMocks.mockShowInputBox, configurable: true }); + Object.defineProperty(vscode.workspace, "getConfiguration", { + value: globalMocks.mockGetConfiguration, + configurable: true, + }); Object.defineProperty(zowe, "DeleteJobs", { value: globalMocks.mockDeleteJobs, configurable: true }); Object.defineProperty(vscode.window, "createQuickPick", { value: globalMocks.mockCreateQuickPick, @@ -170,7 +175,13 @@ async function createGlobalMocks() { }, configurable: true, }); - globalMocks.createTreeView.mockReturnValue("testTreeView"); + Object.defineProperty(globals, "LOG", { value: jest.fn(), configurable: true }); + Object.defineProperty(globals.LOG, "error", { value: jest.fn(), configurable: true }); + Object.defineProperty(ZoweLogger, "error", { value: jest.fn(), configurable: true }); + Object.defineProperty(ZoweLogger, "debug", { value: jest.fn(), configurable: true }); + Object.defineProperty(ZoweLogger, "warn", { value: jest.fn(), configurable: true }); + Object.defineProperty(ZoweLogger, "info", { value: jest.fn(), configurable: true }); + Object.defineProperty(ZoweLogger, "trace", { value: jest.fn(), configurable: true }); globalMocks.testSessionNode = createJobSessionNode(globalMocks.testSession, globalMocks.testProfile); globalMocks.mockGetJob.mockReturnValue(globalMocks.testIJob); globalMocks.mockGetJobsByOwnerAndPrefix.mockReturnValue([globalMocks.testIJob, globalMocks.testIJobComplete]); @@ -182,8 +193,7 @@ async function createGlobalMocks() { return {}; }), }); - jest.spyOn(vscode.workspace, "getConfiguration").mockImplementationOnce(globalMocks.mockGetConfiguration); - globalMocks.testJobsProvider = await createJobsTree(); + globalMocks.testJobsProvider = await createJobsTree(zowe.imperative.Logger.getAppLogger()); globalMocks.testJobsProvider.mSessionNodes.push(globalMocks.testSessionNode); Object.defineProperty(globalMocks.testJobsProvider, "refresh", { value: globalMocks.mockRefresh, @@ -254,7 +264,7 @@ describe("ZosJobsProvider unit tests - Function getChildren", () => { const loadProfilesForFavoritesSpy = jest.spyOn(testTree, "loadProfilesForFavorites").mockImplementationOnce(() => Promise.resolve([])); await testTree.getChildren(favProfileNode); - expect(loadProfilesForFavoritesSpy).toHaveBeenCalledWith(favProfileNode); + expect(loadProfilesForFavoritesSpy).toHaveBeenCalledWith(log, favProfileNode); }); it("Tests that getChildren gets children of a session element", async () => { const globalMocks = await createGlobalMocks(); @@ -404,7 +414,7 @@ describe("ZosJobsProvider unit tests - Function loadProfilesForFavorites", () => }), }); - await testTree.loadProfilesForFavorites(favProfileNode); + await testTree.loadProfilesForFavorites(blockMocks.log, favProfileNode); const resultFavProfileNode = testTree.mFavorites[0]; expect(resultFavProfileNode).toEqual(expectedFavProfileNode); @@ -440,7 +450,7 @@ describe("ZosJobsProvider unit tests - Function loadProfilesForFavorites", () => configurable: true, }); mocked(Gui.errorMessage).mockResolvedValueOnce({ title: "Remove" }); - await testTree.loadProfilesForFavorites(favProfileNode); + await testTree.loadProfilesForFavorites(blockMocks.log, favProfileNode); expect(showErrorMessageSpy).toBeCalledTimes(1); showErrorMessageSpy.mockClear(); }); @@ -479,7 +489,7 @@ describe("ZosJobsProvider unit tests - Function loadProfilesForFavorites", () => ); expectedFavJobNode.contextValue = globals.JOBS_JOB_CONTEXT + globals.FAV_SUFFIX; - await testTree.loadProfilesForFavorites(favProfileNode); + await testTree.loadProfilesForFavorites(blockMocks.log, favProfileNode); const resultFavJobNode = testTree.mFavorites[0].children[0]; expect(resultFavJobNode).toEqual(expectedFavJobNode); @@ -512,7 +522,7 @@ describe("ZosJobsProvider unit tests - Function loadProfilesForFavorites", () => ); expectedFavJobNode.contextValue = globals.JOBS_JOB_CONTEXT + globals.FAV_SUFFIX; - await testTree.loadProfilesForFavorites(favProfileNode); + await testTree.loadProfilesForFavorites(blockMocks.log, favProfileNode); const resultFavJobNode = testTree.mFavorites[0].children[0]; expect(resultFavJobNode).toEqual(expectedFavJobNode); @@ -923,3 +933,52 @@ describe("Jobs utils unit tests - Function jobStringValidator", () => { invalidOpts.forEach((invalidOpt) => expect(jobStringValidator(invalidOpt[0], invalidOpt[1])).toContain("Invalid")); }); }); + +describe("removeSearchHistory", () => { + it("removes the search item passed in from the current history", () => { + const tree = new ZosJobsProvider(); + tree.addSearchHistory("test"); + expect(tree["mHistory"]["mSearchHistory"].length).toEqual(1); + tree.removeSearchHistory("test"); + expect(tree["mHistory"]["mSearchHistory"].length).toEqual(0); + }); +}); + +describe("resetSearchHistory", () => { + it("clears the entire search history", () => { + const tree = new ZosJobsProvider(); + tree.addSearchHistory("test1"); + tree.addSearchHistory("test2"); + tree.addSearchHistory("test3"); + tree.addSearchHistory("test4"); + expect(tree["mHistory"]["mSearchHistory"].length).toEqual(4); + tree.resetSearchHistory(); + expect(tree["mHistory"]["mSearchHistory"].length).toEqual(0); + }); +}); + +describe("getSessions", () => { + it("gets all the available sessions from persistent object", () => { + const tree = new ZosJobsProvider(); + tree["mHistory"]["mSessions"] = ["sestest"]; + expect(tree.getSessions()).toEqual(["sestest"]); + }); +}); + +describe("getFileHistory", () => { + it("gets all the file history from persistent object", () => { + const tree = new ZosJobsProvider(); + tree["mHistory"]["mFileHistory"] = ["test1", "test2", "test3"]; + expect(tree.getFileHistory()).toEqual(["test1", "test2", "test3"]); + }); +}); + +describe("getFavorites", () => { + it("gets all the favorites from persistent object", () => { + const tree = new ZosJobsProvider(); + jest.spyOn(ZoweLocalStorage, "getValue").mockReturnValue({ + favorites: ["test1", "test2", "test3"], + }); + expect(tree.getFavorites()).toEqual(["test1", "test2", "test3"]); + }); +}); diff --git a/packages/zowe-explorer/__tests__/__unit__/job/ZoweJobNode.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/job/ZoweJobNode.unit.test.ts index c776f5dba0..5d7c94c973 100644 --- a/packages/zowe-explorer/__tests__/__unit__/job/ZoweJobNode.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/job/ZoweJobNode.unit.test.ts @@ -16,7 +16,7 @@ import * as zowe from "@zowe/cli"; import * as globals from "../../../src/globals"; import { createIJobFile, createIJobObject, createJobSessionNode } from "../../../__mocks__/mockCreators/jobs"; import { Job } from "../../../src/job/ZoweJobNode"; -import { IZoweJobTreeNode, ProfilesCache, Gui } from "@zowe/zowe-explorer-api"; +import { IZoweJobTreeNode, ProfilesCache, Gui, JobSortOpts, SortDirection } from "@zowe/zowe-explorer-api"; import { ZoweExplorerApiRegister } from "../../../src/ZoweExplorerApiRegister"; import { Profiles } from "../../../src/Profiles"; import * as sessUtils from "../../../src/utils/SessionUtils"; @@ -40,6 +40,7 @@ async function createGlobalMocks() { mockAffectsConfig: jest.fn(), createTreeView: jest.fn(() => ({ reveal: jest.fn(), + onDidCollapseElement: jest.fn(), })), mockCreateSessCfgFromArgs: jest.fn(), mockGetSpoolFiles: jest.fn(), @@ -581,9 +582,8 @@ describe("ZoweJobNode unit tests - Function saveSearch", () => { const expectedJob = favJob; expectedJob.contextValue = globals.JOBS_SESSION_CONTEXT + globals.FAV_SUFFIX; - const savedFavJob = await globalMocks.testJobsProvider.saveSearch(favJob); - - expect(savedFavJob).toEqual(expectedJob); + globalMocks.testJobsProvider.saveSearch(favJob); + expect(expectedJob.contextValue).toEqual(favJob.contextValue); }); }); @@ -849,7 +849,7 @@ describe("Job - sortJobs", () => { jobid: "JOBID120", }, } as IZoweJobTreeNode, - ].sort((a, b) => Job.sortJobs(a, b)); + ].sort(Job.sortJobs({ method: JobSortOpts.Id, direction: SortDirection.Ascending })); expect(sorted[0].job.jobid).toBe("JOBID120"); expect(sorted[1].job.jobid).toBe("JOBID120"); expect(sorted[2].job.jobid).toBe("JOBID123"); diff --git a/packages/zowe-explorer/__tests__/__unit__/job/actions.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/job/actions.unit.test.ts index 3232fc2d21..a712ba30fe 100644 --- a/packages/zowe-explorer/__tests__/__unit__/job/actions.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/job/actions.unit.test.ts @@ -11,7 +11,7 @@ import * as vscode from "vscode"; import * as zowe from "@zowe/cli"; -import { Gui, IZoweJobTreeNode, ValidProfileEnum } from "@zowe/zowe-explorer-api"; +import { Gui, IZoweJobTreeNode, JobSortOpts, SortDirection, ValidProfileEnum } from "@zowe/zowe-explorer-api"; import { Job, Spool } from "../../../src/job/ZoweJobNode"; import { createISession, @@ -41,6 +41,8 @@ import * as refreshActions from "../../../src/shared/refresh"; import * as sharedUtils from "../../../src/shared/utils"; import { ZoweLogger } from "../../../src/utils/LoggerUtils"; import { SpoolFile } from "../../../src/SpoolProvider"; +import { ZosJobsProvider } from "../../../src/job/ZosJobsProvider"; +import { ZoweLocalStorage } from "../../../src/utils/ZoweLocalStorage"; import { LocalFileManagement } from "../../../src/utils/LocalFileManagement"; jest.mock("../../../src/utils/LoggerUtils"); @@ -48,6 +50,48 @@ jest.mock("../../../src/utils/LoggerUtils"); const activeTextEditorDocument = jest.fn(); function createGlobalMocks() { + const newMocks = { + session: createISession(), + treeView: createTreeView(), + iJob: createIJobObject(), + imperativeProfile: createIProfile(), + JobNode1: new Job( + "testProfile", + vscode.TreeItemCollapsibleState.None, + null as any, + createISession(), + settingJobObjects(createIJobObject(), "ZOWEUSR1", "JOB045123", "ABEND S222"), + createIProfile() + ), + JobNode2: new Job( + "testProfile", + vscode.TreeItemCollapsibleState.None, + null as any, + createISession(), + settingJobObjects(createIJobObject(), "ZOWEUSR1", "JOB045120", "CC 0000"), + createIProfile() + ), + JobNode3: new Job( + "testProfile", + vscode.TreeItemCollapsibleState.None, + null as any, + createISession(), + settingJobObjects(createIJobObject(), "ZOWEUSR2", "JOB045125", "CC 0000"), + createIProfile() + ), + mockJobArray: [], + testJobsTree: null as any, + jesApi: null as any, + }; + newMocks.testJobsTree = createJobsTree(newMocks.session, newMocks.iJob, newMocks.imperativeProfile, newMocks.treeView); + newMocks.mockJobArray = [newMocks.JobNode1, newMocks.JobNode2, newMocks.JobNode3] as any; + newMocks.jesApi = createJesApi(newMocks.imperativeProfile); + bindJesApi(newMocks.jesApi); + jest.spyOn(Gui, "createTreeView").mockReturnValue({ onDidCollapseElement: jest.fn() } as any); + Object.defineProperty(vscode.workspace, "getConfiguration", { + value: jest.fn().mockImplementation(() => new Map([["zowe.jobs.confirmSubmission", false]])), + configurable: true, + }); Object.defineProperty(Gui, "showMessage", { value: jest.fn(), configurable: true }); Object.defineProperty(Gui, "warningMessage", { value: jest.fn(), configurable: true }); Object.defineProperty(Gui, "errorMessage", { value: jest.fn(), configurable: true }); @@ -81,6 +125,26 @@ function createGlobalMocks() { Object.defineProperty(vscode.commands, "executeCommand", { value: executeCommand, configurable: true }); Object.defineProperty(SpoolProvider, "encodeJobFile", { value: jest.fn(), configurable: true }); Object.defineProperty(SpoolProvider, "toUniqueJobFileUri", { value: jest.fn(), configurable: true }); + Object.defineProperty(ZoweLogger, "error", { value: jest.fn(), configurable: true }); + Object.defineProperty(ZoweLogger, "debug", { value: jest.fn(), configurable: true }); + Object.defineProperty(ZoweLogger, "trace", { value: jest.fn(), configurable: true }); + Object.defineProperty(vscode.window, "showInformationMessage", { value: jest.fn(), configurable: true }); + Object.defineProperty(ZoweLocalStorage, "storage", { + value: { + get: () => ({ persistence: true, favorites: [], history: [], sessions: ["zosmf"], searchHistory: [], fileHistory: [] }), + update: jest.fn(), + keys: () => [], + }, + configurable: true, + }); + function settingJobObjects(job: zowe.IJob, setjobname: string, setjobid: string, setjobreturncode: string): zowe.IJob { + job.jobname = setjobname; + job.jobid = setjobid; + job.retcode = setjobreturncode; + return job; + } + + return newMocks; } // Idea is borrowed from: https://github.com/kulshekhar/ts-jest/blob/master/src/util/testing.ts @@ -91,25 +155,9 @@ afterEach(() => { }); describe("Jobs Actions Unit Tests - Function setPrefix", () => { - function createBlockMocks() { - const session = createISession(); - const treeView = createTreeView(); - const iJob = createIJobObject(); - const imperativeProfile = createIProfile(); - - return { - session, - treeView, - iJob, - imperativeProfile, - testJobsTree: createJobsTree(session, iJob, imperativeProfile, treeView), - }; - } - it("Checking that the prefix is set correctly on the job", async () => { - createGlobalMocks(); - const blockMocks = createBlockMocks(); - const node = new Job("job", vscode.TreeItemCollapsibleState.None, null, blockMocks.session, null, null); + const blockMocks = createGlobalMocks(); + const node = new Job("job", vscode.TreeItemCollapsibleState.None, null as any, blockMocks.session, null as any, null as any); const mySpy = mocked(vscode.window.showInputBox).mockResolvedValue("*"); await jobActions.setPrefix(node, blockMocks.testJobsTree); @@ -126,25 +174,16 @@ describe("Jobs Actions Unit Tests - Function setPrefix", () => { }); describe("Jobs Actions Unit Tests - Function setOwner", () => { - function createBlockMocks() { - const session = createISession(); - const treeView = createTreeView(); - const iJob = createIJobObject(); - const imperativeProfile = createIProfile(); - - return { - session, - treeView, - iJob, - imperativeProfile, - testJobsTree: createJobsTree(session, iJob, imperativeProfile, treeView), - }; - } - it("Checking that the owner is set correctly on the job", async () => { - createGlobalMocks(); - const blockMocks = createBlockMocks(); - const node = new Job("job", vscode.TreeItemCollapsibleState.None, null, blockMocks.session, blockMocks.iJob, blockMocks.imperativeProfile); + const blockMocks = createGlobalMocks(); + const node = new Job( + "job", + vscode.TreeItemCollapsibleState.None, + null as any, + blockMocks.session, + blockMocks.iJob, + blockMocks.imperativeProfile + ); const mySpy = mocked(vscode.window.showInputBox).mockResolvedValue("OWNER"); await jobActions.setOwner(node, blockMocks.testJobsTree); @@ -161,22 +200,16 @@ describe("Jobs Actions Unit Tests - Function setOwner", () => { }); describe("Jobs Actions Unit Tests - Function stopCommand", () => { - function createBlockMocks() { - const session = createISession(); - const iJob = createIJobObject(); - const imperativeProfile = createIProfile(); - - return { - session, - iJob, - imperativeProfile, - }; - } - it("Checking that stop command of Job Node is executed properly", async () => { - createGlobalMocks(); - const blockMocks = createBlockMocks(); - const node = new Job("job", vscode.TreeItemCollapsibleState.None, null, blockMocks.session, blockMocks.iJob, blockMocks.imperativeProfile); + const blockMocks = createGlobalMocks(); + const node = new Job( + "job", + vscode.TreeItemCollapsibleState.None, + null as any, + blockMocks.session, + blockMocks.iJob, + blockMocks.imperativeProfile + ); mocked(zowe.IssueCommand.issueSimple).mockResolvedValueOnce({ success: false, @@ -188,9 +221,15 @@ describe("Jobs Actions Unit Tests - Function stopCommand", () => { expect(mocked(Gui.showMessage).mock.calls[0][0]).toEqual("Command response: fake response"); }); it("Checking failed attempt to issue stop command for Job Node.", async () => { - createGlobalMocks(); - const blockMocks = createBlockMocks(); - const node = new Job("job", vscode.TreeItemCollapsibleState.None, null, blockMocks.session, undefined, blockMocks.imperativeProfile); + const blockMocks = createGlobalMocks(); + const node = new Job( + "job", + vscode.TreeItemCollapsibleState.None, + null as any, + blockMocks.session, + undefined as any, + blockMocks.imperativeProfile + ); mocked(zowe.IssueCommand.issueSimple).mockResolvedValueOnce({ success: false, zosmfResponse: [], @@ -202,22 +241,16 @@ describe("Jobs Actions Unit Tests - Function stopCommand", () => { }); describe("Jobs Actions Unit Tests - Function modifyCommand", () => { - function createBlockMocks() { - const session = createISession(); - const iJob = createIJobObject(); - const imperativeProfile = createIProfile(); - - return { - session, - iJob, - imperativeProfile, - }; - } - it("Checking modification of Job Node", async () => { - createGlobalMocks(); - const blockMocks = createBlockMocks(); - const node = new Job("job", vscode.TreeItemCollapsibleState.None, null, blockMocks.session, blockMocks.iJob, blockMocks.imperativeProfile); + const blockMocks = createGlobalMocks(); + const node = new Job( + "job", + vscode.TreeItemCollapsibleState.None, + null as any, + blockMocks.session, + blockMocks.iJob, + blockMocks.imperativeProfile + ); mocked(vscode.window.showInputBox).mockResolvedValue("modify"); mocked(zowe.IssueCommand.issueSimple).mockResolvedValueOnce({ @@ -230,9 +263,15 @@ describe("Jobs Actions Unit Tests - Function modifyCommand", () => { expect(mocked(Gui.showMessage).mock.calls[0][0]).toEqual("Command response: fake response"); }); it("Checking failed attempt to modify Job Node", async () => { - createGlobalMocks(); - const blockMocks = createBlockMocks(); - const node = new Job("job", vscode.TreeItemCollapsibleState.None, null, blockMocks.session, undefined, blockMocks.imperativeProfile); + const blockMocks = createGlobalMocks(); + const node = new Job( + "job", + vscode.TreeItemCollapsibleState.None, + null as any, + blockMocks.session, + undefined as any, + blockMocks.imperativeProfile + ); mocked(vscode.window.showInputBox).mockResolvedValue("modify"); mocked(zowe.IssueCommand.issueSimple).mockResolvedValueOnce({ success: false, @@ -245,26 +284,17 @@ describe("Jobs Actions Unit Tests - Function modifyCommand", () => { }); describe("Jobs Actions Unit Tests - Function downloadSpool", () => { - function createBlockMocks() { - const session = createISession(); - const iJob = createIJobObject(); - const imperativeProfile = createIProfile(); - const jesApi = createJesApi(imperativeProfile); - bindJesApi(jesApi); - - return { - session, - iJob, - imperativeProfile, - jesApi, - }; - } - it("Checking download of Job Spool", async () => { - createGlobalMocks(); - const blockMocks = createBlockMocks(); + const blockMocks = createGlobalMocks(); const jobs: Job[] = []; - const node = new Job("job", vscode.TreeItemCollapsibleState.None, null, blockMocks.session, blockMocks.iJob, blockMocks.imperativeProfile); + const node = new Job( + "job", + vscode.TreeItemCollapsibleState.None, + null as any, + blockMocks.session, + blockMocks.iJob, + blockMocks.imperativeProfile + ); const fileUri = { fsPath: "/tmp/foo", scheme: "", @@ -288,7 +318,6 @@ describe("Jobs Actions Unit Tests - Function downloadSpool", () => { }); it("Checking failed attempt to download Job Spool", async () => { createGlobalMocks(); - const blockMocks = createBlockMocks(); const fileUri = { fsPath: "/tmp/foo", scheme: "", @@ -298,36 +327,20 @@ describe("Jobs Actions Unit Tests - Function downloadSpool", () => { query: "", }; mocked(Gui.showOpenDialog).mockResolvedValue([fileUri as vscode.Uri]); - await jobActions.downloadSpool(undefined); + await jobActions.downloadSpool(undefined as any); expect(mocked(Gui.errorMessage).mock.calls.length).toBe(1); }); }); describe("Jobs Actions Unit Tests - Function downloadSingleSpool", () => { - function createBlockMocks() { - const session = createISession(); - const iJob = createIJobObject(); - const imperativeProfile = createIProfile(); - const jesApi = createJesApi(imperativeProfile); - bindJesApi(jesApi); - - return { - session, - iJob, - imperativeProfile, - jesApi, - }; - } - it("Checking download of Job Spool", async () => { - createGlobalMocks(); - const blockMocks = createBlockMocks(); + const blockMocks = createGlobalMocks(); const iJobFile = createIJobFile(); const jobs: Job[] = []; const node = new Job( "test:dd - 1", vscode.TreeItemCollapsibleState.None, - null, + null as any, blockMocks.session, blockMocks.iJob, blockMocks.imperativeProfile @@ -358,14 +371,13 @@ describe("Jobs Actions Unit Tests - Function downloadSingleSpool", () => { }); it("should fail to download single spool files if the extender has not implemented the operation", async () => { - createGlobalMocks(); - const blockMocks = createBlockMocks(); + const blockMocks = createGlobalMocks(); const iJobFile = createIJobFile(); const jobs: Job[] = []; const node = new Job( "test:dd - 1", vscode.TreeItemCollapsibleState.None, - null, + null as any, blockMocks.session, blockMocks.iJob, blockMocks.imperativeProfile @@ -393,22 +405,16 @@ describe("Jobs Actions Unit Tests - Function downloadSingleSpool", () => { }); describe("Jobs Actions Unit Tests - Function downloadJcl", () => { - function createBlockMocks() { - const session = createISession(); - const iJob = createIJobObject(); - const imperativeProfile = createIProfile(); - - return { - session, - iJob, - imperativeProfile, - }; - } - it("Checking download of Job JCL", async () => { - createGlobalMocks(); - const blockMocks = createBlockMocks(); - const node = new Job("job", vscode.TreeItemCollapsibleState.None, null, blockMocks.session, blockMocks.iJob, blockMocks.imperativeProfile); + const blockMocks = createGlobalMocks(); + const node = new Job( + "job", + vscode.TreeItemCollapsibleState.None, + null as any, + blockMocks.session, + blockMocks.iJob, + blockMocks.imperativeProfile + ); await jobActions.downloadJcl(node); expect(mocked(zowe.GetJobs.getJclForJob)).toBeCalled(); expect(mocked(vscode.workspace.openTextDocument)).toBeCalled(); @@ -416,8 +422,7 @@ describe("Jobs Actions Unit Tests - Function downloadJcl", () => { }); it("Checking failed attempt to download Job JCL", async () => { createGlobalMocks(); - const blockMocks = createBlockMocks(); - await jobActions.downloadJcl(undefined); + await jobActions.downloadJcl(undefined as any); expect(mocked(Gui.errorMessage)).toBeCalled(); }); }); @@ -430,14 +435,17 @@ describe("Jobs Actions Unit Tests - Function submitJcl", () => { const imperativeProfile = createIProfile(); const datasetSessionNode = createDatasetSessionNode(session, imperativeProfile); const textDocument = createTextDocument("HLQ.TEST.AFILE(mem)", datasetSessionNode); + (textDocument.languageId as any) = "jcl"; const profileInstance = createInstanceOfProfile(imperativeProfile); const jesApi = createJesApi(imperativeProfile); const mockCheckCurrentProfile = jest.fn(); bindJesApi(jesApi); - Object.defineProperty(profileInstance, "loadNamedProfile", { value: jest.fn(), + configurable: true, }); + const errorGuiMsgSpy = jest.spyOn(Gui, "errorMessage"); + const errorLogSpy = jest.spyOn(ZoweLogger, "error"); return { session, @@ -450,6 +458,8 @@ describe("Jobs Actions Unit Tests - Function submitJcl", () => { profileInstance, jesApi, mockCheckCurrentProfile, + errorLogSpy, + errorGuiMsgSpy, }; } @@ -463,15 +473,44 @@ describe("Jobs Actions Unit Tests - Function submitJcl", () => { resolve(blockMocks.datasetSessionNode.label); }) ); + const mockFile = { + path: "/fake/path/file.txt", + } as vscode.Uri; blockMocks.testDatasetTree.getChildren.mockResolvedValueOnce([ - new ZoweDatasetNode("node", vscode.TreeItemCollapsibleState.None, blockMocks.datasetSessionNode, null), + new ZoweDatasetNode("node", vscode.TreeItemCollapsibleState.None, blockMocks.datasetSessionNode, null as any), blockMocks.datasetSessionNode, ]); + const commandSpy = jest.spyOn(vscode.commands, "executeCommand"); activeTextEditorDocument.mockReturnValue(blockMocks.textDocument); const submitJclSpy = jest.spyOn(blockMocks.jesApi, "submitJcl"); submitJclSpy.mockClear(); submitJclSpy.mockResolvedValueOnce(blockMocks.iJob); - await dsActions.submitJcl(blockMocks.testDatasetTree); + await dsActions.submitJcl(blockMocks.testDatasetTree, mockFile); + + expect(commandSpy).toBeCalled(); + expect(submitJclSpy).toBeCalled(); + expect(mocked(Gui.showMessage)).toBeCalled(); + expect(mocked(Gui.showMessage).mock.calls.length).toBe(1); + expect(mocked(Gui.showMessage).mock.calls[0][0]).toEqual( + "Job submitted [JOB1234](command:zowe.jobs.setJobSpool?%5B%22sestest%22%2C%22JOB1234%22%5D)" + ); + commandSpy.mockClear(); + }); + it("Checking submit of JCL file from VSC explorer tree", async () => { + createGlobalMocks(); + const blockMocks: any = createBlockMocks(); + mocked(zowe.ZosmfSession.createSessCfgFromArgs).mockReturnValue(blockMocks.session); + mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); + mocked(vscode.window.showQuickPick).mockReturnValueOnce(Promise.resolve(blockMocks.datasetSessionNode.label)); + blockMocks.testDatasetTree.getChildren.mockResolvedValueOnce([ + new ZoweDatasetNode("node", vscode.TreeItemCollapsibleState.None, blockMocks.datasetSessionNode, null as any), + blockMocks.datasetSessionNode, + ]); + activeTextEditorDocument.mockReturnValue(blockMocks.textDocument); + const submitJclSpy = jest.spyOn(blockMocks.jesApi, "submitJcl"); + submitJclSpy.mockClear(); + submitJclSpy.mockResolvedValueOnce(blockMocks.iJob); + await dsActions.submitJcl(blockMocks.testDatasetTree, undefined); expect(submitJclSpy).toBeCalled(); expect(mocked(Gui.showMessage)).toBeCalled(); @@ -492,14 +531,14 @@ describe("Jobs Actions Unit Tests - Function submitJcl", () => { }) ); blockMocks.testDatasetTree.getChildren.mockResolvedValueOnce([ - new ZoweDatasetNode("node", vscode.TreeItemCollapsibleState.None, blockMocks.datasetSessionNode, null), + new ZoweDatasetNode("node", vscode.TreeItemCollapsibleState.None, blockMocks.datasetSessionNode, null as any), blockMocks.datasetSessionNode, ]); activeTextEditorDocument.mockReturnValue(blockMocks.textDocument); const submitJclSpy = jest.spyOn(blockMocks.jesApi, "submitJcl"); submitJclSpy.mockClear(); submitJclSpy.mockResolvedValueOnce(blockMocks.iJob); - await dsActions.submitJcl(blockMocks.testDatasetTree); + await dsActions.submitJcl(blockMocks.testDatasetTree, undefined); expect(submitJclSpy).toBeCalled(); expect(mocked(Gui.showMessage)).toBeCalled(); @@ -508,26 +547,40 @@ describe("Jobs Actions Unit Tests - Function submitJcl", () => { "Job submitted [JOB1234](command:zowe.jobs.setJobSpool?%5B%22sestest%22%2C%22JOB1234%22%5D)" ); }); + it("Checking failure of submitting JCL via command palette if not active text editor", async () => { + createGlobalMocks(); + const blockMocks = createBlockMocks(); + Object.defineProperty(vscode.window, "activeTextEditor", { + value: undefined, + configurable: true, + }); + + await dsActions.submitJcl(blockMocks.testDatasetTree, undefined); + + const errorMsg = "No editor with a document that could be submitted as JCL is currently open."; + expect(blockMocks.errorLogSpy).toBeCalledWith(errorMsg); + expect(blockMocks.errorGuiMsgSpy).toBeCalledWith(errorMsg); + }); - it("Checking failed attempt to submit of active text editor content as JCL", async () => { + it("Checking failed attempt to submit of active text editor content as JCL without profile chosen from quickpick", async () => { createGlobalMocks(); const blockMocks = createBlockMocks(); mocked(zowe.ZosmfSession.createSessCfgFromArgs).mockReturnValue(blockMocks.session.ISession); mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - mocked(vscode.window.showQuickPick).mockResolvedValueOnce(null); // Here we imitate the case when no profile was selected + mocked(vscode.window.showQuickPick).mockResolvedValueOnce(undefined); // Here we imitate the case when no profile was selected blockMocks.testDatasetTree.getChildren.mockResolvedValueOnce([ - new ZoweDatasetNode("node", vscode.TreeItemCollapsibleState.None, blockMocks.datasetSessionNode, null), + new ZoweDatasetNode("node", vscode.TreeItemCollapsibleState.None, blockMocks.datasetSessionNode, null as any), blockMocks.datasetSessionNode, ]); activeTextEditorDocument.mockReturnValue(blockMocks.textDocument); + const messageSpy = jest.spyOn(Gui, "infoMessage"); const submitJclSpy = jest.spyOn(blockMocks.jesApi, "submitJcl"); submitJclSpy.mockClear(); - await dsActions.submitJcl(blockMocks.testDatasetTree); + await dsActions.submitJcl(blockMocks.testDatasetTree, undefined); expect(submitJclSpy).not.toBeCalled(); - expect(mocked(ZoweLogger.error)).toBeCalled(); - expect(mocked(ZoweLogger.error).mock.calls[0][0]).toEqual("Session for submitting JCL was null or undefined!"); + expect(messageSpy).toBeCalledWith("Operation Cancelled"); }); it("Checking API error on submit of active text editor content as JCL", async () => { @@ -541,7 +594,7 @@ describe("Jobs Actions Unit Tests - Function submitJcl", () => { }) ); blockMocks.testDatasetTree.getChildren.mockResolvedValueOnce([ - new ZoweDatasetNode("node", vscode.TreeItemCollapsibleState.None, blockMocks.datasetSessionNode, null), + new ZoweDatasetNode("node", vscode.TreeItemCollapsibleState.None, blockMocks.datasetSessionNode, null as any), blockMocks.datasetSessionNode, ]); activeTextEditorDocument.mockReturnValue(blockMocks.textDocument); @@ -549,7 +602,7 @@ describe("Jobs Actions Unit Tests - Function submitJcl", () => { submitJclSpy.mockClear(); const testError = new Error("submitJcl failed"); submitJclSpy.mockRejectedValueOnce(testError); - await dsActions.submitJcl(blockMocks.testDatasetTree); + await dsActions.submitJcl(blockMocks.testDatasetTree, undefined); expect(submitJclSpy).toBeCalled(); expect(mocked(Gui.errorMessage)).toBeCalled(); @@ -583,9 +636,9 @@ describe("Jobs Actions Unit Tests - Function submitMember", () => { createGlobalMocks(); const blockMocks = createBlockMocks(); mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - const subNode = new ZoweDatasetNode("dataset", vscode.TreeItemCollapsibleState.Collapsed, blockMocks.datasetSessionNode, null); + const subNode = new ZoweDatasetNode("dataset", vscode.TreeItemCollapsibleState.Collapsed, blockMocks.datasetSessionNode, null as any); subNode.contextValue = globals.DS_PDS_CONTEXT; - const member = new ZoweDatasetNode("member", vscode.TreeItemCollapsibleState.None, subNode, null); + const member = new ZoweDatasetNode("member", vscode.TreeItemCollapsibleState.None, subNode, null as any); member.contextValue = globals.DS_MEMBER_CONTEXT; const submitJobSpy = jest.spyOn(blockMocks.jesApi, "submitJob"); submitJobSpy.mockResolvedValueOnce(blockMocks.iJob); @@ -613,9 +666,9 @@ describe("Jobs Actions Unit Tests - Function submitMember", () => { }; }), }); - const subNode = new ZoweDatasetNode("dataset", vscode.TreeItemCollapsibleState.Collapsed, blockMocks.datasetSessionNode, null); + const subNode = new ZoweDatasetNode("dataset", vscode.TreeItemCollapsibleState.Collapsed, blockMocks.datasetSessionNode, null as any); subNode.contextValue = globals.DS_PDS_CONTEXT; - const member = new ZoweDatasetNode("member", vscode.TreeItemCollapsibleState.None, subNode, null); + const member = new ZoweDatasetNode("member", vscode.TreeItemCollapsibleState.None, subNode, null as any); member.contextValue = globals.DS_MEMBER_CONTEXT; const submitJobSpy = jest.spyOn(blockMocks.jesApi, "submitJob"); submitJobSpy.mockResolvedValueOnce(blockMocks.iJob); @@ -632,7 +685,7 @@ describe("Jobs Actions Unit Tests - Function submitMember", () => { createGlobalMocks(); const blockMocks = createBlockMocks(); mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - const dataset = new ZoweDatasetNode("dataset", vscode.TreeItemCollapsibleState.Collapsed, blockMocks.datasetSessionNode, null); + const dataset = new ZoweDatasetNode("dataset", vscode.TreeItemCollapsibleState.Collapsed, blockMocks.datasetSessionNode, null as any); dataset.contextValue = globals.DS_DS_CONTEXT; const submitJobSpy = jest.spyOn(blockMocks.jesApi, "submitJob"); submitJobSpy.mockClear(); @@ -650,11 +703,11 @@ describe("Jobs Actions Unit Tests - Function submitMember", () => { createGlobalMocks(); const blockMocks = createBlockMocks(); mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - const favProfileNode = new ZoweDatasetNode("test", vscode.TreeItemCollapsibleState.Collapsed, blockMocks.datasetSessionNode, null); + const favProfileNode = new ZoweDatasetNode("test", vscode.TreeItemCollapsibleState.Collapsed, blockMocks.datasetSessionNode, null as any); favProfileNode.contextValue = globals.FAV_PROFILE_CONTEXT; - const favoriteSubNode = new ZoweDatasetNode("TEST.JCL", vscode.TreeItemCollapsibleState.Collapsed, favProfileNode, null); + const favoriteSubNode = new ZoweDatasetNode("TEST.JCL", vscode.TreeItemCollapsibleState.Collapsed, favProfileNode, null as any); favoriteSubNode.contextValue = globals.DS_PDS_CONTEXT + globals.FAV_SUFFIX; - const favoriteMember = new ZoweDatasetNode(globals.DS_PDS_CONTEXT, vscode.TreeItemCollapsibleState.Collapsed, favoriteSubNode, null); + const favoriteMember = new ZoweDatasetNode(globals.DS_PDS_CONTEXT, vscode.TreeItemCollapsibleState.Collapsed, favoriteSubNode, null as any); favoriteMember.contextValue = globals.DS_MEMBER_CONTEXT; const submitJobSpy = jest.spyOn(blockMocks.jesApi, "submitJob"); submitJobSpy.mockClear(); @@ -672,9 +725,9 @@ describe("Jobs Actions Unit Tests - Function submitMember", () => { createGlobalMocks(); const blockMocks = createBlockMocks(); mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - const favProfileNode = new ZoweDatasetNode("test", vscode.TreeItemCollapsibleState.Collapsed, blockMocks.datasetSessionNode, null); + const favProfileNode = new ZoweDatasetNode("test", vscode.TreeItemCollapsibleState.Collapsed, blockMocks.datasetSessionNode, null as any); favProfileNode.contextValue = globals.FAV_PROFILE_CONTEXT; - const favoriteDataset = new ZoweDatasetNode("TEST.JCL", vscode.TreeItemCollapsibleState.Collapsed, favProfileNode, null); + const favoriteDataset = new ZoweDatasetNode("TEST.JCL", vscode.TreeItemCollapsibleState.Collapsed, favProfileNode, null as any); favoriteDataset.contextValue = globals.DS_DS_CONTEXT + globals.FAV_SUFFIX; const submitJobSpy = jest.spyOn(blockMocks.jesApi, "submitJob"); submitJobSpy.mockClear(); @@ -692,9 +745,9 @@ describe("Jobs Actions Unit Tests - Function submitMember", () => { createGlobalMocks(); const blockMocks = createBlockMocks(); mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - const corruptedNode = new ZoweDatasetNode("gibberish", vscode.TreeItemCollapsibleState.Collapsed, blockMocks.datasetSessionNode, null); + const corruptedNode = new ZoweDatasetNode("gibberish", vscode.TreeItemCollapsibleState.Collapsed, blockMocks.datasetSessionNode, null as any); corruptedNode.contextValue = "gibberish"; - const corruptedSubNode = new ZoweDatasetNode("gibberishmember", vscode.TreeItemCollapsibleState.Collapsed, corruptedNode, null); + const corruptedSubNode = new ZoweDatasetNode("gibberishmember", vscode.TreeItemCollapsibleState.Collapsed, corruptedNode, null as any); const submitJobSpy = jest.spyOn(blockMocks.jesApi, "submitJob"); submitJobSpy.mockClear(); submitJobSpy.mockResolvedValueOnce(blockMocks.iJob); @@ -715,18 +768,19 @@ describe("Jobs Actions Unit Tests - Function submitMember", () => { const blockMocks = createBlockMocks(); mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - const dataset = new ZoweDatasetNode("TESTUSER.DATASET", vscode.TreeItemCollapsibleState.Collapsed, blockMocks.datasetSessionNode, null); + const dataset = new ZoweDatasetNode( + "TESTUSER.DATASET", + vscode.TreeItemCollapsibleState.Collapsed, + blockMocks.datasetSessionNode, + null as any + ); dataset.contextValue = globals.DS_DS_CONTEXT; for (let o = 0; o < sharedUtils.JOB_SUBMIT_DIALOG_OPTS.length; o++) { const option = sharedUtils.JOB_SUBMIT_DIALOG_OPTS[o]; - jest.spyOn(vscode.workspace, "getConfiguration").mockReturnValueOnce({ - has: jest.fn(), - get: (_setting) => { - return option; - }, - inspect: jest.fn(), - update: jest.fn(), + Object.defineProperty(vscode.workspace, "getConfiguration", { + value: jest.fn().mockImplementation(() => new Map([["zowe.jobs.confirmSubmission", option]])), + configurable: true, }); if (option === sharedUtils.JOB_SUBMIT_DIALOG_OPTS[sharedUtils.JobSubmitDialogOpts.Disabled]) { @@ -756,7 +810,7 @@ describe("Jobs Actions Unit Tests - Function submitMember", () => { } // Test for "Cancel" or closing the dialog - mocked(Gui.warningMessage).mockReturnValueOnce(undefined); + mocked(Gui.warningMessage).mockReturnValueOnce(undefined as any); await dsActions.submitMember(dataset); expect(mocked(Gui.warningMessage)).toBeCalledWith("Are you sure you want to submit the following job?\n\n" + dataset.getLabel(), { items: [{ title: "Submit" }], @@ -888,7 +942,7 @@ describe("Jobs Actions Unit Tests - Function getSpoolContent", () => { vscode.TreeItemCollapsibleState.None, createJobFavoritesNode(), createISessionWithoutCredentials(), - null, + null as any, createIProfile() ); jest.spyOn(Spool.prototype, "getProfile").mockReturnValue({ @@ -1041,7 +1095,7 @@ describe("Jobs Actions Unit Tests - Function refreshJobsServer", () => { const job = new Job( "jobtest", vscode.TreeItemCollapsibleState.Expanded, - null, + null as any, blockMocks.session, blockMocks.iJob, blockMocks.imperativeProfile @@ -1058,7 +1112,7 @@ describe("Jobs Actions Unit Tests - Function refreshJobsServer", () => { const job = new Job( "jobtest", vscode.TreeItemCollapsibleState.Expanded, - null, + null as any, blockMocks.session, blockMocks.iJob, blockMocks.imperativeProfile @@ -1194,7 +1248,7 @@ describe("job deletion command", () => { const jobsProvider = createJobsTree(session, job, profile, createTreeView()); jobsProvider.delete.mockResolvedValueOnce(Promise.resolve()); - const jobNode = new Job("jobtest", vscode.TreeItemCollapsibleState.Expanded, null, session, job, profile); + const jobNode = new Job("jobtest", vscode.TreeItemCollapsibleState.Expanded, null as any, session, job, profile); await jobActions.deleteCommand(jobsProvider, jobNode); @@ -1208,8 +1262,8 @@ describe("job deletion command", () => { const jobsProvider = createJobsTree(session, job, profile, createTreeView()); jobsProvider.mSessionNodes.push(job2); jobsProvider.delete.mockResolvedValue(Promise.resolve()); - const jobNode1 = new Job("jobtest1", vscode.TreeItemCollapsibleState.Expanded, null, session, job, profile); - const jobNode2 = new Job("jobtest2", vscode.TreeItemCollapsibleState.Expanded, null, session, job2, profile); + const jobNode1 = new Job("jobtest1", vscode.TreeItemCollapsibleState.Expanded, null as any, session, job, profile); + const jobNode2 = new Job("jobtest2", vscode.TreeItemCollapsibleState.Expanded, null as any, session, job2, profile); const jobs = [jobNode1, jobNode2]; // act await jobActions.deleteCommand(jobsProvider, undefined, jobs); @@ -1223,7 +1277,7 @@ describe("job deletion command", () => { const jobsProvider = createJobsTree(session, job, profile, createTreeView()); jobsProvider.delete.mockResolvedValueOnce(Promise.resolve()); - const jobNode = new Job("jobtest", vscode.TreeItemCollapsibleState.Expanded, null, session, job, profile); + const jobNode = new Job("jobtest", vscode.TreeItemCollapsibleState.Expanded, null as any, session, job, profile); await jobActions.deleteCommand(jobsProvider, jobNode); expect(mocked(jobsProvider.delete)).not.toBeCalled(); @@ -1234,7 +1288,7 @@ describe("job deletion command", () => { const jobsProvider = createJobsTree(session, job, profile, createTreeView()); jobsProvider.delete.mockResolvedValueOnce(Promise.reject(new Error("something went wrong!"))); - const jobNode = new Job("jobtest", vscode.TreeItemCollapsibleState.Expanded, null, session, job, profile); + const jobNode = new Job("jobtest", vscode.TreeItemCollapsibleState.Expanded, null as any, session, job, profile); // act await jobActions.deleteCommand(jobsProvider, jobNode); // assert @@ -1247,7 +1301,7 @@ describe("job deletion command", () => { const jobsProvider = createJobsTree(session, job, profile, createTreeView()); jobsProvider.delete.mockResolvedValueOnce(Promise.resolve()); - const jobNode = new Job("jobtest", vscode.TreeItemCollapsibleState.Expanded, null, session, job, profile); + const jobNode = new Job("jobtest", vscode.TreeItemCollapsibleState.Expanded, null as any, session, job, profile); jobsProvider.getTreeView.mockReturnValueOnce({ ...jobsProvider.getTreeView(), selection: [jobNode] }); // act await jobActions.deleteCommand(jobsProvider, undefined); @@ -1262,7 +1316,7 @@ describe("Job Actions Unit Tests - Misc. functions", () => { const session = createISession(); const profile = createIProfile(); const job = createIJobObject(); - const jobNode = new Job("job", vscode.TreeItemCollapsibleState.None, null, session, job, profile); + const jobNode = new Job("job", vscode.TreeItemCollapsibleState.None, null as any, session, job, profile); it("refreshJob works as intended", () => { const jobsProvider = createJobsTree(session, job, profile, createTreeView()); @@ -1300,3 +1354,83 @@ describe("Job Actions Unit Tests - Misc. functions", () => { expect(statusMsgSpy).toHaveBeenCalledWith(`$(sync~spin) Polling: ${testDoc.fileName}...`); }); }); +describe("sortJobs function", () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + it("sort by name if same sort by increasing id", async () => { + const globalMocks = createGlobalMocks(); + const testtree = new ZosJobsProvider(); + const expected = new ZosJobsProvider(); + testtree.mSessionNodes[0].sort = { + method: JobSortOpts.Id, + direction: SortDirection.Ascending, + }; + testtree.mSessionNodes[0].children = [...[globalMocks.mockJobArray[2], globalMocks.mockJobArray[1], globalMocks.mockJobArray[0]]]; + expected.mSessionNodes[0].children = [...[globalMocks.mockJobArray[1], globalMocks.mockJobArray[0], globalMocks.mockJobArray[2]]]; + jest.spyOn(Gui, "showQuickPick").mockResolvedValueOnce({ label: "$(case-sensitive) Job Name" }); + const sortbynamespy = jest.spyOn(ZosJobsProvider.prototype, "sortBy"); + //act + await jobActions.sortJobs(testtree.mSessionNodes[0], testtree); + //asert + expect(sortbynamespy).toBeCalledWith(testtree.mSessionNodes[0]); + expect(sortbynamespy).toHaveBeenCalled(); + expect(sortbynamespy.mock.calls[0][0].children).toStrictEqual(expected.mSessionNodes[0].children); + }); + it("sorts by increasing order of id", async () => { + const globalMocks = createGlobalMocks(); + const testtree = new ZosJobsProvider(); + const expected = new ZosJobsProvider(); + testtree.mSessionNodes[0].sort = { + method: JobSortOpts.Id, + direction: SortDirection.Ascending, + }; + testtree.mSessionNodes[0].children = [...[globalMocks.mockJobArray[2], globalMocks.mockJobArray[1], globalMocks.mockJobArray[0]]]; + expected.mSessionNodes[0].children = [...[globalMocks.mockJobArray[1], globalMocks.mockJobArray[0], globalMocks.mockJobArray[2]]]; + const sortbyidspy = jest.spyOn(ZosJobsProvider.prototype, "sortBy"); + jest.spyOn(Gui, "showQuickPick").mockResolvedValueOnce({ label: "$(list-ordered) Job ID (default)" }); + //act + await jobActions.sortJobs(testtree.mSessionNodes[0], testtree); + //asert + expect(sortbyidspy).toBeCalledWith(testtree.mSessionNodes[0]); + expect(sortbyidspy).toHaveBeenCalled(); + expect(sortbyidspy.mock.calls[0][0].children).toStrictEqual(expected.mSessionNodes[0].children); + }); + it("sort by retcode if same sort by increasing id", async () => { + const globalMocks = createGlobalMocks(); + const testtree = new ZosJobsProvider(); + const expected = new ZosJobsProvider(); + testtree.mSessionNodes[0].sort = { + method: JobSortOpts.Id, + direction: SortDirection.Ascending, + }; + testtree.mSessionNodes[0].children = [...[globalMocks.mockJobArray[2], globalMocks.mockJobArray[1], globalMocks.mockJobArray[0]]]; + expected.mSessionNodes[0].children = [...[globalMocks.mockJobArray[0], globalMocks.mockJobArray[1], globalMocks.mockJobArray[2]]]; + const sortbyretcodespy = jest.spyOn(ZosJobsProvider.prototype, "sortBy"); + jest.spyOn(Gui, "showQuickPick").mockResolvedValueOnce({ label: "$(symbol-numeric) Return Code" }); + + //act + await jobActions.sortJobs(testtree.mSessionNodes[0], testtree); + //asert + expect(sortbyretcodespy).toBeCalledWith(testtree.mSessionNodes[0]); + expect(sortbyretcodespy).toHaveBeenCalled(); + expect(sortbyretcodespy.mock.calls[0][0].children).toStrictEqual(expected.mSessionNodes[0].children); + }); + + it("updates sort options after selecting sort direction; returns user to sort selection", async () => { + const globalMocks = createGlobalMocks(); + const testtree = new ZosJobsProvider(); + testtree.mSessionNodes[0].sort = { + method: JobSortOpts.Id, + direction: SortDirection.Ascending, + }; + testtree.mSessionNodes[0].children = [globalMocks.mockJobArray[0]]; + const jobsSortBy = jest.spyOn(ZosJobsProvider.prototype, "sortBy"); + const quickPickSpy = jest.spyOn(Gui, "showQuickPick").mockResolvedValueOnce({ label: "$(fold) Sort Direction" }); + quickPickSpy.mockResolvedValueOnce("Descending" as any); + await jobActions.sortJobs(testtree.mSessionNodes[0], testtree); + expect(testtree.mSessionNodes[0].sort.direction).toBe(SortDirection.Descending); + expect(quickPickSpy).toHaveBeenCalledTimes(3); + expect(jobsSortBy).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/zowe-explorer/__tests__/__unit__/shared/HistoryView.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/shared/HistoryView.unit.test.ts new file mode 100644 index 0000000000..6c9e3f2ef1 --- /dev/null +++ b/packages/zowe-explorer/__tests__/__unit__/shared/HistoryView.unit.test.ts @@ -0,0 +1,255 @@ +/** + * 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 * as vscode from "vscode"; +import { HistoryView } from "../../../src/shared/HistoryView"; +import { createDatasetSessionNode, createDatasetTree } from "../../../__mocks__/mockCreators/datasets"; +import { createUSSSessionNode, createUSSTree } from "../../../__mocks__/mockCreators/uss"; +import { + createIProfile, + createISession, + createISessionWithoutCredentials, + createInstanceOfProfile, + createTreeView, +} from "../../../__mocks__/mockCreators/shared"; +import { createIJobObject, createJobsTree } from "../../../__mocks__/mockCreators/jobs"; +import { Gui } from "@zowe/zowe-explorer-api"; +import { Profiles } from "../../../src/Profiles"; +import { ZoweLocalStorage } from "../../../src/utils/ZoweLocalStorage"; + +async function initializeHistoryViewMock(blockMocks: any, globalMocks: any): Promise { + return new HistoryView( + { + extensionPath: "", + } as any, + { + ds: await createDatasetTree(blockMocks.datasetSessionNode, globalMocks.treeView), + uss: createUSSTree([blockMocks.datasetSessionNode], [globalMocks.testSession], globalMocks.treeView), + jobs: await createJobsTree(globalMocks.testSession, createIJobObject(), globalMocks.testProfile, createTreeView()), + } as any + ); +} + +function createGlobalMocks(): any { + const globalMocks = { + session: createISessionWithoutCredentials(), + testSession: createISession(), + treeView: createTreeView(), + imperativeProfile: createIProfile(), + }; + Object.defineProperty(Gui, "showMessage", { value: jest.fn(), configurable: true }); + Object.defineProperty(Gui, "resolveQuickPick", { value: jest.fn(), configurable: true }); + Object.defineProperty(Gui, "createQuickPick", { value: jest.fn(), configurable: true }); + Object.defineProperty(Profiles, "getInstance", { + value: jest.fn().mockReturnValue(createInstanceOfProfile(globalMocks.imperativeProfile)), + configurable: true, + }); + Object.defineProperty(vscode.window, "createTreeView", { value: jest.fn().mockReturnValueOnce(globalMocks.treeView), configurable: true }); + Object.defineProperty(ZoweLocalStorage, "storage", { + value: { + get: () => ({ persistence: true, favorites: [], history: [], sessions: ["zosmf"], searchHistory: [], fileHistory: [] }), + update: jest.fn(), + keys: () => [], + }, + configurable: true, + }); + + return globalMocks; +} + +function createBlockMocks(globalMocks: any): any { + return { + datasetSessionNode: createDatasetSessionNode(globalMocks.session, globalMocks.imperativeProfile), + ussSessionNode: createUSSSessionNode(globalMocks.session, globalMocks.imperativeProfile), + }; +} + +describe("HistoryView Unit Tests", () => { + describe("constructor", () => { + it("should create the webview instance and initialize", () => { + const historyView = new HistoryView( + { + extensionPath: "", + } as any, + { test: "test" } as any + ); + expect(historyView["treeProviders"]).toEqual({ test: "test" }); + expect(historyView["currentSelection"]).toEqual({ ds: "search", uss: "search", jobs: "search" }); + }); + }); + + describe("onDidReceiveMessage", () => { + it("should handle the case where 'refresh' is the command sent", async () => { + const globalMocks = await createGlobalMocks(); + const blockMocks = createBlockMocks(globalMocks); + const historyView = await initializeHistoryViewMock(blockMocks, globalMocks); + const postMessageSpy = jest.spyOn(historyView.panel.webview, "postMessage"); + jest.spyOn(historyView as any, "getHistoryData").mockReturnValue([]); + await historyView["onDidReceiveMessage"]({ command: "refresh", attrs: { type: "uss" } }); + expect(postMessageSpy).toBeCalledTimes(1); + expect(historyView["currentTab"]).toEqual("uss-panel-tab"); + }); + + it("should handle the case where 'ready' is the command sent", async () => { + const globalMocks = await createGlobalMocks(); + const blockMocks = createBlockMocks(globalMocks); + const historyView = await initializeHistoryViewMock(blockMocks, globalMocks); + const postMessageSpy = jest.spyOn(historyView.panel.webview, "postMessage"); + jest.spyOn(historyView as any, "getHistoryData").mockReturnValue([]); + await historyView["onDidReceiveMessage"]({ command: "ready" }); + expect(postMessageSpy).toBeCalledWith({ + ds: [], + uss: [], + jobs: [], + tab: undefined, + selection: { + ds: "search", + jobs: "search", + uss: "search", + }, + }); + }); + + it("should handle the case where 'show-error' is the command sent", async () => { + const globalMocks = await createGlobalMocks(); + const blockMocks = createBlockMocks(globalMocks); + const historyView = await initializeHistoryViewMock(blockMocks, globalMocks); + const errorMessageSpy = jest.spyOn(Gui, "errorMessage"); + await historyView["onDidReceiveMessage"]({ command: "show-error", attrs: { errorMsg: "test error" } }); + expect(errorMessageSpy).toBeCalledWith("test error"); + }); + + it("should handle the case where 'update-selection' is the command sent", async () => { + const globalMocks = await createGlobalMocks(); + const blockMocks = createBlockMocks(globalMocks); + const historyView = await initializeHistoryViewMock(blockMocks, globalMocks); + await historyView["onDidReceiveMessage"]({ command: "update-selection", attrs: { type: "uss", selection: "favorites" } }); + expect(historyView["currentSelection"]).toEqual({ ds: "search", jobs: "search", uss: "favorites" }); + }); + + it("should handle the case where 'add-item' is the command sent", async () => { + const globalMocks = await createGlobalMocks(); + const blockMocks = createBlockMocks(globalMocks); + const historyView = await initializeHistoryViewMock(blockMocks, globalMocks); + jest.spyOn(Gui, "showInputBox").mockResolvedValue("test"); + const addSearchHistorySpy = jest.spyOn(historyView["treeProviders"].uss, "addSearchHistory"); + jest.spyOn(historyView as any, "refreshView").mockImplementation(); + await historyView["onDidReceiveMessage"]({ command: "add-item", attrs: { type: "uss" } }); + expect(historyView["currentSelection"]).toEqual({ ds: "search", jobs: "search", uss: "search" }); + expect(addSearchHistorySpy).toBeCalledWith("test"); + }); + + it("should handle the case where 'remove-item' is the command sent and the selection is 'search'", async () => { + const globalMocks = await createGlobalMocks(); + const blockMocks = createBlockMocks(globalMocks); + const historyView = await initializeHistoryViewMock(blockMocks, globalMocks); + jest.spyOn(historyView as any, "refreshView").mockImplementation(); + const removeSearchHistorySpy = jest.spyOn(historyView["treeProviders"].ds as any, "removeSearchHistory"); + await historyView["onDidReceiveMessage"]({ + command: "remove-item", + attrs: { type: "ds", selection: "search", selectedItems: { test: "test1" } }, + }); + expect(historyView["currentSelection"]).toEqual({ ds: "search", jobs: "search", uss: "search" }); + expect(removeSearchHistorySpy).toBeCalledWith("test"); + }); + + it("should handle the case where 'remove-item' is the command sent and the selection is 'fileHistory'", async () => { + const globalMocks = await createGlobalMocks(); + const blockMocks = createBlockMocks(globalMocks); + const historyView = await initializeHistoryViewMock(blockMocks, globalMocks); + jest.spyOn(historyView as any, "refreshView").mockImplementation(); + const removeFileHistorySpy = jest.spyOn(historyView["treeProviders"].ds, "removeFileHistory"); + await historyView["onDidReceiveMessage"]({ + command: "remove-item", + attrs: { type: "ds", selection: "fileHistory", selectedItems: { test: "test1" } }, + }); + expect(historyView["currentSelection"]).toEqual({ ds: "search", jobs: "search", uss: "search" }); + expect(removeFileHistorySpy).toBeCalledWith("test"); + }); + + it("should handle the case where 'remove-item' is the command sent and the selection is not supported", async () => { + const globalMocks = await createGlobalMocks(); + const blockMocks = createBlockMocks(globalMocks); + const historyView = await initializeHistoryViewMock(blockMocks, globalMocks); + jest.spyOn(historyView as any, "refreshView").mockImplementation(); + const showMessageSpy = jest.spyOn(Gui, "showMessage"); + await historyView["onDidReceiveMessage"]({ + command: "remove-item", + attrs: { type: "ds", selection: "favorites", selectedItems: { test: "test1" } }, + }); + expect(historyView["currentSelection"]).toEqual({ ds: "search", jobs: "search", uss: "search" }); + expect(showMessageSpy).toBeCalledTimes(1); + }); + + it("should handle the case where 'clear-all' is the command sent and the selection is 'search'", async () => { + const globalMocks = await createGlobalMocks(); + const blockMocks = createBlockMocks(globalMocks); + const historyView = await initializeHistoryViewMock(blockMocks, globalMocks); + jest.spyOn(historyView as any, "refreshView").mockImplementation(); + jest.spyOn(Gui, "showMessage").mockResolvedValue("Yes"); + + const resetSearchHistorySpy = jest.spyOn(historyView["treeProviders"].ds as any, "resetSearchHistory"); + await historyView["onDidReceiveMessage"]({ + command: "clear-all", + attrs: { type: "ds", selection: "search" }, + }); + expect(historyView["currentSelection"]).toEqual({ ds: "search", jobs: "search", uss: "search" }); + expect(resetSearchHistorySpy).toBeCalledTimes(1); + }); + + it("should handle the case where 'clear-all' is the command sent and the selection is 'fileHistory'", async () => { + const globalMocks = await createGlobalMocks(); + const blockMocks = createBlockMocks(globalMocks); + const historyView = await initializeHistoryViewMock(blockMocks, globalMocks); + jest.spyOn(historyView as any, "refreshView").mockImplementation(); + jest.spyOn(Gui, "showMessage").mockResolvedValue("Yes"); + + const resetFileHistorySpy = jest.spyOn(historyView["treeProviders"].ds as any, "resetFileHistory"); + await historyView["onDidReceiveMessage"]({ + command: "clear-all", + attrs: { type: "ds", selection: "fileHistory" }, + }); + expect(historyView["currentSelection"]).toEqual({ ds: "search", jobs: "search", uss: "search" }); + expect(resetFileHistorySpy).toBeCalledTimes(1); + }); + + it("should handle the case where 'clear-all' is the command sent and the selection is 'fileHistory'", async () => { + const globalMocks = await createGlobalMocks(); + const blockMocks = createBlockMocks(globalMocks); + const historyView = await initializeHistoryViewMock(blockMocks, globalMocks); + jest.spyOn(historyView as any, "refreshView").mockImplementation(); + jest.spyOn(Gui, "showMessage").mockResolvedValueOnce("Yes"); + const showMessageSpy = jest.spyOn(Gui, "showMessage"); + await historyView["onDidReceiveMessage"]({ + command: "clear-all", + attrs: { type: "ds", selection: "favorites" }, + }); + expect(historyView["currentSelection"]).toEqual({ ds: "search", jobs: "search", uss: "search" }); + expect(showMessageSpy).toBeCalledTimes(2); + }); + }); + + describe("getHistoryData", () => { + it("should get the latest history data", async () => { + const globalMocks = await createGlobalMocks(); + const blockMocks = createBlockMocks(globalMocks); + const historyView = await initializeHistoryViewMock(blockMocks, globalMocks); + + expect(historyView["getHistoryData"]("ds")).toEqual({ + dsTemplates: undefined, + favorites: undefined, + fileHistory: [], + search: undefined, + sessions: undefined, + }); + }); + }); +}); diff --git a/packages/zowe-explorer/__tests__/__unit__/shared/TreeProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/shared/TreeProvider.unit.test.ts new file mode 100644 index 0000000000..7b1f542251 --- /dev/null +++ b/packages/zowe-explorer/__tests__/__unit__/shared/TreeProvider.unit.test.ts @@ -0,0 +1,43 @@ +/** + * 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 { createTreeView } from "../../../__mocks__/mockCreators/shared"; +import { TreeProviders } from "../../../src/shared/TreeProviders"; + +describe("TreeProvider Unit Tests - Function getters", () => { + it("should retrieve the ds provider", async () => { + const mockTree = createTreeView("ds"); + await TreeProviders.initializeProviders({} as any, { + ds: jest.fn(() => mockTree) as any, + uss: jest.fn(), + job: jest.fn(), + }); + expect(TreeProviders.ds).toEqual(mockTree); + }); + it("should retrieve the uss provider", async () => { + const mockTree = createTreeView("uss"); + await TreeProviders.initializeProviders({} as any, { + ds: jest.fn(), + uss: jest.fn(() => mockTree) as any, + job: jest.fn(), + }); + expect(TreeProviders.uss).toEqual(mockTree); + }); + it("should retrieve the uss provider", async () => { + const mockTree = createTreeView("job"); + await TreeProviders.initializeProviders({} as any, { + ds: jest.fn(), + uss: jest.fn(), + job: jest.fn(() => mockTree) as any, + }); + expect(TreeProviders.job).toEqual(mockTree); + }); +}); diff --git a/packages/zowe-explorer/__tests__/__unit__/shared/actions.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/shared/actions.unit.test.ts index 5ede1e5bcc..b614a02d0a 100644 --- a/packages/zowe-explorer/__tests__/__unit__/shared/actions.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/shared/actions.unit.test.ts @@ -70,7 +70,11 @@ async function createGlobalMocks() { }), configurable: true, }); - Object.defineProperty(vscode.window, "createTreeView", { value: jest.fn(), configurable: true }); + Object.defineProperty(vscode.window, "createTreeView", { + value: jest.fn().mockReturnValue({ onDidCollapseElement: jest.fn() }), + configurable: true, + }); + Object.defineProperty(vscode.workspace, "getConfiguration", { value: jest.fn(), configurable: true }); Object.defineProperty(vscode.window, "showInformationMessage", { value: jest.fn(), configurable: true }); Object.defineProperty(vscode.window, "showInputBox", { value: jest.fn(), configurable: true }); Object.defineProperty(vscode.window, "showErrorMessage", { value: jest.fn(), configurable: true }); @@ -95,6 +99,15 @@ async function createGlobalMocks() { }); Object.defineProperty(zowe, "Utilities", { value: jest.fn(), configurable: true }); Object.defineProperty(zowe.Utilities, "isFileTagBinOrAscii", { value: jest.fn(), configurable: true }); + Object.defineProperty(globals, "LOG", { value: jest.fn(), configurable: true }); + Object.defineProperty(globals.LOG, "debug", { value: jest.fn(), configurable: true }); + Object.defineProperty(globals.LOG, "error", { value: jest.fn(), configurable: true }); + Object.defineProperty(globals.LOG, "warn", { value: jest.fn(), configurable: true }); + Object.defineProperty(ZoweLogger, "error", { value: jest.fn(), configurable: true }); + Object.defineProperty(ZoweLogger, "debug", { value: jest.fn(), configurable: true }); + Object.defineProperty(ZoweLogger, "warn", { value: jest.fn(), configurable: true }); + Object.defineProperty(ZoweLogger, "info", { value: jest.fn(), configurable: true }); + Object.defineProperty(ZoweLogger, "trace", { value: jest.fn(), configurable: true }); Object.defineProperty(ZoweLocalStorage, "storage", { value: { get: () => ({ persistence: true, favorites: [], history: [], sessions: ["zosmf"], searchHistory: [], fileHistory: [] }), diff --git a/packages/zowe-explorer/__tests__/__unit__/shared/init.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/shared/init.unit.test.ts index f631e4e6d0..34a04ce416 100644 --- a/packages/zowe-explorer/__tests__/__unit__/shared/init.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/shared/init.unit.test.ts @@ -27,6 +27,7 @@ import { saveUSSFile } from "../../../src/uss/actions"; import { ZoweLogger } from "../../../src/utils/LoggerUtils"; import { ZoweSaveQueue } from "../../../src/abstract/ZoweSaveQueue"; import { ZoweExplorerApiRegister } from "../../../src/ZoweExplorerApiRegister"; +import * as HistoryView from "../../../src/shared/HistoryView"; import { LocalFileManagement } from "../../../src/utils/LocalFileManagement"; jest.mock("../../../src/utils/LoggerUtils"); @@ -56,11 +57,8 @@ describe("Test src/shared/extension", () => { mock: [], }, { - name: "zowe.updateSecureCredentials", - mock: [ - { spy: jest.spyOn(globals, "setGlobalSecurityValue"), arg: [test.value] }, - { spy: jest.spyOn(profUtils.ProfilesUtils, "writeOverridesFile"), arg: [] }, - ], + name: "zowe.editHistory", + mock: [{ spy: jest.spyOn(HistoryView, "HistoryView"), arg: [test.context, test.value.providers] }], }, { name: "zowe.promptCredentials", @@ -399,4 +397,22 @@ describe("Test src/shared/extension", () => { expect(spyExpand).not.toHaveBeenCalled(); }); }); + + describe("registerCredentialManager", () => { + let context: any; + + beforeEach(() => { + context = { subscriptions: [] }; + jest.clearAllMocks(); + }); + afterAll(() => { + jest.restoreAllMocks(); + }); + + it("should register command for updating credentials", () => { + const registerCommandSpy = jest.spyOn(vscode.commands, "registerCommand"); + sharedExtension.registerCredentialManager(context); + expect(registerCommandSpy).toBeCalledWith("zowe.updateSecureCredentials", expect.any(Function)); + }); + }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/shared/refresh.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/shared/refresh.unit.test.ts index 0f3247eefe..55ab37de64 100644 --- a/packages/zowe-explorer/__tests__/__unit__/shared/refresh.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/shared/refresh.unit.test.ts @@ -31,7 +31,7 @@ import { ZoweLocalStorage } from "../../../src/utils/ZoweLocalStorage"; function createGlobalMocks() { const globalMocks = { session: createISessionWithoutCredentials(), - createTreeView: jest.fn(), + createTreeView: jest.fn().mockReturnValue({ onDidCollapseElement: jest.fn() }), mockLog: jest.fn(), mockDebug: jest.fn(), mockError: jest.fn(), diff --git a/packages/zowe-explorer/__tests__/__unit__/uss/AttributeView.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/uss/AttributeView.unit.test.ts index 4e8a224246..f33a6de881 100644 --- a/packages/zowe-explorer/__tests__/__unit__/uss/AttributeView.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/uss/AttributeView.unit.test.ts @@ -13,6 +13,7 @@ import { ExtensionContext } from "vscode"; import { AttributeView } from "../../../src/uss/AttributeView"; import { IUss, IZoweTree, IZoweUSSTreeNode } from "@zowe/zowe-explorer-api"; import { ZoweExplorerApiRegister } from "../../../src/ZoweExplorerApiRegister"; +import * as contextually from "../../../src/shared/context"; describe("AttributeView unit tests", () => { let view: AttributeView; @@ -21,6 +22,7 @@ describe("AttributeView unit tests", () => { const node = { attributes: { perms: "----------", + tag: undefined, }, label: "example node", fullPath: "/z/some/path", @@ -33,7 +35,9 @@ describe("AttributeView unit tests", () => { beforeAll(() => { jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValue({ updateAttributes: updateAttributesMock, - } as unknown as IUss); + getTag: () => Promise.resolve("UTF-8"), + } as unknown as ZoweExplorerApi.IUss); + jest.spyOn(contextually, "isUssDirectory").mockReturnValue(false); view = new AttributeView(context, treeProvider, node); }); @@ -41,21 +45,21 @@ describe("AttributeView unit tests", () => { node.onUpdate = jest.fn(); }); - it("refreshes properly when webview sends 'refresh' command", () => { + it("refreshes properly when webview sends 'refresh' command", async () => { // case 1: node is a root node - (view as any).onDidReceiveMessage({ command: "refresh" }); + await (view as any).onDidReceiveMessage({ command: "refresh" }); expect(treeProvider.refresh).toHaveBeenCalled(); // case 2: node is a child node node.getParent = jest.fn().mockReturnValueOnce({ label: "parent node" } as IZoweUSSTreeNode); - (view as any).onDidReceiveMessage({ command: "refresh" }); + await (view as any).onDidReceiveMessage({ command: "refresh" }); expect(treeProvider.refreshElement).toHaveBeenCalled(); expect(node.onUpdate).toHaveBeenCalledTimes(2); }); - it("dispatches node data to webview when 'ready' command is received", () => { - (view as any).onDidReceiveMessage({ command: "ready" }); + it("dispatches node data to webview when 'ready' command is received", async () => { + await (view as any).onDidReceiveMessage({ command: "ready" }); expect(view.panel.webview.postMessage).toHaveBeenCalledWith({ attributes: node.attributes, name: node.fullPath, @@ -65,7 +69,7 @@ describe("AttributeView unit tests", () => { it("updates attributes when 'update-attributes' command is received", async () => { // case 1: no attributes provided from webview (sanity check) - (view as any).onDidReceiveMessage({ command: "update-attributes" }); + await (view as any).onDidReceiveMessage({ command: "update-attributes" }); expect(updateAttributesMock).not.toHaveBeenCalled(); const attributes = { diff --git a/packages/zowe-explorer/__tests__/__unit__/uss/USSTree.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/uss/USSTree.unit.test.ts index 8896b28e78..00cf996229 100644 --- a/packages/zowe-explorer/__tests__/__unit__/uss/USSTree.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/uss/USSTree.unit.test.ts @@ -49,7 +49,7 @@ async function createGlobalMocks() { showInputBox: jest.fn(), filters: jest.fn(), getFilters: jest.fn(), - createTreeView: jest.fn(), + createTreeView: jest.fn().mockReturnValue({ onDidCollapseElement: jest.fn() }), createQuickPick: jest.fn(), getConfiguration: jest.fn(), ZosmfSession: jest.fn(), @@ -138,6 +138,10 @@ async function createGlobalMocks() { value: globalMocks.showErrorMessage, configurable: true, }); + Object.defineProperty(vscode.workspace, "getConfiguration", { + value: globalMocks.getConfiguration, + configurable: true, + }); Object.defineProperty(vscode.window, "showInputBox", { value: globalMocks.showInputBox, configurable: true }); Object.defineProperty(vscode, "ProgressLocation", { value: globalMocks.ProgressLocation, configurable: true }); Object.defineProperty(vscode.window, "withProgress", { value: globalMocks.withProgress, configurable: true }); @@ -169,7 +173,7 @@ async function createGlobalMocks() { globalMocks.withProgress.mockReturnValue(globalMocks.testResponse); globalMocks.getFilters.mockReturnValue(["/u/aDir{directory}", "/u/myFile.txt{textFile}"]); globalMocks.mockDefaultProfile.mockReturnValue(globalMocks.testProfile); - globalMocks.getConfiguration.mockReturnValueOnce({ + globalMocks.getConfiguration.mockReturnValue({ get: (setting: string) => ["[test]: /u/aDir{directory}", "[test]: /u/myFile.txt{textFile}"], update: jest.fn(() => { return {}; @@ -201,7 +205,7 @@ describe("USSTree Unit Tests - Function USSTree.initializeFavorites()", () => { "[test]: /u/aDir{directory}", "[test]: /u/myFile.txt{textFile}", ]); - const testTree1 = await createUSSTree(); + const testTree1 = await createUSSTree(zowe.imperative.Logger.getAppLogger()); const favProfileNode = testTree1.mFavorites[0]; expect(testTree1.mSessionNodes).toBeDefined(); expect(testTree1.mFavorites.length).toBe(1); @@ -231,7 +235,7 @@ describe("USSTree Unit Tests - Function initializeFavChildNodeForProfile()", () "[test]: /u/aDir{directory}", "[test]: /u/myFile.txt{textFile}", ]); - const testTree1 = await createUSSTree(); + const testTree1 = await createUSSTree(zowe.imperative.Logger.getAppLogger()); const favProfileNode = testTree1.mFavorites[0]; const label = "/u/fakeuser"; const line = "[test]: /u/fakeuser{ussSession}"; @@ -1441,7 +1445,7 @@ describe("USSTree Unit Tests - Function USSTree.getChildren()", () => { await globalMocks.testTree.getChildren(favProfileNode); - expect(loadProfilesForFavoritesSpy).toHaveBeenCalledWith(favProfileNode); + expect(loadProfilesForFavoritesSpy).toHaveBeenCalledWith(log, favProfileNode); }); }); // Idea is borrowed from: https://github.com/kulshekhar/ts-jest/blob/master/src/util/testing.ts @@ -1507,7 +1511,7 @@ describe("USSTree Unit Tests - Function USSTree.loadProfilesForFavorites", () => }), }); - await globalMocks.testTree.loadProfilesForFavorites(favProfileNode); + await globalMocks.testTree.loadProfilesForFavorites(blockMocks.log, favProfileNode); const resultFavProfileNode = globalMocks.testTree.mFavorites[0]; expect(resultFavProfileNode).toEqual(expectedFavProfileNode); @@ -1540,7 +1544,7 @@ describe("USSTree Unit Tests - Function USSTree.loadProfilesForFavorites", () => configurable: true, }); mocked(vscode.window.showErrorMessage).mockResolvedValueOnce({ title: "Remove" }); - await globalMocks.testTree.loadProfilesForFavorites(favProfileNode); + await globalMocks.testTree.loadProfilesForFavorites(blockMocks.log, favProfileNode); expect(showErrorMessageSpy).toBeCalledTimes(1); showErrorMessageSpy.mockClear(); }); @@ -1583,7 +1587,7 @@ describe("USSTree Unit Tests - Function USSTree.loadProfilesForFavorites", () => globalMocks.testProfile ); - await globalMocks.testTree.loadProfilesForFavorites(favProfileNode); + await globalMocks.testTree.loadProfilesForFavorites(blockMocks.log, favProfileNode); const resultFavDirNode = globalMocks.testTree.mFavorites[0].children[0]; expect(resultFavDirNode).toEqual(expectedFavDirNode); @@ -1628,7 +1632,7 @@ describe("USSTree Unit Tests - Function USSTree.loadProfilesForFavorites", () => globalMocks.testProfile ); - await globalMocks.testTree.loadProfilesForFavorites(favProfileNode); + await globalMocks.testTree.loadProfilesForFavorites(blockMocks.log, favProfileNode); const resultFavDirNode = globalMocks.testTree.mFavorites[0].children[0]; expect(resultFavDirNode).toEqual(expectedFavDirNode); @@ -1717,4 +1721,51 @@ describe("USSTree Unit Tests - Function USSTree.editSession()", () => { globalMocks.testTree.editSession(testSessionNode); expect(checkSession).toHaveBeenCalled(); }); + + describe("removeSearchHistory", () => { + it("removes the search item passed in from the current history", async () => { + const globalMocks = await createGlobalMocks(); + expect(globalMocks.testTree["mHistory"]["mSearchHistory"].length).toEqual(1); + globalMocks.testTree.removeSearchHistory("/u/myuser"); + expect(globalMocks.testTree["mHistory"]["mSearchHistory"].length).toEqual(0); + }); + }); + + describe("resetSearchHistory", () => { + it("clears the entire search history", async () => { + const globalMocks = await createGlobalMocks(); + expect(globalMocks.testTree["mHistory"]["mSearchHistory"].length).toEqual(1); + globalMocks.testTree.resetSearchHistory(); + expect(globalMocks.testTree["mHistory"]["mSearchHistory"].length).toEqual(0); + }); + }); + + describe("resetFileHistory", () => { + it("clears the entire file history", async () => { + const globalMocks = await createGlobalMocks(); + globalMocks.testTree["mHistory"]["mFileHistory"] = ["test1", "test2"]; + expect(globalMocks.testTree["mHistory"]["mFileHistory"].length).toEqual(2); + + globalMocks.testTree.resetFileHistory(); + expect(globalMocks.testTree["mHistory"]["mFileHistory"].length).toEqual(0); + }); + }); + + describe("getSessions", () => { + it("gets all the available sessions from persistent object", async () => { + const globalMocks = await createGlobalMocks(); + globalMocks.testTree["mHistory"]["mSessions"] = ["sestest"]; + expect(globalMocks.testTree.getSessions()).toEqual(["sestest"]); + }); + }); + + describe("getFavorites", () => { + it("gets all the favorites from persistent object", async () => { + const globalMocks = await createGlobalMocks(); + jest.spyOn(ZoweLocalStorage, "getValue").mockReturnValue({ + favorites: ["test1", "test2", "test3"], + }); + expect(globalMocks.testTree.getFavorites()).toEqual(["test1", "test2", "test3"]); + }); + }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/uss/ZoweUSSNode.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/uss/ZoweUSSNode.unit.test.ts index 7eeac510cd..77b5691375 100644 --- a/packages/zowe-explorer/__tests__/__unit__/uss/ZoweUSSNode.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/uss/ZoweUSSNode.unit.test.ts @@ -134,7 +134,10 @@ async function createGlobalMocks() { configurable: true, }); Object.defineProperty(vscode.window, "showInputBox", { value: globalMocks.showInputBox, configurable: true }); - Object.defineProperty(vscode.window, "createTreeView", { value: jest.fn(), configurable: true }); + Object.defineProperty(vscode.window, "createTreeView", { + value: jest.fn().mockReturnValue({ onDidCollapseElement: jest.fn() }), + configurable: true, + }); Object.defineProperty(zowe, "ZosmfSession", { value: globalMocks.ZosmfSession, configurable: true }); Object.defineProperty(globalMocks.ZosmfSession, "createSessCfgFromArgs", { value: globalMocks.createSessCfgFromArgs, diff --git a/packages/zowe-explorer/__tests__/__unit__/uss/actions.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/uss/actions.unit.test.ts index 7ba0bca02a..fc1c1ee3f2 100644 --- a/packages/zowe-explorer/__tests__/__unit__/uss/actions.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/uss/actions.unit.test.ts @@ -63,7 +63,7 @@ function createGlobalMocks() { setStatusBarMessage: jest.fn().mockReturnValue({ dispose: jest.fn() }), showWarningMessage: jest.fn(), showErrorMessage: jest.fn(), - createTreeView: jest.fn(), + createTreeView: jest.fn().mockReturnValue({ onDidCollapseElement: jest.fn() }), fileToUSSFile: jest.fn(), Upload: jest.fn(), isBinaryFileSync: jest.fn(), @@ -874,9 +874,7 @@ describe("USS Action Unit Tests - copy file / directory", () => { it("tests pasteUssFile executed successfully with selected nodes", async () => { const globalMocks = createGlobalMocks(); const blockMocks = await createBlockMocks(globalMocks); - const parent = blockMocks.treeNodes.testUSSTree.getTreeView(); - parent.selection = blockMocks.nodes[0]; - await ussNodeActions.pasteUssFile(blockMocks.treeNodes.testUSSTree, undefined); + await ussNodeActions.pasteUssFile(blockMocks.treeNodes.testUSSTree, blockMocks.nodes[0]); expect(sharedUtils.getSelectedNodeList(blockMocks.treeNodes.ussNode, blockMocks.treeNodes.ussNodes)).toEqual([blockMocks.treeNodes.ussNode]); }); it("tests pasteUssFile executed successfully with one node", async () => { @@ -888,6 +886,16 @@ describe("USS Action Unit Tests - copy file / directory", () => { await ussNodeActions.pasteUssFile(blockMocks.treeNodes.testUSSTree, blockMocks.nodes[0]); expect(sharedUtils.getSelectedNodeList(blockMocks.treeNodes.ussNode, blockMocks.treeNodes.ussNodes)).toEqual([blockMocks.treeNodes.ussNode]); }); + it("tests pasteUss returns early if APIs are not supported", async () => { + const globalMocks = createGlobalMocks(); + const blockMocks = await createBlockMocks(globalMocks); + const testNode = blockMocks.nodes[0]; + testNode.copyUssFile = testNode.pasteUssTree = null; + const infoMessageSpy = jest.spyOn(Gui, "infoMessage"); + await ussNodeActions.pasteUss(blockMocks.treeNodes.testUSSTree, testNode); + expect(infoMessageSpy).toHaveBeenCalledWith("The paste operation is not supported for this node."); + infoMessageSpy.mockRestore(); + }); }); describe("USS Action Unit Tests - function deleteUSSFilesPrompt", () => { diff --git a/packages/zowe-explorer/__tests__/__unit__/utils/ProfileManagement.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/utils/ProfileManagement.unit.test.ts new file mode 100644 index 0000000000..6f33f2d2ad --- /dev/null +++ b/packages/zowe-explorer/__tests__/__unit__/utils/ProfileManagement.unit.test.ts @@ -0,0 +1,250 @@ +/** + * 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 { ZoweDatasetNode } from "../../../src/dataset/ZoweDatasetNode"; +import * as sharedMock from "../../../__mocks__/mockCreators/shared"; +import * as dsMock from "../../../__mocks__/mockCreators/datasets"; +import * as unixMock from "../../../__mocks__/mockCreators/uss"; +import * as profUtils from "../../../src/utils/ProfilesUtils"; +import { ProfileManagement } from "../../../src/utils/ProfileManagement"; +import { Gui } from "@zowe/zowe-explorer-api"; +import { ZoweLogger } from "../../../src/utils/LoggerUtils"; +import { Profiles } from "../../../src/Profiles"; +import * as vscode from "vscode"; +import { imperative } from "@zowe/cli"; +import { ZoweUSSNode } from "../../../src/uss/ZoweUSSNode"; + +jest.mock("fs"); +jest.mock("vscode"); + +describe("ProfileManagement unit tests", () => { + afterEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + }); + function createGlobalMocks(): any { + const newMocks = { + mockSession: sharedMock.createISession(), + mockBasicAuthProfile: sharedMock.createValidIProfile(), + mockTokenAuthProfile: sharedMock.createTokenAuthIProfile(), + mockNoAuthProfile: sharedMock.createNoAuthIProfile(), + opCancelledSpy: jest.spyOn(Gui, "infoMessage"), + mockDsSessionNode: ZoweDatasetNode, + mockUnixSessionNode: ZoweUSSNode, + mockResolveQp: jest.fn(), + mockCreateQp: jest.fn(), + mockUpdateChosen: ProfileManagement.basicAuthUpdateQpItems[ProfileManagement.AuthQpLabels.update], + mockAddBasicChosen: ProfileManagement.basicAuthAddQpItems[ProfileManagement.AuthQpLabels.add], + mockLoginChosen: ProfileManagement.tokenAuthLoginQpItem[ProfileManagement.AuthQpLabels.login], + mockLogoutChosen: ProfileManagement.tokenAuthLogoutQpItem[ProfileManagement.AuthQpLabels.logout], + mockEditProfChosen: ProfileManagement.editProfileQpItems[ProfileManagement.AuthQpLabels.edit], + mockDeleteProfChosen: ProfileManagement.deleteProfileQpItem[ProfileManagement.AuthQpLabels.delete], + mockHideProfChosen: ProfileManagement.hideProfileQpItems[ProfileManagement.AuthQpLabels.hide], + mockEnableValidationChosen: ProfileManagement.enableProfileValildationQpItem[ProfileManagement.AuthQpLabels.enable], + mockDisableValidationChosen: ProfileManagement.disableProfileValildationQpItem[ProfileManagement.AuthQpLabels.disable], + mockProfileInfo: { usingTeamConfig: true }, + mockProfileInstance: null as any, + debugLogSpy: null as any, + promptSpy: null as any, + editSpy: null as any, + loginSpy: null as any, + logoutSpy: null as any, + logMsg: null as any, + commandSpy: null as any, + }; + Object.defineProperty(profUtils.ProfilesUtils, "promptCredentials", { value: jest.fn(), configurable: true }); + newMocks.promptSpy = jest.spyOn(profUtils.ProfilesUtils, "promptCredentials"); + Object.defineProperty(ZoweLogger, "debug", { value: jest.fn(), configurable: true }); + newMocks.debugLogSpy = jest.spyOn(ZoweLogger, "debug"); + Object.defineProperty(Gui, "resolveQuickPick", { value: newMocks.mockResolveQp, configurable: true }); + newMocks.mockCreateQp.mockReturnValue({ + show: jest.fn(() => { + return {}; + }), + hide: jest.fn(() => { + return {}; + }), + onDidAccept: jest.fn(() => { + return {}; + }), + }); + Object.defineProperty(Gui, "createQuickPick", { value: newMocks.mockCreateQp, configurable: true }); + newMocks.mockDsSessionNode = dsMock.createDatasetSessionNode(newMocks.mockSession, newMocks.mockBasicAuthProfile) as any; + newMocks.mockProfileInstance = sharedMock.createInstanceOfProfile(newMocks.mockBasicAuthProfile); + Object.defineProperty(Profiles, "getInstance", { + value: jest.fn().mockReturnValue(newMocks.mockProfileInstance), + configurable: true, + }); + Object.defineProperty(newMocks.mockProfileInstance, "editSession", { value: jest.fn(), configurable: true }); + newMocks.editSpy = jest.spyOn(newMocks.mockProfileInstance, "editSession"); + Object.defineProperty(newMocks.mockProfileInstance, "ssoLogin", { value: jest.fn(), configurable: true }); + newMocks.loginSpy = jest.spyOn(newMocks.mockProfileInstance, "ssoLogin"); + Object.defineProperty(newMocks.mockProfileInstance, "ssoLogout", { value: jest.fn(), configurable: true }); + newMocks.logoutSpy = jest.spyOn(newMocks.mockProfileInstance, "ssoLogout"); + Object.defineProperty(vscode.commands, "executeCommand", { value: jest.fn(), configurable: true }); + newMocks.commandSpy = jest.spyOn(vscode.commands, "executeCommand"); + + return newMocks; + } + + describe("unit tests around basic auth selections", () => { + function createBlockMocks(globalMocks): any { + globalMocks.logMsg = `Profile ${globalMocks.mockBasicAuthProfile.name} is using basic authentication.`; + globalMocks.mockDsSessionNode.getProfile = jest.fn().mockReturnValue(globalMocks.mockBasicAuthProfile); + return globalMocks; + } + it("profile using basic authentication should see Operation Cancelled when escaping quick pick", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + mocks.mockResolveQp.mockResolvedValueOnce(undefined); + await ProfileManagement.manageProfile(mocks.mockDsSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.opCancelledSpy).toBeCalledWith("Operation Cancelled"); + }); + it("profile using basic authentication should see promptCredentials called when Update Credentials chosen", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + mocks.mockResolveQp.mockResolvedValueOnce(mocks.mockUpdateChosen); + await ProfileManagement.manageProfile(mocks.mockDsSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.promptSpy).toBeCalled(); + }); + it("profile using basic authentication should see editSession called when Edit Profile chosen", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + mocks.mockResolveQp.mockResolvedValueOnce(mocks.mockEditProfChosen); + await ProfileManagement.manageProfile(mocks.mockDsSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.editSpy).toBeCalled(); + }); + it("profile using basic authentication should see editSession called when Delete Profile chosen with v2 profile", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + Object.defineProperty(mocks.mockProfileInstance, "getProfileInfo", { + value: jest.fn().mockResolvedValue(mocks.mockProfileInfo as imperative.ProfileInfo), + configurable: true, + }); + mocks.mockResolveQp.mockResolvedValueOnce(mocks.mockDeleteProfChosen); + await ProfileManagement.manageProfile(mocks.mockDsSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.editSpy).toBeCalled(); + }); + it("profile using basic authentication should see hide session command called for profile in data set tree view", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + Object.defineProperty(mocks.mockProfileInstance, "getProfileInfo", { + value: jest.fn().mockResolvedValue(mocks.mockProfileInfo as imperative.ProfileInfo), + configurable: true, + }); + mocks.mockResolveQp.mockResolvedValueOnce(mocks.mockHideProfChosen); + await ProfileManagement.manageProfile(mocks.mockDsSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.commandSpy).toHaveBeenLastCalledWith("zowe.ds.removeSession", mocks.mockDsSessionNode); + }); + }); + describe("unit tests around token auth selections", () => { + function createBlockMocks(globalMocks): any { + globalMocks.logMsg = `Profile ${globalMocks.mockTokenAuthProfile.name} is using token authentication.`; + globalMocks.mockUnixSessionNode = unixMock.createUSSSessionNode(globalMocks.mockSession, globalMocks.mockBasicAuthProfile) as any; + Object.defineProperty(profUtils.ProfilesUtils, "isUsingTokenAuth", { value: jest.fn().mockResolvedValueOnce(true), configurable: true }); + globalMocks.mockDsSessionNode.getProfile = jest.fn().mockReturnValue(globalMocks.mockTokenAuthProfile); + globalMocks.mockUnixSessionNode.getProfile = jest.fn().mockReturnValue(globalMocks.mockTokenAuthProfile); + return globalMocks; + } + it("profile using token authentication should see Operation Cancelled when escaping quick pick", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + mocks.mockResolveQp.mockResolvedValueOnce(undefined); + await ProfileManagement.manageProfile(mocks.mockDsSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.opCancelledSpy).toBeCalledWith("Operation Cancelled"); + }); + it("profile using token authentication should see ssoLogin called when Log in to authentication service chosen", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + mocks.mockResolveQp.mockResolvedValueOnce(mocks.mockLoginChosen); + await ProfileManagement.manageProfile(mocks.mockDsSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.loginSpy).toBeCalled(); + }); + it("profile using token authentication should see ssoLogout called when Log out from authentication service chosen", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + mocks.mockResolveQp.mockResolvedValueOnce(mocks.mockLogoutChosen); + await ProfileManagement.manageProfile(mocks.mockDsSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.logoutSpy).toBeCalled(); + }); + it("profile using token authentication should see correct command called for hiding a unix tree session node", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + mocks.mockResolveQp.mockResolvedValueOnce(mocks.mockHideProfChosen); + await ProfileManagement.manageProfile(mocks.mockUnixSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.commandSpy).toHaveBeenLastCalledWith("zowe.uss.removeSession", mocks.mockUnixSessionNode); + }); + it("profile using token authentication should see correct command called for enabling validation a unix tree session node", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + mocks.mockResolveQp.mockResolvedValueOnce(mocks.mockEnableValidationChosen); + await ProfileManagement.manageProfile(mocks.mockUnixSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.commandSpy).toHaveBeenLastCalledWith("zowe.uss.enableValidation", mocks.mockUnixSessionNode); + }); + it("profile using token authentication should see correct command called for disabling validation a unix tree session node", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + mocks.mockResolveQp.mockResolvedValueOnce(mocks.mockDisableValidationChosen); + await ProfileManagement.manageProfile(mocks.mockUnixSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.commandSpy).toHaveBeenLastCalledWith("zowe.uss.disableValidation", mocks.mockUnixSessionNode); + }); + }); + describe("unit tests around no auth declared selections", () => { + function createBlockMocks(globalMocks): any { + globalMocks.logMsg = `Profile ${globalMocks.mockNoAuthProfile.name} authentication method is unkown.`; + Object.defineProperty(profUtils.ProfilesUtils, "isUsingTokenAuth", { value: jest.fn().mockResolvedValueOnce(false), configurable: true }); + globalMocks.mockDsSessionNode.getProfile = jest.fn().mockReturnValue(globalMocks.mockNoAuthProfile); + return globalMocks; + } + it("profile with no authentication method should see Operation Cancelled when escaping quick pick", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + mocks.mockResolveQp.mockResolvedValueOnce(undefined); + await ProfileManagement.manageProfile(mocks.mockDsSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.opCancelledSpy).toBeCalledWith("Operation Cancelled"); + }); + it("profile with no authentication method should see promptCredentials called when Add Basic Credentials chosen", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + mocks.mockResolveQp.mockResolvedValueOnce(mocks.mockAddBasicChosen); + await ProfileManagement.manageProfile(mocks.mockDsSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.promptSpy).toBeCalled(); + }); + it("profile with no authentication method should see ssoLogin called when Log in to authentication service chosen", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + mocks.mockResolveQp.mockResolvedValueOnce(mocks.mockLoginChosen); + await ProfileManagement.manageProfile(mocks.mockDsSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.loginSpy).toBeCalled(); + }); + it("profile with no authentication method should see editSession called when Edit Profile chosen", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + mocks.mockResolveQp.mockResolvedValueOnce(mocks.mockEditProfChosen); + await ProfileManagement.manageProfile(mocks.mockDsSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.editSpy).toBeCalled(); + }); + it("profile using token authentication should see correct command called for enabling validation a data set tree session node", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + mocks.mockResolveQp.mockResolvedValueOnce(mocks.mockEnableValidationChosen); + await ProfileManagement.manageProfile(mocks.mockDsSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.commandSpy).toHaveBeenLastCalledWith("zowe.ds.enableValidation", mocks.mockDsSessionNode); + }); + it("profile using token authentication should see correct command called for disabling validation a data set tree session node", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + mocks.mockResolveQp.mockResolvedValueOnce(mocks.mockDisableValidationChosen); + await ProfileManagement.manageProfile(mocks.mockDsSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.commandSpy).toHaveBeenLastCalledWith("zowe.ds.disableValidation", mocks.mockDsSessionNode); + }); + }); +}); 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 12eda44da2..a4ee857fe5 100644 --- a/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts @@ -461,6 +461,7 @@ describe("ProfilesUtils unit tests", () => { const blockMocks = createBlockMocks(); blockMocks.mockGetDirectValue.mockReturnValue(true); blockMocks.mockExistsSync.mockReturnValue(false); + jest.spyOn(fs, "readFileSync").mockReturnValue(Buffer.from(JSON.stringify({ overrides: { credentialManager: "@zowe/cli" } }), "utf-8")); const createFileSpy = jest.spyOn(profUtils.ProfilesUtils, "writeOverridesFile"); await profUtils.ProfilesUtils.initializeZoweFolder(); expect(globals.PROFILE_SECURITY).toBe(globals.ZOWE_CLI_SCM); @@ -470,9 +471,11 @@ describe("ProfilesUtils unit tests", () => { it("should skip creating directories and files that already exist", async () => { const blockMocks = createBlockMocks(); + jest.spyOn(profUtils.ProfilesUtils, "getCredentialManagerOverride").mockReturnValue("@zowe/cli"); blockMocks.mockGetDirectValue.mockReturnValue("@zowe/cli"); blockMocks.mockExistsSync.mockReturnValue(true); const fileJson = blockMocks.mockFileRead; + jest.spyOn(fs, "readFileSync").mockReturnValue(Buffer.from(JSON.stringify({ overrides: { credentialManager: "@zowe/cli" } }), "utf-8")); blockMocks.mockReadFileSync.mockReturnValueOnce(JSON.stringify(fileJson, null, 2)); await profUtils.ProfilesUtils.initializeZoweFolder(); expect(globals.PROFILE_SECURITY).toBe("@zowe/cli"); @@ -484,11 +487,11 @@ describe("ProfilesUtils unit tests", () => { describe("writeOverridesFile", () => { it("should have file exist", () => { const blockMocks = createBlockMocks(); - const fileJson = { overrides: { CredentialManager: "@zowe/cli", testValue: true } }; - const content = JSON.stringify(fileJson, null, 2); - blockMocks.mockReadFileSync.mockReturnValueOnce(JSON.stringify({ overrides: { CredentialManager: false, testValue: true } }, null, 2)); + blockMocks.mockReadFileSync.mockReturnValueOnce( + JSON.stringify({ overrides: { CredentialManager: "@zowe/cli", testValue: true } }, null, 2) + ); profUtils.ProfilesUtils.writeOverridesFile(); - expect(blockMocks.mockWriteFileSync).toBeCalledWith(blockMocks.zoweDir, content, { encoding: "utf-8", flag: "w" }); + expect(blockMocks.mockWriteFileSync).toBeCalledTimes(0); }); it("should return and have no change to the existing file if PROFILE_SECURITY matches file", () => { @@ -543,6 +546,7 @@ describe("ProfilesUtils unit tests", () => { describe("initializeZoweProfiles", () => { it("should successfully initialize Zowe folder and read config from disk", async () => { const initZoweFolderSpy = jest.spyOn(profUtils.ProfilesUtils, "initializeZoweFolder"); + jest.spyOn(profUtils.ProfilesUtils, "getCredentialManagerOverride").mockReturnValueOnce("@zowe/cli"); const readConfigFromDiskSpy = jest.spyOn(profUtils.ProfilesUtils, "readConfigFromDisk").mockResolvedValueOnce(); await profUtils.ProfilesUtils.initializeZoweProfiles((msg) => ZoweExplorerExtender.showZoweConfigError(msg)); expect(initZoweFolderSpy).toHaveBeenCalledTimes(1); @@ -589,40 +593,6 @@ describe("ProfilesUtils unit tests", () => { expect(testFilterItem.label).toEqual("test undefined"); }); - describe("getCredentialManagerOverride", () => { - afterEach(() => { - jest.clearAllMocks(); - jest.resetAllMocks(); - }); - - it("should successfully retrieve the credential manager override map", () => { - const expectedValue = { - credMgrZEName: "test", - credMgrPluginName: "test", - } as zowe.imperative.ICredentialManagerNameMap; - const zoweLoggerTraceSpy = jest.spyOn(ZoweLogger, "trace"); - jest.spyOn(zowe.imperative.CredentialManagerOverride, "getKnownCredMgrs").mockReturnValueOnce([expectedValue]); - jest.spyOn(vscode.extensions, "getExtension").mockReturnValueOnce({ test: "test" } as any); - jest.spyOn(zowe.imperative.CredentialManagerOverride, "getCredMgrInfoByDisplayName").mockReturnValueOnce(expectedValue); - expect(profUtils.ProfilesUtils.getCredentialManagerOverride()).toEqual(expectedValue); - expect(zoweLoggerTraceSpy).toBeCalledTimes(1); - }); - it("should return undefined if no credential manager is found", () => { - const expectedValue = { - credMgrZEName: "test", - credMgrPluginName: "test", - } as zowe.imperative.ICredentialManagerNameMap; - const zoweLoggerTraceSpy = jest.spyOn(ZoweLogger, "trace"); - jest.spyOn(zowe.imperative.CredentialManagerOverride, "getKnownCredMgrs").mockReturnValueOnce([expectedValue]); - jest.spyOn(vscode.extensions, "getExtension").mockImplementationOnce(() => { - throw new Error("failed to get extension"); - }); - jest.spyOn(zowe.imperative.CredentialManagerOverride, "getCredMgrInfoByDisplayName").mockReturnValueOnce(expectedValue); - expect(profUtils.ProfilesUtils.getCredentialManagerOverride()).toEqual(undefined); - expect(zoweLoggerTraceSpy).toBeCalledTimes(1); - }); - }); - describe("activateCredentialManagerOverride", () => { afterEach(() => { jest.clearAllMocks(); @@ -637,7 +607,7 @@ describe("ProfilesUtils unit tests", () => { isActive: true, } as any; - await expect(profUtils.ProfilesUtils.activateCredentialManagerOverride(credentialManagerExtension)).resolves.toEqual( + await expect((profUtils.ProfilesUtils as any).activateCredentialManagerOverride(credentialManagerExtension)).resolves.toEqual( {} as zowe.imperative.ICredentialManagerConstructor ); expect(activateSpy).toBeCalledTimes(1); @@ -651,7 +621,7 @@ describe("ProfilesUtils unit tests", () => { isActive: true, } as any; - await expect(profUtils.ProfilesUtils.activateCredentialManagerOverride(credentialManagerExtension)).resolves.toEqual(undefined); + await expect((profUtils.ProfilesUtils as any).activateCredentialManagerOverride(credentialManagerExtension)).resolves.toEqual(undefined); expect(activateSpy).toBeCalledTimes(1); }); @@ -664,7 +634,7 @@ describe("ProfilesUtils unit tests", () => { isActive: true, } as any; - await expect(profUtils.ProfilesUtils.activateCredentialManagerOverride(credentialManagerExtension)).rejects.toThrow( + await expect((profUtils.ProfilesUtils as any).activateCredentialManagerOverride(credentialManagerExtension)).rejects.toThrow( "Custom credential manager failed to activate" ); }); @@ -692,35 +662,47 @@ describe("ProfilesUtils unit tests", () => { jest.resetAllMocks(); }); - it("should retrieve the custom credential manager", async () => { - jest.spyOn(profUtils.ProfilesUtils, "getCredentialManagerOverride").mockReturnValueOnce({ - credMgrDisplayName: "test1", - credMgrPluginName: "test2", - credMgrZEName: "test3", + it("should prompt and install for missing extension of custom credential manager if override defined", async () => { + const isVSCodeCredentialPluginInstalledSpy = jest.spyOn(profUtils.ProfilesUtils, "isVSCodeCredentialPluginInstalled"); + + jest.spyOn(SettingsConfig, "getDirectValue").mockReturnValueOnce(true); + jest.spyOn(profUtils.ProfilesUtils as any, "fetchRegisteredPlugins").mockImplementation(); + jest.spyOn(profUtils.ProfilesUtils, "getCredentialManagerOverride").mockReturnValue("test"); + jest.spyOn(profUtils.ProfilesUtils, "isVSCodeCredentialPluginInstalled").mockReturnValueOnce(false); + jest.spyOn(SettingsConfig, "getDirectValue").mockReturnValueOnce(true); + jest.spyOn(profUtils.ProfilesUtils, "getCredentialManagerMap").mockReturnValueOnce({ + credMgrDisplayName: "test", + credMgrPluginName: "test", + credMgrZEName: "test", }); - jest.spyOn(vscode.extensions, "getExtension").mockReturnValueOnce({} as any); + jest.spyOn((profUtils as any).ProfilesUtils, "setupCustomCredentialManager").mockReturnValueOnce({}); + await expect(profUtils.ProfilesUtils.getProfileInfo(false)).resolves.toEqual({}); + expect(isVSCodeCredentialPluginInstalledSpy).toBeCalledTimes(1); + }); + + it("should retrieve the custom credential manager", async () => { + jest.spyOn(SettingsConfig, "getDirectValue").mockReturnValueOnce(true); + jest.spyOn(profUtils.ProfilesUtils as any, "fetchRegisteredPlugins").mockImplementation(); + jest.spyOn(profUtils.ProfilesUtils, "getCredentialManagerOverride").mockReturnValue("test"); + jest.spyOn(profUtils.ProfilesUtils, "isVSCodeCredentialPluginInstalled").mockReturnValueOnce(true); jest.spyOn(SettingsConfig, "getDirectValue").mockReturnValueOnce(true); - const activateCredenitalManagerOverrideSpy = jest - .spyOn(profUtils.ProfilesUtils, "activateCredentialManagerOverride") - .mockResolvedValueOnce({ - prototype: {}, - } as zowe.imperative.ICredentialManagerConstructor); - const updateCredentialManagerSettingSpy = jest.spyOn(profUtils.ProfilesUtils, "updateCredentialManagerSetting").mockImplementation(); - jest.spyOn(zowe.imperative, "ProfileInfo").mockReturnValueOnce({} as any); + jest.spyOn(profUtils.ProfilesUtils, "getCredentialManagerMap").mockReturnValueOnce({ + credMgrDisplayName: "test", + credMgrPluginName: "test", + credMgrZEName: "test", + }); + jest.spyOn((profUtils as any).ProfilesUtils, "setupCustomCredentialManager").mockReturnValueOnce({}); await expect(profUtils.ProfilesUtils.getProfileInfo(false)).resolves.toEqual({}); - expect(activateCredenitalManagerOverrideSpy).toBeCalledWith({}); - expect(updateCredentialManagerSettingSpy).toBeCalledWith("test1"); }); it("should retrieve the default credential manager if no custom credential manager is found", async () => { - jest.spyOn(profUtils.ProfilesUtils, "getCredentialManagerOverride").mockReturnValueOnce(undefined); - const defaultCredMgrSpy = jest.spyOn(zowe.imperative.ProfileCredentials, "defaultCredMgrWithKeytar"); - jest.spyOn(vscode.extensions, "getExtension").mockReturnValueOnce(undefined); + jest.spyOn(SettingsConfig, "getDirectValue").mockReturnValueOnce(false); + jest.spyOn(profUtils.ProfilesUtils, "getCredentialManagerOverride").mockReturnValue("@zowe/cli"); + jest.spyOn(profUtils.ProfilesUtils, "isVSCodeCredentialPluginInstalled").mockReturnValueOnce(false); jest.spyOn(SettingsConfig, "getDirectValue").mockReturnValueOnce(true); - const updateCredentialManagerSettingSpy = jest.spyOn(profUtils.ProfilesUtils, "updateCredentialManagerSetting").mockImplementation(); - await expect(profUtils.ProfilesUtils.getProfileInfo(true)).resolves.toEqual({}); - expect(updateCredentialManagerSettingSpy).toBeCalledWith(globals.ZOWE_CLI_SCM); - expect(defaultCredMgrSpy).toHaveBeenCalledWith(ProfilesCache.requireKeyring); + jest.spyOn(profUtils.ProfilesUtils, "getCredentialManagerMap").mockReturnValueOnce(undefined); + jest.spyOn((profUtils as any).ProfilesUtils, "setupDefaultCredentialManager").mockReturnValueOnce({}); + await expect(profUtils.ProfilesUtils.getProfileInfo(false)).resolves.toEqual({}); }); }); @@ -729,7 +711,207 @@ describe("ProfilesUtils unit tests", () => { jest.spyOn(Profiles.getInstance(), "getDefaultProfile").mockReturnValueOnce({} as any); jest.spyOn(Profiles.getInstance(), "getLoadedProfConfig").mockResolvedValue({ type: "test" } as any); jest.spyOn(Profiles.getInstance(), "getSecurePropsForProfile").mockResolvedValue([]); - await expect(profUtils.isUsingTokenAuth("test")).resolves.toEqual(false); + await expect(profUtils.ProfilesUtils.isUsingTokenAuth("test")).resolves.toEqual(false); + }); + }); + + describe("isVSCodeCredentialPluginInstalled", () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + jest.restoreAllMocks(); + }); + + it("should return false if an error is thrown when getting extension from available VS Code extensions", () => { + const zoweLoggerTraceSpy = jest.spyOn(ZoweLogger, "trace"); + jest.spyOn(zowe.imperative.CredentialManagerOverride, "getCredMgrInfoByDisplayName").mockReturnValue({ + credMgrDisplayName: "test", + credMgrPluginName: "test", + credMgrZEName: "test", + }); + jest.spyOn(vscode.extensions, "getExtension").mockImplementation(() => { + throw new Error("test error"); + }); + expect(profUtils.ProfilesUtils.isVSCodeCredentialPluginInstalled("test")).toBe(false); + expect(zoweLoggerTraceSpy).toBeCalledTimes(1); + }); + }); + + describe("getCredentialManagerOverride", () => { + afterEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + jest.restoreAllMocks(); + }); + + it("should return the custom credential manager override if of type string", () => { + const zoweLoggerTraceSpy = jest.spyOn(ZoweLogger, "trace"); + + jest.spyOn(fs, "readFileSync").mockReturnValueOnce( + Buffer.from( + JSON.stringify({ + overrides: { + credentialManager: "My Custom Credential Manager", + }, + }) + ) + ); + Object.defineProperty(zowe.imperative.CredentialManagerOverride, "CRED_MGR_SETTING_NAME", { + value: "credentialManager", + configurable: true, + }); + + expect(profUtils.ProfilesUtils.getCredentialManagerOverride()).toBe("My Custom Credential Manager"); + expect(zoweLoggerTraceSpy).toBeCalledTimes(1); + }); + + it("should return default manager if the override file does not exist", () => { + const zoweLoggerTraceSpy = jest.spyOn(ZoweLogger, "trace"); + const zoweLoggerInfoSpy = jest.spyOn(ZoweLogger, "info"); + + jest.spyOn(fs, "readFileSync").mockImplementation(() => { + throw new Error("test"); + }); + try { + profUtils.ProfilesUtils.getCredentialManagerOverride(); + } catch (err) { + expect(err).toBe("test"); + } + + expect(zoweLoggerTraceSpy).toBeCalledTimes(1); + expect(zoweLoggerInfoSpy).toBeCalledTimes(1); + }); + }); + + describe("setupCustomCredentialManager", () => { + afterEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + jest.restoreAllMocks(); + }); + + it("should return the profileInfo object with the custom credential manager constructor", async () => { + const zoweLoggerTraceSpy = jest.spyOn(ZoweLogger, "trace"); + const zoweLoggerInfoSpy = jest.spyOn(ZoweLogger, "info"); + + jest.spyOn(vscode.extensions, "getExtension").mockImplementation(); + jest.spyOn(profUtils.ProfilesUtils as any, "activateCredentialManagerOverride").mockResolvedValue(jest.fn()); + + await expect( + profUtils.ProfilesUtils["setupCustomCredentialManager"]({ + credMgrDisplayName: "test", + credMgrPluginName: "test", + credMgrZEName: "test", + }) + ).resolves.toEqual({} as zowe.imperative.ProfileInfo); + expect(zoweLoggerTraceSpy).toBeCalledTimes(2); + expect(zoweLoggerInfoSpy).toBeCalledTimes(1); + }); + }); + + describe("fetchRegisteredPlugins", () => { + afterEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + jest.restoreAllMocks(); + }); + + it("should not find any registered plugins and simply return", async () => { + const zoweLoggerTraceSpy = jest.spyOn(ZoweLogger, "trace"); + const updateCredentialManagerSettingSpy = jest.spyOn(profUtils.ProfilesUtils, "updateCredentialManagerSetting"); + const setDirectValueSpy = jest.spyOn(SettingsConfig, "setDirectValue"); + + jest.spyOn(zowe.imperative.CredentialManagerOverride, "getKnownCredMgrs").mockReturnValue([ + { + credMgrDisplayName: "test", + credMgrPluginName: "test", + credMgrZEName: "test", + }, + ]); + jest.spyOn(vscode.extensions, "getExtension").mockImplementation(() => { + throw new Error("test error"); + }); + + await expect(profUtils.ProfilesUtils["fetchRegisteredPlugins"]()).resolves.not.toThrow(); + expect(zoweLoggerTraceSpy).toBeCalledTimes(1); + expect(updateCredentialManagerSettingSpy).toBeCalledTimes(0); + expect(setDirectValueSpy).toBeCalledTimes(0); + }); + + it("suggest changing the override setting after finding a registered custom credential manager and selecting 'yes'", async () => { + const zoweLoggerTraceSpy = jest.spyOn(ZoweLogger, "trace"); + const updateCredentialManagerSettingSpy = jest.spyOn(profUtils.ProfilesUtils, "updateCredentialManagerSetting"); + const setDirectValueSpy = jest.spyOn(SettingsConfig, "setDirectValue"); + + jest.spyOn(zowe.imperative.CredentialManagerOverride, "getKnownCredMgrs").mockReturnValue([ + { + credMgrDisplayName: "test", + credMgrPluginName: "test", + credMgrZEName: "test", + }, + ]); + jest.spyOn(vscode.extensions, "getExtension").mockReturnValue({ + credMgrDisplayName: "test", + } as any); + jest.spyOn(Gui, "infoMessage").mockResolvedValue("Yes"); + + await expect(profUtils.ProfilesUtils["fetchRegisteredPlugins"]()).resolves.not.toThrow(); + expect(zoweLoggerTraceSpy).toBeCalledTimes(2); + expect(updateCredentialManagerSettingSpy).toBeCalledTimes(1); + expect(setDirectValueSpy).toBeCalledTimes(1); + }); + + it("suggest changing the override setting and selecting 'no' and should keep the default manager", async () => { + const zoweLoggerTraceSpy = jest.spyOn(ZoweLogger, "trace"); + const updateCredentialManagerSettingSpy = jest.spyOn(profUtils.ProfilesUtils, "updateCredentialManagerSetting"); + const setDirectValueSpy = jest.spyOn(SettingsConfig, "setDirectValue"); + + jest.spyOn(zowe.imperative.CredentialManagerOverride, "getKnownCredMgrs").mockReturnValue([ + { + credMgrDisplayName: "test", + credMgrPluginName: "test", + credMgrZEName: "test", + }, + ]); + jest.spyOn(vscode.extensions, "getExtension").mockReturnValue({ + credMgrDisplayName: "test", + } as any); + jest.spyOn(Gui, "infoMessage").mockResolvedValue("Don't ask again"); + + await expect(profUtils.ProfilesUtils["fetchRegisteredPlugins"]()).resolves.not.toThrow(); + expect(zoweLoggerTraceSpy).toBeCalledTimes(1); + expect(updateCredentialManagerSettingSpy).toBeCalledTimes(0); + expect(setDirectValueSpy).toBeCalledTimes(1); + }); + }); + + describe("promptAndHandleMissingCredentialManager", () => { + afterEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + jest.restoreAllMocks(); + }); + + it("should prompt to install missing custom credential manager defined in 'imperative.json'", async () => { + const zoweLoggerTraceSpy = jest.spyOn(ZoweLogger, "trace"); + const reloadWindowSpy = jest.spyOn(vscode.commands, "executeCommand"); + + jest.spyOn(Gui, "infoMessage").mockResolvedValue("Install"); + Object.defineProperty(vscode.env, "openExternal", { + value: () => true, + configurable: true, + }); + jest.spyOn(Gui, "showMessage").mockResolvedValue("Reload"); + + await expect( + profUtils.ProfilesUtils["promptAndHandleMissingCredentialManager"]({ + credMgrDisplayName: "test", + credMgrPluginName: "test", + credMgrZEName: "test", + }) + ).resolves.not.toThrow(); + expect(zoweLoggerTraceSpy).toBeCalledTimes(1); + expect(reloadWindowSpy).toBeCalledWith("workbench.action.reloadWindow"); }); }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/utils/SessionUtils.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/utils/SessionUtils.unit.test.ts index 02ff30e5ef..649455521d 100644 --- a/packages/zowe-explorer/__tests__/__unit__/utils/SessionUtils.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/utils/SessionUtils.unit.test.ts @@ -28,7 +28,16 @@ describe("SessionUtils removeSession Unit Tests", () => { newMocks.datasetSessionNode = createDatasetSessionNode(newMocks.session, newMocks.imperativeProfile); newMocks.testDatasetTree = createDatasetTree(newMocks.datasetSessionNode, newMocks.treeView); newMocks.testDatasetTree.addFileHistory("[profile1]: TEST.NODE"); - Object.defineProperty(vscode.window, "createTreeView", { value: jest.fn(), configurable: true }); + Object.defineProperty(vscode.window, "createTreeView", { + value: jest.fn().mockReturnValue({ onDidCollapseElement: jest.fn() }), + configurable: true, + }); + Object.defineProperty(vscode, "ConfigurationTarget", { value: jest.fn(), configurable: true }); + newMocks.mockGetConfiguration.mockReturnValue(createPersistentConfig()); + Object.defineProperty(vscode.workspace, "getConfiguration", { + value: newMocks.mockGetConfiguration, + configurable: true, + }); Object.defineProperty(vscode, "ConfigurationTarget", { value: jest.fn(), configurable: true }); newMocks.mockGetConfiguration.mockReturnValue(createPersistentConfig()); diff --git a/packages/zowe-explorer/__tests__/__unit__/utils/TreeViewUtils.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/utils/TreeViewUtils.unit.test.ts new file mode 100644 index 0000000000..cbb3cf4e4b --- /dev/null +++ b/packages/zowe-explorer/__tests__/__unit__/utils/TreeViewUtils.unit.test.ts @@ -0,0 +1,26 @@ +/** + * 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 { TreeViewUtils } from "../../../src/utils/TreeViewUtils"; +import * as globals from "../../../src/globals"; + +describe("TreeViewUtils Unit Tests", () => { + it("refreshIconOnCollapse - generated listener function works as intended", () => { + const testTreeProvider = { mOnDidChangeTreeData: { fire: jest.fn() } } as any; + const listenerFn = TreeViewUtils.refreshIconOnCollapse( + [(node): boolean => (node.contextValue as any).includes(globals.DS_PDS_CONTEXT) as boolean], + testTreeProvider + ); + const element = { label: "somenode", contextValue: globals.DS_PDS_CONTEXT } as any; + listenerFn({ element }); + expect(testTreeProvider.mOnDidChangeTreeData.fire).toHaveBeenCalledWith(element); + }); +}); diff --git a/packages/zowe-explorer/i18n/sample/package.i18n.json b/packages/zowe-explorer/i18n/sample/package.i18n.json index 15940008d1..cb416ac5e6 100644 --- a/packages/zowe-explorer/i18n/sample/package.i18n.json +++ b/packages/zowe-explorer/i18n/sample/package.i18n.json @@ -3,7 +3,9 @@ "description": "VS Code extension, powered by Zowe CLI, that streamlines interaction with mainframe data sets, USS files, and jobs", "viewsContainers.activitybar": "Zowe Explorer", "zowe.promptCredentials": "Update Credentials", + "zowe.profileManagement": "Manage Profile", "zowe.extRefresh": "Refresh Zowe Explorer", + "zowe.editHistory": "Edit History", "zowe.ds.explorer": "Data Sets", "zowe.uss.explorer": "Unix System Services (USS)", "zowe.jobs.explorer": "Jobs", @@ -38,7 +40,7 @@ "removeSavedSearch": "Remove Search", "removeSession": "Hide Profile", "saveSearch": "Add to Favorites", - "submitJcl": "Submit JCL", + "submitJcl": "Submit as JCL", "submitMember": "Submit Job", "uss.addSession": "Add Profile to USS View", "uss.copyPath": "Copy Path", @@ -109,6 +111,8 @@ "zowe.automaticProfileValidation": "Allow automatic validation of profiles.", "zowe.pollInterval.info": "Default interval (in milliseconds) when polling spool files.", "zowe.separator.recentFilters": "Recent Filters", + "zowe.settings.version": "Track if migration has been processed", + "zowe.security.checkForCustomCredentialManagers": "Check for any installed VS Code extensions for handling credentials when activating Zowe Explorer", "zowe.security.secureCredentialsEnabled": "Allow credentials to be stored securely. If disabled and autoStore is set to true, z/OS credentials are stored as clear text in zowe.config.json.", "issueTsoCmd": "Issue TSO Command", "deleteProfile": "Delete Profile", @@ -148,6 +152,14 @@ "createZoweSchema.reload.infoMessage": "Team Configuration file created. Location: {0}. \n Please reload your window.", "copyFile": "Copy", "pasteFile": "Paste", + "jobs.sortBy": "Sort jobs...", + "ds.allPdsSort": "all PDS members in {0}", + "ds.singlePdsSort": "the PDS members in {0}", + "ds.selectFilterOpt": "Set a filter for {0}", + "ds.selectSortOpt": "Select a sorting option for {0}", + "jobs.selectSortOpt": "Select a sorting option for jobs in {0}", + "ds.filterBy": "Filter PDS members...", + "ds.sortBy": "Sort PDS members...", "issueUnixCmd": "Issue Unix Command", "selectForCompare": "Select for Compare", "compareWithSelected": "Compare with Selected", diff --git a/packages/zowe-explorer/i18n/sample/src/Profiles.i18n.json b/packages/zowe-explorer/i18n/sample/src/Profiles.i18n.json index 22022ba307..c15e349186 100644 --- a/packages/zowe-explorer/i18n/sample/src/Profiles.i18n.json +++ b/packages/zowe-explorer/i18n/sample/src/Profiles.i18n.json @@ -1,5 +1,6 @@ { "profiles.operation.cancelled": "Operation Cancelled", + "profiles.manualEditMsg": "The Team configuration file has been opened in the editor. Editing or removal of profiles will need to be done manually.", "checkCurrentProfile.tokenAuthError.msg": "Token auth error", "checkCurrentProfile.tokenAuthError.additionalDetails": "Profile was found using token auth, please log in to continue.", "createZoweSession.ds.quickPickOption": "Choose \"Create new...\" to define or select a profile to add to the DATA SETS Explorer", @@ -17,11 +18,12 @@ "validateProfiles.progress": "Validating {0} Profile.", "validateProfiles.cancelled": "Validating {0} was cancelled.", "validateProfiles.error": "Profile validation failed for {0}.", - "ssoAuth.noBase": "This profile does not support token authentication.", - "ssoLogin.successful": "Login to authentication service was successful.", + "ssoAuth.usingBasicAuth": "This profile is using basic authentication and does not support token authentication.", + "ssoLogin.tokenType.error": "Error getting supported tokenType value for profile {0}", "ssoLogin.error": "Unable to log in with {0}. {1}", "ssoLogout.successful": "Logout from authentication service was successful for {0}.", "ssoLogout.error": "Unable to log out with {0}. {1}", + "ssoLogin.successful": "Login to authentication service was successful.", "getConfigLocationPrompt.placeholder.create": "Select the location where the config file will be initialized", "getConfigLocationPrompt.placeholder.edit": "Select the location of the config file to edit", "getConfigLocationPrompt.showQuickPick.global": "Global: in the Zowe home directory", diff --git a/packages/zowe-explorer/i18n/sample/src/dataset/DatasetTree.i18n.json b/packages/zowe-explorer/i18n/sample/src/dataset/DatasetTree.i18n.json index 53eff9a390..61d6e5e457 100644 --- a/packages/zowe-explorer/i18n/sample/src/dataset/DatasetTree.i18n.json +++ b/packages/zowe-explorer/i18n/sample/src/dataset/DatasetTree.i18n.json @@ -26,5 +26,20 @@ "renameDataSet.log.debug": "Renaming data set ", "renameDataSet.error": "Unable to rename data set:", "dataset.validation": "Enter a valid data set name.", + "ds.allPdsSort": "all PDS members in {0}", + "ds.singlePdsSort": "the PDS members in {0}", + "ds.selectSortOpt": "Select a sorting option for {0}", + "setSortDirection": "$(fold) Sort Direction", + "sort.selectDirection": "Select a sorting direction", + "sort.updated": "$(check) Sorting updated for {0}", + "filter.description": "Filter: {0}", + "ds.clearProfileFilter": "$(clear-all) Clear filter for profile", + "ds.clearPdsFilter": "$(clear-all) Clear filter for PDS", + "ds.selectFilterOpt": "Set a filter for {0}", + "filter.cleared": "$(check) Filter cleared for {0}", + "ds.filterEntry.invalidDate": "Invalid date format specified", + "ds.filterEntry.title": "Enter a value to filter by", + "ds.filterEntry.invalid": "Invalid filter specified", + "filter.updated": "$(check) Filter updated for {0}", "defaultFilterPrompt.option.prompt.search": "$(plus) Create a new filter. For example: HLQ.*, HLQ.aaa.bbb, HLQ.ccc.ddd(member)" } diff --git a/packages/zowe-explorer/i18n/sample/src/dataset/actions.i18n.json b/packages/zowe-explorer/i18n/sample/src/dataset/actions.i18n.json index 4358a911a9..304ea11198 100644 --- a/packages/zowe-explorer/i18n/sample/src/dataset/actions.i18n.json +++ b/packages/zowe-explorer/i18n/sample/src/dataset/actions.i18n.json @@ -42,8 +42,8 @@ "showAttributes.lengthError": "No matching names found for query: {0}", "showAttributes.error": "Unable to list attributes.", "attributes.title": "Attributes", - "submitJcl.noDocumentOpen": "No editor with a document that could be submitted as JCL is currently open.", - "submitJcl.submitting": "Submitting JCL in document {0}", + "submitJcl.notActiveEditorMsg": "No editor with a document that could be submitted as JCL is currently open.", + "submitJcl.submitting": "Submitting as JCL in document {0}", "submitJcl.qp.placeholder": "Select the Profile to use to submit the job", "submitJcl.noProfile": "No profiles available", "submitJcl.nullSession.error": "Session for submitting JCL was null or undefined!", diff --git a/packages/zowe-explorer/i18n/sample/src/dataset/utils.i18n.json b/packages/zowe-explorer/i18n/sample/src/dataset/utils.i18n.json new file mode 100644 index 0000000000..0aad47e3d5 --- /dev/null +++ b/packages/zowe-explorer/i18n/sample/src/dataset/utils.i18n.json @@ -0,0 +1,6 @@ +{ + "ds.sortByName": "$(case-sensitive) Name (default)", + "ds.sortByModified": "$(calendar) Date Modified", + "ds.sortByUserId": "$(account) User ID", + "setSortDirection": "$(fold) Sort Direction" +} diff --git a/packages/zowe-explorer/i18n/sample/src/globals.i18n.json b/packages/zowe-explorer/i18n/sample/src/globals.i18n.json index ec7d8bddd5..a6c64f66d8 100644 --- a/packages/zowe-explorer/i18n/sample/src/globals.i18n.json +++ b/packages/zowe-explorer/i18n/sample/src/globals.i18n.json @@ -17,6 +17,7 @@ "createFile.attribute.storclass": "Enter the SMS storage class", "createFile.attribute.volser": "Enter the volume serial on which the data set should be placed", "zowe.separator.recentFilters": "Recent Filters", + "zowe.separator.options": "Options", "globals.defineGlobals.isTheia": "Zowe Explorer is running in Theia environment.", "globals.defineGlobals.tempFolder": "Zowe Explorer's temp folder is located at {0}", "globals.setActivated.success": "Zowe Explorer has activated successfully.", diff --git a/packages/zowe-explorer/i18n/sample/src/job/actions.i18n.json b/packages/zowe-explorer/i18n/sample/src/job/actions.i18n.json index f1c4dad3d1..eb36cd951f 100644 --- a/packages/zowe-explorer/i18n/sample/src/job/actions.i18n.json +++ b/packages/zowe-explorer/i18n/sample/src/job/actions.i18n.json @@ -21,5 +21,9 @@ "cancelJobs.notImplemented": "The cancel function is not implemented in this API.", "cancelJobs.notCancelled": "The job was not cancelled.", "cancelJobs.failed": "One or more jobs failed to cancel: {0}", - "cancelJobs.succeeded": "Cancelled selected jobs successfully." + "cancelJobs.succeeded": "Cancelled selected jobs successfully.", + "jobs.selectSortOpt": "Select a sorting option for jobs in {0}", + "setSortDirection": "$(fold) Sort Direction", + "sort.selectDirection": "Select a sorting direction", + "sort.updated": "$(check) Sorting updated for {0}" } diff --git a/packages/zowe-explorer/i18n/sample/src/job/utils.i18n.json b/packages/zowe-explorer/i18n/sample/src/job/utils.i18n.json new file mode 100644 index 0000000000..b32e729f24 --- /dev/null +++ b/packages/zowe-explorer/i18n/sample/src/job/utils.i18n.json @@ -0,0 +1,7 @@ +{ + "jobs.sortById": "$(list-ordered) Job ID (default)", + "jobs.sortByDateSubmitted": "$(calendar) Date Submitted", + "jobs.sortByName": "$(case-sensitive) Job Name", + "jobs.sortByReturnCode": "$(symbol-numeric) Return Code", + "setSortDirection": "$(fold) Sort Direction" +} diff --git a/packages/zowe-explorer/i18n/sample/src/shared/HistoryView.i18n.json b/packages/zowe-explorer/i18n/sample/src/shared/HistoryView.i18n.json new file mode 100644 index 0000000000..396eec3ae3 --- /dev/null +++ b/packages/zowe-explorer/i18n/sample/src/shared/HistoryView.i18n.json @@ -0,0 +1,7 @@ +{ + "HistoryView.addItem.prompt": "Type the new pattern to add to history", + "HistoryView.removeItem.notSupported": "action is not supported for this property type.", + "HistoryView.clearAll.confirmMessage": "Clear all history items for this persistent property?", + "HistoryView.clearAll.Yes": "Yes", + "HistoryView.clearAll.No": "No" +} diff --git a/packages/zowe-explorer/i18n/sample/src/shared/utils.i18n.json b/packages/zowe-explorer/i18n/sample/src/shared/utils.i18n.json index 33228514f1..971aa4b21b 100644 --- a/packages/zowe-explorer/i18n/sample/src/shared/utils.i18n.json +++ b/packages/zowe-explorer/i18n/sample/src/shared/utils.i18n.json @@ -3,6 +3,8 @@ "zowe.jobs.confirmSubmission.yourJobs": "Your jobs", "zowe.jobs.confirmSubmission.otherUserJobs": "Other user jobs", "zowe.jobs.confirmSubmission.allJobs": "All jobs", + "sort.asc": "Ascending", + "sort.desc": "Descending", "uploadContent.putContents": "Uploading USS file", "saveFile.response.save.title": "Saving data set...", "saveUSSFile.response.title": "Saving file...", diff --git a/packages/zowe-explorer/i18n/sample/src/uss/actions.i18n.json b/packages/zowe-explorer/i18n/sample/src/uss/actions.i18n.json index 6863005b94..af4d5fc0cb 100644 --- a/packages/zowe-explorer/i18n/sample/src/uss/actions.i18n.json +++ b/packages/zowe-explorer/i18n/sample/src/uss/actions.i18n.json @@ -12,6 +12,7 @@ "deleteUssPrompt.confirmation.message": "Are you sure you want to delete the following item?\nThis will permanently remove the following file or folder from your system.\n\n{0}", "deleteUssPrompt.confirmation.cancel.log.debug": "Delete action was canceled.", "ZoweUssNode.copyDownload.progress": "Copying file structure...", + "uss.paste.apiNotAvailable": "The paste operation is not supported for this node.", "ZoweUssNode.copyUpload.progress": "Pasting files...", "downloadUnixFile.invalidNode.error": "open() called from invalid node.", "downloadUnixFile.name.exists": "There is already a file with the same name. Please change your OS file system settings if you want to give case sensitive file names", diff --git a/packages/zowe-explorer/i18n/sample/src/utils/ProfileManagement.i18n.json b/packages/zowe-explorer/i18n/sample/src/utils/ProfileManagement.i18n.json new file mode 100644 index 0000000000..f487573b50 --- /dev/null +++ b/packages/zowe-explorer/i18n/sample/src/utils/ProfileManagement.i18n.json @@ -0,0 +1,23 @@ +{ + "profiles.operation.cancelled": "Operation Cancelled", + "qpPlaceholders.qp.basic": "Profile {0} is using basic authentication. Choose a profile action.", + "qpPlaceholders.qp.token": "Profile {0} is using token authentication. Choose a profile action.", + "qpPlaceholders.qp.choose": "Profile {0} doesn't specify an authentication method. Choose a profile action.", + "addBasicAuthQpItem.addCredentials.qpLabel": "$(plus) Add Credentials", + "addBasicAuthQpItem.addCredentials.qpDetail": "Add username and password for basic authentication", + "updateBasicAuthQpItem.updateCredentials.qpLabel": "$(refresh) Update Credentials", + "updateBasicAuthQpItem.updateCredentials.qpDetail": "Update stored username and password", + "deleteProfileQpItem.delete.qpLabel": "$(trash) Delete Profile", + "disableProfileValildationQpItem.disableValidation.qpLabel": "$(workspace-untrusted) Disable Profile Validation", + "disableProfileValildationQpItem.disableValidation.qpDetail": "Disable validation of server check for profile", + "enableProfileValildationQpItem.enableValidation.qpLabel": "$(workspace-trusted) Enable Profile Validation", + "enableProfileValildationQpItem.enableValidation.qpDetail": "Enable validation of server check for profile", + "editProfileQpItem.editProfile.qpLabel": "$(pencil) Edit Profile", + "editProfileQpItem.editProfile.qpDetail": "Update profile connection information", + "hideProfileQpItems.hideProfile.qpLabel": "$(eye-closed) Hide Profile", + "hideProfileQpItems.hideProfile.qpDetail": "Hide profile name from tree view", + "loginQpItem.login.qpLabel": "$(arrow-right) Log in to authentication service", + "loginQpItem.login.qpDetail": "Log in to obtain a new token value", + "logoutQpItem.logout.qpLabel": "$(arrow-left) Log out of authentication service", + "logoutQpItem.logout.qpDetail": "Log out to invalidate and remove stored token value" +} diff --git a/packages/zowe-explorer/i18n/sample/src/utils/ProfilesUtils.i18n.json b/packages/zowe-explorer/i18n/sample/src/utils/ProfilesUtils.i18n.json index 3e8dd99432..9171d085f3 100644 --- a/packages/zowe-explorer/i18n/sample/src/utils/ProfilesUtils.i18n.json +++ b/packages/zowe-explorer/i18n/sample/src/utils/ProfilesUtils.i18n.json @@ -6,8 +6,18 @@ "errorHandling.checkCredentials.button": "Update Credentials", "errorHandling.checkCredentials.cancelled": "Operation Cancelled", "activateCredentialManagerOverride.failedToActivate": "Custom credential manager failed to activate", - "ProfilesUtils.getProfileInfo.usingCustom": "Custom credential manager found, attempting to activate.", - "ProfilesUtils.getProfileInfo.usingDefault": "No custom credential managers found, using the default instead.", + "ProfilesUtils.setupCustomCredentialManager.usingCustom": "Custom credential manager {0} found, attempting to activate.", + "ProfilesUtils.setupDefaultCredentialManager.usingDefault": "No custom credential managers found, using the default instead.", + "ProfilesUtils.fetchRegisteredPlugins.customCredentialManagerFound": "Custom credential manager {0} found", + "ProfilesUtils.fetchRegisteredPlugins.message": "Do you wish to use this credential manager instead?", + "ProfilesUtils.fetchRegisteredPlugins.yes": "Yes", + "ProfilesUtils.fetchRegisteredPlugins.dontAskAgain": "Don't ask again", + "ProfilesUtils.promptAndHandleMissingCredentialManager.suggestInstallHeader": "Plugin of name '{0}' was defined for custom credential management on imperative.json file.", + "ProfilesUtils.promptAndHandleMissingCredentialManager.suggestInstallMessage": "Please install associated VS Code extension for custom credential manager or revert to default.", + "ProfilesUtils.promptAndHandleMissingCredentialManager.revertToDefault": "Use Default", + "ProfilesUtils.promptAndHandleMissingCredentialManager.install": "Install", + "ProfilesUtils.promptAndHandleMissingCredentialManager.refreshMessage": "After installing the extension, please make sure to reload your VS Code window in order\n to start using the installed credential manager", + "ProfilesUtils.promptAndHandleMissingCredentialManager.refreshButton": "Reload", "readConfigFromDisk.v1profile.error": "Zowe v1 profiles in use. Zowe Explorer no longer supports v1 profiles.", "zowe.promptCredentials.notSupported": "\"Update Credentials\" operation not supported when \"autoStore\" is false", "createNewConnection.option.prompt.profileName.placeholder": "Connection Name", diff --git a/packages/zowe-explorer/package.json b/packages/zowe-explorer/package.json index 764d86d71d..fd7c14cd4c 100644 --- a/packages/zowe-explorer/package.json +++ b/packages/zowe-explorer/package.json @@ -90,6 +90,11 @@ "key": "ctrl+alt+z", "mac": "cmd+alt+z" }, + { + "command": "zowe.editHistory", + "key": "ctrl+y", + "mac": "cmd+y" + }, { "command": "zowe.uss.copyUssFile", "key": "ctrl+c", @@ -121,11 +126,22 @@ } ], "commands": [ + { + "command": "zowe.jobs.sortBy", + "title": "%jobs.sortBy%", + "category": "Zowe Explorer", + "icon": "$(list-ordered)" + }, { "command": "zowe.promptCredentials", "title": "%zowe.promptCredentials%", "category": "Zowe Explorer" }, + { + "command": "zowe.profileManagement", + "title": "%zowe.profileManagement%", + "category": "Zowe Explorer" + }, { "command": "zowe.extRefresh", "title": "%zowe.extRefresh%", @@ -135,6 +151,15 @@ "dark": "./resources/dark/refresh.svg" } }, + { + "command": "zowe.editHistory", + "title": "%zowe.editHistory%", + "category": "Zowe Explorer", + "icon": { + "light": "./resources/light/history.svg", + "dark": "./resources/dark/history.svg" + } + }, { "command": "zowe.ds.disableValidation", "title": "%disableValidation%", @@ -261,6 +286,18 @@ "title": "%deleteProfile%", "category": "Zowe Explorer" }, + { + "command": "zowe.ds.filterBy", + "title": "%ds.filterBy%", + "category": "Zowe Explorer", + "icon": "$(list-filter)" + }, + { + "command": "zowe.ds.sortBy", + "title": "%ds.sortBy%", + "category": "Zowe Explorer", + "icon": "$(list-ordered)" + }, { "command": "zowe.cmd.deleteProfile", "title": "%cmd.deleteProfile%", @@ -795,6 +832,20 @@ } ], "menus": { + "editor/context": [ + { + "when": "editorFocus", + "command": "zowe.ds.submitJcl", + "group": "000_zowe_dsMainframeInteraction@1" + } + ], + "explorer/context": [ + { + "when": "!explorerResourceIsFolder", + "command": "zowe.ds.submitJcl", + "group": "000_zowe_dsMainframeInteraction@1" + } + ], "view/title": [ { "when": "view == zowe.ds.explorer", @@ -973,31 +1024,26 @@ "command": "zowe.uss.deleteNode", "group": "099_zowe_ussModification:@4" }, - { - "when": "view == zowe.uss.explorer && viewItem =~ /_validate/ && !listMultiSelection", - "command": "zowe.uss.disableValidation", - "group": "098_zowe_ussProfileAuthentication@1" - }, - { - "when": "view == zowe.uss.explorer && viewItem =~ /_noValidate/ && !listMultiSelection", - "command": "zowe.uss.enableValidation", - "group": "098_zowe_ussProfileAuthentication@2" - }, { "when": "view == zowe.uss.explorer && viewItem =~ /^(?!.*_fav.*)ussSession.*/ && !listMultiSelection", - "command": "zowe.promptCredentials", - "group": "098_zowe_ussProfileAuthentication@3" + "command": "zowe.profileManagement", + "group": "099_zowe_ussProfileAuthentication@99" }, { - "when": "view == zowe.uss.explorer && viewItem =~ /^(?!.*_fav.*)ussSession.*/ && !listMultiSelection", - "command": "zowe.uss.ssoLogin", - "group": "098_zowe_ussProfileAuthentication@4" + "when": "view == zowe.ds.explorer && viewItem =~ /^session.*/ && !listMultiSelection", + "command": "zowe.ds.filterBy", + "group": "inline@0" }, { "when": "view == zowe.uss.explorer && viewItem =~ /^(?!.*_fav.*)ussSession.*/ && !listMultiSelection", "command": "zowe.uss.ssoLogout", "group": "098_zowe_ussProfileAuthentication@5" }, + { + "when": "view == zowe.uss.explorer && viewItem =~ /^(?!.*_fav.*)ussSession.*/ && !listMultiSelection", + "command": "zowe.editHistory", + "group": "100_zowe_editHistory@100" + }, { "when": "viewItem =~ /^(?!.*_fav.*)ussSession.*/ && !listMultiSelection", "command": "zowe.uss.editSession", @@ -1013,10 +1059,15 @@ "command": "zowe.uss.deleteProfile", "group": "099_zowe_ussProfileModification@99" }, + { + "when": "view == zowe.ds.explorer && viewItem =~ /^session.*/ && !listMultiSelection", + "command": "zowe.ds.sortBy", + "group": "inline@1" + }, { "when": "view == zowe.ds.explorer && viewItem =~ /^(?!.*_fav.*)session.*/ && !listMultiSelection", "command": "zowe.ds.pattern", - "group": "inline" + "group": "inline@2" }, { "when": "view == zowe.ds.explorer && viewItem =~ /^(pds|ds|migr).*_fav.*/", @@ -1128,6 +1179,16 @@ "command": "zowe.ds.removeFavProfile", "group": "002_zowe_dsWorkspace@4" }, + { + "when": "view == zowe.ds.explorer && viewItem =~ /^(pds|session).*/ && !listMultiSelection", + "command": "zowe.ds.filterBy", + "group": "098_zowe_dsSort@0" + }, + { + "when": "view == zowe.ds.explorer && viewItem =~ /^(pds|session).*/ && !listMultiSelection", + "command": "zowe.ds.sortBy", + "group": "098_zowe_dsSort@1" + }, { "when": "view == zowe.ds.explorer && viewItem =~ /^fileError.*/", "command": "zowe.ds.showFileErrorDetails", @@ -1188,50 +1249,25 @@ "command": "zowe.ds.deleteDataset", "group": "099_zowe_dsModification@5" }, - { - "when": "view == zowe.ds.explorer && viewItem =~ /_validate/ && !listMultiSelection", - "command": "zowe.ds.disableValidation", - "group": "098_zowe_dsProfileAuthentication@6" - }, - { - "when": "view == zowe.ds.explorer && viewItem =~ /_noValidate/ && !listMultiSelection", - "command": "zowe.ds.enableValidation", - "group": "098_zowe_dsProfileAuthentication@7" - }, { "when": "view == zowe.ds.explorer && viewItem =~ /^(?!.*_fav.*)session.*/ && !listMultiSelection", - "command": "zowe.promptCredentials", - "group": "098_zowe_dsProfileAuthentication@8" - }, - { - "when": "view == zowe.ds.explorer && viewItem =~ /^(?!.*_fav.*)session.*/ && !listMultiSelection", - "command": "zowe.ds.ssoLogin", - "group": "098_zowe_dsProfileAuthentication@9" - }, - { - "when": "view == zowe.ds.explorer && viewItem =~ /^(?!.*_fav.*)session.*/ && !listMultiSelection", - "command": "zowe.ds.ssoLogout", - "group": "098_zowe_dsProfileAuthentication@10" - }, - { - "when": "view == zowe.ds.explorer && viewItem =~ /^(?!.*_fav.*)session.*/ && !listMultiSelection", - "command": "zowe.ds.editSession", - "group": "099_zowe_dsProfileModification@0" + "command": "zowe.profileManagement", + "group": "100_zowe_dsProfileAuthentication@99" }, { - "when": "view == zowe.ds.explorer && viewItem =~ /^(?!.*_fav.*)session.*/", - "command": "zowe.ds.removeSession", - "group": "099_zowe_dsProfileModification@98" + "when": "view == zowe.jobs.explorer && viewItem =~ /^(?!.*_fav.*)server.*/ && !listMultiSelection", + "command": "zowe.jobs.sortBy", + "group": "inline@0" }, { "when": "view == zowe.ds.explorer && viewItem =~ /^(?!.*_fav.*)session.*/ && !listMultiSelection", - "command": "zowe.ds.deleteProfile", - "group": "099_zowe_dsProfileModification@99" + "command": "zowe.editHistory", + "group": "100_zowe_editHistory@100" }, { "when": "view == zowe.jobs.explorer && viewItem =~ /^(?!.*_fav.*)server.*/ && !listMultiSelection", "command": "zowe.jobs.search", - "group": "inline" + "group": "inline@1" }, { "when": "view == zowe.jobs.explorer && viewItem =~ /^(?!.*_fav.*)job.*/", @@ -1354,29 +1390,19 @@ "group": "099_zowe_jobsModification" }, { - "when": "view == zowe.jobs.explorer && viewItem =~ /_validate/ && !listMultiSelection", - "command": "zowe.jobs.disableValidation", - "group": "098_zowe_jobsProfileAuthentication@3" - }, - { - "when": "view == zowe.jobs.explorer && viewItem =~ /_noValidate/ && !listMultiSelection", - "command": "zowe.jobs.enableValidation", - "group": "098_zowe_jobsProfileAuthentication@4" - }, - { - "when": "view == zowe.jobs.explorer && viewItem =~ /^(?!.*_fav.*)server.*/ && !listMultiSelection", - "command": "zowe.promptCredentials", - "group": "098_zowe_jobsProfileAuthentication@5" + "when": "view == zowe.jobs.explorer && viewItem =~ /^(?!.*_fav.*)server.*/ && !listMultiSelection", + "command": "zowe.jobs.sortBy", + "group": "099_zowe_jobsSort" }, { - "when": "view == zowe.jobs.explorer && viewItem =~ /^(?!.*_fav.*)server.*/ && !listMultiSelection", - "command": "zowe.jobs.ssoLogin", - "group": "098_zowe_jobsProfileAuthentication@6" + "when": "view == zowe.jobs.explorer && viewItem =~ /^(?!.*_fav.*)server.*/ && !listMultiSelection", + "command": "zowe.editHistory", + "group": "100_zowe_editHistory@100" }, { "when": "view == zowe.jobs.explorer && viewItem =~ /^(?!.*_fav.*)server.*/ && !listMultiSelection", - "command": "zowe.jobs.ssoLogout", - "group": "098_zowe_jobsProfileAuthentication@7" + "command": "zowe.profileManagement", + "group": "100_zowe_jobsProfileAuthentication@99" }, { "when": "view == zowe.jobs.explorer && viewItem =~ /^(?!.*_fav.*)server.*/ && !listMultiSelection", @@ -1750,6 +1776,10 @@ "command": "zowe.jobs.deleteJob", "when": "never" }, + { + "command": "zowe.profileManagement", + "when": "never" + }, { "command": "zowe.compareFileStarted", "when": "never" @@ -1946,6 +1976,12 @@ "description": "%zowe.settings.migrated%", "scope": "window" }, + "zowe.security.checkForCustomCredentialManagers": { + "type": "boolean", + "description": "%zowe.security.checkForCustomCredentialManagers%", + "scope": "window", + "default": "true" + }, "zowe.security.secureCredentialsEnabled": { "default": true, "type": "boolean", @@ -2007,7 +2043,9 @@ "updateStrings": "node ../../scripts/stringUpdateScript.js && prettier --write --loglevel warn ./i18n package.nls*", "package": "vsce package --no-dependencies && node ../../scripts/mv-pack.js vscode-extension-for-zowe vsix", "license": "node ../../scripts/updateLicenses.js", - "watch": "gulp build && webpack --mode development --watch --info-verbosity verbose", + "watch": "concurrently \"pnpm run watch:webviews\" \"pnpm run watch:zowe-explorer\"", + "watch:zowe-explorer": "gulp build && webpack --mode development --watch --info-verbosity verbose", + "watch:webviews": "pnpm --filter webviews dev", "compile": "tsc -b", "compile-web": "webpack --target webworker --entry ./src/web/extension.ts --output-path ./out/src/web", "markdown": "markdownlint CHANGELOG.md README.md", @@ -2067,6 +2105,7 @@ "dependencies": { "@zowe/zowe-explorer-api": "3.0.0-next.202309141150", "@zowe/secrets-for-zowe-sdk": "7.18.4", + "dayjs": "^1.11.10", "fs-extra": "8.0.1", "isbinaryfile": "4.0.4", "js-yaml": "3.13.1", diff --git a/packages/zowe-explorer/package.nls.json b/packages/zowe-explorer/package.nls.json index 15940008d1..cb416ac5e6 100644 --- a/packages/zowe-explorer/package.nls.json +++ b/packages/zowe-explorer/package.nls.json @@ -3,7 +3,9 @@ "description": "VS Code extension, powered by Zowe CLI, that streamlines interaction with mainframe data sets, USS files, and jobs", "viewsContainers.activitybar": "Zowe Explorer", "zowe.promptCredentials": "Update Credentials", + "zowe.profileManagement": "Manage Profile", "zowe.extRefresh": "Refresh Zowe Explorer", + "zowe.editHistory": "Edit History", "zowe.ds.explorer": "Data Sets", "zowe.uss.explorer": "Unix System Services (USS)", "zowe.jobs.explorer": "Jobs", @@ -38,7 +40,7 @@ "removeSavedSearch": "Remove Search", "removeSession": "Hide Profile", "saveSearch": "Add to Favorites", - "submitJcl": "Submit JCL", + "submitJcl": "Submit as JCL", "submitMember": "Submit Job", "uss.addSession": "Add Profile to USS View", "uss.copyPath": "Copy Path", @@ -109,6 +111,8 @@ "zowe.automaticProfileValidation": "Allow automatic validation of profiles.", "zowe.pollInterval.info": "Default interval (in milliseconds) when polling spool files.", "zowe.separator.recentFilters": "Recent Filters", + "zowe.settings.version": "Track if migration has been processed", + "zowe.security.checkForCustomCredentialManagers": "Check for any installed VS Code extensions for handling credentials when activating Zowe Explorer", "zowe.security.secureCredentialsEnabled": "Allow credentials to be stored securely. If disabled and autoStore is set to true, z/OS credentials are stored as clear text in zowe.config.json.", "issueTsoCmd": "Issue TSO Command", "deleteProfile": "Delete Profile", @@ -148,6 +152,14 @@ "createZoweSchema.reload.infoMessage": "Team Configuration file created. Location: {0}. \n Please reload your window.", "copyFile": "Copy", "pasteFile": "Paste", + "jobs.sortBy": "Sort jobs...", + "ds.allPdsSort": "all PDS members in {0}", + "ds.singlePdsSort": "the PDS members in {0}", + "ds.selectFilterOpt": "Set a filter for {0}", + "ds.selectSortOpt": "Select a sorting option for {0}", + "jobs.selectSortOpt": "Select a sorting option for jobs in {0}", + "ds.filterBy": "Filter PDS members...", + "ds.sortBy": "Sort PDS members...", "issueUnixCmd": "Issue Unix Command", "selectForCompare": "Select for Compare", "compareWithSelected": "Compare with Selected", diff --git a/packages/zowe-explorer/resources/dark/history.svg b/packages/zowe-explorer/resources/dark/history.svg new file mode 100644 index 0000000000..d82fe00e64 --- /dev/null +++ b/packages/zowe-explorer/resources/dark/history.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/zowe-explorer/resources/light/history.svg b/packages/zowe-explorer/resources/light/history.svg new file mode 100644 index 0000000000..0aded818e8 --- /dev/null +++ b/packages/zowe-explorer/resources/light/history.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/zowe-explorer/src/PersistentFilters.ts b/packages/zowe-explorer/src/PersistentFilters.ts index a5d5b162d3..af4bcc317a 100644 --- a/packages/zowe-explorer/src/PersistentFilters.ts +++ b/packages/zowe-explorer/src/PersistentFilters.ts @@ -221,6 +221,16 @@ export class PersistentFilters { return this.updateFileHistory(); } + public removeSearchHistory(name: string): Thenable { + const index = this.mSearchHistory.findIndex((searchHistoryItem) => { + return searchHistoryItem.includes(name); + }); + if (index >= 0) { + this.mSearchHistory.splice(index, 1); + } + return this.updateSearchHistory(); + } + /*********************************************************************************************************************************************/ /* Reset functions, for resetting the persistent array to empty (in the extension and in settings.json) /*********************************************************************************************************************************************/ diff --git a/packages/zowe-explorer/src/Profiles.ts b/packages/zowe-explorer/src/Profiles.ts index 1caf0e17c8..68da1479c1 100644 --- a/packages/zowe-explorer/src/Profiles.ts +++ b/packages/zowe-explorer/src/Profiles.ts @@ -30,13 +30,14 @@ import { getZoweDir, IRegisterClient, } from "@zowe/zowe-explorer-api"; -import { errorHandling, FilterDescriptor, FilterItem, isUsingTokenAuth } from "./utils/ProfilesUtils"; +import { errorHandling, FilterDescriptor, FilterItem, ProfilesUtils } from "./utils/ProfilesUtils"; import { ZoweExplorerApiRegister } from "./ZoweExplorerApiRegister"; import { ZoweExplorerExtender } from "./ZoweExplorerExtender"; import * as globals from "./globals"; import * as nls from "vscode-nls"; import { SettingsConfig } from "./utils/SettingsConfig"; import { ZoweLogger } from "./utils/LoggerUtils"; +import { TreeProviders } from "./shared/TreeProviders"; // Set up localization nls.config({ @@ -68,6 +69,10 @@ export class Profiles extends ProfilesCache { private jobsSchema: string = globals.SETTINGS_JOBS_HISTORY; private mProfileInfo: zowe.imperative.ProfileInfo; private profilesOpCancelled = localize("profiles.operation.cancelled", "Operation Cancelled"); + private manualEditMsg = localize( + "profiles.manualEditMsg", + "The Team configuration file has been opened in the editor. Editing or removal of profiles will need to be done manually." + ); public constructor(log: zowe.imperative.Logger, cwd?: string) { super(log, cwd); } @@ -88,7 +93,7 @@ export class Profiles extends ProfilesCache { public async checkCurrentProfile(theProfile: zowe.imperative.IProfileLoaded): Promise { ZoweLogger.trace("Profiles.checkCurrentProfile called."); let profileStatus: IProfileValidation; - const usingTokenAuth = await isUsingTokenAuth(theProfile.name); + const usingTokenAuth = await ProfilesUtils.isUsingTokenAuth(theProfile.name); if (usingTokenAuth && !theProfile.profile.tokenType) { const error = new zowe.imperative.ImperativeError({ @@ -111,7 +116,7 @@ export class Profiles extends ProfilesCache { ); let values: string[]; try { - values = await Profiles.getInstance().promptCredentials(theProfile?.name); + values = await Profiles.getInstance().promptCredentials(theProfile); } catch (error) { await errorHandling(error, theProfile.name, error.message); return profileStatus; @@ -273,8 +278,9 @@ export class Profiles extends ProfilesCache { ZoweLogger.trace("Profiles.createZoweSession called."); let profileNamesList: string[] = []; const treeType = zoweFileProvider.getTreeType(); - const allProfiles = Profiles.getInstance().allProfiles; + let allProfiles: zowe.imperative.IProfileLoaded[]; try { + allProfiles = Profiles.getInstance().allProfiles; if (allProfiles) { // Get all profiles and filter to list of the APIs available for current tree explorer profileNamesList = allProfiles @@ -352,7 +358,6 @@ export class Profiles extends ProfilesCache { } else { quickpick.items = [configPick, ...items]; } - quickpick.placeholder = addProfilePlaceholder; quickpick.ignoreFocusOut = true; quickpick.show(); @@ -465,10 +470,12 @@ export class Profiles extends ProfilesCache { const impConfig: zowe.imperative.IImperativeConfig = zowe.getImperativeConfig(); const knownCliConfig: zowe.imperative.ICommandProfileTypeConfiguration[] = impConfig.profiles; - // add extenders config info from global variable - globals.EXTENDER_CONFIG.forEach((item) => { + + const extenderinfo = this.getConfigArray(); + extenderinfo.forEach((item) => { knownCliConfig.push(item); }); + knownCliConfig.push(impConfig.baseProfile); config.setSchema(zowe.imperative.ConfigSchema.buildSchema(knownCliConfig)); @@ -507,32 +514,32 @@ export class Profiles extends ProfilesCache { public async editZoweConfigFile(): Promise { ZoweLogger.trace("Profiles.editZoweConfigFile called."); const existingLayers = await this.getConfigLayers(); - if (existingLayers) { - if (existingLayers.length === 1) { - await this.openConfigFile(existingLayers[0].path); - } - if (existingLayers.length > 1) { - const choice = await this.getConfigLocationPrompt("edit"); - switch (choice) { - case "project": - for (const file of existingLayers) { - if (file.user) { - await this.openConfigFile(file.path); - } + if (existingLayers.length === 1) { + await this.openConfigFile(existingLayers[0].path); + Gui.showMessage(this.manualEditMsg); + } + if (existingLayers && existingLayers.length > 1) { + const choice = await this.getConfigLocationPrompt("edit"); + switch (choice) { + case "project": + for (const file of existingLayers) { + if (file.user) { + await this.openConfigFile(file.path); } - break; - case "global": - for (const file of existingLayers) { - if (file.global) { - await this.openConfigFile(file.path); - } + } + Gui.showMessage(this.manualEditMsg); + break; + case "global": + for (const file of existingLayers) { + if (file.global) { + await this.openConfigFile(file.path); } - break; - default: - Gui.showMessage(this.profilesOpCancelled); - return; - } - return; + } + Gui.showMessage(this.manualEditMsg); + break; + default: + Gui.showMessage(this.profilesOpCancelled); + break; } } } @@ -706,8 +713,10 @@ export class Profiles extends ProfilesCache { serviceProfile = this.loadNamedProfile(label.trim()); } // This check will handle service profiles that have username and password - if (serviceProfile.profile.user && serviceProfile.profile.password) { - Gui.showMessage(localize("ssoAuth.noBase", "This profile does not support token authentication.")); + if (ProfilesUtils.isProfileUsingBasicAuth(serviceProfile)) { + Gui.showMessage( + localize("ssoAuth.usingBasicAuth", "This profile is using basic authentication and does not support token authentication.") + ); return; } @@ -715,7 +724,7 @@ export class Profiles extends ProfilesCache { loginTokenType = await ZoweExplorerApiRegister.getInstance().getCommonApi(serviceProfile).getTokenTypeName(); } catch (error) { ZoweLogger.warn(error); - Gui.showMessage(localize("ssoAuth.noBase", "This profile does not support token authentication.")); + Gui.showMessage(localize("ssoLogin.tokenType.error", "Error getting supported tokenType value for profile {0}", serviceProfile.name)); return; } try { @@ -724,7 +733,6 @@ export class Profiles extends ProfilesCache { } else { await this.loginWithBaseProfile(serviceProfile, loginTokenType, node); } - Gui.showMessage(localize("ssoLogin.successful", "Login to authentication service was successful.")); } catch (err) { const message = localize("ssoLogin.error", "Unable to log in with {0}. {1}", serviceProfile.name, err?.message); ZoweLogger.error(message); @@ -733,15 +741,81 @@ export class Profiles extends ProfilesCache { } } + public clearDSFilterFromTree(node: IZoweNodeType): void { + if (!TreeProviders.ds?.mSessionNodes || !TreeProviders.ds?.mSessionNodes.length) { + return; + } + const dsNode: IZoweDatasetTreeNode = TreeProviders.ds.mSessionNodes.find( + (sessionNode: IZoweDatasetTreeNode) => sessionNode.getProfile()?.name === node.getProfile()?.name + ); + if (!dsNode) { + return; + } + dsNode.tooltip &&= node.getProfile()?.name; + dsNode.description &&= ""; + dsNode.pattern &&= ""; + TreeProviders.ds.flipState(dsNode, false); + TreeProviders.ds.refreshElement(dsNode); + } + + public clearUSSFilterFromTree(node: IZoweNodeType): void { + if (!TreeProviders.uss?.mSessionNodes || !TreeProviders.uss?.mSessionNodes.length) { + return; + } + const ussNode: IZoweUSSTreeNode = TreeProviders.uss.mSessionNodes.find( + (sessionNode: IZoweUSSTreeNode) => sessionNode.getProfile()?.name === node.getProfile()?.name + ); + if (!ussNode) { + return; + } + ussNode.tooltip &&= node.getProfile()?.name; + ussNode.description &&= ""; + ussNode.fullPath &&= ""; + TreeProviders.uss.flipState(ussNode, false); + TreeProviders.uss.refreshElement(ussNode); + } + + public clearJobFilterFromTree(node: IZoweNodeType): void { + if (!TreeProviders.job?.mSessionNodes || !TreeProviders.job?.mSessionNodes.length) { + return; + } + const jobNode: IZoweJobTreeNode = TreeProviders.job.mSessionNodes.find( + (sessionNode: IZoweJobTreeNode) => sessionNode.getProfile()?.name === node.getProfile()?.name + ); + if (!jobNode) { + return; + } + jobNode.tooltip &&= node.getProfile()?.name; + jobNode.description &&= ""; + jobNode.owner &&= ""; + jobNode.prefix &&= ""; + jobNode.status &&= ""; + jobNode.filtered &&= false; + jobNode.children &&= []; + TreeProviders.job.flipState(jobNode, false); + TreeProviders.job.refreshElement(jobNode); + } + + public clearFilterFromAllTrees(node: IZoweNodeType): void { + this.clearDSFilterFromTree(node); + this.clearUSSFilterFromTree(node); + this.clearJobFilterFromTree(node); + } + public async ssoLogout(node: IZoweNodeType): Promise { ZoweLogger.trace("Profiles.ssoLogout called."); const serviceProfile = node.getProfile(); // This check will handle service profiles that have username and password - if (serviceProfile.profile?.user && serviceProfile.profile?.password) { - Gui.showMessage(localize("ssoAuth.noBase", "This profile does not support token authentication.")); + if (ProfilesUtils.isProfileUsingBasicAuth(serviceProfile)) { + Gui.showMessage( + localize("ssoAuth.usingBasicAuth", "This profile is using basic authentication and does not support token authentication.") + ); return; } + try { + this.clearFilterFromAllTrees(node); + // this will handle extenders if (serviceProfile.type !== "zosmf" && serviceProfile.profile?.tokenType !== zowe.imperative.SessConstants.TOKEN_TYPE_APIML) { await ZoweExplorerApiRegister.getInstance() @@ -788,13 +862,16 @@ export class Profiles extends ProfilesCache { if (!profileName) { return []; } - if ((await this.getProfileInfo()).usingTeamConfig) { + const usingSecureCreds = !SettingsConfig.getDirectValue(globals.SETTINGS_SECURE_CREDENTIALS_ENABLED); + if ((await this.getProfileInfo()).usingTeamConfig && !usingSecureCreds) { const config = (await this.getProfileInfo()).getTeamConfig(); return config.api.secure.securePropsForProfile(profileName); } const profAttrs = await this.getProfileFromConfig(profileName); const mergedArgs = (await this.getProfileInfo()).mergeArgsForProfile(profAttrs); - return mergedArgs.knownArgs.filter((arg) => arg.secure).map((arg) => arg.argName); + return mergedArgs.knownArgs + .filter((arg) => arg.secure || arg.argName === "tokenType" || arg.argName === "tokenValue") + .map((arg) => arg.argName); } private async loginWithBaseProfile(serviceProfile: zowe.imperative.IProfileLoaded, loginTokenType: string, node?: IZoweNodeType): Promise { @@ -827,6 +904,7 @@ export class Profiles extends ProfilesCache { profile: { ...node.getProfile().profile, ...updBaseProfile }, }); } + Gui.showMessage(localize("ssoLogin.successful", "Login to authentication service was successful.")); } } @@ -852,6 +930,7 @@ export class Profiles extends ProfilesCache { profile: { ...node.getProfile().profile, ...session }, }); } + Gui.showMessage(localize("ssoLogin.successful", "Login to authentication service was successful.")); } private async getConfigLocationPrompt(action: string): Promise { diff --git a/packages/zowe-explorer/src/ZoweExplorerExtender.ts b/packages/zowe-explorer/src/ZoweExplorerExtender.ts index 2878e9c4c6..4afcf95a22 100644 --- a/packages/zowe-explorer/src/ZoweExplorerExtender.ts +++ b/packages/zowe-explorer/src/ZoweExplorerExtender.ts @@ -190,10 +190,8 @@ export class ZoweExplorerExtender implements IApiExplorerExtender, ZoweExplorerT }); } } - // add extender config info to global variable - profileTypeConfigurations?.forEach((item) => { - globals.EXTENDER_CONFIG.push(item); - }); + if (profileTypeConfigurations !== undefined) Profiles.getInstance().addToConfigArray(profileTypeConfigurations); + // sequentially reload the internal profiles cache to satisfy all the newly added profile types await ZoweExplorerExtender.refreshProfilesQueue.add(async (): Promise => { await Profiles.getInstance().refresh(); diff --git a/packages/zowe-explorer/src/abstract/ZoweTreeProvider.ts b/packages/zowe-explorer/src/abstract/ZoweTreeProvider.ts index f109e69a51..155419fc24 100644 --- a/packages/zowe-explorer/src/abstract/ZoweTreeProvider.ts +++ b/packages/zowe-explorer/src/abstract/ZoweTreeProvider.ts @@ -85,6 +85,16 @@ export class ZoweTreeProvider { } } + /** + * Fire the "onDidChangeTreeData" event to signal that a node in the tree has changed. + * Unlike `refreshElement`, this function does *not* signal a refresh for the given node - + * it simply tells VS Code to repaint the node in the tree. + * @param node The node that should be repainted + */ + public nodeDataChanged(node: IZoweTreeNode): void { + this.mOnDidChangeTreeData.fire(node); + } + /** * Called whenever the tree needs to be refreshed, and fires the data change event * diff --git a/packages/zowe-explorer/src/dataset/DatasetTree.ts b/packages/zowe-explorer/src/dataset/DatasetTree.ts index 14e12bbc09..8147976e00 100644 --- a/packages/zowe-explorer/src/dataset/DatasetTree.ts +++ b/packages/zowe-explorer/src/dataset/DatasetTree.ts @@ -15,28 +15,34 @@ import * as nls from "vscode-nls"; import * as globals from "../globals"; import * as dsActions from "./actions"; import { - Gui, DataSetAllocTemplate, + Gui, ValidProfileEnum, IZoweTree, IZoweDatasetTreeNode, PersistenceSchemaEnum, NodeInteraction, IZoweTreeNode, + DatasetFilter, + DatasetSortOpts, + SortDirection, + NodeSort, + DatasetFilterOpts, } from "@zowe/zowe-explorer-api"; import { Profiles } from "../Profiles"; import { ZoweExplorerApiRegister } from "../ZoweExplorerApiRegister"; import { FilterDescriptor, FilterItem, errorHandling, syncSessionNode } from "../utils/ProfilesUtils"; -import { sortTreeItems, getAppName, getDocumentFilePath } from "../shared/utils"; +import { sortTreeItems, getAppName, getDocumentFilePath, SORT_DIRS } from "../shared/utils"; import { ZoweTreeProvider } from "../abstract/ZoweTreeProvider"; import { ZoweDatasetNode } from "./ZoweDatasetNode"; import { getIconById, getIconByNode, IconId, IIconItem } from "../generators/icons"; +import * as dayjs from "dayjs"; import * as fs from "fs"; import * as contextually from "../shared/context"; import { resetValidationSettings } from "../shared/actions"; import { closeOpenedTextFile } from "../utils/workspace"; import { IDataSet, IListOptions, imperative } from "@zowe/cli"; -import { validateDataSetName, validateMemberName } from "./utils"; +import { DATASET_FILTER_OPTS, DATASET_SORT_OPTS, validateDataSetName, validateMemberName } from "./utils"; import { SettingsConfig } from "../utils/SettingsConfig"; import { ZoweLogger } from "../utils/LoggerUtils"; import { TreeViewUtils } from "../utils/TreeViewUtils"; @@ -53,9 +59,9 @@ const localize: nls.LocalizeFunc = nls.loadMessageBundle(); * * @export */ -export async function createDatasetTree(): Promise { +export async function createDatasetTree(log: imperative.Logger): Promise { const tree = new DatasetTree(); - tree.initializeFavorites(); + tree.initializeFavorites(log); await tree.addSession(); return tree; } @@ -96,6 +102,7 @@ export class DatasetTree extends ZoweTreeProvider implements IZoweTree { + public async loadProfilesForFavorites(log: imperative.Logger, parentNode: IZoweDatasetTreeNode): Promise { ZoweLogger.trace("DatasetTree.loadProfilesForFavorites called."); const profileName = parentNode.label as string; const updatedFavsForProfile: IZoweDatasetTreeNode[] = []; let profile: imperative.IProfileLoaded; let session: imperative.Session; + this.log = log; ZoweLogger.debug(localize("loadProfilesForFavorites.log.debug", "Loading profile: {0} for data set favorites", profileName)); // Load profile for parent profile node in this.mFavorites array if (!parentNode.getProfile() || !parentNode.getSession()) { @@ -755,6 +764,21 @@ export class DatasetTree extends ZoweTreeProvider implements IZoweTree 0) { + // children nodes already exist, sort and repaint to avoid extra refresh + for (const c of node.children) { + if (contextually.isPds(c) && c.children) { + c.sort = node.sort; + for (const ch of c.children) { + // remove any descriptions from child nodes + ch.description = ""; + } + + c.children.sort(ZoweDatasetNode.sortBy(node.sort)); + this.nodeDataChanged(c); + } + } + } + } else if (node.children?.length > 0) { + for (const c of node.children) { + // remove any descriptions from child nodes + c.description = ""; + } + // children nodes already exist, sort and repaint to avoid extra refresh + node.children.sort(ZoweDatasetNode.sortBy(node.sort)); + this.nodeDataChanged(node); + } + } + + /** + * Presents a dialog to the user with options and methods for sorting PDS members. + * @param node The node that was interacted with (via icon or right-click -> "Sort PDS members...") + */ + public async sortPdsMembersDialog(node: IZoweDatasetTreeNode): Promise { + const isSession = contextually.isSession(node); + + // Assume defaults if a user hasn't selected any sort options yet + const sortOpts = node.sort ?? { + method: DatasetSortOpts.Name, + direction: SortDirection.Ascending, + }; + + // Adapt menus to user based on the node that was interacted with + const specifier = isSession + ? localize("ds.allPdsSort", "all PDS members in {0}", node.label as string) + : localize("ds.singlePdsSort", "the PDS members in {0}", node.label as string); + const selection = await Gui.showQuickPick( + DATASET_SORT_OPTS.map((opt, i) => ({ + label: sortOpts.method === i ? `${opt} $(check)` : opt, + description: i === DATASET_SORT_OPTS.length - 1 ? SORT_DIRS[sortOpts.direction] : null, + })), + { + placeHolder: localize("ds.selectSortOpt", "Select a sorting option for {0}", specifier), + } + ); + if (selection == null) { + return; + } + + if (selection.label === localize("setSortDirection", "$(fold) Sort Direction")) { + // Update sort direction (if a new one was provided) + const dir = await Gui.showQuickPick(SORT_DIRS, { + placeHolder: localize("sort.selectDirection", "Select a sorting direction"), + }); + if (dir != null) { + node.sort = { + ...sortOpts, + direction: SORT_DIRS.indexOf(dir), + }; + } + await this.sortPdsMembersDialog(node); + return; + } + + const selectionText = selection.label.replace(" $(check)", ""); + const sortMethod = DATASET_SORT_OPTS.indexOf(selectionText); + if (sortMethod === -1) { + return; + } + + // Update sort for node based on selections + this.updateSortForNode(node, { ...sortOpts, method: sortMethod }, isSession); + Gui.setStatusBarMessage(localize("sort.updated", "$(check) Sorting updated for {0}", node.label as string), globals.MS_PER_SEC * 4); + } + + /** + * Updates or resets the filter for a given data set node. + * @param node The node whose filter should be updated/reset + * @param newFilter Either a valid `DatasetFilter` object, or `null` to reset the filter + * @param isSession Whether the node is a session + */ + public updateFilterForNode(node: IZoweDatasetTreeNode, newFilter: DatasetFilter | null, isSession: boolean): void { + const oldFilter = node.filter; + node.filter = newFilter; + node.description = newFilter ? localize("filter.description", "Filter: {0}", newFilter.value) : null; + this.nodeDataChanged(node); + + // if a session was selected, apply this sort to all PDS members + if (isSession) { + if (node.children?.length > 0) { + // children nodes already exist, sort and repaint to avoid extra refresh + for (const c of node.children) { + const asDs = c as IZoweDatasetTreeNode; + + // PDS-level filters should have precedence over a session-level filter + if (asDs.filter != null) { + continue; + } + + if (contextually.isPds(c)) { + // If there was an old session-wide filter set: refresh to get any + // missing nodes - new filter will be applied + if (oldFilter != null) { + this.refreshElement(c); + continue; + } + + if (newFilter != null && c.children?.length > 0) { + c.children = c.children.filter(ZoweDatasetNode.filterBy(newFilter)); + this.nodeDataChanged(c); + } else { + this.refreshElement(c); + } + } + } + } + return; + } + + // Updating filter for PDS node + // if a filter was already set for either session or PDS, just refresh to grab any missing nodes + const sessionFilterPresent = (node.getSessionNode() as IZoweDatasetTreeNode).filter; + if (oldFilter != null || sessionFilterPresent != null) { + this.refreshElement(node); + return; + } + + // since there wasn't a previous filter, sort and repaint existing nodes + if (newFilter != null && node.children?.length > 0) { + node.children = node.children.filter(ZoweDatasetNode.filterBy(newFilter)); + this.nodeDataChanged(node); + } + } + + /** + * Presents a dialog to the user with options and methods for sorting PDS members. + * @param node The data set node that was interacted with (via icon or right-click => "Filter PDS members...") + */ + public async filterPdsMembersDialog(node: IZoweDatasetTreeNode): Promise { + const isSession = contextually.isSession(node); + + // Adapt menus to user based on the node that was interacted with + const specifier = isSession + ? localize("ds.allPdsSort", "all PDS members in {0}", node.label as string) + : localize("ds.singlePdsSort", "the PDS members in {0}", node.label as string); + const clearFilter = isSession + ? localize("ds.clearProfileFilter", "$(clear-all) Clear filter for profile") + : localize("ds.clearPdsFilter", "$(clear-all) Clear filter for PDS"); + const selection = ( + await Gui.showQuickPick( + [...DATASET_FILTER_OPTS.map((sortOpt, i) => (node.filter?.method === i ? `${sortOpt} $(check)` : sortOpt)), clearFilter], + { + placeHolder: localize("ds.selectFilterOpt", "Set a filter for {0}", specifier), + } + ) + )?.replace(" $(check)", ""); + + const filterMethod = DATASET_FILTER_OPTS.indexOf(selection); + + const userDismissed = filterMethod < 0; + if (userDismissed || selection === clearFilter) { + if (selection === clearFilter) { + this.updateFilterForNode(node, null, isSession); + Gui.setStatusBarMessage(localize("filter.cleared", "$(check) Filter cleared for {0}", node.label as string), globals.MS_PER_SEC * 4); + } + return; + } + + const dateValidation = (value): string => { + return dayjs(value).isValid() ? null : localize("ds.filterEntry.invalidDate", "Invalid date format specified"); + }; + + const filter = await Gui.showInputBox({ + title: localize("ds.filterEntry.title", "Enter a value to filter by"), + placeHolder: "", + validateInput: + filterMethod === DatasetFilterOpts.LastModified + ? dateValidation + : (val): string => (val.length > 0 ? null : localize("ds.filterEntry.invalid", "Invalid filter specified")), + }); + + // User dismissed filter entry, go back to filter selection + if (filter == null) { + await this.filterPdsMembersDialog(node); + return; + } + + // Update filter for node based on selection & filter entry + this.updateFilterForNode( + node, + { + method: filterMethod, + value: filter, + }, + isSession + ); + Gui.setStatusBarMessage(localize("filter.updated", "$(check) Filter updated for {0}", node.label as string), globals.MS_PER_SEC * 4); + } } diff --git a/packages/zowe-explorer/src/dataset/ZoweDatasetNode.ts b/packages/zowe-explorer/src/dataset/ZoweDatasetNode.ts index 2e966abb34..30242cb10c 100644 --- a/packages/zowe-explorer/src/dataset/ZoweDatasetNode.ts +++ b/packages/zowe-explorer/src/dataset/ZoweDatasetNode.ts @@ -13,13 +13,26 @@ import * as zowe from "@zowe/cli"; import * as vscode from "vscode"; import * as globals from "../globals"; import { errorHandling } from "../utils/ProfilesUtils"; -import { Gui, NodeAction, IZoweDatasetTreeNode, ZoweTreeNode } from "@zowe/zowe-explorer-api"; +import { + DatasetFilter, + DatasetFilterOpts, + DatasetSortOpts, + DatasetStats, + Gui, + NodeAction, + IZoweDatasetTreeNode, + ZoweTreeNode, + SortDirection, + NodeSort, +} from "@zowe/zowe-explorer-api"; import { ZoweExplorerApiRegister } from "../ZoweExplorerApiRegister"; import { getIconByNode } from "../generators/icons"; import * as contextually from "../shared/context"; import * as nls from "vscode-nls"; import { Profiles } from "../Profiles"; import { ZoweLogger } from "../utils/LoggerUtils"; +import * as dayjs from "dayjs"; + // Set up localization nls.config({ messageFormat: nls.MessageFormat.bundle, @@ -43,6 +56,9 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod public errorDetails: zowe.imperative.ImperativeError; public ongoingActions: Record> = {}; public wasDoubleClicked: boolean = false; + public stats: DatasetStats; + public sort?: NodeSort; + public filter?: DatasetFilter; /** * Creates an instance of ZoweDatasetNode @@ -77,8 +93,17 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod if (icon) { this.iconPath = icon.path; } - if (!globals.ISTHEIA && this.getParent() && contextually.isSession(this.getParent())) { - this.id = `${mParent?.id ?? mParent?.label?.toString() ?? ""}.${this.label as string}`; + + if (this.getParent() == null) { + // set default sort options for session nodes + this.sort = { + method: DatasetSortOpts.Name, + direction: SortDirection.Ascending, + }; + } + + if (!globals.ISTHEIA && contextually.isSession(this)) { + this.id = this.label as string; } } @@ -93,6 +118,22 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod return this.getProfile() ? this.getProfile().name : undefined; } + public updateStats(item: any): void { + if ("m4date" in item) { + const { m4date, mtime, msec }: { m4date: string; mtime: string; msec: string } = item; + this.stats = { + user: item.user, + modifiedDate: dayjs(`${m4date} ${mtime}:${msec}`).toDate(), + }; + } else if ("id" in item || "changed" in item) { + // missing keys from API response; check for FTP keys + this.stats = { + user: item.id, + modifiedDate: item.changed ? dayjs(item.changed).toDate() : undefined, + }; + } + } + /** * Retrieves child nodes of this ZoweDatasetNode * @@ -142,8 +183,10 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod // Loops through all the returned dataset members and creates nodes for them for (const item of response.apiResponse.items ?? response.apiResponse) { - const existing = this.children.find((element) => element.label.toString() === item.dsname); + const dsEntry = item.dsname ?? item.member; + const existing = this.children.find((element) => element.label.toString() === dsEntry); if (existing) { + existing.updateStats(item); elementChildren[existing.label.toString()] = existing; // Creates a ZoweDatasetNode for a PDS } else if (item.dsorg === "PO" || item.dsorg === "PO-E") { @@ -235,6 +278,9 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod msg: localize("getChildren.invalidMember", "Cannot access member with control characters in the name: {0}", item.member), }); } + + // get user and last modified date for sorting, if available + temp.updateStats(item); elementChildren[temp.label.toString()] = temp; } } @@ -253,16 +299,113 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod ]; } else { const newChildren = Object.keys(elementChildren) - .sort() .filter((label) => this.children.find((c) => (c.label as string) === label) == null) .map((label) => elementChildren[label]); - this.children = this.children.concat(newChildren).filter((c) => (c.label as string) in elementChildren); + // get sort settings for session + const sessionSort = contextually.isSession(this) ? this.sort : this.getSessionNode().sort; + + // use the PDS sort settings if defined; otherwise, use session sort method + const sortOpts = this.sort ?? sessionSort; + + // use the PDS filter if one is set, otherwise try using the session filter + const sessionFilter = contextually.isSession(this) ? this.filter : this.getSessionNode().filter; + const filter = this.filter ?? sessionFilter; + + this.children = this.children + .concat(newChildren) + .filter((c) => (c.label as string) in elementChildren) + .filter(filter ? ZoweDatasetNode.filterBy(filter) : (_c): boolean => true) + .sort(ZoweDatasetNode.sortBy(sortOpts)); } return this.children; } + /** + * Returns a sorting function based on the given sorting method. + * If the nodes are not PDS members, it will simply sort by name. + * @param method The sorting method to use + * @returns A function that sorts 2 nodes based on the given sorting method + */ + public static sortBy(sort: NodeSort): (a: IZoweDatasetTreeNode, b: IZoweDatasetTreeNode) => number { + return (a, b): number => { + const aParent = a.getParent(); + if (aParent == null || !contextually.isPds(aParent)) { + return (a.label as string) < (b.label as string) ? -1 : 1; + } + + const sortLessThan = sort.direction == SortDirection.Ascending ? -1 : 1; + const sortGreaterThan = sortLessThan * -1; + + const sortByName = (nodeA: IZoweDatasetTreeNode, nodeB: IZoweDatasetTreeNode): number => + (nodeA.label as string) < (nodeB.label as string) ? sortLessThan : sortGreaterThan; + + if (!a.stats && !b.stats) { + return sortByName(a, b); + } + + if (sort.method === DatasetSortOpts.LastModified) { + const dateA = dayjs(a.stats?.modifiedDate); + const dateB = dayjs(b.stats?.modifiedDate); + + a.description = dateA.isValid() ? dateA.format("YYYY/MM/DD HH:mm:ss") : undefined; + b.description = dateB.isValid() ? dateB.format("YYYY/MM/DD HH:mm:ss") : undefined; + + // for dates that are equal down to the second, fallback to sorting by name + if (dateA.isSame(dateB, "second")) { + return sortByName(a, b); + } + + return dateA.isBefore(dateB, "second") ? sortLessThan : sortGreaterThan; + } else if (sort.method === DatasetSortOpts.UserId) { + const userA = a.stats?.user ?? ""; + const userB = b.stats?.user ?? ""; + + a.description = userA; + b.description = userB; + + if (userA === userB) { + return sortByName(a, b); + } + + return userA < userB ? sortLessThan : sortGreaterThan; + } + + return sortByName(a, b); + }; + } + + /** + * Returns a filter function based on the given method. + * If the nodes are not PDS members, it will not filter those nodes. + * @param method The sorting method to use + * @returns A function that sorts 2 nodes based on the given sorting method + */ + public static filterBy(filter: DatasetFilter): (node: IZoweDatasetTreeNode) => boolean { + const isDateFilter = (f: string): boolean => { + return dayjs(f).isValid(); + }; + + return (node): boolean => { + const aParent = node.getParent(); + if (aParent == null || !contextually.isPds(aParent)) { + return true; + } + + switch (filter.method) { + case DatasetFilterOpts.LastModified: + if (!isDateFilter(filter.value)) { + return true; + } + + return dayjs(node.stats?.modifiedDate).isSame(filter.value, "day"); + case DatasetFilterOpts.UserId: + return node.stats?.user === filter.value; + } + }; + } + public getSessionNode(): IZoweDatasetTreeNode { ZoweLogger.trace("ZoweDatasetNode.getSessionNode called."); return this.getParent() ? this.getParent().getSessionNode() : this; @@ -305,7 +448,7 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod ), ]; const mvsApi = ZoweExplorerApiRegister.getMvsApi(cachedProfile); - if (!mvsApi.getSession(mvsApi?.profile)) { + if (!mvsApi.getSession(cachedProfile)) { throw new zowe.imperative.ImperativeError({ msg: localize("getDataSets.error.sessionMissing", "Profile auth error"), additionalDetails: localize("getDataSets.error.additionalDetails", "Profile is not authenticated, please log in to continue"), diff --git a/packages/zowe-explorer/src/dataset/actions.ts b/packages/zowe-explorer/src/dataset/actions.ts index 13c9cde9a8..6726d51a56 100644 --- a/packages/zowe-explorer/src/dataset/actions.ts +++ b/packages/zowe-explorer/src/dataset/actions.ts @@ -966,22 +966,28 @@ export async function showAttributes(node: api.IZoweDatasetTreeNode, datasetProv } /** - * Submit the contents of the editor as JCL. + * Submit the contents of the editor or file as JCL. * * @export * @param datasetProvider DatasetTree object */ // This function does not appear to currently be made available in the UI -export async function submitJcl(datasetProvider: api.IZoweTree): Promise { +export async function submitJcl(datasetProvider: api.IZoweTree, file?: vscode.Uri): Promise { ZoweLogger.trace("dataset.actions.submitJcl called."); - if (!vscode.window.activeTextEditor) { - const errorMsg = localize("submitJcl.noDocumentOpen", "No editor with a document that could be submitted as JCL is currently open."); - api.Gui.errorMessage(errorMsg); - ZoweLogger.error(errorMsg); + if (!vscode.window.activeTextEditor && !file) { + const notActiveEditorMsg = localize( + "submitJcl.notActiveEditorMsg", + "No editor with a document that could be submitted as JCL is currently open." + ); + api.Gui.errorMessage(notActiveEditorMsg); + ZoweLogger.error(notActiveEditorMsg); return; } + if (file) { + await vscode.commands.executeCommand("filesExplorer.openFilePreserveFocus", file); + } const doc = vscode.window.activeTextEditor.document; - ZoweLogger.debug(localize("submitJcl.submitting", "Submitting JCL in document {0}", doc.fileName)); + ZoweLogger.debug(localize("submitJcl.submitting", "Submitting as JCL in document {0}", doc.fileName)); // get session name const sessionregex = /\[(.*)(\])(?!.*\])/g; const regExp = sessionregex.exec(doc.fileName); @@ -999,6 +1005,10 @@ export async function submitJcl(datasetProvider: api.IZoweTree> { ZoweLogger.trace("dataset.init.initDatasetProvider called."); - const datasetProvider: IZoweTree = await createDatasetTree(); + const datasetProvider = await createDatasetTree(globals.LOG); if (datasetProvider == null) { return null; } @@ -36,10 +37,10 @@ export async function initDatasetProvider(context: vscode.ExtensionContext): Pro ); context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.addSession", async () => datasetProvider.createZoweSession(datasetProvider))); context.subscriptions.push( - vscode.commands.registerCommand("zowe.ds.addFavorite", (node, nodeList) => { + vscode.commands.registerCommand("zowe.ds.addFavorite", async (node, nodeList) => { const selectedNodes = getSelectedNodeList(node, nodeList); for (const item of selectedNodes) { - datasetProvider.addFavorite(item); + await datasetProvider.addFavorite(item); } }) ); @@ -66,7 +67,7 @@ export async function initDatasetProvider(context: vscode.ExtensionContext): Pro } }) ); - context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.pattern", (node): void => datasetProvider.filterPrompt(node))); + context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.pattern", (node) => datasetProvider.filterPrompt(node))); context.subscriptions.push( vscode.commands.registerCommand("zowe.ds.editSession", async (node) => datasetProvider.editSession(node, datasetProvider)) ); @@ -125,12 +126,12 @@ export async function initDatasetProvider(context: vscode.ExtensionContext): Pro } }) ); - context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.saveSearch", (node): void => datasetProvider.addFavorite(node))); - context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.removeSavedSearch", (node): void => datasetProvider.removeFavorite(node))); + context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.saveSearch", (node) => datasetProvider.addFavorite(node))); + context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.removeSavedSearch", (node) => datasetProvider.removeFavorite(node))); context.subscriptions.push( - vscode.commands.registerCommand("zowe.ds.removeFavProfile", (node): void => datasetProvider.removeFavProfile(node.label, true)) + vscode.commands.registerCommand("zowe.ds.removeFavProfile", (node) => datasetProvider.removeFavProfile(node.label, true)) ); - context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.submitJcl", async () => dsActions.submitJcl(datasetProvider))); + context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.submitJcl", async (file) => dsActions.submitJcl(datasetProvider, file))); context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.submitMember", async (node) => dsActions.submitMember(node))); context.subscriptions.push( vscode.commands.registerCommand("zowe.ds.showAttributes", async (node, nodeList) => { @@ -142,7 +143,7 @@ export async function initDatasetProvider(context: vscode.ExtensionContext): Pro } }) ); - context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.renameDataSet", (node): void => datasetProvider.rename(node))); + context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.renameDataSet", (node) => datasetProvider.rename(node))); context.subscriptions.push( vscode.commands.registerCommand("zowe.ds.copyDataSets", async (node, nodeList) => dsActions.copyDataSets(node, nodeList, datasetProvider)) ); @@ -155,7 +156,7 @@ export async function initDatasetProvider(context: vscode.ExtensionContext): Pro await dsActions.refreshDataset(node.getParent(), datasetProvider); }) ); - context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.renameDataSetMember", (node): void => datasetProvider.rename(node))); + context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.renameDataSetMember", (node) => datasetProvider.rename(node))); context.subscriptions.push( vscode.commands.registerCommand("zowe.ds.hMigrateDataSet", async (node, nodeList) => { let selectedNodes = getSelectedNodeList(node, nodeList); @@ -195,11 +196,20 @@ export async function initDatasetProvider(context: vscode.ExtensionContext): Pro datasetProvider.refreshElement(node); }) ); - context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.ssoLogin", (node: IZoweTreeNode): void => datasetProvider.ssoLogin(node))); - context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.ssoLogout", (node: IZoweTreeNode): void => datasetProvider.ssoLogout(node))); + context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.ssoLogin", (node: IZoweTreeNode) => datasetProvider.ssoLogin(node))); + context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.ssoLogout", (node: IZoweTreeNode) => datasetProvider.ssoLogout(node))); context.subscriptions.push( - vscode.workspace.onDidChangeConfiguration((e) => { - datasetProvider.onDidChangeConfiguration(e); + vscode.commands.registerCommand("zowe.ds.sortBy", async (node: IZoweDatasetTreeNode) => datasetProvider.sortPdsMembersDialog(node)) + ); + context.subscriptions.push( + vscode.commands.registerCommand( + "zowe.ds.filterBy", + async (node: IZoweDatasetTreeNode): Promise => datasetProvider.filterPdsMembersDialog(node) + ) + ); + context.subscriptions.push( + vscode.workspace.onDidChangeConfiguration(async (e) => { + await datasetProvider.onDidChangeConfiguration(e); }) ); diff --git a/packages/zowe-explorer/src/dataset/utils.ts b/packages/zowe-explorer/src/dataset/utils.ts index 1debf45d39..956ae28f31 100644 --- a/packages/zowe-explorer/src/dataset/utils.ts +++ b/packages/zowe-explorer/src/dataset/utils.ts @@ -10,9 +10,26 @@ */ import * as globals from "../globals"; +import * as nls from "vscode-nls"; import { IZoweNodeType } from "@zowe/zowe-explorer-api"; import { ZoweLogger } from "../utils/LoggerUtils"; +// Set up localization +nls.config({ + messageFormat: nls.MessageFormat.bundle, + bundleFormat: nls.BundleFormat.standalone, +})(); +const localize: nls.LocalizeFunc = nls.loadMessageBundle(); + +export const DATASET_SORT_OPTS = [ + localize("ds.sortByName", "$(case-sensitive) Name (default)"), + localize("ds.sortByModified", "$(calendar) Date Modified"), + localize("ds.sortByUserId", "$(account) User ID"), + localize("setSortDirection", "$(fold) Sort Direction"), +]; + +export const DATASET_FILTER_OPTS = [localize("ds.sortByModified", "$(calendar) Date Modified"), localize("ds.sortByUserId", "$(account) User ID")]; + export function getProfileAndDataSetName(node: IZoweNodeType): { profileName: string; dataSetName: string; diff --git a/packages/zowe-explorer/src/extension.ts b/packages/zowe-explorer/src/extension.ts index 0a520ab37c..5018b889f3 100644 --- a/packages/zowe-explorer/src/extension.ts +++ b/packages/zowe-explorer/src/extension.ts @@ -19,14 +19,15 @@ import { ProfilesUtils } from "./utils/ProfilesUtils"; import { initializeSpoolProvider } from "./SpoolProvider"; import { cleanTempDir, hideTempFolder } from "./utils/TempFolder"; import { SettingsConfig } from "./utils/SettingsConfig"; -import { initDatasetProvider } from "./dataset/init"; -import { initUSSProvider } from "./uss/init"; -import { initJobsProvider } from "./job/init"; -import { IZoweProviders, registerCommonCommands, registerRefreshCommand, watchConfigProfile } from "./shared/init"; +import { registerCommonCommands, registerCredentialManager, registerRefreshCommand, watchConfigProfile } from "./shared/init"; import { ZoweLogger } from "./utils/LoggerUtils"; import { ZoweSaveQueue } from "./abstract/ZoweSaveQueue"; import { PollDecorator } from "./utils/DecorationProviders"; import { ZoweLocalStorage } from "./utils/ZoweLocalStorage"; +import { TreeProviders } from "./shared/TreeProviders"; +import { initDatasetProvider } from "./dataset/init"; +import { initUSSProvider } from "./uss/init"; +import { initJobsProvider } from "./job/init"; /** * The function that runs when the extension is loaded @@ -45,6 +46,7 @@ export async function activate(context: vscode.ExtensionContext): Promise ZoweExplorerExtender.showZoweConfigError(msg)); ProfilesUtils.initializeZoweTempFolder(); @@ -55,11 +57,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { +export async function createJobsTree(log: imperative.Logger): Promise { ZoweLogger.trace("ZosJobsProvider.createJobsTree called."); const tree = new ZosJobsProvider(); - await tree.initializeJobsTree(); + await tree.initializeJobsTree(log); await tree.addSession(); return tree; } @@ -139,6 +139,7 @@ export class ZosJobsProvider extends ZoweTreeProvider implements IZoweTree { + public async initializeJobsTree(log: imperative.Logger): Promise { ZoweLogger.trace("ZosJobsProvider.initializeJobsTree called."); + this.log = log; ZoweLogger.debug(localize("initializeJobsTree.log.debug", "Initializing profiles with jobs favorites.")); const lines: string[] = this.mHistory.readFavorites(); if (lines.length === 0) { @@ -400,12 +401,13 @@ export class ZosJobsProvider extends ZoweTreeProvider implements IZoweTree { + public async loadProfilesForFavorites(log: imperative.Logger, parentNode: IZoweJobTreeNode): Promise { ZoweLogger.trace("ZosJobsProvider.loadProfilesForFavorites called."); const profileName = parentNode.label as string; const updatedFavsForProfile: IZoweJobTreeNode[] = []; let profile: imperative.IProfileLoaded; let session: imperative.Session; + this.log = log; ZoweLogger.debug(localize("loadProfilesForFavorites.log.debug", "Loading profile: {0} for jobs favorites", profileName)); // Load profile for parent profile node in this.mFavorites array if (!parentNode.getProfile() || !parentNode.getSession()) { @@ -626,6 +628,31 @@ export class ZosJobsProvider extends ZoweTreeProvider implements IZoweTree { ZoweLogger.trace("ZosJobsProvider.getUserJobsMenuChoice called."); const items: FilterItem[] = this.mHistory @@ -1120,6 +1147,13 @@ export class ZosJobsProvider extends ZoweTreeProvider implements IZoweTree"}.${this.label as string}`; + if (contextually.isSession(this)) { + this.sort = { + method: JobSortOpts.Id, + direction: SortDirection.Ascending, + }; + if (!globals.ISTHEIA) { + this.id = this.label as string; + } } } @@ -225,26 +233,40 @@ export class Job extends ZoweTreeNode implements IZoweJobTreeNode { // Only add new children that are not in the list of existing child nodes const newChildren = Object.values(elementChildren).filter((c) => this.children.find((ch) => ch.label === c.label) == null); + const sortMethod = contextually.isSession(this) ? this.sort : { method: JobSortOpts.Id, direction: SortDirection.Ascending }; // Remove any children that are no longer present in the built record this.children = this.children .concat(newChildren) .filter((ch) => Object.values(elementChildren).find((recordCh) => recordCh.label === ch.label) != null) - .sort((a, b) => Job.sortJobs(a, b)); + .sort(Job.sortJobs(sortMethod)); this.dirty = false; return this.children; } - public static sortJobs(a: IZoweJobTreeNode, b: IZoweJobTreeNode): number { - if (a.job.jobid > b.job.jobid) { - return 1; - } + public static sortJobs(sortOpts: NodeSort): (x: IZoweJobTreeNode, y: IZoweJobTreeNode) => number { + return (x, y) => { + const sortLessThan = sortOpts.direction == SortDirection.Ascending ? -1 : 1; + const sortGreaterThan = sortLessThan * -1; + + const keyToSortBy = JOB_SORT_KEYS[sortOpts.method]; + let xCompare, yCompare; + if (keyToSortBy === "retcode") { + // some jobs (such as active ones) will have a null retcode + // in this case, use status as the key to compare for that node only + xCompare = x.job["retcode"] ?? x.job["status"]; + yCompare = y.job["retcode"] ?? y.job["status"]; + } else { + xCompare = x.job[keyToSortBy]; + yCompare = y.job[keyToSortBy]; + } - if (a.job.jobid < b.job.jobid) { - return -1; - } + if (xCompare === yCompare) { + return x.job["jobid"] > y.job["jobid"] ? sortGreaterThan : sortLessThan; + } - return 0; + return xCompare > yCompare ? sortGreaterThan : sortLessThan; + }; } public getSessionNode(): IZoweJobTreeNode { diff --git a/packages/zowe-explorer/src/job/actions.ts b/packages/zowe-explorer/src/job/actions.ts index 49ac61e2ee..e3cef45c35 100644 --- a/packages/zowe-explorer/src/job/actions.ts +++ b/packages/zowe-explorer/src/job/actions.ts @@ -14,11 +14,15 @@ import * as zowe from "@zowe/cli"; import { errorHandling } from "../utils/ProfilesUtils"; import { Profiles } from "../Profiles"; import { ZoweExplorerApiRegister } from "../ZoweExplorerApiRegister"; -import { Gui, IZoweTree, IZoweJobTreeNode } from "@zowe/zowe-explorer-api"; +import { Gui, IZoweTree, IZoweJobTreeNode, JobSortOpts } from "@zowe/zowe-explorer-api"; import { Job, Spool } from "./ZoweJobNode"; import * as nls from "vscode-nls"; import SpoolProvider, { encodeJobFile, getSpoolFiles, matchSpool } from "../SpoolProvider"; import { ZoweLogger } from "../utils/LoggerUtils"; +import { SORT_DIRS } from "../shared/utils"; +import { ZosJobsProvider } from "./ZosJobsProvider"; +import { JOB_SORT_OPTS } from "./utils"; +import * as globals from "../globals"; import { LocalFileManagement } from "../utils/LocalFileManagement"; // Set up localization @@ -387,8 +391,6 @@ async function deleteSingleJob(job: IZoweJobTreeNode, jobsProvider: IZoweTree, jobsProvider: IZoweTree): Promise { @@ -528,3 +530,34 @@ export async function cancelJobs(jobsProvider: IZoweTree, node await Gui.showMessage(localize("cancelJobs.succeeded", "Cancelled selected jobs successfully.")); } } +export async function sortJobs(session: IZoweJobTreeNode, jobsProvider: ZosJobsProvider): Promise { + const selection = await Gui.showQuickPick( + JOB_SORT_OPTS.map((sortOpt, i) => ({ + label: i === session.sort.method ? `${sortOpt} $(check)` : sortOpt, + description: i === JOB_SORT_OPTS.length - 1 ? SORT_DIRS[session.sort.direction] : null, + })), + { + placeHolder: localize("jobs.selectSortOpt", "Select a sorting option for jobs in {0}", session.label as string), + } + ); + if (selection == null) { + return; + } + if (selection.label === localize("setSortDirection", "$(fold) Sort Direction")) { + const dir = await Gui.showQuickPick(SORT_DIRS, { + placeHolder: localize("sort.selectDirection", "Select a sorting direction"), + }); + if (dir != null) { + session.sort = { + ...(session.sort ?? { method: JobSortOpts.Id }), + direction: SORT_DIRS.indexOf(dir), + }; + } + await sortJobs(session, jobsProvider); + return; + } + + session.sort.method = JOB_SORT_OPTS.indexOf(selection.label.replace(" $(check)", "")); + jobsProvider.sortBy(session); + Gui.setStatusBarMessage(localize("sort.updated", "$(check) Sorting updated for {0}", session.label as string), globals.MS_PER_SEC * 4); +} diff --git a/packages/zowe-explorer/src/job/init.ts b/packages/zowe-explorer/src/job/init.ts index f5ed499375..81527474a2 100644 --- a/packages/zowe-explorer/src/job/init.ts +++ b/packages/zowe-explorer/src/job/init.ts @@ -24,7 +24,7 @@ import { ZoweLogger } from "../utils/LoggerUtils"; export async function initJobsProvider(context: vscode.ExtensionContext): Promise> { ZoweLogger.trace("job.init.initJobsProvider called."); - const jobsProvider: IZoweTree = await createJobsTree(); + const jobsProvider = await createJobsTree(globals.LOG); if (jobsProvider == null) { return null; } @@ -103,7 +103,7 @@ export async function initJobsProvider(context: vscode.ExtensionContext): Promis context.subscriptions.push( vscode.commands.registerCommand("zowe.jobs.setJobSpool", async (session, jobId) => jobActions.focusOnJob(jobsProvider, session, jobId)) ); - context.subscriptions.push(vscode.commands.registerCommand("zowe.jobs.search", (node): void => jobsProvider.filterPrompt(node))); + context.subscriptions.push(vscode.commands.registerCommand("zowe.jobs.search", async (node): Promise => jobsProvider.filterPrompt(node))); context.subscriptions.push( vscode.commands.registerCommand("zowe.jobs.editSession", async (node): Promise => jobsProvider.editSession(node, jobsProvider)) ); @@ -124,9 +124,11 @@ export async function initJobsProvider(context: vscode.ExtensionContext): Promis }) ); context.subscriptions.push(vscode.commands.registerCommand("zowe.jobs.saveSearch", (node): void => jobsProvider.saveSearch(node))); - context.subscriptions.push(vscode.commands.registerCommand("zowe.jobs.removeSearchFavorite", (node): void => jobsProvider.removeFavorite(node))); context.subscriptions.push( - vscode.commands.registerCommand("zowe.jobs.removeFavProfile", (node): void => jobsProvider.removeFavProfile(node.label, true)) + vscode.commands.registerCommand("zowe.jobs.removeSearchFavorite", async (node): Promise => jobsProvider.removeFavorite(node)) + ); + context.subscriptions.push( + vscode.commands.registerCommand("zowe.jobs.removeFavProfile", async (node): Promise => jobsProvider.removeFavProfile(node.label, true)) ); context.subscriptions.push( vscode.commands.registerCommand("zowe.jobs.disableValidation", (node) => { @@ -140,8 +142,8 @@ export async function initJobsProvider(context: vscode.ExtensionContext): Promis jobsProvider.refreshElement(node); }) ); - context.subscriptions.push(vscode.commands.registerCommand("zowe.jobs.ssoLogin", (node: IZoweTreeNode): void => jobsProvider.ssoLogin(node))); - context.subscriptions.push(vscode.commands.registerCommand("zowe.jobs.ssoLogout", (node: IZoweTreeNode): void => jobsProvider.ssoLogout(node))); + context.subscriptions.push(vscode.commands.registerCommand("zowe.jobs.ssoLogin", async (node): Promise => jobsProvider.ssoLogin(node))); + context.subscriptions.push(vscode.commands.registerCommand("zowe.jobs.ssoLogout", async (node): Promise => jobsProvider.ssoLogout(node))); const spoolFileTogglePoll = (startPolling: boolean) => async (node: IZoweTreeNode, nodeList: IZoweTreeNode[]): Promise => { @@ -160,8 +162,8 @@ export async function initJobsProvider(context: vscode.ExtensionContext): Promis context.subscriptions.push(vscode.commands.registerCommand("zowe.jobs.startPolling", spoolFileTogglePoll(true))); context.subscriptions.push(vscode.commands.registerCommand("zowe.jobs.stopPolling", spoolFileTogglePoll(false))); context.subscriptions.push( - vscode.workspace.onDidChangeConfiguration((e) => { - jobsProvider.onDidChangeConfiguration(e); + vscode.workspace.onDidChangeConfiguration(async (e) => { + await jobsProvider.onDidChangeConfiguration(e); }) ); context.subscriptions.push( @@ -169,7 +171,7 @@ export async function initJobsProvider(context: vscode.ExtensionContext): Promis await jobActions.cancelJobs(jobsProvider, getSelectedNodeList(node, nodeList)); }) ); - + context.subscriptions.push(vscode.commands.registerCommand("zowe.jobs.sortBy", async (job) => jobActions.sortJobs(job, jobsProvider))); initSubscribers(context, jobsProvider); return jobsProvider; } diff --git a/packages/zowe-explorer/src/job/utils.ts b/packages/zowe-explorer/src/job/utils.ts index 9a5f3d7611..9747139144 100644 --- a/packages/zowe-explorer/src/job/utils.ts +++ b/packages/zowe-explorer/src/job/utils.ts @@ -9,8 +9,33 @@ * */ +import { JobSortOpts } from "@zowe/zowe-explorer-api"; import { ZoweLogger } from "../utils/LoggerUtils"; import { FilterItem } from "../utils/ProfilesUtils"; +import * as nls from "vscode-nls"; +import { IJob } from "@zowe/cli"; + +// Set up localization +nls.config({ + messageFormat: nls.MessageFormat.bundle, + bundleFormat: nls.BundleFormat.standalone, +})(); +const localize: nls.LocalizeFunc = nls.loadMessageBundle(); + +export const JOB_SORT_OPTS = [ + localize("jobs.sortById", "$(list-ordered) Job ID (default)"), + localize("jobs.sortByDateSubmitted", "$(calendar) Date Submitted"), + localize("jobs.sortByName", "$(case-sensitive) Job Name"), + localize("jobs.sortByReturnCode", "$(symbol-numeric) Return Code"), + localize("setSortDirection", "$(fold) Sort Direction"), +]; + +export const JOB_SORT_KEYS: Record = { + [JobSortOpts.Id]: "jobid", + [JobSortOpts.DateSubmitted]: "exec-submitted", + [JobSortOpts.Name]: "jobname", + [JobSortOpts.ReturnCode]: "retcode", +}; export async function resolveQuickPickHelper(quickpick): Promise { ZoweLogger.trace("job.utils.resolveQuickPickHelper called."); diff --git a/packages/zowe-explorer/src/shared/HistoryView.ts b/packages/zowe-explorer/src/shared/HistoryView.ts new file mode 100644 index 0000000000..d4f0b08620 --- /dev/null +++ b/packages/zowe-explorer/src/shared/HistoryView.ts @@ -0,0 +1,190 @@ +/** + * 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 * as vscode from "vscode"; +import * as nls from "vscode-nls"; +import { WebView, Gui, DataSetAllocTemplate } from "@zowe/zowe-explorer-api"; +import { ExtensionContext } from "vscode"; +import { IZoweProviders } from "./init"; +import { USSTree } from "../uss/USSTree"; +import { DatasetTree } from "../dataset/DatasetTree"; +import { ZosJobsProvider } from "../job/ZosJobsProvider"; +import { ZoweLogger } from "../utils/LoggerUtils"; + +const localize: nls.LocalizeFunc = nls.loadMessageBundle(); + +type TreeProvider = USSTree | DatasetTree | ZosJobsProvider; + +type History = { + search: string[]; + sessions: string[]; + fileHistory: string[]; + dsTemplates?: DataSetAllocTemplate[]; + favorites: string[]; +}; + +const tabs = { + ds: "ds-panel-tab", + uss: "uss-panel-tab", + jobs: "jobs-panel-tab", +}; + +export class HistoryView extends WebView { + private treeProviders: IZoweProviders; + private currentTab: string; + private currentSelection: { [type: string]: string }; + + public constructor(context: ExtensionContext, treeProviders: IZoweProviders) { + const label = "Edit History"; + super(label, "edit-history", context, (message: object) => this.onDidReceiveMessage(message), true); + this.treeProviders = treeProviders; + this.currentSelection = { ds: "search", uss: "search", jobs: "search" }; + } + + protected async onDidReceiveMessage(message: any): Promise { + switch (message.command) { + case "refresh": + await this.refreshView(message); + break; + case "ready": + await this.panel.webview.postMessage({ + ds: this.getHistoryData("ds"), + uss: this.getHistoryData("uss"), + jobs: this.getHistoryData("job"), + tab: this.currentTab, + selection: this.currentSelection, + }); + break; + case "show-error": + this.showError(message); + break; + case "update-selection": + this.updateSelection(message); + break; + case "add-item": + await this.addItem(message); + break; + case "remove-item": + await this.removeItem(message); + break; + case "clear-all": + await this.clearAll(message); + break; + default: + break; + } + } + + private getTreeProvider(type: string): TreeProvider { + return this.treeProviders[type === "jobs" ? "job" : type] as TreeProvider; + } + + private getHistoryData(type: string): History { + const treeProvider = this.treeProviders[type] as TreeProvider; + return { + search: treeProvider.getSearchHistory(), + sessions: treeProvider.getSessions(), + fileHistory: treeProvider.getFileHistory(), + dsTemplates: type === "ds" ? (treeProvider as DatasetTree).getDsTemplates() : undefined, + favorites: treeProvider.getFavorites(), + }; + } + + private showError(message): void { + ZoweLogger.trace("HistoryView.showError called."); + Gui.errorMessage(message.attrs.errorMsg); + } + + private updateSelection(message): void { + ZoweLogger.trace("HistoryView.updateSelection called."); + this.currentSelection[message.attrs.type] = message.attrs.selection; + } + + private async addItem(message): Promise { + ZoweLogger.trace("HistoryView.addItem called."); + + const example = message.attrs.type === "ds" ? "e.g: USER.PDS.*" : "e.g: /u/user/mydir"; + + const options: vscode.InputBoxOptions = { + prompt: localize("HistoryView.addItem.prompt", "Type the new pattern to add to history"), + value: "", + placeHolder: example, + }; + const item = await Gui.showInputBox(options); + const treeProvider = this.getTreeProvider(message.attrs.type); + treeProvider.addSearchHistory(item); + await this.refreshView(message); + } + + private async removeItem(message): Promise { + ZoweLogger.trace("HistoryView.removeItem called."); + const treeProvider = this.getTreeProvider(message.attrs.type); + switch (message.attrs.selection) { + case "search": + Object.keys(message.attrs.selectedItems).forEach((selectedItem) => { + if (message.attrs.selectedItems[selectedItem]) { + treeProvider.removeSearchHistory(selectedItem); + } + }); + break; + case "fileHistory": + if (!(treeProvider instanceof ZosJobsProvider)) { + Object.keys(message.attrs.selectedItems).forEach((selectedItem) => { + if (message.attrs.selectedItems[selectedItem]) { + treeProvider.removeFileHistory(selectedItem); + } + }); + } + break; + default: + Gui.showMessage(localize("HistoryView.removeItem.notSupported", "action is not supported for this property type.")); + break; + } + await this.refreshView(message); + } + + private async clearAll(message): Promise { + ZoweLogger.trace("HistoryView.clearAll called."); + const treeProvider = this.getTreeProvider(message.attrs.type); + const infoMessage = localize("HistoryView.clearAll.confirmMessage", "Clear all history items for this persistent property?"); + const yesButton = localize("HistoryView.clearAll.Yes", "Yes"); + const noButton = localize("HistoryView.clearAll.No", "No"); + const choice = await Gui.showMessage(infoMessage, { items: [yesButton, noButton], vsCodeOpts: { modal: true } }); + if (choice === yesButton) { + switch (message.attrs.selection) { + case "search": + treeProvider.resetSearchHistory(); + break; + case "fileHistory": + if (!(treeProvider instanceof ZosJobsProvider)) { + treeProvider.resetFileHistory(); + } + break; + default: + Gui.showMessage(localize("HistoryView.removeItem.notSupported", "action is not supported for this property type.")); + break; + } + await this.refreshView(message); + } + } + + private async refreshView(message): Promise { + ZoweLogger.trace("HistoryView.refreshView called."); + this.currentTab = tabs[message.attrs.type]; + await this.panel.webview.postMessage({ + ds: this.getHistoryData("ds"), + uss: this.getHistoryData("uss"), + jobs: this.getHistoryData("job"), + tab: this.currentTab, + selection: this.currentSelection, + }); + } +} diff --git a/packages/zowe-explorer/src/shared/TreeProviders.ts b/packages/zowe-explorer/src/shared/TreeProviders.ts new file mode 100644 index 0000000000..703f60c990 --- /dev/null +++ b/packages/zowe-explorer/src/shared/TreeProviders.ts @@ -0,0 +1,51 @@ +/** + * 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 * as vscode from "vscode"; +import { IZoweTree, IZoweTreeNode } from "@zowe/zowe-explorer-api"; +import { IZoweProviders } from "./init"; + +type ProviderFunction = (context: vscode.ExtensionContext) => Promise>; +export class TreeProviders { + static #ds: IZoweTree; + static #uss: IZoweTree; + static #job: IZoweTree; + + public static async initializeProviders( + context: vscode.ExtensionContext, + initializers: { ds: ProviderFunction; uss: ProviderFunction; job: ProviderFunction } + ): Promise { + TreeProviders.#ds = await initializers.ds(context); + TreeProviders.#uss = await initializers.uss(context); + TreeProviders.#job = await initializers.job(context); + return TreeProviders.providers; + } + + public static get ds(): IZoweTree { + return TreeProviders.#ds; + } + + public static get uss(): IZoweTree { + return TreeProviders.#uss; + } + + public static get job(): IZoweTree { + return TreeProviders.#job; + } + + public static get providers(): IZoweProviders { + return { + ds: TreeProviders.#ds, + uss: TreeProviders.#uss, + job: TreeProviders.#job, + }; + } +} diff --git a/packages/zowe-explorer/src/shared/init.ts b/packages/zowe-explorer/src/shared/init.ts index d82550b938..dfb1995e26 100644 --- a/packages/zowe-explorer/src/shared/init.ts +++ b/packages/zowe-explorer/src/shared/init.ts @@ -28,6 +28,8 @@ import { ZoweLogger } from "../utils/LoggerUtils"; import { ZoweSaveQueue } from "../abstract/ZoweSaveQueue"; import { SettingsConfig } from "../utils/SettingsConfig"; import { spoolFilePollEvent } from "../job/actions"; +import { HistoryView } from "./HistoryView"; +import { ProfileManagement } from "../utils/ProfileManagement"; import { LocalFileManagement } from "../utils/LocalFileManagement"; // Set up localization @@ -80,11 +82,10 @@ export function registerCommonCommands(context: vscode.ExtensionContext, provide }) ); - // Update imperative.json to false only when VS Code setting is set to false + // Webview for editing persistent items on Zowe Explorer context.subscriptions.push( - vscode.commands.registerCommand("zowe.updateSecureCredentials", async (customCredentialManager?: string) => { - await globals.setGlobalSecurityValue(customCredentialManager); - ProfilesUtils.writeOverridesFile(); + vscode.commands.registerCommand("zowe.editHistory", () => { + return new HistoryView(context, providers); }) ); @@ -94,6 +95,12 @@ export function registerCommonCommands(context: vscode.ExtensionContext, provide }) ); + context.subscriptions.push( + vscode.commands.registerCommand("zowe.profileManagement", async (node: IZoweTreeNode) => { + await ProfileManagement.manageProfile(node); + }) + ); + // Register functions & event listeners context.subscriptions.push( vscode.workspace.onDidChangeConfiguration(async (e) => { @@ -236,6 +243,16 @@ export function registerCommonCommands(context: vscode.ExtensionContext, provide } } +export function registerCredentialManager(context: vscode.ExtensionContext): void { + // Update imperative.json to false only when VS Code setting is set to false + context.subscriptions.push( + vscode.commands.registerCommand("zowe.updateSecureCredentials", async (customCredentialManager?: string) => { + await globals.setGlobalSecurityValue(customCredentialManager); + ProfilesUtils.writeOverridesFile(); + }) + ); +} + export function watchConfigProfile(context: vscode.ExtensionContext, providers: IZoweProviders): void { ZoweLogger.trace("shared.init.watchConfigProfile called."); const watchers: vscode.FileSystemWatcher[] = []; diff --git a/packages/zowe-explorer/src/shared/utils.ts b/packages/zowe-explorer/src/shared/utils.ts index 8595cd8635..7d2d745f02 100644 --- a/packages/zowe-explorer/src/shared/utils.ts +++ b/packages/zowe-explorer/src/shared/utils.ts @@ -14,7 +14,6 @@ import * as vscode from "vscode"; import * as path from "path"; import * as globals from "../globals"; -import * as os from "os"; import { Gui, IZoweTreeNode, IZoweNodeType, IZoweDatasetTreeNode, IZoweUSSTreeNode, IZoweJobTreeNode } from "@zowe/zowe-explorer-api"; import { ZoweExplorerApiRegister } from "../ZoweExplorerApiRegister"; import * as nls from "vscode-nls"; @@ -42,6 +41,8 @@ export const JOB_SUBMIT_DIALOG_OPTS = [ localize("zowe.jobs.confirmSubmission.allJobs", "All jobs"), ]; +export const SORT_DIRS: string[] = [localize("sort.asc", "Ascending"), localize("sort.desc", "Descending")]; + export type LocalFileInfo = { name: string; path: string; @@ -141,7 +142,7 @@ function appendSuffix(label: string): string { const bracket = label.indexOf("("); const split = bracket > -1 ? label.substr(0, bracket).split(".", limit) : label.split(".", limit); for (let i = split.length - 1; i > 0; i--) { - if (["JCL", "CNTL"].includes(split[i])) { + if (["JCL", "JCLLIB", "CNTL"].includes(split[i])) { return label.concat(".jcl"); } if (["COBOL", "CBL", "COB", "SCBL"].includes(split[i])) { diff --git a/packages/zowe-explorer/src/uss/AttributeView.ts b/packages/zowe-explorer/src/uss/AttributeView.ts index 4ca07d35d6..8bf1bd7b0d 100644 --- a/packages/zowe-explorer/src/uss/AttributeView.ts +++ b/packages/zowe-explorer/src/uss/AttributeView.ts @@ -12,6 +12,7 @@ import { FileAttributes, Gui, IUss, IZoweTree, IZoweUSSTreeNode, WebView } from "@zowe/zowe-explorer-api"; import { Disposable, ExtensionContext } from "vscode"; import { ZoweExplorerApiRegister } from "../ZoweExplorerApiRegister"; +import * as contextually from "../shared/context"; export class AttributeView extends WebView { private treeProvider: IZoweTree; @@ -30,11 +31,18 @@ export class AttributeView extends WebView { this.ussApi = ZoweExplorerApiRegister.getUssApi(this.ussNode.getProfile()); } + private async attachTag(node: IZoweUSSTreeNode): Promise { + if (this.ussApi.getTag && !contextually.isUssDirectory(node)) { + node.attributes.tag = await this.ussApi.getTag(node.fullPath); + } + } + protected async onDidReceiveMessage(message: any): Promise { switch (message.command) { case "refresh": if (this.canUpdate) { this.onUpdateDisposable = this.ussNode.onUpdate(async (node) => { + await this.attachTag(node); await this.panel.webview.postMessage({ attributes: node.attributes, name: node.fullPath, @@ -51,6 +59,7 @@ export class AttributeView extends WebView { } break; case "ready": + await this.attachTag(this.ussNode); await this.panel.webview.postMessage({ attributes: this.ussNode.attributes, name: this.ussNode.fullPath, @@ -104,6 +113,10 @@ export class AttributeView extends WebView { newAttrs.perms = attrs.perms; } + if (this.ussNode.attributes.tag !== attrs.tag && this.ussApi.getTag) { + newAttrs.tag = attrs.tag; + } + await this.ussApi.updateAttributes(this.ussNode.fullPath, newAttrs); this.ussNode.attributes = { ...(this.ussNode.attributes ?? {}), ...newAttrs } as FileAttributes; diff --git a/packages/zowe-explorer/src/uss/USSTree.ts b/packages/zowe-explorer/src/uss/USSTree.ts index d0afd2d2e5..b93d3cc8c2 100644 --- a/packages/zowe-explorer/src/uss/USSTree.ts +++ b/packages/zowe-explorer/src/uss/USSTree.ts @@ -41,10 +41,10 @@ const localize: nls.LocalizeFunc = nls.loadMessageBundle(); * * @export */ -export async function createUSSTree(): Promise { +export async function createUSSTree(log: imperative.Logger): Promise { ZoweLogger.trace("uss.USSTree.createUSSTree called."); const tree = new USSTree(); - await tree.initializeFavorites(); + await tree.initializeFavorites(log); await tree.addSession(); return tree; } @@ -80,6 +80,7 @@ export class USSTree extends ZoweTreeProvider implements IZoweTree { ZoweLogger.trace("USSTree.getAllLoadedItems called."); - ZoweLogger.debug(localize("enterPattern.log.debug.prompt", "Prompting the user to choose a member from the filtered list")); + if (this.log) { + ZoweLogger.debug(localize("enterPattern.log.debug.prompt", "Prompting the user to choose a member from the filtered list")); + } const loadedNodes: IZoweUSSTreeNode[] = []; const sessions = await this.getChildren(); @@ -557,7 +560,9 @@ export class USSTree extends ZoweTreeProvider implements IZoweTree { ZoweLogger.trace("USSTree.filterPrompt called."); - ZoweLogger.debug(localize("filterPrompt.log.debug.promptUSSPath", "Prompting the user for a USS path")); + if (this.log) { + ZoweLogger.debug(localize("filterPrompt.log.debug.promptUSSPath", "Prompting the user for a USS path")); + } await this.checkCurrentProfile(node); if (Profiles.getInstance().validProfile !== ValidProfileEnum.INVALID) { let sessionNode; @@ -684,8 +689,9 @@ export class USSTree extends ZoweTreeProvider implements IZoweTree { + public async initializeFavorites(log: imperative.Logger): Promise { ZoweLogger.trace("USSTree.initializeFavorites called."); + this.log = log; ZoweLogger.debug(localize("initializeFavorites.log.debug", "Initializing profiles with USS favorites.")); const lines: string[] = this.mHistory.readFavorites(); if (lines.length === 0) { @@ -750,12 +756,13 @@ export class USSTree extends ZoweTreeProvider implements IZoweTree { + public async loadProfilesForFavorites(log: imperative.Logger, parentNode: IZoweUSSTreeNode): Promise { ZoweLogger.trace("USSTree.loadProfilesForFavorites called."); const profileName = parentNode.label as string; const updatedFavsForProfile: IZoweUSSTreeNode[] = []; let profile: imperative.IProfileLoaded; let session: imperative.Session; + this.log = log; ZoweLogger.debug(localize("loadProfilesForFavorites.log.debug", "Loading profile: {0} for USS favorites", profileName)); // Load profile for parent profile node in this.mFavorites array if (!parentNode.getProfile() || !parentNode.getSession()) { @@ -844,6 +851,31 @@ export class USSTree extends ZoweTreeProvider implements IZoweTree 0 && this.prevPath !== this.fullPath) { + this.children = []; + } + // Build a list of nodes based on the API response const responseNodes: IZoweUSSTreeNode[] = []; - const newNodeCreated: boolean = response.apiResponse.items.reduce((lastResult: boolean, item) => { + for (const item of response.apiResponse.items) { if (item.name === "." || item.name === "..") { - return lastResult || false; + continue; } const existing = this.children.find( @@ -229,7 +234,7 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { }; responseNodes.push(existing); existing.onUpdateEmitter.fire(existing); - return lastResult || false; + continue; } if (item.mode.startsWith("d")) { @@ -269,20 +274,6 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { }; responseNodes.push(temp); } - - return lastResult || true; - }, false); - - this.dirty = false; - - // If no new nodes were created, return the cached list of children - if (!newNodeCreated) { - return this.children; - } - - // If search path has changed, invalidate all children - if (this.fullPath?.length > 0 && this.prevPath !== this.fullPath) { - this.children = []; } const nodesToAdd = responseNodes.filter((c) => !this.children.includes(c)); @@ -293,6 +284,7 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { .filter((c) => !nodesToRemove.includes(c)) .sort((a, b) => ((a.label as string) < (b.label as string) ? -1 : 1)); this.prevPath = this.fullPath; + this.dirty = false; return this.children; } @@ -716,10 +708,9 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { } const prof = this.getProfile(); - const remotePath = this.fullPath; try { + const fileTreeToPaste: UssFileTree = JSON.parse(clipboardContents); const api = ZoweExplorerApiRegister.getUssApi(this.profile); - const fileTreeToPaste: UssFileTree = JSON.parse(await vscode.env.clipboard.readText()); const sessionName = this.getSessionNode().getLabel() as string; const task: imperative.ITaskWithStatus = { @@ -734,7 +725,7 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { }; for (const subnode of fileTreeToPaste.children) { - await this.paste(sessionName, remotePath, { api, tree: subnode, options }); + await this.paste(sessionName, this.fullPath, { api, tree: subnode, options }); } } catch (error) { await errorHandling(error, this.label.toString(), localize("copyUssFile.error", "Error uploading files")); diff --git a/packages/zowe-explorer/src/uss/actions.ts b/packages/zowe-explorer/src/uss/actions.ts index dbd534501e..fe1db36d4c 100644 --- a/packages/zowe-explorer/src/uss/actions.ts +++ b/packages/zowe-explorer/src/uss/actions.ts @@ -483,23 +483,20 @@ export async function pasteUssFile(ussFileProvider: IZoweTree, */ export async function pasteUss(ussFileProvider: IZoweTree, node: IZoweUSSTreeNode): Promise { ZoweLogger.trace("uss.actions.pasteUss called."); - const a = ussFileProvider.getTreeView().selection as IZoweUSSTreeNode[]; - let selectedNode = node; - if (!selectedNode) { - selectedNode = a.length > 0 ? a[0] : (a as unknown as IZoweUSSTreeNode); + if (node.pasteUssTree == null && node.copyUssFile == null) { + await Gui.infoMessage(localize("uss.paste.apiNotAvailable", "The paste operation is not supported for this node.")); + return; } - await Gui.withProgress( { location: vscode.ProgressLocation.Window, title: localize("ZoweUssNode.copyUpload.progress", "Pasting files..."), }, async () => { - await (selectedNode.pasteUssTree ? selectedNode.pasteUssTree() : selectedNode.copyUssFile()); + await (node.pasteUssTree ? node.pasteUssTree() : node.copyUssFile()); } ); - const nodeToRefresh = node?.contextValue != null && contextually.isUssSession(node) ? selectedNode : selectedNode.getParent(); - ussFileProvider.refreshElement(nodeToRefresh); + ussFileProvider.refreshElement(node); } export async function downloadUnixFile(node: IZoweUSSTreeNode, download: boolean): Promise { diff --git a/packages/zowe-explorer/src/uss/init.ts b/packages/zowe-explorer/src/uss/init.ts index 67a2754ca3..6c3a63f1d9 100644 --- a/packages/zowe-explorer/src/uss/init.ts +++ b/packages/zowe-explorer/src/uss/init.ts @@ -12,6 +12,7 @@ import * as vscode from "vscode"; import * as ussActions from "./actions"; import * as refreshActions from "../shared/refresh"; +import * as globals from "../globals"; import { IZoweUSSTreeNode, IZoweTreeNode, IZoweTree } from "@zowe/zowe-explorer-api"; import { Profiles } from "../Profiles"; import * as contextuals from "../shared/context"; @@ -23,7 +24,7 @@ import { TreeViewUtils } from "../utils/TreeViewUtils"; export async function initUSSProvider(context: vscode.ExtensionContext): Promise> { ZoweLogger.trace("init.initUSSProvider called."); - const ussFileProvider: IZoweTree = await createUSSTree(); + const ussFileProvider: IZoweTree = await createUSSTree(globals.LOG); if (ussFileProvider == null) { return null; } diff --git a/packages/zowe-explorer/src/utils/ProfileManagement.ts b/packages/zowe-explorer/src/utils/ProfileManagement.ts new file mode 100644 index 0000000000..5825a8fdac --- /dev/null +++ b/packages/zowe-explorer/src/utils/ProfileManagement.ts @@ -0,0 +1,277 @@ +/** + * 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 * as vscode from "vscode"; +import * as globals from "../globals"; +import { Gui, IZoweTreeNode, imperative } from "@zowe/zowe-explorer-api"; +import { ZoweLogger } from "./LoggerUtils"; +import { ProfilesUtils } from "./ProfilesUtils"; +import * as nls from "vscode-nls"; +import { Profiles } from "../Profiles"; +import { ZoweExplorerApiRegister } from "../ZoweExplorerApiRegister"; +import { isZoweDatasetTreeNode, isZoweUSSTreeNode } from "../shared/utils"; + +// Set up localization +nls.config({ + messageFormat: nls.MessageFormat.bundle, + bundleFormat: nls.BundleFormat.standalone, +})(); +const localize: nls.LocalizeFunc = nls.loadMessageBundle(); + +export class ProfileManagement { + public static async manageProfile(node: IZoweTreeNode): Promise { + const profile = node.getProfile(); + let selected: vscode.QuickPickItem; + switch (true) { + case ProfilesUtils.isProfileUsingBasicAuth(profile): { + ZoweLogger.debug(`Profile ${profile.name} is using basic authentication.`); + selected = await this.setupProfileManagementQp(imperative.SessConstants.AUTH_TYPE_BASIC, node); + break; + } + case await ProfilesUtils.isUsingTokenAuth(profile.name): { + ZoweLogger.debug(`Profile ${profile.name} is using token authentication.`); + selected = await this.setupProfileManagementQp("token", node); + break; + } + // will need a case for isUsingCertAuth + default: { + ZoweLogger.debug(`Profile ${profile.name} authentication method is unkown.`); + selected = await this.setupProfileManagementQp(null, node); + break; + } + } + await this.handleAuthSelection(selected, node, profile); + } + public static AuthQpLabels = { + add: "add-credentials", + delete: "delete-profile", + disable: "disable-validation", + edit: "edit-profile", + enable: "enable-validation", + hide: "hide-profile", + login: "obtain-token", + logout: "invalidate-token", + update: "update-credentials", + }; + public static basicAuthAddQpItems: Record = { + [this.AuthQpLabels.add]: { + label: localize("addBasicAuthQpItem.addCredentials.qpLabel", "$(plus) Add Credentials"), + description: localize("addBasicAuthQpItem.addCredentials.qpDetail", "Add username and password for basic authentication"), + }, + }; + public static basicAuthUpdateQpItems: Record = { + [this.AuthQpLabels.update]: { + label: localize("updateBasicAuthQpItem.updateCredentials.qpLabel", "$(refresh) Update Credentials"), + description: localize("updateBasicAuthQpItem.updateCredentials.qpDetail", "Update stored username and password"), + }, + }; + public static deleteProfileQpItem: Record = { + [this.AuthQpLabels.delete]: { + label: localize("deleteProfileQpItem.delete.qpLabel", "$(trash) Delete Profile"), + }, + }; + public static disableProfileValildationQpItem: Record = { + [this.AuthQpLabels.disable]: { + label: localize("disableProfileValildationQpItem.disableValidation.qpLabel", "$(workspace-untrusted) Disable Profile Validation"), + description: localize("disableProfileValildationQpItem.disableValidation.qpDetail", "Disable validation of server check for profile"), + }, + }; + public static enableProfileValildationQpItem: Record = { + [this.AuthQpLabels.enable]: { + label: localize("enableProfileValildationQpItem.enableValidation.qpLabel", "$(workspace-trusted) Enable Profile Validation"), + description: localize("enableProfileValildationQpItem.enableValidation.qpDetail", "Enable validation of server check for profile"), + }, + }; + public static editProfileQpItems: Record = { + [this.AuthQpLabels.edit]: { + label: localize("editProfileQpItem.editProfile.qpLabel", "$(pencil) Edit Profile"), + description: localize("editProfileQpItem.editProfile.qpDetail", "Update profile connection information"), + }, + }; + public static hideProfileQpItems: Record = { + [this.AuthQpLabels.hide]: { + label: localize("hideProfileQpItems.hideProfile.qpLabel", "$(eye-closed) Hide Profile"), + description: localize("hideProfileQpItems.hideProfile.qpDetail", "Hide profile name from tree view"), + }, + }; + public static tokenAuthLoginQpItem: Record = { + [this.AuthQpLabels.login]: { + label: localize("loginQpItem.login.qpLabel", "$(arrow-right) Log in to authentication service"), + description: localize("loginQpItem.login.qpDetail", "Log in to obtain a new token value"), + }, + }; + public static tokenAuthLogoutQpItem: Record = { + [this.AuthQpLabels.logout]: { + label: localize("logoutQpItem.logout.qpLabel", "$(arrow-left) Log out of authentication service"), + description: localize("logoutQpItem.logout.qpDetail", "Log out to invalidate and remove stored token value"), + }, + }; + private static async setupProfileManagementQp(managementType: string, node: IZoweTreeNode): Promise { + const profile = node.getProfile(); + const qp = Gui.createQuickPick(); + let quickPickOptions: vscode.QuickPickItem[]; + const placeholders = this.getQpPlaceholders(profile); + switch (managementType) { + case imperative.SessConstants.AUTH_TYPE_BASIC: { + quickPickOptions = this.basicAuthQp(node); + qp.placeholder = placeholders.basicAuth; + break; + } + case "token": { + quickPickOptions = this.tokenAuthQp(node); + qp.placeholder = placeholders.tokenAuth; + break; + } + default: { + quickPickOptions = this.chooseAuthQp(node); + qp.placeholder = placeholders.chooseAuth; + break; + } + } + let selectedItem = quickPickOptions[0]; + qp.items = quickPickOptions; + qp.activeItems = [selectedItem]; + qp.show(); + selectedItem = await Gui.resolveQuickPick(qp); + qp.hide(); + return selectedItem; + } + private static async handleAuthSelection(selected: vscode.QuickPickItem, node: IZoweTreeNode, profile: imperative.IProfileLoaded): Promise { + switch (selected) { + case this.basicAuthAddQpItems[this.AuthQpLabels.add]: { + await ProfilesUtils.promptCredentials(node); + break; + } + case this.editProfileQpItems[this.AuthQpLabels.edit]: { + await Profiles.getInstance().editSession(profile, profile.name); + break; + } + case this.tokenAuthLoginQpItem[this.AuthQpLabels.login]: { + await Profiles.getInstance().ssoLogin(node, profile.name); + break; + } + case this.tokenAuthLogoutQpItem[this.AuthQpLabels.logout]: { + await Profiles.getInstance().ssoLogout(node); + break; + } + case this.basicAuthUpdateQpItems[this.AuthQpLabels.update]: { + await ProfilesUtils.promptCredentials(node); + break; + } + case this.hideProfileQpItems[this.AuthQpLabels.hide]: { + await this.handleHideProfiles(node); + break; + } + case this.deleteProfileQpItem[this.AuthQpLabels.delete]: { + await this.handleDeleteProfiles(node); + break; + } + case this.enableProfileValildationQpItem[this.AuthQpLabels.enable]: { + await this.handleEnableProfileValidation(node); + break; + } + case this.disableProfileValildationQpItem[this.AuthQpLabels.disable]: { + await this.handleDisableProfileValidation(node); + break; + } + default: { + Gui.infoMessage(localize("profiles.operation.cancelled", "Operation Cancelled")); + break; + } + } + } + + private static getQpPlaceholders(profile: imperative.IProfileLoaded): { basicAuth: string; tokenAuth: string; chooseAuth: string } { + return { + basicAuth: localize("qpPlaceholders.qp.basic", "Profile {0} is using basic authentication. Choose a profile action.", profile.name), + tokenAuth: localize("qpPlaceholders.qp.token", "Profile {0} is using token authentication. Choose a profile action.", profile.name), + chooseAuth: localize( + "qpPlaceholders.qp.choose", + "Profile {0} doesn't specify an authentication method. Choose a profile action.", + profile.name + ), + }; + } + + private static basicAuthQp(node: IZoweTreeNode): vscode.QuickPickItem[] { + const quickPickOptions: vscode.QuickPickItem[] = Object.values(this.basicAuthUpdateQpItems); + return this.addFinalQpOptions(node, quickPickOptions); + } + private static tokenAuthQp(node: IZoweTreeNode): vscode.QuickPickItem[] { + const profile = node.getProfile(); + const quickPickOptions: vscode.QuickPickItem[] = Object.values(this.tokenAuthLoginQpItem); + if (profile.profile.tokenType) { + quickPickOptions.push(this.tokenAuthLogoutQpItem[this.AuthQpLabels.logout]); + } + return this.addFinalQpOptions(node, quickPickOptions); + } + private static chooseAuthQp(node: IZoweTreeNode): vscode.QuickPickItem[] { + const profile = node.getProfile(); + const quickPickOptions: vscode.QuickPickItem[] = Object.values(this.basicAuthAddQpItems); + try { + ZoweExplorerApiRegister.getInstance().getCommonApi(profile).getTokenTypeName(); + quickPickOptions.push(this.tokenAuthLoginQpItem[this.AuthQpLabels.login]); + } catch { + ZoweLogger.debug(`Profile ${profile.name} doesn't support token authentication, will not provide option.`); + } + return this.addFinalQpOptions(node, quickPickOptions); + } + private static addFinalQpOptions(node: IZoweTreeNode, quickPickOptions: vscode.QuickPickItem[]): vscode.QuickPickItem[] { + quickPickOptions.push(this.editProfileQpItems[this.AuthQpLabels.edit]); + quickPickOptions.push(this.hideProfileQpItems[this.AuthQpLabels.hide]); + if (node.contextValue.includes(globals.NO_VALIDATE_SUFFIX)) { + quickPickOptions.push(this.enableProfileValildationQpItem[this.AuthQpLabels.enable]); + } else { + quickPickOptions.push(this.disableProfileValildationQpItem[this.AuthQpLabels.disable]); + } + quickPickOptions.push(this.deleteProfileQpItem[this.AuthQpLabels.delete]); + return quickPickOptions; + } + private static async handleDeleteProfiles(node: IZoweTreeNode): Promise { + const profInfo = await Profiles.getInstance().getProfileInfo(); + if (profInfo.usingTeamConfig) { + const profile = node.getProfile(); + await Profiles.getInstance().editSession(profile, profile.name); + return; + } + await vscode.commands.executeCommand("zowe.ds.deleteProfile", node); + } + + private static async handleHideProfiles(node: IZoweTreeNode): Promise { + if (isZoweDatasetTreeNode(node)) { + return vscode.commands.executeCommand("zowe.ds.removeSession", node); + } + if (isZoweUSSTreeNode(node)) { + return vscode.commands.executeCommand("zowe.uss.removeSession", node); + } + return vscode.commands.executeCommand("zowe.jobs.removeJobsSession", node); + } + + private static async handleEnableProfileValidation(node: IZoweTreeNode): Promise { + if (isZoweDatasetTreeNode(node)) { + return vscode.commands.executeCommand("zowe.ds.enableValidation", node); + } + if (isZoweUSSTreeNode(node)) { + return vscode.commands.executeCommand("zowe.uss.enableValidation", node); + } + return vscode.commands.executeCommand("zowe.jobs.enableValidation", node); + } + + private static async handleDisableProfileValidation(node: IZoweTreeNode): Promise { + if (isZoweDatasetTreeNode(node)) { + return vscode.commands.executeCommand("zowe.ds.disableValidation", node); + } + if (isZoweUSSTreeNode(node)) { + return vscode.commands.executeCommand("zowe.uss.disableValidation", node); + } + return vscode.commands.executeCommand("zowe.jobs.disableValidation", node); + } +} diff --git a/packages/zowe-explorer/src/utils/ProfilesUtils.ts b/packages/zowe-explorer/src/utils/ProfilesUtils.ts index 10d7f9beeb..0c1a7ee1ea 100644 --- a/packages/zowe-explorer/src/utils/ProfilesUtils.ts +++ b/packages/zowe-explorer/src/utils/ProfilesUtils.ts @@ -72,7 +72,7 @@ export async function errorHandling(errorDetails: Error | string, label?: string if (imperativeError.mDetails.additionalDetails) { const tokenError: string = imperativeError.mDetails.additionalDetails; - const isTokenAuth = await isUsingTokenAuth(label); + const isTokenAuth = await ProfilesUtils.isUsingTokenAuth(label); if (tokenError.includes("Token is not valid or expired.") || isTokenAuth) { if (isTheia()) { @@ -129,19 +129,6 @@ export function isTheia(): boolean { return false; } -/** - * Function that checks whether a profile is using token based authentication - * @param profileName the name of the profile to check - * @returns {Promise} a boolean representing whether token based auth is being used or not - */ -export async function isUsingTokenAuth(profileName: string): Promise { - const baseProfile = Profiles.getInstance().getDefaultProfile("base"); - const secureProfileProps = await Profiles.getInstance().getSecurePropsForProfile(profileName); - const secureBaseProfileProps = await Profiles.getInstance().getSecurePropsForProfile(baseProfile?.name); - const profileUsesBasicAuth = secureProfileProps.includes("user") && secureProfileProps.includes("password"); - return (secureProfileProps.includes("tokenValue") || secureBaseProfileProps.includes("tokenValue")) && !profileUsesBasicAuth; -} - /** * Function to update session and profile information in provided node * @param profiles is data source to find profiles @@ -206,22 +193,70 @@ export class FilterDescriptor implements vscode.QuickPickItem { } export class ProfilesUtils { - public static getCredentialManagerOverride(): imperative.ICredentialManagerNameMap | undefined { + /** + * Check if the credential manager's vsix is installed for use + * @param credentialManager the display name of the credential manager + * @returns boolean whether the VS Code extension for the custom credential manager is installed + */ + public static isVSCodeCredentialPluginInstalled(credentialManager: string): boolean { + ZoweLogger.trace("ProfilesUtils.isVSCodeCredentialPluginInstalled called."); + try { + const plugin = imperative.CredentialManagerOverride.getCredMgrInfoByDisplayName(credentialManager); + return vscode.extensions.getExtension(plugin?.credMgrZEName) !== undefined; + } catch (err) { + return false; + } + } + + /** + * Get the current credential manager specified in imperative.json + * @returns string the credential manager override + */ + public static getCredentialManagerOverride(): string { ZoweLogger.trace("ProfilesUtils.getCredentialManagerOverride called."); - const knownCredentialManagers = imperative.CredentialManagerOverride.getKnownCredMgrs(); - const credentialManager = knownCredentialManagers.find((knownCredentialManager) => { - try { - return vscode.extensions.getExtension(knownCredentialManager.credMgrZEName); - } catch (err) { - return false; + try { + const settingsFilePath = path.join(getZoweDir(), "settings", "imperative.json"); + const settingsFile = fs.readFileSync(settingsFilePath); + const imperativeConfig = JSON.parse(settingsFile.toString()); + const credentialManagerOverride = imperativeConfig?.overrides[imperative.CredentialManagerOverride.CRED_MGR_SETTING_NAME]; + if (typeof credentialManagerOverride === "string") { + return credentialManagerOverride; } - }); - if (!credentialManager) { - return undefined; + return imperative.CredentialManagerOverride.DEFAULT_CRED_MGR_NAME; + } catch (err) { + ZoweLogger.info("imperative.json does not exist, returning the default override of @zowe/cli"); + return imperative.CredentialManagerOverride.DEFAULT_CRED_MGR_NAME; + } + } + + /** + * Get the map of names associated with the custom credential manager + * @param string credentialManager the credential manager display name + * @returns imperative.ICredentialManagerNameMap the map with all names related to the credential manager + */ + public static getCredentialManagerMap(credentialManager: string): imperative.ICredentialManagerNameMap | undefined { + ZoweLogger.trace("ProfilesUtils.getCredentialManagerNameMap called."); + return imperative.CredentialManagerOverride.getCredMgrInfoByDisplayName(credentialManager); + } + + /** + * Update the current credential manager override + * @param setting the credential manager to use in imperative.json + */ + public static async updateCredentialManagerSetting(setting: string): Promise { + ZoweLogger.trace("ProfilesUtils.updateCredentialManagerSetting called."); + const settingEnabled: boolean = SettingsConfig.getDirectValue(globals.SETTINGS_SECURE_CREDENTIALS_ENABLED); + if (settingEnabled) { + await globals.setGlobalSecurityValue(setting); + imperative.CredentialManagerOverride.recordCredMgrInConfig(setting); } - return imperative.CredentialManagerOverride.getCredMgrInfoByDisplayName(credentialManager.credMgrDisplayName); } + /** + * Activate a vscode extension of a custom credential manager + * @param credentialManagerExtension The credential manager VS Code extension name to activate + * @returns Promise the constructor of the activated credential manager + */ public static async activateCredentialManagerOverride( credentialManagerExtension: vscode.Extension ): Promise { @@ -237,37 +272,44 @@ export class ProfilesUtils { } } - public static async updateCredentialManagerSetting(setting: string): Promise { - ZoweLogger.trace("ProfilesUtils.updateCredentialManagerSetting called."); - const settingEnabled: boolean = SettingsConfig.getDirectValue(globals.SETTINGS_SECURE_CREDENTIALS_ENABLED); - if (settingEnabled) { - await globals.setGlobalSecurityValue(setting); - imperative.CredentialManagerOverride.recordCredMgrInConfig(setting); - } - } - - public static async getProfileInfo(envTheia: boolean): Promise { - ZoweLogger.trace("ProfilesUtils.getProfileInfo called."); - const credentialManagerMap = ProfilesUtils.getCredentialManagerOverride(); + /** + * Use the custom credential manager in Zowe Explorer and setup before use + * @param credentialManagerMap The map with associated names of the custom credential manager + * @returns Promise the object of profileInfo using the custom credential manager + */ + public static async setupCustomCredentialManager(credentialManagerMap: imperative.ICredentialManagerNameMap): Promise { + ZoweLogger.trace("ProfilesUtils.setupCustomCredentialManager called."); + ZoweLogger.info( + localize( + "ProfilesUtils.setupCustomCredentialManager.usingCustom", + "Custom credential manager {0} found, attempting to activate.", + credentialManagerMap.credMgrDisplayName + ) + ); const customCredentialManagerExtension = credentialManagerMap?.credMgrZEName && vscode.extensions.getExtension(credentialManagerMap.credMgrZEName); - const settingEnabled: boolean = SettingsConfig.getDirectValue(globals.SETTINGS_SECURE_CREDENTIALS_ENABLED); - if (credentialManagerMap && customCredentialManagerExtension && settingEnabled) { - ZoweLogger.info(localize("ProfilesUtils.getProfileInfo.usingCustom", "Custom credential manager found, attempting to activate.")); - const credentialManager = await ProfilesUtils.activateCredentialManagerOverride(customCredentialManagerExtension); - if (credentialManager) { - Object.setPrototypeOf(credentialManager.prototype, imperative.AbstractCredentialManager.prototype); - await ProfilesUtils.updateCredentialManagerSetting(credentialManagerMap.credMgrDisplayName); - return new imperative.ProfileInfo("zowe", { - credMgrOverride: { - Manager: credentialManager, - service: credentialManagerMap.credMgrDisplayName, - }, - }); - } + const credentialManager = await ProfilesUtils.activateCredentialManagerOverride(customCredentialManagerExtension); + if (credentialManager) { + Object.setPrototypeOf(credentialManager.prototype, imperative.AbstractCredentialManager.prototype); + await ProfilesUtils.updateCredentialManagerSetting(credentialManagerMap.credMgrDisplayName); + return new imperative.ProfileInfo("zowe", { + credMgrOverride: { + Manager: credentialManager, + service: credentialManagerMap.credMgrDisplayName, + }, + }); } + } - ZoweLogger.info(localize("ProfilesUtils.getProfileInfo.usingDefault", "No custom credential managers found, using the default instead.")); + /** + * Use the default credential manager in Zowe Explorer and setup before use + * @returns Promise the object of profileInfo using the default credential manager + */ + public static async setupDefaultCredentialManager(): Promise { + ZoweLogger.trace("ProfilesUtils.setupDefaultCredentialManager called."); + ZoweLogger.info( + localize("ProfilesUtils.setupDefaultCredentialManager.usingDefault", "No custom credential managers found, using the default instead.") + ); await ProfilesUtils.updateCredentialManagerSetting(globals.ZOWE_CLI_SCM); return new imperative.ProfileInfo("zowe", { // eslint-disable-next-line @typescript-eslint/no-unsafe-return @@ -275,6 +317,123 @@ export class ProfilesUtils { }); } + /** + * Fetches the first available registered custom credential manager from installed VS Code extensions. + * This function will suggest changing the imperative.json file override property if the override is different + * from the available custom credential manager. + * + * @returns Promise + */ + public static async fetchRegisteredPlugins(): Promise { + ZoweLogger.trace("ProfilesUtils.fetchRegisteredPlugins called."); + const knownCredentialManagers = imperative.CredentialManagerOverride.getKnownCredMgrs(); + const credentialManager = knownCredentialManagers.find((knownCredentialManager) => { + try { + return vscode.extensions.getExtension(knownCredentialManager.credMgrZEName); + } catch (err) { + return false; + } + }); + if (credentialManager) { + const header = localize( + "ProfilesUtils.fetchRegisteredPlugins.customCredentialManagerFound", + `Custom credential manager {0} found`, + credentialManager.credMgrDisplayName + ); + + const message = localize("ProfilesUtils.fetchRegisteredPlugins.message", "Do you wish to use this credential manager instead?"); + const optionYes = localize("ProfilesUtils.fetchRegisteredPlugins.yes", "Yes"); + const optionDontAskAgain = localize("ProfilesUtils.fetchRegisteredPlugins.dontAskAgain", "Don't ask again"); + + await Gui.infoMessage(header, { items: [optionYes, optionDontAskAgain], vsCodeOpts: { modal: true, detail: message } }).then( + async (selection) => { + if (selection === optionYes) { + await this.updateCredentialManagerSetting(credentialManager.credMgrDisplayName); + SettingsConfig.setDirectValue( + globals.SETTINGS_CHECK_FOR_CUSTOM_CREDENTIAL_MANAGERS, + false, + vscode.ConfigurationTarget.Global + ); + } + if (selection === optionDontAskAgain) { + SettingsConfig.setDirectValue( + globals.SETTINGS_CHECK_FOR_CUSTOM_CREDENTIAL_MANAGERS, + false, + vscode.ConfigurationTarget.Global + ); + } + } + ); + } + } + + /** + * Prompts to install the missing VS Code extension associated with the credential manager override in imperative.json + * + * @param credentialManager the credential manager to handle its missing VS Code extension + * @returns Promise + */ + public static async promptAndHandleMissingCredentialManager(credentialManager: imperative.ICredentialManagerNameMap): Promise { + ZoweLogger.trace("ProfilesUtils.promptAndHandleMissingCredentialManager called."); + const header = localize( + "ProfilesUtils.promptAndHandleMissingCredentialManager.suggestInstallHeader", + "Plugin of name '{0}' was defined for custom credential management on imperative.json file.", + credentialManager.credMgrDisplayName + ); + const installMessage = localize( + "ProfilesUtils.promptAndHandleMissingCredentialManager.suggestInstallMessage", + "Please install associated VS Code extension for custom credential manager or revert to default." + ); + const revertToDefaultButton = localize("ProfilesUtils.promptAndHandleMissingCredentialManager.revertToDefault", "Use Default"); + const installButton = localize("ProfilesUtils.promptAndHandleMissingCredentialManager.install", "Install"); + await Gui.infoMessage(header, { items: [installButton, revertToDefaultButton], vsCodeOpts: { modal: true, detail: installMessage } }).then( + async (selection) => { + if (selection === installButton) { + const credentialManagerInstallURL = vscode.Uri.parse( + `https://marketplace.visualstudio.com/items?itemName=${credentialManager.credMgrZEName}` + ); + if (await vscode.env.openExternal(credentialManagerInstallURL)) { + const refreshMessage = localize( + "ProfilesUtils.promptAndHandleMissingCredentialManager.refreshMessage", + `After installing the extension, please make sure to reload your VS Code window in order + to start using the installed credential manager` + ); + const reloadButton = localize("ProfilesUtils.promptAndHandleMissingCredentialManager.refreshButton", "Reload"); + if ((await Gui.showMessage(refreshMessage, { items: [reloadButton] })) === reloadButton) { + await vscode.commands.executeCommand("workbench.action.reloadWindow"); + } + } + } + } + ); + } + + public static async getProfileInfo(envTheia: boolean): Promise { + ZoweLogger.trace("ProfilesUtils.getProfileInfo called."); + const hasSecureCredentialManagerEnabled: boolean = SettingsConfig.getDirectValue(globals.SETTINGS_SECURE_CREDENTIALS_ENABLED); + + if (hasSecureCredentialManagerEnabled) { + const shouldCheckForCustomCredentialManagers = SettingsConfig.getDirectValue(globals.SETTINGS_CHECK_FOR_CUSTOM_CREDENTIAL_MANAGERS); + if (shouldCheckForCustomCredentialManagers) { + await this.fetchRegisteredPlugins(); + } + + const credentialManagerOverride = this.getCredentialManagerOverride(); + const isVSCodeCredentialPluginInstalled = this.isVSCodeCredentialPluginInstalled(credentialManagerOverride); + const isCustomCredentialPluginDefined = credentialManagerOverride !== imperative.CredentialManagerOverride.DEFAULT_CRED_MGR_NAME; + const credentialManagerMap = ProfilesUtils.getCredentialManagerMap(credentialManagerOverride); + + if (isCustomCredentialPluginDefined && !isVSCodeCredentialPluginInstalled && credentialManagerMap) { + await this.promptAndHandleMissingCredentialManager(credentialManagerMap); + } + if (credentialManagerMap && isVSCodeCredentialPluginInstalled) { + return this.setupCustomCredentialManager(credentialManagerMap); + } + } + + return this.setupDefaultCredentialManager(); + } + public static async readConfigFromDisk(): Promise { ZoweLogger.trace("ProfilesUtils.readConfigFromDisk called."); let rootPath: string; @@ -310,6 +469,32 @@ export class ProfilesUtils { } } + /** + * Function that checks whether a profile is using basic authentication + * @param profile + * @returns {Promise} a boolean representing whether basic auth is being used or not + */ + public static isProfileUsingBasicAuth(profile: imperative.IProfileLoaded): boolean { + const prof = profile.profile; + return "user" in prof && "password" in prof; + } + + /** + * Function that checks whether a profile is using token based authentication + * @param profileName the name of the profile to check + * @returns {Promise} a boolean representing whether token based auth is being used or not + */ + public static async isUsingTokenAuth(profileName: string): Promise { + const secureProfileProps = await Profiles.getInstance().getSecurePropsForProfile(profileName); + const profileUsesBasicAuth = secureProfileProps.includes("user") && secureProfileProps.includes("password"); + if (secureProfileProps.includes("tokenValue")) { + return secureProfileProps.includes("tokenValue") && !profileUsesBasicAuth; + } + const baseProfile = Profiles.getInstance().getDefaultProfile("base"); + const secureBaseProfileProps = await Profiles.getInstance().getSecurePropsForProfile(baseProfile?.name); + return secureBaseProfileProps.includes("tokenValue") && !profileUsesBasicAuth; + } + public static async promptCredentials(node: IZoweTreeNode): Promise { ZoweLogger.trace("ProfilesUtils.promptCredentials called."); const mProfileInfo = await Profiles.getInstance().getProfileInfo(); @@ -356,7 +541,7 @@ export class ProfilesUtils { // ensure the Secure Credentials Enabled value is read // set globals.PROFILE_SECURITY value accordingly const credentialManagerMap = ProfilesUtils.getCredentialManagerOverride(); - await globals.setGlobalSecurityValue(credentialManagerMap?.credMgrDisplayName ?? globals.ZOWE_CLI_SCM); + await globals.setGlobalSecurityValue(credentialManagerMap ?? globals.ZOWE_CLI_SCM); // Ensure that ~/.zowe folder exists // Ensure that the ~/.zowe/settings/imperative.json exists // TODO: update code below once this imperative issue is resolved. @@ -370,6 +555,9 @@ export class ProfilesUtils { fs.mkdirSync(settingsPath); } 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 + await ProfilesUtils.updateCredentialManagerSetting(ProfilesUtils.getCredentialManagerOverride()); // If not using team config, ensure that the ~/.zowe/profiles directory // exists with appropriate types within if (!imperative.ImperativeConfig.instance.config?.exists) { @@ -406,7 +594,7 @@ export class ProfilesUtils { ZoweLogger.error(errorMsg); ZoweLogger.debug(fileContent.toString()); settings = { ...defaultImperativeJson }; - fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2), { + return fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2), { encoding: "utf-8", flag: "w", }); @@ -421,12 +609,11 @@ export class ProfilesUtils { } else { settings = { ...defaultImperativeJson }; } - settings.overrides.CredentialManager = globals.PROFILE_SECURITY; const newData = JSON.stringify(settings, null, 2); ZoweLogger.debug( localize("writeOverridesFile.updateFile", "Updating imperative.json Credential Manager to {0}.\n{1}", globals.PROFILE_SECURITY, newData) ); - fs.writeFileSync(settingsFile, newData, { + return fs.writeFileSync(settingsFile, newData, { encoding: "utf-8", flag: "w", }); diff --git a/packages/zowe-explorer/src/utils/TreeViewUtils.ts b/packages/zowe-explorer/src/utils/TreeViewUtils.ts index a6d1f27df4..652e528fe5 100644 --- a/packages/zowe-explorer/src/utils/TreeViewUtils.ts +++ b/packages/zowe-explorer/src/utils/TreeViewUtils.ts @@ -11,6 +11,9 @@ import { IZoweNodeType, IZoweTree, IZoweTreeNode } from "@zowe/zowe-explorer-api"; import { ZoweLogger } from "./LoggerUtils"; +import { TreeViewExpansionEvent } from "vscode"; +import { getIconByNode } from "../generators/icons"; +import { ZoweTreeProvider } from "../abstract/ZoweTreeProvider"; export class TreeViewUtils { /** @@ -33,4 +36,21 @@ export class TreeViewUtils { ZoweLogger.trace("ZoweTreeProvider.expandNode called."); await provider.getTreeView().reveal(node, { expand: true }); } + + /** + * Builds an onDidCollapseElement event listener that will refresh node icons depending on the qualifiers given. + * If at least one node qualifier passes, it will refresh the icon for that node. + * @param qualifiers an array of boolean functions that take a tree node as a parameter + * @param treeProvider The tree provider that should update once the icons are changed + * @returns An event listener built to update the node icons based on the given qualifiers + */ + public static refreshIconOnCollapse(qualifiers: ((node: IZoweTreeNode) => boolean)[], treeProvider: ZoweTreeProvider) { + return (e: TreeViewExpansionEvent): any => { + const newIcon = getIconByNode(e.element); + if (qualifiers.some((q) => q(e.element)) && newIcon) { + e.element.iconPath = newIcon; + treeProvider.mOnDidChangeTreeData.fire(e.element); + } + }; + } } diff --git a/packages/zowe-explorer/webviews/edit-attributes/package.json b/packages/zowe-explorer/src/webviews/package.json similarity index 54% rename from packages/zowe-explorer/webviews/edit-attributes/package.json rename to packages/zowe-explorer/src/webviews/package.json index 1fd335a97e..15cf4c1898 100644 --- a/packages/zowe-explorer/webviews/edit-attributes/package.json +++ b/packages/zowe-explorer/src/webviews/package.json @@ -1,22 +1,22 @@ { - "name": "edit-attributes", + "name": "webviews", "private": true, "type": "module", - "version": "2.12.0-SNAPSHOT", + "version": "3.0.0-next.202309141150", "main": "index.js", "license": "EPL-2.0", "scripts": { - "dev": "vite", + "dev": "vite build --watch --config ./vite.config.js", "build": "tsc && vite build", "preview": "vite preview", "fresh-clone": "pnpm clean && rimraf node_modules", "clean": "gulp clean", - "package": "echo \"edit-attributes: nothing to package.\"", - "test": "echo \"edit-attributes: nothing to test\"", - "lint": "echo \"edit-attributes: nothing to lint.\"", - "lint:html": "echo \"edit-attributes: nothing to lint.\"", - "pretty": "echo \"edit-attributes: nothing to pretty.\"", - "madge": "echo \"edit-attributes: nothing to madge.\"" + "package": "echo \"webviews: nothing to package.\"", + "test": "echo \"webviews: nothing to test\"", + "lint": "echo \"webviews: nothing to lint.\"", + "lint:html": "echo \"webviews: nothing to lint.\"", + "pretty": "echo \"webviews: nothing to pretty.\"", + "madge": "echo \"webviews: nothing to madge.\"" }, "dependencies": { "@types/vscode-webview": "^1.57.1", @@ -29,6 +29,7 @@ "@types/lodash.isequal": "^4.5.6", "@vscode/codicons": "^0.0.33", "typescript": "^4.5.3", - "vite": "^4.4.8" + "vite": "^4.4.9", + "vite-plugin-checker": "^0.6.2" } } diff --git a/packages/zowe-explorer/webviews/edit-attributes/src/App.tsx b/packages/zowe-explorer/src/webviews/src/edit-attributes/App.tsx similarity index 90% rename from packages/zowe-explorer/webviews/edit-attributes/src/App.tsx rename to packages/zowe-explorer/src/webviews/src/edit-attributes/App.tsx index b15dcfd43e..7cc13cb430 100644 --- a/packages/zowe-explorer/webviews/edit-attributes/src/App.tsx +++ b/packages/zowe-explorer/src/webviews/src/edit-attributes/App.tsx @@ -15,6 +15,7 @@ import isEqual from "lodash.isequal"; const vscodeApi = acquireVsCodeApi(); export function App() { + const notSupported = "NOT SUPPORTED"; const [readonly, setReadonly] = useState(false); const [allowUpdate, setAllowUpdate] = useState(false); const [attributes, setAttributes] = useState>({ @@ -61,7 +62,13 @@ export function App() { useEffect(() => { window.addEventListener("message", (event) => { // Prevent users from sending data into webview outside of extension/webview context - if (!event.origin?.startsWith("vscode-webview://")) { + const eventUrl = new URL(event.origin); + const isWebUser = + (eventUrl.protocol === document.location.protocol && eventUrl.hostname === document.location.hostname) || + eventUrl.hostname.endsWith(".github.dev"); + const isLocalVSCodeUser = eventUrl.protocol === "vscode-webview:"; + + if (!isWebUser && !isLocalVSCodeUser) { return; } @@ -111,6 +118,7 @@ export function App() { }; return all; }, {}), + tag: attributes.tag ?? notSupported, }; setAttributes({ @@ -155,6 +163,17 @@ export function App() {
{attributes.current.name}
+ {attributes.initial?.directory ?? false ? null : ( +
+ updateFileAttributes("tag", e.target.value)} + > + Tag + +
+ )}
diff --git a/packages/zowe-explorer/webviews/edit-attributes/index.html b/packages/zowe-explorer/src/webviews/src/edit-attributes/index.html similarity index 81% rename from packages/zowe-explorer/webviews/edit-attributes/index.html rename to packages/zowe-explorer/src/webviews/src/edit-attributes/index.html index 3a8c5b718b..5789513d71 100644 --- a/packages/zowe-explorer/webviews/edit-attributes/index.html +++ b/packages/zowe-explorer/src/webviews/src/edit-attributes/index.html @@ -7,6 +7,6 @@
- + diff --git a/packages/zowe-explorer/webviews/edit-attributes/src/index.tsx b/packages/zowe-explorer/src/webviews/src/edit-attributes/index.tsx similarity index 100% rename from packages/zowe-explorer/webviews/edit-attributes/src/index.tsx rename to packages/zowe-explorer/src/webviews/src/edit-attributes/index.tsx diff --git a/packages/zowe-explorer/webviews/edit-attributes/src/types.ts b/packages/zowe-explorer/src/webviews/src/edit-attributes/types.ts similarity index 58% rename from packages/zowe-explorer/webviews/edit-attributes/src/types.ts rename to packages/zowe-explorer/src/webviews/src/edit-attributes/types.ts index f1dba099f4..5abc08315f 100644 --- a/packages/zowe-explorer/webviews/edit-attributes/src/types.ts +++ b/packages/zowe-explorer/src/webviews/src/edit-attributes/types.ts @@ -1,3 +1,14 @@ +/** + * 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. + * + */ + export type PermissionSet = { read: boolean; write: boolean; @@ -15,4 +26,5 @@ export type FileAttributes = { directory: boolean; group: string; perms: FilePermissions; + tag?: string; }; diff --git a/packages/zowe-explorer/src/webviews/src/edit-history/App.tsx b/packages/zowe-explorer/src/webviews/src/edit-history/App.tsx new file mode 100644 index 0000000000..3c9ed2e157 --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/edit-history/App.tsx @@ -0,0 +1,59 @@ +/** + * 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 { VSCodeDivider, VSCodePanels, VSCodePanelTab } from "@vscode/webview-ui-toolkit/react"; +import { JSXInternal } from "preact/src/jsx"; +import { isSecureOrigin } from "./components/PersistentUtils"; +import PersistentDataPanel from "./components/PersistentTable/PersistentDataPanel"; +import PersistentVSCodeAPI from "./components/PersistentVSCodeAPI"; +import PersistentManagerHeader from "./components/PersistentManagerHeader/PersistentManagerHeader"; + +export function App(): JSXInternal.Element { + const [timestamp, setTimestamp] = useState(); + const [currentTab, setCurrentTab] = useState<{ [key: string]: string }>({}); + + useEffect(() => { + window.addEventListener("message", (event) => { + if (!isSecureOrigin(event.origin)) { + return; + } + if ("tab" in event.data) { + setCurrentTab(() => ({ + tab: event.data.tab, + })); + } + setTimestamp(new Date()); + }); + PersistentVSCodeAPI.getVSCodeAPI().postMessage({ command: "ready" }); + }, []); + + return ( +
+ + + + +

Data Sets

+
+ +

Unix System Services (USS)

+
+ +

Jobs

+
+ + + +
+
+ ); +} diff --git a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentManagerHeader/PersistentManagerHeader.tsx b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentManagerHeader/PersistentManagerHeader.tsx new file mode 100644 index 0000000000..9020c76f8a --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentManagerHeader/PersistentManagerHeader.tsx @@ -0,0 +1,25 @@ +/** + * 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 { JSXInternal } from "preact/src/jsx"; + +export default function PersistentManagerHeader({ timestamp }: Readonly<{ timestamp: Readonly }>): JSXInternal.Element { + const renderTimestamp = () => { + return timestamp &&

Last refreshed: {timestamp.toLocaleString(navigator.language)}

; + }; + + return ( +
+

Manage Persistent Properties

+
{renderTimestamp()}
+
+ ); +} diff --git a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentTable/PersistentDataGridHeaders.tsx b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentTable/PersistentDataGridHeaders.tsx new file mode 100644 index 0000000000..0cc61a56ab --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentTable/PersistentDataGridHeaders.tsx @@ -0,0 +1,40 @@ +/** + * 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 { VSCodeDataGridRow, VSCodeDataGridCell } from "@vscode/webview-ui-toolkit/react"; +import { JSXInternal } from "preact/src/jsx"; +import { useDataPanelContext } from "../PersistentUtils"; +import * as nls from "vscode-nls"; + +const localize: nls.LocalizeFunc = nls.loadMessageBundle(); + +export default function PersistentDataGridHeaders(): JSXInternal.Element { + const { type, selection } = useDataPanelContext(); + const itemText = localize("PersistentDataGridHeaders.item", "Item"); + + const renderSelectHeader = () => { + const deleteText = localize("PersistentDataGridHeaders.select", "Select"); + return selection[type] === "search" || selection[type] === "fileHistory" ? ( + + {deleteText} + + ) : null; + }; + + return ( + + + {itemText} + + {renderSelectHeader()} + + ); +} 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 new file mode 100644 index 0000000000..5abf8d6e74 --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentTable/PersistentDataPanel.tsx @@ -0,0 +1,88 @@ +/** + * 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, useMemo, useState } from "preact/hooks"; +import { VSCodePanelView, VSCodeDataGrid } from "@vscode/webview-ui-toolkit/react"; +import { JSXInternal } from "preact/src/jsx"; +import { DataPanelContext, isSecureOrigin } from "../PersistentUtils"; +import { panelId } from "../../types"; +import PersistentToolBar from "../PersistentToolBar/PersistentToolBar"; +import PersistentTableData from "./PersistentTableData"; +import PersistentDataGridHeaders from "./PersistentDataGridHeaders"; +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: {} }); + const [selection, setSelection] = useState<{ [type: string]: string }>({ [type]: "search" }); + const [persistentProp, setPersistentProp] = useState([]); + const [selectedItems, setSelectedItems] = useState({}); + + const selectedItemsMemo = useMemo( + () => ({ + val: selectedItems, + setVal: (newVal: any) => setSelectedItems(newVal), + }), + [selectedItems] + ); + + const handleChange = (newSelection: string) => { + setSelection(() => ({ [type]: newSelection })); + PersistentVSCodeAPI.getVSCodeAPI().postMessage({ + command: "update-selection", + attrs: { + selection: newSelection, + type, + }, + }); + + const newSelectedItems: { [key: string]: boolean } = { ...selectedItemsMemo.val }; + Object.keys(newSelectedItems).forEach((item) => { + newSelectedItems[item] = false; + }); + selectedItemsMemo.setVal(newSelectedItems); + }; + + useEffect(() => { + window.addEventListener("message", (event) => { + if (!isSecureOrigin(event.origin)) { + return; + } + + setData(event.data); + + if ("selection" in event.data) { + setSelection(() => ({ + [type]: event.data.selection[type], + })); + } + }); + }, []); + + useEffect(() => { + setPersistentProp(() => data[type][selection[type]]); + }, [data]); + + useEffect(() => { + setPersistentProp(() => data[type][selection[type]]); + }, [selection]); + + return ( + + + + + + + + + + ); +} diff --git a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentTable/PersistentTableData.tsx b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentTable/PersistentTableData.tsx new file mode 100644 index 0000000000..d41dd3eeba --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentTable/PersistentTableData.tsx @@ -0,0 +1,67 @@ +/** + * 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 { VSCodeCheckbox, VSCodeDataGridCell, VSCodeDataGridRow } from "@vscode/webview-ui-toolkit/react"; +import { JSXInternal } from "preact/src/jsx"; +import { useDataPanelContext } from "../PersistentUtils"; +import { useEffect, useState } from "preact/hooks"; +import isEqual from "lodash.isequal"; + +export default function PersistentTableData({ persistentProp }: Readonly<{ persistentProp: readonly string[] }>): JSXInternal.Element { + const { type, selection, selectedItems } = useDataPanelContext(); + const [oldPersistentProp, setOldPersistentProp] = useState([]); + + useEffect(() => { + if (!isEqual(oldPersistentProp, persistentProp) && persistentProp) { + const newSelectedItemsList: { [key: string]: boolean } = {}; + persistentProp.forEach((prop) => { + newSelectedItemsList[prop] = false; + }); + selectedItems.setVal(newSelectedItemsList); + setOldPersistentProp(persistentProp); + } + }, [persistentProp]); + + const handleClick = (event: any, item: number) => { + selectedItems.setVal({ ...selectedItems.val, [persistentProp[item]]: !event.target.checked }); + }; + + const renderSelectButton = (item: string, i: number) => { + return selection[type] === "search" || selection[type] === "fileHistory" ? ( + + handleClick(event, i)}> + + ) : null; + }; + + const renderOptions = () => { + return persistentProp.map((item, i) => { + return ( + + {item} + {renderSelectButton(item, i)} + + ); + }); + }; + + const renderNoRecordsFound = () => { + return ( + + No records found + + ); + }; + + const data = persistentProp?.length ? renderOptions() : renderNoRecordsFound(); + + return <>{data}; +} 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 new file mode 100644 index 0000000000..6daaa283de --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentAddNewHistoryItemButton.tsx @@ -0,0 +1,43 @@ +/** + * 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 { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; +import { JSXInternal } from "preact/src/jsx"; +import { useDataPanelContext } from "../PersistentUtils"; +import PersistentVSCodeAPI from "../PersistentVSCodeAPI"; +import * as nls from "vscode-nls"; + +const localize: nls.LocalizeFunc = nls.loadMessageBundle(); + +export default function PersistentAddNewHistoryItemButton(): JSXInternal.Element { + const { type, selection } = useDataPanelContext(); + + const handleClick = () => { + PersistentVSCodeAPI.getVSCodeAPI().postMessage({ + command: "add-item", + attrs: { + type, + }, + }); + }; + + const newHistoryItemText = localize("PersistentAddNewHistoryItemButton.newHistoryItem", "Add New History Item"); + + const renderAddItemButton = () => { + return selection[type] === "search" && type !== "jobs" ? ( + + Add + + ) : null; + }; + + return <>{renderAddItemButton()}; +} 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 new file mode 100644 index 0000000000..39ca37d232 --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentClearAllButton.tsx @@ -0,0 +1,44 @@ +/** + * 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 { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; +import { JSXInternal } from "preact/src/jsx"; +import { useDataPanelContext } from "../PersistentUtils"; +import PersistentVSCodeAPI from "../PersistentVSCodeAPI"; +import * as nls from "vscode-nls"; + +const localize: nls.LocalizeFunc = nls.loadMessageBundle(); + +export default function PersistentClearAllButton(): JSXInternal.Element { + const { type, selection } = useDataPanelContext(); + + const handleClick = () => { + PersistentVSCodeAPI.getVSCodeAPI().postMessage({ + command: "clear-all", + attrs: { + type, + selection: selection[type], + }, + }); + }; + + const renderClearAllButton = () => { + const clearAllText = localize("PersistentClearAllButton.clearAll", "Clear All"); + + return selection[type] === "search" || selection[type] === "fileHistory" ? ( + + Clear All + + ) : null; + }; + + return <>{renderClearAllButton()}; +} 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 new file mode 100644 index 0000000000..8806995caf --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentDeleteSelectedButton.tsx @@ -0,0 +1,55 @@ +import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; +import { JSXInternal } from "preact/src/jsx"; +import * as nls from "vscode-nls"; +import PersistentVSCodeAPI from "../PersistentVSCodeAPI"; +import { useDataPanelContext } from "../PersistentUtils"; + +const localize: nls.LocalizeFunc = nls.loadMessageBundle(); + +export default function PersistentDeleteSelectedButton(): JSXInternal.Element { + const deleteSelectedText = localize("PersistentDeleteSelectedButton.deleteSelected", "Delete Selected"); + const { selection, type, selectedItems } = useDataPanelContext(); + + const handleClick = async () => { + const hasSelectedItems = Object.keys(selectedItems.val).find((item) => selectedItems.val[item] === true); + if (!hasSelectedItems) { + PersistentVSCodeAPI.getVSCodeAPI().postMessage({ + command: "show-error", + attrs: { + errorMsg: localize("PersistentDeleteSelectedButton.handleClick.error", "Select an item before deleting"), + }, + }); + return; + } + + PersistentVSCodeAPI.getVSCodeAPI().postMessage({ + command: "remove-item", + attrs: { + selectedItems: selectedItems.val, + selection: selection[type], + type, + }, + }); + + const newSelectedItems: { [key: string]: boolean } = { ...selectedItems.val }; + Object.keys(newSelectedItems).forEach((item) => { + newSelectedItems[item] = false; + }); + selectedItems.setVal(newSelectedItems); + }; + + const renderDeleteSelectedButton = () => { + return selection[type] === "search" || selection[type] === "fileHistory" ? ( + await handleClick()} + > + Delete + + ) : null; + }; + + return <>{renderDeleteSelectedButton()}; +} diff --git a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentDropdownOptions.tsx b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentDropdownOptions.tsx new file mode 100644 index 0000000000..89549ac98d --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentDropdownOptions.tsx @@ -0,0 +1,44 @@ +/** + * 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 { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"; +import { JSXInternal } from "preact/src/jsx"; +import { useDataPanelContext } from "../PersistentUtils"; + +export default function PersistentDropdownOptions({ handleChange }: Readonly<{ handleChange: Readonly }>): JSXInternal.Element { + const dataPanelContext = useDataPanelContext(); + + const options = [ + + Search History + , + + DS Templates + , + + Favorites + , + + File History + , + + Sessions + , + ].filter((option) => dataPanelContext.type === "ds" || option.props.value !== "dsTemplates"); + + return ( +
+ handleChange(event.target.value)}> + {options} + +
+ ); +} 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 new file mode 100644 index 0000000000..543356e77a --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentRefreshButton.tsx @@ -0,0 +1,39 @@ +/** + * 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 { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; +import { JSXInternal } from "preact/src/jsx"; +import { useDataPanelContext } from "../PersistentUtils"; +import PersistentVSCodeAPI from "../PersistentVSCodeAPI"; +import * as nls from "vscode-nls"; + +const localize: nls.LocalizeFunc = nls.loadMessageBundle(); + +export default function PersistentRefreshButton(): JSXInternal.Element { + const { type } = useDataPanelContext(); + + const handleClick = () => { + PersistentVSCodeAPI.getVSCodeAPI().postMessage({ + command: "refresh", + attrs: { + type, + }, + }); + }; + + const refreshText = localize("PersistentRefreshButton.refresh", "Refresh"); + + return ( + + Refresh + + ); +} diff --git a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentToolBar.tsx b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentToolBar.tsx new file mode 100644 index 0000000000..94155d1e21 --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentToolBar.tsx @@ -0,0 +1,29 @@ +/** + * 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 { JSXInternal } from "preact/src/jsx"; +import PersistentClearAllButton from "./PersistentClearAllButton"; +import PersistentRefreshButton from "./PersistentRefreshButton"; +import PersistentDropdownOptions from "./PersistentDropdownOptions"; +import PersistentAddNewHistoryItemButton from "./PersistentAddNewHistoryItemButton"; +import PersistentDeleteSelectedButton from "./PersistentDeleteSelectedButton"; + +export default function PersistentToolBar({ handleChange }: Readonly<{ handleChange: Readonly }>): JSXInternal.Element { + return ( +
+ + + + + +
+ ); +} diff --git a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentUtils.ts b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentUtils.ts new file mode 100644 index 0000000000..86eca22d96 --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentUtils.ts @@ -0,0 +1,38 @@ +/** + * 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 { createContext } from "preact"; +import { DataPanelContextType } from "../types"; +import { useContext } from "preact/hooks"; + +export const DataPanelContext = createContext(null); + +export function isSecureOrigin(origin: string): boolean { + const eventUrl = new URL(origin); + const isWebUser = + (eventUrl.protocol === document.location.protocol && eventUrl.hostname === document.location.hostname) || + eventUrl.hostname.endsWith(".github.dev"); + const isLocalVSCodeUser = eventUrl.protocol === "vscode-webview:"; + + if (!isWebUser && !isLocalVSCodeUser) { + return false; + } + + return true; +} + +export function useDataPanelContext(): DataPanelContextType { + const dataPanelContext = useContext(DataPanelContext); + if (!dataPanelContext) { + throw new Error("DataPanelContext has to be used within "); + } + return dataPanelContext; +} diff --git a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentVSCodeAPI.ts b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentVSCodeAPI.ts new file mode 100644 index 0000000000..740dd37242 --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentVSCodeAPI.ts @@ -0,0 +1,20 @@ +/** + * 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 { WebviewApi } from "vscode-webview"; + +export default class PersistentVSCodeAPI { + private static vscodeAPI = acquireVsCodeApi(); + + public static getVSCodeAPI(): WebviewApi { + return this.vscodeAPI; + } +} diff --git a/packages/zowe-explorer/src/webviews/src/edit-history/index.html b/packages/zowe-explorer/src/webviews/src/edit-history/index.html new file mode 100644 index 0000000000..ec4f8fc819 --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/edit-history/index.html @@ -0,0 +1,16 @@ + + + + + + + + Edit Attributes + + +
+ + + diff --git a/packages/zowe-explorer/src/webviews/src/edit-history/index.tsx b/packages/zowe-explorer/src/webviews/src/edit-history/index.tsx new file mode 100644 index 0000000000..748009dcd9 --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/edit-history/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")!); diff --git a/packages/zowe-explorer/src/webviews/src/edit-history/types.ts b/packages/zowe-explorer/src/webviews/src/edit-history/types.ts new file mode 100644 index 0000000000..77e6d08087 --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/edit-history/types.ts @@ -0,0 +1,25 @@ +/** + * 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. + * + */ + +export const panelId: { [key: string]: string } = { + ds: "ds-panel-view", + uss: "uss-panel-view", + jobs: "jobs-panel-view", +}; + +export type DataPanelContextType = { + type: string; + selection: { [type: string]: string }; + selectedItems: { + val: { [type: string]: boolean }; + setVal: (newVal: any) => void; + }; +}; diff --git a/packages/zowe-explorer/webviews/edit-attributes/tsconfig.json b/packages/zowe-explorer/src/webviews/tsconfig.json similarity index 89% rename from packages/zowe-explorer/webviews/edit-attributes/tsconfig.json rename to packages/zowe-explorer/src/webviews/tsconfig.json index 58f679257b..464518df51 100644 --- a/packages/zowe-explorer/webviews/edit-attributes/tsconfig.json +++ b/packages/zowe-explorer/src/webviews/tsconfig.json @@ -19,7 +19,8 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "types": ["vite/client"] }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] diff --git a/packages/zowe-explorer/webviews/edit-attributes/tsconfig.node.json b/packages/zowe-explorer/src/webviews/tsconfig.node.json similarity index 100% rename from packages/zowe-explorer/webviews/edit-attributes/tsconfig.node.json rename to packages/zowe-explorer/src/webviews/tsconfig.node.json diff --git a/packages/zowe-explorer/src/webviews/vite.config.ts b/packages/zowe-explorer/src/webviews/vite.config.ts new file mode 100644 index 0000000000..63643cfaa5 --- /dev/null +++ b/packages/zowe-explorer/src/webviews/vite.config.ts @@ -0,0 +1,57 @@ +/** + * 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 { defineConfig } from "vite"; +import preact from "@preact/preset-vite"; +import * as path from "path"; +import { readdirSync } from "fs"; +import checker from "vite-plugin-checker"; + +// https://vitejs.dev/config/ + +interface Webviews { + webview?: string; + webviewLocation?: string; +} + +/** + * Get all available webviews under the source specified + * @param source + * @returns Object the object where the key is the webview and the value is the location of the webview + */ +const getAvailableWebviews = (source: string): Webviews => { + return readdirSync(source, { withFileTypes: true }) + .filter((dirent) => dirent.isDirectory()) + .map((dirent) => dirent.name) + .reduce((o, key) => Object.assign(o, { [key]: path.resolve("src", key, "index.html") }), {}); +}; + +export default defineConfig({ + plugins: [ + preact(), + checker({ + typescript: true, + }), + ], + root: path.resolve(__dirname, "src"), + build: { + emptyOutDir: true, + outDir: path.resolve(__dirname, "dist"), + rollupOptions: { + input: getAvailableWebviews(path.resolve(__dirname, "src")) as any, + output: { + entryFileNames: `[name]/[name].js`, + chunkFileNames: `[name]/[name].js`, + assetFileNames: `assets/[name].[ext]`, + }, + }, + }, +}); diff --git a/packages/zowe-explorer/webviews/edit-attributes/vite.config.ts b/packages/zowe-explorer/webviews/edit-attributes/vite.config.ts deleted file mode 100644 index af1080d80b..0000000000 --- a/packages/zowe-explorer/webviews/edit-attributes/vite.config.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { defineConfig } from "vite"; -import preact from "@preact/preset-vite"; - -// https://vitejs.dev/config/ -export default defineConfig({ - plugins: [preact()], - build: { - rollupOptions: { - output: { - entryFileNames: `assets/[name].js`, - chunkFileNames: `assets/[name].js`, - assetFileNames: `assets/[name].[ext]`, - }, - }, - }, -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ae0aa3a358..fc90cf79a6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -113,6 +113,9 @@ importers: "@zowe/zowe-explorer-api": specifier: 3.0.0-next.202309141150 version: link:../zowe-explorer-api + dayjs: + specifier: ^1.11.10 + version: 1.11.10 fs-extra: specifier: 8.0.1 version: 8.0.1 @@ -262,6 +265,9 @@ importers: "@types/semver": specifier: ^7.5.0 version: 7.5.2 + copyfiles: + specifier: ^2.4.1 + version: 2.4.1 packages/zowe-explorer-ftp-extension: dependencies: @@ -288,12 +294,12 @@ importers: specifier: ^3.3.11 version: 3.3.12(webpack@4.47.0) - packages/zowe-explorer/webviews/edit-attributes: + packages/zowe-explorer/src/webviews: dependencies: "@types/vscode-webview": specifier: ^1.57.1 - version: 1.57.2 - "@vscode/webview-ui-toolkit": + version: 1.57.1 + '@vscode/webview-ui-toolkit': specifier: ^1.2.2 version: 1.2.2(react@18.2.0) lodash.isequal: @@ -301,12 +307,12 @@ importers: version: 4.5.0 preact: specifier: ^10.16.0 - version: 10.17.1 + version: 10.16.0 devDependencies: "@preact/preset-vite": specifier: ^2.5.0 - version: 2.5.0(@babel/core@7.22.20)(preact@10.17.1)(vite@4.4.9) - "@types/lodash.isequal": + version: 2.5.0(@babel/core@7.22.20)(preact@10.16.0)(vite@4.4.9) + '@types/lodash.isequal': specifier: ^4.5.6 version: 4.5.6 "@vscode/codicons": @@ -316,8 +322,11 @@ importers: specifier: ^4.5.3 version: 4.9.5 vite: - specifier: ^4.4.8 + specifier: ^4.4.9 version: 4.4.9(@types/node@14.18.62) + vite-plugin-checker: + specifier: ^0.6.2 + version: 0.6.2(eslint@8.49.0)(typescript@4.9.5)(vite@4.4.9) packages: /@aashutoshrathi/word-wrap@1.2.6: @@ -1326,8 +1335,8 @@ packages: resolution: { integrity: sha512-gQutuDHPKNxUEcQ4pypZT4Wmrbapus+P9s3bR/SEOLsMbNqNoXigGImITygI5zhb+aA5rzflM6O8YWkmRbGkPA== } dev: false - /@microsoft/fast-foundation@2.49.1: - resolution: { integrity: sha512-dSajlZeX+lkqjg4108XbIIhVLECgJTCG32bE8P6rNgo8XCPHVJBDiBejrF34lv5pO9Z2uGORZjeip/N0fPib+g== } + /@microsoft/fast-foundation@2.49.2: + resolution: {integrity: sha512-xA7WP/Td33SW0zkpHRH5LUDxyLOPnPQQXieRxc080uLWxoGXhVxo6Rz7b6qwiL+e2IadNCm7X7KcrgsUhJwvBg==} dependencies: "@microsoft/fast-element": 1.12.0 "@microsoft/fast-web-utilities": 5.4.1 @@ -1340,8 +1349,8 @@ packages: peerDependencies: react: ">=16.9.0" dependencies: - "@microsoft/fast-element": 1.12.0 - "@microsoft/fast-foundation": 2.49.1 + '@microsoft/fast-element': 1.12.0 + '@microsoft/fast-foundation': 2.49.2 react: 18.2.0 dev: false @@ -1428,17 +1437,17 @@ packages: resolution: { integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== } dev: true - /@preact/preset-vite@2.5.0(@babel/core@7.22.20)(preact@10.17.1)(vite@4.4.9): - resolution: { integrity: sha512-BUhfB2xQ6ex0yPkrT1Z3LbfPzjpJecOZwQ/xJrXGFSZD84+ObyS//41RdEoQCMWsM0t7UHGaujUxUBub7WM1Jw== } + /@preact/preset-vite@2.5.0(@babel/core@7.22.20)(preact@10.16.0)(vite@4.4.9): + resolution: {integrity: sha512-BUhfB2xQ6ex0yPkrT1Z3LbfPzjpJecOZwQ/xJrXGFSZD84+ObyS//41RdEoQCMWsM0t7UHGaujUxUBub7WM1Jw==} peerDependencies: "@babel/core": 7.x vite: 2.x || 3.x || 4.x dependencies: - "@babel/core": 7.22.20 - "@babel/plugin-transform-react-jsx": 7.22.15(@babel/core@7.22.20) - "@babel/plugin-transform-react-jsx-development": 7.22.5(@babel/core@7.22.20) - "@prefresh/vite": 2.4.1(preact@10.17.1)(vite@4.4.9) - "@rollup/pluginutils": 4.2.1 + '@babel/core': 7.22.20 + '@babel/plugin-transform-react-jsx': 7.22.15(@babel/core@7.22.20) + '@babel/plugin-transform-react-jsx-development': 7.22.5(@babel/core@7.22.20) + '@prefresh/vite': 2.4.1(preact@10.16.0)(vite@4.4.9) + '@rollup/pluginutils': 4.2.1 babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.22.20) debug: 4.3.4(supports-color@8.1.1) kolorist: 1.8.0 @@ -1453,30 +1462,30 @@ packages: resolution: { integrity: sha512-joAwpkUDwo7ZqJnufXRGzUb+udk20RBgfA8oLPBh5aJH2LeStmV1luBfeJTztPdyCscC2j2SmZ/tVxFRMIxAEw== } dev: true - /@prefresh/core@1.5.1(preact@10.17.1): - resolution: { integrity: sha512-e0mB0Oxtog6ZpKPDBYbzFniFJDIktuKMzOHp7sguntU+ot0yi6dbhJRE9Css1qf0u16wdSZjpL2W2ODWuU05Cw== } + /@prefresh/core@1.5.2(preact@10.16.0): + resolution: {integrity: sha512-A/08vkaM1FogrCII5PZKCrygxSsc11obExBScm3JF1CryK2uDS3ZXeni7FeKCx1nYdUkj4UcJxzPzc1WliMzZA==} peerDependencies: preact: ^10.0.0 dependencies: - preact: 10.17.1 + preact: 10.16.0 dev: true /@prefresh/utils@1.2.0: resolution: { integrity: sha512-KtC/fZw+oqtwOLUFM9UtiitB0JsVX0zLKNyRTA332sqREqSALIIQQxdUCS1P3xR/jT1e2e8/5rwH6gdcMLEmsQ== } dev: true - /@prefresh/vite@2.4.1(preact@10.17.1)(vite@4.4.9): - resolution: { integrity: sha512-vthWmEqu8TZFeyrBNc9YE5SiC3DVSzPgsOCp/WQ7FqdHpOIJi7Z8XvCK06rBPOtG4914S52MjG9Ls22eVAiuqQ== } + /@prefresh/vite@2.4.1(preact@10.16.0)(vite@4.4.9): + resolution: {integrity: sha512-vthWmEqu8TZFeyrBNc9YE5SiC3DVSzPgsOCp/WQ7FqdHpOIJi7Z8XvCK06rBPOtG4914S52MjG9Ls22eVAiuqQ==} peerDependencies: preact: ^10.4.0 vite: ">=2.0.0" dependencies: - "@babel/core": 7.22.20 - "@prefresh/babel-plugin": 0.5.0 - "@prefresh/core": 1.5.1(preact@10.17.1) - "@prefresh/utils": 1.2.0 - "@rollup/pluginutils": 4.2.1 - preact: 10.17.1 + '@babel/core': 7.22.20 + '@prefresh/babel-plugin': 0.5.0 + '@prefresh/core': 1.5.2(preact@10.16.0) + '@prefresh/utils': 1.2.0 + '@rollup/pluginutils': 4.2.1 + preact: 10.16.0 vite: 4.4.9(@types/node@14.18.62) transitivePeerDependencies: - supports-color @@ -1652,11 +1661,11 @@ packages: /@types/lodash.isequal@4.5.6: resolution: { integrity: sha512-Ww4UGSe3DmtvLLJm2F16hDwEQSv7U0Rr8SujLUA2wHI2D2dm8kPu6Et+/y303LfjTIwSBKXB/YTUcAKpem/XEg== } dependencies: - "@types/lodash": 4.14.198 + '@types/lodash': 4.14.200 dev: true - /@types/lodash@4.14.198: - resolution: { integrity: sha512-trNJ/vtMZYMLhfN45uLq4ShQSw0/S7xCTLLVM+WM1rmFpba/VS42jVUgaO3w/NOLiWR/09lnYk0yMaA/atdIsg== } + /@types/lodash@4.14.200: + resolution: {integrity: sha512-YI/M/4HRImtNf3pJgbF+W6FrXovqj+T+/HpENLTooK9PnkacBsDpeP3IpHab40CClUfhNmdM2WTNP2sa2dni5Q==} dev: true /@types/markdown-it@12.2.3: @@ -1716,8 +1725,8 @@ packages: resolution: { integrity: sha512-flgpHJjntpBAdJD43ShRosQvNC0ME97DCfGvZEDlAThQmnerRXrLbX6YgzRBQCZTthET9eAWFAMaYP0m0Y4HzQ== } dev: true - /@types/vscode-webview@1.57.2: - resolution: { integrity: sha512-RpkIso3+FVoi9hFwHj9uBFO+9p8PGym0LnLJ9Yabo9mUJaV39CzOxz6EVtHg8AidA9hAf4cVmG0c+l9pvw6Lbw== } + /@types/vscode-webview@1.57.1: + resolution: {integrity: sha512-ghW5SfuDmsGDS2A4xkvGsLwDRNc3Vj5rS6rPOyPm/IryZuf3wceZKxgYaUoW+k9f0f/CB7y2c1rRsdOWZWn0PQ==} dev: false /@types/vscode@1.82.0: @@ -1921,9 +1930,9 @@ packages: peerDependencies: react: ">=16.9.0" dependencies: - "@microsoft/fast-element": 1.12.0 - "@microsoft/fast-foundation": 2.49.1 - "@microsoft/fast-react-wrapper": 0.1.48(react@18.2.0) + '@microsoft/fast-element': 1.12.0 + '@microsoft/fast-foundation': 2.49.2 + '@microsoft/fast-react-wrapper': 0.1.48(react@18.2.0) react: 18.2.0 dev: false @@ -3619,8 +3628,13 @@ packages: dev: true /commander@7.2.0: - resolution: { integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== } - engines: { node: ">= 10" } + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: { node: '>= 10'} + dev: true + + /commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} dev: true /commander@9.4.1: @@ -3742,6 +3756,19 @@ packages: - bluebird dev: true + /copyfiles@2.4.1: + resolution: {integrity: sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==} + hasBin: true + dependencies: + glob: 7.2.3 + minimatch: 3.1.2 + mkdirp: 1.0.4 + noms: 0.0.0 + through2: 2.0.5 + untildify: 4.0.0 + yargs: 16.2.0 + dev: true + /core-util-is@1.0.3: resolution: { integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== } @@ -3935,6 +3962,10 @@ packages: resolution: { integrity: sha512-EelsCzH0gMC2YmXuMeaZ3c6md1sUJQxyb1XXc4xaisi/K6qKukqZhKPrEQyRkdNIncgYyLoDTReq0nNyuKerTg== } dev: true + /dayjs@1.11.10: + resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} + dev: false + /debug-fabulous@1.1.0: resolution: { integrity: sha512-GZqvGIgKNlUnHUPQhepnUZFIMoi3dgZKQBzKDeL2g7oJF9SNAji/AAu36dusFUas0O+pae74lNeoIPHqXWDkLg== } dependencies: @@ -5342,7 +5373,16 @@ packages: dev: true /fs-constants@1.0.0: - resolution: { integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== } + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + dev: true + + /fs-extra@11.1.1: + resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==} + engines: {node: '>=14.14'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.0 dev: true /fs-extra@8.0.1: @@ -7262,6 +7302,14 @@ packages: optionalDependencies: graceful-fs: 4.2.11 + /jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + dependencies: + universalify: 2.0.0 + optionalDependencies: + graceful-fs: 4.2.11 + dev: true + /jsonparse@1.3.1: resolution: { integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== } engines: { "0": node >= 0.2.0 } @@ -7497,6 +7545,10 @@ packages: lodash: 4.17.21 dev: false + /lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + dev: true + /lodash.get@4.4.2: resolution: { integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== } dev: true @@ -7510,7 +7562,11 @@ packages: dev: true /lodash.merge@4.6.2: - resolution: { integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== } + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + /lodash.pick@4.4.0: + resolution: {integrity: sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==} + dev: true /lodash.truncate@4.4.2: resolution: { integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== } @@ -8341,8 +8397,15 @@ packages: dev: true /node-status-codes@1.0.0: - resolution: { integrity: sha512-1cBMgRxdMWE8KeWCqk2RIOrvUb0XCwYfEsY5/y2NlXyq4Y/RumnOZvTj4Nbr77+Vb2C+kyBoRTdkNOS8L3d/aQ== } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-1cBMgRxdMWE8KeWCqk2RIOrvUb0XCwYfEsY5/y2NlXyq4Y/RumnOZvTj4Nbr77+Vb2C+kyBoRTdkNOS8L3d/aQ==} + engines: {node: '>=0.10.0'} + dev: true + + /noms@0.0.0: + resolution: {integrity: sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==} + dependencies: + inherits: 2.0.4 + readable-stream: 1.0.34 dev: true /nopt@1.0.10: @@ -9058,8 +9121,8 @@ packages: source-map-js: 1.0.2 dev: true - /preact@10.17.1: - resolution: { integrity: sha512-X9BODrvQ4Ekwv9GURm9AKAGaomqXmip7NQTZgY7gcNmr7XE83adOMJvd3N42id1tMFU7ojiynRsYnY6/BRFxLA== } + /preact@10.16.0: + resolution: {integrity: sha512-XTSj3dJ4roKIC93pald6rWuB2qQJO9gO2iLLyTe87MrjQN+HklueLsmskbywEWqCHlclgz3/M4YLL2iBr9UmMA==} /prebuild-install@7.1.1: resolution: { integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw== } @@ -9425,6 +9488,15 @@ packages: dependencies: mute-stream: 0.0.8 + /readable-stream@1.0.34: + resolution: {integrity: sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==} + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 0.0.1 + string_decoder: 0.10.31 + dev: true + /readable-stream@1.1.14: resolution: { integrity: sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ== } dependencies: @@ -9714,9 +9786,9 @@ packages: inherits: 2.0.4 dev: true - /rollup@3.29.2: - resolution: { integrity: sha512-CJouHoZ27v6siztc21eEQGo0kIcE5D1gVPA571ez0mMYb25LGYGKnVNXpEj5MGlepmDWGXNjDB5q7uNiPHC11A== } - engines: { node: ">=14.18.0", npm: ">=8.0.0" } + /rollup@3.29.4: + resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true optionalDependencies: fsevents: 2.3.3 @@ -10282,8 +10354,7 @@ packages: strip-ansi: 6.0.1 /string_decoder@0.10.31: - resolution: { integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ== } - dev: false + resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==} /string_decoder@1.1.1: resolution: { integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== } @@ -10593,6 +10664,10 @@ packages: next-tick: 1.1.0 dev: true + /tiny-invariant@1.3.1: + resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} + dev: true + /tmp@0.0.30: resolution: { integrity: sha512-HXdTB7lvMwcb55XFfrTM8CPr/IYREk4hVBFaQ4b/6nInrluSL86hfHm7vu0luYKCfyBZp2trCjpc8caC3vVM3w== } engines: { node: ">=0.4.0" } @@ -10940,8 +11015,13 @@ packages: engines: { node: ">= 4.0.0" } /universalify@0.2.0: - resolution: { integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== } - engines: { node: ">= 4.0.0" } + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + dev: true + + /universalify@2.0.0: + resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} + engines: {node: '>= 10.0.0'} dev: true /unset-value@1.0.0: @@ -10952,6 +11032,11 @@ packages: isobject: 3.0.1 dev: true + /untildify@4.0.0: + resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} + engines: {node: '>=8'} + dev: true + /unzip-response@1.0.2: resolution: { integrity: sha512-pwCcjjhEcpW45JZIySExBHYv5Y9EeL2OIGEfrSKp2dMUFGFv4CpvZkwJbVge8OvGH2BNNtJBx67DuKuJhf+N5Q== } engines: { node: ">=0.10" } @@ -11141,6 +11226,59 @@ packages: replace-ext: 1.0.1 dev: true + /vite-plugin-checker@0.6.2(eslint@8.49.0)(typescript@4.9.5)(vite@4.4.9): + resolution: {integrity: sha512-YvvvQ+IjY09BX7Ab+1pjxkELQsBd4rPhWNw8WLBeFVxu/E7O+n6VYAqNsKdK/a2luFlX/sMpoWdGFfg4HvwdJQ==} + engines: {node: '>=14.16'} + peerDependencies: + eslint: '>=7' + meow: ^9.0.0 + optionator: ^0.9.1 + stylelint: '>=13' + typescript: '*' + vite: '>=2.0.0' + vls: '*' + vti: '*' + vue-tsc: '>=1.3.9' + peerDependenciesMeta: + eslint: + optional: true + meow: + optional: true + optionator: + optional: true + stylelint: + optional: true + typescript: + optional: true + vls: + optional: true + vti: + optional: true + vue-tsc: + optional: true + dependencies: + '@babel/code-frame': 7.22.13 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + chokidar: 3.5.3 + commander: 8.3.0 + eslint: 8.49.0 + fast-glob: 3.3.1 + fs-extra: 11.1.1 + lodash.debounce: 4.0.8 + lodash.pick: 4.4.0 + npm-run-path: 4.0.1 + semver: 7.5.4 + strip-ansi: 6.0.1 + tiny-invariant: 1.3.1 + typescript: 4.9.5 + vite: 4.4.9(@types/node@14.18.62) + vscode-languageclient: 7.0.0 + vscode-languageserver: 7.0.0 + vscode-languageserver-textdocument: 1.0.11 + vscode-uri: 3.0.8 + dev: true + /vite@4.4.9(@types/node@14.18.62): resolution: { integrity: sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA== } engines: { node: ^14.18.0 || >=16.0.0 } @@ -11172,7 +11310,7 @@ packages: "@types/node": 14.18.62 esbuild: 0.18.20 postcss: 8.4.30 - rollup: 3.29.2 + rollup: 3.29.4 optionalDependencies: fsevents: 2.3.3 dev: true @@ -11209,6 +11347,42 @@ packages: yazl: 2.5.1 dev: true + /vscode-jsonrpc@6.0.0: + resolution: {integrity: sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==} + engines: {node: '>=8.0.0 || >=10.0.0'} + dev: true + + /vscode-languageclient@7.0.0: + resolution: {integrity: sha512-P9AXdAPlsCgslpP9pRxYPqkNYV7Xq8300/aZDpO35j1fJm/ncize8iGswzYlcvFw5DQUx4eVk+KvfXdL0rehNg==} + engines: {vscode: ^1.52.0} + dependencies: + minimatch: 3.1.2 + semver: 7.5.4 + vscode-languageserver-protocol: 3.16.0 + dev: true + + /vscode-languageserver-protocol@3.16.0: + resolution: {integrity: sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==} + dependencies: + vscode-jsonrpc: 6.0.0 + vscode-languageserver-types: 3.16.0 + dev: true + + /vscode-languageserver-textdocument@1.0.11: + resolution: {integrity: sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==} + dev: true + + /vscode-languageserver-types@3.16.0: + resolution: {integrity: sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==} + dev: true + + /vscode-languageserver@7.0.0: + resolution: {integrity: sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==} + hasBin: true + dependencies: + vscode-languageserver-protocol: 3.16.0 + dev: true + /vscode-nls-dev@4.0.4: resolution: { integrity: sha512-0KQUVkeRTmKVH4a96ZeD+1RgQV6k21YiBYykrvbMX62m6srPC6aU9CWuWT6zrMAB6qmy9sUD0/Bk6P/atLVMrw== } hasBin: true @@ -11244,6 +11418,10 @@ packages: - supports-color dev: true + /vscode-uri@3.0.8: + resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + dev: true + /w3c-hr-time@1.0.2: resolution: { integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ== } deprecated: Use your platform's native performance.now() and performance.timeOrigin. diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 00cd88ef47..f45e469528 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,4 +1,4 @@ packages: - "packages/*" - - "packages/zowe-explorer/webviews/*" + - "packages/zowe-explorer/src/webviews/" - "!**/test/**" diff --git a/samples/menu-item-sample/package.json b/samples/menu-item-sample/package.json index fec6aa02fa..4f0337d6d2 100644 --- a/samples/menu-item-sample/package.json +++ b/samples/menu-item-sample/package.json @@ -46,7 +46,7 @@ "watch": "tsc -watch -p ./" }, "dependencies": { - "@zowe/zowe-explorer-api": "^2.10.0" + "@zowe/zowe-explorer-api": "file:../../packages/zowe-explorer-api" }, "devDependencies": { "@types/node": "^16.18.34", diff --git a/samples/tree-view-sample/package.json b/samples/tree-view-sample/package.json index bfe07d14bf..f8861780ee 100644 --- a/samples/tree-view-sample/package.json +++ b/samples/tree-view-sample/package.json @@ -50,7 +50,7 @@ }, "dependencies": { "@zowe/imperative": "^5.18.0", - "@zowe/zowe-explorer-api": "^2.10.0" + "@zowe/zowe-explorer-api": "file:../../packages/zowe-explorer-api" }, "devDependencies": { "@types/node": "^16.18.34", diff --git a/samples/tree-view-sample/src/ProfilesTreeProvider.ts b/samples/tree-view-sample/src/ProfilesTreeProvider.ts index 90068db5ce..1649b0f797 100644 --- a/samples/tree-view-sample/src/ProfilesTreeProvider.ts +++ b/samples/tree-view-sample/src/ProfilesTreeProvider.ts @@ -34,7 +34,7 @@ export class ProfilesTreeProvider implements vscode.TreeDataProvider { if (this._dirty) { - const profiles = new ProfilesCache(Logger.getAppLogger(), vscode.workspace.workspaceFolders?.[0]?.uri.fsPath); + const profiles = new ProfilesCache(Logger.getAppLogger() as any, vscode.workspace.workspaceFolders?.[0]?.uri.fsPath); this._profileData = (await profiles.getProfileInfo()).getAllProfiles(); this._dirty = false; } diff --git a/samples/uss-profile-sample/package.json b/samples/uss-profile-sample/package.json index 5733dfebee..9f0e097fd5 100644 --- a/samples/uss-profile-sample/package.json +++ b/samples/uss-profile-sample/package.json @@ -34,7 +34,7 @@ }, "dependencies": { "@zowe/cli": "^7.18.0", - "@zowe/zowe-explorer-api": "^2.10.0", + "@zowe/zowe-explorer-api": "file:../../packages/zowe-explorer-api", "ssh2-sftp-client": "^9.1.0" }, "devDependencies": { diff --git a/samples/uss-profile-sample/src/SshUssApi.ts b/samples/uss-profile-sample/src/SshUssApi.ts index 9037f1612d..54015231a6 100644 --- a/samples/uss-profile-sample/src/SshUssApi.ts +++ b/samples/uss-profile-sample/src/SshUssApi.ts @@ -1,7 +1,7 @@ import * as Client from "ssh2-sftp-client"; import * as vscode from "vscode"; import { IDownloadOptions, IUploadOptions, IZosFilesResponse, ZosUssProfile, imperative } from "@zowe/cli"; -import { ZoweExplorerApi } from "@zowe/zowe-explorer-api"; +import * as ZoweExplorerApi from "@zowe/zowe-explorer-api"; export class SshUssApi implements ZoweExplorerApi.IUss { public constructor(public profile?: imperative.IProfileLoaded) {} @@ -58,14 +58,7 @@ export class SshUssApi implements ZoweExplorerApi.IUss { }); } - public async putContents( - inputFilePath: string, - ussFilePath: string, - binary?: boolean | undefined, - localEncoding?: string | undefined, - etag?: string | undefined, - returnEtag?: boolean | undefined - ): Promise { + public async putContent(inputFilePath: string, ussFilePath: string): Promise { return this.withClient(this.getSession(), async (client) => { const response = await client.fastPut(inputFilePath, ussFilePath); return this.buildZosFilesResponse(response); diff --git a/samples/vue-webview-sample/package.json b/samples/vue-webview-sample/package.json index 3be1dfc345..d574be5cf2 100644 --- a/samples/vue-webview-sample/package.json +++ b/samples/vue-webview-sample/package.json @@ -27,7 +27,7 @@ "watch": "pnpm --package=typescript dlx tsc -watch -p ./" }, "dependencies": { - "@zowe/zowe-explorer-api": "^2.10.0" + "@zowe/zowe-explorer-api": "file:../../packages/zowe-explorer-api" }, "devDependencies": { "@types/node": "^16.18.41",