feat(web): add database overview panel, collection overview and resizable panels; show/hide system databases; use code editor with change-tracking in document view; add getDatabaseStats API and typings; enable overwrite for S3 uploads
This commit is contained in:
12
changelog.md
12
changelog.md
@@ -1,5 +1,17 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-01-25 - 1.4.0 - feat(web)
|
||||
add database overview panel, collection overview and resizable panels; show/hide system databases; use code editor with change-tracking in document view; add getDatabaseStats API and typings; enable overwrite for S3 uploads
|
||||
|
||||
- Add backend handler getDatabaseStats + request/response typings (IReq_GetDatabaseStats, IDatabaseStats) and ApiService.getDatabaseStats()
|
||||
- New UI component tsview-mongo-db-overview to show database statistics (collections, objects, avg size, data/storage size, indexes)
|
||||
- Collections list: added an "Overview" entry that opens the DB overview when selected
|
||||
- Sidebar: context menu option to show/hide system databases (admin/config/local) and database collapse-on-click behavior
|
||||
- Resizable layout improvements: draggable dividers added for sidebar, Mongo editor panel and S3 preview panel (persisted via local state variables)
|
||||
- Document editor: switch to dees-input-code, track original content and unsaved changes, add discard/save flow and improved save handling
|
||||
- S3 handlers: fastPut calls now pass overwrite: true to allow replacing existing keys
|
||||
- Minor dependency bumps: @git.zone/tstest and @design.estate/dees-catalog
|
||||
|
||||
## 2026-01-25 - 1.3.0 - feat(s3)
|
||||
add S3 create file/folder dialogs and in-place text editor; export mongodb plugin
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"@git.zone/tsbuild": "^4.1.2",
|
||||
"@git.zone/tsbundle": "^2.8.3",
|
||||
"@git.zone/tsrun": "^2.0.1",
|
||||
"@git.zone/tstest": "^3.1.6",
|
||||
"@git.zone/tstest": "^3.1.7",
|
||||
"@git.zone/tswatch": "3.0.1",
|
||||
"@types/node": "^25.0.10"
|
||||
},
|
||||
@@ -31,7 +31,7 @@
|
||||
"@api.global/typedrequest-interfaces": "^3.0.19",
|
||||
"@api.global/typedserver": "^8.3.0",
|
||||
"@aws-sdk/client-s3": "^3.975.0",
|
||||
"@design.estate/dees-catalog": "^3.37.0",
|
||||
"@design.estate/dees-catalog": "^3.37.1",
|
||||
"@design.estate/dees-element": "^2.1.5",
|
||||
"@push.rocks/early": "^4.0.4",
|
||||
"@push.rocks/npmextra": "^5.3.3",
|
||||
|
||||
37
pnpm-lock.yaml
generated
37
pnpm-lock.yaml
generated
@@ -21,8 +21,8 @@ importers:
|
||||
specifier: ^3.975.0
|
||||
version: 3.975.0
|
||||
'@design.estate/dees-catalog':
|
||||
specifier: ^3.37.0
|
||||
version: 3.37.0(@tiptap/pm@2.27.2)
|
||||
specifier: ^3.37.1
|
||||
version: 3.37.1(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-element':
|
||||
specifier: ^2.1.5
|
||||
version: 2.1.5
|
||||
@@ -76,8 +76,8 @@ importers:
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1
|
||||
'@git.zone/tstest':
|
||||
specifier: ^3.1.6
|
||||
version: 3.1.6(@push.rocks/smartserve@2.0.1)(socks@2.8.7)(typescript@5.9.3)
|
||||
specifier: ^3.1.7
|
||||
version: 3.1.7(@push.rocks/smartserve@2.0.1)(socks@2.8.7)(typescript@5.9.3)
|
||||
'@git.zone/tswatch':
|
||||
specifier: 3.0.1
|
||||
version: 3.0.1(@tiptap/pm@2.27.2)
|
||||
@@ -319,11 +319,14 @@ packages:
|
||||
'@cloudflare/workers-types@4.20260123.0':
|
||||
resolution: {integrity: sha512-pQccZ8IDLFKkvdKBXZRPkbMtWtS7vVz1giJGkAAZ5cZH2RHK5Bs6p1OoVZA8Z2Sry8Q0tZbZ5Yjud4R7SrG3KQ==}
|
||||
|
||||
'@cloudflare/workers-types@4.20260124.0':
|
||||
resolution: {integrity: sha512-h6TJlew6AtGuEXFc+k5ifalk+tg3fkg0lla6XbMAb2AKKfJGwlFNTwW2xyT/Ha92KY631CIJ+Ace08DPdFohdA==}
|
||||
|
||||
'@configvault.io/interfaces@1.0.17':
|
||||
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
|
||||
|
||||
'@design.estate/dees-catalog@3.37.0':
|
||||
resolution: {integrity: sha512-c6q+yK2FwMsMK72GykUhZnvKUgTzjFO9vdbn6OBxas2/eY/6Wi6BC5i9YONN0UYcW8yqjHIDjN9nP7yE1Ai4PA==}
|
||||
'@design.estate/dees-catalog@3.37.1':
|
||||
resolution: {integrity: sha512-NCgzzCG3NJVF7C7aa1nExCMhB+7nA6glFgZpsff32CpvdtbAuBQiuOngU0suVw65uK7Y0a2r/y2CEPGNNmj3TA==}
|
||||
|
||||
'@design.estate/dees-comms@1.0.30':
|
||||
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
|
||||
@@ -538,8 +541,8 @@ packages:
|
||||
resolution: {integrity: sha512-NEcnsjvlC1o3Z6SS3VhKCf6Ev+Sh4EAinmggslrIR/ppMrvjDbXNFXoyr3PB+GLeSAR0JRZ1fGvVYjpEzjBdIg==}
|
||||
hasBin: true
|
||||
|
||||
'@git.zone/tstest@3.1.6':
|
||||
resolution: {integrity: sha512-xRGc6wO4rJ6mohPCMIBDRH+oNjiIvX6Jeo8v/Y5o5VyKSHFmqol7FCKSBrojMcqgBpESnLHFPJAAOmT9W3JV8Q==}
|
||||
'@git.zone/tstest@3.1.7':
|
||||
resolution: {integrity: sha512-YCDA+65LJhoY3WJxrNduKlpGf37aq4bFe+fdRqE0dZ2W1f7j3sUunBaBzckShSHKRjkMdPZKr0W0sXFXUK/PcA==}
|
||||
hasBin: true
|
||||
|
||||
'@git.zone/tswatch@3.0.1':
|
||||
@@ -2805,8 +2808,8 @@ packages:
|
||||
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
lucide@0.562.0:
|
||||
resolution: {integrity: sha512-k1Fb8ZMnRQovWRlea7Jr0b9UKA29IM7/cu79+mJrhVohvA2YC/Ti3Sk+G+h/SIu3IlrKT4RAbWMHUBBQd1O6XA==}
|
||||
lucide@0.563.0:
|
||||
resolution: {integrity: sha512-2zBzDJ5n2Plj3d0ksj6h9TWPOSiKu9gtxJxnBAye11X/8gfWied6IYJn6ADYBp1NPoJmgpyOYP3wMrVx69+2AA==}
|
||||
|
||||
make-dir@3.1.0:
|
||||
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
|
||||
@@ -4008,7 +4011,7 @@ snapshots:
|
||||
'@api.global/typedrequest': 3.2.5
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
'@api.global/typedsocket': 3.1.1(@push.rocks/smartserve@2.0.1)
|
||||
'@cloudflare/workers-types': 4.20260123.0
|
||||
'@cloudflare/workers-types': 4.20260124.0
|
||||
'@design.estate/dees-comms': 1.0.30
|
||||
'@push.rocks/lik': 6.2.2
|
||||
'@push.rocks/smartchok': 1.2.0
|
||||
@@ -4057,7 +4060,7 @@ snapshots:
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
'@api.global/typedsocket': 4.1.0(@push.rocks/smartserve@2.0.1)
|
||||
'@cloudflare/workers-types': 4.20260123.0
|
||||
'@design.estate/dees-catalog': 3.37.0(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-catalog': 3.37.1(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-comms': 1.0.30
|
||||
'@push.rocks/lik': 6.2.2
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
@@ -4679,11 +4682,13 @@ snapshots:
|
||||
|
||||
'@cloudflare/workers-types@4.20260123.0': {}
|
||||
|
||||
'@cloudflare/workers-types@4.20260124.0': {}
|
||||
|
||||
'@configvault.io/interfaces@1.0.17':
|
||||
dependencies:
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
|
||||
'@design.estate/dees-catalog@3.37.0(@tiptap/pm@2.27.2)':
|
||||
'@design.estate/dees-catalog@3.37.1(@tiptap/pm@2.27.2)':
|
||||
dependencies:
|
||||
'@design.estate/dees-domtools': 2.3.7
|
||||
'@design.estate/dees-element': 2.1.5
|
||||
@@ -4706,7 +4711,7 @@ snapshots:
|
||||
apexcharts: 5.3.6
|
||||
highlight.js: 11.11.1
|
||||
ibantools: 4.5.1
|
||||
lucide: 0.562.0
|
||||
lucide: 0.563.0
|
||||
monaco-editor: 0.55.1
|
||||
pdfjs-dist: 4.10.38
|
||||
xterm: 5.3.0
|
||||
@@ -4962,7 +4967,7 @@ snapshots:
|
||||
'@push.rocks/smartshell': 3.3.0
|
||||
tsx: 4.21.0
|
||||
|
||||
'@git.zone/tstest@3.1.6(@push.rocks/smartserve@2.0.1)(socks@2.8.7)(typescript@5.9.3)':
|
||||
'@git.zone/tstest@3.1.7(@push.rocks/smartserve@2.0.1)(socks@2.8.7)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1)
|
||||
'@git.zone/tsbundle': 2.8.3
|
||||
@@ -8106,7 +8111,7 @@ snapshots:
|
||||
|
||||
lru-cache@7.18.3: {}
|
||||
|
||||
lucide@0.562.0: {}
|
||||
lucide@0.563.0: {}
|
||||
|
||||
make-dir@3.1.0:
|
||||
dependencies:
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@git.zone/tsview',
|
||||
version: '1.3.0',
|
||||
version: '1.4.0',
|
||||
description: 'A CLI tool for viewing S3 and MongoDB data with a web UI'
|
||||
}
|
||||
|
||||
@@ -53,6 +53,37 @@ export async function registerMongoHandlers(
|
||||
)
|
||||
);
|
||||
|
||||
// Get database stats
|
||||
typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.IReq_GetDatabaseStats>(
|
||||
'getDatabaseStats',
|
||||
async (reqData) => {
|
||||
try {
|
||||
const client = await getMongoClient();
|
||||
const db = client.db(reqData.databaseName);
|
||||
const stats = await db.stats();
|
||||
const collections = await db.listCollections().toArray();
|
||||
|
||||
return {
|
||||
stats: {
|
||||
collections: collections.length,
|
||||
views: stats.views || 0,
|
||||
objects: stats.objects || 0,
|
||||
avgObjSize: stats.avgObjSize || 0,
|
||||
dataSize: stats.dataSize || 0,
|
||||
storageSize: stats.storageSize || 0,
|
||||
indexes: stats.indexes || 0,
|
||||
indexSize: stats.indexSize || 0,
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Error getting database stats:', err);
|
||||
return { stats: null };
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// List collections
|
||||
typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.IReq_ListCollections>(
|
||||
|
||||
@@ -292,6 +292,7 @@ export async function registerS3Handlers(
|
||||
await bucket.fastPut({
|
||||
path: reqData.key,
|
||||
contents: content,
|
||||
overwrite: true,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
@@ -354,6 +355,7 @@ export async function registerS3Handlers(
|
||||
await destBucket.fastPut({
|
||||
path: reqData.destKey,
|
||||
contents: content,
|
||||
overwrite: true,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -511,3 +511,27 @@ export interface IReq_GetServerStatus extends plugins.typedrequestInterfaces.imp
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface IDatabaseStats {
|
||||
collections: number;
|
||||
views: number;
|
||||
objects: number;
|
||||
avgObjSize: number;
|
||||
dataSize: number;
|
||||
storageSize: number;
|
||||
indexes: number;
|
||||
indexSize: number;
|
||||
}
|
||||
|
||||
export interface IReq_GetDatabaseStats extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_GetDatabaseStats
|
||||
> {
|
||||
method: 'getDatabaseStats';
|
||||
request: {
|
||||
databaseName: string;
|
||||
};
|
||||
response: {
|
||||
stats: IDatabaseStats | null;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@git.zone/tsview',
|
||||
version: '1.3.0',
|
||||
version: '1.4.0',
|
||||
description: 'A CLI tool for viewing S3 and MongoDB data with a web UI'
|
||||
}
|
||||
|
||||
@@ -13,3 +13,4 @@ export * from './tsview-mongo-collections.js';
|
||||
export * from './tsview-mongo-documents.js';
|
||||
export * from './tsview-mongo-document.js';
|
||||
export * from './tsview-mongo-indexes.js';
|
||||
export * from './tsview-mongo-db-overview.js';
|
||||
|
||||
@@ -45,6 +45,9 @@ export class TsviewApp extends DeesElement {
|
||||
@state()
|
||||
private accessor newDatabaseName: string = '';
|
||||
|
||||
@state()
|
||||
private accessor showSystemDatabases: boolean = false;
|
||||
|
||||
@state()
|
||||
private accessor showS3CreateDialog: boolean = false;
|
||||
|
||||
@@ -57,6 +60,12 @@ export class TsviewApp extends DeesElement {
|
||||
@state()
|
||||
private accessor s3CreateDialogName: string = '';
|
||||
|
||||
@state()
|
||||
private accessor sidebarWidth: number = 240;
|
||||
|
||||
@state()
|
||||
private accessor isResizingSidebar: boolean = false;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
themeStyles,
|
||||
@@ -130,10 +139,22 @@ export class TsviewApp extends DeesElement {
|
||||
|
||||
.app-main {
|
||||
display: grid;
|
||||
grid-template-columns: 240px 1fr;
|
||||
grid-template-columns: var(--sidebar-width, 240px) 4px 1fr;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.resize-divider {
|
||||
width: 4px;
|
||||
background: transparent;
|
||||
cursor: col-resize;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.resize-divider:hover,
|
||||
.resize-divider.active {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background: #1e1e1e;
|
||||
border-right: 1px solid #333;
|
||||
@@ -389,6 +410,15 @@ export class TsviewApp extends DeesElement {
|
||||
`,
|
||||
];
|
||||
|
||||
private readonly SYSTEM_DATABASES = ['admin', 'config', 'local'];
|
||||
|
||||
private get visibleDatabases() {
|
||||
if (this.showSystemDatabases) {
|
||||
return this.databases;
|
||||
}
|
||||
return this.databases.filter(db => !this.SYSTEM_DATABASES.includes(db.name));
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
await this.loadData();
|
||||
@@ -423,8 +453,15 @@ export class TsviewApp extends DeesElement {
|
||||
}
|
||||
|
||||
private selectDatabase(db: string) {
|
||||
this.selectedDatabase = db;
|
||||
this.selectedCollection = '';
|
||||
if (this.selectedDatabase === db) {
|
||||
// Collapse - clicking the same database again
|
||||
// Keep the collection selection intact
|
||||
this.selectedDatabase = '';
|
||||
} else {
|
||||
// Switch to different database - clear collection
|
||||
this.selectedDatabase = db;
|
||||
this.selectedCollection = '';
|
||||
}
|
||||
}
|
||||
|
||||
private selectCollection(collection: string) {
|
||||
@@ -635,6 +672,38 @@ export class TsviewApp extends DeesElement {
|
||||
]);
|
||||
}
|
||||
|
||||
private handleSidebarContextMenu(event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
DeesContextmenu.openContextMenuWithOptions(event, [
|
||||
{
|
||||
name: this.showSystemDatabases ? 'Hide System Databases' : 'Show System Databases',
|
||||
iconName: this.showSystemDatabases ? 'lucide:eyeOff' : 'lucide:eye',
|
||||
action: async () => {
|
||||
this.showSystemDatabases = !this.showSystemDatabases;
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
private startSidebarResize = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
this.isResizingSidebar = true;
|
||||
document.addEventListener('mousemove', this.handleSidebarResize);
|
||||
document.addEventListener('mouseup', this.endSidebarResize);
|
||||
};
|
||||
|
||||
private handleSidebarResize = (e: MouseEvent) => {
|
||||
if (!this.isResizingSidebar) return;
|
||||
const newWidth = Math.min(Math.max(e.clientX, 150), 500);
|
||||
this.sidebarWidth = newWidth;
|
||||
};
|
||||
|
||||
private endSidebarResize = () => {
|
||||
this.isResizingSidebar = false;
|
||||
document.removeEventListener('mousemove', this.handleSidebarResize);
|
||||
document.removeEventListener('mouseup', this.endSidebarResize);
|
||||
};
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="app-container">
|
||||
@@ -669,8 +738,12 @@ export class TsviewApp extends DeesElement {
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="app-main">
|
||||
<main class="app-main" style="--sidebar-width: ${this.sidebarWidth}px">
|
||||
${this.renderSidebar()}
|
||||
<div
|
||||
class="resize-divider ${this.isResizingSidebar ? 'active' : ''}"
|
||||
@mousedown=${this.startSidebarResize}
|
||||
></div>
|
||||
${this.renderContent()}
|
||||
</main>
|
||||
</div>
|
||||
@@ -849,7 +922,7 @@ export class TsviewApp extends DeesElement {
|
||||
|
||||
if (this.viewMode === 'mongo') {
|
||||
return html`
|
||||
<aside class="sidebar">
|
||||
<aside class="sidebar" @contextmenu=${(e: MouseEvent) => this.handleSidebarContextMenu(e)}>
|
||||
<div class="sidebar-header">Databases & Collections</div>
|
||||
<button class="create-btn" @click=${() => this.showCreateDatabaseDialog = true}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
@@ -859,9 +932,9 @@ export class TsviewApp extends DeesElement {
|
||||
New Database
|
||||
</button>
|
||||
<div class="sidebar-list">
|
||||
${this.databases.length === 0
|
||||
${this.visibleDatabases.length === 0
|
||||
? html`<div class="sidebar-item" style="color: #666; cursor: default;">No databases found</div>`
|
||||
: this.databases.map((db) => this.renderDatabaseGroup(db))}
|
||||
: this.visibleDatabases.map((db) => this.renderDatabaseGroup(db))}
|
||||
</div>
|
||||
</aside>
|
||||
`;
|
||||
@@ -954,6 +1027,17 @@ export class TsviewApp extends DeesElement {
|
||||
`;
|
||||
}
|
||||
|
||||
// Show database overview when __overview__ is selected
|
||||
if (this.selectedCollection === '__overview__') {
|
||||
return html`
|
||||
<div class="content-area">
|
||||
<tsview-mongo-db-overview
|
||||
.databaseName=${this.selectedDatabase}
|
||||
></tsview-mongo-db-overview>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="content-area">
|
||||
<tsview-mongo-browser
|
||||
|
||||
@@ -24,6 +24,12 @@ export class TsviewMongoBrowser extends DeesElement {
|
||||
@state()
|
||||
private accessor stats: ICollectionStats | null = null;
|
||||
|
||||
@state()
|
||||
private accessor editorWidth: number = 400;
|
||||
|
||||
@state()
|
||||
private accessor isResizingEditor: boolean = false;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
themeStyles,
|
||||
@@ -102,11 +108,23 @@ export class TsviewMongoBrowser extends DeesElement {
|
||||
.content {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 400px;
|
||||
gap: 16px;
|
||||
grid-template-columns: 1fr 4px var(--editor-width, 400px);
|
||||
gap: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.resize-divider {
|
||||
width: 4px;
|
||||
background: transparent;
|
||||
cursor: col-resize;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.resize-divider:hover,
|
||||
.resize-divider.active {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.main-panel {
|
||||
overflow: auto;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
@@ -117,6 +135,7 @@ export class TsviewMongoBrowser extends DeesElement {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
@@ -124,7 +143,8 @@ export class TsviewMongoBrowser extends DeesElement {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.detail-panel {
|
||||
.detail-panel,
|
||||
.resize-divider {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -162,6 +182,28 @@ export class TsviewMongoBrowser extends DeesElement {
|
||||
this.selectedDocumentId = e.detail.documentId;
|
||||
}
|
||||
|
||||
private startEditorResize = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
this.isResizingEditor = true;
|
||||
document.addEventListener('mousemove', this.handleEditorResize);
|
||||
document.addEventListener('mouseup', this.endEditorResize);
|
||||
};
|
||||
|
||||
private handleEditorResize = (e: MouseEvent) => {
|
||||
if (!this.isResizingEditor) return;
|
||||
const contentEl = this.shadowRoot?.querySelector('.content');
|
||||
if (!contentEl) return;
|
||||
const containerRect = contentEl.getBoundingClientRect();
|
||||
const newWidth = Math.min(Math.max(containerRect.right - e.clientX, 300), 700);
|
||||
this.editorWidth = newWidth;
|
||||
};
|
||||
|
||||
private endEditorResize = () => {
|
||||
this.isResizingEditor = false;
|
||||
document.removeEventListener('mousemove', this.handleEditorResize);
|
||||
document.removeEventListener('mouseup', this.endEditorResize);
|
||||
};
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="browser-container">
|
||||
@@ -201,7 +243,7 @@ export class TsviewMongoBrowser extends DeesElement {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="content" style="--editor-width: ${this.editorWidth}px">
|
||||
<div class="main-panel">
|
||||
${this.activeTab === 'documents'
|
||||
? html`
|
||||
@@ -225,6 +267,11 @@ export class TsviewMongoBrowser extends DeesElement {
|
||||
`}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="resize-divider ${this.isResizingEditor ? 'active' : ''}"
|
||||
@mousedown=${this.startEditorResize}
|
||||
></div>
|
||||
|
||||
<div class="detail-panel">
|
||||
<tsview-mongo-document
|
||||
.databaseName=${this.databaseName}
|
||||
|
||||
@@ -90,6 +90,33 @@ export class TsviewMongoCollections extends DeesElement {
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.overview-item {
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: background 0.1s;
|
||||
color: #a5d6a7;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.overview-item:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.overview-item.selected {
|
||||
background: rgba(165, 214, 167, 0.15);
|
||||
color: #a5d6a7;
|
||||
}
|
||||
|
||||
.overview-item svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -168,36 +195,56 @@ export class TsviewMongoCollections extends DeesElement {
|
||||
await this.loadCollections();
|
||||
}
|
||||
|
||||
private selectOverview() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('collection-selected', {
|
||||
detail: '__overview__',
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.loading) {
|
||||
return html`<div class="loading-state">Loading collections...</div>`;
|
||||
}
|
||||
|
||||
if (this.collections.length === 0) {
|
||||
return html`<div class="empty-state">No collections</div>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="collections-list">
|
||||
${this.collections.map(
|
||||
(coll) => html`
|
||||
<div
|
||||
class="collection-item ${this.selectedCollection === coll.name ? 'selected' : ''}"
|
||||
@click=${() => this.selectCollection(coll.name)}
|
||||
@contextmenu=${(e: MouseEvent) => this.handleCollectionContextMenu(e, coll)}
|
||||
>
|
||||
<span class="collection-name">
|
||||
<svg class="collection-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
|
||||
</svg>
|
||||
${coll.name}
|
||||
</span>
|
||||
${coll.count !== undefined
|
||||
? html`<span class="collection-count">${formatCount(coll.count)}</span>`
|
||||
: ''}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
<div
|
||||
class="overview-item ${this.selectedCollection === '__overview__' ? 'selected' : ''}"
|
||||
@click=${() => this.selectOverview()}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="7" height="7"></rect>
|
||||
<rect x="14" y="3" width="7" height="7"></rect>
|
||||
<rect x="14" y="14" width="7" height="7"></rect>
|
||||
<rect x="3" y="14" width="7" height="7"></rect>
|
||||
</svg>
|
||||
Overview
|
||||
</div>
|
||||
${this.collections.length === 0
|
||||
? html`<div class="empty-state">No collections</div>`
|
||||
: this.collections.map(
|
||||
(coll) => html`
|
||||
<div
|
||||
class="collection-item ${this.selectedCollection === coll.name ? 'selected' : ''}"
|
||||
@click=${() => this.selectCollection(coll.name)}
|
||||
@contextmenu=${(e: MouseEvent) => this.handleCollectionContextMenu(e, coll)}
|
||||
>
|
||||
<span class="collection-name">
|
||||
<svg class="collection-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
|
||||
</svg>
|
||||
${coll.name}
|
||||
</span>
|
||||
${coll.count !== undefined
|
||||
? html`<span class="collection-count">${formatCount(coll.count)}</span>`
|
||||
: ''}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
291
ts_web/elements/tsview-mongo-db-overview.ts
Normal file
291
ts_web/elements/tsview-mongo-db-overview.ts
Normal file
@@ -0,0 +1,291 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import { apiService, type IDatabaseStats } from '../services/index.js';
|
||||
import { formatSize, formatCount } from '../utilities/index.js';
|
||||
import { themeStyles } from '../styles/index.js';
|
||||
|
||||
const { html, css, cssManager, customElement, property, state, DeesElement } = plugins;
|
||||
|
||||
@customElement('tsview-mongo-db-overview')
|
||||
export class TsviewMongoDbOverview extends DeesElement {
|
||||
@property({ type: String })
|
||||
public accessor databaseName: string = '';
|
||||
|
||||
@state()
|
||||
private accessor stats: IDatabaseStats | null = null;
|
||||
|
||||
@state()
|
||||
private accessor loading: boolean = false;
|
||||
|
||||
@state()
|
||||
private accessor error: string = '';
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
themeStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.overview-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 24px;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
margin: 0 0 8px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.header-title svg {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.stat-value.small {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.stat-description {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: #666;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.error-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #f87171;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-state svg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-bottom: 12px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
updated(changedProperties: Map<string, unknown>) {
|
||||
if (changedProperties.has('databaseName') && this.databaseName) {
|
||||
this.loadStats();
|
||||
}
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.databaseName) {
|
||||
await this.loadStats();
|
||||
}
|
||||
}
|
||||
|
||||
private async loadStats() {
|
||||
if (!this.databaseName) return;
|
||||
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
|
||||
try {
|
||||
this.stats = await apiService.getDatabaseStats(this.databaseName);
|
||||
} catch (err) {
|
||||
console.error('Error loading database stats:', err);
|
||||
this.error = 'Failed to load database statistics';
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.databaseName) {
|
||||
return html`
|
||||
<div class="overview-container">
|
||||
<div class="empty-state">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<ellipse cx="12" cy="5" rx="9" ry="3"></ellipse>
|
||||
<path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path>
|
||||
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path>
|
||||
</svg>
|
||||
<p>Select a database to view overview</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this.loading) {
|
||||
return html`
|
||||
<div class="overview-container">
|
||||
<div class="loading-state">Loading database statistics...</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this.error) {
|
||||
return html`
|
||||
<div class="overview-container">
|
||||
<div class="error-state">${this.error}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (!this.stats) {
|
||||
return html`
|
||||
<div class="overview-container">
|
||||
<div class="empty-state">
|
||||
<p>No statistics available</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="overview-container">
|
||||
<div class="header">
|
||||
<h1 class="header-title">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<ellipse cx="12" cy="5" rx="9" ry="3"></ellipse>
|
||||
<path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path>
|
||||
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path>
|
||||
</svg>
|
||||
${this.databaseName}
|
||||
</h1>
|
||||
<p class="header-subtitle">Database Overview</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">Storage</div>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<span class="stat-label">Collections</span>
|
||||
<span class="stat-value">${this.stats.collections}</span>
|
||||
<span class="stat-description">Total collections in database</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-label">Documents</span>
|
||||
<span class="stat-value">${formatCount(this.stats.objects) || this.stats.objects}</span>
|
||||
<span class="stat-description">Total documents stored</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-label">Avg Document Size</span>
|
||||
<span class="stat-value small">${formatSize(this.stats.avgObjSize)}</span>
|
||||
<span class="stat-description">Average size per document</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-label">Data Size</span>
|
||||
<span class="stat-value small">${formatSize(this.stats.dataSize)}</span>
|
||||
<span class="stat-description">Uncompressed data size</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-label">Storage Size</span>
|
||||
<span class="stat-value small">${formatSize(this.stats.storageSize)}</span>
|
||||
<span class="stat-description">Allocated storage</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">Indexes</div>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<span class="stat-label">Index Count</span>
|
||||
<span class="stat-value">${this.stats.indexes}</span>
|
||||
<span class="stat-description">Total indexes in database</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-label">Index Size</span>
|
||||
<span class="stat-value small">${formatSize(this.stats.indexSize)}</span>
|
||||
<span class="stat-description">Total index storage</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,10 @@ export class TsviewMongoDocument extends DeesElement {
|
||||
private accessor editing: boolean = false;
|
||||
|
||||
@state()
|
||||
private accessor editContent: string = '';
|
||||
private accessor originalContent: string = '';
|
||||
|
||||
@state()
|
||||
private accessor hasChanges: boolean = false;
|
||||
|
||||
@state()
|
||||
private accessor error: string = '';
|
||||
@@ -101,56 +104,13 @@ export class TsviewMongoDocument extends DeesElement {
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 12px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.json-view {
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.json-key {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.json-string {
|
||||
color: #a5d6a7;
|
||||
}
|
||||
|
||||
.json-number {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.json-boolean {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.json-null {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.edit-area {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid #444;
|
||||
border-radius: 6px;
|
||||
color: #fff;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
padding: 12px;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.edit-area:focus {
|
||||
outline: none;
|
||||
border-color: #404040;
|
||||
.content dees-input-code {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
@@ -190,10 +150,12 @@ export class TsviewMongoDocument extends DeesElement {
|
||||
updated(changedProperties: Map<string, unknown>) {
|
||||
if (changedProperties.has('documentId')) {
|
||||
this.editing = false;
|
||||
this.hasChanges = false;
|
||||
if (this.documentId) {
|
||||
this.loadDocument();
|
||||
} else {
|
||||
this.document = null;
|
||||
this.originalContent = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -210,6 +172,8 @@ export class TsviewMongoDocument extends DeesElement {
|
||||
this.collectionName,
|
||||
this.documentId
|
||||
);
|
||||
this.originalContent = JSON.stringify(this.document, null, 2);
|
||||
this.hasChanges = false;
|
||||
} catch (err) {
|
||||
console.error('Error loading document:', err);
|
||||
this.error = 'Failed to load document';
|
||||
@@ -219,18 +183,37 @@ export class TsviewMongoDocument extends DeesElement {
|
||||
}
|
||||
|
||||
private startEditing() {
|
||||
this.editContent = JSON.stringify(this.document, null, 2);
|
||||
this.editing = true;
|
||||
}
|
||||
|
||||
private cancelEditing() {
|
||||
this.editing = false;
|
||||
this.editContent = '';
|
||||
// Reset content to original
|
||||
const codeEditor = this.shadowRoot?.querySelector('dees-input-code') as any;
|
||||
if (codeEditor) {
|
||||
codeEditor.value = this.originalContent;
|
||||
}
|
||||
this.hasChanges = false;
|
||||
}
|
||||
|
||||
private handleContentChange(e: CustomEvent) {
|
||||
const newValue = e.detail as string;
|
||||
this.hasChanges = newValue !== this.originalContent;
|
||||
}
|
||||
|
||||
private handleDiscard() {
|
||||
const codeEditor = this.shadowRoot?.querySelector('dees-input-code') as any;
|
||||
if (codeEditor) {
|
||||
codeEditor.value = this.originalContent;
|
||||
}
|
||||
this.hasChanges = false;
|
||||
}
|
||||
|
||||
private async saveDocument() {
|
||||
try {
|
||||
const updatedDoc = JSON.parse(this.editContent);
|
||||
const codeEditor = this.shadowRoot?.querySelector('dees-input-code') as any;
|
||||
const content = codeEditor?.value || this.originalContent;
|
||||
const updatedDoc = JSON.parse(content);
|
||||
|
||||
// Remove _id from update (can't update _id)
|
||||
const { _id, ...updateFields } = updatedDoc;
|
||||
@@ -243,6 +226,7 @@ export class TsviewMongoDocument extends DeesElement {
|
||||
);
|
||||
|
||||
this.editing = false;
|
||||
this.hasChanges = false;
|
||||
await this.loadDocument();
|
||||
|
||||
this.dispatchEvent(
|
||||
@@ -283,20 +267,6 @@ export class TsviewMongoDocument extends DeesElement {
|
||||
}
|
||||
}
|
||||
|
||||
private formatJson(obj: unknown): string {
|
||||
return JSON.stringify(obj, null, 2);
|
||||
}
|
||||
|
||||
private syntaxHighlight(json: string): string {
|
||||
// Basic syntax highlighting
|
||||
return json
|
||||
.replace(/"([^"]+)":/g, '<span class="json-key">"$1"</span>:')
|
||||
.replace(/: "([^"]*)"/g, ': <span class="json-string">"$1"</span>')
|
||||
.replace(/: (\d+\.?\d*)/g, ': <span class="json-number">$1</span>')
|
||||
.replace(/: (true|false)/g, ': <span class="json-boolean">$1</span>')
|
||||
.replace(/: (null)/g, ': <span class="json-null">$1</span>');
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.documentId) {
|
||||
return html`
|
||||
@@ -334,10 +304,14 @@ export class TsviewMongoDocument extends DeesElement {
|
||||
<span class="header-title">Document</span>
|
||||
<div class="header-actions">
|
||||
${this.editing
|
||||
? html`
|
||||
<button class="action-btn" @click=${this.cancelEditing}>Cancel</button>
|
||||
<button class="action-btn primary" @click=${this.saveDocument}>Save</button>
|
||||
`
|
||||
? this.hasChanges
|
||||
? html`
|
||||
<button class="action-btn" @click=${this.handleDiscard}>Discard</button>
|
||||
<button class="action-btn primary" @click=${this.saveDocument}>Save</button>
|
||||
`
|
||||
: html`
|
||||
<button class="action-btn" @click=${this.cancelEditing}>Cancel</button>
|
||||
`
|
||||
: html`
|
||||
<button class="action-btn" @click=${this.startEditing}>Edit</button>
|
||||
<button class="action-btn danger" @click=${this.deleteDocument}>Delete</button>
|
||||
@@ -346,20 +320,12 @@ export class TsviewMongoDocument extends DeesElement {
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
${this.editing
|
||||
? html`
|
||||
<textarea
|
||||
class="edit-area"
|
||||
.value=${this.editContent}
|
||||
@input=${(e: Event) => (this.editContent = (e.target as HTMLTextAreaElement).value)}
|
||||
></textarea>
|
||||
`
|
||||
: html`
|
||||
<div
|
||||
class="json-view"
|
||||
.innerHTML=${this.syntaxHighlight(this.formatJson(this.document))}
|
||||
></div>
|
||||
`}
|
||||
<dees-input-code
|
||||
.value=${this.originalContent}
|
||||
.language=${'json'}
|
||||
.disabled=${!this.editing}
|
||||
@content-change=${(e: CustomEvent) => this.handleContentChange(e)}
|
||||
></dees-input-code>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -23,6 +23,12 @@ export class TsviewS3Browser extends DeesElement {
|
||||
@state()
|
||||
private accessor refreshKey: number = 0;
|
||||
|
||||
@state()
|
||||
private accessor previewWidth: number = 350;
|
||||
|
||||
@state()
|
||||
private accessor isResizingPreview: boolean = false;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
themeStyles,
|
||||
@@ -104,12 +110,24 @@ export class TsviewS3Browser extends DeesElement {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
gap: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content.has-preview {
|
||||
grid-template-columns: 1fr 350px;
|
||||
grid-template-columns: 1fr 4px var(--preview-width, 350px);
|
||||
}
|
||||
|
||||
.resize-divider {
|
||||
width: 4px;
|
||||
background: transparent;
|
||||
cursor: col-resize;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.resize-divider:hover,
|
||||
.resize-divider.active {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.main-view {
|
||||
@@ -122,6 +140,7 @@ export class TsviewS3Browser extends DeesElement {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
@@ -130,7 +149,8 @@ export class TsviewS3Browser extends DeesElement {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.preview-panel {
|
||||
.preview-panel,
|
||||
.resize-divider {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -168,6 +188,28 @@ export class TsviewS3Browser extends DeesElement {
|
||||
}
|
||||
}
|
||||
|
||||
private startPreviewResize = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
this.isResizingPreview = true;
|
||||
document.addEventListener('mousemove', this.handlePreviewResize);
|
||||
document.addEventListener('mouseup', this.endPreviewResize);
|
||||
};
|
||||
|
||||
private handlePreviewResize = (e: MouseEvent) => {
|
||||
if (!this.isResizingPreview) return;
|
||||
const contentEl = this.shadowRoot?.querySelector('.content');
|
||||
if (!contentEl) return;
|
||||
const containerRect = contentEl.getBoundingClientRect();
|
||||
const newWidth = Math.min(Math.max(containerRect.right - e.clientX, 250), 600);
|
||||
this.previewWidth = newWidth;
|
||||
};
|
||||
|
||||
private endPreviewResize = () => {
|
||||
this.isResizingPreview = false;
|
||||
document.removeEventListener('mousemove', this.handlePreviewResize);
|
||||
document.removeEventListener('mouseup', this.endPreviewResize);
|
||||
};
|
||||
|
||||
render() {
|
||||
const breadcrumbParts = this.currentPrefix
|
||||
? this.currentPrefix.split('/').filter(Boolean)
|
||||
@@ -213,7 +255,7 @@ export class TsviewS3Browser extends DeesElement {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content ${this.selectedKey ? 'has-preview' : ''}">
|
||||
<div class="content ${this.selectedKey ? 'has-preview' : ''}" style="--preview-width: ${this.previewWidth}px">
|
||||
<div class="main-view">
|
||||
${this.viewType === 'columns'
|
||||
? html`
|
||||
@@ -238,6 +280,10 @@ export class TsviewS3Browser extends DeesElement {
|
||||
|
||||
${this.selectedKey
|
||||
? html`
|
||||
<div
|
||||
class="resize-divider ${this.isResizingPreview ? 'active' : ''}"
|
||||
@mousedown=${this.startPreviewResize}
|
||||
></div>
|
||||
<div class="preview-panel">
|
||||
<tsview-s3-preview
|
||||
.bucketName=${this.bucketName}
|
||||
|
||||
@@ -35,6 +35,17 @@ export interface ICollectionStats {
|
||||
indexCount: number;
|
||||
}
|
||||
|
||||
export interface IDatabaseStats {
|
||||
collections: number;
|
||||
views: number;
|
||||
objects: number;
|
||||
avgObjSize: number;
|
||||
dataSize: number;
|
||||
storageSize: number;
|
||||
indexes: number;
|
||||
indexSize: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* API service for communicating with the tsview backend
|
||||
*/
|
||||
@@ -344,4 +355,12 @@ export class ApiService {
|
||||
}> {
|
||||
return this.request('getServerStatus', {});
|
||||
}
|
||||
|
||||
async getDatabaseStats(databaseName: string): Promise<IDatabaseStats | null> {
|
||||
const result = await this.request<
|
||||
{ databaseName: string },
|
||||
{ stats: IDatabaseStats | null }
|
||||
>('getDatabaseStats', { databaseName });
|
||||
return result.stats;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user