mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-06-09 17:13:56 +02:00
improve styling
This commit is contained in:
+427
-82
@@ -452,11 +452,14 @@ angular
|
||||
$scope.userInfo;
|
||||
$scope.repositories = [];
|
||||
$scope.search = "";
|
||||
$scope.selected = {};
|
||||
$scope.allSelected = false;
|
||||
|
||||
const adminUserPrefsKey = "admin.user.filterPrefs";
|
||||
const adminUserDefaults = {
|
||||
filters: { status: { ready: true, expired: true, removed: true, error: true, preparing: true } },
|
||||
orderBy: "-anonymizeDate",
|
||||
sort: "anonymizeDate",
|
||||
direction: "desc",
|
||||
};
|
||||
const savedAdminUserPrefs = loadFilterPrefs(adminUserPrefsKey) || {};
|
||||
$scope.filters = {
|
||||
@@ -466,25 +469,49 @@ angular
|
||||
(savedAdminUserPrefs.filters && savedAdminUserPrefs.filters.status) || {}
|
||||
),
|
||||
};
|
||||
$scope.orderBy = savedAdminUserPrefs.orderBy || adminUserDefaults.orderBy;
|
||||
$scope.query = {
|
||||
sort: savedAdminUserPrefs.sort || adminUserDefaults.sort,
|
||||
direction: savedAdminUserPrefs.direction || adminUserDefaults.direction,
|
||||
};
|
||||
$scope.orderBy = ($scope.query.direction === "asc" ? "" : "-") + $scope.query.sort;
|
||||
|
||||
$scope.$watch("orderBy", () => {
|
||||
$scope.sortBy = (field) => {
|
||||
if ($scope.query.sort === field) {
|
||||
$scope.query.direction = $scope.query.direction === "asc" ? "desc" : "asc";
|
||||
} else {
|
||||
$scope.query.sort = field;
|
||||
$scope.query.direction = "desc";
|
||||
}
|
||||
$scope.orderBy = ($scope.query.direction === "asc" ? "" : "-") + $scope.query.sort;
|
||||
};
|
||||
$scope.sortIcon = (field) =>
|
||||
$scope.query.sort === field
|
||||
? ($scope.query.direction === "asc" ? "fa-arrow-up" : "fa-arrow-down")
|
||||
: "";
|
||||
|
||||
$scope.$watch("query", () => {
|
||||
saveFilterPrefs(adminUserPrefsKey, {
|
||||
filters: $scope.filters,
|
||||
orderBy: $scope.orderBy,
|
||||
sort: $scope.query.sort,
|
||||
direction: $scope.query.direction,
|
||||
});
|
||||
});
|
||||
}, true);
|
||||
$scope.$watch(
|
||||
"filters",
|
||||
() => {
|
||||
saveFilterPrefs(adminUserPrefsKey, {
|
||||
filters: $scope.filters,
|
||||
orderBy: $scope.orderBy,
|
||||
sort: $scope.query.sort,
|
||||
direction: $scope.query.direction,
|
||||
});
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
$scope.statusCountFor = (s) => {
|
||||
return ($scope.repositories || []).filter((r) => r.status === s).length;
|
||||
};
|
||||
|
||||
$scope.repoFiler = (repo) => {
|
||||
if ($scope.filters.status[repo.status] == false) return false;
|
||||
|
||||
@@ -492,10 +519,84 @@ angular
|
||||
|
||||
if (repo.source.fullName.indexOf($scope.search) > -1) return true;
|
||||
if (repo.repoId.indexOf($scope.search) > -1) return true;
|
||||
if (repo.statusMessage && repo.statusMessage.indexOf($scope.search) > -1) return true;
|
||||
if (repo.conference && repo.conference.indexOf($scope.search) > -1) return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// -------- selection / bulk --------
|
||||
$scope.selectAllOnPage = () => {
|
||||
$scope.allSelected = !$scope.allSelected;
|
||||
($scope.filteredRepositories || $scope.repositories).forEach((r) => {
|
||||
$scope.selected[r.repoId] = $scope.allSelected;
|
||||
});
|
||||
};
|
||||
$scope.selectedCount = () =>
|
||||
Object.values($scope.selected || {}).filter(Boolean).length;
|
||||
$scope.selectedRepos = () =>
|
||||
$scope.repositories.filter((r) => $scope.selected[r.repoId]);
|
||||
$scope.bulkRefresh = () => {
|
||||
const repos = $scope.selectedRepos();
|
||||
if (!repos.length) return;
|
||||
if (!confirm(`Force refresh ${repos.length} repositories?`)) return;
|
||||
repos.forEach((r) => $scope.updateRepository(r));
|
||||
};
|
||||
$scope.bulkRemoveCache = () => {
|
||||
const repos = $scope.selectedRepos();
|
||||
if (!repos.length) return;
|
||||
if (!confirm(`Purge cache for ${repos.length} repositories?`)) return;
|
||||
repos.forEach((r) => $scope.removeCache(r));
|
||||
};
|
||||
$scope.clearSelection = () => {
|
||||
$scope.selected = {};
|
||||
$scope.allSelected = false;
|
||||
};
|
||||
|
||||
// -------- export --------
|
||||
$scope.exportCsv = () => {
|
||||
const filtered = ($scope.filteredRepositories || $scope.repositories);
|
||||
const columns = ["repoId", "status", "statusMessage", "pageView", "anonymizeDate", "source.fullName", "conference", "size.storage"];
|
||||
const header = columns.join(",");
|
||||
const rows = filtered.map((r) =>
|
||||
[r.repoId, r.status, r.statusMessage || "", r.pageView || 0, r.anonymizeDate || "", (r.source && r.source.fullName) || "", r.conference || "", (r.size && r.size.storage) || 0]
|
||||
.map((v) => { const s = String(v == null ? "" : v); return /[",\n\r]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s; })
|
||||
.join(",")
|
||||
);
|
||||
const blob = new Blob([header + "\n" + rows.join("\n")], { type: "text/csv" });
|
||||
const a = document.createElement("a");
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = $routeParams.username + "-repositories.csv";
|
||||
a.click();
|
||||
};
|
||||
|
||||
$scope.showStatusMessage = (repo) => {
|
||||
const msg = repo.statusMessage || "(no message)";
|
||||
window.prompt(`Status message for ${repo.repoId} (${repo.status}):`, msg);
|
||||
};
|
||||
|
||||
$scope.fetchGithubInfo = (repo) => {
|
||||
const w = window.open("", "_blank");
|
||||
if (w) w.document.write("<pre>Loading GitHub info for " + repo.repoId + "...</pre>");
|
||||
$http.get("/api/admin/repos/" + repo.repoId + "/github").then(
|
||||
(res) => {
|
||||
if (w) {
|
||||
w.document.open();
|
||||
w.document.write(
|
||||
"<pre style=\"font:13px monospace;padding:16px;white-space:pre-wrap\">" +
|
||||
JSON.stringify(res.data, null, 2).replace(/[<>]/g, (c) => c === "<" ? "<" : ">") +
|
||||
"</pre>"
|
||||
);
|
||||
w.document.close();
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
const msg = err && err.data ? JSON.stringify(err.data, null, 2) : String(err);
|
||||
if (w) w.document.body.innerHTML = "<pre style=\"color:#B42318;padding:16px\">" + msg + "</pre>";
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function getUserRepositories(username) {
|
||||
$http.get("/api/admin/users/" + username + "/repos", {}).then(
|
||||
(res) => {
|
||||
@@ -530,6 +631,18 @@ angular
|
||||
.post(`/api/admin/users/${$routeParams.username}/activate`)
|
||||
.then(() => getUser($routeParams.username), (err) => console.error(err));
|
||||
};
|
||||
$scope.promoteUser = () => {
|
||||
if (!confirm(`Promote ${$routeParams.username} to admin?`)) return;
|
||||
$http
|
||||
.post(`/api/admin/users/${$routeParams.username}/promote`)
|
||||
.then(() => getUser($routeParams.username), (err) => console.error(err));
|
||||
};
|
||||
$scope.demoteUser = () => {
|
||||
if (!confirm(`Remove admin privileges from ${$routeParams.username}?`)) return;
|
||||
$http
|
||||
.post(`/api/admin/users/${$routeParams.username}/demote`)
|
||||
.then(() => getUser($routeParams.username), (err) => console.error(err));
|
||||
};
|
||||
|
||||
$scope.tokens = [];
|
||||
$scope.tokenForm = { name: "", plaintext: null };
|
||||
@@ -667,7 +780,9 @@ angular
|
||||
};
|
||||
$scope.chips = [];
|
||||
const recomputeChipsConf = () => {
|
||||
$scope.chips = [];
|
||||
const out = [];
|
||||
if ($scope.query.dateFrom || $scope.query.dateTo) out.push({ key: "dateRange", label: "Date", value: ($scope.query.dateFrom || "…") + " – " + ($scope.query.dateTo || "…") });
|
||||
$scope.chips = out;
|
||||
};
|
||||
|
||||
$scope.statusCountFor = (s) => {
|
||||
@@ -699,9 +814,13 @@ angular
|
||||
sort: "name",
|
||||
direction: "asc",
|
||||
search: "",
|
||||
status: "",
|
||||
dateFrom: "",
|
||||
dateTo: "",
|
||||
ready: false,
|
||||
expired: false,
|
||||
removed: false,
|
||||
error: true,
|
||||
preparing: true,
|
||||
};
|
||||
const savedConfAdminPrefs = loadFilterPrefs(confAdminPrefsKey) || {};
|
||||
$scope.query = Object.assign({}, confAdminDefaults, savedConfAdminPrefs, {
|
||||
@@ -709,6 +828,38 @@ angular
|
||||
search: "",
|
||||
});
|
||||
|
||||
// pre-fill filters from URL ?search=
|
||||
const urlParams = $location.search();
|
||||
if (urlParams.search) $scope.query.search = urlParams.search;
|
||||
|
||||
// -------- presets --------
|
||||
const confPresetsKey = "admin.conferences.presets";
|
||||
$scope.presets = JSON.parse(localStorage.getItem(confPresetsKey) || "[]");
|
||||
$scope.savePreset = () => {
|
||||
const name = window.prompt("Preset name:");
|
||||
if (!name) return;
|
||||
const snapshot = Object.assign({}, $scope.query);
|
||||
delete snapshot.page;
|
||||
$scope.presets = ($scope.presets || []).filter((p) => p.name !== name);
|
||||
$scope.presets.push({ name, query: snapshot });
|
||||
localStorage.setItem(confPresetsKey, JSON.stringify($scope.presets));
|
||||
};
|
||||
$scope.applyPreset = (p) => {
|
||||
Object.assign($scope.query, p.query, { page: 1 });
|
||||
};
|
||||
$scope.deletePreset = (p) => {
|
||||
$scope.presets = ($scope.presets || []).filter((x) => x.name !== p.name);
|
||||
localStorage.setItem(confPresetsKey, JSON.stringify($scope.presets));
|
||||
};
|
||||
|
||||
$scope.removeConference = (conference) => {
|
||||
if (!confirm("Remove conference " + conference.conferenceID + "?")) return;
|
||||
$http.delete("/api/admin/conferences/" + conference.conferenceID).then(
|
||||
() => getConferences(),
|
||||
(err) => console.error(err)
|
||||
);
|
||||
};
|
||||
|
||||
$scope.exportCsv = () => {
|
||||
const params = new URLSearchParams(
|
||||
Object.entries($scope.query).filter(([, v]) => v !== "" && v !== false && v != null)
|
||||
@@ -727,7 +878,6 @@ angular
|
||||
$scope.totalPage = Math.ceil(res.data.total / $scope.query.limit);
|
||||
$scope.conferences = res.data.results;
|
||||
$scope.statusCounts = res.data.statusCounts || [];
|
||||
$scope.$apply();
|
||||
},
|
||||
(err) => {
|
||||
$scope.fetchError = (err && err.data && err.data.error) || "Failed to load conferences";
|
||||
@@ -757,39 +907,24 @@ angular
|
||||
"$http",
|
||||
"$location",
|
||||
"$interval",
|
||||
function ($scope, $http, $location, $interval) {
|
||||
"$timeout",
|
||||
function ($scope, $http, $location, $interval, $timeout) {
|
||||
$scope.$watch("user.status", () => {
|
||||
if ($scope.user == null) {
|
||||
$location.url("/");
|
||||
}
|
||||
if ($scope.user == null) $location.url("/");
|
||||
});
|
||||
if ($scope.user == null) {
|
||||
$location.url("/");
|
||||
}
|
||||
if ($scope.user == null) $location.url("/");
|
||||
|
||||
$scope.downloadJobs = [];
|
||||
$scope.removeJobs = [];
|
||||
$scope.removeCaches = [];
|
||||
$scope.counts = { download: {}, remove: {}, cache: {} };
|
||||
$scope.queueList = [];
|
||||
$scope.jobs = [];
|
||||
$scope.selectedQueue = "download";
|
||||
$scope.selectedStats = null;
|
||||
$scope.range = "1h";
|
||||
$scope.query = {
|
||||
search: "",
|
||||
state: "",
|
||||
state: "active",
|
||||
autoRefresh: true,
|
||||
};
|
||||
|
||||
$scope.jobMatchesState = (job) => {
|
||||
if (!$scope.query.state) return true;
|
||||
const finished = !!job.finishedOn;
|
||||
const failed = (job.stacktrace || []).length > 0 || job.failedReason;
|
||||
const map = {
|
||||
completed: finished && !failed,
|
||||
failed: failed,
|
||||
active: job.processedOn && !finished,
|
||||
waiting: !job.processedOn,
|
||||
};
|
||||
return !!map[$scope.query.state];
|
||||
};
|
||||
|
||||
$scope.jobProgressPct = (job) => {
|
||||
if (job && job.progress && typeof job.progress === "object" && typeof job.progress.percent === "number") {
|
||||
return Math.max(0, Math.min(100, Math.round(job.progress.percent)));
|
||||
@@ -800,31 +935,42 @@ angular
|
||||
return null;
|
||||
};
|
||||
|
||||
$scope.bulkRetryFailed = (queue) => {
|
||||
if (!confirm(`Retry all failed jobs in the ${queue} queue?`)) return;
|
||||
$http.post(`/api/admin/queue/${queue}/retry-failed`).then(getQueues, (err) => console.error(err));
|
||||
$scope.jobDuration = (job) => {
|
||||
if (!job.processedOn) return "-";
|
||||
const end = job.finishedOn || Date.now();
|
||||
const ms = end - job.processedOn;
|
||||
if (ms < 1000) return ms + "ms";
|
||||
return (ms / 1000).toFixed(1) + "s";
|
||||
};
|
||||
$scope.bulkDrain = (queue) => {
|
||||
if (!confirm(`Drain (clear waiting+delayed) the ${queue} queue?`)) return;
|
||||
$http.post(`/api/admin/queue/${queue}/drain`).then(getQueues, (err) => console.error(err));
|
||||
|
||||
$scope.selectQueue = (key) => {
|
||||
$scope.selectedQueue = key;
|
||||
getQueues();
|
||||
};
|
||||
|
||||
$scope.setRange = (r) => {
|
||||
$scope.range = r;
|
||||
getQueues();
|
||||
};
|
||||
|
||||
function getQueues() {
|
||||
$http.get("/api/admin/queues", { params: $scope.query }).then(
|
||||
const params = {
|
||||
queue: $scope.selectedQueue,
|
||||
state: $scope.query.state,
|
||||
search: $scope.query.search,
|
||||
};
|
||||
$http.get("/api/admin/queues", { params }).then(
|
||||
(res) => {
|
||||
$scope.downloadJobs = res.data.downloadQueue;
|
||||
$scope.removeJobs = res.data.removeQueue;
|
||||
$scope.removeCaches = res.data.cacheQueue;
|
||||
$scope.counts = res.data.counts || $scope.counts;
|
||||
$scope.queueList = res.data.queues || [];
|
||||
$scope.jobs = res.data.jobs || [];
|
||||
$scope.selectedStats = $scope.queueList.find((q) => q.key === $scope.selectedQueue) || $scope.queueList[0] || null;
|
||||
$timeout(drawChart, 0);
|
||||
},
|
||||
(err) => {
|
||||
console.error(err);
|
||||
}
|
||||
(err) => console.error(err)
|
||||
);
|
||||
}
|
||||
getQueues();
|
||||
|
||||
// auto-refresh every 5 seconds while autoRefresh is on
|
||||
const stop = $interval(() => {
|
||||
if ($scope.query.autoRefresh) getQueues();
|
||||
}, 5000);
|
||||
@@ -832,45 +978,126 @@ angular
|
||||
|
||||
$scope.refreshNow = getQueues;
|
||||
|
||||
$scope.removeJob = function (queue, job) {
|
||||
$http
|
||||
.delete(`/api/admin/queue/${queue}/${job.id}`, {
|
||||
params: $scope.query,
|
||||
})
|
||||
.then(
|
||||
(res) => {
|
||||
getQueues();
|
||||
},
|
||||
(err) => {
|
||||
console.error(err);
|
||||
}
|
||||
);
|
||||
$scope.removeJob = (job) => {
|
||||
$http.delete(`/api/admin/queue/${$scope.selectedQueue}/${job.id}`).then(getQueues, (err) => console.error(err));
|
||||
};
|
||||
|
||||
$scope.retryJob = function (queue, job) {
|
||||
$http
|
||||
.post(`/api/admin/queue/${queue}/${job.id}`, {
|
||||
params: $scope.query,
|
||||
})
|
||||
.then(
|
||||
(res) => {
|
||||
getQueues();
|
||||
},
|
||||
(err) => {
|
||||
console.error(err);
|
||||
}
|
||||
);
|
||||
$scope.retryJob = (job) => {
|
||||
$http.post(`/api/admin/queue/${$scope.selectedQueue}/${job.id}`).then(getQueues, (err) => console.error(err));
|
||||
};
|
||||
|
||||
$scope.retryFailed = () => {
|
||||
if (!confirm(`Retry all failed jobs in ${$scope.selectedQueue}?`)) return;
|
||||
$http.post(`/api/admin/queue/${$scope.selectedQueue}/retry-failed`).then(getQueues, (err) => console.error(err));
|
||||
};
|
||||
|
||||
$scope.drainSelected = () => {
|
||||
if (!confirm(`Drain the ${$scope.selectedQueue} queue?`)) return;
|
||||
$http.post(`/api/admin/queue/${$scope.selectedQueue}/drain`).then(getQueues, (err) => console.error(err));
|
||||
};
|
||||
|
||||
$scope.togglePause = () => {
|
||||
const action = $scope.selectedStats && $scope.selectedStats.paused ? "resume" : "pause";
|
||||
$http.post(`/api/admin/queue/${$scope.selectedQueue}/${action}`).then(getQueues, (err) => console.error(err));
|
||||
};
|
||||
|
||||
$scope.emptyQueue = () => {
|
||||
if (!confirm(`Empty the ${$scope.selectedQueue} queue? This removes ALL jobs.`)) return;
|
||||
$http.post(`/api/admin/queue/${$scope.selectedQueue}/empty`).then(getQueues, (err) => console.error(err));
|
||||
};
|
||||
|
||||
$scope.pauseAll = () => {
|
||||
if (!confirm("Pause all queues?")) return;
|
||||
$http.post("/api/admin/queues/pause-all").then(getQueues, (err) => console.error(err));
|
||||
};
|
||||
|
||||
let searchClear = null;
|
||||
$scope.$watch(
|
||||
"query.search",
|
||||
() => {
|
||||
clearTimeout(searchClear);
|
||||
searchClear = setTimeout(getQueues, 350);
|
||||
}
|
||||
);
|
||||
$scope.$watch("query.search", () => {
|
||||
clearTimeout(searchClear);
|
||||
searchClear = setTimeout(getQueues, 350);
|
||||
});
|
||||
$scope.$watch("query.state", getQueues);
|
||||
|
||||
function drawChart() {
|
||||
const canvas = document.getElementById("q-throughput-chart");
|
||||
if (!canvas || !$scope.selectedStats) return;
|
||||
const ctx = canvas.getContext("2d");
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const rect = canvas.parentElement.getBoundingClientRect();
|
||||
const w = rect.width - 40;
|
||||
const h = 160;
|
||||
canvas.width = w * dpr;
|
||||
canvas.height = h * dpr;
|
||||
canvas.style.width = w + "px";
|
||||
canvas.style.height = h + "px";
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
|
||||
const data = ($scope.selectedStats.throughput || []).slice().reverse();
|
||||
if (data.length === 0) {
|
||||
ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue("--ink-muted").trim() || "#8A857C";
|
||||
ctx.font = "12px var(--font-mono)";
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText("No throughput data yet", w / 2, h / 2);
|
||||
return;
|
||||
}
|
||||
|
||||
const rangePoints = { "1h": 60, "6h": 120, "24h": 120, "7d": 120 };
|
||||
const pts = data.slice(0, rangePoints[$scope.range] || 60);
|
||||
const max = Math.max(1, ...pts);
|
||||
const step = w / (pts.length - 1 || 1);
|
||||
|
||||
const isDark = document.body.classList.contains("dark-mode");
|
||||
const lineColor = isDark ? "#A7B2FF" : "#3B4AD6";
|
||||
const fillColor = isDark ? "rgba(167,178,255,0.12)" : "rgba(59,74,214,0.08)";
|
||||
const gridColor = isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.06)";
|
||||
|
||||
// grid
|
||||
ctx.strokeStyle = gridColor;
|
||||
ctx.lineWidth = 1;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const y = (h / 4) * i;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y);
|
||||
ctx.lineTo(w, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// area fill
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, h);
|
||||
pts.forEach((v, i) => {
|
||||
const x = i * step;
|
||||
const y = h - (v / max) * (h - 10);
|
||||
if (i === 0) ctx.lineTo(x, y);
|
||||
else {
|
||||
const px = (i - 1) * step;
|
||||
const py = h - (pts[i - 1] / max) * (h - 10);
|
||||
const cx = (px + x) / 2;
|
||||
ctx.bezierCurveTo(cx, py, cx, y, x, y);
|
||||
}
|
||||
});
|
||||
ctx.lineTo(w, h);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = fillColor;
|
||||
ctx.fill();
|
||||
|
||||
// line
|
||||
ctx.beginPath();
|
||||
pts.forEach((v, i) => {
|
||||
const x = i * step;
|
||||
const y = h - (v / max) * (h - 10);
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else {
|
||||
const px = (i - 1) * step;
|
||||
const py = h - (pts[i - 1] / max) * (h - 10);
|
||||
const cx = (px + x) / 2;
|
||||
ctx.bezierCurveTo(cx, py, cx, y, x, y);
|
||||
}
|
||||
});
|
||||
ctx.strokeStyle = lineColor;
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.stroke();
|
||||
}
|
||||
},
|
||||
])
|
||||
.controller("errorsAdminController", [
|
||||
@@ -1310,4 +1537,122 @@ angular
|
||||
$scope.$watch("query.sort", recompute);
|
||||
$scope.$watch("query.group", recompute);
|
||||
},
|
||||
])
|
||||
.controller("overviewAdminController", [
|
||||
"$scope",
|
||||
"$http",
|
||||
"$location",
|
||||
"$interval",
|
||||
function ($scope, $http, $location, $interval) {
|
||||
$scope.Math = Math;
|
||||
$scope.$watch("user.status", () => {
|
||||
if ($scope.user == null) $location.url("/");
|
||||
});
|
||||
if ($scope.user == null) { $location.url("/"); return; }
|
||||
|
||||
$scope.data = null;
|
||||
$scope.loading = true;
|
||||
$scope.error = null;
|
||||
$scope.range = "24h";
|
||||
|
||||
$scope.setRange = function (r) { $scope.range = r; };
|
||||
|
||||
function humanBytes(b) {
|
||||
if (b == null) return "—";
|
||||
var units = ["B","KB","MB","GB","TB"];
|
||||
var i = 0;
|
||||
var v = b;
|
||||
while (v >= 1024 && i < units.length - 1) { v /= 1024; i++; }
|
||||
return v.toFixed(i > 0 ? 1 : 0) + " " + units[i];
|
||||
}
|
||||
$scope.humanBytes = humanBytes;
|
||||
|
||||
function humanDuration(seconds) {
|
||||
if (!seconds) return "—";
|
||||
var d = Math.floor(seconds / 86400);
|
||||
var h = Math.floor((seconds % 86400) / 3600);
|
||||
var m = Math.floor((seconds % 3600) / 60);
|
||||
if (d > 0) return d + "d " + (h < 10 ? "0" : "") + h + "h";
|
||||
if (h > 0) return h + "h " + (m < 10 ? "0" : "") + m + "m";
|
||||
return m + "m";
|
||||
}
|
||||
$scope.humanDuration = humanDuration;
|
||||
|
||||
function humanNum(n) {
|
||||
if (n == null) return "—";
|
||||
if (n >= 1000000) return (n / 1000000).toFixed(1) + "M";
|
||||
if (n >= 1000) return (n / 1000).toFixed(1) + "K";
|
||||
return String(n);
|
||||
}
|
||||
$scope.humanNum = humanNum;
|
||||
|
||||
$scope.queueTotal = function (q) {
|
||||
if (!q) return 0;
|
||||
return (q.waiting || 0) + (q.active || 0) + (q.delayed || 0) + (q.failed || 0);
|
||||
};
|
||||
|
||||
$scope.statusCount = function (status) {
|
||||
if (!$scope.data || !$scope.data.repos) return 0;
|
||||
var bd = $scope.data.repos.statusBreakdown || [];
|
||||
for (var i = 0; i < bd.length; i++) {
|
||||
if (bd[i]._id === status) return bd[i].count;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
$scope.barPct = function (status) {
|
||||
var total = $scope.data && $scope.data.repos ? $scope.data.repos.total : 0;
|
||||
if (!total) return 0;
|
||||
var names = [status];
|
||||
if (status === "expired") names.push("expiring");
|
||||
if (status === "removed") names.push("removing");
|
||||
if (status === "preparing") names.push("download");
|
||||
var sum = 0;
|
||||
names.forEach(function (n) { sum += $scope.statusCount(n); });
|
||||
return Math.max(0.4, (sum / total) * 100);
|
||||
};
|
||||
|
||||
$scope.errPct = function (key) {
|
||||
if (!$scope.data || !$scope.data.errors) return 0;
|
||||
var max = Math.max(
|
||||
$scope.data.errors.severity.error,
|
||||
$scope.data.errors.severity.warn,
|
||||
$scope.data.errors.severity.info,
|
||||
1
|
||||
);
|
||||
return ($scope.data.errors.severity[key] / max) * 100;
|
||||
};
|
||||
|
||||
var historyMaxes = {};
|
||||
$scope.historyBarH = function (d, field) {
|
||||
if (!d || !historyMaxes[field]) return 0;
|
||||
return Math.max(1, Math.round((d[field] / historyMaxes[field]) * 140));
|
||||
};
|
||||
$scope.historyLabel = function (d) {
|
||||
if (!d || !d.date) return "";
|
||||
var dt = new Date(d.date);
|
||||
return (dt.getUTCMonth() + 1) + "/" + dt.getUTCDate();
|
||||
};
|
||||
|
||||
function load() {
|
||||
$http.get("/api/admin/overview").then(function (r) {
|
||||
$scope.data = r.data;
|
||||
$scope.loading = false;
|
||||
$scope.error = null;
|
||||
historyMaxes = {};
|
||||
(r.data.history || []).forEach(function (d) {
|
||||
["nbPageViews", "nbRepositories", "nbUsers"].forEach(function (k) {
|
||||
if (!historyMaxes[k] || d[k] > historyMaxes[k]) historyMaxes[k] = d[k];
|
||||
});
|
||||
});
|
||||
}, function (err) {
|
||||
$scope.loading = false;
|
||||
$scope.error = (err.data && err.data.error) || "Failed to load overview";
|
||||
});
|
||||
}
|
||||
|
||||
load();
|
||||
var stop = $interval(load, 30000);
|
||||
$scope.$on("$destroy", function () { $interval.cancel(stop); });
|
||||
},
|
||||
]);
|
||||
|
||||
+388
-81
@@ -113,6 +113,11 @@ angular
|
||||
reloadOnUrl: false,
|
||||
})
|
||||
.when("/admin/", {
|
||||
templateUrl: "/partials/admin/overview.htm",
|
||||
controller: "overviewAdminController",
|
||||
title: "Admin · Overview – Anonymous GitHub",
|
||||
})
|
||||
.when("/admin/repositories", {
|
||||
templateUrl: "/partials/admin/repositories.htm",
|
||||
controller: "repositoriesAdminController",
|
||||
title: "Admin · Repositories – Anonymous GitHub",
|
||||
@@ -469,7 +474,7 @@ angular
|
||||
function () {
|
||||
return {
|
||||
restrict: "E",
|
||||
scope: { file: "=", parent: "@" },
|
||||
scope: { file: "=", parent: "@", searchQuery: "=", searchResults: "=" },
|
||||
controller: [
|
||||
"$element",
|
||||
"$scope",
|
||||
@@ -491,26 +496,44 @@ angular
|
||||
const toArray = function (arr) {
|
||||
const output = [];
|
||||
const keys = { "": { child: output } };
|
||||
function ensurePath(path) {
|
||||
if (keys[path]) return;
|
||||
const segments = path.split("/");
|
||||
let acc = "";
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const parent = acc;
|
||||
acc = acc ? acc + "/" + segments[i] : segments[i];
|
||||
if (!keys[acc]) {
|
||||
const dir = { name: segments[i], child: [] };
|
||||
keys[acc] = dir;
|
||||
keys[parent].child.push(dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let file of arr) {
|
||||
let current = keys[file.path].child;
|
||||
if (file.path && !keys[file.path]) {
|
||||
ensurePath(file.path);
|
||||
}
|
||||
let current = keys[file.path || ""].child;
|
||||
let fPath = `${file.path}/${file.name}`;
|
||||
if (fPath.startsWith("/")) {
|
||||
fPath = fPath.substring(1);
|
||||
}
|
||||
if (file.size != null) {
|
||||
// it is a file
|
||||
current.push({
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
sha: file.sha,
|
||||
});
|
||||
} else {
|
||||
const dir = {
|
||||
name: file.name,
|
||||
child: [],
|
||||
};
|
||||
keys[fPath] = dir;
|
||||
current.push(dir);
|
||||
if (!keys[fPath]) {
|
||||
const dir = {
|
||||
name: file.name,
|
||||
child: [],
|
||||
};
|
||||
keys[fPath] = dir;
|
||||
current.push(dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
return output;
|
||||
@@ -520,7 +543,7 @@ angular
|
||||
const f1d = !!f1.child;
|
||||
const f2d = !!f2.child;
|
||||
if (f1d && f2d) {
|
||||
return f1.name - f2.name;
|
||||
return f1.name.localeCompare(f2.name);
|
||||
}
|
||||
if (f1d) {
|
||||
return -1;
|
||||
@@ -528,9 +551,18 @@ angular
|
||||
if (f2d) {
|
||||
return 1;
|
||||
}
|
||||
return f1.name - f2.name;
|
||||
return f1.name.localeCompare(f2.name);
|
||||
};
|
||||
|
||||
function getFileCount(folderPath) {
|
||||
const counts = $scope.$parent.fileCounts;
|
||||
if (!counts) return 0;
|
||||
const normalized = folderPath.startsWith("/")
|
||||
? folderPath.substring(1)
|
||||
: folderPath;
|
||||
return counts[normalized] || 0;
|
||||
}
|
||||
|
||||
function isTruncated(folderPath) {
|
||||
const truncated =
|
||||
($scope.$parent.options &&
|
||||
@@ -543,40 +575,82 @@ angular
|
||||
return truncated.indexOf(normalized) !== -1;
|
||||
}
|
||||
|
||||
function generate(current, parentPath) {
|
||||
function escapeHtml(str) {
|
||||
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||||
}
|
||||
|
||||
function buildSearchFilter() {
|
||||
const results = $scope.searchResults;
|
||||
if (!results || !results.length) return null;
|
||||
const matchPaths = new Set();
|
||||
const matchFolders = new Set();
|
||||
for (const f of results) {
|
||||
const full = f.path ? `${f.path}/${f.name}` : f.name;
|
||||
matchPaths.add(full);
|
||||
// Also collect all ancestor folders
|
||||
if (f.path) {
|
||||
const segments = f.path.split("/").filter(Boolean);
|
||||
let acc = "";
|
||||
for (const seg of segments) {
|
||||
acc = acc ? `${acc}/${seg}` : seg;
|
||||
matchFolders.add(acc);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { paths: matchPaths, folders: matchFolders };
|
||||
}
|
||||
|
||||
function nodeMatchesFilter(node, parentPath, filterSet) {
|
||||
if (!filterSet) return true;
|
||||
const path = parentPath
|
||||
? `${parentPath}/${node.name}`
|
||||
: node.name;
|
||||
if (!node.child) {
|
||||
return filterSet.paths.has(path);
|
||||
}
|
||||
// Show folder if it's an ancestor of a match or contains matches
|
||||
if (filterSet.folders.has(path)) return true;
|
||||
return node.child.some((c) =>
|
||||
nodeMatchesFilter(c, path, filterSet)
|
||||
);
|
||||
}
|
||||
|
||||
function generate(current, parentPath, filterSet) {
|
||||
if (!current) return "";
|
||||
current = current.sort(sortFiles);
|
||||
const afiles = current;
|
||||
let output = "<ul>";
|
||||
for (let f of afiles) {
|
||||
for (let f of current) {
|
||||
if (filterSet && !nodeMatchesFilter(f, parentPath ? parentPath.substring(1) : "", filterSet)) {
|
||||
continue;
|
||||
}
|
||||
let dir = !!f.child;
|
||||
let name = f.name;
|
||||
let size = f.size;
|
||||
let collapsed = f;
|
||||
if (dir) {
|
||||
let test = name;
|
||||
current = f.child;
|
||||
while (current && current.length == 1) {
|
||||
test += "/" + current[0].name;
|
||||
size = current[0].size;
|
||||
current = current[0].child;
|
||||
let inner = f.child;
|
||||
while (inner && inner.length == 1) {
|
||||
test += "/" + inner[0].name;
|
||||
size = inner[0].size;
|
||||
inner = inner[0].child;
|
||||
}
|
||||
name = test;
|
||||
collapsed = inner ? { child: inner } : f;
|
||||
if (size != null && size >= 0) {
|
||||
dir = false;
|
||||
}
|
||||
}
|
||||
if (size != null) {
|
||||
size = `Size: ${humanFileSize(size || 0)}`;
|
||||
} else {
|
||||
size = "";
|
||||
}
|
||||
const sizeTitle = size != null ? `Size: ${humanFileSize(size || 0)}` : "";
|
||||
const path = `${parentPath}/${name}`;
|
||||
const fileCount = dir ? getFileCount(path) : 0;
|
||||
|
||||
const isOpen = filterSet ? ($scope.opens[path] !== false) : $scope.opens[path];
|
||||
const cssClasses = ["file"];
|
||||
if (dir) {
|
||||
cssClasses.push("folder");
|
||||
}
|
||||
if ($scope.opens[path]) {
|
||||
if (isOpen) {
|
||||
cssClasses.push("open");
|
||||
}
|
||||
if ($scope.isActive(path)) {
|
||||
@@ -589,48 +663,54 @@ angular
|
||||
|
||||
output += `<li class="${cssClasses.join(
|
||||
" "
|
||||
)}" ng-class="{active: isActive('${path}'), open: opens['${path}']}" title="${size}">`;
|
||||
)}" ng-class="{active: isActive('${path}'), open: ${filterSet ? "opens['" + path + "'] !== false" : "opens['" + path + "']"}}" title="${escapeHtml(sizeTitle)}">`;
|
||||
if (dir) {
|
||||
output += `<a ng-click="openFolder('${path}', $event)">${name}</a>`;
|
||||
output += `<a ng-click="openFolder('${path}', $event)"><span class="tree-toggle"></span><span class="tree-icon-folder"></span><span class="tree-name">${escapeHtml(name)}</span>`;
|
||||
if (truncated) {
|
||||
output += `<span class="truncated-warning" title="{{ 'WARNINGS.folder_truncated' | translate }}"><i class="fas fa-exclamation-triangle"></i></span>`;
|
||||
}
|
||||
if (fileCount > 0) {
|
||||
output += `<span class="tree-count">${fileCount}</span>`;
|
||||
}
|
||||
output += `</a>`;
|
||||
} else {
|
||||
const needsSpacer = parentPath !== "";
|
||||
output += `<a href='/r/${$scope.repoId}${encodePathForUrl(
|
||||
path
|
||||
)}'>${name}</a>`;
|
||||
)}'>${needsSpacer ? '<span class="tree-spacer"></span>' : ''}<span class="tree-icon-file"></span><span class="tree-name">${escapeHtml(name)}</span></a>`;
|
||||
}
|
||||
if (truncated) {
|
||||
output += `<span class="truncated-warning" title="{{ 'WARNINGS.folder_truncated' | translate }}"><i class="fas fa-exclamation-triangle"></i></span>`;
|
||||
}
|
||||
if ($scope.opens[path] && f.child) {
|
||||
if (f.child.length > 1) {
|
||||
output += generate(f.child, path);
|
||||
if (isOpen && collapsed.child) {
|
||||
const children = collapsed.child;
|
||||
if (children.length > 1) {
|
||||
output += generate(children, path, filterSet);
|
||||
} else if (dir) {
|
||||
current = f.child;
|
||||
while (current && current.length == 1) {
|
||||
current = current[0].child;
|
||||
let inner = children;
|
||||
while (inner && inner.length == 1) {
|
||||
inner = inner[0].child;
|
||||
}
|
||||
output += generate(current, path);
|
||||
output += generate(inner, path, filterSet);
|
||||
}
|
||||
}
|
||||
// output += generate(f.child, parentPath + "/" + f.name);
|
||||
output + "</li>";
|
||||
output += "</li>";
|
||||
}
|
||||
return output + "</ul>";
|
||||
}
|
||||
|
||||
function display() {
|
||||
$element.html("");
|
||||
const output = generate(toArray($scope.file).sort(sortFiles), "");
|
||||
const filterSet = $scope.searchQuery ? buildSearchFilter() : null;
|
||||
let output;
|
||||
if (filterSet !== null && filterSet.paths.size === 0) {
|
||||
output = '<div class="tree-search-empty">No files found</div>';
|
||||
} else {
|
||||
output = generate(toArray($scope.file).sort(sortFiles), "", filterSet);
|
||||
}
|
||||
$compile(output)($scope, (clone) => {
|
||||
$element.append(clone);
|
||||
restoreFocus();
|
||||
});
|
||||
}
|
||||
|
||||
// #496 — expand folders whose children are already loaded so
|
||||
// reviewers see the whole tree without clicking through. Skip
|
||||
// folders with empty children to avoid emitting an empty <ul>
|
||||
// that breaks the click-time lazy-load (#496-followup): the
|
||||
// openFolder handler used to detect "needs to load" by looking
|
||||
// at the absence of a sibling node, but a pre-expanded empty
|
||||
// <ul> is a non-null sibling and silently suppressed the fetch.
|
||||
function expandAllFolders(nodes, parentPath) {
|
||||
if (!nodes) return;
|
||||
for (const f of nodes) {
|
||||
@@ -656,24 +736,148 @@ angular
|
||||
true
|
||||
);
|
||||
|
||||
$scope.$watch("searchResults", (newVal, oldVal) => {
|
||||
if (newVal === oldVal) return;
|
||||
if ($scope.file && $scope.file.length) {
|
||||
display();
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch("searchQuery", (newVal, oldVal) => {
|
||||
if (newVal === oldVal) return;
|
||||
if (!newVal && $scope.file && $scope.file.length) {
|
||||
display();
|
||||
}
|
||||
});
|
||||
|
||||
$scope.isActive = function (name) {
|
||||
return $routeParams.path == name.substring(1);
|
||||
};
|
||||
|
||||
$scope.openFolder = async function (folder, event) {
|
||||
$scope.opens[folder] = !$scope.opens[folder];
|
||||
const sib = event.srcElement.nextSibling;
|
||||
// Lazy-load when there's no sibling (folder never expanded) or
|
||||
// when the sibling is an empty <ul> from a pre-expanded folder
|
||||
// whose children weren't fetched yet (#496-followup).
|
||||
$scope.openFolder = function (folder, event) {
|
||||
var currentlyOpen = $scope.opens[folder];
|
||||
if (currentlyOpen === undefined && $scope.searchQuery) {
|
||||
currentlyOpen = true;
|
||||
}
|
||||
$scope.opens[folder] = !currentlyOpen;
|
||||
const li = event.target.closest("li");
|
||||
const childUl = li ? li.querySelector(":scope > ul") : null;
|
||||
const needsLoad =
|
||||
sib == null ||
|
||||
(sib.tagName === "UL" && sib.children.length === 0);
|
||||
childUl == null ||
|
||||
childUl.children.length === 0;
|
||||
if (needsLoad) {
|
||||
await $scope.$parent.getFiles(folder.substring(1));
|
||||
$scope.$apply();
|
||||
$scope.$parent.getFiles(folder.substring(1));
|
||||
}
|
||||
};
|
||||
|
||||
var focusedPath = $routeParams.path ? "/" + $routeParams.path : null;
|
||||
|
||||
function getVisibleLinks() {
|
||||
return Array.from($element[0].querySelectorAll("li > a"));
|
||||
}
|
||||
|
||||
function getFocusedLink() {
|
||||
return $element[0].querySelector("a.tree-focused");
|
||||
}
|
||||
|
||||
function getLinkPath(link) {
|
||||
if (!link) return null;
|
||||
var href = link.getAttribute("href");
|
||||
if (href) {
|
||||
var prefix = "/r/" + $scope.repoId;
|
||||
return href.indexOf(prefix) === 0 ? decodeURIComponent(href.substring(prefix.length)) : null;
|
||||
}
|
||||
var onclick = link.getAttribute("ng-click");
|
||||
if (onclick) {
|
||||
var m = onclick.match(/openFolder\('([^']+)'/);
|
||||
return m ? m[1] : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findLinkByPath(path) {
|
||||
if (!path) return null;
|
||||
var links = getVisibleLinks();
|
||||
for (var i = 0; i < links.length; i++) {
|
||||
if (getLinkPath(links[i]) === path) return links[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function setFocus(link) {
|
||||
var prev = getFocusedLink();
|
||||
if (prev) prev.classList.remove("tree-focused");
|
||||
if (link) {
|
||||
link.classList.add("tree-focused");
|
||||
link.scrollIntoView({ block: "nearest" });
|
||||
focusedPath = getLinkPath(link);
|
||||
} else {
|
||||
focusedPath = null;
|
||||
}
|
||||
}
|
||||
|
||||
function restoreFocus() {
|
||||
if (!focusedPath) return;
|
||||
var link = findLinkByPath(focusedPath);
|
||||
if (link) {
|
||||
link.classList.add("tree-focused");
|
||||
$element[0].focus();
|
||||
}
|
||||
}
|
||||
|
||||
$element[0].setAttribute("tabindex", "0");
|
||||
|
||||
$element[0].addEventListener("keydown", function (e) {
|
||||
var links = getVisibleLinks();
|
||||
if (!links.length) return;
|
||||
var focused = getFocusedLink();
|
||||
var idx = focused ? links.indexOf(focused) : -1;
|
||||
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
var next = idx < links.length - 1 ? idx + 1 : 0;
|
||||
setFocus(links[next]);
|
||||
} else if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
var prev = idx > 0 ? idx - 1 : links.length - 1;
|
||||
setFocus(links[prev]);
|
||||
} else if (e.key === "ArrowRight") {
|
||||
e.preventDefault();
|
||||
if (!focused) return;
|
||||
var li = focused.closest("li");
|
||||
if (li && li.classList.contains("folder")) {
|
||||
if (!li.classList.contains("open")) {
|
||||
focused.click();
|
||||
} else {
|
||||
var childLink = li.querySelector(":scope > ul > li > a");
|
||||
if (childLink) setFocus(childLink);
|
||||
}
|
||||
}
|
||||
} else if (e.key === "ArrowLeft") {
|
||||
e.preventDefault();
|
||||
if (!focused) return;
|
||||
var li = focused.closest("li");
|
||||
if (li && li.classList.contains("folder") && li.classList.contains("open")) {
|
||||
focused.click();
|
||||
} else {
|
||||
var parentLi = li && li.parentElement ? li.parentElement.closest("li.folder") : null;
|
||||
if (parentLi) {
|
||||
var parentLink = parentLi.querySelector(":scope > a");
|
||||
if (parentLink) setFocus(parentLink);
|
||||
}
|
||||
}
|
||||
} else if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
if (focused) focused.click();
|
||||
}
|
||||
});
|
||||
|
||||
$element[0].addEventListener("click", function (e) {
|
||||
var link = e.target.closest("a");
|
||||
if (link && $element[0].contains(link)) {
|
||||
setFocus(link);
|
||||
}
|
||||
});
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -2157,9 +2361,100 @@ angular
|
||||
"$location",
|
||||
"$routeParams",
|
||||
"$sce",
|
||||
"$q",
|
||||
"PDFViewerService",
|
||||
function ($scope, $http, $location, $routeParams, $sce, PDFViewerService) {
|
||||
function ($scope, $http, $location, $routeParams, $sce, $q, PDFViewerService) {
|
||||
$scope.files = [];
|
||||
$scope.isMac = /Mac|iPhone|iPad|iPod/.test(navigator.platform || navigator.userAgent);
|
||||
$scope.fileSearchQuery = "";
|
||||
$scope.fileSearchResults = null;
|
||||
$scope.fileSearchLoading = false;
|
||||
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
||||
e.preventDefault();
|
||||
var input = document.querySelector(".tree-search-input");
|
||||
if (input) {
|
||||
input.focus();
|
||||
input.select();
|
||||
}
|
||||
}
|
||||
});
|
||||
var searchCanceller = null;
|
||||
$scope.onFileSearchChange = function () {
|
||||
// Cancel any in-flight search request
|
||||
if (searchCanceller) {
|
||||
searchCanceller.resolve();
|
||||
searchCanceller = null;
|
||||
}
|
||||
const query = $scope.fileSearchQuery;
|
||||
if (!query || query.length < 2) {
|
||||
$scope.fileSearchResults = null;
|
||||
$scope.fileSearchLoading = false;
|
||||
return;
|
||||
}
|
||||
$scope.fileSearchLoading = true;
|
||||
searchCanceller = $q.defer();
|
||||
$http.get(
|
||||
`/api/repo/${$scope.repoId}/files/search?q=${encodeURIComponent(query)}`,
|
||||
{ timeout: searchCanceller.promise }
|
||||
).then(function (res) {
|
||||
searchCanceller = null;
|
||||
$scope.fileSearchLoading = false;
|
||||
// Merge search results into $scope.files so the tree can render them.
|
||||
// Ancestor folders must appear before their children for toArray() to work.
|
||||
var existing = {};
|
||||
$scope.files.forEach(function(f) {
|
||||
existing[(f.path || "") + "/" + f.name] = true;
|
||||
});
|
||||
// First pass: collect ancestor folders (shallow to deep)
|
||||
var foldersToAdd = [];
|
||||
var folderSeen = {};
|
||||
for (var i = 0; i < res.data.length; i++) {
|
||||
var f = res.data[i];
|
||||
if (f.path) {
|
||||
var segments = f.path.split("/");
|
||||
var acc = "";
|
||||
for (var j = 0; j < segments.length; j++) {
|
||||
var parent = acc;
|
||||
acc = acc ? acc + "/" + segments[j] : segments[j];
|
||||
var folderKey = parent + "/" + segments[j];
|
||||
if (!existing[folderKey] && !folderSeen[folderKey]) {
|
||||
folderSeen[folderKey] = true;
|
||||
foldersToAdd.push({ name: segments[j], path: parent });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Sort folders by depth (shallow first)
|
||||
foldersToAdd.sort(function(a, b) {
|
||||
return (a.path || "").split("/").length - (b.path || "").split("/").length;
|
||||
});
|
||||
// Add folders first, then files
|
||||
if (foldersToAdd.length > 0) {
|
||||
$scope.files.push.apply($scope.files, foldersToAdd);
|
||||
}
|
||||
var filesToAdd = [];
|
||||
for (var k = 0; k < res.data.length; k++) {
|
||||
var rf = res.data[k];
|
||||
var key = (rf.path || "") + "/" + rf.name;
|
||||
if (!existing[key] && rf.size != null) {
|
||||
filesToAdd.push(rf);
|
||||
existing[key] = true;
|
||||
}
|
||||
}
|
||||
if (filesToAdd.length > 0) {
|
||||
$scope.files.push.apply($scope.files, filesToAdd);
|
||||
}
|
||||
$scope.fileSearchResults = res.data;
|
||||
}, function () {
|
||||
// Only clear loading if this wasn't a cancellation
|
||||
if (!searchCanceller) {
|
||||
$scope.fileSearchLoading = false;
|
||||
$scope.fileSearchResults = [];
|
||||
}
|
||||
});
|
||||
};
|
||||
const extensionModes = {
|
||||
yml: "yaml",
|
||||
txt: "text",
|
||||
@@ -2270,21 +2565,30 @@ angular
|
||||
);
|
||||
}
|
||||
}
|
||||
$scope.getFiles = async function (path) {
|
||||
try {
|
||||
const res = await $http.get(
|
||||
`/api/repo/${$scope.repoId}/files/?path=${encodeURIComponent(path)}&v=${$scope.options.lastUpdateDate}`
|
||||
);
|
||||
$scope.fileCounts = null;
|
||||
$scope.getFiles = function (path) {
|
||||
return $http.get(
|
||||
`/api/repo/${$scope.repoId}/files/?path=${encodeURIComponent(path)}&v=${$scope.options.lastUpdateDate}`
|
||||
).then(function (res) {
|
||||
const normalized = path || "";
|
||||
$scope.files = $scope.files.filter((f) => f.path !== normalized);
|
||||
$scope.files.push(...res.data);
|
||||
return res.data;
|
||||
} catch (err) {
|
||||
}, function (err) {
|
||||
$scope.type = "error";
|
||||
$scope.content = (err && err.data && err.data.error) || "unknown_error";
|
||||
$scope.files = [];
|
||||
}
|
||||
});
|
||||
};
|
||||
function fetchFileCounts() {
|
||||
$http.get(
|
||||
`/api/repo/${$scope.repoId}/files/counts`
|
||||
).then(function (res) {
|
||||
$scope.fileCounts = res.data;
|
||||
}, function () {
|
||||
$scope.fileCounts = {};
|
||||
});
|
||||
}
|
||||
|
||||
function getSelectedFile() {
|
||||
return $scope.files.filter(
|
||||
@@ -2592,25 +2896,28 @@ angular
|
||||
$scope.filePath = $routeParams.path || "";
|
||||
$scope.paths = $scope.filePath.split("/");
|
||||
|
||||
getOptions(async (options) => {
|
||||
getOptions(function (options) {
|
||||
fetchFileCounts();
|
||||
var chain = $q.resolve();
|
||||
for (let i = 0; i < $scope.paths.length; i++) {
|
||||
const path = i > 0 ? $scope.paths.slice(0, i).join("/") : "";
|
||||
await $scope.getFiles(path);
|
||||
if ($scope.type === "error") {
|
||||
$scope.$apply();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if ($scope.files.length == 1 && $scope.files[0].name == "") {
|
||||
$scope.files = [];
|
||||
$scope.type = "empty";
|
||||
$scope.$apply();
|
||||
} else {
|
||||
$scope.$apply(() => {
|
||||
selectFile();
|
||||
updateContent();
|
||||
chain = chain.then(function () {
|
||||
return $scope.getFiles(path);
|
||||
}).then(function () {
|
||||
if ($scope.type === "error") {
|
||||
return $q.reject("error");
|
||||
}
|
||||
});
|
||||
}
|
||||
chain.then(function () {
|
||||
if ($scope.files.length == 1 && $scope.files[0].name == "") {
|
||||
$scope.files = [];
|
||||
$scope.type = "empty";
|
||||
} else {
|
||||
selectFile();
|
||||
updateContent();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Vendored
+102
-102
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user