From e0cdfe3cf138e2119646e4346948aa09e21983c9 Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Thu, 6 Jul 2023 15:57:23 +0900 Subject: [PATCH] Adds workaround to get team/org workspaces --- src/plus/workspaces/models.ts | 5 +- src/plus/workspaces/workspacesApi.ts | 212 +++++++++++++++++-------------- src/plus/workspaces/workspacesService.ts | 18 ++- 3 files changed, 130 insertions(+), 105 deletions(-) diff --git a/src/plus/workspaces/models.ts b/src/plus/workspaces/models.ts index 3b2c833..e566fce 100644 --- a/src/plus/workspaces/models.ts +++ b/src/plus/workspaces/models.ts @@ -400,7 +400,7 @@ export interface CloudWorkspaceIssue { repository: CloudWorkspaceRepositoryData; } -interface CloudWorkspaceConnection { +export interface CloudWorkspaceConnection { total_count: number; page_info: { start_cursor: string; @@ -477,6 +477,7 @@ export interface DeleteWorkspaceResponse { data: { delete_project: CloudWorkspaceData | null; }; + errors?: { code: number; message: string }[]; } export type AddRepositoriesToWorkspaceResponse = { @@ -488,6 +489,7 @@ export type AddRepositoriesToWorkspaceResponse = { }; } | null; }; + errors?: { code: number; message: string }[]; }; export interface RemoveRepositoriesFromWorkspaceResponse { @@ -496,6 +498,7 @@ export interface RemoveRepositoriesFromWorkspaceResponse { id: string; } | null; }; + errors?: { code: number; message: string }[]; } export interface AddWorkspaceRepoDescriptor { diff --git a/src/plus/workspaces/workspacesApi.ts b/src/plus/workspaces/workspacesApi.ts index affc86c..4fa8cac 100644 --- a/src/plus/workspaces/workspacesApi.ts +++ b/src/plus/workspaces/workspacesApi.ts @@ -4,6 +4,8 @@ import type { ServerConnection } from '../subscription/serverConnection'; import type { AddRepositoriesToWorkspaceResponse, AddWorkspaceRepoDescriptor, + CloudWorkspaceConnection, + CloudWorkspaceData, CreateWorkspaceResponse, DeleteWorkspaceResponse, RemoveRepositoriesFromWorkspaceResponse, @@ -27,11 +29,11 @@ export class WorkspacesApi { return session.accessToken; } - // TODO@ramint: We have a pagedresponse model available in case it helps here. Takes care of cursor internally - // Make the data return a promise for the repos. Should be async so we're set up for dynamic processing. - async getWorkspacesWithRepos(options?: { + async getWorkspaces(options?: { count?: number; cursor?: string; + includeOrganizations?: boolean; + includeRepositories?: boolean; page?: number; repoCount?: number; repoPage?: number; @@ -41,6 +43,53 @@ export class WorkspacesApi { return; } + let repoQuery: string | undefined; + if (options?.includeRepositories) { + let repoQueryParams = `(first: ${options?.repoCount ?? defaultWorkspaceRepoCount}`; + if (options?.repoPage) { + repoQueryParams += `, page: ${options.repoPage}`; + } + repoQueryParams += ')'; + repoQuery = ` + provider_data { + repositories ${repoQueryParams} { + total_count + page_info { + end_cursor + has_next_page + } + nodes { + id + name + repository_id + provider + provider_organization_id + provider_organization_name + url + } + } + } + `; + } + + const queryData = ` + total_count + page_info { + end_cursor + has_next_page + } + nodes { + id + description + name + organization { + id + } + provider + ${repoQuery ?? ''} + } + `; + let queryParams = `(first: ${options?.count ?? defaultWorkspaceCount}`; if (options?.cursor) { queryParams += `, after: "${options.cursor}"`; @@ -49,52 +98,29 @@ export class WorkspacesApi { } queryParams += ')'; - let repoQueryParams = `(first: ${options?.repoCount ?? defaultWorkspaceRepoCount}`; - if (options?.repoPage) { - repoQueryParams += `, page: ${options.repoPage}`; + let query = 'query getWorkpacesWithRepos {'; + query += `memberProjects: projects ${queryParams} { ${queryData} }`; + + // TODO@axosoft-ramint This is a temporary and hacky workaround until projects api returns all projects the + // user belongs to in one query. Update once that is available. + if (options?.cursor == null && options?.includeOrganizations) { + const organizationIds = + (await this.container.subscription.getSubscription())?.account?.organizationIds ?? []; + for (const organizationId of organizationIds) { + let orgQueryParams = `(first: ${options?.count ?? defaultWorkspaceCount}`; + if (options?.page) { + orgQueryParams += `, page: ${options.page}`; + } + orgQueryParams += `, organization_id: "${organizationId}")`; + query += `organizationProjects_${organizationId}: projects ${orgQueryParams} { ${queryData} }`; + } } - repoQueryParams += ')'; + + query += '}'; const rsp = await this.server.fetchGraphql( { - query: ` - query getWorkspacesWithRepos { - projects ${queryParams} { - total_count - page_info { - end_cursor - has_next_page - } - nodes { - id - description - name - organization { - id - } - provider - provider_data { - repositories ${repoQueryParams} { - total_count - page_info { - end_cursor - has_next_page - } - nodes { - id - name - repository_id - provider - provider_organization_id - provider_organization_name - url - } - } - } - } - } - } - `, + query: query, }, accessToken, ); @@ -104,63 +130,32 @@ export class WorkspacesApi { throw new Error(rsp.statusText); } - const json: WorkspacesResponse | undefined = (await rsp.json()) as WorkspacesResponse | undefined; - - return json; - } - - async getWorkspaces(options?: { - count?: number; - cursor?: string; - page?: number; - }): Promise { - const accessToken = await this.getAccessToken(); - if (accessToken == null) { - return; - } - - let queryparams = `(first: ${options?.count ?? defaultWorkspaceCount}`; - if (options?.cursor) { - queryparams += `, after: "${options.cursor}"`; - } else if (options?.page) { - queryparams += `, page: ${options.page}`; + const addedWorkspaceIds = new Set(); + const json: { data: { [queryKey: string]: CloudWorkspaceConnection | null } } | undefined = + await rsp.json(); + if (json?.data == null) return undefined; + let outputData: WorkspacesResponse | undefined; + for (const workspaceData of Object.values(json.data)) { + if (workspaceData == null) continue; + if (outputData == null) { + outputData = { data: { projects: workspaceData } }; + for (const node of workspaceData.nodes) { + addedWorkspaceIds.add(node.id); + } + } else { + for (const node of workspaceData.nodes) { + if (addedWorkspaceIds.has(node.id)) continue; + addedWorkspaceIds.add(node.id); + outputData.data.projects.nodes.push(node); + } + } } - queryparams += ')'; - const rsp = await this.server.fetchGraphql( - { - query: ` - query getWorkspaces { - projects ${queryparams} { - total_count - page_info { - end_cursor - has_next_page - } - nodes { - id - description - name - organization { - id - } - provider - } - } - } - `, - }, - accessToken, - ); - - if (!rsp.ok) { - Logger.error(undefined, `Getting workspaces failed: (${rsp.status}) ${rsp.statusText}`); - throw new Error(rsp.statusText); + if (outputData != null) { + outputData.data.projects.total_count = addedWorkspaceIds.size; } - const json: WorkspacesResponse | undefined = (await rsp.json()) as WorkspacesResponse | undefined; - - return json; + return outputData; } async getWorkspaceRepositories( @@ -326,6 +321,13 @@ export class WorkspacesApi { const json: DeleteWorkspaceResponse | undefined = (await rsp.json()) as DeleteWorkspaceResponse | undefined; + if (json?.errors?.some(error => error.message.includes('permission'))) { + const errorMessage = + 'Adding repositories to workspace failed: you do not have permission to delete this workspace'; + Logger.error(undefined, errorMessage); + throw new Error(errorMessage); + } + return json; } @@ -391,6 +393,13 @@ export class WorkspacesApi { | AddRepositoriesToWorkspaceResponse | undefined; + if (json?.errors?.some(error => error.message.includes('permission'))) { + const errorMessage = + 'Adding repositories to workspace failed: you do not have permission to add repositories to this workspace'; + Logger.error(undefined, errorMessage); + throw new Error(errorMessage); + } + return json; } @@ -438,6 +447,13 @@ export class WorkspacesApi { | RemoveRepositoriesFromWorkspaceResponse | undefined; + if (json?.errors?.some(error => error.message.includes('permission'))) { + const errorMessage = + 'Adding repositories to workspace failed: you do not have permission to remove repositories from this workspace'; + Logger.error(undefined, errorMessage); + throw new Error(errorMessage); + } + return json; } } diff --git a/src/plus/workspaces/workspacesService.ts b/src/plus/workspaces/workspacesService.ts index 387618c..5969aeb 100644 --- a/src/plus/workspaces/workspacesService.ts +++ b/src/plus/workspaces/workspacesService.ts @@ -87,9 +87,10 @@ export class WorkspacesService implements Disposable { const cloudWorkspaces: CloudWorkspace[] = []; let workspaces: CloudWorkspaceData[] | undefined; try { - const workspaceResponse: WorkspacesResponse | undefined = excludeRepositories - ? await this._workspacesApi.getWorkspaces() - : await this._workspacesApi.getWorkspacesWithRepos(); + const workspaceResponse: WorkspacesResponse | undefined = await this._workspacesApi.getWorkspaces({ + includeRepositories: !excludeRepositories, + includeOrganizations: true, + }); workspaces = workspaceResponse?.data?.projects?.nodes; } catch { return { @@ -653,7 +654,9 @@ export class WorkspacesService implements Disposable { // Remove the workspace from the local workspace list. this._cloudWorkspaces = this._cloudWorkspaces?.filter(w => w.id !== workspaceId); } - } catch {} + } catch (error) { + void window.showErrorMessage(error.message); + } } private async filterReposForProvider( @@ -806,7 +809,8 @@ export class WorkspacesService implements Disposable { newRepoDescriptors = Object.values(response.data.add_repositories_to_project.provider_data).map( descriptor => ({ ...descriptor, workspaceId: workspaceId }), ) as CloudWorkspaceRepositoryDescriptor[]; - } catch { + } catch (error) { + void window.showErrorMessage(error.message); return; } @@ -839,7 +843,9 @@ export class WorkspacesService implements Disposable { if (response?.data.remove_repositories_from_project == null) return; workspace.removeRepositories([descriptor.name]); - } catch {} + } catch (error) { + void window.showErrorMessage(error.message); + } } async resolveWorkspaceRepositoriesByName(