From c3579e7d8d263d90810e3f85ae389677ed7b64a1 Mon Sep 17 00:00:00 2001 From: Daniel Banck Date: Mon, 5 Jun 2023 11:22:54 +0200 Subject: [PATCH] Implement workspace filtering by project (#1455) * Contribute new filter icon and command * Add search query parameter to projects endpoint * Implement project quick pick * Improve error messages --- package.json | 10 +++ src/providers/tfc/workspaceFilters.ts | 102 +++++++++++++++++++++++++ src/providers/tfc/workspaceProvider.ts | 29 ++++++- src/terraformCloud/project.ts | 15 +++- 4 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 src/providers/tfc/workspaceFilters.ts diff --git a/package.json b/package.json index f2a325624..a8f0e3f02 100644 --- a/package.json +++ b/package.json @@ -471,6 +471,11 @@ "command": "terraform.cloud.run.viewInBrowser", "title": "View Run", "icon": "$(globe)" + }, + { + "command": "terraform.cloud.workspaces.filterByProject", + "title": "Filter by Project", + "icon": "$(filter)" } ], "menus": { @@ -536,6 +541,11 @@ "when": "view == terraform.providers", "group": "navigation" }, + { + "command": "terraform.cloud.workspaces.filterByProject", + "when": "view == terraform.cloud.workspaces", + "group": "navigation" + }, { "command": "terraform.cloud.workspaces.refresh", "when": "view == terraform.cloud.workspaces", diff --git a/src/providers/tfc/workspaceFilters.ts b/src/providers/tfc/workspaceFilters.ts new file mode 100644 index 000000000..8f26640b4 --- /dev/null +++ b/src/providers/tfc/workspaceFilters.ts @@ -0,0 +1,102 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import * as vscode from 'vscode'; +import { apiClient } from '../../terraformCloud'; +import { Project } from '../../terraformCloud/project'; + +export class ResetProjectItem implements vscode.QuickPickItem { + get label() { + return '$(clear-all) Clear project filter. Show all workspaces'; + } + get description() { + return ''; + } + get alwaysShow() { + return true; + } +} + +class ProjectItem implements vscode.QuickPickItem { + constructor(protected project: Project) {} + get label() { + return this.project.attributes.name; + } + get description() { + return this.project.id; + } +} + +async function createProjectItems(organization: string, search?: string): Promise { + const projects = await apiClient.listProjects({ + params: { + organization_name: organization, + }, + // Include query parameter only if search argument is passed + ...(search && { + queries: { + q: search, + }, + }), + }); + + return projects.data.map((project) => new ProjectItem(project)); +} + +export class ProjectQuickPick { + private quickPick: vscode.QuickPick; + private fetchTimerKey: NodeJS.Timeout | undefined; + + constructor(private organizationName: string) { + this.quickPick = vscode.window.createQuickPick(); + this.quickPick.title = 'Filter Workspaces'; + this.quickPick.placeholder = 'Select a project (type to search)'; + this.quickPick.onDidChangeValue(this.onDidChangeValue, this); + } + + private onDidChangeValue() { + clearTimeout(this.fetchTimerKey); + // Only starts fetching projects after a user stopped typing for 300ms + this.fetchTimerKey = setTimeout(() => this.fetchProjects.apply(this), 300); + } + + private async fetchProjects() { + // TODO?: To further improve performance, we could consider throttling this function + const resetProjectItem = new ResetProjectItem(); + const picks: vscode.QuickPickItem[] = [resetProjectItem, { label: '', kind: vscode.QuickPickItemKind.Separator }]; + try { + this.quickPick.busy = true; + this.quickPick.show(); + + picks.push(...(await createProjectItems(this.organizationName, this.quickPick.value))); + } catch (error) { + let message = 'Failed to fetch projects'; + if (error instanceof Error) { + message = error.message; + } else if (typeof error === 'string') { + message = error; + } + + picks.push({ label: `$(error) Error: ${message}`, alwaysShow: true }); + console.error(error); + } finally { + this.quickPick.items = picks; + this.quickPick.busy = false; + } + } + + async pick() { + await this.fetchProjects(); + + const project = await new Promise((c) => { + this.quickPick.onDidAccept(() => c(this.quickPick.selectedItems[0])); + this.quickPick.onDidHide(() => c(undefined)); + this.quickPick.show(); + }); + this.quickPick.hide(); + + return project; + } +} diff --git a/src/providers/tfc/workspaceProvider.ts b/src/providers/tfc/workspaceProvider.ts index 15d0e9ade..08bc68b29 100644 --- a/src/providers/tfc/workspaceProvider.ts +++ b/src/providers/tfc/workspaceProvider.ts @@ -4,14 +4,17 @@ */ import * as vscode from 'vscode'; +import axios from 'axios'; + import { RunTreeDataProvider } from './runProvider'; import { apiClient } from '../../terraformCloud'; import { TerraformCloudAuthenticationProvider } from '../authenticationProvider'; -import axios from 'axios'; +import { ProjectQuickPick, ResetProjectItem } from './workspaceFilters'; export class WorkspaceTreeDataProvider implements vscode.TreeDataProvider, vscode.Disposable { private readonly didChangeTreeData = new vscode.EventEmitter(); public readonly onDidChangeTreeData = this.didChangeTreeData.event; + private projectFilter: string | undefined; constructor(private ctx: vscode.ExtensionContext, private runDataProvider: RunTreeDataProvider) { this.ctx.subscriptions.push( @@ -19,6 +22,7 @@ export class WorkspaceTreeDataProvider implements vscode.TreeDataProvider this.filterByProject()), ); } @@ -26,6 +30,20 @@ export class WorkspaceTreeDataProvider implements vscode.TreeDataProvider { + // TODO! only run this if user is logged in + const organization = this.ctx.workspaceState.get('terraform.cloud.organization', ''); + const projectQuickPick = new ProjectQuickPick(organization); + const project = await projectQuickPick.pick(); + + if (project === undefined || project instanceof ResetProjectItem) { + this.projectFilter = undefined; + } else { + this.projectFilter = project.description; + } + this.refresh(); + } + getTreeItem(element: WorkspaceTreeItem): vscode.TreeItem | Thenable { return element; } @@ -62,8 +80,17 @@ export class WorkspaceTreeDataProvider implements vscode.TreeDataProvider; + const projects = z.object({ data: z.array(project), meta: z.object({ @@ -21,6 +23,15 @@ const projects = z.object({ }), }); +const searchQueryParams = makeParameters([ + { + name: 'q', + type: 'Query', + description: ' A search query string. This query searches projects by name. This search is case-insensitive.', + schema: z.string().optional(), + }, +]); + export const projectEndpoints = makeApi([ { method: 'get', @@ -28,7 +39,7 @@ export const projectEndpoints = makeApi([ alias: 'listProjects', description: 'List projects in the organization', response: projects, - parameters: paginationParams, + parameters: [...paginationParams, ...searchQueryParams], }, { method: 'get',