mirror of
https://github.com/garraflavatra/rolens.git
synced 2025-06-29 14:05:12 +00:00
Compare commits
14 Commits
v0.2.2
...
shell_quer
Author | SHA1 | Date | |
---|---|---|---|
0965344f35
|
|||
62f4b88ea6
|
|||
04221bf63f | |||
24b0df95df
|
|||
0b9f23365b
|
|||
84f1b85356
|
|||
86639f766d
|
|||
d1ce48c773
|
|||
b0d3e31378
|
|||
48b26c9df5
|
|||
958a0197a4
|
|||
3893c8dd06
|
|||
51897adf8d
|
|||
c284cb4cfc
|
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -73,7 +73,7 @@ jobs:
|
|||||||
path: releases/*
|
path: releases/*
|
||||||
|
|
||||||
- name: Test build script for users
|
- name: Test build script for users
|
||||||
run: ./build.js
|
run: node ./build.js
|
||||||
|
|
||||||
bundle:
|
bundle:
|
||||||
name: Bundle artifacts
|
name: Bundle artifacts
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
* Added log view (#53, #54).
|
||||||
|
|
||||||
## [v0.2.2]
|
## [v0.2.2]
|
||||||
|
|
||||||
* Added Excel export format (#46).
|
* Added Excel export format (#46).
|
||||||
|
@ -36,7 +36,7 @@ You can obtain a pre-compiled Rolens binary for macOS or installer for Windows f
|
|||||||
|
|
||||||
### Compiling from source
|
### Compiling from source
|
||||||
|
|
||||||
If you have Node.js installed, just download the source from GitHub, and run `./build.js`. The install script will check that dependencies are present and build Rolens for you.
|
If you have Node.js installed, just download the source from GitHub, and run `node ./build.js`. The install script will check that dependencies are present and build Rolens for you.
|
||||||
|
|
||||||
If you want to build it yourself, please refer to the [advanced build process documentation](https://garraflavatra.github.io/rolens/development/advanced-build/) for detailed compilation instructions.
|
If you want to build it yourself, please refer to the [advanced build process documentation](https://garraflavatra.github.io/rolens/development/advanced-build/) for detailed compilation instructions.
|
||||||
|
|
||||||
|
28
build.js
28
build.js
@ -52,32 +52,43 @@ function isNullish(val) {
|
|||||||
|
|
||||||
// Check that Go ^1.18 is installed.
|
// Check that Go ^1.18 is installed.
|
||||||
|
|
||||||
|
try {
|
||||||
const goMinorVersion = /go1\.([0-9][0-9])/.exec(
|
const goMinorVersion = /go1\.([0-9][0-9])/.exec(
|
||||||
execSync('go version').toString()
|
execSync('go version').toString()
|
||||||
)?.pop();
|
)?.pop();
|
||||||
|
|
||||||
|
|
||||||
if (isNullish(goMinorVersion) || (parseInt(goMinorVersion) < 18)) {
|
if (isNullish(goMinorVersion) || (parseInt(goMinorVersion) < 18)) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
missingDependencies.push({ name: 'Go ^1.18 ^16', url: 'https://go.dev/doc/install' });
|
missingDependencies.push({ name: 'Go ^1.18 ^16', url: 'https://go.dev/doc/install' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that Node.js ^16 is installed.
|
// Check that Node.js ^16 is installed.
|
||||||
|
|
||||||
|
try {
|
||||||
const nodeMajorVersion = /v([0-9]{1,2})\.[0-9]{1,3}\.[0-9]{1,3}/.exec(
|
const nodeMajorVersion = /v([0-9]{1,2})\.[0-9]{1,3}\.[0-9]{1,3}/.exec(
|
||||||
execSync('node --version').toString()
|
execSync('node --version').toString()
|
||||||
)?.pop();
|
)?.pop();
|
||||||
|
|
||||||
if (isNullish(nodeMajorVersion) || (parseInt(nodeMajorVersion) < 16)) {
|
if (isNullish(nodeMajorVersion) || (parseInt(nodeMajorVersion) < 16)) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
missingDependencies.push({ name: 'Node.js ^16', url: 'https://go.dev/doc/install' });
|
missingDependencies.push({ name: 'Node.js ^16', url: 'https://go.dev/doc/install' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that Wails is installed.
|
// Check that Wails is installed.
|
||||||
|
|
||||||
|
try {
|
||||||
const wailsMinorVersion = /v2\.([0-9])\.[0-9]/.exec(
|
const wailsMinorVersion = /v2\.([0-9])\.[0-9]/.exec(
|
||||||
execSync('wails version').toString()
|
execSync('wails version').toString()
|
||||||
)?.pop();
|
)?.pop();
|
||||||
|
|
||||||
if (isNullish(wailsMinorVersion) || (parseInt(wailsMinorVersion) < 3)) {
|
if (isNullish(wailsMinorVersion) || (parseInt(wailsMinorVersion) < 3)) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
missingDependencies.push({
|
missingDependencies.push({
|
||||||
name: 'Wails ^2.3',
|
name: 'Wails ^2.3',
|
||||||
command: 'go install github.com/wailsapp/wails/v2/cmd/wails@latest',
|
command: 'go install github.com/wailsapp/wails/v2/cmd/wails@latest',
|
||||||
@ -88,9 +99,18 @@ if (isNullish(wailsMinorVersion) || (parseInt(wailsMinorVersion) < 3)) {
|
|||||||
// Check that NSIS is installed on Windows.
|
// Check that NSIS is installed on Windows.
|
||||||
|
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
|
try {
|
||||||
const nsisInstalled = /v3\.([0-9][0-9])/.test(execSync('makensis.exe /VERSION').toString());
|
const nsisInstalled = /v3\.([0-9][0-9])/.test(execSync('makensis.exe /VERSION').toString());
|
||||||
if (!nsisInstalled) {
|
if (!nsisInstalled) {
|
||||||
missingDependencies.push({ name: 'Nullsoft Install System ^3', url: 'https://nsis.sourceforge.io/Download' });
|
throw new Error();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
missingDependencies.push({
|
||||||
|
name: 'Nullsoft Install System ^3',
|
||||||
|
command: 'choco install nsis',
|
||||||
|
url: 'https://nsis.sourceforge.io/Download',
|
||||||
|
comment: 'Note: you should add makensis.exe to your path:\n setx /M PATH "%PATH%;C:\\Program Files (x86)\\NSIS\\Bin"'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,6 +132,10 @@ if (missingDependencies.length > 0) {
|
|||||||
console.log(' Visit the following page for more information:');
|
console.log(' Visit the following page for more information:');
|
||||||
console.log(` ${dependency.url}`);
|
console.log(` ${dependency.url}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dependency.comment) {
|
||||||
|
console.log(` ${dependency.comment}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
@ -23,7 +23,7 @@ If you use a Linux-based OS, please continue reading.
|
|||||||
|
|
||||||
Rolens is free and open-source software, which means that you can compile it from source on your own machine by cloning [the repository](https://github.com/garraflavatra/rolens).
|
Rolens is free and open-source software, which means that you can compile it from source on your own machine by cloning [the repository](https://github.com/garraflavatra/rolens).
|
||||||
|
|
||||||
If you have Node.js installed, just download the source from GitHub, and run `./build.js`. The install script will check that dependencies are present and build Rolens for you. If you want to build it yourself, please continue reading.
|
If you have Node.js installed, just download the source from GitHub, and run `node ./build.js`. The install script will check that dependencies are present and build Rolens for you. If you want to build it yourself, please continue reading.
|
||||||
|
|
||||||
## Advanced build
|
## Advanced build
|
||||||
|
|
||||||
|
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>
|
4
frontend/src/components/icon.svelte
vendored
4
frontend/src/components/icon.svelte
vendored
@ -147,5 +147,9 @@
|
|||||||
<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" />
|
||||||
|
{:else if name === 'doc'}
|
||||||
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><path d="M14 2v6h6M16 13H8M16 17H8M10 9H8" />
|
||||||
{/if}
|
{/if}
|
||||||
</svg>
|
</svg>
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { Beep } from '$wails/go/ui/UI';
|
import { Beep } from '$wails/go/ui/UI';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import { fade, fly } from 'svelte/transition';
|
|
||||||
import Icon from './icon.svelte';
|
import Icon from './icon.svelte';
|
||||||
|
|
||||||
export let show = true;
|
export let show = true;
|
||||||
@ -43,8 +42,8 @@
|
|||||||
<svelte:window on:keydown={keydown} />
|
<svelte:window on:keydown={keydown} />
|
||||||
|
|
||||||
{#if show}
|
{#if show}
|
||||||
<div class="modal outer" transition:fade on:pointerdown|self={Beep}>
|
<div class="modal outer" on:pointerdown|self={Beep}>
|
||||||
<div class="inner" style:max-width={width || '80vw'} transition:fly={{ y: -100 }}>
|
<div class="inner" style:max-width={width || '80vw'}>
|
||||||
{#if title}
|
{#if title}
|
||||||
<header>
|
<header>
|
||||||
<div class="title">{title}</div>
|
<div class="title">{title}</div>
|
||||||
|
@ -1,41 +1,17 @@
|
|||||||
<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;
|
||||||
|
export let readonly = false;
|
||||||
|
|
||||||
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 +19,13 @@
|
|||||||
insert: text,
|
insert: text,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatch('inited', { editor });
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={editorParent} class="editor"></div>
|
<CodeEditor bind:editor
|
||||||
|
bind:text
|
||||||
<style>
|
on:inited
|
||||||
.editor {
|
on:updated
|
||||||
width: 100%;
|
{extensions}
|
||||||
background-color: #fff;
|
{readonly}
|
||||||
border-radius: var(--radius);
|
/>
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor :global(.cm-editor) {
|
|
||||||
overflow: auto;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
@ -11,15 +11,16 @@
|
|||||||
export let errorTitle = '';
|
export let errorTitle = '';
|
||||||
export let errorDescription = '';
|
export let errorDescription = '';
|
||||||
export let busy = false;
|
export let busy = false;
|
||||||
|
export let showTypes = true;
|
||||||
const columns = [
|
|
||||||
{ key: 'key', label: 'Key' },
|
|
||||||
{ key: 'value', label: 'Value' },
|
|
||||||
{ key: 'type', label: 'Type' },
|
|
||||||
];
|
|
||||||
|
|
||||||
let items = [];
|
let items = [];
|
||||||
|
|
||||||
|
$: columns = [
|
||||||
|
{ key: 'key', label: 'Key' },
|
||||||
|
{ key: 'value', label: 'Value' },
|
||||||
|
showTypes ? { key: 'type', label: 'Type' } : undefined,
|
||||||
|
].filter(c => !!c);
|
||||||
|
|
||||||
$: if (data) {
|
$: if (data) {
|
||||||
// items = dissectObject(data).map(item => ({ ...item, menu: getRootMenu(item.key, item) }));
|
// items = dissectObject(data).map(item => ({ ...item, menu: getRootMenu(item.key, item) }));
|
||||||
items = [];
|
items = [];
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
import ObjectEditor from './objecteditor.svelte';
|
import ObjectEditor from './objecteditor.svelte';
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
|
export let readonly = false;
|
||||||
export let saveable = false;
|
export let saveable = false;
|
||||||
export let successMessage = '';
|
export let successMessage = '';
|
||||||
|
|
||||||
@ -37,7 +38,7 @@
|
|||||||
{#if data}
|
{#if data}
|
||||||
<Modal bind:show={data} contentPadding={false}>
|
<Modal bind:show={data} contentPadding={false}>
|
||||||
<div class="objectviewer">
|
<div class="objectviewer">
|
||||||
<ObjectEditor bind:text on:updated={() => successMessage = ''} />
|
<ObjectEditor bind:text on:updated={() => successMessage = ''} {readonly} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<svelte:fragment slot="footer">
|
<svelte:fragment slot="footer">
|
||||||
|
@ -2,8 +2,10 @@ import { ObjectId } from 'bson';
|
|||||||
import aggregationStages from './aggregation-stages.json';
|
import aggregationStages from './aggregation-stages.json';
|
||||||
import atomicUpdateOperators from './atomic-update-operators.json';
|
import atomicUpdateOperators from './atomic-update-operators.json';
|
||||||
import locales from './locales.json';
|
import locales from './locales.json';
|
||||||
|
import logComponents from './log-components.json';
|
||||||
|
import logLevels from './loglevels.json';
|
||||||
|
|
||||||
export { aggregationStages, atomicUpdateOperators, locales };
|
export { aggregationStages, atomicUpdateOperators, locales, logComponents, logLevels };
|
||||||
|
|
||||||
// Calculate the min and max values of (un)signed integers with n bits
|
// Calculate the min and max values of (un)signed integers with n bits
|
||||||
export const intMin = bits => Math.pow(2, bits - 1) * -1;
|
export const intMin = bits => Math.pow(2, bits - 1) * -1;
|
||||||
|
36
frontend/src/lib/mongo/log-components.json
Normal file
36
frontend/src/lib/mongo/log-components.json
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
[
|
||||||
|
"ACCESS",
|
||||||
|
"COMMAND",
|
||||||
|
"CONTROL",
|
||||||
|
"ELECTION",
|
||||||
|
"FTDC",
|
||||||
|
"GEO",
|
||||||
|
"INDEX",
|
||||||
|
"INITSYNC",
|
||||||
|
"JOURNAL",
|
||||||
|
"NETWORK",
|
||||||
|
"QUERY",
|
||||||
|
"RECOVERY",
|
||||||
|
"REPL",
|
||||||
|
"REPL_HB",
|
||||||
|
"ROLLBACK",
|
||||||
|
"SHARDING",
|
||||||
|
"STORAGE",
|
||||||
|
"TXN",
|
||||||
|
"WRITE",
|
||||||
|
"WT",
|
||||||
|
"WTBACKUP",
|
||||||
|
"WTCHKPT",
|
||||||
|
"WTCMPCT",
|
||||||
|
"WTEVICT",
|
||||||
|
"WTHS",
|
||||||
|
"WTRECOV",
|
||||||
|
"WTRTS",
|
||||||
|
"WTSLVG",
|
||||||
|
"WTTIER",
|
||||||
|
"WTTS",
|
||||||
|
"WTTXN",
|
||||||
|
"WTVRFY",
|
||||||
|
"WTWRTLOG",
|
||||||
|
"-"
|
||||||
|
]
|
7
frontend/src/lib/mongo/loglevels.json
Normal file
7
frontend/src/lib/mongo/loglevels.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"F": "Fatal",
|
||||||
|
"E": "Error",
|
||||||
|
"W": "Warning",
|
||||||
|
"I": "Info",
|
||||||
|
"D": "Debug"
|
||||||
|
}
|
@ -16,7 +16,9 @@ import {
|
|||||||
DropCollection,
|
DropCollection,
|
||||||
DropDatabase,
|
DropDatabase,
|
||||||
DropIndex,
|
DropIndex,
|
||||||
|
ExecuteShellScript,
|
||||||
GetIndexes,
|
GetIndexes,
|
||||||
|
HostLogs,
|
||||||
Hosts,
|
Hosts,
|
||||||
OpenCollection,
|
OpenCollection,
|
||||||
OpenConnection,
|
OpenConnection,
|
||||||
@ -223,6 +225,11 @@ async function refresh() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
collection.executeShellScript = async function(script) {
|
||||||
|
const result = await ExecuteShellScript(hostKey, dbKey, collKey, script);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await refresh();
|
await refresh();
|
||||||
@ -261,8 +268,21 @@ async function refresh() {
|
|||||||
await database.open();
|
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() {
|
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.', '');
|
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) {
|
if (name) {
|
||||||
@ -271,9 +291,6 @@ async function refresh() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
await refresh();
|
|
||||||
};
|
|
||||||
|
|
||||||
host.edit = async function() {
|
host.edit = async function() {
|
||||||
const dialog = dialogs.new(HostDetailDialog, { hostKey });
|
const dialog = dialogs.new(HostDetailDialog, { hostKey });
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
@ -283,6 +300,10 @@ async function refresh() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
host.getLogs = async function(filter = 'global') {
|
||||||
|
return await HostLogs(hostKey, filter);
|
||||||
|
};
|
||||||
|
|
||||||
host.remove = async function() {
|
host.remove = async function() {
|
||||||
await RemoveHost(hostKey);
|
await RemoveHost(hostKey);
|
||||||
await refresh();
|
await refresh();
|
||||||
|
@ -164,7 +164,7 @@
|
|||||||
|
|
||||||
<div class="find">
|
<div class="find">
|
||||||
<form on:submit|preventDefault={submitQuery}>
|
<form on:submit|preventDefault={submitQuery}>
|
||||||
<div class="form-row one">
|
<div class="formrow one">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span class="label">Query or id</span>
|
<span class="label">Query or id</span>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
@ -192,7 +192,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row two">
|
<div class="formrow two">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span class="label">Fields</span>
|
<span class="label">Fields</span>
|
||||||
<input
|
<input
|
||||||
@ -229,7 +229,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row actions">
|
<div class="formrow actions">
|
||||||
<button type="submit" class="button" title="Run query">
|
<button type="submit" class="button" title="Run query">
|
||||||
<Icon name="play" /> Run
|
<Icon name="play" /> Run
|
||||||
</button>
|
</button>
|
||||||
@ -342,18 +342,18 @@
|
|||||||
grid-template: auto 1fr / 1fr;
|
grid-template: auto 1fr / 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-row {
|
.formrow {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
.form-row.one {
|
.formrow.one {
|
||||||
grid-template: 1fr / 3fr 2fr;
|
grid-template: 1fr / 3fr 2fr;
|
||||||
}
|
}
|
||||||
.form-row.two {
|
.formrow.two {
|
||||||
grid-template: 1fr / 5fr 1fr 1fr;
|
grid-template: 1fr / 5fr 1fr 1fr;
|
||||||
}
|
}
|
||||||
.form-row.actions {
|
.formrow.actions {
|
||||||
margin-bottom: 0rem;
|
margin-bottom: 0rem;
|
||||||
grid-template: 1fr / repeat(4, auto);
|
grid-template: 1fr / repeat(4, auto);
|
||||||
justify-content: start;
|
justify-content: start;
|
||||||
|
@ -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';
|
||||||
|
|
||||||
@ -42,7 +43,8 @@
|
|||||||
<div class="view" class:empty={!collection}>
|
<div class="view" class:empty={!collection}>
|
||||||
{#if collection}
|
{#if collection}
|
||||||
{#key collection}
|
{#key collection}
|
||||||
<TabBar tabs={[
|
<TabBar
|
||||||
|
tabs={[
|
||||||
{ key: 'stats', icon: 'chart', title: 'Stats' },
|
{ key: 'stats', icon: 'chart', title: 'Stats' },
|
||||||
{ key: 'find', icon: 'db', title: 'Find' },
|
{ key: 'find', icon: 'db', title: 'Find' },
|
||||||
{ key: 'insert', icon: '+', title: 'Insert' },
|
{ key: 'insert', icon: '+', title: 'Insert' },
|
||||||
@ -50,8 +52,10 @@
|
|||||||
{ 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}
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{#if tab === 'stats'} <Stats {collection} />
|
{#if tab === 'stats'} <Stats {collection} />
|
||||||
@ -61,6 +65,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}
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
<div class="grid">
|
<div class="grid">
|
||||||
<ObjectGrid
|
<ObjectGrid
|
||||||
data={collection.stats}
|
data={collection.stats}
|
||||||
|
showTypes={false}
|
||||||
errorTitle={collection.statsError ? 'Error fetching collection stats' : ''}
|
errorTitle={collection.statsError ? 'Error fetching collection stats' : ''}
|
||||||
errorDescription={collection.statsError}
|
errorDescription={collection.statsError}
|
||||||
busy={!collection.stats && !collection.statsError && `Fetching stats for ${collection.key}…`}
|
busy={!collection.stats && !collection.statsError && `Fetching stats for ${collection.key}…`}
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
import BlankState from '$components/blankstate.svelte';
|
import BlankState from '$components/blankstate.svelte';
|
||||||
import TabBar from '$components/tabbar.svelte';
|
import TabBar from '$components/tabbar.svelte';
|
||||||
import { EventsOn } from '$wails/runtime/runtime';
|
import { EventsOn } from '$wails/runtime/runtime';
|
||||||
|
|
||||||
|
import Shell from '../shell.svelte';
|
||||||
import Stats from './stats.svelte';
|
import Stats from './stats.svelte';
|
||||||
|
|
||||||
export let database;
|
export let database;
|
||||||
@ -24,9 +26,15 @@
|
|||||||
<div class="view" class:empty={!database}>
|
<div class="view" class:empty={!database}>
|
||||||
{#if database}
|
{#if database}
|
||||||
{#key database}
|
{#key database}
|
||||||
<TabBar tabs={[ { key: 'stats', icon: 'chart', title: 'Database stats' } ]} bind:selectedKey={tab} />
|
<TabBar
|
||||||
|
tabs={[
|
||||||
|
{ key: 'stats', icon: 'chart', title: 'Database stats' },
|
||||||
|
{ key: 'shell', icon: 'shell', title: 'Shell' },
|
||||||
|
]}
|
||||||
|
bind:selectedKey={tab} />
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{#if tab === 'stats'} <Stats {database} />
|
{#if tab === 'stats'} <Stats {database} />
|
||||||
|
{:else if tab === 'shell'} <Shell {database} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/key}
|
{/key}
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
<div class="grid">
|
<div class="grid">
|
||||||
<ObjectGrid
|
<ObjectGrid
|
||||||
data={database.stats}
|
data={database.stats}
|
||||||
|
showTypes={false}
|
||||||
errorTitle={database.statsError ? 'Error fetching database stats' : ''}
|
errorTitle={database.statsError ? 'Error fetching database stats' : ''}
|
||||||
errorDescription={database.statsError}
|
errorDescription={database.statsError}
|
||||||
busy={!database.stats && !database.statsError && `Fetching stats for ${database.key}…`}
|
busy={!database.stats && !database.statsError && `Fetching stats for ${database.key}…`}
|
||||||
|
@ -2,6 +2,9 @@
|
|||||||
import BlankState from '$components/blankstate.svelte';
|
import BlankState from '$components/blankstate.svelte';
|
||||||
import TabBar from '$components/tabbar.svelte';
|
import TabBar from '$components/tabbar.svelte';
|
||||||
import { EventsOn } from '$wails/runtime/runtime';
|
import { EventsOn } from '$wails/runtime/runtime';
|
||||||
|
|
||||||
|
import Logs from './logs.svelte';
|
||||||
|
import Shell from '../shell.svelte';
|
||||||
import Status from './status.svelte';
|
import Status from './status.svelte';
|
||||||
import SystemInfo from './systeminfo.svelte';
|
import SystemInfo from './systeminfo.svelte';
|
||||||
|
|
||||||
@ -23,15 +26,21 @@
|
|||||||
<div class="view" class:empty={!host}>
|
<div class="view" class:empty={!host}>
|
||||||
{#if host}
|
{#if host}
|
||||||
{#key host}
|
{#key host}
|
||||||
<TabBar tabs={[
|
<TabBar
|
||||||
|
tabs={[
|
||||||
{ key: 'status', icon: 'chart', title: 'Host status' },
|
{ key: 'status', icon: 'chart', title: 'Host status' },
|
||||||
|
{ key: 'shell', icon: 'shell', title: 'Shell' },
|
||||||
|
{ key: 'logs', icon: 'doc', title: 'Logs' },
|
||||||
{ key: 'systemInfo', icon: 'server', title: 'System info' },
|
{ key: 'systemInfo', icon: 'server', title: 'System info' },
|
||||||
]}
|
]}
|
||||||
bind:selectedKey={tab} />
|
bind:selectedKey={tab}
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{#if tab === 'status'} <Status {host} />
|
{#if tab === 'status'} <Status {host} />
|
||||||
|
{:else if tab === 'logs'} <Logs {host} />
|
||||||
{:else if tab === 'systemInfo'} <SystemInfo {host} />
|
{:else if tab === 'systemInfo'} <SystemInfo {host} />
|
||||||
|
{:else if tab === 'shell'} <Shell {host} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/key}
|
{/key}
|
||||||
|
194
frontend/src/organisms/connection/host/logs.svelte
Normal file
194
frontend/src/organisms/connection/host/logs.svelte
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
<script>
|
||||||
|
import Grid from '$components/grid.svelte';
|
||||||
|
import Icon from '$components/icon.svelte';
|
||||||
|
import ObjectViewer from '$components/objectviewer.svelte';
|
||||||
|
import input from '$lib/actions/input';
|
||||||
|
import { logComponents, logLevels } from '$lib/mongo';
|
||||||
|
import { BrowserOpenURL } from '$wails/runtime/runtime';
|
||||||
|
import { onDestroy } from 'svelte';
|
||||||
|
|
||||||
|
export let host;
|
||||||
|
|
||||||
|
const autoReloadIntervals = [ 1, 2, 5, 10, 30, 60 ];
|
||||||
|
let filter = 'global';
|
||||||
|
let severityFilter = '';
|
||||||
|
let componentFilter = '';
|
||||||
|
let logs;
|
||||||
|
let total = 0;
|
||||||
|
let error = '';
|
||||||
|
let copySucceeded = false;
|
||||||
|
let autoReloadInterval = 0;
|
||||||
|
let objectViewerData;
|
||||||
|
let interval;
|
||||||
|
$: (filter || severityFilter || componentFilter) && refresh();
|
||||||
|
$: busy = !logs && !error && 'Requesting logs…';
|
||||||
|
|
||||||
|
$: if (autoReloadInterval) {
|
||||||
|
if (interval) {
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
interval = setInterval(refresh, autoReloadInterval * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
let _logs = [];
|
||||||
|
({ logs: _logs, total, error } = await host.getLogs(filter));
|
||||||
|
logs = [];
|
||||||
|
|
||||||
|
for (let index = 0; index < _logs.length; index++) {
|
||||||
|
const log = JSON.parse(_logs[index]);
|
||||||
|
|
||||||
|
const matchesLevel = severityFilter ? log.s?.startsWith(severityFilter) : true;
|
||||||
|
const matchesComponent = componentFilter ? (componentFilter === log.c?.toUpperCase()) : true;
|
||||||
|
|
||||||
|
if (matchesLevel && matchesComponent) {
|
||||||
|
log._index = index;
|
||||||
|
log.s = logLevels[log.s] || log.s;
|
||||||
|
logs = [ ...logs, log ];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openFilterDocs() {
|
||||||
|
BrowserOpenURL('https://www.mongodb.com/docs/manual/reference/command/getLog/#command-fields');
|
||||||
|
}
|
||||||
|
|
||||||
|
function openLogDetail(event) {
|
||||||
|
objectViewerData = logs[event.detail.index];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copy() {
|
||||||
|
const json = JSON.stringify(host.status, undefined, '\t');
|
||||||
|
await navigator.clipboard.writeText(json);
|
||||||
|
copySucceeded = true;
|
||||||
|
setTimeout(() => copySucceeded = false, 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (interval) {
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="stats">
|
||||||
|
<div class="formrow">
|
||||||
|
<label class="field">
|
||||||
|
<span class="label">Auto reload (seconds)</span>
|
||||||
|
<input type="number" class="autoreloadinput" bind:value={autoReloadInterval} list="autoreloadintervals" use:input />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="field">
|
||||||
|
<span class="label">Log type</span>
|
||||||
|
<select bind:value={filter}>
|
||||||
|
<option value="global">Global</option>
|
||||||
|
<option value="startupWarnings">Startup warnings</option>
|
||||||
|
</select>
|
||||||
|
<button class="button secondary" on:click={openFilterDocs} title="Documentation">
|
||||||
|
<Icon name="?" />
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="formrow">
|
||||||
|
<label class="field">
|
||||||
|
<span class="label">Severity</span>
|
||||||
|
<select bind:value={severityFilter}>
|
||||||
|
<option value="">All</option>
|
||||||
|
{#each Object.entries(logLevels) as [ value, name ]}
|
||||||
|
<option {value}>{value} ({name})</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="field">
|
||||||
|
<span class="label">Component</span>
|
||||||
|
<select bind:value={componentFilter}>
|
||||||
|
<option value="">All</option>
|
||||||
|
{#each logComponents as value}
|
||||||
|
<option {value}>{value}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<Grid
|
||||||
|
items={logs || []}
|
||||||
|
columns={[
|
||||||
|
{ title: 'Date', key: 't.$date' },
|
||||||
|
{ title: 'Severity', key: 's' },
|
||||||
|
{ title: 'ID', key: 'id' },
|
||||||
|
{ title: 'Component', key: 'c' },
|
||||||
|
{ title: 'Context', key: 'ctx' },
|
||||||
|
{ title: 'Message', key: 'msg' },
|
||||||
|
]}
|
||||||
|
key="_index"
|
||||||
|
showHeaders
|
||||||
|
errorTitle={error ? 'Error fetching server status' : ''}
|
||||||
|
errorDescription={error}
|
||||||
|
on:trigger={openLogDetail}
|
||||||
|
{busy}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<button class="button" on:click={refresh}>
|
||||||
|
<Icon name="reload" spin={busy} /> Reload
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="button secondary" on:click={copy} disabled={!host.status}>
|
||||||
|
<Icon name={copySucceeded ? 'check' : 'clipboard'} />
|
||||||
|
Copy JSON
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if total}
|
||||||
|
<div class="total">
|
||||||
|
Total: {total}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if objectViewerData}
|
||||||
|
<ObjectViewer bind:data={objectViewerData} readonly />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<datalist id="autoreloadintervals">
|
||||||
|
{#each autoReloadIntervals as value}
|
||||||
|
<option {value} />
|
||||||
|
{/each}
|
||||||
|
</datalist>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.stats {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.5rem;
|
||||||
|
grid-template: auto auto 1fr auto / 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formrow {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.5rem;
|
||||||
|
grid-template: 1fr / 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
overflow: auto;
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.2rem;
|
||||||
|
}
|
||||||
|
.total {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.autoreloadinput {
|
||||||
|
width: 1.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
@ -20,6 +20,7 @@
|
|||||||
<div class="grid">
|
<div class="grid">
|
||||||
<ObjectGrid
|
<ObjectGrid
|
||||||
data={host.status || {}}
|
data={host.status || {}}
|
||||||
|
showTypes={false}
|
||||||
errorTitle={host.statusError ? 'Error fetching server status' : ''}
|
errorTitle={host.statusError ? 'Error fetching server status' : ''}
|
||||||
errorDescription={host.statusError}
|
errorDescription={host.statusError}
|
||||||
busy={!host.status && !host.statusError && 'Fetching server status…'}
|
busy={!host.status && !host.statusError && 'Fetching server status…'}
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
<div class="grid">
|
<div class="grid">
|
||||||
<ObjectGrid
|
<ObjectGrid
|
||||||
data={host.systemInfo}
|
data={host.systemInfo}
|
||||||
|
showTypes={false}
|
||||||
errorTitle={host.systemInfoError ? 'Error fetching system info' : ''}
|
errorTitle={host.systemInfoError ? 'Error fetching system info' : ''}
|
||||||
errorDescription={host.systemInfoError}
|
errorDescription={host.systemInfoError}
|
||||||
busy={!host.systemInfo && !host.systemInfoError && 'Fetching system info…'}
|
busy={!host.systemInfo && !host.systemInfoError && 'Fetching system info…'}
|
||||||
|
146
frontend/src/organisms/connection/shell.svelte
Normal file
146
frontend/src/organisms/connection/shell.svelte
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
<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, onMount } from 'svelte';
|
||||||
|
|
||||||
|
export let host = undefined;
|
||||||
|
export let database = undefined;
|
||||||
|
export let collection = undefined;
|
||||||
|
|
||||||
|
const placeholder = '// Write your script here...';
|
||||||
|
const extensions = [ javascript() ];
|
||||||
|
let script = '';
|
||||||
|
let result = {};
|
||||||
|
let copySucceeded = false;
|
||||||
|
let timeout;
|
||||||
|
let busy = false;
|
||||||
|
let editor;
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
busy = true;
|
||||||
|
|
||||||
|
if (collection) {
|
||||||
|
result = await collection.executeShellScript(script);
|
||||||
|
}
|
||||||
|
else if (database) {
|
||||||
|
result = await database.executeShellScript(script);
|
||||||
|
}
|
||||||
|
else if (host) {
|
||||||
|
result = await host.executeShellScript(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
busy = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyErrorDescription() {
|
||||||
|
await navigator.clipboard.writeText(result.errorDescription);
|
||||||
|
copySucceeded = true;
|
||||||
|
timeout = setTimeout(() => copySucceeded = false, 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
editor.dispatch({
|
||||||
|
changes: {
|
||||||
|
from: 0,
|
||||||
|
to: editor.state.doc.length,
|
||||||
|
insert: placeholder,
|
||||||
|
},
|
||||||
|
selection: {
|
||||||
|
from: 0,
|
||||||
|
anchor: 0,
|
||||||
|
to: placeholder.length,
|
||||||
|
head: placeholder.length,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
editor.focus();
|
||||||
|
});
|
||||||
|
onDestroy(() => clearTimeout(timeout));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="shell">
|
||||||
|
<div class="overflow">
|
||||||
|
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||||
|
<label class="field">
|
||||||
|
<CodeEditor bind:editor bind:text={script} {extensions} />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="output">
|
||||||
|
{#if busy}
|
||||||
|
<BlankState icon="loading" label="Executing…" />
|
||||||
|
{:else 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 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 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflow {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
height: 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;
|
||||||
|
user-select: text;
|
||||||
|
-webkit-user-select: text;
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
.output :global(.blankstate) {
|
||||||
|
margin: auto;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
}
|
||||||
|
.controls .status {
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
</style>
|
@ -50,8 +50,8 @@ a {
|
|||||||
|
|
||||||
hr {
|
hr {
|
||||||
border: none;
|
border: none;
|
||||||
height: 1px;
|
border-top: 1px solid #ccc;
|
||||||
background-color: #ccc;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
@ -142,7 +142,7 @@ select:disabled {
|
|||||||
.field > textarea,
|
.field > textarea,
|
||||||
.field > select {
|
.field > select {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 0.5rem;
|
padding: 0 0.5rem;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
|
4
frontend/wailsjs/go/app/App.d.ts
generated
vendored
4
frontend/wailsjs/go/app/App.d.ts
generated
vendored
@ -20,10 +20,14 @@ 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>;
|
||||||
|
|
||||||
|
export function HostLogs(arg1:string,arg2:string):Promise<app.HostLogsResult>;
|
||||||
|
|
||||||
export function Hosts():Promise<map[string]app.Host>;
|
export function Hosts():Promise<map[string]app.Host>;
|
||||||
|
|
||||||
export function InsertItems(arg1:string,arg2:string,arg3:string,arg4:string):Promise<any>;
|
export function InsertItems(arg1:string,arg2:string,arg3:string,arg4:string):Promise<any>;
|
||||||
|
8
frontend/wailsjs/go/app/App.js
generated
8
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);
|
||||||
}
|
}
|
||||||
@ -38,6 +42,10 @@ export function GetIndexes(arg1, arg2, arg3) {
|
|||||||
return window['go']['app']['App']['GetIndexes'](arg1, arg2, arg3);
|
return window['go']['app']['App']['GetIndexes'](arg1, arg2, arg3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function HostLogs(arg1, arg2) {
|
||||||
|
return window['go']['app']['App']['HostLogs'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
export function Hosts() {
|
export function Hosts() {
|
||||||
return window['go']['app']['App']['Hosts']();
|
return window['go']['app']['App']['Hosts']();
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,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"`
|
||||||
@ -49,6 +50,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"))
|
||||||
|
@ -42,7 +42,11 @@ func (a *App) connectToHost(hostKey string) (*mongo.Client, context.Context, fun
|
|||||||
return nil, nil, nil, errors.New("invalid uri")
|
return nil, nil, nil, errors.New("invalid uri")
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := mongo.NewClient(mongoOptions.Client().ApplyURI(h.URI))
|
appName := "Rolens v" + a.Env.Version
|
||||||
|
opts := mongoOptions.Client()
|
||||||
|
opts.AppName = &appName
|
||||||
|
opts.ApplyURI(h.URI)
|
||||||
|
client, err := mongo.NewClient(opts)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
runtime.LogWarningf(a.ctx, "Could not connect to host %s: %s", hostKey, err.Error())
|
runtime.LogWarningf(a.ctx, "Could not connect to host %s: %s", hostKey, err.Error())
|
||||||
|
46
internal/app/host_logs.go
Normal file
46
internal/app/host_logs.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HostLogsResult struct {
|
||||||
|
Total int32 `json:"total"`
|
||||||
|
Logs []string `json:"logs"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) HostLogs(hostKey, filter string) (result HostLogsResult) {
|
||||||
|
client, ctx, close, err := a.connectToHost(hostKey)
|
||||||
|
if err != nil {
|
||||||
|
result.Error = "Could not connect to host"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer close()
|
||||||
|
|
||||||
|
var res bson.M
|
||||||
|
err = client.Database("admin").RunCommand(ctx, bson.M{"getLog": filter}).Decode(&res)
|
||||||
|
if err != nil {
|
||||||
|
runtime.LogWarningf(a.ctx, "Could not get %s logs for %s: %s", filter, hostKey, err.Error())
|
||||||
|
result.Error = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if res["totalLinesWritten"] != nil {
|
||||||
|
result.Total = res["totalLinesWritten"].(int32)
|
||||||
|
} else {
|
||||||
|
result.Total = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Logs = make([]string, 0)
|
||||||
|
|
||||||
|
switch res["log"].(type) {
|
||||||
|
case bson.A:
|
||||||
|
for _, v := range res["log"].(bson.A) {
|
||||||
|
result.Logs = append(result.Logs, v.(string))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
105
internal/app/host_shell.go
Normal file
105
internal/app/host_shell.go
Normal file
@ -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
|
||||||
|
}
|
Reference in New Issue
Block a user