diff --git a/packages/zowe-explorer-api/CHANGELOG.md b/packages/zowe-explorer-api/CHANGELOG.md index 182c6cc8b0..0eb1e421fc 100644 --- a/packages/zowe-explorer-api/CHANGELOG.md +++ b/packages/zowe-explorer-api/CHANGELOG.md @@ -6,6 +6,14 @@ All notable changes to the "zowe-explorer-api" extension will be documented in t ### New features and enhancements +- Added optional `getLocalStorage` function to the `IApiExplorerExtender` interface to expose local storage access to Zowe Explorer extenders. [#3180](https://github.com/zowe/zowe-explorer-vscode/issues/3180) + +### Bug fixes + +## `3.0.3` + +### New features and enhancements + - Update Zowe SDKs to `8.8.2` to get the latest enhancements from Imperative. [#3296](https://github.com/zowe/zowe-explorer-vscode/pull/3296) ### Bug fixes diff --git a/packages/zowe-explorer-api/src/extend/IApiExplorerExtender.ts b/packages/zowe-explorer-api/src/extend/IApiExplorerExtender.ts index 348c9e773b..d566bb60b5 100644 --- a/packages/zowe-explorer-api/src/extend/IApiExplorerExtender.ts +++ b/packages/zowe-explorer-api/src/extend/IApiExplorerExtender.ts @@ -12,6 +12,7 @@ import * as imperative from "@zowe/imperative"; import { ProfilesCache } from "../profiles/ProfilesCache"; import { ErrorCorrelator } from "../utils/ErrorCorrelator"; +import { ILocalStorageAccess } from "./ILocalStorageAccess"; /** * This interface can be used by other VS Code Extensions to access an alternative @@ -53,4 +54,11 @@ export interface IApiExplorerExtender { * provide tips or additional resources for errors. */ getErrorCorrelator?(): ErrorCorrelator; + + /** + * Allows extenders to access Zowe Explorer's local storage values. Retrieve a list of + * readable and writable keys by calling the `getReadableKeys, getWritableKeys` functions + * on the returned instance. + */ + getLocalStorage?(): ILocalStorageAccess; } diff --git a/packages/zowe-explorer-api/src/extend/ILocalStorageAccess.ts b/packages/zowe-explorer-api/src/extend/ILocalStorageAccess.ts new file mode 100644 index 0000000000..0c4b9299b7 --- /dev/null +++ b/packages/zowe-explorer-api/src/extend/ILocalStorageAccess.ts @@ -0,0 +1,38 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +export interface ILocalStorageAccess { + /** + * @returns The list of readable keys from the access facility + */ + getReadableKeys(): string[]; + + /** + * @returns The list of writable keys from the access facility + */ + getWritableKeys(): string[]; + + /** + * Retrieve the value from local storage for the given key. + * @param key A readable key + * @returns The value if it exists in local storage, or `undefined` otherwise + * @throws If the extender does not have appropriate read permissions for the given key + */ + getValue(key: string): T; + + /** + * Set a value in local storage for the given key. + * @param key A writable key + * @param value The new value for the given key to set in local storage + * @throws If the extender does not have appropriate write permissions for the given key + */ + setValue(key: string, value: T): Thenable; +} diff --git a/packages/zowe-explorer-api/src/extend/index.ts b/packages/zowe-explorer-api/src/extend/index.ts index 20ac2b4956..eeeae7597e 100644 --- a/packages/zowe-explorer-api/src/extend/index.ts +++ b/packages/zowe-explorer-api/src/extend/index.ts @@ -10,5 +10,6 @@ */ export * from "./IApiExplorerExtender"; +export * from "./ILocalStorageAccess"; export * from "./IRegisterClient"; export * from "./MainframeInteraction"; diff --git a/packages/zowe-explorer/CHANGELOG.md b/packages/zowe-explorer/CHANGELOG.md index f34bc22683..53edd48f8d 100644 --- a/packages/zowe-explorer/CHANGELOG.md +++ b/packages/zowe-explorer/CHANGELOG.md @@ -14,12 +14,14 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen - 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) - Allow extenders to add context menu actions to a top level node, i.e. data sets, USS, Jobs, by encoding the profile type in the context value. [#3309](https://github.com/zowe/zowe-explorer-vscode/pull/3309) - You can now add multiple partitioned data sets or USS directories to your workspace at once using the "Add to Workspace" feature. [#3324](https://github.com/zowe/zowe-explorer-vscode/issues/3324) +- Exposed read and write access to local storage keys for Zowe Explorer extenders. [#3180](https://github.com/zowe/zowe-explorer-vscode/issues/3180) ### Bug fixes - Fixed an issue during initialization where the error dialog shown for a broken team configuration file was missing the "Show Config" action. [#3322](https://github.com/zowe/zowe-explorer-vscode/pull/3322) - Fixed an issue where editing a team config file or updating secrets in the OS credential vault could trigger multiple events for a single action. [#3296](https://github.com/zowe/zowe-explorer-vscode/pull/3296) - Fixed an issue where opening a PDS member after renaming an expanded PDS resulted in an error. [#3314](https://github.com/zowe/zowe-explorer-vscode/issues/3314) +- Fixed issue where persistent settings defined at the workspace level were migrated into global storage rather than workspace-specific storage. [#3180](https://github.com/zowe/zowe-explorer-vscode/issues/3180) ## `3.0.3` 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 c9f4909705..2867def4a4 100644 --- a/packages/zowe-explorer/__tests__/__unit__/commands/MvsCommandHandler.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/commands/MvsCommandHandler.unit.test.ts @@ -55,7 +55,7 @@ describe("mvsCommandActions unit testing", () => { }; }), }); - Object.defineProperty(ZoweLocalStorage, "storage", { + Object.defineProperty(ZoweLocalStorage, "globalState", { value: { get: () => ({ persistence: true, favorites: [], history: [], sessions: ["zosmf"], searchHistory: [], fileHistory: [] }), update: jest.fn(), 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 43b0ea17a9..bca89468fd 100644 --- a/packages/zowe-explorer/__tests__/__unit__/commands/TsoCommandHandler.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/commands/TsoCommandHandler.unit.test.ts @@ -54,7 +54,7 @@ describe("TsoCommandHandler unit testing", () => { }; }), }); - Object.defineProperty(ZoweLocalStorage, "storage", { + Object.defineProperty(ZoweLocalStorage, "globalState", { value: { get: () => ({ persistence: true, favorites: [], history: [], sessions: ["zosmf"], searchHistory: [], fileHistory: [] }), update: jest.fn(), 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 1f0e43a500..1eddd873be 100644 --- a/packages/zowe-explorer/__tests__/__unit__/commands/UnixCommandHandler.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/commands/UnixCommandHandler.unit.test.ts @@ -59,7 +59,7 @@ describe("UnixCommand Actions Unit Testing", () => { }), }); - Object.defineProperty(ZoweLocalStorage, "storage", { + Object.defineProperty(ZoweLocalStorage, "globalState", { value: { get: () => ({ persistence: true, favorites: [], history: [], sessions: ["zosmf"], searchHistory: [], fileHistory: [] }), update: jest.fn(), 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 47879ffa10..8128ac7fd8 100644 --- a/packages/zowe-explorer/__tests__/__unit__/configuration/Profiles.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/configuration/Profiles.unit.test.ts @@ -144,7 +144,7 @@ function createGlobalMocks(): { [key: string]: any } { configurable: true, }); - Object.defineProperty(ZoweLocalStorage, "storage", { + Object.defineProperty(ZoweLocalStorage, "globalState", { value: { get: jest.fn(() => ({ persistence: true })), update: jest.fn(), diff --git a/packages/zowe-explorer/__tests__/__unit__/configuration/SettingsConfig.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/configuration/SettingsConfig.unit.test.ts index 55acf4ca17..8a05be1271 100644 --- a/packages/zowe-explorer/__tests__/__unit__/configuration/SettingsConfig.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/configuration/SettingsConfig.unit.test.ts @@ -18,7 +18,7 @@ import { SettingsConfig } from "../../../src/configuration/SettingsConfig"; describe("SettingsConfig Unit Tests", () => { beforeEach(() => { Object.defineProperty(ZoweLogger, "trace", { value: jest.fn(), configurable: true }); - Object.defineProperty(ZoweLocalStorage, "storage", { + Object.defineProperty(ZoweLocalStorage, "globalState", { value: { get: () => ({ persistence: true, @@ -209,4 +209,58 @@ describe("SettingsConfig Unit Tests", () => { expect(migrateToLocalStorageSpy).toHaveBeenCalledTimes(1); }); }); + + describe("function migrateSettingsAtLevel", () => { + function getBlockMocks() { + const configurationsMock = jest.spyOn(SettingsConfig as any, "configurations", "get"); + const setDirectValueMock = jest.spyOn(SettingsConfig, "setDirectValue").mockImplementation(); + const setValueMock = jest.spyOn(ZoweLocalStorage, "setValue").mockImplementation(); + jest.spyOn(SettingsConfig, "setMigratedDsTemplates").mockImplementation(); + + return { + configurationsMock, + setDirectValueMock, + setValueMock, + }; + } + + it("migrates workspace-level settings from settings config", async () => { + const blockMocks = getBlockMocks(); + blockMocks.configurationsMock.mockReturnValue({ + inspect: () => ({ + globalValue: undefined, + workspaceValue: 123, + }), + }); + await (SettingsConfig as any).migrateSettingsAtLevel(vscode.ConfigurationTarget.Workspace); + for (const [_, value, setInWorkspace] of blockMocks.setValueMock.mock.calls) { + expect(value).toBe(123); + expect(setInWorkspace).toBe(true); + } + for (const [_, value, target] of blockMocks.setDirectValueMock.mock.calls) { + expect(value).toEqual(undefined); + expect(target).toBe(vscode.ConfigurationTarget.Workspace); + } + }); + + it("migrates global-level settings from settings config", async () => { + const blockMocks = getBlockMocks(); + blockMocks.configurationsMock.mockReturnValue({ + inspect: () => ({ + globalValue: 123, + workspaceValue: undefined, + }), + }); + await (SettingsConfig as any).migrateSettingsAtLevel(vscode.ConfigurationTarget.Global); + + for (const [_, value, setInWorkspace] of blockMocks.setValueMock.mock.calls) { + expect(value).toBe(123); + expect(setInWorkspace).toBe(false); + } + for (const [_, value, target] of blockMocks.setDirectValueMock.mock.calls) { + expect(value).toEqual(undefined); + expect(target).toBe(vscode.ConfigurationTarget.Global); + } + }); + }); }); 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 6800b11f5f..07246d1218 100644 --- a/packages/zowe-explorer/__tests__/__unit__/extending/ZoweExplorerExtender.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/extending/ZoweExplorerExtender.unit.test.ts @@ -19,7 +19,7 @@ import { createUSSSessionNode, createUSSTree } from "../../__mocks__/mockCreator import { createJobsTree, createIJobObject } from "../../__mocks__/mockCreators/jobs"; import { SettingsConfig } from "../../../src/configuration/SettingsConfig"; import { ZoweExplorerExtender } from "../../../src/extending/ZoweExplorerExtender"; -import { ZoweLocalStorage } from "../../../src/tools/ZoweLocalStorage"; +import { LocalStorageAccess, ZoweLocalStorage } from "../../../src/tools/ZoweLocalStorage"; import { ZoweLogger } from "../../../src/tools/ZoweLogger"; import { UssFSProvider } from "../../../src/trees/uss/UssFSProvider"; import { ProfilesUtils } from "../../../src/utils/ProfilesUtils"; @@ -71,7 +71,7 @@ describe("ZoweExplorerExtender unit tests", () => { value: newMocks.mockTextDocument, configurable: true, }); - Object.defineProperty(ZoweLocalStorage, "storage", { + Object.defineProperty(ZoweLocalStorage, "globalState", { value: { get: () => ({ persistence: true, favorites: [], history: [], sessions: ["zosmf"], searchHistory: [], fileHistory: [] }), update: jest.fn(), @@ -324,4 +324,11 @@ describe("ZoweExplorerExtender unit tests", () => { expect(blockMocks.instTest.getErrorCorrelator()).toBe(ErrorCorrelator.getInstance()); }); }); + + describe("getLocalStorage", () => { + it("returns the singleton instance of LocalStorageAccess", () => { + const blockMocks = createBlockMocks(); + expect(blockMocks.instTest.getLocalStorage()).toBe(LocalStorageAccess); + }); + }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts index 348b75da69..b593df5c69 100644 --- a/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts @@ -388,7 +388,7 @@ async function createGlobalMocks() { value: jest.fn(), configurable: true, }); - Object.defineProperty(ZoweLocalStorage, "storage", { + Object.defineProperty(ZoweLocalStorage, "globalState", { value: { get: () => ({ persistence: true, favorites: [], history: [], sessions: ["zosmf"], searchHistory: [], fileHistory: [] }), update: jest.fn(), diff --git a/packages/zowe-explorer/__tests__/__unit__/icons/Icon.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/icons/Icon.unit.test.ts index f0f30b66a9..affff18c1b 100644 --- a/packages/zowe-explorer/__tests__/__unit__/icons/Icon.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/icons/Icon.unit.test.ts @@ -23,7 +23,7 @@ describe("Checking icon generator's basics", () => { const createTreeView = jest.fn().mockReturnValue({ onDidCollapseElement: jest.fn() }); Object.defineProperty(vscode.window, "createTreeView", { value: createTreeView }); - Object.defineProperty(ZoweLocalStorage, "storage", { + Object.defineProperty(ZoweLocalStorage, "globalState", { value: { get: () => ({ persistence: true, favorites: [], history: [], sessions: ["zosmf"], searchHistory: [], fileHistory: [] }), update: jest.fn(), diff --git a/packages/zowe-explorer/__tests__/__unit__/tools/ZoweLocalStorage.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/tools/ZoweLocalStorage.unit.test.ts index 038d7f31e6..b1352ab407 100644 --- a/packages/zowe-explorer/__tests__/__unit__/tools/ZoweLocalStorage.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/tools/ZoweLocalStorage.unit.test.ts @@ -10,14 +10,15 @@ */ import * as vscode from "vscode"; -import { ZoweLocalStorage } from "../../../src/tools/ZoweLocalStorage"; +import { LocalStorageAccess, StorageKeys, ZoweLocalStorage } from "../../../src/tools/ZoweLocalStorage"; import { PersistenceSchemaEnum } from "@zowe/zowe-explorer-api"; +import { Definitions } from "../../../src/configuration/Definitions"; describe("ZoweLocalStorage Unit Tests", () => { it("should initialize successfully", () => { const mockGlobalState = { get: jest.fn(), update: jest.fn(), keys: () => [] } as vscode.Memento; ZoweLocalStorage.initializeZoweLocalStorage(mockGlobalState); - expect((ZoweLocalStorage as any).storage).toEqual(mockGlobalState); + expect((ZoweLocalStorage as any).globalState).toEqual(mockGlobalState); }); it("should get and set values successfully", () => { @@ -31,4 +32,102 @@ describe("ZoweLocalStorage Unit Tests", () => { ZoweLocalStorage.setValue("fruit" as PersistenceSchemaEnum, "banana"); expect(ZoweLocalStorage.getValue("fruit" as PersistenceSchemaEnum)).toEqual("banana"); }); + + it("should get workspace values with no default and fallback to global if not found", () => { + const globalStorage = {}; + const workspaceStorage = {}; + const mockGlobalState = { + get: jest.fn().mockImplementation((key, defaultValue) => globalStorage[key] ?? defaultValue), + update: jest.fn().mockImplementation((key, value) => (globalStorage[key] = value)), + keys: () => [], + }; + const mockWorkspaceState = { + get: jest.fn().mockImplementation((key, defaultValue) => workspaceStorage[key] ?? defaultValue), + update: jest.fn().mockImplementation((key, value) => (workspaceStorage[key] = value)), + keys: () => [], + }; + ZoweLocalStorage.initializeZoweLocalStorage(mockGlobalState, mockWorkspaceState); + // add value into local storage + ZoweLocalStorage.setValue("fruit" as PersistenceSchemaEnum, "banana"); + + // assert that it can still be retrieved from global storage + expect(ZoweLocalStorage.getValue("fruit" as PersistenceSchemaEnum)).toEqual("banana"); + + // workspace state access should have default value of undefined + // covers `ZoweLocalStorage.workspaceState?.get(key, undefined) ?? ZoweLocalStorage.globalState.get(key, defaultValue);` + expect(mockWorkspaceState.get).toHaveBeenCalledWith("fruit" as PersistenceSchemaEnum, undefined); + // expect global state to be accessed since key in workspace state was undefined + expect(mockGlobalState.get).toHaveBeenCalledWith("fruit" as PersistenceSchemaEnum, undefined); + }); + + it("should set workspace values successfully when setInWorkspace is true", () => { + const globalState = { get: jest.fn(), update: jest.fn(), keys: () => [] } as vscode.Memento; + const workspaceState = { get: jest.fn(), update: jest.fn(), keys: () => [] } as vscode.Memento; + ZoweLocalStorage.initializeZoweLocalStorage(globalState, workspaceState); + ZoweLocalStorage.setValue("fruit" as PersistenceSchemaEnum, "banana", true); + expect(workspaceState.update).toHaveBeenCalled(); + expect(globalState.update).not.toHaveBeenCalled(); + }); +}); + +describe("LocalStorageAccess", () => { + // Read: 1, Write: 2, Read | Write: 3 + function omitKeysWithPermission(permBits: number): StorageKeys[] { + return Object.values({ ...Definitions.LocalStorageKey, ...PersistenceSchemaEnum }).filter( + (k) => !(((LocalStorageAccess as any).accessControl[k] & permBits) > 0) + ); + } + function keysWithPerm(permBits: number): StorageKeys[] { + return Object.values({ ...Definitions.LocalStorageKey, ...PersistenceSchemaEnum }).filter( + (k) => (LocalStorageAccess as any).accessControl[k] === permBits + ); + } + + describe("getReadableKeys", () => { + it("returns a list of readable keys to the user", () => { + expect(LocalStorageAccess.getReadableKeys()).toStrictEqual([...keysWithPerm(1), ...keysWithPerm(3)]); + }); + }); + + describe("getWritableKeys", () => { + it("returns a list of writable keys to the user", () => { + expect(LocalStorageAccess.getWritableKeys()).toStrictEqual([...keysWithPerm(2), ...keysWithPerm(3)]); + }); + }); + + describe("getValue", () => { + it("calls ZoweLocalStorage.getValue for all readable keys", () => { + const getValueMock = jest.spyOn(ZoweLocalStorage, "getValue").mockReturnValue(123); + for (const key of keysWithPerm(1)) { + expect(LocalStorageAccess.getValue(key)).toBe(123); + expect(getValueMock).toHaveBeenCalledWith(key); + } + }); + + it("throws error for all keys that are not readable", () => { + for (const key of omitKeysWithPermission(1)) { + expect(() => LocalStorageAccess.getValue(key)).toThrow(`Insufficient read permissions for ${key as string} in local storage.`); + } + }); + }); + + describe("setValue", () => { + it("calls ZoweLocalStorage.setValue for all writable keys", async () => { + const setValueMock = jest.spyOn(ZoweLocalStorage, "setValue").mockImplementation(); + const expectWritableSpy = jest.spyOn(LocalStorageAccess as any, "expectWritable"); + for (const key of keysWithPerm(2)) { + await LocalStorageAccess.setValue(key, 123); + expect(setValueMock).toHaveBeenCalledWith(key, 123); + expect(expectWritableSpy).not.toThrow(); + } + }); + + it("throws error for all keys that are not writable", () => { + for (const key of omitKeysWithPermission(2)) { + expect(() => LocalStorageAccess.setValue(key, undefined)).toThrow( + `Insufficient write permissions for ${key as string} in local storage.` + ); + } + }); + }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/tools/ZowePersistentFilters.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/tools/ZowePersistentFilters.unit.test.ts index b46746eb0d..d54afbf354 100644 --- a/packages/zowe-explorer/__tests__/__unit__/tools/ZowePersistentFilters.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/tools/ZowePersistentFilters.unit.test.ts @@ -16,7 +16,7 @@ import { ZowePersistentFilters } from "../../../src/tools/ZowePersistentFilters" describe("PersistentFilters Unit Test", () => { Object.defineProperty(ZoweLogger, "trace", { value: jest.fn(), configurable: true }); - Object.defineProperty(ZoweLocalStorage, "storage", { + Object.defineProperty(ZoweLocalStorage, "globalState", { value: { get: () => ({ persistence: true, diff --git a/packages/zowe-explorer/__tests__/__unit__/tools/ZoweSaveQueue.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/tools/ZoweSaveQueue.unit.test.ts index 53669531f9..cc88c50503 100644 --- a/packages/zowe-explorer/__tests__/__unit__/tools/ZoweSaveQueue.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/tools/ZoweSaveQueue.unit.test.ts @@ -20,7 +20,7 @@ jest.mock("../../../src/tools/ZoweLogger"); describe("ZoweSaveQueue - unit tests", () => { const createGlobalMocks = () => { - Object.defineProperty(ZoweLocalStorage, "storage", { + Object.defineProperty(ZoweLocalStorage, "globalState", { value: { get: () => ({ persistence: true, favorites: [], history: [], sessions: ["zosmf"], searchHistory: [], fileHistory: [] }), update: jest.fn(), diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/ZoweTreeProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/ZoweTreeProvider.unit.test.ts index 49972a9e6d..c846772137 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/ZoweTreeProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/ZoweTreeProvider.unit.test.ts @@ -39,7 +39,7 @@ import { AuthUtils } from "../../../src/utils/AuthUtils"; import { IconGenerator } from "../../../src/icons/IconGenerator"; async function createGlobalMocks() { - Object.defineProperty(ZoweLocalStorage, "storage", { + Object.defineProperty(ZoweLocalStorage, "globalState", { value: { get: () => ({ persistence: true, favorites: [], history: [], sessions: ["zosmf"], searchHistory: [], fileHistory: [] }), update: jest.fn(), diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetTree.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetTree.unit.test.ts index a2dd637fab..6bb69c649b 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetTree.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetTree.unit.test.ts @@ -70,7 +70,7 @@ function createGlobalMocks() { globalMocks.mockProfileInstance = createInstanceOfProfile(globalMocks.testProfileLoaded); - Object.defineProperty(ZoweLocalStorage, "storage", { + Object.defineProperty(ZoweLocalStorage, "globalState", { value: { get: () => ({ persistence: true, favorites: [], history: [], sessions: ["zosmf"], searchHistory: [], fileHistory: [] }), update: jest.fn(), 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 914a227dcd..973fd49613 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 @@ -123,7 +123,7 @@ function createGlobalMocks() { Object.defineProperty(ZoweLogger, "debug", { value: jest.fn(), configurable: true }); Object.defineProperty(ZoweLogger, "trace", { value: jest.fn(), configurable: true }); Object.defineProperty(vscode.window, "showInformationMessage", { value: jest.fn(), configurable: true }); - Object.defineProperty(ZoweLocalStorage, "storage", { + Object.defineProperty(ZoweLocalStorage, "globalState", { value: { get: () => ({ persistence: true, favorites: [], history: [], sessions: ["zosmf"], searchHistory: [], fileHistory: [] }), update: jest.fn(), diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/job/JobInit.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/job/JobInit.unit.test.ts index c3e8a754ac..f89bb1804e 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/job/JobInit.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/job/JobInit.unit.test.ts @@ -158,7 +158,7 @@ describe("Test src/jobs/extension", () => { Object.defineProperty(vscode.workspace, "onDidChangeConfiguration", { value: onDidChangeConfiguration }); Object.defineProperty(vscode.window, "showWarningMessage", { value: onDidChangeConfiguration }); Object.defineProperty(ZoweLogger, "trace", { value: jest.fn(), configurable: true }); - Object.defineProperty(ZoweLocalStorage, "storage", { + Object.defineProperty(ZoweLocalStorage, "globalState", { value: { get: () => ({ persistence: true, favorites: [], history: [], sessions: ["zosmf"], searchHistory: [], fileHistory: [] }), update: jest.fn(), diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/job/JobTree.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/job/JobTree.unit.test.ts index 66b0b8f557..319f70310d 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/job/JobTree.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/job/JobTree.unit.test.ts @@ -218,7 +218,7 @@ async function createGlobalMocks() { globalMocks.mockGetJesApi.mockReturnValue(globalMocks.jesApi); ZoweExplorerApiRegister.getJesApi = globalMocks.mockGetJesApi.bind(ZoweExplorerApiRegister); - Object.defineProperty(ZoweLocalStorage, "storage", { + Object.defineProperty(ZoweLocalStorage, "globalState", { value: { get: () => ({ persistence: true, favorites: [], history: [], sessions: ["zosmf"], searchHistory: [], fileHistory: [] }), update: jest.fn(), diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/job/ZoweJobNode.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/job/ZoweJobNode.unit.test.ts index 71b039b6fb..96fae712cb 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/job/ZoweJobNode.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/job/ZoweJobNode.unit.test.ts @@ -148,7 +148,7 @@ async function createGlobalMocks() { value: jest.fn().mockImplementationOnce(() => Promise.resolve()), configurable: true, }); - Object.defineProperty(ZoweLocalStorage, "storage", { + Object.defineProperty(ZoweLocalStorage, "globalState", { value: { get: () => ({ persistence: true, favorites: [], history: [], sessions: ["zosmf"], searchHistory: [], fileHistory: [] }), update: jest.fn(), diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/shared/SharedActions.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/shared/SharedActions.unit.test.ts index e7a5117e45..da75165b94 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/shared/SharedActions.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/shared/SharedActions.unit.test.ts @@ -115,7 +115,7 @@ function createGlobalMocks() { Object.defineProperty(ZoweLogger, "warn", { value: jest.fn(), configurable: true }); Object.defineProperty(ZoweLogger, "info", { value: jest.fn(), configurable: true }); Object.defineProperty(ZoweLogger, "trace", { value: jest.fn(), configurable: true }); - Object.defineProperty(ZoweLocalStorage, "storage", { + Object.defineProperty(ZoweLocalStorage, "globalState", { value: { get: () => ({ persistence: true, favorites: [], history: [], sessions: ["zosmf"], searchHistory: [], fileHistory: [] }), update: jest.fn(), diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/shared/SharedHistoryView.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/shared/SharedHistoryView.unit.test.ts index 39784dc520..7c66eb7aa4 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/shared/SharedHistoryView.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/shared/SharedHistoryView.unit.test.ts @@ -61,7 +61,7 @@ function createGlobalMocks(): any { configurable: true, }); Object.defineProperty(vscode.window, "createTreeView", { value: jest.fn().mockReturnValue(globalMocks.treeView), configurable: true }); - Object.defineProperty(ZoweLocalStorage, "storage", { + Object.defineProperty(ZoweLocalStorage, "globalState", { value: { get: () => ({ persistence: true, favorites: [], history: [], sessions: ["zosmf"], searchHistory: [], fileHistory: [] }), update: jest.fn(), diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSActions.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSActions.unit.test.ts index 1a4ee05bbe..9ea8964048 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSActions.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSActions.unit.test.ts @@ -162,7 +162,7 @@ function createGlobalMocks() { }; }), }); - Object.defineProperty(ZoweLocalStorage, "storage", { + Object.defineProperty(ZoweLocalStorage, "globalState", { value: { get: () => ({ persistence: true, favorites: [], history: [], sessions: ["zosmf"], searchHistory: [], fileHistory: [] }), update: jest.fn(), diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSTree.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSTree.unit.test.ts index c1bff5f4c9..8e5990c225 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSTree.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSTree.unit.test.ts @@ -175,7 +175,7 @@ function createGlobalMocks() { value: jest.fn().mockReturnValue(globalMocks.mockProfilesInstance), configurable: true, }); - Object.defineProperty(ZoweLocalStorage, "storage", { + Object.defineProperty(ZoweLocalStorage, "globalState", { value: { get: () => ({ persistence: true, 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 d5fc66cf70..5a6da76892 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 @@ -159,7 +159,7 @@ function createGlobalMocks() { value: globalMocks.fileExistsCaseSensitveSync, configurable: true, }); - Object.defineProperty(ZoweLocalStorage, "storage", { + Object.defineProperty(ZoweLocalStorage, "globalState", { value: { get: () => ({ persistence: true, favorites: [], history: [], sessions: ["zosmf"], searchHistory: [], fileHistory: [] }), update: jest.fn(), diff --git a/packages/zowe-explorer/__tests__/__unit__/utils/LoggerUtils.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/utils/LoggerUtils.unit.test.ts index b070db36c4..3c5d5a083f 100644 --- a/packages/zowe-explorer/__tests__/__unit__/utils/LoggerUtils.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/utils/LoggerUtils.unit.test.ts @@ -46,7 +46,7 @@ function createGlobalMocks(): { [key: string]: any } { }); Object.defineProperty(Gui, "infoMessage", { value: jest.fn(), configurable: true }); jest.spyOn(vscode.workspace, "getConfiguration").mockImplementationOnce(newMocks.mockGetConfiguration); - Object.defineProperty(ZoweLocalStorage, "storage", { + Object.defineProperty(ZoweLocalStorage, "globalState", { value: { get: () => ({ persistence: true, favorites: [], history: [], sessions: ["zosmf"], searchHistory: [], fileHistory: [] }), update: jest.fn(), diff --git a/packages/zowe-explorer/__tests__/__unit__/utils/TreeViewUtils.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/utils/TreeViewUtils.unit.test.ts index 01707e29fa..be8130c5d9 100644 --- a/packages/zowe-explorer/__tests__/__unit__/utils/TreeViewUtils.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/utils/TreeViewUtils.unit.test.ts @@ -34,7 +34,7 @@ describe("TreeViewUtils Unit Tests", () => { }); newMocks.testDatasetTree = createDatasetTree(newMocks.datasetSessionNode, newMocks.treeView); newMocks.testDatasetTree.addFileHistory("[profile1]: TEST.NODE"); - Object.defineProperty(ZoweLocalStorage, "storage", { + Object.defineProperty(ZoweLocalStorage, "globalState", { value: createPersistentConfig(), configurable: true, }); diff --git a/packages/zowe-explorer/src/configuration/SettingsConfig.ts b/packages/zowe-explorer/src/configuration/SettingsConfig.ts index a85350fde1..1380139fcd 100644 --- a/packages/zowe-explorer/src/configuration/SettingsConfig.ts +++ b/packages/zowe-explorer/src/configuration/SettingsConfig.ts @@ -14,7 +14,6 @@ import { Gui, PersistenceSchemaEnum, ZoweVsCodeExtension } from "@zowe/zowe-expl import { Constants } from "./Constants"; import { ZoweLocalStorage } from "../tools/ZoweLocalStorage"; import { Definitions } from "./Definitions"; -import { ZoweLogger } from "../tools/ZoweLogger"; export class SettingsConfig { /** @@ -185,33 +184,52 @@ export class SettingsConfig { } } + private static PERSISTENT_SETTINGS = [ + PersistenceSchemaEnum.Dataset, + PersistenceSchemaEnum.USS, + PersistenceSchemaEnum.Job, + PersistenceSchemaEnum.Commands, + Definitions.LocalStorageKey.CLI_LOGGER_SETTING_PRESENTED, + ]; + private static async migrateToLocalStorage(): Promise { // Migrate persistent settings to new LocalStorage solution - const persistentSettings = [ - PersistenceSchemaEnum.Dataset, - PersistenceSchemaEnum.USS, - PersistenceSchemaEnum.Job, - PersistenceSchemaEnum.Commands, - Definitions.LocalStorageKey.CLI_LOGGER_SETTING_PRESENTED, - ]; - const vscodePersistentSettings = persistentSettings.filter((setting) => { - return SettingsConfig.configurations.inspect(setting).globalValue; - }); - if (vscodePersistentSettings.length > 0) { - // eslint-disable-next-line @typescript-eslint/no-misused-promises - vscodePersistentSettings.forEach(async (setting) => { - ZoweLogger.debug(setting.toString()); - if (setting === PersistenceSchemaEnum.Dataset) { - await this.setMigratedDsTemplates(); - } - ZoweLocalStorage.setValue(setting, SettingsConfig.configurations.inspect(setting).globalValue); - SettingsConfig.setDirectValue(setting, undefined, vscode.ConfigurationTarget.Global); - }); + const globalSettingsMigrated = await SettingsConfig.migrateSettingsAtLevel(vscode.ConfigurationTarget.Global); + const workspaceSettingsMigrated = await SettingsConfig.migrateSettingsAtLevel(vscode.ConfigurationTarget.Workspace); + + if (globalSettingsMigrated || workspaceSettingsMigrated) { ZoweLocalStorage.setValue(Definitions.LocalStorageKey.SETTINGS_LOCAL_STORAGE_MIGRATED, true); await SettingsConfig.promptReload(); } } + /** + * Migrates settings from `settings.json` at the given level. + * @param level Global or workspace configuration target + * @returns Whether at least one setting was migrated to local storage for the given level + */ + private static async migrateSettingsAtLevel(level: vscode.ConfigurationTarget.Global | vscode.ConfigurationTarget.Workspace): Promise { + const isWorkspace = level === vscode.ConfigurationTarget.Workspace; + let valueMigrated = false; + for (const setting of SettingsConfig.PERSISTENT_SETTINGS) { + const settingInfo = SettingsConfig.configurations.inspect(setting); + const value = isWorkspace ? settingInfo.workspaceValue : settingInfo.globalValue; + if (value == null) { + continue; + } + + if (setting === PersistenceSchemaEnum.Dataset) { + await this.setMigratedDsTemplates(); + } + + valueMigrated ||= true; + await ZoweLocalStorage.setValue(setting, value, isWorkspace); + await SettingsConfig.setDirectValue(setting, undefined, level); + } + + return valueMigrated; + } + public static async setMigratedDsTemplates(): Promise { const settings: any = this.getDirectValue(PersistenceSchemaEnum.Dataset); for (const key in settings) { diff --git a/packages/zowe-explorer/src/extending/ZoweExplorerExtender.ts b/packages/zowe-explorer/src/extending/ZoweExplorerExtender.ts index 037b984335..583bcf8885 100644 --- a/packages/zowe-explorer/src/extending/ZoweExplorerExtender.ts +++ b/packages/zowe-explorer/src/extending/ZoweExplorerExtender.ts @@ -28,6 +28,8 @@ import { import { Constants } from "../configuration/Constants"; import { ProfilesUtils } from "../utils/ProfilesUtils"; import { ZoweLogger } from "../tools/ZoweLogger"; +import { LocalStorageAccess } from "../tools/ZoweLocalStorage"; +import { ILocalStorageAccess } from "@zowe/zowe-explorer-api/src/extend/ILocalStorageAccess"; /** * The Zowe Explorer API Register singleton that gets exposed to other VS Code @@ -221,6 +223,10 @@ export class ZoweExplorerExtender implements IApiExplorerExtender, IZoweExplorer } } + public getLocalStorage(): ILocalStorageAccess { + return LocalStorageAccess; + } + /** * This method can be used by other VS Code Extensions to access the primary profile. * diff --git a/packages/zowe-explorer/src/extension.ts b/packages/zowe-explorer/src/extension.ts index 8bc9d2b226..61a9afecf9 100644 --- a/packages/zowe-explorer/src/extension.ts +++ b/packages/zowe-explorer/src/extension.ts @@ -32,7 +32,7 @@ import { ProfilesUtils } from "./utils/ProfilesUtils"; * @returns {Promise} */ export async function activate(context: vscode.ExtensionContext): Promise { - ZoweLocalStorage.initializeZoweLocalStorage(context.globalState); + ZoweLocalStorage.initializeZoweLocalStorage(context.globalState, context.workspaceState); await SharedInit.initZoweLogger(context); await ProfilesUtils.initializeZoweProfiles((msg) => ZoweExplorerExtender.showZoweConfigError(msg)); diff --git a/packages/zowe-explorer/src/tools/ZoweLocalStorage.ts b/packages/zowe-explorer/src/tools/ZoweLocalStorage.ts index a6c774283b..74e159594a 100644 --- a/packages/zowe-explorer/src/tools/ZoweLocalStorage.ts +++ b/packages/zowe-explorer/src/tools/ZoweLocalStorage.ts @@ -12,23 +12,143 @@ import * as vscode from "vscode"; import * as meta from "../../package.json"; import { ZoweLogger } from "./ZoweLogger"; -import type { PersistenceSchemaEnum } from "@zowe/zowe-explorer-api"; -import type { Definitions } from "../configuration/Definitions"; +import { PersistenceSchemaEnum } from "@zowe/zowe-explorer-api"; +import { Definitions } from "../configuration/Definitions"; + +enum StorageAccessLevel { + None = 0, + Read = 1 << 0, + Write = 1 << 1, +} + +export type StorageKeys = Definitions.LocalStorageKey | PersistenceSchemaEnum; + +type LocalStorageACL = { + [key in StorageKeys]?: StorageAccessLevel; +}; export class ZoweLocalStorage { - private static storage: vscode.Memento; - public static initializeZoweLocalStorage(state: vscode.Memento): void { - ZoweLocalStorage.storage = state; + private static globalState: vscode.Memento; + private static workspaceState?: vscode.Memento; + + public static initializeZoweLocalStorage(globalState: vscode.Memento, workspaceState?: vscode.Memento): void { + ZoweLocalStorage.globalState = globalState; + ZoweLocalStorage.workspaceState = workspaceState; } - public static getValue(key: Definitions.LocalStorageKey | PersistenceSchemaEnum): T { - ZoweLogger.trace("ZoweLocalStorage.getValue called."); + public static getValue(key: keyof LocalStorageACL): T { + ZoweLogger.trace("ZoweLocalStorage.getValue called"); const defaultValue = meta.contributes.configuration.properties[key]?.default; - return ZoweLocalStorage.storage.get(key, defaultValue); + return ZoweLocalStorage.workspaceState?.get(key, undefined) ?? ZoweLocalStorage.globalState.get(key, defaultValue); } - public static setValue(key: Definitions.LocalStorageKey | PersistenceSchemaEnum, value: T): Thenable { + public static setValue(key: keyof LocalStorageACL, value: T, setInWorkspace?: boolean): Thenable { ZoweLogger.trace("ZoweLocalStorage.setValue called."); - return ZoweLocalStorage.storage.update(key, value); + return setInWorkspace && ZoweLocalStorage.workspaceState + ? ZoweLocalStorage.workspaceState.update(key, value) + : ZoweLocalStorage.globalState.update(key, value); + } +} + +/** + * @brief + * + * External-facing, local storage access facility that controls what keys can be read from or written to. + * + * @details + * - Access control rules are defined using the bit-flags specified in the {@link StorageAccessLevel} enum. + * - Define new local storage keys in the access control list to expose read or write access to extenders. + */ +export class LocalStorageAccess extends ZoweLocalStorage { + private static accessControl: LocalStorageACL = { + [Definitions.LocalStorageKey.CLI_LOGGER_SETTING_PRESENTED]: StorageAccessLevel.Read, + [Definitions.LocalStorageKey.SETTINGS_LOCAL_STORAGE_MIGRATED]: StorageAccessLevel.Read, + [Definitions.LocalStorageKey.SETTINGS_OLD_SETTINGS_MIGRATED]: StorageAccessLevel.Read, + [Definitions.LocalStorageKey.ENCODING_HISTORY]: StorageAccessLevel.Read | StorageAccessLevel.Write, + [PersistenceSchemaEnum.Dataset]: StorageAccessLevel.Read | StorageAccessLevel.Write, + [PersistenceSchemaEnum.USS]: StorageAccessLevel.Read | StorageAccessLevel.Write, + [PersistenceSchemaEnum.Job]: StorageAccessLevel.Read | StorageAccessLevel.Write, + [PersistenceSchemaEnum.Commands]: StorageAccessLevel.Read | StorageAccessLevel.Write, + [Definitions.LocalStorageKey.V1_MIGRATION_STATUS]: StorageAccessLevel.None, + }; + + /** + * Asserts that the given key is readable from local storage. + * @param key The key to read data from in local storage + * @throws If the key is not readable from the access facility + */ + private static expectReadable(key: keyof LocalStorageACL): void { + if ((LocalStorageAccess.accessControl[key] & StorageAccessLevel.Read) > 0) { + return; + } + + throw new Error( + vscode.l10n.t({ + message: "Insufficient read permissions for {0} in local storage.", + args: [key], + comment: "Local storage key", + }) + ); + } + + /** + * Asserts that the given key is writable from local storage. + * @param key The key to write data to in local storage + * @throws If the key is not writable from the access facility + */ + private static expectWritable(key: keyof LocalStorageACL): void { + if ((LocalStorageAccess.accessControl[key] & StorageAccessLevel.Write) > 0) { + return; + } + + throw new Error( + vscode.l10n.t({ + message: "Insufficient write permissions for {0} in local storage.", + args: [key], + comment: "Local storage key", + }) + ); + } + + /** + * @returns The list of readable keys from the access facility + */ + public static getReadableKeys(): StorageKeys[] { + return Object.keys(LocalStorageAccess.accessControl).filter( + (k) => LocalStorageAccess.accessControl[k] & StorageAccessLevel.Read + ) as StorageKeys[]; + } + + /** + * @returns The list of writable keys from the access facility + */ + public static getWritableKeys(): StorageKeys[] { + return Object.keys(LocalStorageAccess.accessControl).filter( + (k) => LocalStorageAccess.accessControl[k] & StorageAccessLevel.Write + ) as StorageKeys[]; + } + + /** + * Retrieve the value from local storage for the given key. + * @param key A readable key + * @returns The value if it exists in local storage, or `undefined` otherwise + * @throws If the extender does not have appropriate read permissions for the given key + */ + public static getValue(key: keyof LocalStorageACL): T { + ZoweLogger.trace(`LocalStorageAccess.getValue called with key ${key}.`); + LocalStorageAccess.expectReadable(key); + return ZoweLocalStorage.getValue(key); + } + + /** + * Set a value in local storage for the given key. + * @param key A writable key + * @param value The new value for the given key to set in local storage + * @throws If the extender does not have appropriate write permissions for the given key + */ + public static setValue(key: keyof LocalStorageACL, value: T): Thenable { + ZoweLogger.trace(`LocalStorageAccess.setValue called with key ${key}.`); + LocalStorageAccess.expectWritable(key); + return ZoweLocalStorage.setValue(key, value); } }