- 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.
398 lines
12 KiB
PHP
398 lines
12 KiB
PHP
<?= $this->extend('layouts/main.php') ?>
|
|
|
|
<?= $this->section('content') ?>
|
|
<div class="page-wrapper">
|
|
<div class="container-fluid">
|
|
<div class="row page-titles">
|
|
<div class="col-md-6 align-self-center">
|
|
<h4 class="text-themecolor">Gitea Dashboard (DB)</h4>
|
|
</div>
|
|
<div class="col-md-6 text-end">
|
|
<?php if (!empty($isAdmin)): ?>
|
|
<button id="btnSync" class="btn btn-primary">Sync Now</button>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-3 mb-3">
|
|
<div class="col-md-3">
|
|
<label class="form-label">User</label>
|
|
<select id="filterUser" class="form-select"><option value="">All User</option></select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label">Repository</label>
|
|
<select id="filterRepo" class="form-select"><option value="">All Repository</option></select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label">Start Date</label>
|
|
<input type="date" id="filterStart" class="form-control">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label">End Date</label>
|
|
<input type="date" id="filterEnd" class="form-control">
|
|
</div>
|
|
<div class="col-md-2 d-flex align-items-end">
|
|
<button id="btnApplyFilter" class="btn btn-success w-100">Apply</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-12 mb-3">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
<h5 class="mb-0">Latest Commits</h5>
|
|
<div><strong>Total Commits:</strong> <span id="totalCommits">0</span></div>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm table-bordered" id="tableCommits">
|
|
<thead>
|
|
<tr>
|
|
<th>Date</th>
|
|
<th>Repository</th>
|
|
<th>User</th>
|
|
<th>SHA</th>
|
|
<th>Message</th>
|
|
</tr>
|
|
</thead>
|
|
<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>
|
|
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<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>
|
|
<tr>
|
|
<th>Updated</th>
|
|
<th>Repository</th>
|
|
<th>User</th>
|
|
<th>PR</th>
|
|
<th>State</th>
|
|
</tr>
|
|
</thead>
|
|
<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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?= $this->endSection() ?>
|
|
|
|
<?= $this->section('script') ?>
|
|
<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');
|
|
const filterStart = document.getElementById('filterStart');
|
|
const filterEnd = document.getElementById('filterEnd');
|
|
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();
|
|
updatePager(commitPrev, commitNext, commitPageInfo, commitsState);
|
|
updatePager(prPrev, prNext, prPageInfo, prsState);
|
|
|
|
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 () => {
|
|
btnSync.disabled = true;
|
|
btnSync.innerText = 'Syncing...';
|
|
|
|
try {
|
|
const response = await fetch(`<?= base_url('api/git/sync') ?>`, {
|
|
method: 'POST'
|
|
});
|
|
const result = await response.json();
|
|
if (response.ok) {
|
|
alert('Sync complete');
|
|
if (commitsState.loaded || prsState.loaded) {
|
|
await loadTables();
|
|
}
|
|
} else {
|
|
alert(result.message || 'Sync failed');
|
|
}
|
|
} catch (error) {
|
|
alert('Sync request failed');
|
|
}
|
|
|
|
btnSync.disabled = false;
|
|
btnSync.innerText = 'Sync Now';
|
|
});
|
|
}
|
|
|
|
function setDefaultDateRange() {
|
|
const today = new Date();
|
|
const startOfYear = new Date(today.getFullYear(), 0, 1);
|
|
filterStart.value = toInputDate(startOfYear);
|
|
filterEnd.value = toInputDate(today);
|
|
}
|
|
|
|
function toInputDate(date) {
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
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();
|
|
if (!response.ok || !Array.isArray(result.data)) {
|
|
return;
|
|
}
|
|
|
|
result.data.forEach(item => {
|
|
const option = document.createElement('option');
|
|
option.value = item.id;
|
|
option.textContent = item.username;
|
|
filterUser.appendChild(option);
|
|
});
|
|
}
|
|
|
|
async function loadRepos() {
|
|
const response = await fetch(`<?= base_url('api/git/repositories') ?>`);
|
|
const result = await response.json();
|
|
if (!response.ok || !Array.isArray(result.data)) {
|
|
return;
|
|
}
|
|
|
|
result.data.forEach(item => {
|
|
const option = document.createElement('option');
|
|
option.value = item.id;
|
|
option.textContent = item.full_name;
|
|
filterRepo.appendChild(option);
|
|
});
|
|
}
|
|
|
|
async function loadTables() {
|
|
await loadCommits();
|
|
await loadPullRequests();
|
|
}
|
|
|
|
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') ?>?${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;
|
|
}
|
|
|
|
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 = 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>${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') ?>?${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 = 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>${escapeHtml(item.updated_at_gitea || '')}</td>
|
|
<td>${escapeHtml(item.repository_full_name || '')}</td>
|
|
<td>${escapeHtml(item.user_username || '')}</td>
|
|
<td>${link}</td>
|
|
<td>${escapeHtml(item.state || '')}</td>
|
|
</tr>`;
|
|
}).join('');
|
|
|
|
updatePager(prPrev, prNext, prPageInfo, prsState);
|
|
}
|
|
});
|
|
</script>
|
|
<?= $this->endSection() ?>
|