mirror of
https://github.com/garraflavatra/rolens.git
synced 2025-01-18 21:17:59 +00:00
Started working on shell feature (WIP)
This commit is contained in:
parent
0027a4333b
commit
c284cb4cfc
57
frontend/src/components/codeeditor.svelte
Normal file
57
frontend/src/components/codeeditor.svelte
Normal 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>
|
2
frontend/src/components/icon.svelte
vendored
2
frontend/src/components/icon.svelte
vendored
@ -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" />
|
<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'}
|
{: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" />
|
<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}
|
{/if}
|
||||||
</svg>
|
</svg>
|
||||||
|
@ -1,41 +1,16 @@
|
|||||||
<script>
|
<script>
|
||||||
import { indentWithTab } from '@codemirror/commands';
|
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
import { indentOnInput } from '@codemirror/language';
|
import { onMount } from 'svelte';
|
||||||
import { EditorState } from '@codemirror/state';
|
import CodeEditor from './codeeditor.svelte';
|
||||||
import { EditorView, keymap } from '@codemirror/view';
|
|
||||||
import { basicSetup } from 'codemirror';
|
|
||||||
import { createEventDispatcher, onMount } from 'svelte';
|
|
||||||
|
|
||||||
export let text = '';
|
export let text = '';
|
||||||
export let editor = undefined;
|
export let editor = undefined;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const extensions = [
|
||||||
let editorParent;
|
|
||||||
|
|
||||||
const editorState = EditorState.create({
|
|
||||||
doc: '',
|
|
||||||
extensions: [
|
|
||||||
basicSetup,
|
|
||||||
keymap.of([ indentWithTab, indentOnInput ]),
|
|
||||||
javascript(),
|
javascript(),
|
||||||
EditorState.tabSize.of(4),
|
];
|
||||||
EditorView.updateListener.of(e => {
|
|
||||||
if (!e.docChanged) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
text = e.state.doc.toString();
|
|
||||||
dispatch('updated', { text });
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
editor = new EditorView({
|
|
||||||
parent: editorParent,
|
|
||||||
state: editorState,
|
|
||||||
});
|
|
||||||
|
|
||||||
editor.dispatch({
|
editor.dispatch({
|
||||||
changes: {
|
changes: {
|
||||||
from: 0,
|
from: 0,
|
||||||
@ -43,23 +18,7 @@
|
|||||||
insert: text,
|
insert: text,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatch('inited', { editor });
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={editorParent} class="editor"></div>
|
<CodeEditor bind:editor bind:text on:inited on:updated {extensions} />
|
||||||
|
|
||||||
<style>
|
|
||||||
.editor {
|
|
||||||
width: 100%;
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor :global(.cm-editor) {
|
|
||||||
overflow: auto;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
@ -16,6 +16,7 @@ import {
|
|||||||
DropCollection,
|
DropCollection,
|
||||||
DropDatabase,
|
DropDatabase,
|
||||||
DropIndex,
|
DropIndex,
|
||||||
|
ExecuteShellScript,
|
||||||
GetIndexes,
|
GetIndexes,
|
||||||
Hosts,
|
Hosts,
|
||||||
OpenCollection,
|
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();
|
await refresh();
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
import Indexes from './indexes.svelte';
|
import Indexes from './indexes.svelte';
|
||||||
import Insert from './insert.svelte';
|
import Insert from './insert.svelte';
|
||||||
import Remove from './remove.svelte';
|
import Remove from './remove.svelte';
|
||||||
|
import Shell from './shell.svelte';
|
||||||
import Stats from './stats.svelte';
|
import Stats from './stats.svelte';
|
||||||
import Update from './update.svelte';
|
import Update from './update.svelte';
|
||||||
|
|
||||||
@ -50,6 +51,7 @@
|
|||||||
{ key: 'remove', icon: 'trash', title: 'Remove' },
|
{ key: 'remove', icon: 'trash', title: 'Remove' },
|
||||||
{ key: 'indexes', icon: 'list', title: 'Indexes' },
|
{ key: 'indexes', icon: 'list', title: 'Indexes' },
|
||||||
{ key: 'aggregate', icon: 're', title: 'Aggregate' },
|
{ key: 'aggregate', icon: 're', title: 'Aggregate' },
|
||||||
|
{ key: 'shell', icon: 'shell', title: 'Shell' },
|
||||||
]}
|
]}
|
||||||
bind:selectedKey={tab} />
|
bind:selectedKey={tab} />
|
||||||
|
|
||||||
@ -61,6 +63,7 @@
|
|||||||
{:else if tab === 'remove'} <Remove {collection} />
|
{:else if tab === 'remove'} <Remove {collection} />
|
||||||
{:else if tab === 'indexes'} <Indexes {collection} />
|
{:else if tab === 'indexes'} <Indexes {collection} />
|
||||||
{:else if tab === 'aggregate'} <Aggregate {collection} />
|
{:else if tab === 'aggregate'} <Aggregate {collection} />
|
||||||
|
{:else if tab === 'shell'} <Shell {collection} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/key}
|
{/key}
|
||||||
|
109
frontend/src/organisms/connection/collection/shell.svelte
Normal file
109
frontend/src/organisms/connection/collection/shell.svelte
Normal 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
2
frontend/wailsjs/go/app/App.d.ts
generated
vendored
@ -20,6 +20,8 @@ export function DropIndex(arg1:string,arg2:string,arg3:string,arg4:string):Promi
|
|||||||
|
|
||||||
export function Environment():Promise<app.EnvironmentInfo>;
|
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 FindItems(arg1:string,arg2:string,arg3:string,arg4:string):Promise<app.FindItemsResult>;
|
||||||
|
|
||||||
export function GetIndexes(arg1:string,arg2:string,arg3:string):Promise<app.GetIndexesResult>;
|
export function GetIndexes(arg1:string,arg2:string,arg3:string):Promise<app.GetIndexesResult>;
|
||||||
|
4
frontend/wailsjs/go/app/App.js
generated
4
frontend/wailsjs/go/app/App.js
generated
@ -30,6 +30,10 @@ export function Environment() {
|
|||||||
return window['go']['app']['App']['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) {
|
export function FindItems(arg1, arg2, arg3, arg4) {
|
||||||
return window['go']['app']['App']['FindItems'](arg1, arg2, arg3, arg4);
|
return window['go']['app']['App']['FindItems'](arg1, arg2, arg3, arg4);
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ type EnvironmentInfo struct {
|
|||||||
|
|
||||||
HasMongoExport bool `json:"hasMongoExport"`
|
HasMongoExport bool `json:"hasMongoExport"`
|
||||||
HasMongoDump bool `json:"hasMongoDump"`
|
HasMongoDump bool `json:"hasMongoDump"`
|
||||||
|
HasMongoShell bool `json:"hasMongoShell"`
|
||||||
|
|
||||||
HomeDirectory string `json:"homeDirectory"`
|
HomeDirectory string `json:"homeDirectory"`
|
||||||
DataDirectory string `json:"dataDirectory"`
|
DataDirectory string `json:"dataDirectory"`
|
||||||
@ -50,6 +51,9 @@ func NewApp(version string) *App {
|
|||||||
_, err = exec.LookPath("mongoexport")
|
_, err = exec.LookPath("mongoexport")
|
||||||
a.Env.HasMongoExport = err == nil
|
a.Env.HasMongoExport = err == nil
|
||||||
|
|
||||||
|
_, err = exec.LookPath("mongosh")
|
||||||
|
a.Env.HasMongoShell = err == nil
|
||||||
|
|
||||||
a.Env.HomeDirectory, err = os.UserHomeDir()
|
a.Env.HomeDirectory, err = os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(errors.New("encountered an error while getting home directory"))
|
panic(errors.New("encountered an error while getting home directory"))
|
||||||
|
83
internal/app/collection_shell.go
Normal file
83
internal/app/collection_shell.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user