mongoku 2.0.2 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +27 -0
- package/build/client/_app/immutable/chunks/{MHUppGzk.js → BAM9w9EL.js} +1 -1
- package/build/client/_app/immutable/chunks/BAM9w9EL.js.br +0 -0
- package/build/client/_app/immutable/chunks/BAM9w9EL.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{CN-ecO3-.js → BMa204Dm.js} +1 -1
- package/build/client/_app/immutable/chunks/BMa204Dm.js.br +0 -0
- package/build/client/_app/immutable/chunks/BMa204Dm.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{_2kcttvK.js → BdR-m9Ad.js} +1 -1
- package/build/client/_app/immutable/chunks/BdR-m9Ad.js.br +0 -0
- package/build/client/_app/immutable/chunks/BdR-m9Ad.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BzAcxkRZ.js +4 -0
- package/build/client/_app/immutable/chunks/BzAcxkRZ.js.br +0 -0
- package/build/client/_app/immutable/chunks/BzAcxkRZ.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{BFhvhM4X.js → CyQLXPZI.js} +1 -1
- package/build/client/_app/immutable/chunks/CyQLXPZI.js.br +0 -0
- package/build/client/_app/immutable/chunks/CyQLXPZI.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{BdWVCPGW.js → D4VhtiDg.js} +1 -1
- package/build/client/_app/immutable/chunks/D4VhtiDg.js.br +0 -0
- package/build/client/_app/immutable/chunks/D4VhtiDg.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{zXvB9_Mi.js → XYFbSe2V.js} +1 -1
- package/build/client/_app/immutable/chunks/XYFbSe2V.js.br +0 -0
- package/build/client/_app/immutable/chunks/XYFbSe2V.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{CGIdus8b.js → uMNMODvc.js} +1 -1
- package/build/client/_app/immutable/chunks/uMNMODvc.js.br +0 -0
- package/build/client/_app/immutable/chunks/uMNMODvc.js.gz +0 -0
- package/build/client/_app/immutable/entry/{app.hGE78f-O.js → app.9nC_873E.js} +2 -2
- package/build/client/_app/immutable/entry/app.9nC_873E.js.br +0 -0
- package/build/client/_app/immutable/entry/app.9nC_873E.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.Bn88Alw2.js +1 -0
- package/build/client/_app/immutable/entry/start.Bn88Alw2.js.br +2 -0
- package/build/client/_app/immutable/entry/start.Bn88Alw2.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{0.DyBXVtnT.js → 0.COxTCtn2.js} +1 -1
- package/build/client/_app/immutable/nodes/0.COxTCtn2.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.COxTCtn2.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{1.FqB0jq88.js → 1.Bc8yPK_D.js} +1 -1
- package/build/client/_app/immutable/nodes/1.Bc8yPK_D.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.Bc8yPK_D.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{3.Bekn_8hM.js → 3.CI2GcqTf.js} +1 -1
- package/build/client/_app/immutable/nodes/3.CI2GcqTf.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.CI2GcqTf.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{4.DQfaAvJi.js → 4.ChSdW7ac.js} +1 -1
- package/build/client/_app/immutable/nodes/4.ChSdW7ac.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.ChSdW7ac.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{5.B1E6iW2R.js → 5.DaMML2go.js} +1 -1
- package/build/client/_app/immutable/nodes/5.DaMML2go.js.br +0 -0
- package/build/client/_app/immutable/nodes/5.DaMML2go.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{6.28eZQkvz.js → 6.Dcq0qwvO.js} +1 -1
- package/build/client/_app/immutable/nodes/6.Dcq0qwvO.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.Dcq0qwvO.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{7.qpcLWZb7.js → 7.CU-ncPes.js} +1 -1
- package/build/client/_app/immutable/nodes/7.CU-ncPes.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.CU-ncPes.js.gz +0 -0
- package/build/client/_app/version.json +1 -1
- package/build/client/_app/version.json.br +0 -0
- package/build/client/_app/version.json.gz +0 -0
- package/build/server/chunks/{0-m42kIUxj.js → 0-C1NyHW8A.js} +2 -2
- package/build/server/chunks/{0-m42kIUxj.js.map → 0-C1NyHW8A.js.map} +1 -1
- package/build/server/chunks/{1-uc74UVG3.js → 1-CThf4W5r.js} +2 -2
- package/build/server/chunks/{1-uc74UVG3.js.map → 1-CThf4W5r.js.map} +1 -1
- package/build/server/chunks/{3-Bi8teWON.js → 3-CJf0NbiV.js} +2 -2
- package/build/server/chunks/{3-Bi8teWON.js.map → 3-CJf0NbiV.js.map} +1 -1
- package/build/server/chunks/{4-u1WGAtFU.js → 4-Dfbpsagm.js} +2 -2
- package/build/server/chunks/{4-u1WGAtFU.js.map → 4-Dfbpsagm.js.map} +1 -1
- package/build/server/chunks/{5-BlGdcdjs.js → 5-DLB6GOjf.js} +2 -2
- package/build/server/chunks/{5-BlGdcdjs.js.map → 5-DLB6GOjf.js.map} +1 -1
- package/build/server/chunks/{6-YCp6xyCU.js → 6-DfCARDKO.js} +2 -2
- package/build/server/chunks/{6-YCp6xyCU.js.map → 6-DfCARDKO.js.map} +1 -1
- package/build/server/chunks/{7-ieA4k9K_.js → 7-B5o4OymX.js} +2 -2
- package/build/server/chunks/{7-ieA4k9K_.js.map → 7-B5o4OymX.js.map} +1 -1
- package/build/server/index.js +1 -1
- package/build/server/index.js.map +1 -1
- package/build/server/manifest.js +8 -8
- package/build/server/manifest.js.map +1 -1
- package/cli.ts +148 -0
- package/dist/cli.js +2 -3
- package/ecosystem.config.js +9 -0
- package/package.json +10 -2
- package/src/api/servers.remote.ts +98 -0
- package/src/app.css +228 -0
- package/src/app.d.ts +16 -0
- package/src/app.html +11 -0
- package/src/hooks.server.ts +34 -0
- package/src/lib/components/Breadcrumbs.svelte +133 -0
- package/src/lib/components/JsonValue.svelte +248 -0
- package/src/lib/components/Notifications.svelte +81 -0
- package/src/lib/components/Panel.svelte +37 -0
- package/src/lib/components/PrettyJson.svelte +187 -0
- package/src/lib/components/SearchBox.svelte +160 -0
- package/src/lib/components/TooltipTable.svelte +137 -0
- package/src/lib/server/HostsManager.ts +105 -0
- package/src/lib/server/JsonEncoder.ts +62 -0
- package/src/lib/server/mongo.ts +199 -0
- package/src/lib/stores/notifications.svelte.ts +45 -0
- package/src/lib/types.ts +56 -0
- package/src/lib/utils/filters.ts +25 -0
- package/src/lib/utils/jsonParser.ts +125 -0
- package/src/routes/+layout.server.ts +7 -0
- package/src/routes/+layout.svelte +27 -0
- package/src/routes/+page.server.ts +6 -0
- package/src/routes/servers/+page.server.ts +53 -0
- package/src/routes/servers/+page.svelte +196 -0
- package/src/routes/servers/[server]/databases/+page.server.ts +47 -0
- package/src/routes/servers/[server]/databases/+page.svelte +88 -0
- package/src/routes/servers/[server]/databases/[database]/collections/+page.server.ts +21 -0
- package/src/routes/servers/[server]/databases/[database]/collections/+page.svelte +110 -0
- package/src/routes/servers/[server]/databases/[database]/collections/[collection]/+page.server.ts +106 -0
- package/src/routes/servers/[server]/databases/[database]/collections/[collection]/+page.svelte +174 -0
- package/src/routes/servers/[server]/databases/[database]/collections/[collection]/documents/[document]/+page.server.ts +25 -0
- package/src/routes/servers/[server]/databases/[database]/collections/[collection]/documents/[document]/+page.svelte +90 -0
- package/src/tests/api/readonly.test.ts +89 -0
- package/src/tests/setup.ts +19 -0
- package/svelte.config.js +28 -0
- package/tsconfig.cli.json +15 -0
- package/tsconfig.json +19 -0
- package/vite.config.ts +7 -0
- package/build/client/_app/immutable/chunks/BFhvhM4X.js.br +0 -0
- package/build/client/_app/immutable/chunks/BFhvhM4X.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BdWVCPGW.js.br +0 -0
- package/build/client/_app/immutable/chunks/BdWVCPGW.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CGIdus8b.js.br +0 -0
- package/build/client/_app/immutable/chunks/CGIdus8b.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CN-ecO3-.js.br +0 -0
- package/build/client/_app/immutable/chunks/CN-ecO3-.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DB3PPjLu.js +0 -4
- package/build/client/_app/immutable/chunks/DB3PPjLu.js.br +0 -0
- package/build/client/_app/immutable/chunks/DB3PPjLu.js.gz +0 -0
- package/build/client/_app/immutable/chunks/MHUppGzk.js.br +0 -0
- package/build/client/_app/immutable/chunks/MHUppGzk.js.gz +0 -0
- package/build/client/_app/immutable/chunks/_2kcttvK.js.br +0 -0
- package/build/client/_app/immutable/chunks/_2kcttvK.js.gz +0 -0
- package/build/client/_app/immutable/chunks/zXvB9_Mi.js.br +0 -0
- package/build/client/_app/immutable/chunks/zXvB9_Mi.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.hGE78f-O.js.br +0 -0
- package/build/client/_app/immutable/entry/app.hGE78f-O.js.gz +0 -0
- package/build/client/_app/immutable/entry/start._GE1Zd3d.js +0 -1
- package/build/client/_app/immutable/entry/start._GE1Zd3d.js.br +0 -2
- package/build/client/_app/immutable/entry/start._GE1Zd3d.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.DyBXVtnT.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.DyBXVtnT.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.FqB0jq88.js.br +0 -2
- package/build/client/_app/immutable/nodes/1.FqB0jq88.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.Bekn_8hM.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.Bekn_8hM.js.gz +0 -0
- package/build/client/_app/immutable/nodes/4.DQfaAvJi.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.DQfaAvJi.js.gz +0 -0
- package/build/client/_app/immutable/nodes/5.B1E6iW2R.js.br +0 -0
- package/build/client/_app/immutable/nodes/5.B1E6iW2R.js.gz +0 -0
- package/build/client/_app/immutable/nodes/6.28eZQkvz.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.28eZQkvz.js.gz +0 -0
- package/build/client/_app/immutable/nodes/7.qpcLWZb7.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.qpcLWZb7.js.gz +0 -0
package/src/lib/types.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export interface MongoDocument {
|
|
2
|
+
_id?: {
|
|
3
|
+
$type: string;
|
|
4
|
+
$value: string;
|
|
5
|
+
};
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SearchParams {
|
|
10
|
+
query: string;
|
|
11
|
+
sort: string;
|
|
12
|
+
project: string;
|
|
13
|
+
limit: number;
|
|
14
|
+
skip: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Database stats as returned by the db.stats() method
|
|
19
|
+
*
|
|
20
|
+
* Type not exposed by MongoDB
|
|
21
|
+
*/
|
|
22
|
+
export interface DatabaseStats {
|
|
23
|
+
db: string;
|
|
24
|
+
collections: number;
|
|
25
|
+
views: number;
|
|
26
|
+
objects: number;
|
|
27
|
+
avgObjSize: number;
|
|
28
|
+
dataSize: number;
|
|
29
|
+
storageSize: number;
|
|
30
|
+
indexes: number;
|
|
31
|
+
indexSize: number;
|
|
32
|
+
totalSize: number;
|
|
33
|
+
fsUsedSize: number;
|
|
34
|
+
fsTotalSize: number;
|
|
35
|
+
ok: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface CollectionJSON {
|
|
39
|
+
name: string;
|
|
40
|
+
size: number;
|
|
41
|
+
dataSize: number;
|
|
42
|
+
count: number;
|
|
43
|
+
avgObjSize: number;
|
|
44
|
+
storageSize: number;
|
|
45
|
+
capped: boolean;
|
|
46
|
+
nIndexes: number;
|
|
47
|
+
totalIndexSize: number;
|
|
48
|
+
indexSizes: {
|
|
49
|
+
[name: string]: number;
|
|
50
|
+
};
|
|
51
|
+
indexes: Array<{
|
|
52
|
+
name: string;
|
|
53
|
+
key?: Record<string, number>;
|
|
54
|
+
size: number;
|
|
55
|
+
}>;
|
|
56
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function formatBytes(bytes: number): string {
|
|
2
|
+
if (bytes === 0) return "0 B";
|
|
3
|
+
if (bytes < 1000) return bytes + " B";
|
|
4
|
+
if (bytes < 1000000) return (bytes / 1000).toFixed(2) + " kB";
|
|
5
|
+
if (bytes < 1000000000) return (bytes / 1000000).toFixed(2) + " MB";
|
|
6
|
+
if (bytes < 1000000000000) return (bytes / 1000000000).toFixed(2) + " GB";
|
|
7
|
+
return (bytes / 1000000000000).toFixed(2) + " TB";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function formatNumber(num: number): string {
|
|
11
|
+
return num.toLocaleString();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function serverName(name: string): string {
|
|
15
|
+
// Extract hostname from MongoDB connection string
|
|
16
|
+
if (name.startsWith("mongodb://") || name.startsWith("mongodb+srv://")) {
|
|
17
|
+
try {
|
|
18
|
+
const url = new URL(name);
|
|
19
|
+
return url.hostname;
|
|
20
|
+
} catch {
|
|
21
|
+
return name;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return name;
|
|
25
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { parseScript } from "esprima";
|
|
2
|
+
import type { Expression, Node } from "estree";
|
|
3
|
+
|
|
4
|
+
function buildObject(node: Node | Expression): unknown {
|
|
5
|
+
switch (node.type) {
|
|
6
|
+
case "ObjectExpression": {
|
|
7
|
+
const obj: Record<string, unknown> = {};
|
|
8
|
+
for (const prop of node.properties) {
|
|
9
|
+
let name;
|
|
10
|
+
if (prop.type === "SpreadElement") {
|
|
11
|
+
throw new Error(`Expected "Property" but received: ${prop.type}`);
|
|
12
|
+
}
|
|
13
|
+
if (prop.key.type === "Identifier") {
|
|
14
|
+
name = prop.key.name;
|
|
15
|
+
} else if (prop.key.type === "Literal") {
|
|
16
|
+
if (
|
|
17
|
+
prop.key.value instanceof RegExp ||
|
|
18
|
+
typeof prop.key.value === "bigint" ||
|
|
19
|
+
prop.key.value === false ||
|
|
20
|
+
prop.key.value === null ||
|
|
21
|
+
prop.key.value === true ||
|
|
22
|
+
prop.key.value === undefined
|
|
23
|
+
) {
|
|
24
|
+
throw new Error(`Expected "Identifier" for object key but received: ${prop.key.type}`);
|
|
25
|
+
}
|
|
26
|
+
name = prop.key.value;
|
|
27
|
+
} else {
|
|
28
|
+
throw new Error(`Expected "Identifier" but received: ${prop.key.type}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
obj[name] = buildObject(prop.value);
|
|
32
|
+
}
|
|
33
|
+
return obj;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
case "ArrayExpression": {
|
|
37
|
+
const obj: unknown[] = [];
|
|
38
|
+
for (const prop of node.elements) {
|
|
39
|
+
if (prop === null) {
|
|
40
|
+
throw new Error(`Expected "Expression" but received: ${prop}`);
|
|
41
|
+
}
|
|
42
|
+
obj.push(buildObject(prop));
|
|
43
|
+
}
|
|
44
|
+
return obj;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
case "Literal": {
|
|
48
|
+
if (node.value instanceof RegExp) {
|
|
49
|
+
return {
|
|
50
|
+
$type: "RegExp",
|
|
51
|
+
$value: {
|
|
52
|
+
$pattern: node.value.source,
|
|
53
|
+
$flags: node.value.flags,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return node.value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
case "UnaryExpression": {
|
|
62
|
+
if (node.operator === "-" && node.argument.type === "Literal" && typeof node.argument.value === "number") {
|
|
63
|
+
return -node.argument.value;
|
|
64
|
+
}
|
|
65
|
+
// const arg = buildObject(node.argument);
|
|
66
|
+
// const exp = node.prefix ? `${node.operator}${arg}` : `${arg}${node.operator}`;
|
|
67
|
+
|
|
68
|
+
// return eval(exp);
|
|
69
|
+
throw new Error(`${node.type} are not authorized`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
case "NewExpression":
|
|
73
|
+
case "CallExpression": {
|
|
74
|
+
const authorizedCalls = ["ObjectId", "Date", "RegExp"];
|
|
75
|
+
const callee = node.callee.type === "Identifier" ? node.callee.name : null;
|
|
76
|
+
if (callee && authorizedCalls.includes(callee)) {
|
|
77
|
+
if (callee === "RegExp") {
|
|
78
|
+
const [pattern, flags] = node.arguments.map((arg) => buildObject(arg));
|
|
79
|
+
return {
|
|
80
|
+
$type: "RegExp",
|
|
81
|
+
$value: {
|
|
82
|
+
$pattern: pattern,
|
|
83
|
+
$flags: flags,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
$type: callee,
|
|
90
|
+
$value: buildObject(node.arguments[0]),
|
|
91
|
+
};
|
|
92
|
+
} else {
|
|
93
|
+
throw new Error(`Unknown ${node.type}: ${callee}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
case "Identifier": {
|
|
98
|
+
if (node.name === "undefined") {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
throw `Unknown identifier: ${node.name}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
default:
|
|
105
|
+
throw new Error(`Sorry but ${node.type} are not authorized`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function parseJSON(text: string): unknown {
|
|
110
|
+
const tree = parseScript(`var __JSON__ = ${text};`, {
|
|
111
|
+
tolerant: true,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const varDeclaration = tree.body[0];
|
|
115
|
+
if (varDeclaration.type !== "VariableDeclaration") {
|
|
116
|
+
throw new Error("Expected VariableDeclaration but received: " + varDeclaration.type);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const objExpression = varDeclaration.declarations[0].init;
|
|
120
|
+
if (objExpression?.type !== "ObjectExpression") {
|
|
121
|
+
throw new Error("Expected ObjectExpression but received: " + objExpression?.type);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return buildObject(objExpression);
|
|
125
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { resolve } from "$app/paths";
|
|
3
|
+
import Breadcrumbs from "$lib/components/Breadcrumbs.svelte";
|
|
4
|
+
import Notifications from "$lib/components/Notifications.svelte";
|
|
5
|
+
import "../app.css";
|
|
6
|
+
|
|
7
|
+
let { children } = $props();
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<div style="min-height: 100vh">
|
|
11
|
+
<nav class="navbar px-6 py-4 flex items-center">
|
|
12
|
+
<a href={resolve("/")} class="text-2xl font-medium">Mongoku</a>
|
|
13
|
+
<Breadcrumbs />
|
|
14
|
+
</nav>
|
|
15
|
+
|
|
16
|
+
<div class="px-6 py-6 flex flex-col gap-6">
|
|
17
|
+
<Notifications />
|
|
18
|
+
{@render children()}
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<style lang="postcss">
|
|
23
|
+
.navbar {
|
|
24
|
+
background-color: var(--color-2);
|
|
25
|
+
border-bottom: var(--border);
|
|
26
|
+
}
|
|
27
|
+
</style>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { getMongo } from "$lib/server/mongo";
|
|
2
|
+
import type { DatabaseStats } from "$lib/types";
|
|
3
|
+
import type { PageServerLoad } from "./$types";
|
|
4
|
+
|
|
5
|
+
export const load: PageServerLoad = async () => {
|
|
6
|
+
const mongo = await getMongo();
|
|
7
|
+
|
|
8
|
+
const servers = mongo.listClients().map(({ name, _id, client }) => ({
|
|
9
|
+
name,
|
|
10
|
+
_id,
|
|
11
|
+
details: (async () => {
|
|
12
|
+
try {
|
|
13
|
+
const adminDb = client.db("test").admin();
|
|
14
|
+
const results = await adminDb.listDatabases();
|
|
15
|
+
const collectionsCount = await Promise.all(
|
|
16
|
+
results.databases.map(async (d) => {
|
|
17
|
+
const db = client.db(d.name);
|
|
18
|
+
const dbStats = (await db.stats()) as DatabaseStats;
|
|
19
|
+
return dbStats.collections;
|
|
20
|
+
}),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
databases: results.databases.map((d, index) => ({
|
|
25
|
+
name: d.name,
|
|
26
|
+
size: d.sizeOnDisk ?? 0,
|
|
27
|
+
collections: collectionsCount[index],
|
|
28
|
+
})),
|
|
29
|
+
size: results.totalSize ?? 0,
|
|
30
|
+
};
|
|
31
|
+
} catch (err) {
|
|
32
|
+
return {
|
|
33
|
+
error: {
|
|
34
|
+
code: err instanceof Error && "code" in err ? err.code : undefined,
|
|
35
|
+
name: err instanceof Error ? err.name : "Error",
|
|
36
|
+
message: err instanceof Error ? err.message : String(err),
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
})().catch((err) => ({
|
|
41
|
+
error: {
|
|
42
|
+
code: err instanceof Error && "code" in err ? err.code : undefined,
|
|
43
|
+
name: err instanceof Error ? err.name : "Error",
|
|
44
|
+
message: err instanceof Error ? err.message : String(err),
|
|
45
|
+
},
|
|
46
|
+
})),
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
servers.sort((a, b) => a.name.localeCompare(b.name));
|
|
50
|
+
return {
|
|
51
|
+
servers,
|
|
52
|
+
};
|
|
53
|
+
};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { addServer as addServerCommand, removeServer as removeServerCommand } from "$api/servers.remote";
|
|
3
|
+
import { invalidateAll } from "$app/navigation";
|
|
4
|
+
import { resolve } from "$app/paths";
|
|
5
|
+
import Panel from "$lib/components/Panel.svelte";
|
|
6
|
+
import TooltipTable from "$lib/components/TooltipTable.svelte";
|
|
7
|
+
import { notificationStore } from "$lib/stores/notifications.svelte";
|
|
8
|
+
import { formatBytes, serverName } from "$lib/utils/filters";
|
|
9
|
+
import type { PageData } from "./$types";
|
|
10
|
+
|
|
11
|
+
let { data }: { data: PageData } = $props();
|
|
12
|
+
|
|
13
|
+
type Server = PageData["servers"][number];
|
|
14
|
+
|
|
15
|
+
let adding = $state(false);
|
|
16
|
+
let newServer = $state("");
|
|
17
|
+
let loading = $state(false);
|
|
18
|
+
let showRemoveModal = $state(false);
|
|
19
|
+
let serverToRemove: Server | null = null;
|
|
20
|
+
|
|
21
|
+
async function addServer() {
|
|
22
|
+
if (!newServer) return;
|
|
23
|
+
|
|
24
|
+
loading = true;
|
|
25
|
+
try {
|
|
26
|
+
await addServerCommand({ url: newServer });
|
|
27
|
+
newServer = "";
|
|
28
|
+
adding = false;
|
|
29
|
+
notificationStore.notifySuccess("Server added successfully");
|
|
30
|
+
// Reload the page to get updated servers
|
|
31
|
+
await invalidateAll();
|
|
32
|
+
} catch (error) {
|
|
33
|
+
notificationStore.notifyError(error, "Failed to add server");
|
|
34
|
+
} finally {
|
|
35
|
+
loading = false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function openRemoveModal(server: Server) {
|
|
40
|
+
serverToRemove = server;
|
|
41
|
+
showRemoveModal = true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function closeRemoveModal() {
|
|
45
|
+
showRemoveModal = false;
|
|
46
|
+
serverToRemove = null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function confirmRemove() {
|
|
50
|
+
if (!serverToRemove) return;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
await removeServerCommand(serverToRemove.name);
|
|
54
|
+
notificationStore.notifySuccess("Server removed successfully");
|
|
55
|
+
closeRemoveModal();
|
|
56
|
+
// Reload the page to get updated servers
|
|
57
|
+
await invalidateAll();
|
|
58
|
+
} catch (error) {
|
|
59
|
+
notificationStore.notifyError(error, "Failed to remove server");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
64
|
+
if (e.key === "Escape") {
|
|
65
|
+
closeRemoveModal();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<svelte:window on:keydown={handleKeyDown} />
|
|
71
|
+
|
|
72
|
+
<Panel title="Servers">
|
|
73
|
+
{#snippet actions()}
|
|
74
|
+
<button class="btn btn-default btn-sm" onclick={() => (adding = !adding)}>
|
|
75
|
+
{adding ? "Cancel" : "Add Server"}
|
|
76
|
+
</button>
|
|
77
|
+
{#if adding}
|
|
78
|
+
<input
|
|
79
|
+
type="text"
|
|
80
|
+
placeholder="mongodb://user:password@hostname:port?directConnection=true"
|
|
81
|
+
bind:value={newServer}
|
|
82
|
+
disabled={loading}
|
|
83
|
+
class="form-input"
|
|
84
|
+
/>
|
|
85
|
+
<button class="btn btn-outline-success btn-sm" onclick={addServer} disabled={!newServer.length || loading}>
|
|
86
|
+
Add
|
|
87
|
+
</button>
|
|
88
|
+
{/if}
|
|
89
|
+
{/snippet}
|
|
90
|
+
|
|
91
|
+
<table class="table">
|
|
92
|
+
<thead>
|
|
93
|
+
<tr>
|
|
94
|
+
<th>Name</th>
|
|
95
|
+
<th>Databases</th>
|
|
96
|
+
<th>Size</th>
|
|
97
|
+
<th></th>
|
|
98
|
+
</tr>
|
|
99
|
+
</thead>
|
|
100
|
+
<tbody>
|
|
101
|
+
{#if data.servers && data.servers.length > 0}
|
|
102
|
+
{#each data.servers as server (server._id)}
|
|
103
|
+
<tr class="group">
|
|
104
|
+
<td>
|
|
105
|
+
{#await server.details}
|
|
106
|
+
<span>
|
|
107
|
+
{serverName(server.name)}
|
|
108
|
+
</span>
|
|
109
|
+
{:then details}
|
|
110
|
+
{#if "error" in details && details.error}
|
|
111
|
+
<span class="error">
|
|
112
|
+
<span class="badge badge-danger" title={details.error.message}>Error</span>
|
|
113
|
+
{serverName(server.name)}
|
|
114
|
+
</span>
|
|
115
|
+
{:else}
|
|
116
|
+
<a href={resolve(`/servers/${encodeURIComponent(serverName(server.name))}/databases`)}>
|
|
117
|
+
{serverName(server.name)}
|
|
118
|
+
</a>
|
|
119
|
+
{/if}
|
|
120
|
+
{/await}
|
|
121
|
+
</td>
|
|
122
|
+
<td>
|
|
123
|
+
{#await server.details}
|
|
124
|
+
<span class="text-gray-400">...</span>
|
|
125
|
+
{:then details}
|
|
126
|
+
{#if "databases" in details && details.databases}
|
|
127
|
+
<TooltipTable
|
|
128
|
+
columns={[
|
|
129
|
+
{ header: "Database", key: "name" },
|
|
130
|
+
{ header: "Collections", key: "collections" },
|
|
131
|
+
{ header: "Size", key: "size" },
|
|
132
|
+
]}
|
|
133
|
+
rows={details.databases.map((db) => ({
|
|
134
|
+
name: db.name,
|
|
135
|
+
collections: db.collections,
|
|
136
|
+
size: formatBytes(db.size),
|
|
137
|
+
}))}
|
|
138
|
+
>
|
|
139
|
+
{details.databases.length}
|
|
140
|
+
</TooltipTable>
|
|
141
|
+
{/if}
|
|
142
|
+
{/await}
|
|
143
|
+
</td>
|
|
144
|
+
<td>
|
|
145
|
+
{#await server.details}
|
|
146
|
+
<span class="text-gray-400">...</span>
|
|
147
|
+
{:then details}
|
|
148
|
+
{#if "size" in details && details.size !== undefined}
|
|
149
|
+
{formatBytes(details.size)}
|
|
150
|
+
{/if}
|
|
151
|
+
{/await}
|
|
152
|
+
</td>
|
|
153
|
+
<td style="width: 140px">
|
|
154
|
+
<button
|
|
155
|
+
class="btn btn-outline-danger btn-sm -my-2 hidden group-hover:inline"
|
|
156
|
+
onclick={() => openRemoveModal(server)}
|
|
157
|
+
>
|
|
158
|
+
Remove from list
|
|
159
|
+
</button>
|
|
160
|
+
</td>
|
|
161
|
+
</tr>
|
|
162
|
+
{/each}
|
|
163
|
+
{:else}
|
|
164
|
+
<tr>
|
|
165
|
+
<td colspan="4">
|
|
166
|
+
<div class="text-center">No servers...</div>
|
|
167
|
+
</td>
|
|
168
|
+
</tr>
|
|
169
|
+
{/if}
|
|
170
|
+
</tbody>
|
|
171
|
+
</table>
|
|
172
|
+
</Panel>
|
|
173
|
+
|
|
174
|
+
{#if showRemoveModal}
|
|
175
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
176
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
177
|
+
<div class="modal-overlay" onclick={closeRemoveModal}>
|
|
178
|
+
<div class="modal" onclick={(e) => e.stopPropagation()}>
|
|
179
|
+
<div class="modal-body">
|
|
180
|
+
<p>
|
|
181
|
+
Are you sure you want to remove this server? This will only remove it from this list. You can add it back.
|
|
182
|
+
</p>
|
|
183
|
+
</div>
|
|
184
|
+
<div class="modal-footer">
|
|
185
|
+
<button class="btn btn-default btn-sm" onclick={closeRemoveModal}>No</button>
|
|
186
|
+
<button class="btn btn-outline-danger btn-sm" onclick={confirmRemove}>Yes</button>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
{/if}
|
|
191
|
+
|
|
192
|
+
<style lang="postcss">
|
|
193
|
+
input[type="text"] {
|
|
194
|
+
min-width: 400px;
|
|
195
|
+
}
|
|
196
|
+
</style>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { getCollectionJson, getMongo } from "$lib/server/mongo";
|
|
2
|
+
import type { CollectionJSON, DatabaseStats } from "$lib/types";
|
|
3
|
+
import type { PageServerLoad } from "./$types";
|
|
4
|
+
|
|
5
|
+
export const load: PageServerLoad = async ({ params }) => {
|
|
6
|
+
const mongo = await getMongo();
|
|
7
|
+
const client = mongo.getClient(params.server);
|
|
8
|
+
const adminDb = client.db("test").admin();
|
|
9
|
+
const results = await adminDb.listDatabases();
|
|
10
|
+
|
|
11
|
+
if (!Array.isArray(results.databases)) {
|
|
12
|
+
return {
|
|
13
|
+
server: params.server,
|
|
14
|
+
databases: [],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const databases = await Promise.all(
|
|
19
|
+
results.databases.map(async (d) => {
|
|
20
|
+
const db = client.db(d.name);
|
|
21
|
+
const dbStats = (await db.stats()) as DatabaseStats;
|
|
22
|
+
|
|
23
|
+
const collectionsJson = db
|
|
24
|
+
.listCollections()
|
|
25
|
+
.toArray()
|
|
26
|
+
.then((c) => c.sort((a, b) => a.name.localeCompare(b.name)))
|
|
27
|
+
.then((c) => Promise.all(c.map((c) => getCollectionJson(db.collection(c.name), c.type))));
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
name: db.databaseName,
|
|
31
|
+
size: d.sizeOnDisk ?? 0,
|
|
32
|
+
dataSize: dbStats.dataSize,
|
|
33
|
+
avgObjSize: dbStats.avgObjSize,
|
|
34
|
+
storageSize: dbStats.storageSize,
|
|
35
|
+
totalIndexSize: dbStats.indexSize,
|
|
36
|
+
empty: d.empty ?? true,
|
|
37
|
+
nCollections: dbStats.collections,
|
|
38
|
+
collections: collectionsJson.catch(() => [] as CollectionJSON[]),
|
|
39
|
+
};
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
server: params.server,
|
|
45
|
+
databases,
|
|
46
|
+
};
|
|
47
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { resolve } from "$app/paths";
|
|
3
|
+
import Panel from "$lib/components/Panel.svelte";
|
|
4
|
+
import TooltipTable from "$lib/components/TooltipTable.svelte";
|
|
5
|
+
import { formatBytes } from "$lib/utils/filters";
|
|
6
|
+
import type { PageData } from "./$types";
|
|
7
|
+
|
|
8
|
+
let { data }: { data: PageData } = $props();
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<Panel title="Databases on {data.server}">
|
|
12
|
+
<table class="table">
|
|
13
|
+
<thead>
|
|
14
|
+
<tr>
|
|
15
|
+
<th>Name</th>
|
|
16
|
+
<th>Collections</th>
|
|
17
|
+
<th>Size</th>
|
|
18
|
+
</tr>
|
|
19
|
+
</thead>
|
|
20
|
+
<tbody>
|
|
21
|
+
{#if data.databases && data.databases.length > 0}
|
|
22
|
+
{#each data.databases as database (database.name)}
|
|
23
|
+
<tr>
|
|
24
|
+
<td>
|
|
25
|
+
<a
|
|
26
|
+
href={resolve(
|
|
27
|
+
`/servers/${encodeURIComponent(data.server)}/databases/${encodeURIComponent(
|
|
28
|
+
database.name,
|
|
29
|
+
)}/collections`,
|
|
30
|
+
)}
|
|
31
|
+
>
|
|
32
|
+
{database.name}
|
|
33
|
+
</a>
|
|
34
|
+
</td>
|
|
35
|
+
<td>
|
|
36
|
+
{#await database.collections}
|
|
37
|
+
{database.nCollections}
|
|
38
|
+
{:then collections}
|
|
39
|
+
<TooltipTable
|
|
40
|
+
columns={[
|
|
41
|
+
{ header: "Collection", key: "name", align: "left" },
|
|
42
|
+
{ header: "Size", key: "size", align: "right" },
|
|
43
|
+
]}
|
|
44
|
+
rows={collections.map((collection) => ({
|
|
45
|
+
name: collection.name,
|
|
46
|
+
size: formatBytes(collection.size),
|
|
47
|
+
}))}
|
|
48
|
+
>
|
|
49
|
+
{database.nCollections}
|
|
50
|
+
</TooltipTable>
|
|
51
|
+
{/await}
|
|
52
|
+
</td>
|
|
53
|
+
<td>
|
|
54
|
+
{#if database.size !== undefined}
|
|
55
|
+
<TooltipTable
|
|
56
|
+
hideHeader
|
|
57
|
+
columns={[
|
|
58
|
+
{ header: "Metric", key: "metric", align: "left" },
|
|
59
|
+
{ header: "Value", key: "value", align: "right" },
|
|
60
|
+
]}
|
|
61
|
+
rows={[
|
|
62
|
+
{ metric: "Total Size", value: database.size },
|
|
63
|
+
{ metric: "Data Size", value: database.dataSize },
|
|
64
|
+
{ metric: "Storage Size", value: database.storageSize },
|
|
65
|
+
{ metric: "Index Size", value: database.totalIndexSize },
|
|
66
|
+
{ metric: "Avg Object Size", value: database.avgObjSize },
|
|
67
|
+
{ metric: "Empty", value: database.empty ? "Yes" : "No" },
|
|
68
|
+
].map((row) => ({
|
|
69
|
+
...row,
|
|
70
|
+
value: typeof row.value === "number" ? formatBytes(row.value) : row.value,
|
|
71
|
+
}))}
|
|
72
|
+
>
|
|
73
|
+
{formatBytes(database.size)}
|
|
74
|
+
</TooltipTable>
|
|
75
|
+
{/if}
|
|
76
|
+
</td>
|
|
77
|
+
</tr>
|
|
78
|
+
{/each}
|
|
79
|
+
{:else}
|
|
80
|
+
<tr>
|
|
81
|
+
<td colspan="3">
|
|
82
|
+
<div class="text-center">No databases...</div>
|
|
83
|
+
</td>
|
|
84
|
+
</tr>
|
|
85
|
+
{/if}
|
|
86
|
+
</tbody>
|
|
87
|
+
</table>
|
|
88
|
+
</Panel>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { getCollectionJson, getMongo } from "$lib/server/mongo";
|
|
2
|
+
import type { PageServerLoad } from "./$types";
|
|
3
|
+
|
|
4
|
+
export const load: PageServerLoad = async ({ params }) => {
|
|
5
|
+
const mongo = await getMongo();
|
|
6
|
+
const client = mongo.getClient(params.server);
|
|
7
|
+
const db = client.db(params.database);
|
|
8
|
+
const collections = (await db.listCollections().toArray()).sort((a, b) => a.name.localeCompare(b.name));
|
|
9
|
+
|
|
10
|
+
const collectionsWithDetails = collections.map((c) => ({
|
|
11
|
+
name: c.name,
|
|
12
|
+
type: c.type,
|
|
13
|
+
details: getCollectionJson(db.collection(c.name), c.type).catch(() => null),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
server: params.server,
|
|
18
|
+
database: params.database,
|
|
19
|
+
collections: collectionsWithDetails,
|
|
20
|
+
};
|
|
21
|
+
};
|