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
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { goto } from "$app/navigation";
|
|
3
|
+
import { resolve } from "$app/paths";
|
|
4
|
+
import { page } from "$app/state";
|
|
5
|
+
import type { SearchParams } from "$lib/types";
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
params: SearchParams;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let { params = $bindable() }: Props = $props();
|
|
12
|
+
|
|
13
|
+
// Show optional fields - start with all hidden
|
|
14
|
+
let showOptionalFields = $state(
|
|
15
|
+
params.sort !== "{}" || params.project !== "{}" || params.skip !== 0 || params.limit !== 20,
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
let counter = $state(Math.random());
|
|
19
|
+
|
|
20
|
+
let queryInput: HTMLInputElement | undefined;
|
|
21
|
+
|
|
22
|
+
$effect(() => {
|
|
23
|
+
if (queryInput) {
|
|
24
|
+
queryInput.setSelectionRange(1, 1 /* queryInput.value.length - 1 */);
|
|
25
|
+
queryInput.focus();
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
async function submit(event: SubmitEvent) {
|
|
30
|
+
event.preventDefault();
|
|
31
|
+
counter++;
|
|
32
|
+
const formData = new FormData(form);
|
|
33
|
+
await goto(
|
|
34
|
+
resolve(
|
|
35
|
+
(page.url.pathname +
|
|
36
|
+
"?" +
|
|
37
|
+
[...formData.entries()]
|
|
38
|
+
.map((e) => encodeURIComponent(e[0]) + "=" + encodeURIComponent(e[1] as string))
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
40
|
+
.join("&")) as any,
|
|
41
|
+
),
|
|
42
|
+
{
|
|
43
|
+
keepFocus: true,
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let form: HTMLFormElement | undefined;
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<form class="flex items-stretch w-full" method="GET" action="?" onsubmit={submit} bind:this={form}>
|
|
52
|
+
<!-- Parameters group -->
|
|
53
|
+
<div class="flex-grow">
|
|
54
|
+
<!-- Query input (always shown) -->
|
|
55
|
+
<div class="flex items-stretch w-full h-10">
|
|
56
|
+
<div
|
|
57
|
+
class="min-w-[100px] flex justify-center items-center border border-[var(--color-4)] {!showOptionalFields
|
|
58
|
+
? 'border-b rounded-bl-md'
|
|
59
|
+
: 'border-b-0'} bg-[var(--color-1)] rounded-tl-md"
|
|
60
|
+
>
|
|
61
|
+
Query:
|
|
62
|
+
</div>
|
|
63
|
+
<input
|
|
64
|
+
type="text"
|
|
65
|
+
bind:this={queryInput}
|
|
66
|
+
bind:value={params.query}
|
|
67
|
+
placeholder={"{}"}
|
|
68
|
+
name="query"
|
|
69
|
+
class="flex-grow border-0 bg-[var(--color-3)] pl-2.5 font-mono"
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<input type="hidden" value={counter} name="v" />
|
|
74
|
+
<!-- Sort input -->
|
|
75
|
+
{#if showOptionalFields}
|
|
76
|
+
<div class="flex items-stretch w-full h-10">
|
|
77
|
+
<div
|
|
78
|
+
class="min-w-[100px] flex justify-center items-center border border-[var(--color-4)] border-b-0 bg-[var(--color-1)]"
|
|
79
|
+
>
|
|
80
|
+
Sort:
|
|
81
|
+
</div>
|
|
82
|
+
<input
|
|
83
|
+
type="text"
|
|
84
|
+
bind:value={params.sort}
|
|
85
|
+
name="sort"
|
|
86
|
+
placeholder={"{}"}
|
|
87
|
+
class="flex-grow border-0 border-t border-[var(--color-4)] bg-[var(--color-3)] pl-2.5 font-mono"
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<!-- Skip input -->
|
|
92
|
+
<div class="flex items-stretch w-full h-10">
|
|
93
|
+
<div
|
|
94
|
+
class="min-w-[100px] flex justify-center items-center border border-[var(--color-4)] border-b-0 bg-[var(--color-1)]"
|
|
95
|
+
>
|
|
96
|
+
Skip:
|
|
97
|
+
</div>
|
|
98
|
+
<input
|
|
99
|
+
type="number"
|
|
100
|
+
bind:value={params.skip}
|
|
101
|
+
name="skip"
|
|
102
|
+
min="0"
|
|
103
|
+
class="flex-grow border-0 border-t border-[var(--color-4)] bg-[var(--color-3)] pl-2.5 font-mono"
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<!-- Limit input -->
|
|
108
|
+
<div class="flex items-stretch w-full h-10">
|
|
109
|
+
<div
|
|
110
|
+
class="min-w-[100px] flex justify-center items-center border border-[var(--color-4)] border-b-0 bg-[var(--color-1)]"
|
|
111
|
+
>
|
|
112
|
+
Limit:
|
|
113
|
+
</div>
|
|
114
|
+
<input
|
|
115
|
+
type="number"
|
|
116
|
+
bind:value={params.limit}
|
|
117
|
+
name="limit"
|
|
118
|
+
min="1"
|
|
119
|
+
class="flex-grow border-0 border-t border-[var(--color-4)] bg-[var(--color-3)] pl-2.5 font-mono"
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<!-- Project input -->
|
|
124
|
+
<div class="flex items-stretch w-full h-10">
|
|
125
|
+
<div
|
|
126
|
+
class="min-w-[100px] flex justify-center items-center border border-[var(--color-4)] border-b bg-[var(--color-1)] rounded-bl-md"
|
|
127
|
+
>
|
|
128
|
+
Project:
|
|
129
|
+
</div>
|
|
130
|
+
<input
|
|
131
|
+
type="text"
|
|
132
|
+
bind:value={params.project}
|
|
133
|
+
name="project"
|
|
134
|
+
placeholder={"{}"}
|
|
135
|
+
class="flex-grow border-0 border-t border-[var(--color-4)] bg-[var(--color-3)] pl-2.5 font-mono"
|
|
136
|
+
/>
|
|
137
|
+
</div>
|
|
138
|
+
{/if}
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<!-- Toggle optional fields button -->
|
|
142
|
+
<button
|
|
143
|
+
class="btn btn-default !w-12 !rounded-none !border-r-0 text-2xl leading-none font-bold !py-1.5"
|
|
144
|
+
type="button"
|
|
145
|
+
onclick={() => {
|
|
146
|
+
showOptionalFields = !showOptionalFields;
|
|
147
|
+
}}
|
|
148
|
+
>
|
|
149
|
+
{showOptionalFields ? "−" : "+"}
|
|
150
|
+
</button>
|
|
151
|
+
|
|
152
|
+
<!-- Search button -->
|
|
153
|
+
<button class="btn btn-success !rounded-l-none !rounded-r-md font-bold !py-1.5" type="submit"> GO! </button>
|
|
154
|
+
</form>
|
|
155
|
+
|
|
156
|
+
<style lang="postcss">
|
|
157
|
+
input {
|
|
158
|
+
border-radius: 0;
|
|
159
|
+
}
|
|
160
|
+
</style>
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { tick, type Snippet } from "svelte";
|
|
3
|
+
|
|
4
|
+
interface TableColumn {
|
|
5
|
+
header: string;
|
|
6
|
+
key: string;
|
|
7
|
+
align?: "left" | "right" | "center";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface TableRow {
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let {
|
|
15
|
+
columns,
|
|
16
|
+
rows,
|
|
17
|
+
children,
|
|
18
|
+
hideHeader,
|
|
19
|
+
}: {
|
|
20
|
+
columns: TableColumn[];
|
|
21
|
+
rows: TableRow[];
|
|
22
|
+
children: Snippet;
|
|
23
|
+
hideHeader?: boolean;
|
|
24
|
+
} = $props();
|
|
25
|
+
|
|
26
|
+
let showTooltip = $state(false);
|
|
27
|
+
let tooltipElement = $state<HTMLDivElement>();
|
|
28
|
+
let containerElement = $state<HTMLDivElement>();
|
|
29
|
+
|
|
30
|
+
let tooltipPosition = $state({
|
|
31
|
+
left: "",
|
|
32
|
+
right: "",
|
|
33
|
+
top: "",
|
|
34
|
+
bottom: "",
|
|
35
|
+
marginTop: "",
|
|
36
|
+
marginBottom: "",
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
function handleMouseEnter() {
|
|
40
|
+
showTooltip = true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function handleMouseLeave() {
|
|
44
|
+
showTooltip = false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
$effect(() => {
|
|
48
|
+
if (showTooltip && tooltipElement && containerElement) {
|
|
49
|
+
tick().then(() => {
|
|
50
|
+
if (!containerElement || !tooltipElement) return;
|
|
51
|
+
|
|
52
|
+
const tooltipRect = tooltipElement.getBoundingClientRect();
|
|
53
|
+
const viewportWidth = window.innerWidth;
|
|
54
|
+
const viewportHeight = window.innerHeight;
|
|
55
|
+
|
|
56
|
+
// Reset positioning
|
|
57
|
+
tooltipPosition = {
|
|
58
|
+
left: "",
|
|
59
|
+
right: "",
|
|
60
|
+
top: "",
|
|
61
|
+
bottom: "",
|
|
62
|
+
marginTop: "",
|
|
63
|
+
marginBottom: "",
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Position horizontally
|
|
67
|
+
if (tooltipRect.right > viewportWidth) {
|
|
68
|
+
tooltipPosition.right = "0";
|
|
69
|
+
} else {
|
|
70
|
+
tooltipPosition.left = "0";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Position vertically
|
|
74
|
+
if (tooltipRect.bottom > viewportHeight) {
|
|
75
|
+
tooltipPosition.bottom = "100%";
|
|
76
|
+
tooltipPosition.marginBottom = "5px";
|
|
77
|
+
} else {
|
|
78
|
+
tooltipPosition.top = "100%";
|
|
79
|
+
tooltipPosition.marginTop = "5px";
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
</script>
|
|
85
|
+
|
|
86
|
+
<div class="relative inline-block" bind:this={containerElement}>
|
|
87
|
+
<button class="dotted text-center" onmouseenter={handleMouseEnter} onmouseleave={handleMouseLeave}>
|
|
88
|
+
{@render children?.()}
|
|
89
|
+
</button>
|
|
90
|
+
{#if showTooltip}
|
|
91
|
+
<div
|
|
92
|
+
class="absolute bg-[var(--color-2)] border border-[var(--color-3)] rounded p-0 z-[1000] whitespace-nowrap shadow-lg"
|
|
93
|
+
bind:this={tooltipElement}
|
|
94
|
+
style:left={tooltipPosition.left}
|
|
95
|
+
style:right={tooltipPosition.right}
|
|
96
|
+
style:top={tooltipPosition.top}
|
|
97
|
+
style:bottom={tooltipPosition.bottom}
|
|
98
|
+
style:margin-top={tooltipPosition.marginTop}
|
|
99
|
+
style:margin-bottom={tooltipPosition.marginBottom}
|
|
100
|
+
>
|
|
101
|
+
<table class="w-full border-collapse text-base font-medium">
|
|
102
|
+
{#if !hideHeader}
|
|
103
|
+
<thead>
|
|
104
|
+
<tr>
|
|
105
|
+
{#each columns as column, index (index)}
|
|
106
|
+
<th
|
|
107
|
+
class="px-2 py-1 border-b border-[var(--color-3)] bg-[var(--color-4)] font-bold"
|
|
108
|
+
class:text-left={column.align === "left" || !column.align}
|
|
109
|
+
class:text-right={column.align === "right"}
|
|
110
|
+
class:text-center={column.align === "center"}
|
|
111
|
+
>
|
|
112
|
+
{column.header}
|
|
113
|
+
</th>
|
|
114
|
+
{/each}
|
|
115
|
+
</tr>
|
|
116
|
+
</thead>
|
|
117
|
+
{/if}
|
|
118
|
+
<tbody>
|
|
119
|
+
{#each rows as row, index (index)}
|
|
120
|
+
<tr>
|
|
121
|
+
{#each columns as column, index (index)}
|
|
122
|
+
<td
|
|
123
|
+
class="px-2 py-1 border-b border-[var(--color-3)]"
|
|
124
|
+
class:text-left={column.align === "left" || !column.align}
|
|
125
|
+
class:text-right={column.align === "right"}
|
|
126
|
+
class:text-center={column.align === "center"}
|
|
127
|
+
>
|
|
128
|
+
{row[column.key]}
|
|
129
|
+
</td>
|
|
130
|
+
{/each}
|
|
131
|
+
</tr>
|
|
132
|
+
{/each}
|
|
133
|
+
</tbody>
|
|
134
|
+
</table>
|
|
135
|
+
</div>
|
|
136
|
+
{/if}
|
|
137
|
+
</div>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
|
|
5
|
+
export interface Host {
|
|
6
|
+
path: string;
|
|
7
|
+
_id: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const DEFAULT_HOSTS = process.env.MONGOKU_DEFAULT_HOST
|
|
11
|
+
? process.env.MONGOKU_DEFAULT_HOST.split(";")
|
|
12
|
+
: ["localhost:27017"];
|
|
13
|
+
const DATABASE_FILE = process.env.MONGOKU_DATABASE_FILE || path.join(os.homedir(), ".mongoku.db");
|
|
14
|
+
|
|
15
|
+
export class HostsManager {
|
|
16
|
+
private _hosts: Map<string, string> = new Map(); // path -> _id
|
|
17
|
+
|
|
18
|
+
async load() {
|
|
19
|
+
let first = false;
|
|
20
|
+
try {
|
|
21
|
+
await fs.promises.stat(DATABASE_FILE);
|
|
22
|
+
} catch {
|
|
23
|
+
first = true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!first) {
|
|
27
|
+
await this._loadFromFile();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (first || this._hosts.size === 0) {
|
|
31
|
+
// Initialize with default hosts
|
|
32
|
+
for (const hostname of DEFAULT_HOSTS) {
|
|
33
|
+
this._hosts.set(hostname, this._generateId());
|
|
34
|
+
}
|
|
35
|
+
await this._saveToFile();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private async _loadFromFile(): Promise<void> {
|
|
40
|
+
const content = await fs.promises.readFile(DATABASE_FILE, "utf8");
|
|
41
|
+
const lines = content
|
|
42
|
+
.trim()
|
|
43
|
+
.split("\n")
|
|
44
|
+
.filter((line) => line.trim());
|
|
45
|
+
|
|
46
|
+
const newHosts = new Map<string, string>();
|
|
47
|
+
for (const line of lines) {
|
|
48
|
+
const host = JSON.parse(line);
|
|
49
|
+
if (host && typeof host.path === "string") {
|
|
50
|
+
// Use existing _id if available, generate new one if not
|
|
51
|
+
const id = host._id || this._generateId();
|
|
52
|
+
newHosts.set(host.path, id);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
this._hosts = newHosts;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private async _saveToFile(): Promise<void> {
|
|
59
|
+
const lines = Array.from(this._hosts).map(([hostPath, id]) => JSON.stringify({ path: hostPath, _id: id }));
|
|
60
|
+
await fs.promises.writeFile(DATABASE_FILE, lines.join("\n") + "\n", "utf8");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private _generateId(): string {
|
|
64
|
+
// Generate a NeDB-compatible ID (16 characters, alphanumeric)
|
|
65
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
66
|
+
let result = "";
|
|
67
|
+
for (let i = 0; i < 16; i++) {
|
|
68
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async getHosts(): Promise<Host[]> {
|
|
74
|
+
return Array.from(this._hosts).map(([hostPath, id]) => ({ path: hostPath, _id: id }));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async add(hostPath: string): Promise<string> {
|
|
78
|
+
// Use existing ID if host already exists, generate new one if not
|
|
79
|
+
let id = this._hosts.get(hostPath);
|
|
80
|
+
if (!id) {
|
|
81
|
+
id = this._generateId();
|
|
82
|
+
this._hosts.set(hostPath, id);
|
|
83
|
+
}
|
|
84
|
+
await this._saveToFile();
|
|
85
|
+
return id;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async remove(hostPath: string): Promise<void> {
|
|
89
|
+
// Remove exact matches and regex pattern matches
|
|
90
|
+
const toRemove = Array.from(this._hosts.keys()).filter((existingPath) => {
|
|
91
|
+
try {
|
|
92
|
+
const regex = new RegExp(hostPath);
|
|
93
|
+
return existingPath === hostPath || regex.test(existingPath);
|
|
94
|
+
} catch {
|
|
95
|
+
// If hostPath is not a valid regex, just do exact match
|
|
96
|
+
return existingPath === hostPath;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
for (const host of toRemove) {
|
|
101
|
+
this._hosts.delete(host);
|
|
102
|
+
}
|
|
103
|
+
await this._saveToFile();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { ObjectId } from "mongodb";
|
|
2
|
+
|
|
3
|
+
export default class JsonEncoder {
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5
|
+
static encode(obj: any): any {
|
|
6
|
+
if (obj instanceof ObjectId) {
|
|
7
|
+
return {
|
|
8
|
+
$type: "ObjectId",
|
|
9
|
+
$value: obj.toHexString(),
|
|
10
|
+
$date: obj.getTimestamp().getTime(),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
if (obj instanceof Date) {
|
|
14
|
+
return {
|
|
15
|
+
$type: "Date",
|
|
16
|
+
$value: obj.toISOString(),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (obj instanceof RegExp) {
|
|
20
|
+
return {
|
|
21
|
+
$type: "RegExp",
|
|
22
|
+
$value: {
|
|
23
|
+
$pattern: obj.source,
|
|
24
|
+
$flags: obj.flags,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (Array.isArray(obj)) {
|
|
29
|
+
return [...obj.map(JsonEncoder.encode)];
|
|
30
|
+
}
|
|
31
|
+
if (obj && typeof obj === "object") {
|
|
32
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
33
|
+
obj[key] = JsonEncoder.encode(value);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return obj;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
41
|
+
static decode(obj: any): any {
|
|
42
|
+
if (obj && obj.$type === "ObjectId") {
|
|
43
|
+
return ObjectId.createFromHexString(obj.$value);
|
|
44
|
+
}
|
|
45
|
+
if (obj && obj.$type === "Date") {
|
|
46
|
+
return new Date(obj.$value);
|
|
47
|
+
}
|
|
48
|
+
if (obj && obj.$type === "RegExp") {
|
|
49
|
+
return new RegExp(obj.$value.$pattern, obj.$value.$flags);
|
|
50
|
+
}
|
|
51
|
+
if (Array.isArray(obj)) {
|
|
52
|
+
return [...obj.map(JsonEncoder.decode)];
|
|
53
|
+
}
|
|
54
|
+
if (obj && typeof obj === "object") {
|
|
55
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
56
|
+
obj[key] = JsonEncoder.decode(value);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return obj;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { CollectionJSON } from "$lib/types";
|
|
2
|
+
import { MongoClient, type Collection } from "mongodb";
|
|
3
|
+
import { URL } from "url";
|
|
4
|
+
import { HostsManager } from "./HostsManager";
|
|
5
|
+
|
|
6
|
+
export async function getCollectionJson(
|
|
7
|
+
collection: Collection,
|
|
8
|
+
type?: "view" | "timeseries" | "collection" | string,
|
|
9
|
+
): Promise<CollectionJSON> {
|
|
10
|
+
const stats = {
|
|
11
|
+
size: 0,
|
|
12
|
+
count: 0,
|
|
13
|
+
avgObjSize: 0,
|
|
14
|
+
storageSize: 0,
|
|
15
|
+
capped: false,
|
|
16
|
+
nindexes: 0,
|
|
17
|
+
totalIndexSize: 0,
|
|
18
|
+
indexSizes: {},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
if (type !== "view") {
|
|
22
|
+
const agg = (await collection
|
|
23
|
+
.aggregate([
|
|
24
|
+
{
|
|
25
|
+
$collStats: {
|
|
26
|
+
storageStats: {},
|
|
27
|
+
count: {},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
])
|
|
31
|
+
.next()
|
|
32
|
+
.catch(() => null)) as {
|
|
33
|
+
storageStats: {
|
|
34
|
+
size: number;
|
|
35
|
+
count: number;
|
|
36
|
+
storageSize: number;
|
|
37
|
+
capped: boolean;
|
|
38
|
+
nindexes: number;
|
|
39
|
+
totalIndexSize: number;
|
|
40
|
+
indexSizes: Record<string, number>;
|
|
41
|
+
};
|
|
42
|
+
} | null;
|
|
43
|
+
|
|
44
|
+
if (agg) {
|
|
45
|
+
stats.size = agg.storageStats.size;
|
|
46
|
+
stats.count = agg.storageStats.count;
|
|
47
|
+
stats.avgObjSize = Math.round(agg.storageStats.size / agg.storageStats.count);
|
|
48
|
+
stats.storageSize = agg.storageStats.storageSize;
|
|
49
|
+
stats.capped = agg.storageStats.capped;
|
|
50
|
+
stats.nindexes = agg.storageStats.nindexes;
|
|
51
|
+
stats.totalIndexSize = agg.storageStats.totalIndexSize;
|
|
52
|
+
stats.indexSizes = agg.storageStats.indexSizes;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Get index definitions
|
|
57
|
+
let indexes: Array<{ name: string; key?: Record<string, number>; size: number }> = [];
|
|
58
|
+
if (type !== "view") {
|
|
59
|
+
try {
|
|
60
|
+
const indexList = await collection.listIndexes().toArray();
|
|
61
|
+
indexes = indexList.map((index: { name: string; key: Record<string, number> }) => ({
|
|
62
|
+
name: index.name,
|
|
63
|
+
key: index.key,
|
|
64
|
+
size: (stats.indexSizes as Record<string, number>)[index.name] || 0,
|
|
65
|
+
}));
|
|
66
|
+
} catch {
|
|
67
|
+
// If we can't get index details, fall back to indexSizes
|
|
68
|
+
indexes = Object.entries(stats.indexSizes).map(([name, size]) => ({
|
|
69
|
+
name,
|
|
70
|
+
size: size as number,
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
name: collection.collectionName,
|
|
77
|
+
size: (stats.storageSize ?? 0) + (stats.totalIndexSize ?? 0),
|
|
78
|
+
dataSize: stats.size,
|
|
79
|
+
count: stats.count,
|
|
80
|
+
avgObjSize: stats.avgObjSize ?? 0,
|
|
81
|
+
storageSize: stats.storageSize ?? 0,
|
|
82
|
+
capped: stats.capped,
|
|
83
|
+
nIndexes: stats.nindexes,
|
|
84
|
+
totalIndexSize: stats.totalIndexSize ?? 0,
|
|
85
|
+
indexSizes: stats.indexSizes,
|
|
86
|
+
indexes,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
class MongoConnections {
|
|
91
|
+
/**
|
|
92
|
+
* Todo: better system where we can have mutiple servers with same hostname, and labels for each server that
|
|
93
|
+
* would be displayed in the UI instead of the hostname.
|
|
94
|
+
*/
|
|
95
|
+
private clients: Map<string, MongoClient> = new Map();
|
|
96
|
+
private clientIds: Map<string, string> = new Map(); // hostname -> _id
|
|
97
|
+
private hostsManager: HostsManager;
|
|
98
|
+
private countTimeout = parseInt(process.env.MONGOKU_COUNT_TIMEOUT!, 10) || 5000;
|
|
99
|
+
|
|
100
|
+
constructor() {
|
|
101
|
+
this.hostsManager = new HostsManager();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async initialize() {
|
|
105
|
+
await this.hostsManager.load();
|
|
106
|
+
const hosts = await this.hostsManager.getHosts();
|
|
107
|
+
|
|
108
|
+
// Create MongoClient instances without connecting (lazy connection)
|
|
109
|
+
for (const host of hosts) {
|
|
110
|
+
const urlStr = host.path.startsWith("mongodb") ? host.path : `mongodb://${host.path}`;
|
|
111
|
+
try {
|
|
112
|
+
const url = new URL(urlStr);
|
|
113
|
+
const hostname = url.host || host.path;
|
|
114
|
+
|
|
115
|
+
if (!this.clients.has(hostname)) {
|
|
116
|
+
const client = new MongoClient(urlStr);
|
|
117
|
+
this.clients.set(hostname, client);
|
|
118
|
+
this.clientIds.set(hostname, host._id);
|
|
119
|
+
}
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.error(`Failed to parse URL for host ${host.path}:`, err);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
getClient(name: string): MongoClient {
|
|
127
|
+
const client = this.clients.get(name) || this.clients.get(`${name}:27017`);
|
|
128
|
+
if (!client) {
|
|
129
|
+
throw new Error("Server does not exist");
|
|
130
|
+
}
|
|
131
|
+
return client;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
listClients(): Array<{ name: string; _id: string; client: MongoClient }> {
|
|
135
|
+
return Array.from(this.clients.entries())
|
|
136
|
+
.filter(([, client]) => client instanceof MongoClient)
|
|
137
|
+
.map(([name, client]) => ({
|
|
138
|
+
name,
|
|
139
|
+
_id: this.clientIds.get(name) || "",
|
|
140
|
+
client: client as MongoClient,
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
getCollection(serverName: string, databaseName: string, collectionName: string) {
|
|
145
|
+
const client = this.getClient(serverName);
|
|
146
|
+
const db = client.db(databaseName);
|
|
147
|
+
return db.collection(collectionName);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
getCountTimeout() {
|
|
151
|
+
return this.countTimeout;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async addServer(hostPath: string) {
|
|
155
|
+
const id = await this.hostsManager.add(hostPath);
|
|
156
|
+
|
|
157
|
+
// Add the new server client
|
|
158
|
+
const urlStr = hostPath.startsWith("mongodb") ? hostPath : `mongodb://${hostPath}`;
|
|
159
|
+
try {
|
|
160
|
+
const url = new URL(urlStr);
|
|
161
|
+
const hostname = url.host || hostPath;
|
|
162
|
+
|
|
163
|
+
if (!this.clients.has(hostname)) {
|
|
164
|
+
const client = new MongoClient(urlStr);
|
|
165
|
+
this.clients.set(hostname, client);
|
|
166
|
+
this.clientIds.set(hostname, id);
|
|
167
|
+
}
|
|
168
|
+
} catch (err) {
|
|
169
|
+
console.error(`Failed to parse URL for host ${hostPath}:`, err);
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async removeServer(name: string) {
|
|
175
|
+
await this.hostsManager.remove(name);
|
|
176
|
+
this.clients.delete(name);
|
|
177
|
+
this.clientIds.delete(name);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Singleton instance
|
|
182
|
+
let mongoConnections: MongoConnections | null = null;
|
|
183
|
+
let initPromise: Promise<MongoConnections> | null = null;
|
|
184
|
+
|
|
185
|
+
export async function getMongo(): Promise<MongoConnections> {
|
|
186
|
+
if (!mongoConnections) {
|
|
187
|
+
if (!initPromise) {
|
|
188
|
+
initPromise = (async () => {
|
|
189
|
+
mongoConnections = new MongoConnections();
|
|
190
|
+
await mongoConnections.initialize();
|
|
191
|
+
return mongoConnections;
|
|
192
|
+
})();
|
|
193
|
+
}
|
|
194
|
+
return initPromise;
|
|
195
|
+
}
|
|
196
|
+
return mongoConnections;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export { mongoConnections };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
|
|
3
|
+
interface Notification {
|
|
4
|
+
id: number;
|
|
5
|
+
message: string;
|
|
6
|
+
type: "success" | "error" | "info";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let notifications = $state<Notification[]>([]);
|
|
10
|
+
let nextId = 1;
|
|
11
|
+
|
|
12
|
+
export const notificationStore = {
|
|
13
|
+
get items() {
|
|
14
|
+
return notifications;
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
notify(message: string, type: "success" | "error" | "info" = "info") {
|
|
18
|
+
const id = nextId++;
|
|
19
|
+
notifications = [...notifications, { id, message, type }];
|
|
20
|
+
|
|
21
|
+
setTimeout(() => {
|
|
22
|
+
this.remove(id);
|
|
23
|
+
}, 5000);
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
async notifyError(message: string | unknown, fallbackMessage?: string) {
|
|
27
|
+
const finalMessage =
|
|
28
|
+
typeof message === "string"
|
|
29
|
+
? message
|
|
30
|
+
: (z.object({ message: z.string() }).safeParse(message).data?.message ??
|
|
31
|
+
z.object({ body: z.object({ message: z.string() }), status: z.number() }).safeParse(message).data?.body
|
|
32
|
+
?.message ??
|
|
33
|
+
fallbackMessage ??
|
|
34
|
+
"An unexpected error occurred");
|
|
35
|
+
this.notify(finalMessage, "error");
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
notifySuccess(message: string) {
|
|
39
|
+
this.notify(message, "success");
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
remove(id: number) {
|
|
43
|
+
notifications = notifications.filter((n) => n.id !== id);
|
|
44
|
+
},
|
|
45
|
+
};
|