From 8b36de862669e72788188cb91ad599fe82c3c4c3 Mon Sep 17 00:00:00 2001 From: maddiebaka <madeline@cray.lgbt> Date: Mon, 10 Jun 2024 13:53:40 -0400 Subject: [PATCH] Implementation of gitlab overview widget as a hugo shortcode --- layouts/partials/head.html | 2 + layouts/shortcodes/gitlab-overview.html | 64 ++++ static/css/gitlab-widgets/gitlab-widgets.css | 110 +++++++ .../gitlab-widgets/gitlab-widgets.js | 289 ++++++++++++++++++ 4 files changed, 465 insertions(+) create mode 100644 layouts/shortcodes/gitlab-overview.html create mode 100644 static/css/gitlab-widgets/gitlab-widgets.css create mode 100644 static/javascripts/gitlab-widgets/gitlab-widgets.js diff --git a/layouts/partials/head.html b/layouts/partials/head.html index 7eb45d134..de8d4e583 100644 --- a/layouts/partials/head.html +++ b/layouts/partials/head.html @@ -126,6 +126,7 @@ <link rel="stylesheet" href="/css/glyphicons.css"> <link rel="stylesheet" href="/css/pseudocode.css"> <link rel="stylesheet" href="/javascripts/cal-heatmap/cal-heatmap.css"> +<link rel="stylesheet" href="/css/gitlab-widgets/gitlab-widgets.css"> <link rel="stylesheet" href="/css/main.css"> {{ if eq .Params.w3c "working_draft" -}} <link rel="stylesheet" href="/css/W3C-WD.css"> @@ -141,6 +142,7 @@ <script type="text/javascript" src="/javascripts/pseudocode.js"></script> <script type="text/javascript" src="/javascripts/jquery.js"></script> <script type="text/javascript" src="/javascripts/head.js"></script> +<script type="text/javascript" src="/javascripts/gitlab-widgets/gitlab-widgets.js"></script> <script type="text/javascript" > document.addEventListener("DOMContentLoaded", function() { var blocks = document.getElementsByClassName("pseudocode"); diff --git a/layouts/shortcodes/gitlab-overview.html b/layouts/shortcodes/gitlab-overview.html new file mode 100644 index 000000000..8ad679971 --- /dev/null +++ b/layouts/shortcodes/gitlab-overview.html @@ -0,0 +1,64 @@ +<div class="gitlab-overview-container"> + <!-- Adds widget arguments to DOM --> + + {{ range $key, $value := $.Params }} + <input type="hidden" id={{ $key }} value={{ $value }}></input> + {{ end }} + + {{ if isset .Params `displayCount` }} + <input type="hidden" class="displayCount" value={{ .Get "displayCount" }}></input> + {{ end }} + + {{ if isset .Params `stats` }} + <input type="hidden" id="stats" value={{ .Get "stats" }}></input> + {{ end }} + + <!-- End of widget arguments --> + + <h1 class="project-name"></h2> + <!-- + <h2 class="project-header">Overview</h2> + --> + + <div class="gitlab-overview" class="gitlab-overview"> + + <div id="overview"> + <span class="label">Releases:</span> + <span class="gitlab-overview-release-list"></span> + </div> + + <table class="gitlab-progress-list"> + <th>Release</th> + <th>Completion</th> + <th>Due Date</th> + <th class="percentage-column">Percentage</th> + </table> + + <div class="gitlab-overview-footer"> + <div class="footer-element"> + <span class="label">Commits:</span> + <span id="commits"></span> + </div> + <div class="footer-element"> + <span class="label">Tickets:</span> + <span id="issues"> + <span id="issue-count"></span> + <span>/</span> + <span id="issue-total"></span> + </span> + </div> + <div class="footer-element"> + <span class="label">MR:</span> + <span id="merge-requests"> + <span id="mr-count"></span> + <span>/</span> + <span id="mr-total"></span> + </span> + </div> + <div class="footer-element"> + <span class="label">Stars:</span> + <span id="stars"></span> + </div> + </div> + </div> +</div> diff --git a/static/css/gitlab-widgets/gitlab-widgets.css b/static/css/gitlab-widgets/gitlab-widgets.css new file mode 100644 index 000000000..d3e748ca5 --- /dev/null +++ b/static/css/gitlab-widgets/gitlab-widgets.css @@ -0,0 +1,110 @@ +.project-name { + font-size: 36px !important; + text-align: center; + background-color: #gray; +} + +.project-header { + font-size: 32px !important; + text-align: center; + margin-bottom: 0; +} + +.gitlab-overview #overview { + width: 50%; + padding-top: 1em; + padding-bottom: 1em; + margin: auto; + display: flex; +} + +#gitlab-overview .label { + font-weight: bold; +} + +#overview-release-list span { + margin: auto; +} + +.gitlab-overview-release-list { + width: 80%; + display: flex; +} + +.gitlab-overview-release-list a { + width: 100%; + text-align: center; +} + + +.gitlab-progress-list { + width: 90%; + margin: auto; +} + +.gitlab-progress-list .gitlabOverviewEntry { + width: auto; + margin: 0 0; +} + +.gitlab-progress-list tr td { + white-space: nowrap; +} + +.gitlab-progress-list .percentage-column { + width: 100%; +} + +.gitlab-progress-list .gitlabOverviewEntry span { + width: 15em; +} + +.gitlab-progress-list .gitlabOverviewEntry progress { + width: 100%; + margin: 0 0; +} + +.gitlab-progress-list .gitlabOverviewEntry td { + padding: 1em; +} + +.gitlab-overview-footer { + display: flex; + justify-content: space-between; + margin: auto; + width: 90%; + padding-top: 1em; +} + +.gitlab-overview-footer .footer-element { + padding-left: 2em; + padding-right: 2em; +} + +.gitlab-overview-footer .footer-element span { + padding: 1em; +} + +.gitlab-overview-footer #commits { + text-align: center; +} + +.gitlab-overview-footer #issues { + text-align: center; +} + +.gitlab-overview-footer #merge-requests { + text-align: center; +} + +.gitlab-overview-footer #stars { + text-align: center; +} + +.green { + color: green; +} + +.red { + color: red; +} diff --git a/static/javascripts/gitlab-widgets/gitlab-widgets.js b/static/javascripts/gitlab-widgets/gitlab-widgets.js new file mode 100644 index 000000000..a1ef8c512 --- /dev/null +++ b/static/javascripts/gitlab-widgets/gitlab-widgets.js @@ -0,0 +1,289 @@ +/* + * + * Examples of use of the gitlab overview widget + * {{< gitlab-overview servers="158:https://git.cleverthis.com/,1876:https://git.qoto.org/" displayCount=2 stats=true v9.2.6="http://example.com" >}} + * + * {{< gitlab-overview servers="158:https://git.cleverthis.com/" displayCount=1 stats=true v9.2.6="http://example.com" >}} + * + * {{< gitlab-overview servers="166:https://git.cleverthis.com/" v1.0="http://example2.com/" >}} + */ + + +const GITLAB_CLEVERTHIS_TOKEN = "TOKEN_HERE" +const GITLAB_QOTO_TOKEN = "TOKEN2_HERE" + +const PUBLIC_PROJECT_ENDPOINT = "/projects/" + +const ENDPOINTS = { + PROJECT: "/api/v4/projects/", + + P: { + MILESTONES: "/milestones", + ISSUES: "/issues", + MERGE_REQUESTS: "/merge_requests", + COMMITS: "/repository/commits", + }, + ISSUES: "/api/v4/issues", +} + +const progressBarTitle = '<span class="releaseTitle"></span>'; +const progressBar = '<progress class="releaseProgressBar"></progress>'; + +const issuesSearchParams = { scope: "all" }; +const issuesBugSearchParams = { scope: "all", labels: "bug" }; +const issuesClosedSearchParams = { scope: "all", state: "closed" }; + +function initOverviewWidgets() { + for(var widget of $(".gitlab-overview-container")) { + initializeWidget(widget) + } +} + +function initializeWidget(widget) { + var overviewWidget = $(widget) + var projectName = overviewWidget.find(".project-name") + var releaseList = overviewWidget.find(".gitlab-overview-release-list") + var progressList = overviewWidget.find(".gitlab-progress-list") + var projectId = getGitlabServerProjectID(overviewWidget, 0) + var displayCount = getDisplayCount(overviewWidget) + + apiCall(buildProjectURL(overviewWidget, 0), (data) => { + projectName.text(data['path_with_namespace']) + }); + + apiCall(buildProjectURL(overviewWidget, 0) + ENDPOINTS.P.MILESTONES, (data) => { + + var displayedData = null + + if(displayCount != null) { + displayedData = data.slice(0, displayCount) + } else { + displayedData = data + } + + displayedData.forEach(release => { + let releaseAnchor = generateMilestoneAnchor(release) + releaseAnchor.appendTo(releaseList) + }) + + styleReleaseName(overviewWidget, releaseList.children()) + + populateProgressList(overviewWidget, progressList, data) + + let displayStats = $('#stats') + if(displayStats.length > 0 && displayStats[0].value == "true") { + populateFooter(overviewWidget) + } else { + removeFooter() + } + }) +} + +async function populateFooter(widget) { + var starCount = 0 + var issueCount = 0 + var issueTotal = 0 + var mergeRequestCount = 0 + var mergeRequestTotal = 0 + var commitCount = 0 + + for(let i = 0; i < getGitlabServerCount(widget); i++) { + // Project info + await apiCall(buildProjectURL(widget, i), (data) => { + starCount += data['star_count'] + issueCount += data['open_issues_count'] + }) + + // Issue count + await apiCall(buildProjectURL(widget, i) + ENDPOINTS.P.ISSUES, (data) => { + issueTotal += data.length + }) + + // Merge request count + let openMRSearchParams = new URLSearchParams({ state: "opened" }) + await apiCall(buildProjectURL(widget, i) + ENDPOINTS.P.MERGE_REQUESTS + "?" + openMRSearchParams.toString(), (data) => { + mergeRequestCount += data.length + }) + + // Merge request count, part 2 + let allMRSearchParams = new URLSearchParams({ state: "all" }) + await apiCall(buildProjectURL(widget, i) + ENDPOINTS.P.MERGE_REQUESTS + "?" + allMRSearchParams.toString(), (data) => { + mergeRequestTotal += data.length + }) + + // Count the project commits + let projectStatsSearchParams = new URLSearchParams({ statistics: "true" }) + await apiCall(buildProjectURL(widget, i) + "?" + projectStatsSearchParams.toString(), (data) => { + // We don't sum these values because we don't want to double-count commits + // We want the higher count, though + let singleProjectCommitCount = data['statistics']['commit_count'] + if(commitCount < singleProjectCommitCount) { + commitCount = singleProjectCommitCount + } + }) + } + + widget.find(".gitlab-overview-footer #stars").text(starCount) + widget.find(".gitlab-overview-footer #issue-count").text(issueCount) + widget.find(".gitlab-overview-footer #issue-total").text(issueTotal) + widget.find(".gitlab-overview-footer #mr-count").text(mergeRequestCount) + widget.find(".gitlab-overview-footer #mr-total").text(mergeRequestTotal) + widget.find(".gitlab-overview-footer #commits").text(commitCount) +} + +function removeFooter() { + $('.gitlab-overview-footer').remove() +} + +function styleReleaseName(widget, releases) { + Array.from(releases).forEach(release => { + var allBugIssuesParams = new URLSearchParams({ ...issuesBugSearchParams, milestone: release.innerText }) + apiCall(getGitlabServerURL(widget, 0) + ENDPOINTS.ISSUES + "?" + allBugIssuesParams.toString(), (data) => { + if(data.length == 0) { + $(release).addClass("green") + } else { + $(release).addClass("red") + } + }); + }) +} + +async function populateProgressList(widget, progressList, releases) { + var displayCount = getDisplayCount(widget) + + var displayedReleases = null + + if(displayCount != null) { + displayedReleases = releases.slice(0, displayCount) + } else { + displayedReleases = releases + } + + + for(const release of displayedReleases) { + let releaseAnchor = generateMilestoneAnchor(release) + let releaseNameDatum = $("<td></td>").append(releaseAnchor) + + var entry = $('<tr class="gitlabOverviewEntry"></div>').appendTo(progressList); + + releaseNameDatum.appendTo(entry) + + var percentageComplete = $('<td></td>').appendTo(entry) + var dueDate = $('<td></td>').appendTo(entry) + + var entryBar = entry.append('<td><progress></progress></td>'); + + var allIssuesParams = new URLSearchParams({ ...issuesSearchParams, milestone: release.title }) + var closedIssuesParams = new URLSearchParams({ ...issuesClosedSearchParams, milestone: release.title }) + + var allIssuesCount = 0 + var closedIssuesCount = 0 + + for(let i = 0; i < getGitlabServerCount(widget); i++) { + + // Get issues of milestone_title, open and closed + await apiCall(getGitlabServerURL(widget, i) + ENDPOINTS.ISSUES + "?" + allIssuesParams.toString(), (allIssuesData) => { + allIssuesCount += allIssuesData.length + }); + + await apiCall(getGitlabServerURL(widget, i) + ENDPOINTS.ISSUES + "?" + closedIssuesParams.toString(), (closedIssuesData) => { + closedIssuesCount += closedIssuesData.length + }); + } + + + // Initialize progress bar + if(allIssuesCount == 0) { + percentageComplete.text("0%") + } else { + let progressText = ((closedIssuesCount / allIssuesCount)*100).toFixed(1) + "%" + percentageComplete.text(progressText) + } + + entry.find("progress").attr('max', allIssuesCount); + entry.find("progress").val(closedIssuesCount); + + let progress = entryBar.find("progress") + let progressMax = progress[0].max + let progressCount = progress.val() + + let progressText = (progressCount / progressMax)*100 + "%" + + if(release['due_date'] == null) { + dueDate.text("No due date") + } else { + dueDate.text(release['due_date']) + } + } +} + +/* + * Helper functions + * + */ + +function getDisplayCount(widget) { + var overviewWidget = $(widget) + var displayCount = null + + if(overviewWidget.find('.displayCount').length > 0) { + displayCount = overviewWidget.find('.displayCount')[0].value + } + + return displayCount +} + +function getGitlabServerCount(widget) { + return widget.find("#servers").val().split(",").length +} + +function getGitlabServerURLs() { + return $("#servers") +} + +function getGitlabServerURL(widget, index) { + var overviewWidget = $(widget) + return overviewWidget.find("#servers").val().split(",")[index].split(/:(.*)/s)[1] +} + +function getGitlabServerProjectID(widget, index) { + var overviewWidget = $(widget) + return overviewWidget.find("#servers").val().split(",")[index].split(/:(.*)/s)[0] +} + +function generateMilestoneAnchor(release) { + let override = $("#" + $.escapeSelector(release.title)) + if (override.length > 0) { + return $("<a href='" + override.val() + "'>" + release.title + "</a>") + } else { + return $("<a href='" + release['web_url'] + "'>" + release.title + "</a>") + } +} + +function buildProjectURL(widget, index) { + const endpoint = ENDPOINTS.PROJECT + getGitlabServerProjectID(widget, index) + var url = new URL(endpoint, getGitlabServerURL(widget, index)) + return url +} + +function apiCall(endpoint, callback) { + var token = null + if(endpoint.toString().includes("cleverthis.com")) { + token = GITLAB_CLEVERTHIS_TOKEN + } else if(endpoint.toString().includes("qoto.org")) { + token = GITLAB_QOTO_TOKEN + } else { + return + } + + return $.ajax({ + url: endpoint, + dataType: 'json', + headers: { "PRIVATE-TOKEN": token }, + }).done(callback) +} + +// Only initialize widgets when page is done loading +$(document).ready(() => { + initOverviewWidgets() +}) -- GitLab