Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error correlator and troubleshoot webview #3243

Merged
merged 75 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from 71 commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
5c59d41
wip: ErrorCorrelator facility and work on error handling
traeok Sep 23, 2024
96de43b
chore: add typedoc to ErrorCorrelation; details -> summary
traeok Sep 25, 2024
17f1bea
wip: Error prompts and webview PoC
traeok Sep 26, 2024
a9ec1b7
feat: Troubleshoot webview, move PersistentVSCodeAPI
traeok Sep 27, 2024
2b1083c
refactor: Move TipList into component file, troubleshoot format
traeok Oct 1, 2024
4466a30
Merge branch 'main' into fix/error-handling-saves
traeok Oct 3, 2024
3a414ff
wip: set up test cases and rename function
traeok Oct 3, 2024
3f0eff1
tests: impl ErrorCorrelator.displayError test cases
traeok Oct 7, 2024
590c124
tests: ErrorCorrelator.correlateError test cases
traeok Oct 7, 2024
b44ce2d
wip: Add more error correlations; test data set error handling
traeok Oct 7, 2024
4dc0a25
wip(ErrorCorrelator): collapsible error section, Copy Details btn
traeok Oct 7, 2024
0a2d4fa
copy details button, fix summary toggle state
traeok Oct 8, 2024
0c490f6
update copied content for copy details button
traeok Oct 8, 2024
dbfa1f3
feat: support template args in error summaries
traeok Oct 9, 2024
0f42997
wip: update AuthUtils.errorHandling to use correlator
traeok Oct 9, 2024
2ad5cbb
Merge branch 'main' into fix/error-handling-saves
traeok Oct 10, 2024
5c5e963
wip: update AuthUtils.errorHandling signature and update calls
traeok Oct 10, 2024
9376009
pass template args from error context, add mvs error correlation
traeok Oct 10, 2024
3976e68
wip: separate function to display correlation
traeok Oct 10, 2024
01f5ae5
wip: add params to AuthUtils.errorHandling for correlator
traeok Oct 11, 2024
27a21db
tests: Resolve failing test cases
traeok Oct 14, 2024
5186e84
refactor: Use API type, then profile type for narrowing
traeok Oct 14, 2024
53e9518
wip: Prompt for creds when opening DS
traeok Oct 16, 2024
41d058c
fix(api): Fix profile references being lost when cache is refreshed (…
t1m0thyj Oct 16, 2024
42fe233
Revert "wip: Prompt for creds when opening DS"
traeok Oct 16, 2024
fdc6874
refactor: Add back error correlator changes
traeok Oct 16, 2024
e342fec
refactor: fix tests to handle new format
traeok Oct 16, 2024
2b2ee7b
tests: TroubleshootError webview class
traeok Oct 16, 2024
971a44e
refactor: rename TroubleshootError.setErrorData -> sendErrorData
traeok Oct 16, 2024
6dbc519
remaining TroubleshootError cases, add log for unknown cmd
traeok Oct 16, 2024
9c93f46
refactor: NetworkError -> CorrelatedError; cleanup class, fix type guard
traeok Oct 16, 2024
ab4ea58
impl. correlator for FSP fns; update correlator tests
traeok Oct 16, 2024
a3167c9
tests: resolve failing test cases
traeok Oct 16, 2024
b416fa1
refactor: throw errs instead of return; update tests
traeok Oct 17, 2024
2cf2e72
Merge branch 'main' into fix/error-handling-saves
traeok Oct 18, 2024
e89342c
fix delete & stat error tests, run prepublish
traeok Oct 18, 2024
f23f3f4
error handling cases for stat
traeok Oct 18, 2024
50e45d4
refactor _handleError, avoid use of await for errors
traeok Oct 18, 2024
39107c4
Merge branch 'main' into fix/error-handling-saves
traeok Oct 21, 2024
746ebdf
wip: add coverage to DataSet FSP
traeok Oct 21, 2024
4453ef6
wip: more ds/uss test cases
traeok Oct 21, 2024
91c8d65
Merge branch 'main' into fix/error-handling-saves
traeok Oct 21, 2024
656036c
BaseProvider._handleError test cases
traeok Oct 21, 2024
1884cb5
jobs test cases for error handling
traeok Oct 21, 2024
d9597e3
chore: update changelogs
traeok Oct 21, 2024
732a083
expose error correlator in extender API
traeok Oct 22, 2024
afba192
chore: address changelog feedback
traeok Oct 22, 2024
ab3ac95
Merge branch 'main' into fix/error-handling-saves
traeok Oct 22, 2024
5ceaf6c
fix circular dep
traeok Oct 22, 2024
d9e022a
allow extenders to contribute resources for errors
traeok Oct 22, 2024
b2e489f
Merge branch 'main' into fix/error-handling-saves
JillieBeanSim Oct 24, 2024
6051d71
handle errors when listing files in virtual workspaces
traeok Oct 24, 2024
194dc41
remove check for handleError mock in listFiles test
traeok Oct 24, 2024
c6c2fe8
skip dialog if no correlation found, fix missing info in webview
traeok Oct 24, 2024
c1a14f4
fix tests, update logic for returning selection
traeok Oct 24, 2024
1c1e0a9
offer show log opt in first dialog if correlation not found
traeok Oct 24, 2024
3ad3b93
omit profile details from log, update failing tests
traeok Oct 24, 2024
b97eaf3
Merge branch 'main' into fix/error-handling-saves
traeok Oct 24, 2024
f2eba25
restore changes to ZoweTreeNode
traeok Oct 29, 2024
376de38
move HandleErrorOpts to fs/types/abstract
traeok Oct 29, 2024
162836a
remove export from ErrorContext interface
traeok Oct 29, 2024
2d61516
Merge branch 'main' into fix/error-handling-saves
traeok Oct 29, 2024
be3c949
revert changes to ZoweTreeNode tests
traeok Oct 29, 2024
3befca9
make IApiExplorerExtender.getErrorCorrelator optional
traeok Oct 29, 2024
98173e3
Merge branch 'main' into fix/error-handling-saves
traeok Oct 30, 2024
7432f4a
update command count, run l10n prepublish
traeok Oct 30, 2024
1c9e32b
Merge branch 'main' into fix/error-handling-saves
traeok Oct 31, 2024
2ff27a6
address duplicate errors, pass profileName as template arg
traeok Oct 31, 2024
a3cfc88
resolve failing tests from changes
traeok Oct 31, 2024
80f7be2
refactor: use optional chaining to spread templateArgs
traeok Nov 5, 2024
5f456d2
Merge branch 'main' into fix/error-handling-saves
JillieBeanSim Nov 5, 2024
a2f0307
refactor: use dsName instead of path; rm handling in autoDetectEncoding
traeok Nov 5, 2024
694ebd5
run package
JillieBeanSim Nov 5, 2024
ccbafef
fix: propagate USS listFiles error
traeok Nov 5, 2024
df1534c
Merge branch 'main' into fix/error-handling-saves
traeok Nov 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/zowe-explorer-api/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,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)
zFernand0 marked this conversation as resolved.
Show resolved Hide resolved

