From 075997a972fb45dda8f79256331545e7afe35173 Mon Sep 17 00:00:00 2001 From: Jan Keromnes Date: Tue, 15 Mar 2022 12:35:49 +0000 Subject: [PATCH] [server] For GitLab projects without an owner avatar, fall back to the top-level group avatar, or generate the default GitLab avatar --- .../dashboard/src/projects/NewProject.tsx | 6 +- .../ee/src/gitlab/gitlab-app-support.ts | 58 +++++++++++++++++-- 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/components/dashboard/src/projects/NewProject.tsx b/components/dashboard/src/projects/NewProject.tsx index fbf1d9fc0c6894..ff754f769786ed 100644 --- a/components/dashboard/src/projects/NewProject.tsx +++ b/components/dashboard/src/projects/NewProject.tsx @@ -237,7 +237,11 @@ export default function NewProject() { const accounts = new Map(); reposInAccounts.forEach((r) => { - if (!accounts.has(r.account)) accounts.set(r.account, { avatarUrl: r.accountAvatarUrl }); + if (!accounts.has(r.account)) { + accounts.set(r.account, { avatarUrl: r.accountAvatarUrl }); + } else if (!accounts.get(r.account)?.avatarUrl && r.accountAvatarUrl) { + accounts.get(r.account)!.avatarUrl = r.accountAvatarUrl; + } }); const getDropDownEntries = (accounts: Map) => { diff --git a/components/server/ee/src/gitlab/gitlab-app-support.ts b/components/server/ee/src/gitlab/gitlab-app-support.ts index d07dbb1faaa25d..78f0deb9f7cc58 100644 --- a/components/server/ee/src/gitlab/gitlab-app-support.ts +++ b/components/server/ee/src/gitlab/gitlab-app-support.ts @@ -9,6 +9,21 @@ import { inject, injectable } from "inversify"; import { TokenProvider } from "../../../src/user/token-provider"; import { UserDB } from "@gitpod/gitpod-db/lib"; import { Gitlab } from "@gitbeaker/node"; +import { ProjectSchemaDefault, NamespaceInfoSchemaDefault } from "@gitbeaker/core/dist/types/services/Projects"; + +// Add missing fields to Gitbeaker's ProjectSchema type +type ProjectSchema = ProjectSchemaDefault & { + last_activity_at: string; + namespace: NamespaceInfoSchemaDefault & { + avatar_url: string | null; + parent_id: number | null; + }; + owner?: { + id: number; + name: string; + avatar_url: string | null; + }; +}; @injectable() export class GitLabAppSupport { @@ -38,12 +53,12 @@ export class GitLabAppSupport { // const projectsWithAccess = await api.Projects.all({ min_access_level: "40", perPage: 100 }); for (const project of projectsWithAccess) { - const anyProject = project as any; - const path = anyProject.path as string; - const fullPath = anyProject.path_with_namespace as string; - const cloneUrl = anyProject.http_url_to_repo as string; - const updatedAt = anyProject.last_activity_at as string; - const accountAvatarUrl = anyProject.owner?.avatar_url as string; + const aProject = project as ProjectSchema; + const path = aProject.path as string; + const fullPath = aProject.path_with_namespace as string; + const cloneUrl = aProject.http_url_to_repo as string; + const updatedAt = aProject.last_activity_at as string; + const accountAvatarUrl = await this.getAccountAvatarUrl(aProject, params.provider.host); const account = fullPath.split("/")[0]; (account === usersGitLabAccount ? ownersRepos : result).push({ @@ -61,4 +76,35 @@ export class GitLabAppSupport { result.unshift(...ownersRepos); return result; } + + protected async getAccountAvatarUrl(project: ProjectSchema, providerHost: string): Promise { + let owner = project.owner; + if (!owner && project.namespace && !project.namespace.parent_id) { + // Fall back to "root namespace" / "top-level group" + owner = project.namespace; + } + if (!owner) { + // Could not determine account avatar + return ""; + } + if (owner.avatar_url) { + const url = owner.avatar_url; + // Sometimes GitLab avatar URLs are relative -- ensure we always use the correct host + return url[0] === "/" ? `https://${providerHost}${url}` : url; + } + // If there is no avatar, generate the same default avatar that GitLab uses. Based on: + // - https://gitlab.com/gitlab-org/gitlab/-/blob/b2a22b6e85200ce55ab09b5c765043441b086c96/app/helpers/avatars_helper.rb#L151-161 + // - https://gitlab.com/gitlab-org/gitlab/-/blob/861f52858a1db07bdb122fe947dec9b0a09ce807/app/assets/stylesheets/startup/startup-general.scss#L1611-1631 + // - https://gitlab.com/gitlab-org/gitlab/-/blob/861f52858a1db07bdb122fe947dec9b0a09ce807/app/assets/stylesheets/startup/startup-general.scss#L420-422 + const backgroundColors = ["#fcf1ef", "#f4f0ff", "#f1f1ff", "#e9f3fc", "#ecf4ee", "#fdf1dd", "#f0f0f0"]; + const backgroundColor = backgroundColors[owner.id % backgroundColors.length]; + // Uppercase first character of the name, support emojis, default to whitespace. + const text = String.fromCodePoint(owner.name.codePointAt(0) || 32 /* space */).toUpperCase(); + const svg = ` + + ${text} + + `; + return `data:image/svg+xml,${encodeURIComponent(svg.replace(/\s+/g, " "))}`; + } }