diff --git a/frontend/src/components/codeeditor.svelte b/frontend/src/components/codeeditor.svelte new file mode 100644 index 0000000..262eaea --- /dev/null +++ b/frontend/src/components/codeeditor.svelte @@ -0,0 +1,57 @@ + + +
+ + diff --git a/frontend/src/components/icon.svelte b/frontend/src/components/icon.svelte index 645a996..c5e2ff6 100644 --- a/frontend/src/components/icon.svelte +++ b/frontend/src/components/icon.svelte @@ -147,6 +147,8 @@ {:else if name === 'loading'} + {:else if name === 'shell'} + {:else if name === 'doc'} {/if} diff --git a/frontend/src/components/objecteditor.svelte b/frontend/src/components/objecteditor.svelte index 13e7494..4a6b4db 100644 --- a/frontend/src/components/objecteditor.svelte +++ b/frontend/src/components/objecteditor.svelte @@ -1,43 +1,17 @@ -
- - + diff --git a/frontend/src/lib/stores/hosttree.js b/frontend/src/lib/stores/hosttree.js index b3f6940..b54a608 100644 --- a/frontend/src/lib/stores/hosttree.js +++ b/frontend/src/lib/stores/hosttree.js @@ -16,6 +16,7 @@ import { DropCollection, DropDatabase, DropIndex, + ExecuteShellScript, GetIndexes, HostLogs, Hosts, @@ -224,6 +225,11 @@ async function refresh() { }); }); }; + + collection.executeShellScript = async function(script) { + const result = await ExecuteShellScript(hostKey, dbKey, collKey, script); + return result; + }; } await refresh(); @@ -262,11 +268,21 @@ async function refresh() { await database.open(); } }; + + database.executeShellScript = async function(script) { + const result = await ExecuteShellScript(hostKey, dbKey, '', script); + return result; + }; } await refresh(); }; + host.executeShellScript = async function(script) { + const result = await ExecuteShellScript(hostKey, '', '', script); + return result; + }; + host.newDatabase = async function() { const name = await dialogs.enterText('Create a database', 'Enter the database name. Note: databases in MongoDB do not exist until they have a collection and an item. Your new database will not persist on the server; fill it to have it created.', ''); if (name) { diff --git a/frontend/src/organisms/connection/collection/index.svelte b/frontend/src/organisms/connection/collection/index.svelte index 0d82563..6ab9c37 100644 --- a/frontend/src/organisms/connection/collection/index.svelte +++ b/frontend/src/organisms/connection/collection/index.svelte @@ -9,6 +9,7 @@ import Indexes from './indexes.svelte'; import Insert from './insert.svelte'; import Remove from './remove.svelte'; + import Shell from '../shell.svelte'; import Stats from './stats.svelte'; import Update from './update.svelte'; @@ -42,16 +43,19 @@
{#if collection} {#key collection} - +
{#if tab === 'stats'} @@ -61,6 +65,7 @@ {:else if tab === 'remove'} {:else if tab === 'indexes'} {:else if tab === 'aggregate'} + {:else if tab === 'shell'} {/if}
{/key} diff --git a/frontend/src/organisms/connection/database/index.svelte b/frontend/src/organisms/connection/database/index.svelte index c5f13ea..b3d37eb 100644 --- a/frontend/src/organisms/connection/database/index.svelte +++ b/frontend/src/organisms/connection/database/index.svelte @@ -2,6 +2,8 @@ import BlankState from '$components/blankstate.svelte'; import TabBar from '$components/tabbar.svelte'; import { EventsOn } from '$wails/runtime/runtime'; + + import Shell from '../shell.svelte'; import Stats from './stats.svelte'; export let database; @@ -24,9 +26,15 @@
{#if database} {#key database} - +
{#if tab === 'stats'} + {:else if tab === 'shell'} {/if}
{/key} diff --git a/frontend/src/organisms/connection/host/index.svelte b/frontend/src/organisms/connection/host/index.svelte index f46a37d..3da1b4a 100644 --- a/frontend/src/organisms/connection/host/index.svelte +++ b/frontend/src/organisms/connection/host/index.svelte @@ -4,6 +4,7 @@ import { EventsOn } from '$wails/runtime/runtime'; import Logs from './logs.svelte'; + import Shell from '../shell.svelte'; import Status from './status.svelte'; import SystemInfo from './systeminfo.svelte'; @@ -28,6 +29,7 @@ {:else if tab === 'logs'} {:else if tab === 'systemInfo'} + {:else if tab === 'shell'} {/if}
{/key} diff --git a/frontend/src/organisms/connection/shell.svelte b/frontend/src/organisms/connection/shell.svelte new file mode 100644 index 0000000..3e4d62f --- /dev/null +++ b/frontend/src/organisms/connection/shell.svelte @@ -0,0 +1,146 @@ + + +
+
+ + +
+ +
+ {#if busy} + + {:else if result.errorTitle || result.errorDescription} + + + + {:else} +
{result.output || ''}
+ {/if} +
+ +
+ {#key result} +
+ {#if result?.status} + Exit code: {result.status} + {/if} +
+ {/key} + + +
+
+ + diff --git a/frontend/wailsjs/go/app/App.d.ts b/frontend/wailsjs/go/app/App.d.ts index ecd015c..565eb1a 100755 --- a/frontend/wailsjs/go/app/App.d.ts +++ b/frontend/wailsjs/go/app/App.d.ts @@ -20,6 +20,8 @@ export function DropIndex(arg1:string,arg2:string,arg3:string,arg4:string):Promi export function Environment():Promise; +export function ExecuteShellScript(arg1:string,arg2:string,arg3:string,arg4:string):Promise; + export function FindItems(arg1:string,arg2:string,arg3:string,arg4:string):Promise; export function GetIndexes(arg1:string,arg2:string,arg3:string):Promise; diff --git a/frontend/wailsjs/go/app/App.js b/frontend/wailsjs/go/app/App.js index a2548f1..aee0d09 100755 --- a/frontend/wailsjs/go/app/App.js +++ b/frontend/wailsjs/go/app/App.js @@ -30,6 +30,10 @@ export function Environment() { return window['go']['app']['App']['Environment'](); } +export function ExecuteShellScript(arg1, arg2, arg3, arg4) { + return window['go']['app']['App']['ExecuteShellScript'](arg1, arg2, arg3, arg4); +} + export function FindItems(arg1, arg2, arg3, arg4) { return window['go']['app']['App']['FindItems'](arg1, arg2, arg3, arg4); } diff --git a/internal/app/app.go b/internal/app/app.go index 4ea2f99..096b039 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -25,6 +25,7 @@ type EnvironmentInfo struct { HasMongoExport bool `json:"hasMongoExport"` HasMongoDump bool `json:"hasMongoDump"` + HasMongoShell bool `json:"hasMongoShell"` HomeDirectory string `json:"homeDirectory"` DataDirectory string `json:"dataDirectory"` @@ -49,6 +50,9 @@ func NewApp(version string) *App { _, err = exec.LookPath("mongoexport") a.Env.HasMongoExport = err == nil + _, err = exec.LookPath("mongosh") + a.Env.HasMongoShell = err == nil + a.Env.HomeDirectory, err = os.UserHomeDir() if err != nil { panic(errors.New("encountered an error while getting home directory")) diff --git a/internal/app/host_shell.go b/internal/app/host_shell.go new file mode 100644 index 0000000..9b27c07 --- /dev/null +++ b/internal/app/host_shell.go @@ -0,0 +1,105 @@ +package app + +import ( + "fmt" + "net/url" + "os" + "os/exec" + "path" + + "github.com/google/uuid" + "github.com/wailsapp/wails/v2/pkg/runtime" +) + +type ExecuteShellScriptResult struct { + Output string `json:"output"` + Status int `json:"status"` + ErrorTitle string `json:"errorTitle"` + ErrorDescription string `json:"errorDescription"` +} + +func (a *App) ExecuteShellScript(hostKey, dbKey, collKey, script string) (result ExecuteShellScriptResult) { + if !a.Env.HasMongoShell { + result.ErrorTitle = "mongosh not found" + result.ErrorDescription = "The mongosh executable is required to run a shell script. Please see https://www.mongodb.com/docs/mongodb-shell/install/" + return + } + + hosts, err := a.Hosts() + if err != nil { + result.ErrorTitle = "Could not get hosts" + result.ErrorDescription = err.Error() + return + } + + host, hostFound := hosts[hostKey] + if !hostFound { + result.ErrorTitle = "The specified host does not seem to exist" + return + } + + id, err := uuid.NewRandom() + if err != nil { + runtime.LogErrorf(a.ctx, "Shell: failed to generate a UUID: %s", err.Error()) + result.ErrorTitle = "Could not generate UUID" + result.ErrorDescription = err.Error() + return + } + + dirname := path.Join(a.Env.DataDirectory, "Shell Scripts") + fname := path.Join(dirname, fmt.Sprintf("%s.mongosh.js", id.String())) + + if err := os.MkdirAll(dirname, os.ModePerm); err != nil { + runtime.LogWarningf(a.ctx, "Shell: failed to mkdir %s", err.Error()) + result.ErrorTitle = "Could not create temporary directory" + result.ErrorDescription = err.Error() + return + } + + scriptHeader := fmt.Sprintf("// Namespace: %s.%s\n", dbKey, collKey) + + if dbKey != "" { + url, err := url.Parse(host.URI) + if err != nil { + runtime.LogWarningf(a.ctx, "Shell: failed to parse host URI %s: %s", host.URI, err.Error()) + result.ErrorTitle = "Could parse host URI" + result.ErrorDescription = err.Error() + return + } + + url.Path = "/" + dbKey + scriptHeader = scriptHeader + fmt.Sprintf("db = connect('%s');\n", url.String()) + } + + if collKey != "" { + scriptHeader = scriptHeader + fmt.Sprintf("coll = db.getCollection('%s');\n", collKey) + } + + scriptHeader = scriptHeader + "\n// Start of user script\n" + script = scriptHeader + script + + if err := os.WriteFile(fname, []byte(script), os.ModePerm); err != nil { + runtime.LogWarningf(a.ctx, "Shell: failed to write script to %s", err.Error()) + result.ErrorTitle = "Could not create temporary script file" + result.ErrorDescription = err.Error() + return + } + + cmd := exec.Command("mongosh", "--file", fname, host.URI) + stdout, err := cmd.Output() + + if exiterr, ok := err.(*exec.ExitError); ok { + result.Status = exiterr.ExitCode() + } else if err != nil { + runtime.LogWarningf(a.ctx, "Shell: failed to execute: mongosh --file %s: %s", fname, err.Error()) + result.ErrorTitle = "Could not execute script" + result.ErrorDescription = err.Error() + return + } else { + result.Status = 0 + } + + os.Remove(fname) + result.Output = string(stdout) + return +}