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

945
public/css/dbmanager.css Normal file
View File

@@ -0,0 +1,945 @@
:root {
--dbm-bg: #ffffff;
--dbm-bg-secondary: #fafafa;
--dbm-bg-tertiary: #f0f0f0;
--dbm-border: #e0e0e0;
--dbm-text: #333333;
--dbm-text-secondary: #666666;
--dbm-text-muted: #999999;
--dbm-accent: #555555;
--dbm-accent-light: #888888;
--dbm-success: #6b9b6b;
--dbm-danger: #b57575;
--dbm-warning: #a89a6b;
--dbm-info: #6b8a9b;
--dbm-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
--dbm-radius: 4px;
--dbm-transition: all 0.15s ease;
}
[data-lucide] {
width: 14px;
height: 14px;
vertical-align: middle;
display: inline-block;
}
.dbm-page-wrapper {
max-width: 1600px;
width: 95%;
background: #f5f5f5;
}
.dbm-wrapper {
display: flex;
flex-direction: column;
min-height: calc(100vh - 120px);
background: var(--dbm-bg);
border: 1px solid var(--dbm-border);
border-radius: var(--dbm-radius);
overflow: hidden;
margin: 20px 0;
box-shadow: var(--dbm-shadow);
}
.dbm-main {
display: flex;
flex: 1;
overflow: hidden;
}
.dbm-sidebar {
width: 260px;
background: var(--dbm-bg-secondary);
border-right: 1px solid var(--dbm-border);
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.dbm-sidebar-header {
padding: 14px 16px;
border-bottom: 1px solid var(--dbm-border);
display: flex;
align-items: center;
gap: 8px;
background: var(--dbm-bg);
}
.dbm-sidebar-header h3 {
margin: 0;
font-size: 12px;
font-weight: 600;
color: var(--dbm-text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.dbm-sidebar-header .icon {
width: 14px;
height: 14px;
color: var(--dbm-text-muted);
flex-shrink: 0;
}
.dbm-sidebar-header .icon svg {
width: 14px;
height: 14px;
}
.dbm-tree {
flex: 1;
overflow-y: auto;
padding: 8px 0;
}
.dbm-tree::-webkit-scrollbar {
width: 6px;
}
.dbm-tree::-webkit-scrollbar-track {
background: transparent;
}
.dbm-tree::-webkit-scrollbar-thumb {
background: var(--dbm-border);
border-radius: 3px;
}
.tree-item {
user-select: none;
}
.tree-header {
display: flex;
align-items: center;
padding: 6px 12px;
cursor: pointer;
transition: var(--dbm-transition);
border-left: 2px solid transparent;
text-decoration: none;
}
.tree-header:hover {
background: var(--dbm-bg-tertiary);
}
.tree-header.active {
background: var(--dbm-bg-tertiary);
border-left-color: var(--dbm-accent);
}
.tree-toggle {
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 4px;
color: var(--dbm-text-muted);
transition: var(--dbm-transition);
flex-shrink: 0;
}
.tree-toggle svg,
.tree-toggle [data-lucide] {
width: 10px;
height: 10px;
transition: transform 0.15s ease;
}
.tree-item.expanded > .tree-header .tree-toggle svg,
.tree-item.expanded > .tree-header .tree-toggle [data-lucide] {
transform: rotate(90deg);
}
.tree-icon {
width: 12px;
height: 12px;
margin-right: 6px;
flex-shrink: 0;
color: var(--dbm-text-muted);
display: flex;
align-items: center;
}
.tree-icon [data-lucide] {
width: 12px;
height: 12px;
}
.tree-icon.database { color: var(--dbm-accent-light); }
.tree-icon.table { color: var(--dbm-accent-light); }
.tree-icon.column { color: var(--dbm-text-muted); }
.tree-icon.key { color: var(--dbm-success); }
.tree-label {
font-size: 12px;
color: var(--dbm-text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
text-decoration: none;
}
a.tree-label:hover {
color: var(--dbm-accent);
}
.tree-badge {
font-size: 10px;
padding: 1px 5px;
background: var(--dbm-bg);
color: var(--dbm-text-muted);
border-radius: 8px;
margin-left: 6px;
border: 1px solid var(--dbm-border);
}
.tree-children {
display: none;
padding-left: 16px;
}
.tree-item.expanded > .tree-children {
display: block;
}
.tree-children .tree-header {
padding-left: 20px;
}
.tree-children .tree-children .tree-header {
padding-left: 28px;
}
.dbm-sidebar-actions {
padding: 12px;
border-top: 1px solid var(--dbm-border);
background: var(--dbm-bg);
}
.dbm-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
background: var(--dbm-bg);
}
.dbm-content-header {
padding: 16px 20px;
border-bottom: 1px solid var(--dbm-border);
background: var(--dbm-bg-secondary);
}
.dbm-breadcrumb {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--dbm-text-secondary);
margin-bottom: 6px;
}
.dbm-breadcrumb a {
color: var(--dbm-text-secondary);
text-decoration: none;
}
.dbm-breadcrumb a:hover {
color: var(--dbm-text);
text-decoration: underline;
}
.dbm-breadcrumb .separator {
color: var(--dbm-text-muted);
}
.dbm-title {
display: flex;
align-items: center;
gap: 10px;
}
.dbm-title h1 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--dbm-text);
}
.dbm-title .badge {
font-size: 11px;
padding: 3px 8px;
background: var(--dbm-bg);
color: var(--dbm-text-muted);
border-radius: 10px;
border: 1px solid var(--dbm-border);
}
.dbm-actions {
display: flex;
gap: 8px;
margin-top: 12px;
}
.dbm-content-body {
flex: 1;
overflow: auto;
padding: 20px;
}
.dbm-table-wrapper {
background: var(--dbm-bg);
border-radius: var(--dbm-radius);
border: 1px solid var(--dbm-border);
overflow: hidden;
}
.dbm-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.dbm-table th {
background: var(--dbm-bg-secondary);
padding: 10px 14px;
text-align: left;
font-weight: 600;
color: var(--dbm-text-secondary);
border-bottom: 1px solid var(--dbm-border);
white-space: nowrap;
position: sticky;
top: 0;
z-index: 10;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.dbm-table td {
padding: 8px 14px;
color: var(--dbm-text-secondary);
border-bottom: 1px solid var(--dbm-border);
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dbm-table tr:last-child td {
border-bottom: none;
}
.dbm-table tr:hover td {
background: var(--dbm-bg-secondary);
}
.dbm-table .null-value {
color: var(--dbm-text-muted);
font-style: italic;
font-size: 11px;
}
.dbm-table .key-column {
color: var(--dbm-success);
}
.dbm-table .type-column {
color: var(--dbm-text-muted);
font-family: 'Consolas', 'Monaco', monospace;
font-size: 11px;
}
.dbm-pagination {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: var(--dbm-bg-secondary);
border-top: 1px solid var(--dbm-border);
margin-top: 16px;
border-radius: var(--dbm-radius);
}
.dbm-pagination-info {
font-size: 12px;
color: var(--dbm-text-muted);
}
.dbm-pagination-controls {
display: flex;
align-items: center;
gap: 4px;
}
.dbm-pagination-btn {
padding: 5px 10px;
background: var(--dbm-bg);
border: 1px solid var(--dbm-border);
border-radius: var(--dbm-radius);
color: var(--dbm-text-secondary);
font-size: 12px;
cursor: pointer;
transition: var(--dbm-transition);
text-decoration: none;
}
.dbm-pagination-btn:hover:not(:disabled) {
background: var(--dbm-bg-secondary);
border-color: var(--dbm-accent-light);
}
.dbm-pagination-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.dbm-pagination-btn.active {
background: var(--dbm-accent);
border-color: var(--dbm-accent);
color: #fff;
}
.dbm-console {
background: var(--dbm-bg);
border-top: 1px solid var(--dbm-border);
display: flex;
flex-direction: column;
}
.dbm-console-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
background: var(--dbm-bg-secondary);
border-bottom: 1px solid var(--dbm-border);
cursor: pointer;
transition: var(--dbm-transition);
}
.dbm-console-header:hover {
background: var(--dbm-bg-tertiary);
}
.dbm-console-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
font-weight: 600;
color: var(--dbm-text-secondary);
}
.dbm-console-title .icon {
width: 14px;
height: 14px;
color: var(--dbm-text-muted);
}
.dbm-console-toggle {
width: 14px;
height: 14px;
color: var(--dbm-text-muted);
transition: var(--dbm-transition);
}
.dbm-console.expanded .dbm-console-toggle {
transform: rotate(180deg);
}
.dbm-console-body {
display: none;
padding: 16px;
}
.dbm-console.expanded .dbm-console-body {
display: block;
}
.dbm-sql-editor {
position: relative;
background: var(--dbm-bg-secondary);
border: 1px solid var(--dbm-border);
border-radius: var(--dbm-radius);
overflow: hidden;
}
.dbm-sql-editor textarea {
width: 100%;
min-height: 100px;
padding: 12px;
background: transparent;
border: none;
color: var(--dbm-text);
font-family: 'Consolas', 'Monaco', monospace;
font-size: 13px;
line-height: 1.5;
resize: vertical;
outline: none;
box-sizing: border-box;
}
.dbm-sql-editor textarea::placeholder {
color: var(--dbm-text-muted);
}
.dbm-sql-highlight {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
padding: 12px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 13px;
line-height: 1.5;
white-space: pre-wrap;
word-wrap: break-word;
pointer-events: none;
color: transparent;
overflow: hidden;
}
.sql-keyword { color: #555; font-weight: 600; }
.sql-function { color: #666; }
.sql-string { color: #888; }
.sql-number { color: #777; }
.sql-operator { color: #444; }
.sql-comment { color: #aaa; font-style: italic; }
.dbm-sql-actions {
display: flex;
align-items: center;
gap: 10px;
margin-top: 10px;
}
.dbm-sql-actions .db-select {
padding: 6px 10px;
background: var(--dbm-bg);
border: 1px solid var(--dbm-border);
border-radius: var(--dbm-radius);
color: var(--dbm-text);
font-size: 12px;
outline: none;
}
.dbm-sql-actions .db-select:focus {
border-color: var(--dbm-accent-light);
}
.dbm-sql-result {
margin-top: 16px;
border-radius: var(--dbm-radius);
overflow: hidden;
}
.dbm-sql-result.success {
background: #f5f8f5;
border: 1px solid #dde5dd;
}
.dbm-sql-result.error {
background: #f9f5f5;
border: 1px solid #e5dddd;
}
.dbm-sql-result-header {
padding: 10px 14px;
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
}
.dbm-sql-result-header i {
width: 14px;
height: 14px;
}
.dbm-sql-result.success .dbm-sql-result-header {
color: #5a7a5a;
}
.dbm-sql-result.error .dbm-sql-result-header {
color: #8a5a5a;
}
.dbm-sql-result-body {
padding: 0 14px 14px;
overflow-x: auto;
}
.dbm-sql-result-body .dbm-table-wrapper {
background: transparent;
border: none;
}
.dbm-btn {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 7px 14px;
font-size: 12px;
font-weight: 500;
border: 1px solid transparent;
border-radius: var(--dbm-radius);
cursor: pointer;
transition: var(--dbm-transition);
text-decoration: none;
white-space: nowrap;
font-family: Arial, sans-serif;
}
.dbm-btn svg,
.dbm-btn i {
width: 14px;
height: 14px;
}
.dbm-btn-primary {
background: var(--dbm-accent);
color: #fff;
border-color: var(--dbm-accent);
}
.dbm-btn-primary:hover {
background: #444;
border-color: #444;
}
.dbm-btn-success {
background: var(--dbm-success);
color: #fff;
border-color: var(--dbm-success);
}
.dbm-btn-success:hover {
background: #5a8a5a;
border-color: #5a8a5a;
}
.dbm-btn-danger {
background: var(--dbm-danger);
color: #fff;
border-color: var(--dbm-danger);
}
.dbm-btn-danger:hover {
background: #a56565;
border-color: #a56565;
}
.dbm-btn-secondary {
background: var(--dbm-bg);
border-color: var(--dbm-border);
color: var(--dbm-text-secondary);
}
.dbm-btn-secondary:hover {
background: var(--dbm-bg-secondary);
border-color: var(--dbm-accent-light);
color: var(--dbm-text);
}
.dbm-btn-sm {
padding: 4px 8px;
font-size: 11px;
}
.dbm-btn-sm svg,
.dbm-btn-sm i {
width: 12px;
height: 12px;
}
.dbm-form-group {
margin-bottom: 16px;
}
.dbm-form-label {
display: block;
margin-bottom: 6px;
font-size: 12px;
font-weight: 600;
color: var(--dbm-text-secondary);
}
.dbm-form-input,
.dbm-form-select {
width: 100%;
max-width: 350px;
padding: 8px 12px;
background: var(--dbm-bg);
border: 1px solid var(--dbm-border);
border-radius: var(--dbm-radius);
color: var(--dbm-text);
font-size: 13px;
outline: none;
transition: var(--dbm-transition);
font-family: Arial, sans-serif;
}
.dbm-form-input:focus,
.dbm-form-select:focus {
border-color: var(--dbm-accent-light);
box-shadow: 0 0 0 2px rgba(85, 85, 85, 0.1);
}
.dbm-form-input::placeholder {
color: var(--dbm-text-muted);
}
.dbm-card {
background: var(--dbm-bg);
border: 1px solid var(--dbm-border);
border-radius: var(--dbm-radius);
overflow: hidden;
}
.dbm-card-header {
padding: 12px 16px;
border-bottom: 1px solid var(--dbm-border);
background: var(--dbm-bg-secondary);
}
.dbm-card-header h3 {
margin: 0;
font-size: 12px;
font-weight: 600;
color: var(--dbm-text-secondary);
text-transform: uppercase;
letter-spacing: 0.3px;
}
.dbm-card-body {
padding: 16px;
}
.dbm-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
gap: 12px;
margin-bottom: 20px;
}
.dbm-stat {
background: var(--dbm-bg-secondary);
border: 1px solid var(--dbm-border);
border-radius: var(--dbm-radius);
padding: 14px 16px;
}
.dbm-stat-value {
font-size: 18px;
font-weight: 600;
color: var(--dbm-text);
margin-bottom: 2px;
}
.dbm-stat-label {
font-size: 10px;
color: var(--dbm-text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.dbm-empty {
text-align: center;
padding: 50px 20px;
color: var(--dbm-text-secondary);
}
.dbm-empty-icon {
width: 40px;
height: 40px;
margin: 0 auto 16px;
color: var(--dbm-border);
display: block;
}
.dbm-empty h3 {
margin: 0 0 6px;
font-size: 15px;
color: var(--dbm-text-secondary);
font-weight: 500;
}
.dbm-empty p {
margin: 0;
font-size: 13px;
color: var(--dbm-text-muted);
}
.dbm-loading {
display: flex;
align-items: center;
justify-content: center;
padding: 30px;
}
.dbm-spinner {
width: 20px;
height: 20px;
border: 2px solid var(--dbm-border);
border-top-color: var(--dbm-accent-light);
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@media (max-width: 1024px) {
.dbm-sidebar {
width: 220px;
}
}
@media (max-width: 768px) {
.dbm-main {
flex-direction: column;
}
.dbm-sidebar {
width: 100%;
max-height: 250px;
border-right: none;
border-bottom: 1px solid var(--dbm-border);
}
.dbm-content-body {
padding: 14px;
}
}
.data-row.editing {
background: #fafaf5 !important;
}
.data-row.new-row {
background: #f5faf5 !important;
}
.editable-cell {
position: relative;
}
.editable-cell .cell-input {
border: 1px solid var(--dbm-accent-light);
border-radius: 3px;
background: var(--dbm-bg);
}
.editable-cell .cell-input:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(85, 85, 85, 0.1);
}
.view-actions,
.edit-actions {
display: inline-flex;
gap: 4px;
}
.actions-cell .dbm-btn-sm {
padding: 4px 6px;
}
.data-row:hover {
background: var(--dbm-bg-secondary);
}
.data-row.editing:hover {
background: #fafaf5 !important;
}
.data-row.new-row:hover {
background: #f5faf5 !important;
}
.dbm-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.dbm-modal-content {
background: var(--dbm-bg);
border-radius: var(--dbm-radius);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
width: 100%;
max-width: 420px;
max-height: 90vh;
overflow: hidden;
}
.dbm-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
border-bottom: 1px solid var(--dbm-border);
background: var(--dbm-bg-secondary);
}
.dbm-modal-header h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: var(--dbm-text);
}
.dbm-modal-close {
background: none;
border: none;
font-size: 20px;
color: var(--dbm-text-muted);
cursor: pointer;
line-height: 1;
padding: 0;
}
.dbm-modal-close:hover {
color: var(--dbm-text);
}
.dbm-modal-body {
padding: 18px;
}
.dbm-modal-body .dbm-form-input,
.dbm-modal-body .dbm-form-select {
max-width: 100%;
}
.dbm-modal-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 14px 18px;
border-top: 1px solid var(--dbm-border);
background: var(--dbm-bg-secondary);
}
.badge {
font-size: 10px;
padding: 2px 6px;
background: var(--dbm-bg-tertiary);
color: var(--dbm-text-muted);
border-radius: 8px;
border: 1px solid var(--dbm-border);
}

389
public/js/dbmanager.js Normal file
View File

@@ -0,0 +1,389 @@
const DBManager = {
baseUrl: '',
init(baseUrl) {
this.baseUrl = baseUrl;
this.initTree();
this.initConsole();
this.initSqlHighlighting();
this.initAjaxForms();
},
initTree() {
document.querySelectorAll('.tree-header').forEach(header => {
header.addEventListener('click', (e) => {
const item = header.closest('.tree-item');
const children = item.querySelector('.tree-children');
const href = header.dataset.href;
const toggle = e.target.closest('.tree-toggle');
// If clicking on toggle icon, just expand/collapse
if (toggle) {
e.preventDefault();
e.stopPropagation();
if (item.dataset.db && children && !children.dataset.loaded) {
this.loadTables(item.dataset.db, children);
}
this.toggleTreeItem(item);
return;
}
// If it's a table or database item with href, navigate directly
if (href) {
// For databases, also load tables if not loaded
if (item.dataset.db && children && !children.dataset.loaded) {
this.loadTables(item.dataset.db, children);
}
window.location.href = href;
return;
}
});
});
},
toggleTreeItem(item) {
item.classList.toggle('expanded');
},
async loadTables(dbName, container) {
container.innerHTML = '<div class="tree-loading">Loading...</div>';
container.dataset.loaded = 'true';
try {
const response = await fetch(`${this.baseUrl}database/getStructure/${encodeURIComponent(dbName)}`);
const data = await response.json();
if (data.success && data.structure) {
let html = '';
for (const [table, columns] of Object.entries(data.structure)) {
html += this.renderTableTreeItem(dbName, table, columns);
}
container.innerHTML = html || '<div class="tree-empty">No tables</div>';
this.initTree();
this.refreshIcons();
}
} catch (error) {
container.innerHTML = '<div class="tree-error">Failed to load</div>';
}
},
renderTableTreeItem(dbName, tableName, columns) {
const columnsHtml = columns.map(col => `
<div class="tree-item">
<div class="tree-header">
<span class="tree-icon column">
${col.Key === 'PRI' ? this.icons.key : this.icons.column}
</span>
<span class="tree-label">${this.escapeHtml(col.Field)}</span>
<span class="tree-badge">${this.escapeHtml(col.Type)}</span>
</div>
</div>
`).join('');
return `
<div class="tree-item" data-table="${this.escapeHtml(tableName)}">
<div class="tree-header" data-href="${this.baseUrl}table/show/${encodeURIComponent(dbName)}/${encodeURIComponent(tableName)}">
<span class="tree-toggle">${this.icons.chevron}</span>
<span class="tree-icon table">${this.icons.table}</span>
<span class="tree-label">${this.escapeHtml(tableName)}</span>
</div>
<div class="tree-children">
${columnsHtml}
</div>
</div>
`;
},
initConsole() {
const consoleHeader = document.querySelector('.dbm-console-header');
if (consoleHeader) {
consoleHeader.addEventListener('click', () => {
const console = consoleHeader.closest('.dbm-console');
console.classList.toggle('expanded');
localStorage.setItem('dbm-console-expanded', console.classList.contains('expanded'));
});
const wasExpanded = localStorage.getItem('dbm-console-expanded') === 'true';
if (wasExpanded) {
consoleHeader.closest('.dbm-console').classList.add('expanded');
}
}
const sqlForm = document.getElementById('sql-form');
if (sqlForm) {
sqlForm.addEventListener('submit', async (e) => {
e.preventDefault();
await this.executeQuery(sqlForm);
});
}
},
async executeQuery(form) {
const formData = new FormData(form);
const resultContainer = document.getElementById('sql-result');
resultContainer.innerHTML = '<div class="dbm-loading"><div class="dbm-spinner"></div></div>';
try {
const response = await fetch(`${this.baseUrl}sql/execute`, {
method: 'POST',
body: formData,
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await response.json();
this.renderSqlResult(data, resultContainer);
} catch (error) {
resultContainer.innerHTML = `
<div class="dbm-sql-result error">
<div class="dbm-sql-result-header">
${this.icons.error} Error: Failed to execute query
</div>
</div>
`;
}
},
renderSqlResult(data, container) {
if (data.success) {
let tableHtml = '';
if (data.result && data.result.length > 0) {
const columns = Object.keys(data.result[0]);
tableHtml = `
<div class="dbm-table-wrapper">
<table class="dbm-table">
<thead>
<tr>${columns.map(col => `<th>${this.escapeHtml(col)}</th>`).join('')}</tr>
</thead>
<tbody>
${data.result.map(row => `
<tr>${columns.map(col => `
<td>${row[col] === null ? '<span class="null-value">NULL</span>' : this.escapeHtml(String(row[col]).substring(0, 100))}</td>
`).join('')}</tr>
`).join('')}
</tbody>
</table>
</div>
`;
}
container.innerHTML = `
<div class="dbm-sql-result success">
<div class="dbm-sql-result-header">
${this.icons.success} ${this.escapeHtml(data.message)}
<span style="margin-left: auto; color: var(--text-muted); font-size: 12px;">
${data.execution_time}ms
</span>
</div>
${tableHtml ? `<div class="dbm-sql-result-body">${tableHtml}</div>` : ''}
</div>
`;
} else {
container.innerHTML = `
<div class="dbm-sql-result error">
<div class="dbm-sql-result-header">
${this.icons.error} ${this.escapeHtml(data.message)}
</div>
${data.error ? `<div class="dbm-sql-result-body" style="padding: 16px; font-family: monospace; font-size: 13px; color: var(--accent-red);">${this.escapeHtml(data.error)}</div>` : ''}
</div>
`;
}
this.refreshIcons();
},
initSqlHighlighting() {
const textarea = document.getElementById('sql_query');
if (!textarea) return;
textarea.addEventListener('input', () => this.highlightSql(textarea));
textarea.addEventListener('scroll', () => this.syncScroll(textarea));
this.highlightSql(textarea);
},
highlightSql(textarea) {
const highlight = document.getElementById('sql-highlight');
if (!highlight) return;
let code = textarea.value;
code = this.escapeHtml(code);
code = this.applySqlSyntax(code);
highlight.innerHTML = code + '\n';
},
applySqlSyntax(code) {
const keywords = [
'SELECT', 'FROM', 'WHERE', 'AND', 'OR', 'NOT', 'IN', 'LIKE', 'BETWEEN',
'ORDER BY', 'GROUP BY', 'HAVING', 'LIMIT', 'OFFSET', 'JOIN', 'INNER JOIN',
'LEFT JOIN', 'RIGHT JOIN', 'OUTER JOIN', 'ON', 'AS', 'DISTINCT', 'ALL',
'INSERT', 'INTO', 'VALUES', 'UPDATE', 'SET', 'DELETE', 'CREATE', 'TABLE',
'DATABASE', 'INDEX', 'VIEW', 'DROP', 'ALTER', 'ADD', 'COLUMN', 'PRIMARY KEY',
'FOREIGN KEY', 'REFERENCES', 'CONSTRAINT', 'DEFAULT', 'NULL', 'NOT NULL',
'AUTO_INCREMENT', 'UNIQUE', 'ENGINE', 'CHARSET', 'COLLATE', 'IF', 'EXISTS',
'SHOW', 'DESCRIBE', 'EXPLAIN', 'USE', 'GRANT', 'REVOKE', 'UNION', 'CASE',
'WHEN', 'THEN', 'ELSE', 'END', 'IS', 'TRUE', 'FALSE', 'ASC', 'DESC'
];
const functions = [
'COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'CONCAT', 'SUBSTRING', 'LENGTH',
'UPPER', 'LOWER', 'TRIM', 'REPLACE', 'NOW', 'CURDATE', 'DATE', 'YEAR',
'MONTH', 'DAY', 'HOUR', 'MINUTE', 'COALESCE', 'IFNULL', 'NULLIF', 'CAST',
'CONVERT', 'FORMAT', 'ROUND', 'FLOOR', 'CEIL', 'ABS', 'MOD', 'RAND'
];
code = code.replace(/'([^'\\]|\\.)*'/g, '<span class="sql-string">$&</span>');
code = code.replace(/"([^"\\]|\\.)*"/g, '<span class="sql-string">$&</span>');
code = code.replace(/\b(\d+\.?\d*)\b/g, '<span class="sql-number">$1</span>');
functions.forEach(func => {
const regex = new RegExp(`\\b(${func})\\s*\\(`, 'gi');
code = code.replace(regex, '<span class="sql-function">$1</span>(');
});
keywords.forEach(keyword => {
const regex = new RegExp(`\\b(${keyword.replace(' ', '\\s+')})\\b`, 'gi');
code = code.replace(regex, '<span class="sql-keyword">$1</span>');
});
code = code.replace(/(--[^\n]*)/g, '<span class="sql-comment">$1</span>');
code = code.replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="sql-comment">$1</span>');
return code;
},
syncScroll(textarea) {
const highlight = document.getElementById('sql-highlight');
if (highlight) {
highlight.scrollTop = textarea.scrollTop;
highlight.scrollLeft = textarea.scrollLeft;
}
},
initAjaxForms() {
document.querySelectorAll('[data-ajax-form]').forEach(form => {
form.addEventListener('submit', async (e) => {
e.preventDefault();
await this.submitAjaxForm(form);
});
});
document.querySelectorAll('[data-confirm]').forEach(el => {
el.addEventListener('click', (e) => {
if (!confirm(el.dataset.confirm)) {
e.preventDefault();
}
});
});
},
async submitAjaxForm(form) {
const formData = new FormData(form);
const submitBtn = form.querySelector('[type="submit"]');
const originalText = submitBtn?.innerHTML;
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.innerHTML = '<span class="dbm-spinner" style="width:16px;height:16px;border-width:2px;"></span>';
}
try {
const response = await fetch(form.action, {
method: 'POST',
body: formData,
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await response.json();
if (data.success) {
if (data.redirect) {
window.location.href = data.redirect;
} else if (data.reload) {
window.location.reload();
} else {
this.showNotification(data.message, 'success');
}
} else {
this.showNotification(data.message || 'An error occurred', 'error');
}
} catch (error) {
this.showNotification('Request failed', 'error');
} finally {
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.innerHTML = originalText;
}
}
},
showNotification(message, type) {
const notification = document.createElement('div');
notification.className = `dbm-notification ${type}`;
notification.innerHTML = `
${type === 'success' ? this.icons.success : this.icons.error}
<span>${this.escapeHtml(message)}</span>
`;
notification.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
padding: 12px 20px;
background: ${type === 'success' ? 'var(--accent-green)' : 'var(--accent-red)'};
color: #fff;
border-radius: var(--radius);
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
box-shadow: var(--shadow);
z-index: 1000;
animation: slideIn 0.3s ease;
`;
document.body.appendChild(notification);
this.refreshIcons();
setTimeout(() => {
notification.style.animation = 'slideOut 0.3s ease';
setTimeout(() => notification.remove(), 300);
}, 3000);
},
escapeHtml(str) {
if (!str) return '';
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
},
icons: {
chevron: '<i data-lucide="chevron-right"></i>',
database: '<i data-lucide="database"></i>',
table: '<i data-lucide="table"></i>',
column: '<i data-lucide="columns-2"></i>',
key: '<i data-lucide="key-round"></i>',
success: '<i data-lucide="check-circle"></i>',
error: '<i data-lucide="x-circle"></i>',
terminal: '<i data-lucide="terminal"></i>'
},
refreshIcons() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
}
};
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
`;
document.head.appendChild(style);