Add paginated git API and dashboard controls
- Add shared pagination params and meta output for commits and pull requests API endpoints. - Switch dashboard lists to page-based loading with prev/next controls and total counters. - Add safer HTML escaping and initial empty-state placeholders in gitea dashboard.
This commit is contained in:
parent
ec5f2fc385
commit
836618eaaf
@ -108,6 +108,21 @@ class GitApi extends BaseController
|
||||
], 200);
|
||||
}
|
||||
|
||||
private function getPaginationParams(): array
|
||||
{
|
||||
$page = (int) ($this->request->getGet('page') ?? 1);
|
||||
if ($page <= 0) {
|
||||
$page = 1;
|
||||
}
|
||||
|
||||
$perPage = (int) ($this->request->getGet('per_page') ?? $this->request->getGet('limit') ?? 25);
|
||||
if ($perPage <= 0 || $perPage > 100) {
|
||||
$perPage = 25;
|
||||
}
|
||||
|
||||
return [$page, $perPage];
|
||||
}
|
||||
|
||||
public function commits()
|
||||
{
|
||||
if ($response = $this->ensureLoggedIn()) {
|
||||
@ -118,36 +133,37 @@ class GitApi extends BaseController
|
||||
$userId = $this->request->getGet('user_id');
|
||||
$startDate = $this->request->getGet('start_date');
|
||||
$endDate = $this->request->getGet('end_date');
|
||||
$limit = (int) ($this->request->getGet('limit') ?? 200);
|
||||
if ($limit <= 0 || $limit > 1000) {
|
||||
$limit = 200;
|
||||
}
|
||||
[$page, $perPage] = $this->getPaginationParams();
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
$builder = $db->table('git_commits c')
|
||||
->select('c.id, c.sha, c.short_sha, c.message, c.author_name, c.author_email, c.committed_at, c.html_url, r.id as repository_id, r.full_name as repository_full_name, u.id as user_id, u.username as user_username')
|
||||
$baseBuilder = $db->table('git_commits c')
|
||||
->join('git_repositories r', 'r.id = c.repository_id', 'inner')
|
||||
->join('git_users u', 'u.id = c.author_user_id', 'left');
|
||||
|
||||
if (!empty($repoId)) {
|
||||
$builder->where('c.repository_id', (int) $repoId);
|
||||
$baseBuilder->where('c.repository_id', (int) $repoId);
|
||||
}
|
||||
|
||||
if (!empty($userId)) {
|
||||
$builder->where('c.author_user_id', (int) $userId);
|
||||
$baseBuilder->where('c.author_user_id', (int) $userId);
|
||||
}
|
||||
|
||||
if (!empty($startDate)) {
|
||||
$builder->where('c.committed_at >=', $startDate . ' 00:00:00');
|
||||
$baseBuilder->where('c.committed_at >=', $startDate . ' 00:00:00');
|
||||
}
|
||||
|
||||
if (!empty($endDate)) {
|
||||
$builder->where('c.committed_at <=', $endDate . ' 23:59:59');
|
||||
$baseBuilder->where('c.committed_at <=', $endDate . ' 23:59:59');
|
||||
}
|
||||
|
||||
$rows = $builder
|
||||
$total = (int) (clone $baseBuilder)
|
||||
->countAllResults();
|
||||
|
||||
$rows = $baseBuilder
|
||||
->select('c.id, c.sha, c.short_sha, c.message, c.author_name, c.author_email, c.committed_at, c.html_url, r.id as repository_id, r.full_name as repository_full_name, u.id as user_id, u.username as user_username')
|
||||
->orderBy('c.committed_at', 'DESC')
|
||||
->limit($limit)
|
||||
->limit($perPage, $offset)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
@ -155,6 +171,12 @@ class GitApi extends BaseController
|
||||
'status' => 'success',
|
||||
'message' => 'Commits fetched',
|
||||
'data' => $rows,
|
||||
'meta' => [
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'per_page' => $perPage,
|
||||
'total_pages' => $perPage > 0 ? (int) ceil($total / $perPage) : 0,
|
||||
],
|
||||
], 200);
|
||||
}
|
||||
|
||||
@ -169,40 +191,41 @@ class GitApi extends BaseController
|
||||
$state = $this->request->getGet('state');
|
||||
$startDate = $this->request->getGet('start_date');
|
||||
$endDate = $this->request->getGet('end_date');
|
||||
$limit = (int) ($this->request->getGet('limit') ?? 200);
|
||||
if ($limit <= 0 || $limit > 1000) {
|
||||
$limit = 200;
|
||||
}
|
||||
[$page, $perPage] = $this->getPaginationParams();
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
$builder = $db->table('git_pull_requests p')
|
||||
->select('p.id, p.number, p.title, p.state, p.is_draft, p.is_merged, p.created_at_gitea, p.updated_at_gitea, p.merged_at, p.closed_at, p.html_url, r.id as repository_id, r.full_name as repository_full_name, u.id as user_id, u.username as user_username')
|
||||
$baseBuilder = $db->table('git_pull_requests p')
|
||||
->join('git_repositories r', 'r.id = p.repository_id', 'inner')
|
||||
->join('git_users u', 'u.id = p.author_user_id', 'left');
|
||||
|
||||
if (!empty($repoId)) {
|
||||
$builder->where('p.repository_id', (int) $repoId);
|
||||
$baseBuilder->where('p.repository_id', (int) $repoId);
|
||||
}
|
||||
|
||||
if (!empty($userId)) {
|
||||
$builder->where('p.author_user_id', (int) $userId);
|
||||
$baseBuilder->where('p.author_user_id', (int) $userId);
|
||||
}
|
||||
|
||||
if (!empty($state)) {
|
||||
$builder->where('p.state', $state);
|
||||
$baseBuilder->where('p.state', $state);
|
||||
}
|
||||
|
||||
if (!empty($startDate)) {
|
||||
$builder->where('p.updated_at_gitea >=', $startDate . ' 00:00:00');
|
||||
$baseBuilder->where('p.updated_at_gitea >=', $startDate . ' 00:00:00');
|
||||
}
|
||||
|
||||
if (!empty($endDate)) {
|
||||
$builder->where('p.updated_at_gitea <=', $endDate . ' 23:59:59');
|
||||
$baseBuilder->where('p.updated_at_gitea <=', $endDate . ' 23:59:59');
|
||||
}
|
||||
|
||||
$rows = $builder
|
||||
$total = (int) (clone $baseBuilder)
|
||||
->countAllResults();
|
||||
|
||||
$rows = $baseBuilder
|
||||
->select('p.id, p.number, p.title, p.state, p.is_draft, p.is_merged, p.created_at_gitea, p.updated_at_gitea, p.merged_at, p.closed_at, p.html_url, r.id as repository_id, r.full_name as repository_full_name, u.id as user_id, u.username as user_username')
|
||||
->orderBy('p.updated_at_gitea', 'DESC')
|
||||
->limit($limit)
|
||||
->limit($perPage, $offset)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
@ -210,6 +233,12 @@ class GitApi extends BaseController
|
||||
'status' => 'success',
|
||||
'message' => 'Pull requests fetched',
|
||||
'data' => $rows,
|
||||
'meta' => [
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'per_page' => $perPage,
|
||||
'total_pages' => $perPage > 0 ? (int) ceil($total / $perPage) : 0,
|
||||
],
|
||||
], 200);
|
||||
}
|
||||
|
||||
|
||||
@ -14,7 +14,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">User</label>
|
||||
@ -59,6 +58,11 @@
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mt-2">
|
||||
<button id="commitPrev" class="btn btn-outline-secondary btn-sm">Prev</button>
|
||||
<div id="commitPageInfo" class="small text-muted">Page 0 of 0</div>
|
||||
<button id="commitNext" class="btn btn-outline-secondary btn-sm">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -66,7 +70,10 @@
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5>Latest Pull Requests</h5>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h5 class="mb-0">Latest Pull Requests</h5>
|
||||
<div><strong>Total Pull Requests:</strong> <span id="totalPullRequests">0</span></div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered" id="tablePrs">
|
||||
<thead>
|
||||
@ -81,6 +88,11 @@
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mt-2">
|
||||
<button id="prPrev" class="btn btn-outline-secondary btn-sm">Prev</button>
|
||||
<div id="prPageInfo" class="small text-muted">Page 0 of 0</div>
|
||||
<button id="prNext" class="btn btn-outline-secondary btn-sm">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -93,6 +105,7 @@
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const totalCommits = document.getElementById('totalCommits');
|
||||
const totalPullRequests = document.getElementById('totalPullRequests');
|
||||
|
||||
const filterRepo = document.getElementById('filterRepo');
|
||||
const filterUser = document.getElementById('filterUser');
|
||||
@ -101,12 +114,72 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
const btnApplyFilter = document.getElementById('btnApplyFilter');
|
||||
const btnSync = document.getElementById('btnSync');
|
||||
|
||||
const commitPrev = document.getElementById('commitPrev');
|
||||
const commitNext = document.getElementById('commitNext');
|
||||
const commitPageInfo = document.getElementById('commitPageInfo');
|
||||
|
||||
const prPrev = document.getElementById('prPrev');
|
||||
const prNext = document.getElementById('prNext');
|
||||
const prPageInfo = document.getElementById('prPageInfo');
|
||||
|
||||
const commitsState = {
|
||||
page: 1,
|
||||
perPage: 25,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
loaded: false,
|
||||
};
|
||||
|
||||
const prsState = {
|
||||
page: 1,
|
||||
perPage: 25,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
loaded: false,
|
||||
};
|
||||
|
||||
setDefaultDateRange();
|
||||
setInitialPlaceholders();
|
||||
await loadUsers();
|
||||
await loadRepos();
|
||||
await loadTables();
|
||||
updatePager(commitPrev, commitNext, commitPageInfo, commitsState);
|
||||
updatePager(prPrev, prNext, prPageInfo, prsState);
|
||||
|
||||
btnApplyFilter.addEventListener('click', loadTables);
|
||||
btnApplyFilter.addEventListener('click', async () => {
|
||||
commitsState.page = 1;
|
||||
prsState.page = 1;
|
||||
commitsState.loaded = true;
|
||||
prsState.loaded = true;
|
||||
await loadTables();
|
||||
});
|
||||
|
||||
commitPrev.addEventListener('click', async () => {
|
||||
if (commitsState.page > 1) {
|
||||
commitsState.page--;
|
||||
await loadCommits();
|
||||
}
|
||||
});
|
||||
|
||||
commitNext.addEventListener('click', async () => {
|
||||
if (commitsState.page < commitsState.totalPages) {
|
||||
commitsState.page++;
|
||||
await loadCommits();
|
||||
}
|
||||
});
|
||||
|
||||
prPrev.addEventListener('click', async () => {
|
||||
if (prsState.page > 1) {
|
||||
prsState.page--;
|
||||
await loadPullRequests();
|
||||
}
|
||||
});
|
||||
|
||||
prNext.addEventListener('click', async () => {
|
||||
if (prsState.page < prsState.totalPages) {
|
||||
prsState.page++;
|
||||
await loadPullRequests();
|
||||
}
|
||||
});
|
||||
|
||||
if (btnSync) {
|
||||
btnSync.addEventListener('click', async () => {
|
||||
@ -120,7 +193,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
const result = await response.json();
|
||||
if (response.ok) {
|
||||
alert('Sync complete');
|
||||
await loadTables();
|
||||
if (commitsState.loaded || prsState.loaded) {
|
||||
await loadTables();
|
||||
}
|
||||
} else {
|
||||
alert(result.message || 'Sync failed');
|
||||
}
|
||||
@ -147,6 +222,42 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function setInitialPlaceholders() {
|
||||
document.querySelector('#tableCommits tbody').innerHTML = '<tr><td colspan="5">Submit filter to load data</td></tr>';
|
||||
document.querySelector('#tablePrs tbody').innerHTML = '<tr><td colspan="5">Submit filter to load data</td></tr>';
|
||||
totalCommits.innerText = '0';
|
||||
totalPullRequests.innerText = '0';
|
||||
}
|
||||
|
||||
function updatePager(prevBtn, nextBtn, infoEl, state) {
|
||||
const totalPages = state.loaded ? state.totalPages : 0;
|
||||
const page = state.loaded && totalPages > 0 ? state.page : 0;
|
||||
|
||||
prevBtn.disabled = !state.loaded || page <= 1;
|
||||
nextBtn.disabled = !state.loaded || page >= totalPages || totalPages <= 1;
|
||||
infoEl.innerText = `Page ${page} of ${totalPages}`;
|
||||
}
|
||||
|
||||
function getBaseParams(page, perPage) {
|
||||
const params = new URLSearchParams();
|
||||
if (filterRepo.value) params.set('repo_id', filterRepo.value);
|
||||
if (filterUser.value) params.set('user_id', filterUser.value);
|
||||
if (filterStart.value) params.set('start_date', filterStart.value);
|
||||
if (filterEnd.value) params.set('end_date', filterEnd.value);
|
||||
params.set('page', String(page));
|
||||
params.set('per_page', String(perPage));
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
const response = await fetch(`<?= base_url('api/git/users') ?>`);
|
||||
const result = await response.json();
|
||||
@ -182,77 +293,104 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
await loadPullRequests();
|
||||
}
|
||||
|
||||
function buildParams() {
|
||||
const params = new URLSearchParams();
|
||||
if (filterRepo.value) params.set('repo_id', filterRepo.value);
|
||||
if (filterUser.value) params.set('user_id', filterUser.value);
|
||||
if (filterStart.value) params.set('start_date', filterStart.value);
|
||||
if (filterEnd.value) params.set('end_date', filterEnd.value);
|
||||
params.set('limit', '200');
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
async function loadCommits() {
|
||||
const tbody = document.querySelector('#tableCommits tbody');
|
||||
tbody.innerHTML = '<tr><td colspan="5">Loading...</td></tr>';
|
||||
|
||||
const response = await fetch(`<?= base_url('api/git/commits') ?>?${buildParams()}`);
|
||||
const response = await fetch(`<?= base_url('api/git/commits') ?>?${getBaseParams(commitsState.page, commitsState.perPage)}`);
|
||||
const result = await response.json();
|
||||
if (!response.ok || !Array.isArray(result.data)) {
|
||||
commitsState.total = 0;
|
||||
commitsState.totalPages = 0;
|
||||
totalCommits.innerText = '0';
|
||||
tbody.innerHTML = '<tr><td colspan="5">Failed loading commits</td></tr>';
|
||||
updatePager(commitPrev, commitNext, commitPageInfo, commitsState);
|
||||
return;
|
||||
}
|
||||
|
||||
totalCommits.innerText = String(result.data.length);
|
||||
commitsState.total = Number(result.meta?.total ?? result.data.length ?? 0);
|
||||
commitsState.totalPages = Number(result.meta?.total_pages ?? 0);
|
||||
commitsState.page = Number(result.meta?.page ?? commitsState.page);
|
||||
commitsState.perPage = Number(result.meta?.per_page ?? commitsState.perPage);
|
||||
commitsState.loaded = true;
|
||||
totalCommits.innerText = String(commitsState.total);
|
||||
|
||||
if (commitsState.totalPages > 0 && commitsState.page > commitsState.totalPages) {
|
||||
commitsState.page = commitsState.totalPages;
|
||||
await loadCommits();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.data.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="5">No commits found</td></tr>';
|
||||
updatePager(commitPrev, commitNext, commitPageInfo, commitsState);
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = result.data.map(item => {
|
||||
const msg = (item.message || '').replace(/</g, '<').replace(/>/g, '>').slice(0, 120);
|
||||
const sha = item.short_sha || '';
|
||||
const link = item.html_url ? `<a href="${item.html_url}" target="_blank">${sha}</a>` : sha;
|
||||
const msg = escapeHtml((item.message || '').slice(0, 120));
|
||||
const sha = escapeHtml(item.short_sha || '');
|
||||
const link = item.html_url ? `<a href="${escapeHtml(item.html_url)}" target="_blank">${sha}</a>` : sha;
|
||||
return `<tr>
|
||||
<td>${item.committed_at || ''}</td>
|
||||
<td>${item.repository_full_name || ''}</td>
|
||||
<td>${item.user_username || item.author_name || ''}</td>
|
||||
<td>${escapeHtml(item.committed_at || '')}</td>
|
||||
<td>${escapeHtml(item.repository_full_name || '')}</td>
|
||||
<td>${escapeHtml(item.user_username || item.author_name || '')}</td>
|
||||
<td>${link}</td>
|
||||
<td>${msg}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
updatePager(commitPrev, commitNext, commitPageInfo, commitsState);
|
||||
}
|
||||
|
||||
async function loadPullRequests() {
|
||||
const tbody = document.querySelector('#tablePrs tbody');
|
||||
tbody.innerHTML = '<tr><td colspan="5">Loading...</td></tr>';
|
||||
|
||||
const response = await fetch(`<?= base_url('api/git/pull-requests') ?>?${buildParams()}`);
|
||||
const response = await fetch(`<?= base_url('api/git/pull-requests') ?>?${getBaseParams(prsState.page, prsState.perPage)}`);
|
||||
const result = await response.json();
|
||||
if (!response.ok || !Array.isArray(result.data)) {
|
||||
prsState.total = 0;
|
||||
prsState.totalPages = 0;
|
||||
totalPullRequests.innerText = '0';
|
||||
tbody.innerHTML = '<tr><td colspan="5">Failed loading pull requests</td></tr>';
|
||||
updatePager(prPrev, prNext, prPageInfo, prsState);
|
||||
return;
|
||||
}
|
||||
|
||||
prsState.total = Number(result.meta?.total ?? result.data.length ?? 0);
|
||||
prsState.totalPages = Number(result.meta?.total_pages ?? 0);
|
||||
prsState.page = Number(result.meta?.page ?? prsState.page);
|
||||
prsState.perPage = Number(result.meta?.per_page ?? prsState.perPage);
|
||||
prsState.loaded = true;
|
||||
totalPullRequests.innerText = String(prsState.total);
|
||||
|
||||
if (prsState.totalPages > 0 && prsState.page > prsState.totalPages) {
|
||||
prsState.page = prsState.totalPages;
|
||||
await loadPullRequests();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.data.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="5">No pull requests found</td></tr>';
|
||||
updatePager(prPrev, prNext, prPageInfo, prsState);
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = result.data.map(item => {
|
||||
const title = (item.title || '').replace(/</g, '<').replace(/>/g, '>').slice(0, 100);
|
||||
const prText = `#${item.number} ${title}`;
|
||||
const link = item.html_url ? `<a href="${item.html_url}" target="_blank">${prText}</a>` : prText;
|
||||
const title = escapeHtml((item.title || '').slice(0, 100));
|
||||
const prText = `#${escapeHtml(item.number || '')} ${title}`;
|
||||
const link = item.html_url ? `<a href="${escapeHtml(item.html_url)}" target="_blank">${prText}</a>` : prText;
|
||||
return `<tr>
|
||||
<td>${item.updated_at_gitea || ''}</td>
|
||||
<td>${item.repository_full_name || ''}</td>
|
||||
<td>${item.user_username || ''}</td>
|
||||
<td>${escapeHtml(item.updated_at_gitea || '')}</td>
|
||||
<td>${escapeHtml(item.repository_full_name || '')}</td>
|
||||
<td>${escapeHtml(item.user_username || '')}</td>
|
||||
<td>${link}</td>
|
||||
<td>${item.state || ''}</td>
|
||||
<td>${escapeHtml(item.state || '')}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
updatePager(prPrev, prNext, prPageInfo, prsState);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user