@immobiliarelabs/backstage-plugin-gitlab
Version:
<p align="center"> <img src="https://avatars.githubusercontent.com/u/10090828?s=200&v=4" width="200px" alt="logo"/> </p> <h1 align="center">Backstage Plugin GitLab</h1>
437 lines (434 loc) • 12.5 kB
JavaScript
import { parseCodeOwners } from '../components/utils.esm.js';
import { AccessLevel } from '@gitbeaker/rest';
import dayjs from 'dayjs';
class GitlabCIClient {
discoveryApi;
identityApi;
gitlabAuthApi;
useOAth;
codeOwnersPath;
gitlabInstance;
readmePath;
cacheTTL;
cacheEnabled;
httpFetch;
constructor({
discoveryApi,
identityApi,
codeOwnersPath,
readmePath,
gitlabAuthApi,
gitlabInstance,
cache,
useOAuth,
httpFetch = window.fetch.bind(window)
}) {
this.discoveryApi = discoveryApi;
this.codeOwnersPath = codeOwnersPath || "CODEOWNERS";
this.readmePath = readmePath || "README.md";
this.gitlabInstance = gitlabInstance;
this.identityApi = identityApi;
this.gitlabAuthApi = gitlabAuthApi;
this.useOAth = useOAuth ?? false;
this.cacheEnabled = cache?.enabled ?? false;
this.cacheTTL = this.cacheEnabled ? (cache?.ttl ?? 60 * 5) * 1e3 : undefined;
this.httpFetch = httpFetch;
this.cleanupExpiredCache();
}
static setupAPI({
discoveryApi,
identityApi,
codeOwnersPath,
readmePath,
gitlabAuthApi,
useOAuth,
cache
}) {
return {
build: (gitlabInstance) => new this({
discoveryApi,
identityApi,
codeOwnersPath,
readmePath,
gitlabInstance,
gitlabAuthApi,
useOAuth,
cache
})
};
}
cleanupExpiredCache() {
if (!this.cacheEnabled || !this.cacheTTL) return;
const now = Date.now();
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key?.startsWith("gitlab-cache:")) {
try {
const cached = localStorage.getItem(key);
if (cached) {
const { timestamp } = JSON.parse(cached);
if (now - timestamp >= this.cacheTTL) {
localStorage.removeItem(key);
i--;
}
}
} catch (error) {
localStorage.removeItem(key);
i--;
}
}
}
}
getCacheKey(path, query, APIkind) {
return `gitlab-cache:${APIkind}:${path}:${JSON.stringify(query)}`;
}
getCachedData(key) {
if (!this.cacheEnabled || !this.cacheTTL) return undefined;
const cached = localStorage.getItem(key);
if (cached) {
try {
const { data, timestamp } = JSON.parse(cached);
if (Date.now() - timestamp < this.cacheTTL) {
return data;
}
localStorage.removeItem(key);
} catch (error) {
localStorage.removeItem(key);
}
}
return undefined;
}
setCachedData(key, data) {
if (!this.cacheEnabled) return;
localStorage.setItem(
key,
JSON.stringify({
data,
timestamp: Date.now()
})
);
}
async callApi(path, query, APIkind = "rest", options = {}) {
const cacheKey = this.getCacheKey(path, query, APIkind);
const cachedData = this.getCachedData(cacheKey);
if (cachedData) {
return cachedData;
}
const apiUrl = `${await this.discoveryApi.getBaseUrl(
"gitlab"
)}/${APIkind}/${this.gitlabInstance}`;
const token = (await this.identityApi.getCredentials()).token;
const headers = {};
if (token) {
headers.Authorization = `Bearer ${token}`;
}
if (this.useOAth) {
const oauthToken = await this.gitlabAuthApi.getAccessToken([
"read_api"
]);
headers["gitlab-authorization"] = `Bearer ${oauthToken}`;
}
options = {
...options,
headers: {
...options?.headers,
...headers
}
};
const response = await this.httpFetch(
`${apiUrl}${path ? `/${path}` : ""}?${new URLSearchParams(
query
).toString()}`,
options
);
if (response.status === 200) {
let data;
if (response.headers.get("content-type")?.includes("application/json")) {
data = await response.json();
} else {
data = await response.text();
}
this.setCachedData(cacheKey, data);
return data;
}
return undefined;
}
callGraphQLApi(query) {
const options = {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(query)
};
return this.callApi("", {}, "graphql", options);
}
async getPipelineSummary(projectID) {
const [projectObj, pipelineObjects] = await Promise.all([
this.callApi("projects/" + projectID, {}),
this.callApi(
"projects/" + projectID + "/pipelines",
{}
)
]);
if (!projectObj) return undefined;
if (pipelineObjects) {
pipelineObjects.forEach((element) => {
element.project_name = projectObj.name;
});
return pipelineObjects;
}
return undefined;
}
async getIssuesSummary(projectId) {
const [projectObj, issuesObject] = await Promise.all([
this.callApi("projects/" + projectId, {}),
this.callApi(`projects/${projectId}/issues`, {})
]);
if (!projectObj) return undefined;
if (issuesObject) {
issuesObject.forEach((element) => {
element.project_name = projectObj.name;
});
return issuesObject;
}
return undefined;
}
async getProjectName(projectID) {
const projectObj = await this.callApi(
"projects/" + projectID,
{}
);
return projectObj?.name;
}
//TODO: Merge with getUserDetail
async getUserProfilesData(contributorsData) {
const uniqueEmails = [...new Set(contributorsData.map((c) => c.email))];
const userProfiles = await Promise.all(
uniqueEmails.map(
(email) => this.callApi("users", {
search: email,
without_project_bots: "true"
})
)
);
const emailToUser = /* @__PURE__ */ new Map();
userProfiles.forEach((profiles, idx) => {
if (profiles) {
const email = uniqueEmails[idx];
const user = profiles.find(
(v) => contributorsData.some(
(c) => c.email === email && c.name === v.name
)
);
if (user) {
emailToUser.set(email, user);
}
}
});
return contributorsData.map((contributor) => {
const user = emailToUser.get(contributor.email);
return user ? { ...contributor, ...user } : contributor;
});
}
async getUserMembersData(membersData) {
return membersData.filter(
(member) => member.state == "active" && member.membership_state == "active"
).map((member) => {
let access_level_label;
switch (member.access_level) {
case AccessLevel.NO_ACCESS:
access_level_label = "No access";
break;
case AccessLevel.MINIMAL_ACCESS:
access_level_label = "Minimal access";
break;
case AccessLevel.GUEST:
access_level_label = "Guest";
break;
case AccessLevel.REPORTER:
access_level_label = "Reporter";
break;
case AccessLevel.DEVELOPER:
access_level_label = "Developer";
break;
case AccessLevel.MAINTAINER:
access_level_label = "Maintainer";
break;
case AccessLevel.OWNER:
access_level_label = "Owner";
break;
default:
access_level_label = String(member.access_level);
}
return { ...member, access_level_label };
});
}
async getUserDetail(username) {
if (username.startsWith("@")) {
username = username.slice(1);
}
const userDetail = (await this.callApi("users", {
username
}))?.[0];
if (!userDetail) throw new Error(`user ${username} does not exist`);
return userDetail;
}
async getGroupDetail(name) {
if (name.startsWith("@")) {
name = name.slice(1);
}
const groupDetail = await this.callApi(
`groups/${encodeURIComponent(name)}`,
{ with_projects: "false" }
);
if (!groupDetail) throw new Error(`group ${name} does not exist`);
return groupDetail;
}
async getMergeRequestsSummary(projectID) {
return this.callApi(
"projects/" + projectID + "/merge_requests",
{}
);
}
async getMergeRequestsStatusSummary(projectID, count) {
return this.callApi(
"projects/" + projectID + "/merge_requests",
{ per_page: (count ?? 20).toString(10) }
);
}
async getContributorsSummary(projectID) {
const contributorsData = await this.callApi("projects/" + projectID + "/repository/contributors", {
sort: "desc"
});
if (!contributorsData) return undefined;
const updatedContributorsData = await this.getUserProfilesData(
contributorsData
);
return updatedContributorsData;
}
async getMembersSummary(projectID) {
const membersData = await this.callApi(
"projects/" + projectID + "/members/all",
{}
);
if (!membersData) return undefined;
const updatedMembersData = await this.getUserMembersData(membersData);
return updatedMembersData;
}
async getLanguagesSummary(projectID) {
return this.callApi(
"projects/" + projectID + "/languages",
{}
);
}
async getReleasesSummary(projectID) {
return this.callApi(
"projects/" + projectID + "/releases",
{}
);
}
async getTags(projectID) {
return this.callApi(
"projects/" + projectID + "/repository/tags",
{}
);
}
async getProjectDetails(projectSlug) {
if (!projectSlug) return undefined;
return this.callApi(
"projects/" + encodeURIComponent(projectSlug),
{}
);
}
async getProjectCoverage(projectSlug, projectDefaultBranch) {
if (!projectSlug) return undefined;
return this.callGraphQLApi({
variables: {
projectSlug,
projectDefaultBranch,
updatedAfter: dayjs().subtract(30, "days").format("YYYY-MM-DD")
},
query: (
/* GraphQL */
`
query getProjectCoverage(
$projectSlug: ID!
$updatedAfter: Time
$projectDefaultBranch: String
) {
project(fullPath: $projectSlug) {
pipelines(
ref: $projectDefaultBranch
updatedAfter: $updatedAfter
) {
nodes {
coverage
createdAt
}
}
}
}
`
)
});
}
async getCodeOwners(projectID, branch = "HEAD", filePath) {
filePath = filePath || this.codeOwnersPath;
if (filePath.startsWith("./")) filePath = filePath.slice(2);
const codeOwnersStr = await this.callApi(
`projects/${projectID}/repository/files/${encodeURIComponent(
filePath
)}/raw`,
{ ref: branch }
);
if (!codeOwnersStr) {
throw Error(`Code owners file not found`);
}
const codeOwners = parseCodeOwners(codeOwnersStr);
const uniqueOwners = [
...new Set(codeOwners.flatMap((owner) => owner.owners))
];
const ownersMap = /* @__PURE__ */ new Map();
await Promise.all(
uniqueOwners.map(async (owner) => {
try {
const ownerData = await this.getUserDetail(owner);
ownersMap.set(owner, ownerData);
} catch {
try {
const groupData = await this.getGroupDetail(owner);
ownersMap.set(owner, groupData);
} catch {
}
}
})
);
return Array.from(ownersMap.values());
}
async getReadme(projectID, branch = "HEAD", filePath) {
filePath = filePath || this.readmePath;
if (filePath.startsWith("./")) filePath = filePath.slice(2);
const readmeStr = await this.callApi(
`projects/${projectID}/repository/files/${encodeURIComponent(
filePath
)}/raw`,
{ ref: branch }
);
if (!readmeStr) {
throw Error(`README file not found`);
}
return readmeStr;
}
getContributorsLink(projectWebUrl, projectDefaultBranch) {
return `${projectWebUrl}/-/graphs/${projectDefaultBranch}`;
}
getMembersLink(projectWebUrl) {
return `${projectWebUrl}/-/project_members`;
}
getOwnersLink(projectWebUrl, projectDefaultBranch, codeOwnersPath) {
return `${projectWebUrl}/-/blob/${projectDefaultBranch}/${codeOwnersPath || this.codeOwnersPath}`;
}
}
export { GitlabCIClient };
//# sourceMappingURL=GitlabCIClient.esm.js.map