1
0
mirror of https://github.com/garraflavatra/rolens.git synced 2025-01-18 13:07:58 +00:00

Started working on shell feature (WIP)

This commit is contained in:
Romein van Buren 2023-06-24 10:11:32 +02:00
parent 0027a4333b
commit c284cb4cfc
Signed by: romein
GPG Key ID: 0EFF8478ADDF6C49
10 changed files with 276 additions and 47 deletions

View File

@ -0,0 +1,57 @@
<script>
import { indentWithTab } from '@codemirror/commands';
import { indentOnInput } from '@codemirror/language';
import { EditorState } from '@codemirror/state';
import { EditorView, keymap } from '@codemirror/view';
import { basicSetup } from 'codemirror';
import { createEventDispatcher, onMount } from 'svelte';
export let text = '';
export let editor = undefined;
export let extensions = [];
const dispatch = createEventDispatcher();
let editorParent;
const editorState = EditorState.create({
doc: '',
extensions: [
basicSetup,
keymap.of([ indentWithTab, indentOnInput ]),
EditorState.tabSize.of(4),
EditorView.updateListener.of(e => {
if (!e.docChanged) {
return;
}
text = e.state.doc.toString();
dispatch('updated', { text });
}),
...extensions,
],
});
onMount(() => {
editor = new EditorView({
parent: editorParent,
state: editorState,
});
dispatch('inited', { editor });
});
</script>
<div bind:this={editorParent} class="editor"></div>
<style>
.editor {
width: 100%;
background-color: #fff;
border-radius: var(--radius);
overflow: hidden;
}
.editor :global(.cm-editor) {
overflow: auto;
height: 100%;
}
</style>

View File

@ -142,5 +142,7 @@
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0zM12 9v4M12 17h.01" />
{:else if name === 'loading'}
<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" />
{:else if name === 'shell'}
<path d="m4 17 6-6-6-6M12 19h8" />
{/if}
</svg>

View File

@ -1,41 +1,16 @@
<script>
import { indentWithTab } from '@codemirror/commands';
import { javascript } from '@codemirror/lang-javascript';
import { indentOnInput } from '@codemirror/language';
import { EditorState } from '@codemirror/state';
import { EditorView, keymap } from '@codemirror/view';
import { basicSetup } from 'codemirror';
import { createEventDispatcher, onMount } from 'svelte';
import { onMount } from 'svelte';
import CodeEditor from './codeeditor.svelte';
export let text = '';
export let editor = undefined;
const dispatch = createEventDispatcher();
let editorParent;
const editorState = EditorState.create({
doc: '',
extensions: [
basicSetup,
keymap.of([ indentWithTab, indentOnInput ]),
javascript(),
EditorState.tabSize.of(4),
EditorView.updateListener.of(e => {
if (!e.docChanged) {
return;
}
text = e.state.doc.toString();
dispatch('updated', { text });
}),
],
});
const extensions = [
javascript(),
];
onMount(() => {
editor = new EditorView({
parent: editorParent,
state: editorState,
});
editor.dispatch({
changes: {
from: 0,
@ -43,23 +18,7 @@
insert: text,
},
});
dispatch('inited', { editor });
});
</script>
<div bind:this={editorParent} class="editor"></div>
<style>
.editor {
width: 100%;
background-color: #fff;
border-radius: var(--radius);
overflow: hidden;
}
.editor :global(.cm-editor) {
overflow: auto;
height: 100%;
}
</style>
<CodeEditor bind:editor bind:text on:inited on:updated {extensions} />

View File

@ -16,6 +16,7 @@ import {
DropCollection,
DropDatabase,
DropIndex,
ExecuteShellScript,
GetIndexes,
Hosts,
OpenCollection,
@ -239,6 +240,11 @@ async function refresh() {
});
});
};
collection.executeShellScript = async function(script) {
const result = await ExecuteShellScript(hostKey, dbKey, collKey, script);
return result;
};
}
await refresh();

View File

