Skip to content

Commit 075997a

Browse files
committed
[server] For GitLab projects without an owner avatar, fall back to the top-level group avatar, or generate the default GitLab avatar
1 parent 8b9a40a commit 075997a

File tree

2 files changed

+57
-7
lines changed

2 files changed

+57
-7
lines changed

components/dashboard/src/projects/NewProject.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,11 @@ export default function NewProject() {
237237

238238
const accounts = new Map<string, { avatarUrl: string }>();
239239
reposInAccounts.forEach((r) => {
240-
if (!accounts.has(r.account)) accounts.set(r.account, { avatarUrl: r.accountAvatarUrl });
240+
if (!accounts.has(r.account)) {
241+
accounts.set(r.account, { avatarUrl: r.accountAvatarUrl });
242+
} else if (!accounts.get(r.account)?.avatarUrl && r.accountAvatarUrl) {
243+
accounts.get(r.account)!.avatarUrl = r.accountAvatarUrl;
244+
}
241245
});
242246

243247
const getDropDownEntries = (accounts: Map<string, { avatarUrl: string }>) => {

components/server/ee/src/gitlab/gitlab-app-support.ts

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,21 @@ import { inject, injectable } from "inversify";
99
import { TokenProvider } from "../../../src/user/token-provider";
1010
import { UserDB } from "@gitpod/gitpod-db/lib";
1111
import { Gitlab } from "@gitbeaker/node";
12+
import { ProjectSchemaDefault, NamespaceInfoSchemaDefault } from "@gitbeaker/core/dist/types/services/Projects";
13+
14+
// Add missing fields to Gitbeaker's ProjectSchema type
15+
type ProjectSchema = ProjectSchemaDefault & {
16+
last_activity_at: string;
17+
namespace: NamespaceInfoSchemaDefault & {
18+
avatar_url: string | null;
19+
parent_id: number | null;
20+
};
21+
owner?: {
22+
id: number;
23+
name: string;
24+
avatar_url: string | null;
25+
};
26+
};
1227

1328
@injectable()
1429
export class GitLabAppSupport {
@@ -38,12 +53,12 @@ export class GitLabAppSupport {
3853
//
3954
const projectsWithAccess = await api.Projects.all({ min_access_level: "40", perPage: 100 });
4055
for (const project of projectsWithAccess) {
41-
const anyProject = project as any;
42-
const path = anyProject.path as string;
43-
const fullPath = anyProject.path_with_namespace as string;
44-
const cloneUrl = anyProject.http_url_to_repo as string;
45-
const updatedAt = anyProject.last_activity_at as string;
46-
const accountAvatarUrl = anyProject.owner?.avatar_url as string;
56+
const aProject = project as ProjectSchema;
57+
const path = aProject.path as string;
58+
const fullPath = aProject.path_with_namespace as string;
59+
const cloneUrl = aProject.http_url_to_repo as string;
60+
const updatedAt = aProject.last_activity_at as string;
61+
const accountAvatarUrl = await this.getAccountAvatarUrl(aProject, params.provider.host);
4762
const account = fullPath.split("/")[0];
4863

4964
(account === usersGitLabAccount ? ownersRepos : result).push({
@@ -61,4 +76,35 @@ export class GitLabAppSupport {
6176
result.unshift(...ownersRepos);
6277
return result;
6378
}
79+
80+
protected async getAccountAvatarUrl(project: ProjectSchema, providerHost: string): Promise<string> {
81+
let owner = project.owner;
82+
if (!owner && project.namespace && !project.namespace.parent_id) {
83+
// Fall back to "root namespace" / "top-level group"
84+
owner = project.namespace;
85+
}
86+
if (!owner) {
87+
// Could not determine account avatar
88+
return "";
89+
}
90+
if (owner.avatar_url) {
91+
const url = owner.avatar_url;
92+
// Sometimes GitLab avatar URLs are relative -- ensure we always use the correct host
93+
return url[0] === "/" ? `https://${providerHost}${url}` : url;
94+
}
95+
// If there is no avatar, generate the same default avatar that GitLab uses. Based on:
96+
// - https://gitlab.com/gitlab-org/gitlab/-/blob/b2a22b6e85200ce55ab09b5c765043441b086c96/app/helpers/avatars_helper.rb#L151-161
97+
// - https://gitlab.com/gitlab-org/gitlab/-/blob/861f52858a1db07bdb122fe947dec9b0a09ce807/app/assets/stylesheets/startup/startup-general.scss#L1611-1631
98+
// - https://gitlab.com/gitlab-org/gitlab/-/blob/861f52858a1db07bdb122fe947dec9b0a09ce807/app/assets/stylesheets/startup/startup-general.scss#L420-422
99+
const backgroundColors = ["#fcf1ef", "#f4f0ff", "#f1f1ff", "#e9f3fc", "#ecf4ee", "#fdf1dd", "#f0f0f0"];
100+
const backgroundColor = backgroundColors[owner.id % backgroundColors.length];
101+
// Uppercase first character of the name, support emojis, default to whitespace.
102+
const text = String.fromCodePoint(owner.name.codePointAt(0) || 32 /* space */).toUpperCase();
103+
const svg = `<svg viewBox="0 0 32 32" height="32" width="32" style="background-color: ${backgroundColor}" xmlns="http://www.w3.org/2000/svg">
104+
<text x="50%" y="50%" dominant-baseline="central" text-anchor="middle" style='font-size: 0.875rem; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans", Ubuntu, Cantarell, "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'>
105+
${text}
106+
</text>
107+
</svg>`;
108+
return `data:image/svg+xml,${encodeURIComponent(svg.replace(/\s+/g, " "))}`;
109+
}
64110
}

0 commit comments

Comments
 (0)