From 891b79d8b58f3011eb755135062749cad561d1d2 Mon Sep 17 00:00:00 2001 From: az10az Date: Sun, 22 Feb 2026 11:48:57 -0800 Subject: [PATCH] Add Collapse All button to Bundle Resource Explorer - Use createTreeView with showCollapseAll: true instead of registerTreeDataProvider so the Collapse All button appears in the tree view title bar (fixes #1698) - Default ResourceTypeHeaderTreeNode, TaskHeaderTreeNode, and getCollapsibleState to Collapsed so all nodes start folded - Add unit tests for the three changed modules --- packages/databricks-vscode/src/extension.ts | 8 +- .../ResourceTypeHeaderTreeNode.test.ts | 115 +++++++++++++++++ .../ResourceTypeHeaderTreeNode.ts | 2 +- .../TaskHeaderTreeNode.test.ts | 119 ++++++++++++++++++ .../TaskHeaderTreeNode.ts | 2 +- .../src/ui/utils/DecorationUtils.test.ts | 47 +++++++ .../src/ui/utils/DecorationUtils.ts | 6 +- 7 files changed, 288 insertions(+), 11 deletions(-) create mode 100644 packages/databricks-vscode/src/ui/bundle-resource-explorer/ResourceTypeHeaderTreeNode.test.ts create mode 100644 packages/databricks-vscode/src/ui/bundle-resource-explorer/TaskHeaderTreeNode.test.ts create mode 100644 packages/databricks-vscode/src/ui/utils/DecorationUtils.test.ts diff --git a/packages/databricks-vscode/src/extension.ts b/packages/databricks-vscode/src/extension.ts index 4713b02eb..9077e23a4 100644 --- a/packages/databricks-vscode/src/extension.ts +++ b/packages/databricks-vscode/src/extension.ts @@ -668,10 +668,10 @@ export async function activate( bundlePipelinesManager, decorationProvider, window.registerFileDecorationProvider(decorationProvider), - window.registerTreeDataProvider( - "dabsResourceExplorerView", - bundleResourceExplorerTreeDataProvider - ), + window.createTreeView("dabsResourceExplorerView", { + treeDataProvider: bundleResourceExplorerTreeDataProvider, + showCollapseAll: true, + }), telemetry.registerCommand( "databricks.bundle.refreshRemoteState", bundleCommands.refreshCommand, diff --git a/packages/databricks-vscode/src/ui/bundle-resource-explorer/ResourceTypeHeaderTreeNode.test.ts b/packages/databricks-vscode/src/ui/bundle-resource-explorer/ResourceTypeHeaderTreeNode.test.ts new file mode 100644 index 000000000..c73a0b418 --- /dev/null +++ b/packages/databricks-vscode/src/ui/bundle-resource-explorer/ResourceTypeHeaderTreeNode.test.ts @@ -0,0 +1,115 @@ +import * as assert from "assert"; +import {TreeItemCollapsibleState, ExtensionContext} from "vscode"; +import {ResourceTypeHeaderTreeNode} from "./ResourceTypeHeaderTreeNode"; +import {BundleResourceExplorerTreeNode} from "./types"; + +function createMockContext(): ExtensionContext { + return { + asAbsolutePath: (relativePath: string) => + `/mock/extension/${relativePath}`, + } as unknown as ExtensionContext; +} + +function createMockChild( + type: string = "jobs" +): BundleResourceExplorerTreeNode { + return { + type: type as any, + getTreeItem: () => ({label: "mock"}), + getChildren: () => [], + }; +} + +describe("ResourceTypeHeaderTreeNode", () => { + describe("getTreeItem", () => { + it("should return Collapsed collapsible state for 'jobs' header", () => { + const context = createMockContext(); + const children = [createMockChild("jobs")]; + const node = new ResourceTypeHeaderTreeNode( + context, + "jobs", + children + ); + + const treeItem = node.getTreeItem(); + + assert.strictEqual( + treeItem.collapsibleState, + TreeItemCollapsibleState.Collapsed + ); + }); + + it("should return Collapsed collapsible state for 'pipelines' header", () => { + const context = createMockContext(); + const children = [createMockChild("pipelines")]; + const node = new ResourceTypeHeaderTreeNode( + context, + "pipelines", + children + ); + + const treeItem = node.getTreeItem(); + + assert.strictEqual( + treeItem.collapsibleState, + TreeItemCollapsibleState.Collapsed + ); + }); + + it("should set the label to 'Jobs' for jobs resource type", () => { + const context = createMockContext(); + const children = [createMockChild("jobs")]; + const node = new ResourceTypeHeaderTreeNode( + context, + "jobs", + children + ); + + const treeItem = node.getTreeItem(); + + assert.strictEqual(treeItem.label, "Jobs"); + }); + + it("should set the label to 'Pipelines' for pipelines resource type", () => { + const context = createMockContext(); + const children = [createMockChild("pipelines")]; + const node = new ResourceTypeHeaderTreeNode( + context, + "pipelines", + children + ); + + const treeItem = node.getTreeItem(); + + assert.strictEqual(treeItem.label, "Pipelines"); + }); + }); + + describe("getChildren", () => { + it("should return the children passed in the constructor", () => { + const context = createMockContext(); + const child1 = createMockChild("jobs"); + const child2 = createMockChild("jobs"); + const node = new ResourceTypeHeaderTreeNode(context, "jobs", [ + child1, + child2, + ]); + + const children = node.getChildren(); + + assert.strictEqual(children.length, 2); + assert.strictEqual(children[0], child1); + assert.strictEqual(children[1], child2); + }); + + it("should set parent on children", () => { + const context = createMockContext(); + const child = createMockChild("jobs"); + const node = new ResourceTypeHeaderTreeNode(context, "jobs", [ + child, + ]); + + assert.strictEqual(child.parent, node); + }); + }); +}); diff --git a/packages/databricks-vscode/src/ui/bundle-resource-explorer/ResourceTypeHeaderTreeNode.ts b/packages/databricks-vscode/src/ui/bundle-resource-explorer/ResourceTypeHeaderTreeNode.ts index 108c94742..9127a5da7 100644 --- a/packages/databricks-vscode/src/ui/bundle-resource-explorer/ResourceTypeHeaderTreeNode.ts +++ b/packages/databricks-vscode/src/ui/bundle-resource-explorer/ResourceTypeHeaderTreeNode.ts @@ -67,7 +67,7 @@ export class ResourceTypeHeaderTreeNode label: humaniseResourceType(this.resourceType), iconPath: this.getIconPath(this.resourceType), contextValue: `${this.resourceType}-header`, - collapsibleState: TreeItemCollapsibleState.Expanded, + collapsibleState: TreeItemCollapsibleState.Collapsed, }; } diff --git a/packages/databricks-vscode/src/ui/bundle-resource-explorer/TaskHeaderTreeNode.test.ts b/packages/databricks-vscode/src/ui/bundle-resource-explorer/TaskHeaderTreeNode.test.ts new file mode 100644 index 000000000..ee56f9757 --- /dev/null +++ b/packages/databricks-vscode/src/ui/bundle-resource-explorer/TaskHeaderTreeNode.test.ts @@ -0,0 +1,119 @@ +import * as assert from "assert"; +import {TreeItemCollapsibleState} from "vscode"; +import {TaskHeaderTreeNode} from "./TaskHeaderTreeNode"; +import {BundleResourceExplorerTreeNode} from "./types"; +import {TaskTreeNode} from "./TaskTreeNode"; + +function createMockParent(): BundleResourceExplorerTreeNode { + return { + type: "jobs" as any, + getTreeItem: () => ({label: "mock-job"}), + getChildren: () => [], + }; +} + +function createMockTaskTreeNode(): TaskTreeNode { + return { + type: "task", + getTreeItem: () => ({label: "mock-task"}), + getChildren: () => [], + } as unknown as TaskTreeNode; +} + +describe("TaskHeaderTreeNode", () => { + describe("getTreeItem", () => { + it("should return Collapsed when there are children", () => { + const parent = createMockParent(); + const tasks = [createMockTaskTreeNode()]; + const node = new TaskHeaderTreeNode(tasks, parent); + + const treeItem = node.getTreeItem(); + + assert.strictEqual( + treeItem.collapsibleState, + TreeItemCollapsibleState.Collapsed + ); + }); + + it("should return Collapsed when there are multiple children", () => { + const parent = createMockParent(); + const tasks = [ + createMockTaskTreeNode(), + createMockTaskTreeNode(), + createMockTaskTreeNode(), + ]; + const node = new TaskHeaderTreeNode(tasks, parent); + + const treeItem = node.getTreeItem(); + + assert.strictEqual( + treeItem.collapsibleState, + TreeItemCollapsibleState.Collapsed + ); + }); + + it("should return None when there are no children", () => { + const parent = createMockParent(); + const node = new TaskHeaderTreeNode([], parent); + + const treeItem = node.getTreeItem(); + + assert.strictEqual( + treeItem.collapsibleState, + TreeItemCollapsibleState.None + ); + }); + + it("should set the label to 'Tasks'", () => { + const parent = createMockParent(); + const tasks = [createMockTaskTreeNode()]; + const node = new TaskHeaderTreeNode(tasks, parent); + + const treeItem = node.getTreeItem(); + + assert.strictEqual(treeItem.label, "Tasks"); + }); + + it("should set contextValue to 'task_header'", () => { + const parent = createMockParent(); + const tasks = [createMockTaskTreeNode()]; + const node = new TaskHeaderTreeNode(tasks, parent); + + const treeItem = node.getTreeItem(); + + assert.strictEqual(treeItem.contextValue, "task_header"); + }); + }); + + describe("getChildren", () => { + it("should return the task children", () => { + const parent = createMockParent(); + const task1 = createMockTaskTreeNode(); + const task2 = createMockTaskTreeNode(); + const node = new TaskHeaderTreeNode([task1, task2], parent); + + const children = node.getChildren(); + + assert.strictEqual(children.length, 2); + assert.strictEqual(children[0], task1); + assert.strictEqual(children[1], task2); + }); + + it("should return empty array when no children", () => { + const parent = createMockParent(); + const node = new TaskHeaderTreeNode([], parent); + + const children = node.getChildren(); + + assert.strictEqual(children.length, 0); + }); + + it("should set parent on task children to this node", () => { + const parent = createMockParent(); + const task = createMockTaskTreeNode(); + const node = new TaskHeaderTreeNode([task], parent); + + assert.strictEqual(task.parent, node); + }); + }); +}); diff --git a/packages/databricks-vscode/src/ui/bundle-resource-explorer/TaskHeaderTreeNode.ts b/packages/databricks-vscode/src/ui/bundle-resource-explorer/TaskHeaderTreeNode.ts index 26aebb913..ab1ac5d14 100644 --- a/packages/databricks-vscode/src/ui/bundle-resource-explorer/TaskHeaderTreeNode.ts +++ b/packages/databricks-vscode/src/ui/bundle-resource-explorer/TaskHeaderTreeNode.ts @@ -23,7 +23,7 @@ export class TaskHeaderTreeNode implements BundleResourceExplorerTreeNode { contextValue: "task_header", collapsibleState: this.children.length > 0 - ? TreeItemCollapsibleState.Expanded + ? TreeItemCollapsibleState.Collapsed : TreeItemCollapsibleState.None, }; } diff --git a/packages/databricks-vscode/src/ui/utils/DecorationUtils.test.ts b/packages/databricks-vscode/src/ui/utils/DecorationUtils.test.ts new file mode 100644 index 000000000..75976b106 --- /dev/null +++ b/packages/databricks-vscode/src/ui/utils/DecorationUtils.test.ts @@ -0,0 +1,47 @@ +import * as assert from "assert"; +import {TreeItemCollapsibleState} from "vscode"; +import {getCollapsibleState} from "./DecorationUtils"; + +describe("DecorationUtils", () => { + describe("getCollapsibleState", () => { + it("should return None when modifiedStatus is 'deleted'", () => { + const result = getCollapsibleState(false, "deleted"); + assert.strictEqual(result, TreeItemCollapsibleState.None); + }); + + it("should return None when modifiedStatus is 'deleted' even if running", () => { + const result = getCollapsibleState(true, "deleted"); + assert.strictEqual(result, TreeItemCollapsibleState.None); + }); + + it("should return Collapsed when isRunning is true and not deleted", () => { + const result = getCollapsibleState(true, undefined); + assert.strictEqual(result, TreeItemCollapsibleState.Collapsed); + }); + + it("should return Collapsed when isRunning is true with 'created' status", () => { + const result = getCollapsibleState(true, "created"); + assert.strictEqual(result, TreeItemCollapsibleState.Collapsed); + }); + + it("should return Collapsed when isRunning is true with 'updated' status", () => { + const result = getCollapsibleState(true, "updated"); + assert.strictEqual(result, TreeItemCollapsibleState.Collapsed); + }); + + it("should return Collapsed when not running and no modifiedStatus", () => { + const result = getCollapsibleState(false, undefined); + assert.strictEqual(result, TreeItemCollapsibleState.Collapsed); + }); + + it("should return Collapsed when not running with 'created' status", () => { + const result = getCollapsibleState(false, "created"); + assert.strictEqual(result, TreeItemCollapsibleState.Collapsed); + }); + + it("should return Collapsed when not running with 'updated' status", () => { + const result = getCollapsibleState(false, "updated"); + assert.strictEqual(result, TreeItemCollapsibleState.Collapsed); + }); + }); +}); diff --git a/packages/databricks-vscode/src/ui/utils/DecorationUtils.ts b/packages/databricks-vscode/src/ui/utils/DecorationUtils.ts index 206bc5c5f..2c5dc3899 100644 --- a/packages/databricks-vscode/src/ui/utils/DecorationUtils.ts +++ b/packages/databricks-vscode/src/ui/utils/DecorationUtils.ts @@ -40,9 +40,5 @@ export function getCollapsibleState( return TreeItemCollapsibleState.None; } - if (isRunning) { - return TreeItemCollapsibleState.Collapsed; - } - - return TreeItemCollapsibleState.Expanded; + return TreeItemCollapsibleState.Collapsed; }