mirror of
https://github.com/garraflavatra/rolens.git
synced 2025-01-18 13:07:58 +00:00
Add host log panel
This commit is contained in:
parent
0b9f23365b
commit
f30827ae2e
2
frontend/src/components/icon.svelte
vendored
2
frontend/src/components/icon.svelte
vendored
@ -147,5 +147,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 === '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>
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
export let text = '';
|
export let text = '';
|
||||||
export let editor = undefined;
|
export let editor = undefined;
|
||||||
|
export let readonly = false;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
let editorParent;
|
let editorParent;
|
||||||
@ -20,6 +21,7 @@
|
|||||||
keymap.of([ indentWithTab, indentOnInput ]),
|
keymap.of([ indentWithTab, indentOnInput ]),
|
||||||
javascript(),
|
javascript(),
|
||||||
EditorState.tabSize.of(4),
|
EditorState.tabSize.of(4),
|
||||||
|
EditorState.readOnly.of(readonly),
|
||||||
EditorView.updateListener.of(e => {
|
EditorView.updateListener.of(e => {
|
||||||
if (!e.docChanged) {
|
if (!e.docChanged) {
|
||||||
return;
|
return;
|
||||||
|
@ -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">
|
||||||
|
@ -17,6 +17,7 @@ import {
|
|||||||
DropDatabase,
|
DropDatabase,
|
||||||
DropIndex,
|
DropIndex,
|
||||||
GetIndexes,
|
GetIndexes,
|
||||||
|
HostLogs,
|
||||||
Hosts,
|
Hosts,
|
||||||
OpenCollection,
|
OpenCollection,
|
||||||
OpenConnection,
|
OpenConnection,
|
||||||
@ -263,17 +264,17 @@ async function refresh() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
|
||||||
host.databases[name] = { key: name, new: true };
|
|
||||||
await host.open();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
await refresh();
|
await refresh();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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) {
|
||||||
|
host.databases[name] = { key: name, new: true };
|
||||||
|
await host.open();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
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 +284,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();
|
||||||
|
@ -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 Logs from './logs.svelte';
|
||||||
import Status from './status.svelte';
|
import Status from './status.svelte';
|
||||||
import SystemInfo from './systeminfo.svelte';
|
import SystemInfo from './systeminfo.svelte';
|
||||||
|
|
||||||
@ -23,14 +25,18 @@
|
|||||||
<div class="view" class:empty={!host}>
|
<div class="view" class:empty={!host}>
|
||||||
{#if host}
|
{#if host}
|
||||||
{#key host}
|
{#key host}
|
||||||
<TabBar tabs={[
|
<TabBar
|
||||||
{ key: 'status', icon: 'chart', title: 'Host status' },
|
tabs={[
|
||||||
{ key: 'systemInfo', icon: 'server', title: 'System info' },
|
{ key: 'status', icon: 'chart', title: 'Host status' },
|
||||||
]}
|
{ key: 'logs', icon: 'text', title: 'Logs' },
|
||||||
bind:selectedKey={tab} />
|
{ key: 'systemInfo', icon: 'server', title: 'System info' },
|
||||||
|
]}
|
||||||
|
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} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
139
frontend/src/organisms/connection/host/logs.svelte
Normal file
139
frontend/src/organisms/connection/host/logs.svelte
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
<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 { BrowserOpenURL } from '$wails/runtime/runtime';
|
||||||
|
|
||||||
|
export let host;
|
||||||
|
|
||||||
|
const autoReloadIntervals = [ 1, 2, 5, 10, 30, 60 ];
|
||||||
|
let filter = 'global';
|
||||||
|
let logs;
|
||||||
|
let total = 0;
|
||||||
|
let error = '';
|
||||||
|
let copySucceeded = false;
|
||||||
|
let autoReloadInterval = 0;
|
||||||
|
let objectViewerData;
|
||||||
|
$: filter && refresh();
|
||||||
|
$: busy = !logs && !error && 'Requesting logs…';
|
||||||
|
|
||||||
|
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]);
|
||||||
|
log._index = index;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="stats">
|
||||||
|
<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">
|
||||||
|
<div>
|
||||||
|
<div class="field inline">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="field inline">
|
||||||
|
<span class="label">Reload (sec)</span>
|
||||||
|
<input type="number" bind:value={autoReloadInterval} list="autoreloadintervals" use:input />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="field inline">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{#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: 1fr auto / 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats .grid {
|
||||||
|
overflow: auto;
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.1rem;
|
||||||
|
}
|
||||||
|
.total {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
</style>
|
@ -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;
|
||||||
|
2
frontend/wailsjs/go/app/App.d.ts
generated
vendored
2
frontend/wailsjs/go/app/App.d.ts
generated
vendored
@ -24,6 +24,8 @@ export function FindItems(arg1:string,arg2:string,arg3:string,arg4:string):Promi
|
|||||||
|
|
||||||
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>;
|
||||||
|
4
frontend/wailsjs/go/app/App.js
generated
4
frontend/wailsjs/go/app/App.js
generated
@ -38,6 +38,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']();
|
||||||
}
|
}
|
||||||
|
37
internal/app/host_logs.go
Normal file
37
internal/app/host_logs.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Total = res["totalLinesWritten"].(int32)
|
||||||
|
result.Logs = make([]string, 0)
|
||||||
|
|
||||||
|
for _, v := range res["log"].(bson.A) {
|
||||||
|
result.Logs = append(result.Logs, v.(string))
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user