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
|
# 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)
|
## 2026-01-25 - 1.3.0 - feat(s3)
|
||||||
add S3 create file/folder dialogs and in-place text editor; export mongodb plugin
|
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/tsbuild": "^4.1.2",
|
||||||
"@git.zone/tsbundle": "^2.8.3",
|
"@git.zone/tsbundle": "^2.8.3",
|
||||||
"@git.zone/tsrun": "^2.0.1",
|
"@git.zone/tsrun": "^2.0.1",
|
||||||
"@git.zone/tstest": "^3.1.6",
|
"@git.zone/tstest": "^3.1.7",
|
||||||
"@git.zone/tswatch": "3.0.1",
|
"@git.zone/tswatch": "3.0.1",
|
||||||
"@types/node": "^25.0.10"
|
"@types/node": "^25.0.10"
|
||||||
},
|
},
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
"@api.global/typedrequest-interfaces": "^3.0.19",
|
"@api.global/typedrequest-interfaces": "^3.0.19",
|
||||||
"@api.global/typedserver": "^8.3.0",
|
"@api.global/typedserver": "^8.3.0",
|
||||||
"@aws-sdk/client-s3": "^3.975.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",
|
"@design.estate/dees-element": "^2.1.5",
|
||||||
"@push.rocks/early": "^4.0.4",
|
"@push.rocks/early": "^4.0.4",
|
||||||
"@push.rocks/npmextra": "^5.3.3",
|
"@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
|
specifier: ^3.975.0
|
||||||
version: 3.975.0
|
version: 3.975.0
|
||||||
'@design.estate/dees-catalog':
|
'@design.estate/dees-catalog':
|
||||||
specifier: ^3.37.0
|
specifier: ^3.37.1
|
||||||
version: 3.37.0(@tiptap/pm@2.27.2)
|
version: 3.37.1(@tiptap/pm@2.27.2)
|
||||||
'@design.estate/dees-element':
|
'@design.estate/dees-element':
|
||||||
specifier: ^2.1.5
|
specifier: ^2.1.5
|
||||||
version: 2.1.5
|
version: 2.1.5
|
||||||
@@ -76,8 +76,8 @@ importers:
|
|||||||
specifier: ^2.0.1
|
specifier: ^2.0.1
|
||||||
version: 2.0.1
|
version: 2.0.1
|
||||||
'@git.zone/tstest':
|
'@git.zone/tstest':
|
||||||
specifier: ^3.1.6
|
specifier: ^3.1.7
|
||||||
version: 3.1.6(@push.rocks/smartserve@2.0.1)(socks@2.8.7)(typescript@5.9.3)
|
version: 3.1.7(@push.rocks/smartserve@2.0.1)(socks@2.8.7)(typescript@5.9.3)
|
||||||
'@git.zone/tswatch':
|
'@git.zone/tswatch':
|
||||||
specifier: 3.0.1
|
specifier: 3.0.1
|
||||||
version: 3.0.1(@tiptap/pm@2.27.2)
|
version: 3.0.1(@tiptap/pm@2.27.2)
|
||||||
@@ -319,11 +319,14 @@ packages:
|
|||||||
'@cloudflare/workers-types@4.20260123.0':
|
'@cloudflare/workers-types@4.20260123.0':
|
||||||
resolution: {integrity: sha512-pQccZ8IDLFKkvdKBXZRPkbMtWtS7vVz1giJGkAAZ5cZH2RHK5Bs6p1OoVZA8Z2Sry8Q0tZbZ5Yjud4R7SrG3KQ==}
|
resolution: {integrity: sha512-pQccZ8IDLFKkvdKBXZRPkbMtWtS7vVz1giJGkAAZ5cZH2RHK5Bs6p1OoVZA8Z2Sry8Q0tZbZ5Yjud4R7SrG3KQ==}
|
||||||
|
|
||||||
|
'@cloudflare/workers-types@4.20260124.0':
|
||||||
|
resolution: {integrity: sha512-h6TJlew6AtGuEXFc+k5ifalk+tg3fkg0lla6XbMAb2AKKfJGwlFNTwW2xyT/Ha92KY631CIJ+Ace08DPdFohdA==}
|
||||||
|
|
||||||
'@configvault.io/interfaces@1.0.17':
|
'@configvault.io/interfaces@1.0.17':
|
||||||
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
|
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
|
||||||
|
|
||||||
'@design.estate/dees-catalog@3.37.0':
|
'@design.estate/dees-catalog@3.37.1':
|
||||||
resolution: {integrity: sha512-c6q+yK2FwMsMK72GykUhZnvKUgTzjFO9vdbn6OBxas2/eY/6Wi6BC5i9YONN0UYcW8yqjHIDjN9nP7yE1Ai4PA==}
|
resolution: {integrity: sha512-NCgzzCG3NJVF7C7aa1nExCMhB+7nA6glFgZpsff32CpvdtbAuBQiuOngU0suVw65uK7Y0a2r/y2CEPGNNmj3TA==}
|
||||||
|
|
||||||
'@design.estate/dees-comms@1.0.30':
|
'@design.estate/dees-comms@1.0.30':
|
||||||
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
|
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
|
||||||
@@ -538,8 +541,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-NEcnsjvlC1o3Z6SS3VhKCf6Ev+Sh4EAinmggslrIR/ppMrvjDbXNFXoyr3PB+GLeSAR0JRZ1fGvVYjpEzjBdIg==}
|
resolution: {integrity: sha512-NEcnsjvlC1o3Z6SS3VhKCf6Ev+Sh4EAinmggslrIR/ppMrvjDbXNFXoyr3PB+GLeSAR0JRZ1fGvVYjpEzjBdIg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@git.zone/tstest@3.1.6':
|
'@git.zone/tstest@3.1.7':
|
||||||
resolution: {integrity: sha512-xRGc6wO4rJ6mohPCMIBDRH+oNjiIvX6Jeo8v/Y5o5VyKSHFmqol7FCKSBrojMcqgBpESnLHFPJAAOmT9W3JV8Q==}
|
resolution: {integrity: sha512-YCDA+65LJhoY3WJxrNduKlpGf37aq4bFe+fdRqE0dZ2W1f7j3sUunBaBzckShSHKRjkMdPZKr0W0sXFXUK/PcA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@git.zone/tswatch@3.0.1':
|
'@git.zone/tswatch@3.0.1':
|
||||||
@@ -2805,8 +2808,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
|
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
lucide@0.562.0:
|
lucide@0.563.0:
|
||||||
resolution: {integrity: sha512-k1Fb8ZMnRQovWRlea7Jr0b9UKA29IM7/cu79+mJrhVohvA2YC/Ti3Sk+G+h/SIu3IlrKT4RAbWMHUBBQd1O6XA==}
|
resolution: {integrity: sha512-2zBzDJ5n2Plj3d0ksj6h9TWPOSiKu9gtxJxnBAye11X/8gfWied6IYJn6ADYBp1NPoJmgpyOYP3wMrVx69+2AA==}
|
||||||
|
|
||||||
make-dir@3.1.0:
|
make-dir@3.1.0:
|
||||||
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
|
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
|
||||||
@@ -4008,7 +4011,7 @@ snapshots:
|
|||||||
'@api.global/typedrequest': 3.2.5
|
'@api.global/typedrequest': 3.2.5
|
||||||
'@api.global/typedrequest-interfaces': 3.0.19
|
'@api.global/typedrequest-interfaces': 3.0.19
|
||||||
'@api.global/typedsocket': 3.1.1(@push.rocks/smartserve@2.0.1)
|
'@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
|
'@design.estate/dees-comms': 1.0.30
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
'@push.rocks/smartchok': 1.2.0
|
'@push.rocks/smartchok': 1.2.0
|
||||||
@@ -4057,7 +4060,7 @@ snapshots:
|
|||||||
'@api.global/typedrequest-interfaces': 3.0.19
|
'@api.global/typedrequest-interfaces': 3.0.19
|
||||||
'@api.global/typedsocket': 4.1.0(@push.rocks/smartserve@2.0.1)
|
'@api.global/typedsocket': 4.1.0(@push.rocks/smartserve@2.0.1)
|
||||||
'@cloudflare/workers-types': 4.20260123.0
|
'@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
|
'@design.estate/dees-comms': 1.0.30
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
@@ -4679,11 +4682,13 @@ snapshots:
|
|||||||
|
|
||||||
'@cloudflare/workers-types@4.20260123.0': {}
|
'@cloudflare/workers-types@4.20260123.0': {}
|
||||||
|
|
||||||
|
'@cloudflare/workers-types@4.20260124.0': {}
|
||||||
|
|
||||||
'@configvault.io/interfaces@1.0.17':
|
'@configvault.io/interfaces@1.0.17':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@api.global/typedrequest-interfaces': 3.0.19
|
'@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:
|
dependencies:
|
||||||
'@design.estate/dees-domtools': 2.3.7
|
'@design.estate/dees-domtools': 2.3.7
|
||||||
'@design.estate/dees-element': 2.1.5
|
'@design.estate/dees-element': 2.1.5
|
||||||
@@ -4706,7 +4711,7 @@ snapshots:
|
|||||||
apexcharts: 5.3.6
|
apexcharts: 5.3.6
|
||||||
highlight.js: 11.11.1
|
highlight.js: 11.11.1
|
||||||
ibantools: 4.5.1
|
ibantools: 4.5.1
|
||||||
lucide: 0.562.0
|
lucide: 0.563.0
|
||||||
monaco-editor: 0.55.1
|
monaco-editor: 0.55.1
|
||||||
pdfjs-dist: 4.10.38
|
pdfjs-dist: 4.10.38
|
||||||
xterm: 5.3.0
|
xterm: 5.3.0
|
||||||
@@ -4962,7 +4967,7 @@ snapshots:
|
|||||||
'@push.rocks/smartshell': 3.3.0
|
'@push.rocks/smartshell': 3.3.0
|
||||||
tsx: 4.21.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:
|
dependencies:
|
||||||
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1)
|
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1)
|
||||||
'@git.zone/tsbundle': 2.8.3
|
'@git.zone/tsbundle': 2.8.3
|
||||||
@@ -8106,7 +8111,7 @@ snapshots:
|
|||||||
|
|
||||||
lru-cache@7.18.3: {}
|
lru-cache@7.18.3: {}
|
||||||
|
|
||||||
lucide@0.562.0: {}
|
lucide@0.563.0: {}
|
||||||
|
|
||||||
make-dir@3.1.0:
|
make-dir@3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@git.zone/tsview',
|
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'
|
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
|
// List collections
|
||||||
typedrouter.addTypedHandler(
|
typedrouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.IReq_ListCollections>(
|
new plugins.typedrequest.TypedHandler<interfaces.IReq_ListCollections>(
|
||||||
|
|||||||
@@ -292,6 +292,7 @@ export async function registerS3Handlers(
|
|||||||
await bucket.fastPut({
|
await bucket.fastPut({
|
||||||
path: reqData.key,
|
path: reqData.key,
|
||||||
contents: content,
|
contents: content,
|
||||||
|
overwrite: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
@@ -354,6 +355,7 @@ export async function registerS3Handlers(
|
|||||||
await destBucket.fastPut({
|
await destBucket.fastPut({
|
||||||
path: reqData.destKey,
|
path: reqData.destKey,
|
||||||
contents: content,
|
contents: content,
|
||||||
|
overwrite: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { success: 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 = {
|
export const commitinfo = {
|
||||||
name: '@git.zone/tsview',
|
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'
|
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-documents.js';
|
||||||
export * from './tsview-mongo-document.js';
|
export * from './tsview-mongo-document.js';
|
||||||
export * from './tsview-mongo-indexes.js';
|
export * from './tsview-mongo-indexes.js';
|
||||||
|
export * from './tsview-mongo-db-overview.js';
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ export class TsviewApp extends DeesElement {
|
|||||||
@state()
|
@state()
|
||||||
private accessor newDatabaseName: string = '';
|
private accessor newDatabaseName: string = '';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor showSystemDatabases: boolean = false;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private accessor showS3CreateDialog: boolean = false;
|
private accessor showS3CreateDialog: boolean = false;
|
||||||
|
|
||||||
@@ -57,6 +60,12 @@ export class TsviewApp extends DeesElement {
|
|||||||
@state()
|
@state()
|
||||||
private accessor s3CreateDialogName: string = '';
|
private accessor s3CreateDialogName: string = '';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor sidebarWidth: number = 240;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor isResizingSidebar: boolean = false;
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
themeStyles,
|
themeStyles,
|
||||||
@@ -130,10 +139,22 @@ export class TsviewApp extends DeesElement {
|
|||||||
|
|
||||||
.app-main {
|
.app-main {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 240px 1fr;
|
grid-template-columns: var(--sidebar-width, 240px) 4px 1fr;
|
||||||
overflow: hidden;
|
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 {
|
.sidebar {
|
||||||
background: #1e1e1e;
|
background: #1e1e1e;
|
||||||
border-right: 1px solid #333;
|
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() {
|
async connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
await this.loadData();
|
await this.loadData();
|
||||||
@@ -423,9 +453,16 @@ export class TsviewApp extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private selectDatabase(db: string) {
|
private selectDatabase(db: string) {
|
||||||
|
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.selectedDatabase = db;
|
||||||
this.selectedCollection = '';
|
this.selectedCollection = '';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private selectCollection(collection: string) {
|
private selectCollection(collection: string) {
|
||||||
this.selectedCollection = collection;
|
this.selectedCollection = collection;
|
||||||
@@ -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() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
@@ -669,8 +738,12 @@ export class TsviewApp extends DeesElement {
|
|||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="app-main">
|
<main class="app-main" style="--sidebar-width: ${this.sidebarWidth}px">
|
||||||
${this.renderSidebar()}
|
${this.renderSidebar()}
|
||||||
|
<div
|
||||||
|
class="resize-divider ${this.isResizingSidebar ? 'active' : ''}"
|
||||||
|
@mousedown=${this.startSidebarResize}
|
||||||
|
></div>
|
||||||
${this.renderContent()}
|
${this.renderContent()}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
@@ -849,7 +922,7 @@ export class TsviewApp extends DeesElement {
|
|||||||
|
|
||||||
if (this.viewMode === 'mongo') {
|
if (this.viewMode === 'mongo') {
|
||||||
return html`
|
return html`
|
||||||
<aside class="sidebar">
|
<aside class="sidebar" @contextmenu=${(e: MouseEvent) => this.handleSidebarContextMenu(e)}>
|
||||||
<div class="sidebar-header">Databases & Collections</div>
|
<div class="sidebar-header">Databases & Collections</div>
|
||||||
<button class="create-btn" @click=${() => this.showCreateDatabaseDialog = true}>
|
<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">
|
<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
|
New Database
|
||||||
</button>
|
</button>
|
||||||
<div class="sidebar-list">
|
<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>`
|
? 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>
|
</div>
|
||||||
</aside>
|
</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`
|
return html`
|
||||||
<div class="content-area">
|
<div class="content-area">
|
||||||
<tsview-mongo-browser
|
<tsview-mongo-browser
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ export class TsviewMongoBrowser extends DeesElement {
|
|||||||
@state()
|
@state()
|
||||||
private accessor stats: ICollectionStats | null = null;
|
private accessor stats: ICollectionStats | null = null;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor editorWidth: number = 400;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor isResizingEditor: boolean = false;
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
themeStyles,
|
themeStyles,
|
||||||
@@ -102,11 +108,23 @@ export class TsviewMongoBrowser extends DeesElement {
|
|||||||
.content {
|
.content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 400px;
|
grid-template-columns: 1fr 4px var(--editor-width, 400px);
|
||||||
gap: 16px;
|
gap: 0;
|
||||||
overflow: hidden;
|
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 {
|
.main-panel {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
background: rgba(0, 0, 0, 0.2);
|
background: rgba(0, 0, 0, 0.2);
|
||||||
@@ -117,6 +135,7 @@ export class TsviewMongoBrowser extends DeesElement {
|
|||||||
background: rgba(0, 0, 0, 0.2);
|
background: rgba(0, 0, 0, 0.2);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
margin-left: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
@@ -124,7 +143,8 @@ export class TsviewMongoBrowser extends DeesElement {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-panel {
|
.detail-panel,
|
||||||
|
.resize-divider {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,6 +182,28 @@ export class TsviewMongoBrowser extends DeesElement {
|
|||||||
this.selectedDocumentId = e.detail.documentId;
|
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() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<div class="browser-container">
|
<div class="browser-container">
|
||||||
@@ -201,7 +243,7 @@ export class TsviewMongoBrowser extends DeesElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content" style="--editor-width: ${this.editorWidth}px">
|
||||||
<div class="main-panel">
|
<div class="main-panel">
|
||||||
${this.activeTab === 'documents'
|
${this.activeTab === 'documents'
|
||||||
? html`
|
? html`
|
||||||
@@ -225,6 +267,11 @@ export class TsviewMongoBrowser extends DeesElement {
|
|||||||
`}
|
`}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="resize-divider ${this.isResizingEditor ? 'active' : ''}"
|
||||||
|
@mousedown=${this.startEditorResize}
|
||||||
|
></div>
|
||||||
|
|
||||||
<div class="detail-panel">
|
<div class="detail-panel">
|
||||||
<tsview-mongo-document
|
<tsview-mongo-document
|
||||||
.databaseName=${this.databaseName}
|
.databaseName=${this.databaseName}
|
||||||
|
|||||||
@@ -90,6 +90,33 @@ export class TsviewMongoCollections extends DeesElement {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-style: italic;
|
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,18 +195,38 @@ export class TsviewMongoCollections extends DeesElement {
|
|||||||
await this.loadCollections();
|
await this.loadCollections();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private selectOverview() {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('collection-selected', {
|
||||||
|
detail: '__overview__',
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.loading) {
|
if (this.loading) {
|
||||||
return html`<div class="loading-state">Loading collections...</div>`;
|
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`
|
return html`
|
||||||
<div class="collections-list">
|
<div class="collections-list">
|
||||||
${this.collections.map(
|
<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`
|
(coll) => html`
|
||||||
<div
|
<div
|
||||||
class="collection-item ${this.selectedCollection === coll.name ? 'selected' : ''}"
|
class="collection-item ${this.selectedCollection === coll.name ? 'selected' : ''}"
|
||||||
|
|||||||
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;
|
private accessor editing: boolean = false;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private accessor editContent: string = '';
|
private accessor originalContent: string = '';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor hasChanges: boolean = false;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private accessor error: string = '';
|
private accessor error: string = '';
|
||||||
@@ -101,56 +104,13 @@ export class TsviewMongoDocument extends DeesElement {
|
|||||||
|
|
||||||
.content {
|
.content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
padding: 12px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.json-view {
|
.content dees-input-code {
|
||||||
font-family: 'Monaco', 'Menlo', monospace;
|
flex: 1;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
@@ -190,10 +150,12 @@ export class TsviewMongoDocument extends DeesElement {
|
|||||||
updated(changedProperties: Map<string, unknown>) {
|
updated(changedProperties: Map<string, unknown>) {
|
||||||
if (changedProperties.has('documentId')) {
|
if (changedProperties.has('documentId')) {
|
||||||
this.editing = false;
|
this.editing = false;
|
||||||
|
this.hasChanges = false;
|
||||||
if (this.documentId) {
|
if (this.documentId) {
|
||||||
this.loadDocument();
|
this.loadDocument();
|
||||||
} else {
|
} else {
|
||||||
this.document = null;
|
this.document = null;
|
||||||
|
this.originalContent = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,6 +172,8 @@ export class TsviewMongoDocument extends DeesElement {
|
|||||||
this.collectionName,
|
this.collectionName,
|
||||||
this.documentId
|
this.documentId
|
||||||
);
|
);
|
||||||
|
this.originalContent = JSON.stringify(this.document, null, 2);
|
||||||
|
this.hasChanges = false;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading document:', err);
|
console.error('Error loading document:', err);
|
||||||
this.error = 'Failed to load document';
|
this.error = 'Failed to load document';
|
||||||
@@ -219,18 +183,37 @@ export class TsviewMongoDocument extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private startEditing() {
|
private startEditing() {
|
||||||
this.editContent = JSON.stringify(this.document, null, 2);
|
|
||||||
this.editing = true;
|
this.editing = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private cancelEditing() {
|
private cancelEditing() {
|
||||||
this.editing = false;
|
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() {
|
private async saveDocument() {
|
||||||
try {
|
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)
|
// Remove _id from update (can't update _id)
|
||||||
const { _id, ...updateFields } = updatedDoc;
|
const { _id, ...updateFields } = updatedDoc;
|
||||||
@@ -243,6 +226,7 @@ export class TsviewMongoDocument extends DeesElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.editing = false;
|
this.editing = false;
|
||||||
|
this.hasChanges = false;
|
||||||
await this.loadDocument();
|
await this.loadDocument();
|
||||||
|
|
||||||
this.dispatchEvent(
|
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() {
|
render() {
|
||||||
if (!this.documentId) {
|
if (!this.documentId) {
|
||||||
return html`
|
return html`
|
||||||
@@ -334,10 +304,14 @@ export class TsviewMongoDocument extends DeesElement {
|
|||||||
<span class="header-title">Document</span>
|
<span class="header-title">Document</span>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
${this.editing
|
${this.editing
|
||||||
|
? this.hasChanges
|
||||||
? html`
|
? html`
|
||||||
<button class="action-btn" @click=${this.cancelEditing}>Cancel</button>
|
<button class="action-btn" @click=${this.handleDiscard}>Discard</button>
|
||||||
<button class="action-btn primary" @click=${this.saveDocument}>Save</button>
|
<button class="action-btn primary" @click=${this.saveDocument}>Save</button>
|
||||||
`
|
`
|
||||||
|
: html`
|
||||||
|
<button class="action-btn" @click=${this.cancelEditing}>Cancel</button>
|
||||||
|
`
|
||||||
: html`
|
: html`
|
||||||
<button class="action-btn" @click=${this.startEditing}>Edit</button>
|
<button class="action-btn" @click=${this.startEditing}>Edit</button>
|
||||||
<button class="action-btn danger" @click=${this.deleteDocument}>Delete</button>
|
<button class="action-btn danger" @click=${this.deleteDocument}>Delete</button>
|
||||||
@@ -346,20 +320,12 @@ export class TsviewMongoDocument extends DeesElement {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
${this.editing
|
<dees-input-code
|
||||||
? html`
|
.value=${this.originalContent}
|
||||||
<textarea
|
.language=${'json'}
|
||||||
class="edit-area"
|
.disabled=${!this.editing}
|
||||||
.value=${this.editContent}
|
@content-change=${(e: CustomEvent) => this.handleContentChange(e)}
|
||||||
@input=${(e: Event) => (this.editContent = (e.target as HTMLTextAreaElement).value)}
|
></dees-input-code>
|
||||||
></textarea>
|
|
||||||
`
|
|
||||||
: html`
|
|
||||||
<div
|
|
||||||
class="json-view"
|
|
||||||
.innerHTML=${this.syntaxHighlight(this.formatJson(this.document))}
|
|
||||||
></div>
|
|
||||||
`}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ export class TsviewS3Browser extends DeesElement {
|
|||||||
@state()
|
@state()
|
||||||
private accessor refreshKey: number = 0;
|
private accessor refreshKey: number = 0;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor previewWidth: number = 350;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private accessor isResizingPreview: boolean = false;
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
themeStyles,
|
themeStyles,
|
||||||
@@ -104,12 +110,24 @@ export class TsviewS3Browser extends DeesElement {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 16px;
|
gap: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content.has-preview {
|
.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 {
|
.main-view {
|
||||||
@@ -122,6 +140,7 @@ export class TsviewS3Browser extends DeesElement {
|
|||||||
background: rgba(0, 0, 0, 0.2);
|
background: rgba(0, 0, 0, 0.2);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
margin-left: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
@@ -130,7 +149,8 @@ export class TsviewS3Browser extends DeesElement {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-panel {
|
.preview-panel,
|
||||||
|
.resize-divider {
|
||||||
display: none;
|
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() {
|
render() {
|
||||||
const breadcrumbParts = this.currentPrefix
|
const breadcrumbParts = this.currentPrefix
|
||||||
? this.currentPrefix.split('/').filter(Boolean)
|
? this.currentPrefix.split('/').filter(Boolean)
|
||||||
@@ -213,7 +255,7 @@ export class TsviewS3Browser extends DeesElement {
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="main-view">
|
||||||
${this.viewType === 'columns'
|
${this.viewType === 'columns'
|
||||||
? html`
|
? html`
|
||||||
@@ -238,6 +280,10 @@ export class TsviewS3Browser extends DeesElement {
|
|||||||
|
|
||||||
${this.selectedKey
|
${this.selectedKey
|
||||||
? html`
|
? html`
|
||||||
|
<div
|
||||||
|
class="resize-divider ${this.isResizingPreview ? 'active' : ''}"
|
||||||
|
@mousedown=${this.startPreviewResize}
|
||||||
|
></div>
|
||||||
<div class="preview-panel">
|
<div class="preview-panel">
|
||||||
<tsview-s3-preview
|
<tsview-s3-preview
|
||||||
.bucketName=${this.bucketName}
|
.bucketName=${this.bucketName}
|
||||||
|
|||||||
@@ -35,6 +35,17 @@ export interface ICollectionStats {
|
|||||||
indexCount: number;
|
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
|
* API service for communicating with the tsview backend
|
||||||
*/
|
*/
|
||||||
@@ -344,4 +355,12 @@ export class ApiService {
|
|||||||
}> {
|
}> {
|
||||||
return this.request('getServerStatus', {});
|
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