UNPKG

@immobiliarelabs/backstage-plugin-gitlab

Version:
437 lines (434 loc) 12.5 kB
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