Initial commit

This commit is contained in:
2026-01-14 23:04:53 +01:00
parent 2f7d11b7d2
commit d9b4c73baa
25 changed files with 3742 additions and 30 deletions

View File

@@ -0,0 +1,435 @@
<?php
$total_pages = ceil($this->total_rows / $this->per_page);
$pk_column = null;
foreach ($this->columns as $col) {
if ($col['Key'] === 'PRI') {
$pk_column = $col['Field'];
break;
}
}
?>
<div class="dbm-content-header">
<div class="dbm-breadcrumb">
<a href="<?php echo Config::get('URL'); ?>database/index">Databases</a>
<span class="separator">/</span>
<a href="<?php echo Config::get('URL'); ?>database/show/<?php echo urlencode($this->database_name); ?>"><?php echo htmlspecialchars($this->database_name); ?></a>
<span class="separator">/</span>
<span><?php echo htmlspecialchars($this->table_name); ?></span>
</div>
<div class="dbm-title">
<h1><?php echo htmlspecialchars($this->table_name); ?></h1>
<span class="badge"><?php echo number_format($this->total_rows); ?> rows</span>
</div>
<div class="dbm-actions">
<button type="button" id="btn-add-row" class="dbm-btn dbm-btn-success">
<i data-lucide="plus"></i>
Add Row
</button>
<a href="<?php echo Config::get('URL'); ?>table/structure/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>" class="dbm-btn dbm-btn-secondary">
<i data-lucide="settings"></i>
Structure
</a>
<a href="<?php echo Config::get('URL'); ?>table/export/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>" class="dbm-btn dbm-btn-secondary" target="_blank">
<i data-lucide="download"></i>
Export
</a>
<a href="<?php echo Config::get('URL'); ?>table/delete/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>"
class="dbm-btn dbm-btn-danger"
data-confirm="Drop table '<?php echo htmlspecialchars($this->table_name); ?>'? This cannot be undone!">
<i data-lucide="trash-2"></i>
Drop Table
</a>
</div>
</div>
<div class="dbm-content-body">
<?php if (!empty($this->table_info)): ?>
<div class="dbm-stats">
<div class="dbm-stat">
<div class="dbm-stat-value"><?php echo number_format($this->total_rows); ?></div>
<div class="dbm-stat-label">Total Rows</div>
</div>
<div class="dbm-stat">
<div class="dbm-stat-value"><?php echo count($this->columns); ?></div>
<div class="dbm-stat-label">Columns</div>
</div>
<div class="dbm-stat">
<div class="dbm-stat-value"><?php echo $this->table_info['total_size'] ?? '-'; ?></div>
<div class="dbm-stat-label">Size</div>
</div>
<div class="dbm-stat">
<div class="dbm-stat-value"><?php echo $this->table_info['engine'] ?? '-'; ?></div>
<div class="dbm-stat-label">Engine</div>
</div>
</div>
<?php endif; ?>
<div class="dbm-table-wrapper" id="data-table-wrapper"
data-database="<?php echo htmlspecialchars($this->database_name); ?>"
data-table="<?php echo htmlspecialchars($this->table_name); ?>"
data-pk-column="<?php echo htmlspecialchars($pk_column ?? ''); ?>"
style="max-height: 500px; overflow: auto;">
<table class="dbm-table" id="data-table">
<thead>
<tr>
<?php foreach ($this->columns as $col): ?>
<th data-column="<?php echo htmlspecialchars($col['Field']); ?>" data-type="<?php echo htmlspecialchars($col['Type']); ?>">
<?php if ($col['Key'] === 'PRI'): ?>
<span style="color: var(--accent-green);" title="Primary Key">
<i data-lucide="key-round" style="width: 12px; height: 12px; vertical-align: middle;"></i>
</span>
<?php endif; ?>
<?php echo htmlspecialchars($col['Field']); ?>
</th>
<?php endforeach; ?>
<th style="width: 120px; text-align: center;">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($this->rows)): ?>
<tr class="empty-row">
<td colspan="<?php echo count($this->columns) + 1; ?>" style="text-align: center; padding: 40px; color: var(--text-muted);">
No data in this table. Click "Add Row" to insert data.
</td>
</tr>
<?php else: ?>
<?php foreach ($this->rows as $row): ?>
<tr class="data-row" data-pk="<?php echo htmlspecialchars($pk_column ? ($row[$pk_column] ?? '') : ''); ?>">
<?php foreach ($this->columns as $col): ?>
<td class="editable-cell" data-column="<?php echo htmlspecialchars($col['Field']); ?>" data-original="<?php echo htmlspecialchars($row[$col['Field']] ?? ''); ?>">
<span class="cell-display"><?php
$value = $row[$col['Field']] ?? null;
if ($value === null) {
echo '<span class="null-value">NULL</span>';
} else {
$display = htmlspecialchars(substr($value, 0, 100));
if (strlen($value) > 100) $display .= '...';
echo $display;
}
?></span>
<input type="text" class="cell-input dbm-form-input" style="display: none; width: 100%; padding: 4px 8px; font-size: 13px;" value="<?php echo htmlspecialchars($row[$col['Field']] ?? ''); ?>">
</td>
<?php endforeach; ?>
<td class="actions-cell" style="text-align: center; white-space: nowrap;">
<div class="view-actions">
<button type="button" class="dbm-btn dbm-btn-sm dbm-btn-secondary btn-edit-row" title="Edit">
<i data-lucide="pencil" style="width: 12px; height: 12px;"></i>
</button>
<button type="button" class="dbm-btn dbm-btn-sm dbm-btn-danger btn-delete-row" title="Delete">
<i data-lucide="trash-2" style="width: 12px; height: 12px;"></i>
</button>
</div>
<div class="edit-actions" style="display: none;">
<button type="button" class="dbm-btn dbm-btn-sm dbm-btn-success btn-save-row" title="Save">
<i data-lucide="check" style="width: 12px; height: 12px;"></i>
</button>
<button type="button" class="dbm-btn dbm-btn-sm dbm-btn-secondary btn-cancel-edit" title="Cancel">
<i data-lucide="x" style="width: 12px; height: 12px;"></i>
</button>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<?php if ($total_pages > 1): ?>
<div class="dbm-pagination">
<div class="dbm-pagination-info">
Showing <?php echo (($this->current_page - 1) * $this->per_page) + 1; ?> - <?php echo min($this->current_page * $this->per_page, $this->total_rows); ?> of <?php echo number_format($this->total_rows); ?>
</div>
<div class="dbm-pagination-controls">
<?php if ($this->current_page > 1): ?>
<a href="<?php echo Config::get('URL'); ?>table/show/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>/1" class="dbm-pagination-btn">First</a>
<a href="<?php echo Config::get('URL'); ?>table/show/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>/<?php echo $this->current_page - 1; ?>" class="dbm-pagination-btn">Prev</a>
<?php endif; ?>
<?php
$start = max(1, $this->current_page - 2);
$end = min($total_pages, $this->current_page + 2);
for ($i = $start; $i <= $end; $i++):
?>
<a href="<?php echo Config::get('URL'); ?>table/show/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>/<?php echo $i; ?>"
class="dbm-pagination-btn <?php echo $i === $this->current_page ? 'active' : ''; ?>"><?php echo $i; ?></a>
<?php endfor; ?>
<?php if ($this->current_page < $total_pages): ?>
<a href="<?php echo Config::get('URL'); ?>table/show/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>/<?php echo $this->current_page + 1; ?>" class="dbm-pagination-btn">Next</a>
<a href="<?php echo Config::get('URL'); ?>table/show/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>/<?php echo $total_pages; ?>" class="dbm-pagination-btn">Last</a>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const wrapper = document.getElementById('data-table-wrapper');
const table = document.getElementById('data-table');
const database = wrapper.dataset.database;
const tableName = wrapper.dataset.table;
const pkColumn = wrapper.dataset.pkColumn;
const baseUrl = '<?php echo Config::get('URL'); ?>';
// Get column info from table headers
function getColumns() {
const headers = table.querySelectorAll('thead th[data-column]');
return Array.from(headers).map(th => ({
name: th.dataset.column,
type: th.dataset.type
}));
}
// Edit row
table.addEventListener('click', function(e) {
const editBtn = e.target.closest('.btn-edit-row');
if (editBtn) {
const row = editBtn.closest('tr');
enterEditMode(row);
}
});
// Save row
table.addEventListener('click', function(e) {
const saveBtn = e.target.closest('.btn-save-row');
if (saveBtn) {
const row = saveBtn.closest('tr');
saveRow(row);
}
});
// Cancel edit
table.addEventListener('click', function(e) {
const cancelBtn = e.target.closest('.btn-cancel-edit');
if (cancelBtn) {
const row = cancelBtn.closest('tr');
cancelEdit(row);
}
});
// Delete row
table.addEventListener('click', function(e) {
const deleteBtn = e.target.closest('.btn-delete-row');
if (deleteBtn) {
const row = deleteBtn.closest('tr');
if (confirm('Delete this row? This cannot be undone.')) {
deleteRow(row);
}
}
});
// Add new row
document.getElementById('btn-add-row').addEventListener('click', function() {
addNewRow();
});
function enterEditMode(row) {
row.classList.add('editing');
row.querySelectorAll('.cell-display').forEach(el => el.style.display = 'none');
row.querySelectorAll('.cell-input').forEach(el => el.style.display = 'block');
row.querySelector('.view-actions').style.display = 'none';
row.querySelector('.edit-actions').style.display = 'inline-flex';
}
function exitEditMode(row) {
row.classList.remove('editing');
row.querySelectorAll('.cell-display').forEach(el => el.style.display = 'inline');
row.querySelectorAll('.cell-input').forEach(el => el.style.display = 'none');
row.querySelector('.view-actions').style.display = 'inline-flex';
row.querySelector('.edit-actions').style.display = 'none';
}
function cancelEdit(row) {
if (row.classList.contains('new-row')) {
row.remove();
// Show empty message if no rows left
const remainingRows = table.querySelectorAll('tbody tr.data-row');
if (remainingRows.length === 0) {
const cols = getColumns();
const emptyRow = document.createElement('tr');
emptyRow.className = 'empty-row';
emptyRow.innerHTML = '<td colspan="' + (cols.length + 1) + '" style="text-align: center; padding: 40px; color: var(--text-muted);">No data in this table. Click "Add Row" to insert data.</td>';
table.querySelector('tbody').appendChild(emptyRow);
}
return;
}
// Restore original values
row.querySelectorAll('.editable-cell').forEach(cell => {
const original = cell.dataset.original;
cell.querySelector('.cell-input').value = original;
});
exitEditMode(row);
}
function saveRow(row) {
const isNew = row.classList.contains('new-row');
const pkValue = row.dataset.pk;
const data = {};
row.querySelectorAll('.editable-cell').forEach(cell => {
const column = cell.dataset.column;
const input = cell.querySelector('.cell-input');
data[column] = input.value;
});
const url = isNew
? baseUrl + 'table/insertRow/' + encodeURIComponent(database) + '/' + encodeURIComponent(tableName)
: baseUrl + 'table/updateRow/' + encodeURIComponent(database) + '/' + encodeURIComponent(tableName);
const formData = new FormData();
if (!isNew) {
formData.append('pk_value', pkValue);
}
for (const key in data) {
formData.append('data[' + key + ']', data[key]);
}
fetch(url, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(result => {
if (result.success) {
// Update display values
row.querySelectorAll('.editable-cell').forEach(cell => {
const input = cell.querySelector('.cell-input');
const display = cell.querySelector('.cell-display');
const value = input.value;
cell.dataset.original = value;
if (value === '' || value === null) {
display.innerHTML = '<span class="null-value">NULL</span>';
} else {
display.textContent = value.length > 100 ? value.substring(0, 100) + '...' : value;
}
});
if (isNew && result.insert_id && pkColumn) {
row.dataset.pk = result.insert_id;
// Update the PK cell display
const pkCell = row.querySelector('.editable-cell[data-column="' + pkColumn + '"]');
if (pkCell) {
pkCell.querySelector('.cell-input').value = result.insert_id;
pkCell.querySelector('.cell-display').textContent = result.insert_id;
pkCell.dataset.original = result.insert_id;
}
}
row.classList.remove('new-row');
exitEditMode(row);
} else {
alert('Error: ' + result.message);
}
})
.catch(error => {
alert('Error saving row: ' + error.message);
});
}
function deleteRow(row) {
const pkValue = row.dataset.pk;
if (!pkValue) {
alert('Cannot delete: no primary key');
return;
}
const formData = new FormData();
formData.append('pk_value', pkValue);
fetch(baseUrl + 'table/deleteRow/' + encodeURIComponent(database) + '/' + encodeURIComponent(tableName), {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(result => {
if (result.success) {
row.remove();
// Show empty message if no rows left
const remainingRows = table.querySelectorAll('tbody tr.data-row');
if (remainingRows.length === 0) {
const cols = getColumns();
const emptyRow = document.createElement('tr');
emptyRow.className = 'empty-row';
emptyRow.innerHTML = '<td colspan="' + (cols.length + 1) + '" style="text-align: center; padding: 40px; color: var(--text-muted);">No data in this table. Click "Add Row" to insert data.</td>';
table.querySelector('tbody').appendChild(emptyRow);
}
} else {
alert('Error: ' + result.message);
}
})
.catch(error => {
alert('Error deleting row: ' + error.message);
});
}
function addNewRow() {
// Remove empty message if present
const emptyRow = table.querySelector('tbody tr.empty-row');
if (emptyRow) {
emptyRow.remove();
}
const columns = getColumns();
const tr = document.createElement('tr');
tr.className = 'data-row new-row editing';
tr.dataset.pk = '';
columns.forEach(col => {
const td = document.createElement('td');
td.className = 'editable-cell';
td.dataset.column = col.name;
td.dataset.original = '';
td.innerHTML = `
<span class="cell-display" style="display: none;"><span class="null-value">NULL</span></span>
<input type="text" class="cell-input dbm-form-input" style="display: block; width: 100%; padding: 4px 8px; font-size: 13px;" value="">
`;
tr.appendChild(td);
});
// Actions cell
const actionsTd = document.createElement('td');
actionsTd.className = 'actions-cell';
actionsTd.style.textAlign = 'center';
actionsTd.style.whiteSpace = 'nowrap';
actionsTd.innerHTML = `
<div class="view-actions" style="display: none;">
<button type="button" class="dbm-btn dbm-btn-sm dbm-btn-secondary btn-edit-row" title="Edit">
<i data-lucide="pencil" style="width: 12px; height: 12px;"></i>
</button>
<button type="button" class="dbm-btn dbm-btn-sm dbm-btn-danger btn-delete-row" title="Delete">
<i data-lucide="trash-2" style="width: 12px; height: 12px;"></i>
</button>
</div>
<div class="edit-actions" style="display: inline-flex;">
<button type="button" class="dbm-btn dbm-btn-sm dbm-btn-success btn-save-row" title="Save">
<i data-lucide="check" style="width: 12px; height: 12px;"></i>
</button>
<button type="button" class="dbm-btn dbm-btn-sm dbm-btn-secondary btn-cancel-edit" title="Cancel">
<i data-lucide="x" style="width: 12px; height: 12px;"></i>
</button>
</div>
`;
lucide.createIcons();
tr.appendChild(actionsTd);
// Insert at the top of tbody
const tbody = table.querySelector('tbody');
tbody.insertBefore(tr, tbody.firstChild);
// Focus first input
const firstInput = tr.querySelector('.cell-input');
if (firstInput) {
firstInput.focus();
}
}
});
</script>