@ -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';
@ -50,6 +51,7 @@
{ key: 'remove', icon: 'trash', title: 'Remove' },
{ key: 'indexes', icon: 'list', title: 'Indexes' },
{ key: 'aggregate', icon: 're', title: 'Aggregate' },
{ key: 'shell', icon: 'shell', title: 'Shell' },
]}
bind:selectedKey={tab} />
@ -61,6 +63,7 @@
{:else if tab === 'remove'} <Remove {collection} />
{:else if tab === 'indexes'} <Indexes {collection} />
{:else if tab === 'aggregate'} <Aggregate {collection} />
{:else if tab === 'shell'} <Shell {collection} />
{/if}
</div>
{/key}

View File

@ -0,0 +1,109 @@
<script>
import BlankState from '$components/blankstate.svelte';
import CodeEditor from '$components/codeeditor.svelte';
import Icon from '$components/icon.svelte';
import { javascript } from '@codemirror/lang-javascript';
import { onDestroy } from 'svelte';
export let collection;
const extensions = [ javascript() ];
let script = '';
let result = {};
let copySucceeded = false;
let timeout;
async function run() {
result = await collection.executeShellScript(script);
}
async function copyErrorDescription() {
await navigator.clipboard.writeText(result.errorDescription);
copySucceeded = true;
timeout = setTimeout(() => copySucceeded = false, 1500);
}
onDestroy(() => clearTimeout(timeout));
</script>
<div class="shell">
<div class="panels">
<!-- svelte-ignore a11y-label-has-associated-control -->
<label class="field">
<CodeEditor bind:text={script} {extensions} />
</label>
<div class="output">
{#if result.errorTitle || result.errorDescription}
<BlankState title={result.errorTitle} label={result.errorDescription} icon="!">
<button class="button-small" on:click={copyErrorDescription}>
<Icon name={copySucceeded ? 'check' : 'clipboard'} /> Copy error message
</button>
</BlankState>
{:else}
<pre>{result.output || ''}</pre>
{/if}
</div>
</div>
<div class="controls">
{#key result}
<div class="status flash-green">
{#if result?.status}
Exit code: {result.status}
{/if}
</div>
{/key}
<button class="btn" on:click={run}>
<Icon name="play" /> Run
</button>
</div>
</div>
<style>
.shell {
display: grid;
grid-template: 1fr auto / 1fr;
}
.panels {
display: flex;
height: 100%;
}
.panels > * {
flex: 1 1 0;
width: 100%;
}
.field :global(.editor) {
border-radius: 0;
}
.output {
background-color: #111;
color: #fff;
overflow: auto;
display: flex;
}
.output :global(*) {
color: #fff;
}
.output pre {
font-family: monospace;
padding: 0.5rem;
}
.output :global(.blankstate) {
margin: auto;
padding: 0.5rem;
}
.controls {
margin-top: 0.5rem;
display: flex;
align-items: center;
}
.controls .status {
margin-right: auto;
}
</style>

2
frontend/wailsjs/go/app/App.d.ts generated vendored
View File

@ -20,6 +20,8 @@ export function DropIndex(arg1:string,arg2:string,arg3:string,arg4:string):Promi
export function Environment():Promise<app.EnvironmentInfo>;
export function ExecuteShellScript(arg1:string,arg2:string,arg3:string,arg4:string):Promise<app.ExecuteShellScriptResult>;
export function FindItems(arg1:string,arg2:string,arg3:string,arg4:string):Promise<app.FindItemsResult>;
export function GetIndexes(arg1:string,arg2:string,arg3:string):Promise<app.GetIndexesResult>;

View File

@ -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);
}

View File

@ -26,6 +26,7 @@ type EnvironmentInfo struct {
HasMongoExport bool `json:"hasMongoExport"`
HasMongoDump bool `json:"hasMongoDump"`
HasMongoShell bool `json:"hasMongoShell"`
HomeDirectory string `json:"homeDirectory"`
DataDirectory string `json:"dataDirectory"`
@ -50,6 +51,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"))

View File

@ -0,0 +1,83 @@
package app
import (
"fmt"
"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.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
}
script = fmt.Sprintf("db = connect('%s');\n\n%s", host.URI, 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)
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
}
result.Output = string(stdout)
return
}