## `3.0.1`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
});
});
8 changes: 8 additions & 0 deletions packages/zowe-explorer-api/src/extend/IApiExplorerExtender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<void>;

/**
* 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;
}
21 changes: 20 additions & 1 deletion packages/zowe-explorer-api/src/fs/BaseProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
zFernand0 marked this conversation as resolved.
Show resolved Hide resolved
});
}

protected _lookupAsDirectory(uri: vscode.Uri, silent: boolean): DirEntry {
const entry = this.lookup(uri, silent);
if (entry != null && !FsAbstractUtils.isDirectoryEntry(entry) && !silent) {
Expand Down
12 changes: 12 additions & 0 deletions packages/zowe-explorer-api/src/fs/types/abstract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -147,3 +148,14 @@ export type UriFsInfo = {
profileName: string;
profile?: IProfileLoaded;
};

export interface HandleErrorOpts {
retry?: {
fn: (...args: any[]) => any | PromiseLike<any>;
args?: any[];
};
profileType?: string;
apiType?: ZoweExplorerApiType;
templateArgs?: Record<string, string>;
additionalContext?: string;
}
3 changes: 2 additions & 1 deletion packages/zowe-explorer-api/src/profiles/ProfilesCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
traeok marked this conversation as resolved.
Show resolved Hide resolved
}
allProfiles.push(...tmpAllProfiles);
this.profilesByType.set(type, tmpAllProfiles);
Expand Down
Loading
Loading