static/plugins/ghactions.js (94 lines of code) (raw):
let ghactions_json = null;
const DEFAULT_HOURS = 168;
const DEFAULT_LIMIT = 15; // top N items
const DEFAULT_GROUP = "name"; // Group workflows by name or path
const DEFAULT_OTHERS_GHA = "(other projects)";
const DEFAULT_OTHERS_GHA_SINGLE = "(other builds)";
async function seed_ghactions() {
let qs = new URLSearchParams(document.location.hash);
let qsnew = new URLSearchParams();
if (qs.get("project")) qsnew.set("project", qs.get("project"));
if (qs.get("hours")) qsnew.set("hours", qs.get("hours"));
if (qs.get("limit")) qsnew.set("limit", qs.get("limit"));
if (qs.get("group")) qsnew.set("group", qs.get("group"));
if (qs.get("selfhosted")) qsnew.set("selfhosted", qs.get("selfhosted"));
ghactions_json = await (await fetch(`/api/ghactions?${qsnew.toString()}`)).json();
ghactions_json.all_projects.unshift("All projects");
show_ghactions(qs.get("project"), parseInt(qs.get("hours")||DEFAULT_HOURS), parseInt(qs.get("limit")||DEFAULT_LIMIT), qs.get("group")||DEFAULT_GROUP, qs.get("selfhosted")||false);
}
async function render_dashboard_ghactions() {
await OAuthGate(seed_ghactions);
}
function seconds_to_text(seconds) {
const hours = Math.floor(seconds/3600);
const minutes = Math.floor(seconds%3600/60);
return `${hours}h${minutes}m`;
}
function setHash(project, hours, limit, group, selfhosted) {
let newHash = "#ghactions";
if (project) newHash += "&project=" + project;
if (hours) newHash += "&hours=" + hours;
if (limit) newHash += "&limit=" + limit;
if (group) newHash += "&group=" + group;
if (selfhosted) newHash += "&selfhosted=true";
location.hash = newHash;
}
async function click_gha_project(params, old_project, hours, limit, group) {
if (params && params.name && params.name !== DEFAULT_OTHERS_GHA && !old_project) { // If on global view and we click a project name, show only that project.
setHash(params.name, hours, limit, group);
await seed_ghactions();
}
}
function show_ghactions(project, hours = DEFAULT_HOURS, topN = DEFAULT_LIMIT, group = DEFAULT_GROUP, selfhosted = false) {
let project_txt = project ? project : "All projects";
if (!project) group = DEFAULT_GROUP
document.getElementById('page_title').innerText = `GitHub Actions Statistics, ${project_txt}`;
document.getElementById('page_description').innerText = "This page shows the GitHub Actions usage for projects you are a part of. If you do not see any data here, your project is likely not using GitHub Actions. By default, builds on self-hosted runners are not included in the stats, but can be included by using the self-hosted checkbox field at the bottom.";
const outer_chart_area = document.getElementById('chart_area');
outer_chart_area.innerText = "";
const cost_per_runner_minute_private = 0.01072
const cost_per_runner_minute_public = 0.006341958
const projects_by_time = {}
let total_seconds = 0;
for (const build of ghactions_json.builds) {
if (project && project !== build.project) continue
if (project) {
for (const job of build.jobs) {
// Skip self-hosted job durations by setting them to 0 seconds, unless we mean to include them.
let jd = job.job_duration;
if (!selfhosted) {
for (const label of job.labels || []) {
if (label.includes("self-hosted")) jd = 0;
}
}
// Group by workflow name or the actions .yml file used
const groupkey = (group === "name") ? job.name : (build.workflow_path||"unknown.yml");
if (jd) projects_by_time[groupkey] = (projects_by_time[groupkey] ? projects_by_time[groupkey] : 0) + jd;
}
}
else if (build.seconds_used) {
projects_by_time[build.project] = (projects_by_time[build.project] ? projects_by_time[build.project] : 0) + build.seconds_used;
}
total_seconds += build.seconds_used;
}
const r_array = [];
for (const [k,v] of Object.entries(projects_by_time)) {
r_array.push({name: k, value: v});
}
const r_array_sorted = r_array.slice();
r_array_sorted.sort((a,b) => b.value-a.value);
r_array_sorted.splice(topN);
if (r_array_sorted.length < r_array.length) {
const sumval = r_array.reduce((psum, a) => (psum.value ? psum.value : psum) + (r_array_sorted.includes(a) ? 0 : a.value));
r_array_sorted.push({
name: project ? DEFAULT_OTHERS_GHA_SINGLE : DEFAULT_OTHERS_GHA,
value: sumval,
itemStyle: {
color: "#999"
}
});
}
const legends = r_array.map((x) => x.name);
const timetxt = hours > 24 ? Math.floor(hours/24) + " days" : hours + " hours";
const donut_recipients = chart_pie(`GitHub Actions Build Time Used, past ${timetxt}.\nTotal usage: ${Math.round(total_seconds/60).pretty()} minutes, or ${Math.round(total_seconds/(hours*3600))} FT runners. Estimated credit use: \$${(cost_per_runner_minute_public*total_seconds/60).pretty()}`, "", r_array_sorted, {width: "1240px", height: "500px"}, donut=false,
fmtoptions={
value: (val) => `${val.data.name}: ${seconds_to_text(val.data.value)}, or ${Math.round(val.data.value/(hours*3600))} FT runner(s)`,
legend: (val) => `${val.data.name}: \n${((val.data.value/total_seconds)*100).toFixed(2)}%`
}, legend=null, onclick=(params) => click_gha_project(params, project, hours, topN, group));
donut_recipients.style.maxWidth = "1240px";
donut_recipients.style.height = "500px";
outer_chart_area.appendChild(donut_recipients);
// filters
const hourpicker = document.createElement('select');
hourpicker.style.marginRight = "20px";
for (const val of [1, 2, 4, 8, 12, 24, 120, 168, 720]) {
const opt = document.createElement('option');
opt.value = val;
opt.text = "Past " + (val > 24 ? Math.floor(val/24) + " days" : val + " hours");
opt.selected = val === hours;
hourpicker.appendChild(opt);
}
hourpicker.addEventListener('change', (e) => {
hours = e.target.value;
setHash(project, hours, topN, group, selfhosted);
seed_ghactions();
})
const projectpicker = document.createElement('select');
projectpicker.style.marginRight = "20px";
for (const val of ghactions_json.all_projects) {
const opt = document.createElement('option');
opt.value = val;
opt.text = val;
opt.selected = project === val;
projectpicker.appendChild(opt);
}
projectpicker.addEventListener('change', (e) => {
let val = e.target.value;
project = val.includes(" ") ? null : val;
setHash(project, hours, topN, group);
seed_ghactions();
})
const limitpicker = document.createElement('select');
limitpicker.style.marginRight = "20px";
for (const val of [10, 15, 20, 25, 30, 50]) {
const opt = document.createElement('option');
opt.value = val;
opt.text = "Top " + val;
opt.selected = val === topN;
limitpicker.appendChild(opt);
}
limitpicker.addEventListener('change', (e) => {
topN = e.target.value;
setHash(project, hours, topN, group, selfhosted);
seed_ghactions();
})
outer_chart_area.appendChild(document.createElement('br'))
outer_chart_area.appendChild(projectpicker)
outer_chart_area.appendChild(hourpicker)
outer_chart_area.appendChild(limitpicker)
// This option is only available when viewing a single project
if (project) {
const groupby = document.createElement('select');
groupby.style.marginRight = "20px";
for (const val of ['name', 'path']) {
const opt = document.createElement('option');
opt.value = val;
opt.text = "Group workflows by " + val;
opt.selected = val === group;
groupby.appendChild(opt);
}
groupby.addEventListener('change', (e) => {
group = e.target.value;
setHash(project, hours, topN, group);
seed_ghactions();
})
outer_chart_area.appendChild(groupby)
}
// include self-hosted? Only valid in single project view
if (project) {
const shcheck = document.createElement('input');
shcheck.type = "checkbox";
shcheck.id = "shosted";
shcheck.checked = !! selfhosted;
shcheck.addEventListener('change', (e) => {
selfhosted = e.target.checked;
setHash(project, hours, topN, group, selfhosted);
seed_ghactions();
});
const lbl = document.createElement('label');
lbl.setAttribute('for', 'shosted');
lbl.innerText = "Include self-hosted runners";
outer_chart_area.appendChild(shcheck);
outer_chart_area.appendChild(lbl);
}
}