Initial commit
This commit is contained in:
945
public/css/dbmanager.css
Normal file
945
public/css/dbmanager.css
Normal 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
389
public/js/dbmanager.js
Normal 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);
|
||||
Reference in New Issue
Block a user