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.
Files changed (151) hide show
  1. package/Dockerfile +27 -0
  2. package/build/client/_app/immutable/chunks/{MHUppGzk.js → BAM9w9EL.js} +1 -1
  3. package/build/client/_app/immutable/chunks/BAM9w9EL.js.br +0 -0
  4. package/build/client/_app/immutable/chunks/BAM9w9EL.js.gz +0 -0
  5. package/build/client/_app/immutable/chunks/{CN-ecO3-.js → BMa204Dm.js} +1 -1
  6. package/build/client/_app/immutable/chunks/BMa204Dm.js.br +0 -0
  7. package/build/client/_app/immutable/chunks/BMa204Dm.js.gz +0 -0
  8. package/build/client/_app/immutable/chunks/{_2kcttvK.js → BdR-m9Ad.js} +1 -1
  9. package/build/client/_app/immutable/chunks/BdR-m9Ad.js.br +0 -0
  10. package/build/client/_app/immutable/chunks/BdR-m9Ad.js.gz +0 -0
  11. package/build/client/_app/immutable/chunks/BzAcxkRZ.js +4 -0
  12. package/build/client/_app/immutable/chunks/BzAcxkRZ.js.br +0 -0
  13. package/build/client/_app/immutable/chunks/BzAcxkRZ.js.gz +0 -0
  14. package/build/client/_app/immutable/chunks/{BFhvhM4X.js → CyQLXPZI.js} +1 -1
  15. package/build/client/_app/immutable/chunks/CyQLXPZI.js.br +0 -0
  16. package/build/client/_app/immutable/chunks/CyQLXPZI.js.gz +0 -0
  17. package/build/client/_app/immutable/chunks/{BdWVCPGW.js → D4VhtiDg.js} +1 -1
  18. package/build/client/_app/immutable/chunks/D4VhtiDg.js.br +0 -0
  19. package/build/client/_app/immutable/chunks/D4VhtiDg.js.gz +0 -0
  20. package/build/client/_app/immutable/chunks/{zXvB9_Mi.js → XYFbSe2V.js} +1 -1
  21. package/build/client/_app/immutable/chunks/XYFbSe2V.js.br +0 -0
  22. package/build/client/_app/immutable/chunks/XYFbSe2V.js.gz +0 -0
  23. package/build/client/_app/immutable/chunks/{CGIdus8b.js → uMNMODvc.js} +1 -1
  24. package/build/client/_app/immutable/chunks/uMNMODvc.js.br +0 -0
  25. package/build/client/_app/immutable/chunks/uMNMODvc.js.gz +0 -0
  26. package/build/client/_app/immutable/entry/{app.hGE78f-O.js → app.9nC_873E.js} +2 -2
  27. package/build/client/_app/immutable/entry/app.9nC_873E.js.br +0 -0
  28. package/build/client/_app/immutable/entry/app.9nC_873E.js.gz +0 -0
  29. package/build/client/_app/immutable/entry/start.Bn88Alw2.js +1 -0
  30. package/build/client/_app/immutable/entry/start.Bn88Alw2.js.br +2 -0
  31. package/build/client/_app/immutable/entry/start.Bn88Alw2.js.gz +0 -0
  32. package/build/client/_app/immutable/nodes/{0.DyBXVtnT.js → 0.COxTCtn2.js} +1 -1
  33. package/build/client/_app/immutable/nodes/0.COxTCtn2.js.br +0 -0
  34. package/build/client/_app/immutable/nodes/0.COxTCtn2.js.gz +0 -0
  35. package/build/client/_app/immutable/nodes/{1.FqB0jq88.js → 1.Bc8yPK_D.js} +1 -1
  36. package/build/client/_app/immutable/nodes/1.Bc8yPK_D.js.br +0 -0
  37. package/build/client/_app/immutable/nodes/1.Bc8yPK_D.js.gz +0 -0
  38. package/build/client/_app/immutable/nodes/{3.Bekn_8hM.js → 3.CI2GcqTf.js} +1 -1
  39. package/build/client/_app/immutable/nodes/3.CI2GcqTf.js.br +0 -0
  40. package/build/client/_app/immutable/nodes/3.CI2GcqTf.js.gz +0 -0
  41. package/build/client/_app/immutable/nodes/{4.DQfaAvJi.js → 4.ChSdW7ac.js} +1 -1
  42. package/build/client/_app/immutable/nodes/4.ChSdW7ac.js.br +0 -0
  43. package/build/client/_app/immutable/nodes/4.ChSdW7ac.js.gz +0 -0
  44. package/build/client/_app/immutable/nodes/{5.B1E6iW2R.js → 5.DaMML2go.js} +1 -1
  45. package/build/client/_app/immutable/nodes/5.DaMML2go.js.br +0 -0
  46. package/build/client/_app/immutable/nodes/5.DaMML2go.js.gz +0 -0
  47. package/build/client/_app/immutable/nodes/{6.28eZQkvz.js → 6.Dcq0qwvO.js} +1 -1
  48. package/build/client/_app/immutable/nodes/6.Dcq0qwvO.js.br +0 -0
  49. package/build/client/_app/immutable/nodes/6.Dcq0qwvO.js.gz +0 -0
  50. package/build/client/_app/immutable/nodes/{7.qpcLWZb7.js → 7.CU-ncPes.js} +1 -1
  51. package/build/client/_app/immutable/nodes/7.CU-ncPes.js.br +0 -0
  52. package/build/client/_app/immutable/nodes/7.CU-ncPes.js.gz +0 -0
  53. package/build/client/_app/version.json +1 -1
  54. package/build/client/_app/version.json.br +0 -0
  55. package/build/client/_app/version.json.gz +0 -0
  56. package/build/server/chunks/{0-m42kIUxj.js → 0-C1NyHW8A.js} +2 -2
  57. package/build/server/chunks/{0-m42kIUxj.js.map → 0-C1NyHW8A.js.map} +1 -1
  58. package/build/server/chunks/{1-uc74UVG3.js → 1-CThf4W5r.js} +2 -2
  59. package/build/server/chunks/{1-uc74UVG3.js.map → 1-CThf4W5r.js.map} +1 -1
  60. package/build/server/chunks/{3-Bi8teWON.js → 3-CJf0NbiV.js} +2 -2
  61. package/build/server/chunks/{3-Bi8teWON.js.map → 3-CJf0NbiV.js.map} +1 -1
  62. package/build/server/chunks/{4-u1WGAtFU.js → 4-Dfbpsagm.js} +2 -2
  63. package/build/server/chunks/{4-u1WGAtFU.js.map → 4-Dfbpsagm.js.map} +1 -1
  64. package/build/server/chunks/{5-BlGdcdjs.js → 5-DLB6GOjf.js} +2 -2
  65. package/build/server/chunks/{5-BlGdcdjs.js.map → 5-DLB6GOjf.js.map} +1 -1
  66. package/build/server/chunks/{6-YCp6xyCU.js → 6-DfCARDKO.js} +2 -2
  67. package/build/server/chunks/{6-YCp6xyCU.js.map → 6-DfCARDKO.js.map} +1 -1
  68. package/build/server/chunks/{7-ieA4k9K_.js → 7-B5o4OymX.js} +2 -2
  69. package/build/server/chunks/{7-ieA4k9K_.js.map → 7-B5o4OymX.js.map} +1 -1
  70. package/build/server/index.js +1 -1
  71. package/build/server/index.js.map +1 -1
  72. package/build/server/manifest.js +8 -8
  73. package/build/server/manifest.js.map +1 -1
  74. package/cli.ts +148 -0
  75. package/dist/cli.js +2 -3
  76. package/ecosystem.config.js +9 -0
  77. package/package.json +10 -2
  78. package/src/api/servers.remote.ts +98 -0
  79. package/src/app.css +228 -0
  80. package/src/app.d.ts +16 -0
  81. package/src/app.html +11 -0
  82. package/src/hooks.server.ts +34 -0
  83. package/src/lib/components/Breadcrumbs.svelte +133 -0
  84. package/src/lib/components/JsonValue.svelte +248 -0
  85. package/src/lib/components/Notifications.svelte +81 -0
  86. package/src/lib/components/Panel.svelte +37 -0
  87. package/src/lib/components/PrettyJson.svelte +187 -0
  88. package/src/lib/components/SearchBox.svelte +160 -0
  89. package/src/lib/components/TooltipTable.svelte +137 -0
  90. package/src/lib/server/HostsManager.ts +105 -0
  91. package/src/lib/server/JsonEncoder.ts +62 -0
  92. package/src/lib/server/mongo.ts +199 -0
  93. package/src/lib/stores/notifications.svelte.ts +45 -0
  94. package/src/lib/types.ts +56 -0
  95. package/src/lib/utils/filters.ts +25 -0
  96. package/src/lib/utils/jsonParser.ts +125 -0
  97. package/src/routes/+layout.server.ts +7 -0
  98. package/src/routes/+layout.svelte +27 -0
  99. package/src/routes/+page.server.ts +6 -0
  100. package/src/routes/servers/+page.server.ts +53 -0
  101. package/src/routes/servers/+page.svelte +196 -0
  102. package/src/routes/servers/[server]/databases/+page.server.ts +47 -0
  103. package/src/routes/servers/[server]/databases/+page.svelte +88 -0
  104. package/src/routes/servers/[server]/databases/[database]/collections/+page.server.ts +21 -0
  105. package/src/routes/servers/[server]/databases/[database]/collections/+page.svelte +110 -0
  106. package/src/routes/servers/[server]/databases/[database]/collections/[collection]/+page.server.ts +106 -0
  107. package/src/routes/servers/[server]/databases/[database]/collections/[collection]/+page.svelte +174 -0
  108. package/src/routes/servers/[server]/databases/[database]/collections/[collection]/documents/[document]/+page.server.ts +25 -0
  109. package/src/routes/servers/[server]/databases/[database]/collections/[collection]/documents/[document]/+page.svelte +90 -0
  110. package/src/tests/api/readonly.test.ts +89 -0
  111. package/src/tests/setup.ts +19 -0
  112. package/svelte.config.js +28 -0
  113. package/tsconfig.cli.json +15 -0
  114. package/tsconfig.json +19 -0
  115. package/vite.config.ts +7 -0
  116. package/build/client/_app/immutable/chunks/BFhvhM4X.js.br +0 -0
  117. package/build/client/_app/immutable/chunks/BFhvhM4X.js.gz +0 -0
  118. package/build/client/_app/immutable/chunks/BdWVCPGW.js.br +0 -0
  119. package/build/client/_app/immutable/chunks/BdWVCPGW.js.gz +0 -0
  120. package/build/client/_app/immutable/chunks/CGIdus8b.js.br +0 -0
  121. package/build/client/_app/immutable/chunks/CGIdus8b.js.gz +0 -0
  122. package/build/client/_app/immutable/chunks/CN-ecO3-.js.br +0 -0
  123. package/build/client/_app/immutable/chunks/CN-ecO3-.js.gz +0 -0
  124. package/build/client/_app/immutable/chunks/DB3PPjLu.js +0 -4
  125. package/build/client/_app/immutable/chunks/DB3PPjLu.js.br +0 -0
  126. package/build/client/_app/immutable/chunks/DB3PPjLu.js.gz +0 -0
  127. package/build/client/_app/immutable/chunks/MHUppGzk.js.br +0 -0
  128. package/build/client/_app/immutable/chunks/MHUppGzk.js.gz +0 -0
  129. package/build/client/_app/immutable/chunks/_2kcttvK.js.br +0 -0
  130. package/build/client/_app/immutable/chunks/_2kcttvK.js.gz +0 -0
  131. package/build/client/_app/immutable/chunks/zXvB9_Mi.js.br +0 -0
  132. package/build/client/_app/immutable/chunks/zXvB9_Mi.js.gz +0 -0
  133. package/build/client/_app/immutable/entry/app.hGE78f-O.js.br +0 -0
  134. package/build/client/_app/immutable/entry/app.hGE78f-O.js.gz +0 -0
  135. package/build/client/_app/immutable/entry/start._GE1Zd3d.js +0 -1
  136. package/build/client/_app/immutable/entry/start._GE1Zd3d.js.br +0 -2
  137. package/build/client/_app/immutable/entry/start._GE1Zd3d.js.gz +0 -0
  138. package/build/client/_app/immutable/nodes/0.DyBXVtnT.js.br +0 -0
  139. package/build/client/_app/immutable/nodes/0.DyBXVtnT.js.gz +0 -0
  140. package/build/client/_app/immutable/nodes/1.FqB0jq88.js.br +0 -2
  141. package/build/client/_app/immutable/nodes/1.FqB0jq88.js.gz +0 -0
  142. package/build/client/_app/immutable/nodes/3.Bekn_8hM.js.br +0 -0
  143. package/build/client/_app/immutable/nodes/3.Bekn_8hM.js.gz +0 -0
  144. package/build/client/_app/immutable/nodes/4.DQfaAvJi.js.br +0 -0
  145. package/build/client/_app/immutable/nodes/4.DQfaAvJi.js.gz +0 -0
  146. package/build/client/_app/immutable/nodes/5.B1E6iW2R.js.br +0 -0
  147. package/build/client/_app/immutable/nodes/5.B1E6iW2R.js.gz +0 -0
  148. package/build/client/_app/immutable/nodes/6.28eZQkvz.js.br +0 -0
  149. package/build/client/_app/immutable/nodes/6.28eZQkvz.js.gz +0 -0
  150. package/build/client/_app/immutable/nodes/7.qpcLWZb7.js.br +0 -0
  151. package/build/client/_app/immutable/nodes/7.qpcLWZb7.js.gz +0 -0
@@ -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,7 @@
1
+ import type { LayoutServerLoad } from "./$types";
2
+
3
+ export const load: LayoutServerLoad = async () => {
4
+ return {
5
+ readOnly: process.env.MONGOKU_READ_ONLY_MODE === "true",
6
+ };
7
+ };
@@ -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,6 @@
1
+ import { redirect } from "@sveltejs/kit";
2
+ import type { PageServerLoad } from "./$types";
3
+
4
+ export const load: PageServerLoad = async () => {
5
+ redirect(307, "/servers");
6
+ };
@@ -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
+ };