diff --git a/.vscode/settings.json b/.vscode/settings.json index d224c7c..0873375 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,7 +9,5 @@ "dist": true // set this to false to include "dist" folder in search results }, // Turn off tsc task auto detection since we have the necessary tasks as npm scripts - "typescript.tsc.autoDetect": "off", - "editor.defaultFormatter": "rvest.vs-code-prettier-eslint", - "editor.formatOnSave": true + "typescript.tsc.autoDetect": "off" } diff --git a/openapi-ts.config.ts b/openapi-ts.config.ts index efede1e..da1cce4 100644 --- a/openapi-ts.config.ts +++ b/openapi-ts.config.ts @@ -13,6 +13,6 @@ export default defineConfig({ enums: 'javascript', }, services: { - filter: '^\\w+ /(accounts|workspaces|runs|plans|applies)', + filter: '^\\w+ /(accounts|workspaces|runs|plans|applies|environments)', }, }); diff --git a/package-lock.json b/package-lock.json index b927dc2..240b489 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "scalr", - "version": "0.0.1", + "version": "0.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "scalr", - "version": "0.0.1", + "version": "0.0.2", "license": "MPL-2.0", "dependencies": { "@hey-api/client-fetch": "^0.1.13", diff --git a/package.json b/package.json index cc2e67a..f2a7439 100644 --- a/package.json +++ b/package.json @@ -94,10 +94,27 @@ "title": "Refresh", "icon": "$(refresh)", "enablement": "scalr.signed-in" + }, + { + "command": "workspace.filter", + "title": "Filter", + "icon": "$(filter)", + "enablement": "scalr.signed-in" + }, + { + "command": "workspace.clearFilters", + "title": "Clear filters", + "icon": "$(clear-all)", + "enablement": "scalr.signed-in" } ], "menus": { "view/title": [ + { + "command": "workspace.filter", + "group": "navigation", + "when": "view == workspaces" + }, { "command": "workspace.refresh", "group": "navigation", @@ -129,6 +146,11 @@ "command": "plan.open", "group": "inline", "when": "view == runs && viewItem =~ /planItem/" + }, + { + "command": "workspace.clearFilters", + "group": "inline", + "when": "view == workspaces && viewItem =~ /workspaceFilterInfo/" } ] } diff --git a/src/api/services.gen.ts b/src/api/services.gen.ts index 4bf3cc6..485a99f 100644 --- a/src/api/services.gen.ts +++ b/src/api/services.gen.ts @@ -38,6 +38,33 @@ import type { GetApplyLogData, GetApplyLogError, GetApplyLogResponse, + ListEnvironmentsData, + ListEnvironmentsError, + ListEnvironmentsResponse, + CreateEnvironmentData, + CreateEnvironmentError, + CreateEnvironmentResponse, + DeleteEnvironmentData, + DeleteEnvironmentError, + DeleteEnvironmentResponse, + GetEnvironmentData, + GetEnvironmentError, + GetEnvironmentResponse, + UpdateEnvironmentData, + UpdateEnvironmentError, + UpdateEnvironmentResponse, + DeleteEnvironmentTagsData, + DeleteEnvironmentTagsError, + DeleteEnvironmentTagsResponse, + ListEnvironmentTagsData, + ListEnvironmentTagsError, + ListEnvironmentTagsResponse, + ReplaceEnvironmentTagsData, + ReplaceEnvironmentTagsError, + ReplaceEnvironmentTagsResponse, + AddEnvironmentTagsData, + AddEnvironmentTagsError, + AddEnvironmentTagsResponse, GetPlanData, GetPlanError, GetPlanResponse, @@ -301,6 +328,108 @@ export const getApplyLog = (options: Options) => { }); }; +/** + * List Environments + * This endpoint lists account environments. + */ +export const listEnvironments = (options?: Options) => { + return (options?.client ?? client).get({ + ...options, + url: '/environments', + }); +}; + +/** + * Create an Environment + * Create a new environment in the account. + */ +export const createEnvironment = (options?: Options) => { + return (options?.client ?? client).post({ + ...options, + url: '/environments', + }); +}; + +/** + * Delete an Environment + */ +export const deleteEnvironment = (options: Options) => { + return (options?.client ?? client).delete({ + ...options, + url: '/environments/{environment}', + }); +}; + +/** + * Get an Environment + * Show details of a specific environment. + */ +export const getEnvironment = (options: Options) => { + return (options?.client ?? client).get({ + ...options, + url: '/environments/{environment}', + }); +}; + +/** + * Update Environment + */ +export const updateEnvironment = (options: Options) => { + return (options?.client ?? client).patch({ + ...options, + url: '/environments/{environment}', + }); +}; + +/** + * Delete environment's tags + * This endpoint removes given [tags](tags.html#the-tag-resource) from the environment. + * + */ +export const deleteEnvironmentTags = (options: Options) => { + return (options?.client ?? client).delete({ + ...options, + url: '/environments/{environment}/relationships/tags', + }); +}; + +/** + * List environment's tags + * This endpoint returns a list of [tags](tags.html#the-tag-resource), + * assigned to an environment. + * + */ +export const listEnvironmentTags = (options: Options) => { + return (options?.client ?? client).get({ + ...options, + url: '/environments/{environment}/relationships/tags', + }); +}; + +/** + * Replace environment's tags + * This endpoint completely replaces environment's tags with provided list. + * + */ +export const replaceEnvironmentTags = (options: Options) => { + return (options?.client ?? client).patch({ + ...options, + url: '/environments/{environment}/relationships/tags', + }); +}; + +/** + * Add tags to the environment + * This endpoint assigns the list of [tags](tags.html#the-tag-resource) to the environment. + * + */ +export const addEnvironmentTags = (options: Options) => { + return (options?.client ?? client).post({ + ...options, + url: '/environments/{environment}/relationships/tags', + }); +}; + /** * Get a Plan * Show details of a specific Terraform Plan stage. diff --git a/src/api/types.gen.ts b/src/api/types.gen.ts index ccbc5e6..1c45a20 100644 --- a/src/api/types.gen.ts +++ b/src/api/types.gen.ts @@ -7648,6 +7648,316 @@ export type GetApplyLogResponse = unknown | void; export type GetApplyLogError = unknown; +export type ListEnvironmentsData = { + query?: { + /** + * The value of the fields[resource-type] parameter is a comma-separated list that refers to the name of the fields to be returned for the resource. An empty value indicates that no fields should be returned. + */ + fields?: { + /** + * The comma-separated list of fields to return in response for Account resource. + */ + accounts?: string; + /** + * The comma-separated list of fields to return in response for Environment resource. + */ + environments?: string; + /** + * The comma-separated list of fields to return in response for PolicyGroup resource. + */ + 'policy-groups'?: string; + /** + * The comma-separated list of fields to return in response for ProviderConfiguration resource. + */ + 'provider-configurations'?: string; + /** + * The comma-separated list of fields to return in response for Tag resource. + */ + tags?: string; + /** + * The comma-separated list of fields to return in response for User resource. + */ + users?: string; + }; + /** + * The ID of the Account + */ + 'filter[account]'?: string; + /** + * The ID of the Environment + */ + 'filter[environment]'?: string; + /** + * Filter by latest run date. Example: `filter[latest-run-date]=between:2022-01-01T00:00:00Z,2022-02-01T00:00:00Z` + */ + 'filter[latest-run-date]'?: string; + /** + * The environment name filter. + */ + 'filter[name]'?: string; + /** + * The ID of the Policy Group. + */ + 'filter[policy-group]'?: string; + /** + * Filter environments by tags + */ + 'filter[tag]'?: string; + /** + * The comma-separated list of relationship paths. + */ + include?: Array< + | 'account' + | 'created-by' + | 'default-provider-configurations' + | 'policy-groups' + | 'provider-configurations' + | 'tags' + >; + /** + * Page number + */ + 'page[number]'?: string; + /** + * Page size + */ + 'page[size]'?: string; + /** + * Query string, search by id, name. + */ + query?: string; + /** + * The comma-separated list of attributes. + */ + sort?: Array<'account' | 'cost-estimation-enabled' | 'created-at' | 'created-by-email' | 'name'>; + }; +}; + +export type ListEnvironmentsResponse = EnvironmentListingDocument; + +export type ListEnvironmentsError = unknown; + +export type CreateEnvironmentData = { + body?: EnvironmentDocument; + query?: { + /** + * The value of the fields[resource-type] parameter is a comma-separated list that refers to the name of the fields to be returned for the resource. An empty value indicates that no fields should be returned. + */ + fields?: { + /** + * The comma-separated list of fields to return in response for Account resource. + */ + accounts?: string; + /** + * The comma-separated list of fields to return in response for Environment resource. + */ + environments?: string; + /** + * The comma-separated list of fields to return in response for PolicyGroup resource. + */ + 'policy-groups'?: string; + /** + * The comma-separated list of fields to return in response for ProviderConfiguration resource. + */ + 'provider-configurations'?: string; + /** + * The comma-separated list of fields to return in response for Tag resource. + */ + tags?: string; + /** + * The comma-separated list of fields to return in response for User resource. + */ + users?: string; + }; + }; +}; + +export type CreateEnvironmentResponse = EnvironmentDocument; + +export type CreateEnvironmentError = unknown; + +export type DeleteEnvironmentData = { + path: { + /** + * The ID of the environment to delete. + */ + environment: string; + }; +}; + +export type DeleteEnvironmentResponse = void; + +export type DeleteEnvironmentError = unknown; + +export type GetEnvironmentData = { + path: { + /** + * The ID of the environment. + */ + environment: string; + }; + query?: { + /** + * The value of the fields[resource-type] parameter is a comma-separated list that refers to the name of the fields to be returned for the resource. An empty value indicates that no fields should be returned. + */ + fields?: { + /** + * The comma-separated list of fields to return in response for Account resource. + */ + accounts?: string; + /** + * The comma-separated list of fields to return in response for Environment resource. + */ + environments?: string; + /** + * The comma-separated list of fields to return in response for PolicyGroup resource. + */ + 'policy-groups'?: string; + /** + * The comma-separated list of fields to return in response for ProviderConfiguration resource. + */ + 'provider-configurations'?: string; + /** + * The comma-separated list of fields to return in response for Tag resource. + */ + tags?: string; + /** + * The comma-separated list of fields to return in response for User resource. + */ + users?: string; + }; + /** + * The comma-separated list of relationship paths. + */ + include?: Array< + | 'account' + | 'created-by' + | 'default-provider-configurations' + | 'policy-groups' + | 'provider-configurations' + | 'tags' + >; + }; +}; + +export type GetEnvironmentResponse = EnvironmentDocument; + +export type GetEnvironmentError = unknown; + +export type UpdateEnvironmentData = { + body?: EnvironmentDocument; + path: { + /** + * The ID of the environment to update. + */ + environment: string; + }; + query?: { + /** + * The value of the fields[resource-type] parameter is a comma-separated list that refers to the name of the fields to be returned for the resource. An empty value indicates that no fields should be returned. + */ + fields?: { + /** + * The comma-separated list of fields to return in response for Account resource. + */ + accounts?: string; + /** + * The comma-separated list of fields to return in response for Environment resource. + */ + environments?: string; + /** + * The comma-separated list of fields to return in response for PolicyGroup resource. + */ + 'policy-groups'?: string; + /** + * The comma-separated list of fields to return in response for ProviderConfiguration resource. + */ + 'provider-configurations'?: string; + /** + * The comma-separated list of fields to return in response for Tag resource. + */ + tags?: string; + /** + * The comma-separated list of fields to return in response for User resource. + */ + users?: string; + }; + }; +}; + +export type UpdateEnvironmentResponse = EnvironmentDocument; + +export type UpdateEnvironmentError = unknown; + +export type DeleteEnvironmentTagsData = { + body?: TagRelationshipFieldsetsListingDocument; + path: { + /** + * The environment whose tags will be deleted. + * + */ + environment: string; + }; +}; + +export type DeleteEnvironmentTagsResponse = void; + +export type DeleteEnvironmentTagsError = unknown; + +export type ListEnvironmentTagsData = { + path: { + /** + * The environment to list tags for. + * + */ + environment: string; + }; + query?: { + /** + * Page number + */ + 'page[number]'?: string; + /** + * Page size + */ + 'page[size]'?: string; + }; +}; + +export type ListEnvironmentTagsResponse = TagRelationshipFieldsetsListingDocument; + +export type ListEnvironmentTagsError = unknown; + +export type ReplaceEnvironmentTagsData = { + body?: TagRelationshipFieldsetsListingDocument; + path: { + /** + * The environment whose tags will be replaced. + * + */ + environment: string; + }; +}; + +export type ReplaceEnvironmentTagsResponse = void; + +export type ReplaceEnvironmentTagsError = unknown; + +export type AddEnvironmentTagsData = { + body?: TagRelationshipFieldsetsListingDocument; + path: { + /** + * The environment to add the tags to. + * + */ + environment: string; + }; +}; + +export type AddEnvironmentTagsResponse = void; + +export type AddEnvironmentTagsError = unknown; + export type GetPlanData = { path: { /** @@ -9044,6 +9354,213 @@ export type $OpenApiTs = { }; }; }; + '/environments': { + get: { + req: ListEnvironmentsData; + res: { + /** + * Success. + */ + '200': EnvironmentListingDocument; + /** + * Account not found or user unauthorized to perform action. + */ + '404': unknown; + /** + * Client error. + */ + '4XX': unknown; + /** + * Server error. + */ + '5XX': unknown; + }; + }; + post: { + req: CreateEnvironmentData; + res: { + /** + * The environment was created. + */ + '201': EnvironmentDocument; + /** + * Account relationship not found, or user unauthorized to perform action. + */ + '404': unknown; + /** + * Invalid arguments. + */ + '422': unknown; + /** + * Client error. + */ + '4XX': unknown; + /** + * Server error. + */ + '5XX': unknown; + }; + }; + }; + '/environments/{environment}': { + delete: { + req: DeleteEnvironmentData; + res: { + /** + * Successfully deleted the environment. + */ + '204': void; + /** + * Environment deletion is forbidden. + */ + '403': unknown; + /** + * Environment not found, or user unauthorized to perform action. + */ + '404': unknown; + /** + * Client error. + */ + '4XX': unknown; + /** + * Server error. + */ + '5XX': unknown; + }; + }; + get: { + req: GetEnvironmentData; + res: { + /** + * Success. + */ + '200': EnvironmentDocument; + /** + * Environment not found or user unauthorized to perform action. + */ + '404': unknown; + /** + * Client error. + */ + '4XX': unknown; + /** + * Server error. + */ + '5XX': unknown; + }; + }; + patch: { + req: UpdateEnvironmentData; + res: { + /** + * Successfully updated the environment. + */ + '200': EnvironmentDocument; + /** + * Environment or relationship not found, or user unauthorized to perform action. + */ + '404': unknown; + /** + * Invalid arguments. + */ + '422': unknown; + /** + * Client error. + */ + '4XX': unknown; + /** + * Server error. + */ + '5XX': unknown; + }; + }; + }; + '/environments/{environment}/relationships/tags': { + delete: { + req: DeleteEnvironmentTagsData; + res: { + /** + * Success. + */ + '204': void; + /** + * User unauthorized to perform this action. + */ + '403': unknown; + /** + * Client error. + */ + '4XX': unknown; + /** + * Server error. + */ + '5XX': unknown; + }; + }; + get: { + req: ListEnvironmentTagsData; + res: { + /** + * Success. + */ + '200': TagRelationshipFieldsetsListingDocument; + /** + * User unauthorized to perform this action. + */ + '403': unknown; + /** + * Client error. + */ + '4XX': unknown; + /** + * Server error. + */ + '5XX': unknown; + }; + }; + patch: { + req: ReplaceEnvironmentTagsData; + res: { + /** + * Success. + */ + '204': void; + /** + * User unauthorized to perform this action. + */ + '403': unknown; + /** + * Client error. + */ + '4XX': unknown; + /** + * Server error. + */ + '5XX': unknown; + }; + }; + post: { + req: AddEnvironmentTagsData; + res: { + /** + * Success. + */ + '204': void; + /** + * User unauthorized to perform this action. + */ + '403': unknown; + /** + * Client error. + */ + '4XX': unknown; + /** + * Server error. + */ + '5XX': unknown; + }; + }; + }; '/plans/{plan}': { get: { req: GetPlanData; diff --git a/src/providers/workspaceProvider.ts b/src/providers/workspaceProvider.ts index 5f8a75e..bd6d24b 100644 --- a/src/providers/workspaceProvider.ts +++ b/src/providers/workspaceProvider.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; -import { Workspace, Environment, Run, WorkspaceListingDocument } from '../api/types.gen'; -import { getWorkspaces } from '../api/services.gen'; +import { Workspace, Environment, Run, WorkspaceListingDocument, EnvironmentListingDocument } from '../api/types.gen'; +import { getWorkspaces, listEnvironments } from '../api/services.gen'; import { ScalrAuthenticationProvider, ScalrSession } from './authenticationProvider'; import { getRunStatusIcon, RunTreeDataProvider } from './runProvider'; import { Pagination } from '../@types/api'; @@ -12,11 +12,13 @@ export class WorkspaceTreeDataProvider implements vscode.TreeDataProvider; constructor( private ctx: vscode.ExtensionContext, private runProvider: RunTreeDataProvider ) { + this.filters = this.loadFilters(); ctx.subscriptions.push( vscode.commands.registerCommand('workspace.open', (ws: WorkspaceItem) => { vscode.env.openExternal(ws.webLink); @@ -31,6 +33,11 @@ export class WorkspaceTreeDataProvider implements vscode.TreeDataProvider this.chooseFilterOrClear()), + vscode.commands.registerCommand('workspace.clearFilters', () => { + this.filters.clear(); + this.applyFilters(); }) ); } @@ -56,6 +63,10 @@ export class WorkspaceTreeDataProvider implements vscode.TreeDataProvider 0) { + workspaces.unshift(new FilterInfoItem(this.filters)); + } + return workspaces; } @@ -68,6 +79,11 @@ export class WorkspaceTreeDataProvider implements vscode.TreeDataProvider('workspaceFilters'); + return savedFilters ? new Map(JSON.parse(savedFilters)) : new Map(); + } + private async getWorkspaces(): Promise { const session = (await vscode.authentication.getSession(ScalrAuthenticationProvider.id, [], { createIfNone: false, @@ -77,6 +93,17 @@ export class WorkspaceTreeDataProvider implements vscode.TreeDataProvider item.id).join(','); + } else { + queryFilters[filterName] = filterValue; + } + } + const { data, error } = await getWorkspaces({ query: { include: ['latest-run', 'environment'], @@ -86,6 +113,7 @@ export class WorkspaceTreeDataProvider implements vscode.TreeDataProvider { + // @ts-expect-error we override the type with our custom property + return this.filters.get('environment')?.some((selectedEnv) => selectedEnv.id === env.id); + }); + + environmentsQuickPicks.onDidChangeValue((value) => { + if (typingTimer) { + clearTimeout(typingTimer); + } + + typingTimer = setTimeout(async () => { + environmentsQuickPicks.items = await this.getEnvironmentQuickPick(value); + }, delay); + }); + + environmentsQuickPicks.onDidAccept(() => { + const selectedEnvironments = environmentsQuickPicks.selectedItems as QuickPickItem[]; + if (selectedEnvironments.length === 0) { + this.filters.delete('environment'); + } else { + this.filters.set('environment', selectedEnvironments); + } + this.applyFilters(); + environmentsQuickPicks.hide(); + }); + + environmentsQuickPicks.show(); + break; + } + case WorkspaceFilter.query: { + const currentQuery = (this.filters.get('query') || '') as string; + const query = await vscode.window.showInputBox({ + placeHolder: currentQuery || 'Enter a workspace name or ID', + prompt: 'Filtering by workspace name or ID', + }); + if (query) { + this.filters.set('query', query); + } else { + this.filters.delete('query'); + } + + this.applyFilters(); + break; + } + default: + throw new Error('Unknown filter type'); + } + } + + private applyFilters() { + this.reset(); + this.refresh(); + this.ctx.workspaceState.update('workspaceFilters', JSON.stringify(Array.from(this.filters.entries()))); } + + private async getEnvironmentQuickPick(query?: string): Promise { + const { data, error } = await listEnvironments({ + query: { + fields: { environments: 'name' }, + query: query, + }, + }); + + if (error || !data) { + vscode.window.showErrorMessage('Unable to get environments: ' + error); + return []; + } + + data as EnvironmentListingDocument; + const environments = data.data as Environment[]; + // const currentEnvironments = this.filters.get('environment') || []; + + return environments.map((env) => ({ + label: env.attributes.name, + id: env.id as string, + })); + } + + dispose() {} } export class WorkspaceItem extends vscode.TreeItem { @@ -172,7 +299,48 @@ class LoadMoreItem extends vscode.TreeItem { this.iconPath = new vscode.ThemeIcon('more', new vscode.ThemeColor('charts.gray')); this.command = { command: 'workspace.loadMore', - title: 'Show more', + title: 'Next page', + tooltip: 'Load more workspaces', + }; + } +} + +class FilterInfoItem extends vscode.TreeItem { + constructor(private filters: Map) { + super('Applied Filters', vscode.TreeItemCollapsibleState.None); + this.description = 'By ' + Array.from(filters.keys()).join(', '); + this.tooltip = ''; + for (const [filterKey, filterValue] of filters.entries()) { + this.tooltip += `${filterKey}: `; + if (Array.isArray(filterValue)) { + this.tooltip += filterValue.map((item) => item.label).join(', '); + } else { + this.tooltip += filterValue; + } + this.tooltip += '\n'; + } + + this.iconPath = new vscode.ThemeIcon('filter-filled'); + this.contextValue = 'workspaceFilterInfo'; + this.command = { + command: 'workspace.filter', + title: 'Change filters', + tooltip: 'Change applied filters', }; } } + +class QuickPickItem implements vscode.QuickPickItem { + constructor( + public label: string, + public id: string + ) {} +} + +enum WorkspaceFilter { + //important the key value must be the same as the filter key in the API + environment = 'By environments', + query = 'By workspace name of ID', +} + +type WorkspaceFilterApiType = keyof typeof WorkspaceFilter;