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

Troubleshooting

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

Error details

+

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

+

+ Description: + {error.message} +

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

Additional resources

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

Tips

+
    + {tips.map((tip) => ( +
  • {tip}
  • + ))} +
+
+ ); +}; diff --git a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/index.html b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/index.html new file mode 100644 index 0000000000..9beb19e39c --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/index.html @@ -0,0 +1,16 @@ + + + + + + + + Troubleshoot Error + + +
+ + + diff --git a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/index.tsx b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/index.tsx new file mode 100644 index 0000000000..748009dcd9 --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/index.tsx @@ -0,0 +1,15 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { render } from "preact"; +import { App } from "./App"; + +render(, document.getElementById("webviewRoot")!);