diff --git a/packages/zowe-explorer-api/CHANGELOG.md b/packages/zowe-explorer-api/CHANGELOG.md index c8fc4db2b2..7dc201454d 100644 --- a/packages/zowe-explorer-api/CHANGELOG.md +++ b/packages/zowe-explorer-api/CHANGELOG.md @@ -8,6 +8,9 @@ All notable changes to the "zowe-explorer-api" extension will be documented in t - Added optional `getTag` function to `ZoweExplorerAPI.IUss` for getting the tag of a file on USS. - Added new API {ZE Extender MetaData} to allow extenders to have the metadata of registered extenders to aid in team configuration file creation from a view that isn't Zowe Explorer's. [#2394](https://github.com/zowe/vscode-extension-for-zowe/issues/2394) +- Add `sort` and `filter` optional variables for storing sort/filter options alongside tree nodes. [#2420](https://github.com/zowe/vscode-extension-for-zowe/issues/2420) +- Add `stats` optional variable for storing dataset stats (such as user, modified date, etc.) +- Add option enums and types for sorting, filtering and sort direction in tree nodes. [#2420](https://github.com/zowe/vscode-extension-for-zowe/issues/2420) ### Bug fixes diff --git a/packages/zowe-explorer-api/src/index.ts b/packages/zowe-explorer-api/src/index.ts index 33d3dad8c4..a9e308a3f0 100644 --- a/packages/zowe-explorer-api/src/index.ts +++ b/packages/zowe-explorer-api/src/index.ts @@ -23,6 +23,7 @@ export * from "./tree/ZoweExplorerTreeApi"; export * from "./tree/ZoweTreeNode"; export * from "./tree/IZoweTree"; export * from "./tree/IZoweTreeNode"; +export * from "./tree/sorting"; export * from "./utils"; export * from "./vscode/ZoweVsCodeExtension"; export * from "./vscode/ui"; diff --git a/packages/zowe-explorer-api/src/tree/IZoweTreeNode.ts b/packages/zowe-explorer-api/src/tree/IZoweTreeNode.ts index 7bd2349f31..4f731b8c14 100644 --- a/packages/zowe-explorer-api/src/tree/IZoweTreeNode.ts +++ b/packages/zowe-explorer-api/src/tree/IZoweTreeNode.ts @@ -13,6 +13,7 @@ import * as vscode from "vscode"; import { IJob, imperative } from "@zowe/cli"; import { IZoweTree } from "./IZoweTree"; import { FileAttributes } from "../utils/files"; +import { DatasetFilter, NodeSort } from "./sorting"; export type IZoweNodeType = IZoweDatasetTreeNode | IZoweUSSTreeNode | IZoweJobTreeNode; @@ -78,6 +79,10 @@ export interface IZoweTreeNode { * whether the node was double-clicked */ wasDoubleClicked?: boolean; + /** + * Sorting method for this node's children + */ + sort?: NodeSort; /** * Retrieves the node label */ @@ -120,6 +125,12 @@ export interface IZoweTreeNode { setSessionToChoice(sessionObj: imperative.Session): void; } +export type DatasetStats = { + user: string; + // built from "m4date", "mtime" and "msec" variables from z/OSMF API response + modifiedDate: Date; +}; + /** * Extended interface for Zowe Dataset tree nodes. * @@ -135,6 +146,14 @@ export interface IZoweDatasetTreeNode extends IZoweTreeNode { * Search criteria for a Dataset member search */ memberPattern?: string; + /** + * Additional statistics about this data set + */ + stats?: Partial; + /** + * Filter method for this data set's children + */ + filter?: DatasetFilter; /** * Retrieves child nodes of this IZoweDatasetTreeNode * diff --git a/packages/zowe-explorer-api/src/tree/index.ts b/packages/zowe-explorer-api/src/tree/index.ts index 420c04ac45..23d5dd01bc 100644 --- a/packages/zowe-explorer-api/src/tree/index.ts +++ b/packages/zowe-explorer-api/src/tree/index.ts @@ -9,6 +9,7 @@ * */ +export * from "./sorting"; export * from "./ZoweExplorerTreeApi"; export * from "./ZoweTreeNode"; export * from "./IZoweTree"; diff --git a/packages/zowe-explorer-api/src/tree/sorting.ts b/packages/zowe-explorer-api/src/tree/sorting.ts new file mode 100644 index 0000000000..cefcf68eb0 --- /dev/null +++ b/packages/zowe-explorer-api/src/tree/sorting.ts @@ -0,0 +1,43 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +export enum DatasetSortOpts { + Name, + LastModified, + UserId, +} + +export enum SortDirection { + Ascending, + Descending, +} + +export enum DatasetFilterOpts { + LastModified, + UserId, +} + +export type DatasetFilter = { + method: DatasetFilterOpts; + value: string; +}; + +export type NodeSort = { + method: DatasetSortOpts | JobSortOpts; + direction: SortDirection; +}; + +export enum JobSortOpts { + Id, + DateSubmitted, + Name, + ReturnCode, +} diff --git a/packages/zowe-explorer/CHANGELOG.md b/packages/zowe-explorer/CHANGELOG.md index b4815bcd01..88a92b1ea1 100644 --- a/packages/zowe-explorer/CHANGELOG.md +++ b/packages/zowe-explorer/CHANGELOG.md @@ -6,10 +6,12 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen ### New features and enhancements -- Added "Sort Jobs" feature for job nodes in Jobs tree view. [#2257](https://github.com/zowe/vscode-extension-for-zowe/issues/2251) -- Introduce a new user interface for managing profiles via right-click action "Manage Profile". +- Added "Sort Jobs" feature for job nodes in Jobs tree view: accessible via sort icon or right-clicking on session node. [#2257](https://github.com/zowe/vscode-extension-for-zowe/issues/2251) - Added new edit feature on `Edit Attributes` view for changing file tags on USS [#2113](https://github.com/zowe/vscode-extension-for-zowe/issues/2113) - Added new API {ZE Extender MetaData} to allow extenders to have the metadata of registered extenders to aid in team configuration file creation from a view that isn't Zowe Explorer's. [#2394](https://github.com/zowe/vscode-extension-for-zowe/issues/2394) +- Added "Sort PDS members" feature in Data Sets tree view: accessible via sort icon on session node, or by right-clicking a PDS or session. [#2420](https://github.com/zowe/vscode-extension-for-zowe/issues/2420) +- Added "Filter PDS members" feature in Data Sets tree view: accessible via filter icon on session node, or by right-clicking a PDS or session. [#2420](https://github.com/zowe/vscode-extension-for-zowe/issues/2420) +- Added "Local Filtering of Jobs Tree" feature for job nodes in Jobs tree view. [#2476](https://github.com/zowe/vscode-extension-for-zowe/issues/2476) ### Bug fixes diff --git a/packages/zowe-explorer/__tests__/__unit__/ZoweNode.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/ZoweNode.unit.test.ts index 3bc557fb9f..f52fb1af0a 100644 --- a/packages/zowe-explorer/__tests__/__unit__/ZoweNode.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/ZoweNode.unit.test.ts @@ -18,6 +18,7 @@ import { List, imperative } from "@zowe/cli"; import { Profiles } from "../../src/Profiles"; import * as globals from "../../src/globals"; import { ZoweLogger } from "../../src/utils/LoggerUtils"; +import { DatasetSortOpts, SortDirection } from "@zowe/zowe-explorer-api"; describe("Unit Tests (Jest)", () => { // Globals @@ -238,7 +239,6 @@ describe("Unit Tests (Jest)", () => { undefined, profileOne ); - infoChild.id = "root.Use the search button to display data sets"; rootNode.contextValue = globals.DS_SESSION_CONTEXT; rootNode.dirty = false; await expect(await rootNode.getChildren()).toEqual([infoChild]); @@ -259,7 +259,6 @@ describe("Unit Tests (Jest)", () => { undefined, profileOne ); - infoChild.id = "root.Use the search button to display data sets"; rootNode.contextValue = globals.DS_SESSION_CONTEXT; await expect(await rootNode.getChildren()).toEqual([infoChild]); }); @@ -356,11 +355,16 @@ describe("Unit Tests (Jest)", () => { }; }), }); + const sessionNode = { + getSessionNode: jest.fn(), + sort: { method: DatasetSortOpts.Name, direction: SortDirection.Ascending }, + } as unknown as ZoweDatasetNode; + const getSessionNodeSpy = jest.spyOn(ZoweDatasetNode.prototype, "getSessionNode").mockReturnValue(sessionNode); // Creating a rootNode const pds = new ZoweDatasetNode( "[root]: something", vscode.TreeItemCollapsibleState.Collapsed, - { getSessionNode: jest.fn() } as unknown as ZoweDatasetNode, + sessionNode, session, undefined, undefined, @@ -383,6 +387,7 @@ describe("Unit Tests (Jest)", () => { expect(pdsChildren[0].contextValue).toEqual(globals.DS_FILE_ERROR_CONTEXT); expect(pdsChildren[1].label).toEqual("GOODMEM1"); expect(pdsChildren[1].contextValue).toEqual(globals.DS_MEMBER_CONTEXT); + getSessionNodeSpy.mockRestore(); }); /************************************************************************************************************* diff --git a/packages/zowe-explorer/__tests__/__unit__/dataset/DatasetTree.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/dataset/DatasetTree.unit.test.ts index 9cb5357619..65cb06a4a7 100644 --- a/packages/zowe-explorer/__tests__/__unit__/dataset/DatasetTree.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/dataset/DatasetTree.unit.test.ts @@ -15,7 +15,7 @@ import * as fs from "fs"; import * as zowe from "@zowe/cli"; import { DatasetTree } from "../../../src/dataset/DatasetTree"; import { ZoweDatasetNode } from "../../../src/dataset/ZoweDatasetNode"; -import { Gui, IZoweDatasetTreeNode, ProfilesCache, ValidProfileEnum } from "@zowe/zowe-explorer-api"; +import { DatasetFilterOpts, Gui, IZoweDatasetTreeNode, ProfilesCache, ValidProfileEnum } from "@zowe/zowe-explorer-api"; import { ZoweExplorerApiRegister } from "../../../src/ZoweExplorerApiRegister"; import { Profiles } from "../../../src/Profiles"; import * as utils from "../../../src/utils/ProfilesUtils"; @@ -2696,3 +2696,205 @@ describe("Dataset Tree Unit Tests - Function initializeFavorites", () => { expect(() => testTree.initializeFavorites(log)).not.toThrow(); }); }); +describe("Dataset Tree Unit Tests - Sorting and Filtering operations", () => { + const tree = new DatasetTree(); + const nodesForSuite = (): Record => { + const session = new ZoweDatasetNode("testSession", vscode.TreeItemCollapsibleState.Collapsed, null, createISession()); + session.contextValue = globals.DS_SESSION_CONTEXT; + const pds = new ZoweDatasetNode("testPds", vscode.TreeItemCollapsibleState.Collapsed, session, createISession()); + pds.contextValue = globals.DS_PDS_CONTEXT; + + const nodeA = new ZoweDatasetNode("A", vscode.TreeItemCollapsibleState.Collapsed, pds, createISession()); + nodeA.stats = { user: "someUser", modifiedDate: new Date() }; + const nodeB = new ZoweDatasetNode("B", vscode.TreeItemCollapsibleState.Collapsed, pds, createISession()); + nodeB.stats = { user: "anotherUser", modifiedDate: new Date("2022-01-01T12:00:00") }; + const nodeC = new ZoweDatasetNode("C", vscode.TreeItemCollapsibleState.Collapsed, pds, createISession()); + nodeC.stats = { user: "someUser", modifiedDate: new Date("2022-03-15T16:30:00") }; + pds.children = [nodeA, nodeB, nodeC]; + session.children = [pds]; + + return { + session, + pds, + }; + }; + + const getBlockMocks = (): Record => ({ + nodeDataChanged: jest.spyOn(DatasetTree.prototype, "nodeDataChanged"), + refreshElement: jest.spyOn(DatasetTree.prototype, "refreshElement"), + showQuickPick: jest.spyOn(Gui, "showQuickPick"), + showInputBox: jest.spyOn(Gui, "showInputBox"), + }); + + afterEach(() => { + const mocks = getBlockMocks(); + for (const mock of Object.values(mocks)) { + mock.mockClear(); + } + }); + + afterAll(() => { + const mocks = getBlockMocks(); + for (const mock of Object.values(mocks)) { + mock.mockRestore(); + } + }); + + describe("sortBy & sortPdsMembersDialog", () => { + // for sorting, we shouldn't need to refresh since all nodes + // should be intact, just in a different order + it("does nothing if no children exist", async () => { + const mocks = getBlockMocks(); + const nodes = nodesForSuite(); + // case 1: called on PDS node + mocks.showQuickPick.mockResolvedValueOnce({ label: "$(case-sensitive) Name (default)" }); + nodes.pds.children = []; + await tree.sortPdsMembersDialog(nodes.pds); + expect(mocks.nodeDataChanged).not.toHaveBeenCalled(); + + // case 2: called on session node + mocks.showQuickPick.mockResolvedValueOnce({ label: "$(case-sensitive) Name (default)" }); + nodes.session.children = []; + await tree.sortPdsMembersDialog(nodes.session); + expect(mocks.nodeDataChanged).not.toHaveBeenCalled(); + }); + + it("sorts by name", async () => { + const mocks = getBlockMocks(); + const nodes = nodesForSuite(); + mocks.showQuickPick.mockResolvedValueOnce({ label: "$(case-sensitive) Name (default)" }); + await tree.sortPdsMembersDialog(nodes.pds); + expect(mocks.nodeDataChanged).toHaveBeenCalled(); + expect(mocks.refreshElement).not.toHaveBeenCalled(); + expect(nodes.pds.children?.map((c: IZoweDatasetTreeNode) => c.label)).toStrictEqual(["A", "B", "C"]); + }); + + it("sorts by last modified date", async () => { + const mocks = getBlockMocks(); + const nodes = nodesForSuite(); + mocks.showQuickPick.mockResolvedValueOnce({ label: "$(calendar) Date Modified" }); + await tree.sortPdsMembersDialog(nodes.pds); + expect(mocks.nodeDataChanged).toHaveBeenCalled(); + expect(mocks.refreshElement).not.toHaveBeenCalled(); + expect(nodes.pds.children?.map((c: IZoweDatasetTreeNode) => c.label)).toStrictEqual(["B", "C", "A"]); + }); + + it("sorts by user ID", async () => { + const mocks = getBlockMocks(); + const nodes = nodesForSuite(); + mocks.showQuickPick.mockResolvedValueOnce({ label: "$(account) User ID" }); + await tree.sortPdsMembersDialog(nodes.pds); + expect(mocks.nodeDataChanged).toHaveBeenCalled(); + expect(mocks.refreshElement).not.toHaveBeenCalled(); + expect(nodes.pds.children?.map((c: IZoweDatasetTreeNode) => c.label)).toStrictEqual(["B", "A", "C"]); + }); + + it("returns to sort selection dialog when sort direction selection is canceled", async () => { + const sortPdsMembersDialog = jest.spyOn(tree, "sortPdsMembersDialog"); + const mocks = getBlockMocks(); + const nodes = nodesForSuite(); + mocks.showQuickPick.mockResolvedValueOnce({ label: "$(fold) Sort Direction" }); + mocks.showQuickPick.mockResolvedValueOnce(undefined); + await tree.sortPdsMembersDialog(nodes.pds); + expect(mocks.nodeDataChanged).not.toHaveBeenCalled(); + expect(mocks.refreshElement).not.toHaveBeenCalled(); + expect(sortPdsMembersDialog).toHaveBeenCalledTimes(2); + }); + }); + + describe("filterBy & filterPdsMembersDialog", () => { + afterEach(() => { + const mocks = getBlockMocks(); + for (const mock of Object.values(mocks)) { + mock.mockReset(); + } + }); + + afterAll(() => { + const mocks = getBlockMocks(); + for (const mock of Object.values(mocks)) { + mock.mockRestore(); + } + }); + + it("calls refreshElement if PDS children were removed from a previous filter", async () => { + const mocks = getBlockMocks(); + const nodes = nodesForSuite(); + mocks.showQuickPick.mockResolvedValueOnce("$(calendar) Date Modified" as any); + mocks.showInputBox.mockResolvedValueOnce("2022-01-01"); + + nodes.pds.filter = { method: DatasetFilterOpts.UserId, value: "invalidUserId" }; + nodes.pds.children = []; + await tree.filterPdsMembersDialog(nodes.pds); + expect(mocks.nodeDataChanged).not.toHaveBeenCalled(); + expect(mocks.refreshElement).toHaveBeenCalledWith(nodes.pds); + }); + + it("returns to filter selection dialog when filter entry is canceled", async () => { + const filterPdsMembersSpy = jest.spyOn(tree, "filterPdsMembersDialog"); + const mocks = getBlockMocks(); + const nodes = nodesForSuite(); + mocks.showQuickPick.mockResolvedValueOnce("$(calendar) Date Modified" as any); + mocks.showInputBox.mockResolvedValueOnce(undefined); + await tree.filterPdsMembersDialog(nodes.pds); + expect(mocks.nodeDataChanged).not.toHaveBeenCalled(); + expect(mocks.refreshElement).not.toHaveBeenCalled(); + expect(filterPdsMembersSpy).toHaveBeenCalledTimes(2); + }); + + it("filters single PDS by last modified date", async () => { + const mocks = getBlockMocks(); + const nodes = nodesForSuite(); + mocks.showQuickPick.mockResolvedValueOnce("$(calendar) Date Modified" as any); + mocks.showInputBox.mockResolvedValueOnce("2022-03-15"); + await tree.filterPdsMembersDialog(nodes.pds); + expect(mocks.nodeDataChanged).toHaveBeenCalled(); + expect(mocks.refreshElement).not.toHaveBeenCalled(); + expect(nodes.pds.children?.map((c: IZoweDatasetTreeNode) => c.label)).toStrictEqual(["C"]); + }); + + it("filters single PDS by user ID", async () => { + const mocks = getBlockMocks(); + const nodes = nodesForSuite(); + mocks.showQuickPick.mockResolvedValueOnce("$(account) User ID" as any); + mocks.showInputBox.mockResolvedValueOnce("anotherUser"); + await tree.filterPdsMembersDialog(nodes.pds); + expect(mocks.nodeDataChanged).toHaveBeenCalled(); + expect(mocks.refreshElement).not.toHaveBeenCalled(); + expect(nodes.pds.children?.map((c: IZoweDatasetTreeNode) => c.label)).toStrictEqual(["B"]); + }); + + it("filters PDS members using the session node filter", async () => { + const mocks = getBlockMocks(); + const nodes = nodesForSuite(); + const uidString = "$(account) User ID" as any; + const anotherUser = "anotherUser"; + mocks.showQuickPick.mockResolvedValueOnce(uidString).mockResolvedValueOnce(uidString); + mocks.showInputBox.mockResolvedValueOnce(anotherUser).mockResolvedValueOnce(anotherUser); + + // case 1: old filter was set on session, just refresh PDS to use new filter + nodes.session.filter = { + method: DatasetFilterOpts.LastModified, + value: "2020-01-01", + }; + await tree.filterPdsMembersDialog(nodes.session); + expect(mocks.refreshElement).toHaveBeenCalled(); + + // case 2: no old filter present, PDS has children to be filtered + nodes.session.filter = undefined; + await tree.filterPdsMembersDialog(nodes.session); + expect(mocks.nodeDataChanged).toHaveBeenCalled(); + }); + + it("clears filter for a PDS when selected in dialog", async () => { + const mocks = getBlockMocks(); + const nodes = nodesForSuite(); + const resp = "$(clear-all) Clear filter for PDS" as any; + mocks.showQuickPick.mockResolvedValueOnce(resp); + const updateFilterForNode = jest.spyOn(DatasetTree.prototype, "updateFilterForNode"); + await tree.filterPdsMembersDialog(nodes.pds); + expect(mocks.refreshElement).not.toHaveBeenCalled(); + expect(updateFilterForNode).toHaveBeenCalledWith(nodes.pds, null, false); + }); + }); +}); diff --git a/packages/zowe-explorer/__tests__/__unit__/dataset/init.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/dataset/init.unit.test.ts index bacbe6f822..2697d100c1 100644 --- a/packages/zowe-explorer/__tests__/__unit__/dataset/init.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/dataset/init.unit.test.ts @@ -45,6 +45,8 @@ describe("Test src/dataset/extension", () => { onDidChangeConfiguration: jest.fn(), getTreeView: jest.fn(), refreshElement: jest.fn(), + sortPdsMembersDialog: jest.fn(), + filterPdsMembersDialog: jest.fn(), }; const commands: IJestIt[] = [ { @@ -250,6 +252,14 @@ describe("Test src/dataset/extension", () => { name: "zowe.ds.ssoLogout", mock: [{ spy: jest.spyOn(dsProvider, "ssoLogout"), arg: [test.value] }], }, + { + name: "zowe.ds.sortBy", + mock: [{ spy: jest.spyOn(dsProvider, "sortPdsMembersDialog"), arg: [test.value] }], + }, + { + name: "zowe.ds.filterBy", + mock: [{ spy: jest.spyOn(dsProvider, "filterPdsMembersDialog"), arg: [test.value] }], + }, { name: "onDidChangeConfiguration", mock: [{ spy: jest.spyOn(dsProvider, "onDidChangeConfiguration"), arg: [test.value] }], diff --git a/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts index 5f111c1d64..4f17e2531a 100644 --- a/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts @@ -176,6 +176,8 @@ async function createGlobalMocks() { "zowe.ds.enableValidation", "zowe.ds.ssoLogin", "zowe.ds.ssoLogout", + "zowe.ds.sortBy", + "zowe.ds.filterBy", "zowe.uss.addFavorite", "zowe.uss.removeFavorite", "zowe.uss.addSession", @@ -238,9 +240,8 @@ async function createGlobalMocks() { "zowe.jobs.startPolling", "zowe.jobs.stopPolling", "zowe.jobs.cancelJob", - "zowe.jobs.sortbyname", - "zowe.jobs.sortbyid", - "zowe.jobs.sortbyreturncode", + "zowe.jobs.sortBy", + "zowe.jobs.filterJobs", "zowe.manualPoll", "zowe.updateSecureCredentials", "zowe.promptCredentials", diff --git a/packages/zowe-explorer/__tests__/__unit__/job/ZoweJobNode.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/job/ZoweJobNode.unit.test.ts index 2ab7070292..ac20a2055c 100644 --- a/packages/zowe-explorer/__tests__/__unit__/job/ZoweJobNode.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/job/ZoweJobNode.unit.test.ts @@ -16,7 +16,7 @@ import * as zowe from "@zowe/cli"; import * as globals from "../../../src/globals"; import { createIJobFile, createIJobObject, createJobSessionNode } from "../../../__mocks__/mockCreators/jobs"; import { Job } from "../../../src/job/ZoweJobNode"; -import { IZoweJobTreeNode, ProfilesCache, Gui } from "@zowe/zowe-explorer-api"; +import { IZoweJobTreeNode, ProfilesCache, Gui, JobSortOpts, SortDirection } from "@zowe/zowe-explorer-api"; import { ZoweExplorerApiRegister } from "../../../src/ZoweExplorerApiRegister"; import { Profiles } from "../../../src/Profiles"; import * as sessUtils from "../../../src/utils/SessionUtils"; @@ -583,9 +583,8 @@ describe("ZoweJobNode unit tests - Function saveSearch", () => { const expectedJob = favJob; expectedJob.contextValue = globals.JOBS_SESSION_CONTEXT + globals.FAV_SUFFIX; - const savedFavJob = await globalMocks.testJobsProvider.saveSearch(favJob); - - expect(savedFavJob).toEqual(expectedJob); + globalMocks.testJobsProvider.saveSearch(favJob); + expect(expectedJob.contextValue).toEqual(favJob.contextValue); }); }); @@ -851,7 +850,7 @@ describe("Job - sortJobs", () => { jobid: "JOBID120", }, } as IZoweJobTreeNode, - ].sort((a, b) => Job.sortJobs(a, b)); + ].sort(Job.sortJobs({ method: JobSortOpts.Id, direction: SortDirection.Ascending })); expect(sorted[0].job.jobid).toBe("JOBID120"); expect(sorted[1].job.jobid).toBe("JOBID120"); expect(sorted[2].job.jobid).toBe("JOBID123"); diff --git a/packages/zowe-explorer/__tests__/__unit__/job/actions.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/job/actions.unit.test.ts index 85737a5c89..8394ba051a 100644 --- a/packages/zowe-explorer/__tests__/__unit__/job/actions.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/job/actions.unit.test.ts @@ -11,7 +11,7 @@ import * as vscode from "vscode"; import * as zowe from "@zowe/cli"; -import { Gui, IZoweJobTreeNode, ValidProfileEnum } from "@zowe/zowe-explorer-api"; +import { Gui, IZoweJobTreeNode, JobSortOpts, SortDirection, ValidProfileEnum } from "@zowe/zowe-explorer-api"; import { Job, Spool } from "../../../src/job/ZoweJobNode"; import { createISession, @@ -45,12 +45,57 @@ import { ZosJobsProvider } from "../../../src/job/ZosJobsProvider"; const activeTextEditorDocument = jest.fn(); +jest.mock("vscode"); + +const showMock = jest.fn(); + +const onDidChangeValueMock = { + event: (callback: (value: string) => void): vscode.Disposable => { + const disposable = { + dispose: jest.fn(), + }; + callback(""); + return disposable; + }, +}; + +const mockInputBox: vscode.InputBox = { + title: "", + value: "", + placeholder: "", + password: false, + onDidChangeValue: onDidChangeValueMock.event, + onDidAccept: jest.fn(), + show: showMock, + hide: jest.fn(), + dispose: jest.fn(), + buttons: [], + onDidTriggerButton: jest.fn(), + prompt: "", + validationMessage: "", + step: 1, + totalSteps: 100, + enabled: true, + busy: false, + ignoreFocusOut: false, + onDidHide: jest.fn(), +}; + +function setJobObjects(job: zowe.IJob, newJobName: string, newJobId: string, newRetCode: string) { + job.jobname = newJobName; + job.jobid = newJobId; + job.retcode = newRetCode; + return job; +} + function createGlobalMocks() { Object.defineProperty(vscode.workspace, "getConfiguration", { value: jest.fn().mockImplementation(() => new Map([["zowe.jobs.confirmSubmission", false]])), configurable: true, }); Object.defineProperty(Gui, "showMessage", { value: jest.fn(), configurable: true }); + Object.defineProperty(Gui, "infoMessage", { value: jest.fn(), configurable: true }); + Object.defineProperty(Gui, "infoMessage", { value: jest.fn(), configurable: true }); Object.defineProperty(Gui, "warningMessage", { value: jest.fn(), configurable: true }); Object.defineProperty(Gui, "errorMessage", { value: jest.fn(), configurable: true }); Object.defineProperty(Gui, "showOpenDialog", { value: jest.fn(), configurable: true }); @@ -86,6 +131,12 @@ function createGlobalMocks() { Object.defineProperty(ZoweLogger, "error", { value: jest.fn(), configurable: true }); Object.defineProperty(ZoweLogger, "debug", { value: jest.fn(), configurable: true }); Object.defineProperty(ZoweLogger, "trace", { value: jest.fn(), configurable: true }); + Object.defineProperty(vscode.window, "createInputBox", { + value: jest.fn(() => mockInputBox), + configurable: true, + }); + + Object.defineProperty(vscode.window, "showInformationMessage", { value: jest.fn(), configurable: true }); Object.defineProperty(vscode.window, "showInformationMessage", { value: jest.fn(), configurable: true }); function settingJobObjects(job: zowe.IJob, setjobname: string, setjobid: string, setjobreturncode: string): zowe.IJob { job.jobname = setjobname; @@ -1344,31 +1395,26 @@ describe("Job Actions Unit Tests - Misc. functions", () => { expect(statusMsgSpy).toHaveBeenCalledWith(`$(sync~spin) Polling: ${testDoc.fileName}...`); }); }); -describe("sortjobsby function", () => { +describe("sortJobs function", () => { afterEach(() => { jest.restoreAllMocks(); }); - it("if there are no jobs in the zosmf level yet", async () => { - createGlobalMocks(); - const testtree = new ZosJobsProvider(); - //act - await jobActions.sortJobsBy(testtree.mSessionNodes[0], testtree, "jobname"); - await jobActions.sortJobsBy(testtree.mSessionNodes[0], testtree, "jobid"); - await jobActions.sortJobsBy(testtree.mSessionNodes[0], testtree, "retcode"); - //assert - expect(mocked(vscode.window.showInformationMessage)).toBeCalled(); - }); it("sort by name if same sort by increasing id", async () => { const globalMocks = createGlobalMocks(); const testtree = new ZosJobsProvider(); const expected = new ZosJobsProvider(); + testtree.mSessionNodes[0].sort = { + method: JobSortOpts.Id, + direction: SortDirection.Ascending, + }; testtree.mSessionNodes[0].children = [...[globalMocks()[2], globalMocks()[1], globalMocks()[0]]]; expected.mSessionNodes[0].children = [...[globalMocks()[1], globalMocks()[0], globalMocks()[2]]]; - const sortbynamespy = jest.spyOn(jobActions, "sortJobsBy"); + jest.spyOn(Gui, "showQuickPick").mockResolvedValueOnce({ label: "$(case-sensitive) Job Name" }); + const sortbynamespy = jest.spyOn(ZosJobsProvider.prototype, "sortBy"); //act - await jobActions.sortJobsBy(testtree.mSessionNodes[0], testtree, "jobname"); + await jobActions.sortJobs(testtree.mSessionNodes[0], testtree); //asert - expect(sortbynamespy).toBeCalledWith(testtree.mSessionNodes[0], testtree, "jobname"); + expect(sortbynamespy).toBeCalledWith(testtree.mSessionNodes[0]); expect(sortbynamespy).toHaveBeenCalled(); expect(sortbynamespy.mock.calls[0][0].children).toStrictEqual(expected.mSessionNodes[0].children); }); @@ -1376,13 +1422,18 @@ describe("sortjobsby function", () => { const globalMocks = createGlobalMocks(); const testtree = new ZosJobsProvider(); const expected = new ZosJobsProvider(); + testtree.mSessionNodes[0].sort = { + method: JobSortOpts.Id, + direction: SortDirection.Ascending, + }; testtree.mSessionNodes[0].children = [...[globalMocks()[2], globalMocks()[1], globalMocks()[0]]]; expected.mSessionNodes[0].children = [...[globalMocks()[1], globalMocks()[0], globalMocks()[2]]]; - const sortbyidspy = jest.spyOn(jobActions, "sortJobsBy"); + const sortbyidspy = jest.spyOn(ZosJobsProvider.prototype, "sortBy"); + jest.spyOn(Gui, "showQuickPick").mockResolvedValueOnce({ label: "$(list-ordered) Job ID (default)" }); //act - await jobActions.sortJobsBy(testtree.mSessionNodes[0], testtree, "jobid"); + await jobActions.sortJobs(testtree.mSessionNodes[0], testtree); //asert - expect(sortbyidspy).toBeCalledWith(testtree.mSessionNodes[0], testtree, "jobid"); + expect(sortbyidspy).toBeCalledWith(testtree.mSessionNodes[0]); expect(sortbyidspy).toHaveBeenCalled(); expect(sortbyidspy.mock.calls[0][0].children).toStrictEqual(expected.mSessionNodes[0].children); }); @@ -1390,14 +1441,94 @@ describe("sortjobsby function", () => { const globalMocks = createGlobalMocks(); const testtree = new ZosJobsProvider(); const expected = new ZosJobsProvider(); + testtree.mSessionNodes[0].sort = { + method: JobSortOpts.Id, + direction: SortDirection.Ascending, + }; testtree.mSessionNodes[0].children = [...[globalMocks()[2], globalMocks()[1], globalMocks()[0]]]; expected.mSessionNodes[0].children = [...[globalMocks()[0], globalMocks()[1], globalMocks()[2]]]; - const sortbyretcodespy = jest.spyOn(jobActions, "sortJobsBy"); + const sortbyretcodespy = jest.spyOn(ZosJobsProvider.prototype, "sortBy"); + jest.spyOn(Gui, "showQuickPick").mockResolvedValueOnce({ label: "$(symbol-numeric) Return Code" }); + //act - await jobActions.sortJobsBy(testtree.mSessionNodes[0], testtree, "retcode"); + await jobActions.sortJobs(testtree.mSessionNodes[0], testtree); //asert - expect(sortbyretcodespy).toBeCalledWith(testtree.mSessionNodes[0], testtree, "retcode"); + expect(sortbyretcodespy).toBeCalledWith(testtree.mSessionNodes[0]); expect(sortbyretcodespy).toHaveBeenCalled(); expect(sortbyretcodespy.mock.calls[0][0].children).toStrictEqual(expected.mSessionNodes[0].children); }); + + it("updates sort options after selecting sort direction; returns user to sort selection", async () => { + const globalMocks = createGlobalMocks(); + const testtree = new ZosJobsProvider(); + testtree.mSessionNodes[0].sort = { + method: JobSortOpts.Id, + direction: SortDirection.Ascending, + }; + testtree.mSessionNodes[0].children = [globalMocks()[0]]; + const jobsSortBy = jest.spyOn(ZosJobsProvider.prototype, "sortBy"); + const quickPickSpy = jest.spyOn(Gui, "showQuickPick").mockResolvedValueOnce({ label: "$(fold) Sort Direction" }); + quickPickSpy.mockResolvedValueOnce("Descending" as any); + await jobActions.sortJobs(testtree.mSessionNodes[0], testtree); + expect(testtree.mSessionNodes[0].sort.direction).toBe(SortDirection.Descending); + expect(quickPickSpy).toHaveBeenCalledTimes(3); + expect(jobsSortBy).not.toHaveBeenCalled(); + }); +}); + +describe("Job Actions Unit Tests - Filter Jobs", () => { + const node1 = new Job( + "jobnew", + vscode.TreeItemCollapsibleState.None, + null, + null, + setJobObjects(createIJobObject(), "ZOWEUSR1", "JOB04945", "CC 0000"), + null + ); + const node2 = new Job( + "jobnew", + vscode.TreeItemCollapsibleState.None, + null, + null, + setJobObjects(createIJobObject(), "ZOWEUSR2", "JOB05037", "CC 0000"), + null + ); + const node3 = new Job( + "jobnew", + vscode.TreeItemCollapsibleState.None, + null, + null, + setJobObjects(createIJobObject(), "ZOWEUSR3", "TSU07707", "ABEND S222"), + null + ); + + it("To show showInformationMessage", async () => { + const testTree = new ZosJobsProvider(); + testTree.mSessionNodes[0].label = "zosmf"; + node1.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; + + await jobActions.filterJobs(testTree, node1); + + expect(mocked(Gui.infoMessage)).toHaveBeenCalled(); + expect(mocked(Gui.infoMessage)).toHaveBeenCalled(); + }); + + it("To filter jobs based on a combination of JobName, JobId and Return code", async () => { + const testTree = new ZosJobsProvider(); + testTree.mSessionNodes[0].label = "zosmf"; + node1.collapsibleState = vscode.TreeItemCollapsibleState.Expanded; + node1.children = [node2, node3]; + + const createInputBoxSpy = jest.spyOn(vscode.window, "createInputBox"); + mockInputBox.value = "ZOWEUSR2(JOB05037) - CC 0000"; + createInputBoxSpy.mockReturnValue(mockInputBox); + const filterJobsSpy = jest.spyOn(jobActions, "filterJobs"); + await jobActions.filterJobs(testTree, node1); + testTree.mSessionNodes[0].children = [node2]; + + expect(createInputBoxSpy).toHaveBeenCalled(); + expect(filterJobsSpy).toHaveBeenCalled(); + expect(filterJobsSpy).toBeCalledWith(testTree, node1); + expect(filterJobsSpy.mock.calls[0][0].mSessionNodes[0].children).toBe(testTree.mSessionNodes[0].children); + }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/job/init.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/job/init.unit.test.ts index 9a9f9fdd85..56821ae57e 100644 --- a/packages/zowe-explorer/__tests__/__unit__/job/init.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/job/init.unit.test.ts @@ -54,6 +54,7 @@ describe("Test src/jobs/extension", () => { onDidChangeConfiguration: jest.fn(), pollData: jest.fn(), refreshElement: jest.fn(), + filterJobs: jest.fn(), }; const commands: IJestIt[] = [ { @@ -219,6 +220,10 @@ describe("Test src/jobs/extension", () => { mock: [{ spy: jest.spyOn(jobActions, "cancelJobs"), arg: [jobsProvider, [exampleData.job]] }], parm: [exampleData.job], }, + { + name: "zowe.jobs.filterJobs", + mock: [{ spy: jest.spyOn(jobActions, "filterJobs"), arg: [jobsProvider, test.value] }], + }, ]; beforeAll(async () => { diff --git a/packages/zowe-explorer/i18n/sample/package.i18n.json b/packages/zowe-explorer/i18n/sample/package.i18n.json index a73e7985e5..af2e332f35 100644 --- a/packages/zowe-explorer/i18n/sample/package.i18n.json +++ b/packages/zowe-explorer/i18n/sample/package.i18n.json @@ -148,7 +148,12 @@ "createZoweSchema.reload.infoMessage": "Team Configuration file created. Location: {0}. \n Please reload your window.", "copyFile": "Copy", "pasteFile": "Paste", - "jobs.sortbyreturncode": "Sort by ReturnCode", - "jobs.sortbyname": "Sort by Name", - "jobs.sortbyid": "Sort by ID" + "jobs.sortBy": "Sort jobs...", + "ds.allPdsSort": "all PDS members in {0}", + "ds.singlePdsSort": "the PDS members in {0}", + "ds.selectFilterOpt": "Set a filter for {0}", + "ds.selectSortOpt": "Select a sorting option for {0}", + "jobs.selectSortOpt": "Select a sorting option for jobs in {0}", + "ds.filterBy": "Filter PDS members...", + "ds.sortBy": "Sort PDS members..." } diff --git a/packages/zowe-explorer/i18n/sample/src/dataset/DatasetTree.i18n.json b/packages/zowe-explorer/i18n/sample/src/dataset/DatasetTree.i18n.json index 53eff9a390..a466d5c330 100644 --- a/packages/zowe-explorer/i18n/sample/src/dataset/DatasetTree.i18n.json +++ b/packages/zowe-explorer/i18n/sample/src/dataset/DatasetTree.i18n.json @@ -26,5 +26,19 @@ "renameDataSet.log.debug": "Renaming data set ", "renameDataSet.error": "Unable to rename data set:", "dataset.validation": "Enter a valid data set name.", + "ds.allPdsSort": "all PDS members in {0}", + "ds.singlePdsSort": "the PDS members in {0}", + "ds.selectSortOpt": "Select a sorting option for {0}", + "setSortDirection": "$(fold) Sort Direction", + "sort.selectDirection": "Select a sorting direction", + "sort.updated": "$(check) Sorting updated for {0}", + "ds.clearProfileFilter": "$(clear-all) Clear filter for profile", + "ds.clearPdsFilter": "$(clear-all) Clear filter for PDS", + "ds.selectFilterOpt": "Set a filter for {0}", + "filter.cleared": "$(check) Filter cleared for {0}", + "ds.filterEntry.invalidDate": "Invalid date format specified", + "ds.filterEntry.title": "Enter a value to filter by", + "ds.filterEntry.invalid": "Invalid filter specified", + "filter.updated": "$(check) Filter updated for {0}", "defaultFilterPrompt.option.prompt.search": "$(plus) Create a new filter. For example: HLQ.*, HLQ.aaa.bbb, HLQ.ccc.ddd(member)" } diff --git a/packages/zowe-explorer/i18n/sample/src/dataset/utils.i18n.json b/packages/zowe-explorer/i18n/sample/src/dataset/utils.i18n.json new file mode 100644 index 0000000000..0aad47e3d5 --- /dev/null +++ b/packages/zowe-explorer/i18n/sample/src/dataset/utils.i18n.json @@ -0,0 +1,6 @@ +{ + "ds.sortByName": "$(case-sensitive) Name (default)", + "ds.sortByModified": "$(calendar) Date Modified", + "ds.sortByUserId": "$(account) User ID", + "setSortDirection": "$(fold) Sort Direction" +} diff --git a/packages/zowe-explorer/i18n/sample/src/globals.i18n.json b/packages/zowe-explorer/i18n/sample/src/globals.i18n.json index ec7d8bddd5..a6c64f66d8 100644 --- a/packages/zowe-explorer/i18n/sample/src/globals.i18n.json +++ b/packages/zowe-explorer/i18n/sample/src/globals.i18n.json @@ -17,6 +17,7 @@ "createFile.attribute.storclass": "Enter the SMS storage class", "createFile.attribute.volser": "Enter the volume serial on which the data set should be placed", "zowe.separator.recentFilters": "Recent Filters", + "zowe.separator.options": "Options", "globals.defineGlobals.isTheia": "Zowe Explorer is running in Theia environment.", "globals.defineGlobals.tempFolder": "Zowe Explorer's temp folder is located at {0}", "globals.setActivated.success": "Zowe Explorer has activated successfully.", diff --git a/packages/zowe-explorer/i18n/sample/src/job/actions.i18n.json b/packages/zowe-explorer/i18n/sample/src/job/actions.i18n.json index f1c4dad3d1..0cf7ce4d92 100644 --- a/packages/zowe-explorer/i18n/sample/src/job/actions.i18n.json +++ b/packages/zowe-explorer/i18n/sample/src/job/actions.i18n.json @@ -21,5 +21,11 @@ "cancelJobs.notImplemented": "The cancel function is not implemented in this API.", "cancelJobs.notCancelled": "The job was not cancelled.", "cancelJobs.failed": "One or more jobs failed to cancel: {0}", - "cancelJobs.succeeded": "Cancelled selected jobs successfully." + "cancelJobs.succeeded": "Cancelled selected jobs successfully.", + "jobs.selectSortOpt": "Select a sorting option for jobs in {0}", + "setSortDirection": "$(fold) Sort Direction", + "sort.selectDirection": "Select a sorting direction", + "sort.updated": "$(check) Sorting updated for {0}", + "filterJobs.message": "Use the search button to display jobs", + "filterJobs.prompt.message": "Enter local filter..." } diff --git a/packages/zowe-explorer/i18n/sample/src/job/utils.i18n.json b/packages/zowe-explorer/i18n/sample/src/job/utils.i18n.json new file mode 100644 index 0000000000..b32e729f24 --- /dev/null +++ b/packages/zowe-explorer/i18n/sample/src/job/utils.i18n.json @@ -0,0 +1,7 @@ +{ + "jobs.sortById": "$(list-ordered) Job ID (default)", + "jobs.sortByDateSubmitted": "$(calendar) Date Submitted", + "jobs.sortByName": "$(case-sensitive) Job Name", + "jobs.sortByReturnCode": "$(symbol-numeric) Return Code", + "setSortDirection": "$(fold) Sort Direction" +} diff --git a/packages/zowe-explorer/i18n/sample/src/shared/utils.i18n.json b/packages/zowe-explorer/i18n/sample/src/shared/utils.i18n.json index 7bdcd3a109..9ee68cb025 100644 --- a/packages/zowe-explorer/i18n/sample/src/shared/utils.i18n.json +++ b/packages/zowe-explorer/i18n/sample/src/shared/utils.i18n.json @@ -3,6 +3,8 @@ "zowe.jobs.confirmSubmission.yourJobs": "Your jobs", "zowe.jobs.confirmSubmission.otherUserJobs": "Other user jobs", "zowe.jobs.confirmSubmission.allJobs": "All jobs", + "sort.asc": "Ascending", + "sort.desc": "Descending", "uploadContent.putContents": "Uploading USS file", "saveFile.response.save.title": "Saving data set...", "saveUSSFile.response.title": "Saving file...", diff --git a/packages/zowe-explorer/package.json b/packages/zowe-explorer/package.json index c602c3c2d8..e77f74a440 100644 --- a/packages/zowe-explorer/package.json +++ b/packages/zowe-explorer/package.json @@ -122,18 +122,23 @@ ], "commands": [ { - "command": "zowe.jobs.sortbyreturncode", - "title": "%jobs.sortbyreturncode%", + "command": "zowe.jobs.sortByReturnCode", + "title": "%jobs.sortByReturnCode%", "category": "Zowe Explorer" }, { - "command": "zowe.jobs.sortbyname", - "title": "%jobs.sortbyname%", + "command": "zowe.jobs.sortByName", + "title": "%sortByName%", "category": "Zowe Explorer" }, { - "command": "zowe.jobs.sortbyid", - "title": "%jobs.sortbyid%", + "command": "zowe.jobs.sortById", + "title": "%jobs.sortById%", + "category": "Zowe Explorer" + }, + { + "command": "zowe.jobs.sortByDateSubmitted", + "title": "%jobs.sortByDateSubmitted%", "category": "Zowe Explorer" }, { @@ -281,6 +286,18 @@ "title": "%deleteProfile%", "category": "Zowe Explorer" }, + { + "command": "zowe.ds.filterBy", + "title": "%ds.filterBy%", + "category": "Zowe Explorer", + "icon": "$(list-filter)" + }, + { + "command": "zowe.ds.sortBy", + "title": "%ds.sortBy%", + "category": "Zowe Explorer", + "icon": "$(list-ordered)" + }, { "command": "zowe.cmd.deleteProfile", "title": "%cmd.deleteProfile%", @@ -749,6 +766,15 @@ "title": "%cancelJobs%", "category": "Zowe Explorer" }, + { + "command": "zowe.jobs.filterJobs", + "title": "Filter Jobs", + "category": "Zowe Explorer", + "icon": { + "light": "./resources/light/filter-light.svg", + "dark": "./resources/dark/filter-dark.svg" + } + }, { "command": "zowe.jobs.search", "title": "%jobs.search%", @@ -960,13 +986,48 @@ }, { "when": "view == zowe.uss.explorer && viewItem =~ /^(?!.*_fav.*)ussSession.*/ && !listMultiSelection", - "command": "zowe.profileManagement", - "group": "099_zowe_ussProfileAuthentication@99" + "command": "zowe.promptCredentials", + "group": "098_zowe_ussProfileAuthentication@3" + }, + { + "when": "view == zowe.uss.explorer && viewItem =~ /^(?!.*_fav.*)ussSession.*/ && !listMultiSelection", + "command": "zowe.uss.ssoLogin", + "group": "098_zowe_ussProfileAuthentication@4" + }, + { + "when": "view == zowe.uss.explorer && viewItem =~ /^(?!.*_fav.*)ussSession.*/ && !listMultiSelection", + "command": "zowe.uss.ssoLogout", + "group": "098_zowe_ussProfileAuthentication@5" + }, + { + "when": "viewItem =~ /^(?!.*_fav.*)ussSession.*/ && !listMultiSelection", + "command": "zowe.uss.editSession", + "group": "099_zowe_ussProfileModification@1" + }, + { + "when": "viewItem =~ /^(?!.*_fav.*)ussSession.*/", + "command": "zowe.uss.removeSession", + "group": "099_zowe_ussProfileModification@98" + }, + { + "when": "viewItem =~ /^(?!.*_fav.*)ussSession.*/ && !listMultiSelection", + "command": "zowe.uss.deleteProfile", + "group": "099_zowe_ussProfileModification@99" + }, + { + "when": "view == zowe.ds.explorer && viewItem =~ /^session.*/ && !listMultiSelection", + "command": "zowe.ds.filterBy", + "group": "inline@0" + }, + { + "when": "view == zowe.ds.explorer && viewItem =~ /^session.*/ && !listMultiSelection", + "command": "zowe.ds.sortBy", + "group": "inline@1" }, { "when": "view == zowe.ds.explorer && viewItem =~ /^(?!.*_fav.*)session.*/ && !listMultiSelection", "command": "zowe.ds.pattern", - "group": "inline" + "group": "inline@2" }, { "when": "view == zowe.ds.explorer && viewItem =~ /^(pds|ds|migr).*_fav.*/", @@ -1091,7 +1152,7 @@ { "when": "view == zowe.ds.explorer && viewItem =~ /^(ds.*|^pds.*)/", "command": "zowe.ds.hMigrateDataSet", - "group": "099_zowe_dsModification@0" + "group": "z_zowe_dsModification@0" }, { "when": "view == zowe.ds.explorer && viewItem =~ /^ds.*/", @@ -1116,7 +1177,7 @@ { "when": "view == zowe.ds.explorer && viewItem =~ /^pds.*/ && !listMultiSelection", "command": "zowe.ds.renameDataSet", - "group": "099_zowe_dsModification@2" + "group": "z_zowe_dsModification@2" }, { "when": "view == zowe.ds.explorer && viewItem =~ /^ds.*/", @@ -1131,7 +1192,7 @@ { "when": "view == zowe.ds.explorer && viewItem =~ /^pds.*/", "command": "zowe.ds.deleteDataset", - "group": "099_zowe_dsModification@5" + "group": "z_zowe_dsModification@5" }, { "when": "view == zowe.ds.explorer && viewItem =~ /^migr.*/", @@ -1143,10 +1204,15 @@ "command": "zowe.profileManagement", "group": "099_zowe_dsProfileAuthentication@99" }, + { + "when": "view == zowe.jobs.explorer && viewItem =~ /^(?!.*_fav.*)server.*/ && !listMultiSelection", + "command": "zowe.jobs.sortBy", + "group": "inline@0" + }, { "when": "view == zowe.jobs.explorer && viewItem =~ /^(?!.*_fav.*)server.*/ && !listMultiSelection", "command": "zowe.jobs.search", - "group": "inline" + "group": "inline@1" }, { "when": "view == zowe.jobs.explorer && viewItem =~ /^(?!.*_fav.*)job.*/", @@ -1163,6 +1229,11 @@ "command": "zowe.jobs.removeSearchFavorite", "group": "inline" }, + { + "when": "view == zowe.jobs.explorer && viewItem =~ /^(?!.*_fav.*)server.*/ && !listMultiSelection", + "command": "zowe.jobs.filterJobs", + "group": "inline" + }, { "when": "view == zowe.jobs.explorer && viewItem =~ /^(?!.*_fav.*)server.*/ && !listMultiSelection", "command": "zowe.jobs.search", @@ -1218,6 +1289,11 @@ "command": "zowe.jobs.addFavorite", "group": "002_zowe_jobsWorkspace@0" }, + { + "when": "view == zowe.jobs.explorer && viewItem =~ /^(?!.*_fav.*)server.*/ && !listMultiSelection", + "command": "zowe.jobs.filterJobs", + "group": "002_zowe_jobsProfileModification@99" + }, { "when": "view == zowe.jobs.explorer && viewItem =~ /^job.*_fav.*/", "command": "zowe.jobs.removeFavorite", @@ -1275,18 +1351,23 @@ }, { "when": "view == zowe.jobs.explorer && viewItem =~ /^(?!.*_fav.*)server.*/ && !listMultiSelection", - "command": "zowe.jobs.sortbyid", + "command": "zowe.jobs.sortById", "group": "000_zowe_jobsProfileModification@1" }, { "when": "view == zowe.jobs.explorer && viewItem =~ /^(?!.*_fav.*)server.*/ && !listMultiSelection", - "command": "zowe.jobs.sortbyname", + "command": "zowe.jobs.sortByName", "group": "000_zowe_jobsProfileModification@2" }, { "when": "view == zowe.jobs.explorer && viewItem =~ /^(?!.*_fav.*)server.*/ && !listMultiSelection", - "command": "zowe.jobs.sortbyreturncode", + "command": "zowe.jobs.sortByReturnCode", "group": "000_zowe_jobsProfileModification@2" + }, + { + "when": "view == zowe.jobs.explorer && viewItem =~ /^(?!.*_fav.*)server.*/ && !listMultiSelection", + "command": "zowe.jobs.sortByDateSubmitted", + "group": "000_zowe_jobsProfileModification@3" } ], "commandPalette": [ @@ -1929,6 +2010,7 @@ "dependencies": { "@zowe/secrets-for-zowe-sdk": "7.18.4", "@zowe/zowe-explorer-api": "2.12.0-SNAPSHOT", + "dayjs": "^1.11.10", "fs-extra": "8.0.1", "isbinaryfile": "4.0.4", "js-yaml": "3.13.1", diff --git a/packages/zowe-explorer/package.nls.json b/packages/zowe-explorer/package.nls.json index a73e7985e5..af2e332f35 100644 --- a/packages/zowe-explorer/package.nls.json +++ b/packages/zowe-explorer/package.nls.json @@ -148,7 +148,12 @@ "createZoweSchema.reload.infoMessage": "Team Configuration file created. Location: {0}. \n Please reload your window.", "copyFile": "Copy", "pasteFile": "Paste", - "jobs.sortbyreturncode": "Sort by ReturnCode", - "jobs.sortbyname": "Sort by Name", - "jobs.sortbyid": "Sort by ID" + "jobs.sortBy": "Sort jobs...", + "ds.allPdsSort": "all PDS members in {0}", + "ds.singlePdsSort": "the PDS members in {0}", + "ds.selectFilterOpt": "Set a filter for {0}", + "ds.selectSortOpt": "Select a sorting option for {0}", + "jobs.selectSortOpt": "Select a sorting option for jobs in {0}", + "ds.filterBy": "Filter PDS members...", + "ds.sortBy": "Sort PDS members..." } diff --git a/packages/zowe-explorer/resources/dark/filter-dark.svg b/packages/zowe-explorer/resources/dark/filter-dark.svg new file mode 100644 index 0000000000..cdba9f4963 --- /dev/null +++ b/packages/zowe-explorer/resources/dark/filter-dark.svg @@ -0,0 +1,12 @@ + + + diff --git a/packages/zowe-explorer/resources/light/filter-light.svg b/packages/zowe-explorer/resources/light/filter-light.svg new file mode 100644 index 0000000000..3013b2a0fd --- /dev/null +++ b/packages/zowe-explorer/resources/light/filter-light.svg @@ -0,0 +1,12 @@ + + + diff --git a/packages/zowe-explorer/src/abstract/ZoweTreeProvider.ts b/packages/zowe-explorer/src/abstract/ZoweTreeProvider.ts index 0321cad081..eddd2868d5 100644 --- a/packages/zowe-explorer/src/abstract/ZoweTreeProvider.ts +++ b/packages/zowe-explorer/src/abstract/ZoweTreeProvider.ts @@ -85,6 +85,16 @@ export class ZoweTreeProvider { } } + /** + * Fire the "onDidChangeTreeData" event to signal that a node in the tree has changed. + * Unlike `refreshElement`, this function does *not* signal a refresh for the given node - + * it simply tells VS Code to repaint the node in the tree. + * @param node The node that should be repainted + */ + public nodeDataChanged(node: IZoweTreeNode): void { + this.mOnDidChangeTreeData.fire(node); + } + /** * Called whenever the tree needs to be refreshed, and fires the data change event * diff --git a/packages/zowe-explorer/src/dataset/DatasetTree.ts b/packages/zowe-explorer/src/dataset/DatasetTree.ts index 41e428768d..c479f8141f 100644 --- a/packages/zowe-explorer/src/dataset/DatasetTree.ts +++ b/packages/zowe-explorer/src/dataset/DatasetTree.ts @@ -15,28 +15,34 @@ import * as nls from "vscode-nls"; import * as globals from "../globals"; import * as dsActions from "./actions"; import { - Gui, DataSetAllocTemplate, + Gui, ValidProfileEnum, IZoweTree, IZoweDatasetTreeNode, PersistenceSchemaEnum, NodeInteraction, IZoweTreeNode, + DatasetFilter, + DatasetSortOpts, + SortDirection, + NodeSort, + DatasetFilterOpts, } from "@zowe/zowe-explorer-api"; import { Profiles } from "../Profiles"; import { ZoweExplorerApiRegister } from "../ZoweExplorerApiRegister"; import { FilterDescriptor, FilterItem, errorHandling, syncSessionNode } from "../utils/ProfilesUtils"; -import { sortTreeItems, getAppName, getDocumentFilePath } from "../shared/utils"; +import { sortTreeItems, getAppName, getDocumentFilePath, SORT_DIRS } from "../shared/utils"; import { ZoweTreeProvider } from "../abstract/ZoweTreeProvider"; import { ZoweDatasetNode } from "./ZoweDatasetNode"; import { getIconById, getIconByNode, IconId, IIconItem } from "../generators/icons"; +import * as dayjs from "dayjs"; import * as fs from "fs"; import * as contextually from "../shared/context"; import { resetValidationSettings } from "../shared/actions"; import { closeOpenedTextFile } from "../utils/workspace"; import { IDataSet, IListOptions, imperative } from "@zowe/cli"; -import { validateDataSetName, validateMemberName } from "./utils"; +import { DATASET_FILTER_OPTS, DATASET_SORT_OPTS, validateDataSetName, validateMemberName } from "./utils"; import { SettingsConfig } from "../utils/SettingsConfig"; import { ZoweLogger } from "../utils/LoggerUtils"; import { TreeViewUtils } from "../utils/TreeViewUtils"; @@ -1287,4 +1293,210 @@ export class DatasetTree extends ZoweTreeProvider implements IZoweTree 0) { + // children nodes already exist, sort and repaint to avoid extra refresh + for (const c of node.children) { + if (contextually.isPds(c) && c.children) { + c.sort = node.sort; + c.children.sort(ZoweDatasetNode.sortBy(node.sort)); + this.nodeDataChanged(c); + } + } + } + } else if (node.children?.length > 0) { + // children nodes already exist, sort and repaint to avoid extra refresh + node.children.sort(ZoweDatasetNode.sortBy(node.sort)); + this.nodeDataChanged(node); + } + } + + /** + * Presents a dialog to the user with options and methods for sorting PDS members. + * @param node The node that was interacted with (via icon or right-click -> "Sort PDS members...") + */ + public async sortPdsMembersDialog(node: IZoweDatasetTreeNode): Promise { + const isSession = contextually.isSession(node); + + // Assume defaults if a user hasn't selected any sort options yet + const sortOpts = node.sort ?? { + method: DatasetSortOpts.Name, + direction: SortDirection.Ascending, + }; + + // Adapt menus to user based on the node that was interacted with + const specifier = isSession + ? localize("ds.allPdsSort", "all PDS members in {0}", node.label as string) + : localize("ds.singlePdsSort", "the PDS members in {0}", node.label as string); + const selection = await Gui.showQuickPick( + DATASET_SORT_OPTS.map((opt, i) => ({ + label: sortOpts.method === i ? `${opt} $(check)` : opt, + description: i === DATASET_SORT_OPTS.length - 1 ? SORT_DIRS[sortOpts.direction] : null, + })), + { + placeHolder: localize("ds.selectSortOpt", "Select a sorting option for {0}", specifier), + } + ); + if (selection == null) { + return; + } + + if (selection.label === localize("setSortDirection", "$(fold) Sort Direction")) { + // Update sort direction (if a new one was provided) + const dir = await Gui.showQuickPick(SORT_DIRS, { + placeHolder: localize("sort.selectDirection", "Select a sorting direction"), + }); + if (dir != null) { + node.sort = { + ...sortOpts, + direction: SORT_DIRS.indexOf(dir), + }; + } + await this.sortPdsMembersDialog(node); + return; + } + + const selectionText = selection.label.replace(" $(check)", ""); + const sortMethod = DATASET_SORT_OPTS.indexOf(selectionText); + if (sortMethod === -1) { + return; + } + + // Update sort for node based on selections + this.updateSortForNode(node, { ...sortOpts, method: sortMethod }, isSession); + Gui.setStatusBarMessage(localize("sort.updated", "$(check) Sorting updated for {0}", node.label as string), globals.MS_PER_SEC * 4); + } + + /** + * Updates or resets the filter for a given data set node. + * @param node The node whose filter should be updated/reset + * @param newFilter Either a valid `DatasetFilter` object, or `null` to reset the filter + * @param isSession Whether the node is a session + */ + public updateFilterForNode(node: IZoweDatasetTreeNode, newFilter: DatasetFilter | null, isSession: boolean): void { + const oldFilter = node.filter; + node.filter = newFilter; + + // if a session was selected, apply this sort to all PDS members + if (isSession) { + if (node.children?.length > 0) { + // children nodes already exist, sort and repaint to avoid extra refresh + for (const c of node.children) { + const asDs = c as IZoweDatasetTreeNode; + + // PDS-level filters should have precedence over a session-level filter + if (asDs.filter != null) { + continue; + } + + if (contextually.isPds(c)) { + // If there was an old session-wide filter set: refresh to get any + // missing nodes - new filter will be applied + if (oldFilter != null) { + this.refreshElement(c); + continue; + } + + if (newFilter != null && c.children?.length > 0) { + c.children = c.children.filter(ZoweDatasetNode.filterBy(newFilter)); + this.nodeDataChanged(c); + } else { + this.refreshElement(c); + } + } + } + } + return; + } + + // Updating filter for PDS node + // if a filter was already set for either session or PDS, just refresh to grab any missing nodes + const sessionFilterPresent = (node.getSessionNode() as IZoweDatasetTreeNode).filter; + if (oldFilter != null || sessionFilterPresent != null) { + this.refreshElement(node); + return; + } + + // since there wasn't a previous filter, sort and repaint existing nodes + if (newFilter != null && node.children?.length > 0) { + node.children = node.children.filter(ZoweDatasetNode.filterBy(newFilter)); + this.nodeDataChanged(node); + } + } + + /** + * Presents a dialog to the user with options and methods for sorting PDS members. + * @param node The data set node that was interacted with (via icon or right-click => "Filter PDS members...") + */ + public async filterPdsMembersDialog(node: IZoweDatasetTreeNode): Promise { + const isSession = contextually.isSession(node); + + // Adapt menus to user based on the node that was interacted with + const specifier = isSession + ? localize("ds.allPdsSort", "all PDS members in {0}", node.label as string) + : localize("ds.singlePdsSort", "the PDS members in {0}", node.label as string); + const clearFilter = isSession + ? localize("ds.clearProfileFilter", "$(clear-all) Clear filter for profile") + : localize("ds.clearPdsFilter", "$(clear-all) Clear filter for PDS"); + const selection = ( + await Gui.showQuickPick( + [...DATASET_FILTER_OPTS.map((sortOpt, i) => (node.filter?.method === i ? `${sortOpt} $(check)` : sortOpt)), clearFilter], + { + placeHolder: localize("ds.selectFilterOpt", "Set a filter for {0}", specifier), + } + ) + )?.replace(" $(check)", ""); + + const filterMethod = DATASET_FILTER_OPTS.indexOf(selection); + + const userDismissed = filterMethod < 0; + if (userDismissed || selection === clearFilter) { + if (selection === clearFilter) { + this.updateFilterForNode(node, null, isSession); + Gui.setStatusBarMessage(localize("filter.cleared", "$(check) Filter cleared for {0}", node.label as string), globals.MS_PER_SEC * 4); + } + return; + } + + const dateValidation = (value): string => { + return dayjs(value).isValid() ? null : localize("ds.filterEntry.invalidDate", "Invalid date format specified"); + }; + + const filter = await Gui.showInputBox({ + title: localize("ds.filterEntry.title", "Enter a value to filter by"), + placeHolder: "", + validateInput: + filterMethod === DatasetFilterOpts.LastModified + ? dateValidation + : (val): string => (val.length > 0 ? null : localize("ds.filterEntry.invalid", "Invalid filter specified")), + }); + + // User dismissed filter entry, go back to filter selection + if (filter == null) { + await this.filterPdsMembersDialog(node); + return; + } + + // Update filter for node based on selection & filter entry + this.updateFilterForNode( + node, + { + method: filterMethod, + value: filter, + }, + isSession + ); + Gui.setStatusBarMessage(localize("filter.updated", "$(check) Filter updated for {0}", node.label as string), globals.MS_PER_SEC * 4); + } } diff --git a/packages/zowe-explorer/src/dataset/ZoweDatasetNode.ts b/packages/zowe-explorer/src/dataset/ZoweDatasetNode.ts index 8d369eb00d..62a5510737 100644 --- a/packages/zowe-explorer/src/dataset/ZoweDatasetNode.ts +++ b/packages/zowe-explorer/src/dataset/ZoweDatasetNode.ts @@ -13,13 +13,26 @@ import * as zowe from "@zowe/cli"; import * as vscode from "vscode"; import * as globals from "../globals"; import { errorHandling } from "../utils/ProfilesUtils"; -import { Gui, NodeAction, IZoweDatasetTreeNode, ZoweTreeNode } from "@zowe/zowe-explorer-api"; +import { + DatasetFilter, + DatasetFilterOpts, + DatasetSortOpts, + DatasetStats, + Gui, + NodeAction, + IZoweDatasetTreeNode, + ZoweTreeNode, + SortDirection, + NodeSort, +} from "@zowe/zowe-explorer-api"; import { ZoweExplorerApiRegister } from "../ZoweExplorerApiRegister"; import { getIconByNode } from "../generators/icons"; import * as contextually from "../shared/context"; import * as nls from "vscode-nls"; import { Profiles } from "../Profiles"; import { ZoweLogger } from "../utils/LoggerUtils"; +import * as dayjs from "dayjs"; + // Set up localization nls.config({ messageFormat: nls.MessageFormat.bundle, @@ -43,6 +56,9 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod public errorDetails: zowe.imperative.ImperativeError; public ongoingActions: Record> = {}; public wasDoubleClicked: boolean = false; + public stats: DatasetStats; + public sort?: NodeSort; + public filter?: DatasetFilter; /** * Creates an instance of ZoweDatasetNode @@ -77,8 +93,17 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod if (icon) { this.iconPath = icon.path; } - if (!globals.ISTHEIA && this.getParent() && contextually.isSession(this.getParent())) { - this.id = `${mParent?.id ?? mParent?.label?.toString() ?? ""}.${this.label as string}`; + + if (this.getParent() == null) { + // set default sort options for session nodes + this.sort = { + method: DatasetSortOpts.Name, + direction: SortDirection.Ascending, + }; + } + + if (!globals.ISTHEIA && contextually.isSession(this)) { + this.id = this.label as string; } } @@ -235,6 +260,21 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod msg: localize("getChildren.invalidMember", "Cannot access member with control characters in the name: {0}", item.member), }); } + + // get user and last modified date for sorting, if available + if ("m4date" in item) { + const { m4date, mtime, msec }: { m4date: string; mtime: string; msec: string } = item; + temp.stats = { + user: item.user, + modifiedDate: dayjs(`${m4date} ${mtime}:${msec}`).toDate(), + }; + } else if ("id" in item || "changed" in item) { + // missing keys from API response; check for FTP keys + temp.stats = { + user: item.id, + modifiedDate: item.changed ? dayjs(item.changed).toDate() : null, + }; + } elementChildren[temp.label.toString()] = temp; } } @@ -253,16 +293,87 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod ]; } else { const newChildren = Object.keys(elementChildren) - .sort() .filter((label) => this.children.find((c) => (c.label as string) === label) == null) .map((label) => elementChildren[label]); - this.children = this.children.concat(newChildren).filter((c) => (c.label as string) in elementChildren); + // get sort settings for session + const sessionSort = contextually.isSession(this) ? this.sort : this.getSessionNode().sort; + + // use the PDS sort settings if defined; otherwise, use session sort method + const sortOpts = this.sort ?? sessionSort; + + // use the PDS filter if one is set, otherwise try using the session filter + const sessionFilter = contextually.isSession(this) ? this.filter : this.getSessionNode().filter; + const filter = this.filter ?? sessionFilter; + + this.children = this.children + .concat(newChildren) + .filter((c) => (c.label as string) in elementChildren) + .filter(filter ? ZoweDatasetNode.filterBy(filter) : (_c): boolean => true) + .sort(ZoweDatasetNode.sortBy(sortOpts)); } return this.children; } + /** + * Returns a sorting function based on the given sorting method. + * If the nodes are not PDS members, it will simply sort by name. + * @param method The sorting method to use + * @returns A function that sorts 2 nodes based on the given sorting method + */ + public static sortBy(sort: NodeSort): (a: IZoweDatasetTreeNode, b: IZoweDatasetTreeNode) => number { + return (a, b): number => { + const aParent = a.getParent(); + if (aParent == null || !contextually.isPds(aParent)) { + return (a.label as string) < (b.label as string) ? -1 : 1; + } + + const sortLessThan = sort.direction == SortDirection.Ascending ? -1 : 1; + const sortGreaterThan = sortLessThan * -1; + + switch (sort.method) { + case DatasetSortOpts.LastModified: + return a.stats?.modifiedDate < b.stats?.modifiedDate ? sortLessThan : sortGreaterThan; + case DatasetSortOpts.UserId: + return a.stats?.user < b.stats?.user ? sortLessThan : sortGreaterThan; + case DatasetSortOpts.Name: + default: + return (a.label as string) < (b.label as string) ? sortLessThan : sortGreaterThan; + } + }; + } + + /** + * Returns a filter function based on the given method. + * If the nodes are not PDS members, it will not filter those nodes. + * @param method The sorting method to use + * @returns A function that sorts 2 nodes based on the given sorting method + */ + public static filterBy(filter: DatasetFilter): (node: IZoweDatasetTreeNode) => boolean { + const isDateFilter = (f: string): boolean => { + return dayjs(f).isValid(); + }; + + return (node): boolean => { + const aParent = node.getParent(); + if (aParent == null || !contextually.isPds(aParent)) { + return true; + } + + switch (filter.method) { + case DatasetFilterOpts.LastModified: + if (!isDateFilter(filter.value)) { + return true; + } + + return dayjs(node.stats?.modifiedDate).isSame(filter.value, "day"); + case DatasetFilterOpts.UserId: + return node.stats?.user === filter.value; + } + }; + } + public getSessionNode(): IZoweDatasetTreeNode { ZoweLogger.trace("ZoweDatasetNode.getSessionNode called."); return this.getParent() ? this.getParent().getSessionNode() : this; diff --git a/packages/zowe-explorer/src/dataset/init.ts b/packages/zowe-explorer/src/dataset/init.ts index 12e81d74eb..01ffc33e0f 100644 --- a/packages/zowe-explorer/src/dataset/init.ts +++ b/packages/zowe-explorer/src/dataset/init.ts @@ -25,7 +25,7 @@ import { TreeViewUtils } from "../utils/TreeViewUtils"; export async function initDatasetProvider(context: vscode.ExtensionContext): Promise> { ZoweLogger.trace("dataset.init.initDatasetProvider called."); - const datasetProvider: IZoweTree = await createDatasetTree(globals.LOG); + const datasetProvider = await createDatasetTree(globals.LOG); if (datasetProvider == null) { return null; } @@ -37,10 +37,10 @@ export async function initDatasetProvider(context: vscode.ExtensionContext): Pro ); context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.addSession", async () => datasetProvider.createZoweSession(datasetProvider))); context.subscriptions.push( - vscode.commands.registerCommand("zowe.ds.addFavorite", (node, nodeList) => { + vscode.commands.registerCommand("zowe.ds.addFavorite", async (node, nodeList) => { const selectedNodes = getSelectedNodeList(node, nodeList); for (const item of selectedNodes) { - datasetProvider.addFavorite(item); + await datasetProvider.addFavorite(item); } }) ); @@ -67,7 +67,7 @@ export async function initDatasetProvider(context: vscode.ExtensionContext): Pro } }) ); - context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.pattern", (node): void => datasetProvider.filterPrompt(node))); + context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.pattern", (node) => datasetProvider.filterPrompt(node))); context.subscriptions.push( vscode.commands.registerCommand("zowe.ds.editSession", async (node) => datasetProvider.editSession(node, datasetProvider)) ); @@ -126,10 +126,10 @@ export async function initDatasetProvider(context: vscode.ExtensionContext): Pro } }) ); - context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.saveSearch", (node): void => datasetProvider.addFavorite(node))); - context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.removeSavedSearch", (node): void => datasetProvider.removeFavorite(node))); + context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.saveSearch", (node) => datasetProvider.addFavorite(node))); + context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.removeSavedSearch", (node) => datasetProvider.removeFavorite(node))); context.subscriptions.push( - vscode.commands.registerCommand("zowe.ds.removeFavProfile", (node): void => datasetProvider.removeFavProfile(node.label, true)) + vscode.commands.registerCommand("zowe.ds.removeFavProfile", (node) => datasetProvider.removeFavProfile(node.label, true)) ); context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.submitJcl", async () => dsActions.submitJcl(datasetProvider))); context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.submitMember", async (node) => dsActions.submitMember(node))); @@ -143,7 +143,7 @@ export async function initDatasetProvider(context: vscode.ExtensionContext): Pro } }) ); - context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.renameDataSet", (node): void => datasetProvider.rename(node))); + context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.renameDataSet", (node) => datasetProvider.rename(node))); context.subscriptions.push( vscode.commands.registerCommand("zowe.ds.copyDataSets", async (node, nodeList) => dsActions.copyDataSets(node, nodeList, datasetProvider)) ); @@ -156,7 +156,7 @@ export async function initDatasetProvider(context: vscode.ExtensionContext): Pro await dsActions.refreshDataset(node.getParent(), datasetProvider); }) ); - context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.renameDataSetMember", (node): void => datasetProvider.rename(node))); + context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.renameDataSetMember", (node) => datasetProvider.rename(node))); context.subscriptions.push( vscode.commands.registerCommand("zowe.ds.hMigrateDataSet", async (node, nodeList) => { let selectedNodes = getSelectedNodeList(node, nodeList); @@ -196,11 +196,20 @@ export async function initDatasetProvider(context: vscode.ExtensionContext): Pro datasetProvider.refreshElement(node); }) ); - context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.ssoLogin", (node: IZoweTreeNode): void => datasetProvider.ssoLogin(node))); - context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.ssoLogout", (node: IZoweTreeNode): void => datasetProvider.ssoLogout(node))); + context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.ssoLogin", (node: IZoweTreeNode) => datasetProvider.ssoLogin(node))); + context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.ssoLogout", (node: IZoweTreeNode) => datasetProvider.ssoLogout(node))); context.subscriptions.push( - vscode.workspace.onDidChangeConfiguration((e) => { - datasetProvider.onDidChangeConfiguration(e); + vscode.commands.registerCommand("zowe.ds.sortBy", async (node: IZoweDatasetTreeNode) => datasetProvider.sortPdsMembersDialog(node)) + ); + context.subscriptions.push( + vscode.commands.registerCommand( + "zowe.ds.filterBy", + async (node: IZoweDatasetTreeNode): Promise => datasetProvider.filterPdsMembersDialog(node) + ) + ); + context.subscriptions.push( + vscode.workspace.onDidChangeConfiguration(async (e) => { + await datasetProvider.onDidChangeConfiguration(e); }) ); diff --git a/packages/zowe-explorer/src/dataset/utils.ts b/packages/zowe-explorer/src/dataset/utils.ts index 1debf45d39..956ae28f31 100644 --- a/packages/zowe-explorer/src/dataset/utils.ts +++ b/packages/zowe-explorer/src/dataset/utils.ts @@ -10,9 +10,26 @@ */ import * as globals from "../globals"; +import * as nls from "vscode-nls"; import { IZoweNodeType } from "@zowe/zowe-explorer-api"; import { ZoweLogger } from "../utils/LoggerUtils"; +// Set up localization +nls.config({ + messageFormat: nls.MessageFormat.bundle, + bundleFormat: nls.BundleFormat.standalone, +})(); +const localize: nls.LocalizeFunc = nls.loadMessageBundle(); + +export const DATASET_SORT_OPTS = [ + localize("ds.sortByName", "$(case-sensitive) Name (default)"), + localize("ds.sortByModified", "$(calendar) Date Modified"), + localize("ds.sortByUserId", "$(account) User ID"), + localize("setSortDirection", "$(fold) Sort Direction"), +]; + +export const DATASET_FILTER_OPTS = [localize("ds.sortByModified", "$(calendar) Date Modified"), localize("ds.sortByUserId", "$(account) User ID")]; + export function getProfileAndDataSetName(node: IZoweNodeType): { profileName: string; dataSetName: string; diff --git a/packages/zowe-explorer/src/globals.ts b/packages/zowe-explorer/src/globals.ts index 3d3d338c62..3624a6a84e 100644 --- a/packages/zowe-explorer/src/globals.ts +++ b/packages/zowe-explorer/src/globals.ts @@ -35,7 +35,7 @@ export let DS_DIR: string; export let CONFIG_PATH; // set during activate export let ISTHEIA = false; // set during activate export let LOG: imperative.Logger; -export const COMMAND_COUNT = 114; +export const COMMAND_COUNT = 115; export const MAX_SEARCH_HISTORY = 5; export const MAX_FILE_HISTORY = 10; export const MS_PER_SEC = 1000; @@ -279,6 +279,7 @@ export enum JobPickerTypes { export const SEPARATORS = { BLANK: { kind: vscode.QuickPickItemKind.Separator, label: "" }, RECENT_FILTERS: { kind: vscode.QuickPickItemKind.Separator, label: localize("zowe.separator.recentFilters", "Recent Filters") }, + OPTIONS: { kind: vscode.QuickPickItemKind.Separator, label: localize("zowe.separator.options", "Options") }, }; /** diff --git a/packages/zowe-explorer/src/job/ZosJobsProvider.ts b/packages/zowe-explorer/src/job/ZosJobsProvider.ts index 3333724e6f..1da57d443b 100644 --- a/packages/zowe-explorer/src/job/ZosJobsProvider.ts +++ b/packages/zowe-explorer/src/job/ZosJobsProvider.ts @@ -159,10 +159,9 @@ export class ZosJobsProvider extends ZoweTreeProvider implements IZoweTree"}.${this.label as string}`; + if (contextually.isSession(this)) { + this.sort = { + method: JobSortOpts.Id, + direction: SortDirection.Ascending, + }; + if (!globals.ISTHEIA) { + this.id = this.label as string; + } } } @@ -225,26 +233,40 @@ export class Job extends ZoweTreeNode implements IZoweJobTreeNode { // Only add new children that are not in the list of existing child nodes const newChildren = Object.values(elementChildren).filter((c) => this.children.find((ch) => ch.label === c.label) == null); + const sortMethod = contextually.isSession(this) ? this.sort : { method: JobSortOpts.Id, direction: SortDirection.Ascending }; // Remove any children that are no longer present in the built record this.children = this.children .concat(newChildren) .filter((ch) => Object.values(elementChildren).find((recordCh) => recordCh.label === ch.label) != null) - .sort((a, b) => Job.sortJobs(a, b)); + .sort(Job.sortJobs(sortMethod)); this.dirty = false; return this.children; } - public static sortJobs(a: IZoweJobTreeNode, b: IZoweJobTreeNode): number { - if (a.job.jobid > b.job.jobid) { - return 1; - } + public static sortJobs(sortOpts: NodeSort): (x: IZoweJobTreeNode, y: IZoweJobTreeNode) => number { + return (x, y) => { + const sortLessThan = sortOpts.direction == SortDirection.Ascending ? -1 : 1; + const sortGreaterThan = sortLessThan * -1; + + const keyToSortBy = JOB_SORT_KEYS[sortOpts.method]; + let xCompare, yCompare; + if (keyToSortBy === "retcode") { + // some jobs (such as active ones) will have a null retcode + // in this case, use status as the key to compare for that node only + xCompare = x.job["retcode"] ?? x.job["status"]; + yCompare = y.job["retcode"] ?? y.job["status"]; + } else { + xCompare = x.job[keyToSortBy]; + yCompare = y.job[keyToSortBy]; + } - if (a.job.jobid < b.job.jobid) { - return -1; - } + if (xCompare === yCompare) { + return x.job["jobid"] > y.job["jobid"] ? sortGreaterThan : sortLessThan; + } - return 0; + return xCompare > yCompare ? sortGreaterThan : sortLessThan; + }; } public getSessionNode(): IZoweJobTreeNode { diff --git a/packages/zowe-explorer/src/job/actions.ts b/packages/zowe-explorer/src/job/actions.ts index b7dd81ef9e..305f3c247a 100644 --- a/packages/zowe-explorer/src/job/actions.ts +++ b/packages/zowe-explorer/src/job/actions.ts @@ -14,12 +14,15 @@ import * as zowe from "@zowe/cli"; import { errorHandling } from "../utils/ProfilesUtils"; import { Profiles } from "../Profiles"; import { ZoweExplorerApiRegister } from "../ZoweExplorerApiRegister"; -import { Gui, IZoweTree, IZoweJobTreeNode } from "@zowe/zowe-explorer-api"; +import { Gui, IZoweTree, IZoweJobTreeNode, JobSortOpts } from "@zowe/zowe-explorer-api"; import { Job, Spool } from "./ZoweJobNode"; import * as nls from "vscode-nls"; import SpoolProvider, { encodeJobFile, getSpoolFiles, matchSpool } from "../SpoolProvider"; import { ZoweLogger } from "../utils/LoggerUtils"; -import { getDefaultUri } from "../shared/utils"; +import { SORT_DIRS, getDefaultUri } from "../shared/utils"; +import { ZosJobsProvider } from "./ZosJobsProvider"; +import { JOB_SORT_OPTS } from "./utils"; +import * as globals from "../globals"; // Set up localization nls.config({ @@ -528,16 +531,54 @@ export async function cancelJobs(jobsProvider: IZoweTree, node await Gui.showMessage(localize("cancelJobs.succeeded", "Cancelled selected jobs successfully.")); } } -export async function sortJobsBy(jobs: IZoweJobTreeNode, jobsProvider: IZoweTree, key: keyof zowe.IJob): Promise { - if (jobs["children"].length == 0) { - await vscode.window.showInformationMessage("No jobs are present in the profile."); +export async function sortJobs(session: IZoweJobTreeNode, jobsProvider: ZosJobsProvider): Promise { + const selection = await Gui.showQuickPick( + JOB_SORT_OPTS.map((sortOpt, i) => ({ + label: i === session.sort.method ? `${sortOpt} $(check)` : sortOpt, + description: i === JOB_SORT_OPTS.length - 1 ? SORT_DIRS[session.sort.direction] : null, + })), + { + placeHolder: localize("jobs.selectSortOpt", "Select a sorting option for jobs in {0}", session.label as string), + } + ); + if (selection == null) { + return; } - jobs["children"].sort((x, y) => { - if (key !== "jobid" && x["job"][key] == y["job"][key]) { - return x["job"]["jobid"] > y["job"]["jobid"] ? 1 : -1; - } else { - return x["job"][key] > y["job"][key] ? 1 : -1; + if (selection.label === localize("setSortDirection", "$(fold) Sort Direction")) { + const dir = await Gui.showQuickPick(SORT_DIRS, { + placeHolder: localize("sort.selectDirection", "Select a sorting direction"), + }); + if (dir != null) { + session.sort = { + ...(session.sort ?? { method: JobSortOpts.Id }), + direction: SORT_DIRS.indexOf(dir), + }; } + await sortJobs(session, jobsProvider); + return; + } + + session.sort.method = JOB_SORT_OPTS.indexOf(selection.label.replace(" $(check)", "")); + jobsProvider.sortBy(session); + Gui.setStatusBarMessage(localize("sort.updated", "$(check) Sorting updated for {0}", session.label as string), globals.MS_PER_SEC * 4); +} + +export async function filterJobs(jobsProvider: IZoweTree, job: IZoweJobTreeNode): Promise { + if (job.collapsibleState === vscode.TreeItemCollapsibleState.Collapsed) { + Gui.infoMessage(localize("filterJobs.message", "Use the search button to display jobs")); + return; + } + const acutal_jobs = job["children"]; + const inputBox = await vscode.window.createInputBox(); + inputBox.placeholder = localize("filterJobs.prompt.message", "Enter local filter..."); + inputBox.onDidChangeValue((query) => { + query = query.toUpperCase(); + job["children"] = acutal_jobs.filter((item) => `${item["job"].jobname}(${item["job"].jobid}) - ${item["job"].retcode}`.includes(query)); + jobsProvider.refresh(); + }); + inputBox.onDidAccept(() => { + inputBox.hide(); }); - jobsProvider.refresh(); + inputBox.show(); + return inputBox; } diff --git a/packages/zowe-explorer/src/job/init.ts b/packages/zowe-explorer/src/job/init.ts index 55cb416fa2..991dea18ca 100644 --- a/packages/zowe-explorer/src/job/init.ts +++ b/packages/zowe-explorer/src/job/init.ts @@ -24,7 +24,7 @@ import { ZoweLogger } from "../utils/LoggerUtils"; export async function initJobsProvider(context: vscode.ExtensionContext): Promise> { ZoweLogger.trace("job.init.initJobsProvider called."); - const jobsProvider: IZoweTree = await createJobsTree(globals.LOG); + const jobsProvider = await createJobsTree(globals.LOG); if (jobsProvider == null) { return null; } @@ -103,7 +103,7 @@ export async function initJobsProvider(context: vscode.ExtensionContext): Promis context.subscriptions.push( vscode.commands.registerCommand("zowe.jobs.setJobSpool", async (session, jobId) => jobActions.focusOnJob(jobsProvider, session, jobId)) ); - context.subscriptions.push(vscode.commands.registerCommand("zowe.jobs.search", (node): void => jobsProvider.filterPrompt(node))); + context.subscriptions.push(vscode.commands.registerCommand("zowe.jobs.search", async (node): Promise => jobsProvider.filterPrompt(node))); context.subscriptions.push( vscode.commands.registerCommand("zowe.jobs.editSession", async (node): Promise => jobsProvider.editSession(node, jobsProvider)) ); @@ -124,9 +124,11 @@ export async function initJobsProvider(context: vscode.ExtensionContext): Promis }) ); context.subscriptions.push(vscode.commands.registerCommand("zowe.jobs.saveSearch", (node): void => jobsProvider.saveSearch(node))); - context.subscriptions.push(vscode.commands.registerCommand("zowe.jobs.removeSearchFavorite", (node): void => jobsProvider.removeFavorite(node))); context.subscriptions.push( - vscode.commands.registerCommand("zowe.jobs.removeFavProfile", (node): void => jobsProvider.removeFavProfile(node.label, true)) + vscode.commands.registerCommand("zowe.jobs.removeSearchFavorite", async (node): Promise => jobsProvider.removeFavorite(node)) + ); + context.subscriptions.push( + vscode.commands.registerCommand("zowe.jobs.removeFavProfile", async (node): Promise => jobsProvider.removeFavProfile(node.label, true)) ); context.subscriptions.push( vscode.commands.registerCommand("zowe.jobs.disableValidation", (node) => { @@ -140,8 +142,8 @@ export async function initJobsProvider(context: vscode.ExtensionContext): Promis jobsProvider.refreshElement(node); }) ); - context.subscriptions.push(vscode.commands.registerCommand("zowe.jobs.ssoLogin", (node: IZoweTreeNode): void => jobsProvider.ssoLogin(node))); - context.subscriptions.push(vscode.commands.registerCommand("zowe.jobs.ssoLogout", (node: IZoweTreeNode): void => jobsProvider.ssoLogout(node))); + context.subscriptions.push(vscode.commands.registerCommand("zowe.jobs.ssoLogin", async (node): Promise => jobsProvider.ssoLogin(node))); + context.subscriptions.push(vscode.commands.registerCommand("zowe.jobs.ssoLogout", async (node): Promise => jobsProvider.ssoLogout(node))); const spoolFileTogglePoll = (startPolling: boolean) => async (node: IZoweTreeNode, nodeList: IZoweTreeNode[]): Promise => { @@ -160,8 +162,8 @@ export async function initJobsProvider(context: vscode.ExtensionContext): Promis context.subscriptions.push(vscode.commands.registerCommand("zowe.jobs.startPolling", spoolFileTogglePoll(true))); context.subscriptions.push(vscode.commands.registerCommand("zowe.jobs.stopPolling", spoolFileTogglePoll(false))); context.subscriptions.push( - vscode.workspace.onDidChangeConfiguration((e) => { - jobsProvider.onDidChangeConfiguration(e); + vscode.workspace.onDidChangeConfiguration(async (e) => { + await jobsProvider.onDidChangeConfiguration(e); }) ); context.subscriptions.push( @@ -169,11 +171,13 @@ export async function initJobsProvider(context: vscode.ExtensionContext): Promis await jobActions.cancelJobs(jobsProvider, getSelectedNodeList(node, nodeList)); }) ); - context.subscriptions.push(vscode.commands.registerCommand("zowe.jobs.sortbyname", (job) => jobActions.sortJobsBy(job, jobsProvider, "jobname"))); - context.subscriptions.push(vscode.commands.registerCommand("zowe.jobs.sortbyid", (job) => jobActions.sortJobsBy(job, jobsProvider, "jobid"))); + context.subscriptions.push(vscode.commands.registerCommand("zowe.jobs.sortBy", async (job) => jobActions.sortJobs(job, jobsProvider))); context.subscriptions.push( - vscode.commands.registerCommand("zowe.jobs.sortbyreturncode", (job) => jobActions.sortJobsBy(job, jobsProvider, "retcode")) + vscode.commands.registerCommand("zowe.jobs.filterJobs", async (job) => { + await jobActions.filterJobs(jobsProvider, job); + }) ); + initSubscribers(context, jobsProvider); return jobsProvider; } diff --git a/packages/zowe-explorer/src/job/utils.ts b/packages/zowe-explorer/src/job/utils.ts index 9a5f3d7611..9747139144 100644 --- a/packages/zowe-explorer/src/job/utils.ts +++ b/packages/zowe-explorer/src/job/utils.ts @@ -9,8 +9,33 @@ * */ +import { JobSortOpts } from "@zowe/zowe-explorer-api"; import { ZoweLogger } from "../utils/LoggerUtils"; import { FilterItem } from "../utils/ProfilesUtils"; +import * as nls from "vscode-nls"; +import { IJob } from "@zowe/cli"; + +// Set up localization +nls.config({ + messageFormat: nls.MessageFormat.bundle, + bundleFormat: nls.BundleFormat.standalone, +})(); +const localize: nls.LocalizeFunc = nls.loadMessageBundle(); + +export const JOB_SORT_OPTS = [ + localize("jobs.sortById", "$(list-ordered) Job ID (default)"), + localize("jobs.sortByDateSubmitted", "$(calendar) Date Submitted"), + localize("jobs.sortByName", "$(case-sensitive) Job Name"), + localize("jobs.sortByReturnCode", "$(symbol-numeric) Return Code"), + localize("setSortDirection", "$(fold) Sort Direction"), +]; + +export const JOB_SORT_KEYS: Record = { + [JobSortOpts.Id]: "jobid", + [JobSortOpts.DateSubmitted]: "exec-submitted", + [JobSortOpts.Name]: "jobname", + [JobSortOpts.ReturnCode]: "retcode", +}; export async function resolveQuickPickHelper(quickpick): Promise { ZoweLogger.trace("job.utils.resolveQuickPickHelper called."); diff --git a/packages/zowe-explorer/src/shared/utils.ts b/packages/zowe-explorer/src/shared/utils.ts index 354c9a6732..af0ce5a45c 100644 --- a/packages/zowe-explorer/src/shared/utils.ts +++ b/packages/zowe-explorer/src/shared/utils.ts @@ -45,6 +45,8 @@ export const JOB_SUBMIT_DIALOG_OPTS = [ localize("zowe.jobs.confirmSubmission.allJobs", "All jobs"), ]; +export const SORT_DIRS: string[] = [localize("sort.asc", "Ascending"), localize("sort.desc", "Descending")]; + export function filterTreeByString(value: string, treeItems: vscode.QuickPickItem[]): vscode.QuickPickItem[] { ZoweLogger.trace("shared.utils.filterTreeByString called."); const filteredArray: vscode.QuickPickItem[] = []; diff --git a/yarn.lock b/yarn.lock index 08e9d1f09a..d0f1c2792c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -139,7 +139,7 @@ "@jridgewell/gen-mapping" "^0.1.0" jsesc "^2.5.1" -"@babel/generator@^7.20.1", "@babel/generator@^7.20.2", "@babel/generator@^7.7.2": +"@babel/generator@^7.20.2", "@babel/generator@^7.7.2": version "7.20.4" resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.20.4.tgz#4d9f8f0c30be75fd90a0562099a26e5839602ab8" integrity sha512-luCf7yk/cm7yab6CAW1aiFnmEfBJplb/JojV56MYEK7ziWfGmFlTfmL9Ehwfy4gFhbjBfWO1wj7/TuSbVNEEtA== @@ -278,14 +278,6 @@ "@babel/template" "^7.16.7" "@babel/types" "^7.17.0" -"@babel/helper-function-name@^7.19.0": - version "7.19.0" - resolved "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz#941574ed5390682e872e52d3f38ce9d1bef4648c" - integrity sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w== - dependencies: - "@babel/template" "^7.18.10" - "@babel/types" "^7.19.0" - "@babel/helper-function-name@^7.23.0": version "7.23.0" resolved "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" @@ -301,13 +293,6 @@ dependencies: "@babel/types" "^7.16.7" -"@babel/helper-hoist-variables@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" - integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q== - dependencies: - "@babel/types" "^7.18.6" - "@babel/helper-hoist-variables@^7.22.5": version "7.22.5" resolved "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" @@ -582,7 +567,7 @@ resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.17.10.tgz#873b16db82a8909e0fbd7f115772f4b739f6ce78" integrity sha512-n2Q6i+fnJqzOaq2VkdXxy2TCPCWQZHiCo0XqmrCvDWcZQKRyZzYi4Z0yxlBuN0w+r2ZHmre+Q087DSrw3pbJDQ== -"@babel/parser@^7.18.10", "@babel/parser@^7.20.1", "@babel/parser@^7.20.2": +"@babel/parser@^7.18.10", "@babel/parser@^7.20.2": version "7.20.3" resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.20.3.tgz#5358cf62e380cf69efcb87a7bb922ff88bfac6e2" integrity sha512-OP/s5a94frIPXwjzEcv5S/tpQfc6XhxYUnmWpgdqMWGgYCuErA3SzozaRAMQgSZWKeTJxht9aWAkUY+0UzvOFg== @@ -1287,42 +1272,10 @@ "@babel/parser" "^7.22.15" "@babel/types" "^7.22.15" -"@babel/traverse@^7.13.0", "@babel/traverse@^7.16.7", "@babel/traverse@^7.16.8", "@babel/traverse@^7.17.10", "@babel/traverse@^7.17.3", "@babel/traverse@^7.17.9": - version "7.17.10" - resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.10.tgz#1ee1a5ac39f4eac844e6cf855b35520e5eb6f8b5" - integrity sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw== - dependencies: - "@babel/code-frame" "^7.16.7" - "@babel/generator" "^7.17.10" - "@babel/helper-environment-visitor" "^7.16.7" - "@babel/helper-function-name" "^7.17.9" - "@babel/helper-hoist-variables" "^7.16.7" - "@babel/helper-split-export-declaration" "^7.16.7" - "@babel/parser" "^7.17.10" - "@babel/types" "^7.17.10" - debug "^4.1.0" - globals "^11.1.0" - -"@babel/traverse@^7.20.1", "@babel/traverse@^7.7.2": - version "7.20.1" - resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.1.tgz#9b15ccbf882f6d107eeeecf263fbcdd208777ec8" - integrity sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA== - dependencies: - "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.1" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.19.0" - "@babel/helper-hoist-variables" "^7.18.6" - "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.20.1" - "@babel/types" "^7.20.0" - debug "^4.1.0" - globals "^11.1.0" - -"@babel/traverse@^7.23.0": - version "7.23.0" - resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.0.tgz#18196ddfbcf4ccea324b7f6d3ada00d8c5a99c53" - integrity sha512-t/QaEvyIoIkwzpiZ7aoSKK8kObQYeF7T2v+dazAYCb8SXtp58zEVkWW7zAnju8FNKNdr4ScAOEDmMItbyOmEYw== +"@babel/traverse@^7.13.0", "@babel/traverse@^7.16.7", "@babel/traverse@^7.16.8", "@babel/traverse@^7.17.10", "@babel/traverse@^7.17.3", "@babel/traverse@^7.17.9", "@babel/traverse@^7.20.1", "@babel/traverse@^7.23.0", "@babel/traverse@^7.7.2": + version "7.23.2" + resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" + integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw== dependencies: "@babel/code-frame" "^7.22.13" "@babel/generator" "^7.23.0" @@ -1343,7 +1296,7 @@ "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" -"@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.19.0", "@babel/types@^7.20.0", "@babel/types@^7.20.2": +"@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.20.0", "@babel/types@^7.20.2": version "7.20.2" resolved "https://registry.npmjs.org/@babel/types/-/types-7.20.2.tgz#67ac09266606190f496322dbaff360fdaa5e7842" integrity sha512-FnnvsNWgZCr232sqtXggapvlkk/tuwR/qhGzcmxI0GXLCjmPYQPzio2FbdlWuY6y1sHFfQKk+rRbUZ9VStQMog== @@ -4573,6 +4526,11 @@ dateformat@3.0.2: resolved "https://registry.npmjs.org/dateformat/-/dateformat-3.0.2.tgz#9a4df4bff158ac2f34bc637abdb15471607e1659" integrity sha1-mk30v/FYrC80vGN6vbFUcWB+Flk= +dayjs@^1.11.10: + version "1.11.10" + resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0" + integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ== + debug-fabulous@1.X: version "1.1.0" resolved "https://registry.npmjs.org/debug-fabulous/-/debug-fabulous-1.1.0.tgz#af8a08632465224ef4174a9f06308c3c2a1ebc8e"