Skip to content

Commit

Permalink
Merge pull request #2507 from zowe/feat/ds/sort
Browse files Browse the repository at this point in the history
feat(ds, jobs): Filter & sort PDS members; update UX for "Sort Jobs"
  • Loading branch information
JillieBeanSim authored Oct 17, 2023
2 parents ccccc65 + e0dda45 commit aaa67c9
Show file tree
Hide file tree
Showing 34 changed files with 942 additions and 132 deletions.
3 changes: 3 additions & 0 deletions packages/zowe-explorer-api/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions packages/zowe-explorer-api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
19 changes: 19 additions & 0 deletions packages/zowe-explorer-api/src/tree/IZoweTreeNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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.
*
Expand All @@ -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<DatasetStats>;
/**
* Filter method for this data set's children
*/
filter?: DatasetFilter;
/**
* Retrieves child nodes of this IZoweDatasetTreeNode
*
Expand Down
1 change: 1 addition & 0 deletions packages/zowe-explorer-api/src/tree/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*
*/

export * from "./sorting";
export * from "./ZoweExplorerTreeApi";
export * from "./ZoweTreeNode";
export * from "./IZoweTree";
Expand Down
43 changes: 43 additions & 0 deletions packages/zowe-explorer-api/src/tree/sorting.ts
Original file line number Diff line number Diff line change
@@ -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,
}
4 changes: 3 additions & 1 deletion packages/zowe-explorer/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
- Added "Sort Jobs" feature in Jobs tree view: accessible via sort icon or right-clicking on session node. [#2257](https://github.com/zowe/vscode-extension-for-zowe/issues/2257)
- Introduce a new user interface for managing profiles via right-click action "Manage Profile".
- Added new edit feature on `Edit Attributes` view for changing file tags on USS [#2113](https://github.com/zowe/vscode-extension-for-zowe/issues/2113)
- Added new API {ZE Extender MetaData} to allow extenders to have the metadata of registered extenders to aid in team configuration file creation from a view that isn't Zowe Explorer's. [#2394](https://github.com/zowe/vscode-extension-for-zowe/issues/2394)
- Added "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)

### Bug fixes

Expand Down
11 changes: 8 additions & 3 deletions packages/zowe-explorer/__tests__/__unit__/ZoweNode.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]);
Expand All @@ -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]);
});
Expand Down Expand Up @@ -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,
Expand All @@ -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();
});

/*************************************************************************************************************
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<string, IZoweDatasetTreeNode> => {
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<string, jest.SpyInstance> => ({
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);
});
});
});
Loading

0 comments on commit aaa67c9

Please sign in to comment.