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

First commit in a long time

This commit is contained in:
Romein van Buren 2023-05-26 17:21:39 +02:00
parent b15fde11db
commit a84b1498a3
Signed by: romein
GPG Key ID: 0EFF8478ADDF6C49
17 changed files with 321 additions and 87 deletions

View File

@ -0,0 +1,16 @@
import { StartProgressBar, StopProgressBar } from '$wails/go/ui/UI';
let taskCounter = 0;
export function startProgress(taskDescription = 'Loading…') {
const taskIndex = ++taskCounter;
StartProgressBar(taskIndex, taskDescription);
const task = {
id: taskIndex,
description: taskDescription,
end: () => StopProgressBar(taskIndex),
};
return task;
}

View File

@ -1,33 +0,0 @@
import { StartProgressBar, StopProgressBar } from '$wails/go/ui/UI';
import { writable } from 'svelte/store';
const { update, subscribe } = writable(0);
let timer;
let progressBarShown = false;
subscribe(isBusy => {
if (isBusy) {
document.body.classList.add('busy');
if (!progressBarShown) {
progressBarShown = true;
timer = setTimeout(() => StartProgressBar(''), 100);
}
}
else {
if (timer) {
clearTimeout(timer);
timer = undefined;
}
progressBarShown = false;
document.body.classList.remove('busy');
StopProgressBar();
}
});
const busy = {
start: () => update(v => ++v),
end: () => update(v => --v),
subscribe,
};
export default busy;

View File

@ -0,0 +1,61 @@
<script>
import Icon from '$components/icon.svelte';
import Modal from '$components/modal.svelte';
import { startProgress } from '$lib/progress';
import views from '$lib/stores/views';
import { createEventDispatcher } from 'svelte';
export let info;
export let collection;
const dispatch = createEventDispatcher();
let viewKey = collection.viewKey;
$: viewKey = collection.viewKey;
async function performExport() {
const progress = startProgress('Performing export…');
info.view = $views[viewKey];
//...
progress.end();
}
</script>
<Modal bind:show={info} title="Export results" width="400px">
<form on:submit|preventDefault={performExport}>
<label class="field">
<span class="label">Export</span>
<select bind:value={info.contents}>
<option value="all">all records</option>
<option value="query">all records matching query</option>
<option value="querylimitskip">all records matching query, considering limit and skip</option>
</select>
</label>
<label class="field">
<span class="label">Format</span>
<select bind:value={info.format}>
<option value="jsonarray">JSON array</option>
<option value="jsonnewline">JSON: newline-separated objects</option>
<option value="csv">CSV</option>
</select>
</label>
<label class="field">
<span class="label">View to use</span>
<select bind:value={viewKey}>
{#each Object.entries(views.forCollection(collection.hostKey, collection.dbKey, collection.key)) as [key, { name }]}
<option value={key}>{name}</option>
{/each}
</select>
<button class="btn" type="button" on:click={() => dispatch('openViewConfig')} title="Edit view">
<Icon name="cog" />
</button>
</label>
</form>
</Modal>
<style>
form {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
</style>

View File

@ -1,19 +1,18 @@
<script>
// import CodeExample from '$components/code-example.svelte';
import Grid from '$components/grid.svelte';
import Icon from '$components/icon.svelte';
import ObjectGrid from '$components/objectgrid.svelte';
import input from '$lib/actions/input';
import { deepClone } from '$lib/objects';
import busy from '$lib/stores/busy';
import { startProgress } from '$lib/progress';
import queries from '$lib/stores/queries';
import applicationSettings from '$lib/stores/settings';
import views from '$lib/stores/views';
import { FindItems, RemoveItemById } from '$wails/go/app/App';
import { EJSON } from 'bson';
import { createEventDispatcher, onMount } from 'svelte';
import ExportInfo from './components/export.svelte';
import QueryChooser from './components/querychooser.svelte';
// import ObjectViewer from '$components/objectviewer.svelte';
export let collection;
export let hosts = {};
@ -35,6 +34,7 @@
let objectViewerData;
let queryToSave;
let showQueryChooser = false;
let exportInfo;
$: viewsForCollection = views.forCollection(collection.hostKey, collection.dbKey, collection.key);
$: code = `db.${collection.key}.find(${form.query || '{}'}${form.fields && form.fields !== '{}' ? `, ${form.fields}` : ''}).sort(${form.sort})${form.skip ? `.skip(${form.skip})` : ''}${form.limit ? `.limit(${form.limit})` : ''};`;
@ -42,7 +42,7 @@
$: activePage = (submittedForm.limit && submittedForm.skip && result?.results?.length) ? submittedForm.skip / submittedForm.limit : 0;
async function submitQuery() {
busy.start();
const progress = startProgress('Performing query…');
activePath = [];
const newResult = await FindItems(collection.hostKey, collection.dbKey, collection.key, JSON.stringify(form));
if (newResult) {
@ -50,7 +50,7 @@
result = newResult;
submittedForm = deepClone(form);
}
busy.end();
progress.end();
resetFocus();
}
@ -168,7 +168,7 @@
<button class="btn" type="button" on:click={saveQuery}>
<Icon name="save" /> Save as…
</button>
<button class="btn" type="button" on:click={saveQuery}>
<button class="btn" type="button" on:click={() => exportInfo = {}}>
<Icon name="save" /> Export results…
</button>
<button type="submit" class="btn" title="Run query">
@ -245,6 +245,8 @@
{collection}
/>
<ExportInfo on:openViewConfig bind:collection bind:info={exportInfo} />
<!-- <ObjectViewer bind:data={objectViewerData} /> -->
<datalist id="limits">

View File

@ -1,5 +1,4 @@
<script>
// import CodeExample from '$components/code-example.svelte';
import Icon from '$components/icon.svelte';
import input from '$lib/actions/input';
import { RemoveItems } from '$wails/go/app/App';

View File

@ -1,5 +1,4 @@
<script>
// import CodeExample from '$components/code-example.svelte';
import ObjectGrid from '$components/objectgrid.svelte';
export let collection;

View File

@ -1,5 +1,4 @@
<script>
// import CodeExample from '$components/code-example.svelte';
import Icon from '$components/icon.svelte';
import input from '$lib/actions/input';
import { atomicUpdateOperators } from '$lib/mongo';

View File

@ -2,7 +2,7 @@
import DirectoryChooser from '$components/directorychooser.svelte';
import Grid from '$components/grid.svelte';
import Modal from '$components/modal.svelte';
import busy from '$lib/stores/busy';
import { startProgress } from '$lib/progress';
import { connections } from '$lib/stores/connections';
import applicationSettings from '$lib/stores/settings';
import { OpenConnection, OpenDatabase, PerformDump } from '$wails/go/app/App';
@ -21,7 +21,7 @@
info.collKeys = [];
if (hostKey) {
busy.start();
const progress = startProgress(`Opening connection to host "${hostKey}"`);
const databases = await OpenConnection(hostKey);
if (databases && !$connections[hostKey]) {
@ -31,7 +31,7 @@
});
}
busy.end();
progress.end();
}
}
@ -40,14 +40,14 @@
info.dbKey = dbKey;
if (dbKey) {
busy.start();
const progress = startProgress(`Opening database "${dbKey}"`);
const collections = await OpenDatabase(info.hostKey, dbKey);
for (const collKey of collections?.sort() || []) {
$connections[info.hostKey].databases[dbKey].collections[collKey] = {};
}
busy.end();
progress.end();
}
}

View File

@ -1,6 +1,6 @@
<script>
import Grid from '$components/grid.svelte';
import busy from '$lib/stores/busy';
import { startProgress } from '$lib/progress';
import { connections } from '$lib/stores/connections';
import { WindowSetTitle } from '$wails/runtime/runtime';
import { createEventDispatcher } from 'svelte';
@ -28,7 +28,7 @@
}
async function openConnection(hostKey) {
busy.start();
const progress = startProgress(`Connecting to "${hostKey}"…`);
const databases = await OpenConnection(hostKey);
if (databases) {
@ -41,47 +41,47 @@
WindowSetTitle(`${hosts[activeHostKey].name} - Rolens`);
}
busy.end();
progress.end();
}
async function openDatabase(dbKey) {
busy.start();
const progress = startProgress(`Opening database "${dbKey}"…`);
const collections = await OpenDatabase(activeHostKey, dbKey);
for (const collKey of collections || []) {
$connections[activeHostKey].databases[dbKey].collections[collKey] = {};
}
busy.end();
progress.end();
}
async function dropDatabase(dbKey) {
busy.start();
const progress = startProgress(`Dropping database "${dbKey}"…`);
await DropDatabase(activeHostKey, dbKey);
await reload();
busy.end();
progress.end();
}
async function openCollection(collKey) {
busy.start();
const progress = startProgress(`Opening collection "${collKey}"…`);
const stats = await OpenCollection(activeHostKey, activeDbKey, collKey);
$connections[activeHostKey].databases[activeDbKey].collections[collKey] = $connections[activeHostKey].databases[activeDbKey].collections[collKey] || {};
$connections[activeHostKey].databases[activeDbKey].collections[collKey].stats = stats;
busy.end();
progress.end();
}
async function truncateCollection(dbKey, collKey) {
busy.start();
const progress = startProgress(`Truncating collection "${collKey}"…`);
await TruncateCollection(activeHostKey, dbKey, collKey);
await reload();
busy.end();
progress.end();
}
async function dropCollection(dbKey, collKey) {
busy.start();
const progress = startProgress(`Dropping collection "${collKey}"…`);
await DropCollection(activeHostKey, dbKey, collKey);
await reload();
busy.end();
progress.end();
}
</script>

View File

@ -1,5 +1,5 @@
<script>
import busy from '$lib/stores/busy';
import { startProgress } from '$lib/progress';
import { connections } from '$lib/stores/connections';
import { Hosts, RenameCollection } from '$wails/go/app/App';
import { EnterText } from '$wails/go/ui/UI';
@ -44,13 +44,13 @@
async function renameCollection(oldCollKey) {
const newCollKey = await EnterText('Rename collection', `Enter a new name for collection ${oldCollKey}.`, oldCollKey);
if (newCollKey && (newCollKey !== oldCollKey)) {
busy.start();
const progress = startProgress(`Renaming collection "${oldCollKey}" to "${newCollKey}"…`);
const ok = await RenameCollection(activeHostKey, activeDbKey, oldCollKey, newCollKey);
if (ok) {
activeCollKey = newCollKey;
await hostTree.reload();
}
busy.end();
progress.end();
}
}

View File

@ -39,6 +39,8 @@ export function OpenDatabase(arg1:string,arg2:string):Promise<Array<string>>;
export function PerformDump(arg1:string):Promise<boolean>;
export function PerformFindExport(arg1:string,arg2:string,arg3:string,arg4:string):Promise<boolean>;
export function PurgeLogDirectory():Promise<void>;
export function RemoveHost(arg1:string):Promise<void>;

View File

@ -66,6 +66,10 @@ export function PerformDump(arg1) {
return window['go']['app']['App']['PerformDump'](arg1);
}
export function PerformFindExport(arg1, arg2, arg3, arg4) {
return window['go']['app']['App']['PerformFindExport'](arg1, arg2, arg3, arg4);
}
export function PurgeLogDirectory() {
return window['go']['app']['App']['PurgeLogDirectory']();
}

View File

@ -10,8 +10,8 @@ export function OpenDirectory(arg1:string,arg2:string):Promise<string>;
export function Reveal(arg1:string):Promise<void>;
export function StartProgressBar(arg1:string):Promise<void>;
export function StartProgressBar(arg1:number,arg2:string):Promise<void>;
export function Startup(arg1:context.Context):Promise<void>;
export function StopProgressBar():Promise<void>;
export function StopProgressBar(arg1:number):Promise<void>;

View File

@ -18,14 +18,14 @@ export function Reveal(arg1) {
return window['go']['ui']['UI']['Reveal'](arg1);
}
export function StartProgressBar(arg1) {
return window['go']['ui']['UI']['StartProgressBar'](arg1);
export function StartProgressBar(arg1, arg2) {
return window['go']['ui']['UI']['StartProgressBar'](arg1, arg2);
}
export function Startup(arg1) {
return window['go']['ui']['UI']['Startup'](arg1);
}
export function StopProgressBar() {
return window['go']['ui']['UI']['StopProgressBar']();
export function StopProgressBar(arg1) {
return window['go']['ui']['UI']['StopProgressBar'](arg1);
}

View File

@ -0,0 +1,178 @@
package app
import (
"encoding/csv"
"encoding/json"
"fmt"
"os"
"github.com/ncruces/zenity"
"github.com/wailsapp/wails/v2/pkg/runtime"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
)
type ExportContents string
type ExportFormat string
const (
ExportContentsAll ExportContents = "all"
ExportContentsQuery ExportContents = "query"
ExportContentsQueryLimitSkip ExportContents = "querylimitskip"
ExportFormatJsonArray ExportFormat = "jsonarray"
ExportFormatJsonNewline ExportFormat = "jsonnewline"
ExportFormatCsv ExportFormat = "csv"
)
type ExportSettings struct {
Contents ExportContents `json:"contents"`
Format ExportFormat `json:"format"`
ViewKey string `json:"viewKey"`
QueryJson string `json:"query"`
Limit int64 `json:"limit"`
Skip int64 `json:"skip"`
OutFile string `json:"outfile"`
}
func (a *App) PerformFindExport(hostKey, dbKey, collKey, settingsJson string) bool {
var settings ExportSettings
if err := json.Unmarshal([]byte(settingsJson), &settings); err != nil {
runtime.LogWarning(a.ctx, "Could not parse export settings:")
runtime.LogWarning(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Couldn't parse export settings!"), zenity.ErrorIcon)
return false
}
if _, err := os.Stat(settings.OutFile); err == nil {
zenity.Error(fmt.Sprintf("File %s already exists, export aborted.", settings.OutFile), zenity.ErrorIcon)
return false
}
views, err := a.Views()
if err != nil {
return false
}
view, found := views[settings.ViewKey]
if !found {
zenity.Error(fmt.Sprintf("View %s is not known", settings.ViewKey), zenity.ErrorIcon)
return false
}
var query bson.M
if settings.Contents != ExportContentsAll {
if err = bson.UnmarshalExtJSON([]byte(settings.QueryJson), true, &query); err != nil {
runtime.LogInfo(a.ctx, "Invalid find query (exporting):")
runtime.LogInfo(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Invalid query"), zenity.ErrorIcon)
return false
}
}
client, ctx, close, err := a.connectToHost(hostKey)
if err != nil {
return false
}
defer close()
projection := bson.M{}
if settings.ViewKey != "list" {
for _, col := range view.Columns {
projection[col.Key] = ""
}
}
cur, err := client.Database(dbKey).Collection(collKey).Find(ctx, query, &options.FindOptions{
Skip: &settings.Skip,
Limit: &settings.Limit,
Projection: projection,
})
if err != nil {
runtime.LogInfo(a.ctx, "Unable to get cursor while exporting:")
runtime.LogInfo(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Unable to get cursor"), zenity.ErrorIcon)
return false
}
file, err := os.OpenFile(settings.OutFile, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
zenity.Error(fmt.Sprintf(err.Error(), zenity.Title("Error while opening file"), settings.OutFile), zenity.ErrorIcon)
return false
}
defer file.Close()
var csvWriter *csv.Writer
var csvColumnKeys []any
switch settings.Format {
case ExportFormatJsonArray:
file.WriteString("[\n")
case ExportFormatCsv:
csvWriter = csv.NewWriter(file)
}
index := -1
for cur.Next(ctx) {
index++
var item map[any]interface{}
if err := bson.Unmarshal(cur.Current, &item); err != nil {
runtime.LogInfo(a.ctx, fmt.Sprintf("Unable to unmarshal item %d while exporting", index))
runtime.LogInfo(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Unable to unmarshal item %d"), zenity.ErrorIcon)
continue
}
switch settings.Format {
case ExportFormatCsv:
csvItem := make([]string, 0)
switch settings.ViewKey {
case "list":
if csvColumnKeys == nil {
csvColumnKeys = make([]any, 0)
for k := range item {
csvColumnKeys = append(csvColumnKeys, k)
}
}
for _, k := range csvColumnKeys {
csvItem = append(csvItem, item[k].(string))
}
default:
for _, v := range item {
csvItem = append(csvItem, v.(string))
}
}
if err := csvWriter.Write(csvItem); err != nil {
runtime.LogInfo(a.ctx, fmt.Sprintf("Unable to write item %d to CSV while exporting", index))
runtime.LogInfo(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Unable to write item %d to CSV"), zenity.ErrorIcon)
}
case ExportFormatJsonArray, ExportFormatJsonNewline:
itemJson, err := json.Marshal(item)
if err != nil {
runtime.LogInfo(a.ctx, fmt.Sprintf("Unable to marshal item %d to JSON while exporting", index))
runtime.LogInfo(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Unable to marshal item %d to JSON"), zenity.ErrorIcon)
}
file.Write(itemJson)
if settings.Format == ExportFormatJsonArray {
file.WriteString(",")
}
file.WriteString("\n")
}
}
if settings.Format == ExportFormatJsonArray {
file.WriteString("]\n")
}
return true
}

View File

@ -1,28 +1,33 @@
package ui
import "github.com/ncruces/zenity"
import (
"time"
func (u *UI) StartProgressBar(title string) {
if u.progress != nil {
// already loading
return
}
"github.com/ncruces/zenity"
)
func (u *UI) StartProgressBar(id uint, title string) {
if title == "" {
// default title
title = "Loading"
title = "Loading"
}
p, err := zenity.Progress(zenity.Title(title), zenity.Pulsate(), zenity.NoCancel(), zenity.Modal())
p, err := zenity.Progress(zenity.Title(title), zenity.Pulsate(), zenity.Modal())
if err != nil {
return
}
u.progress = p
u.progressBars[id] = p
}
func (u *UI) StopProgressBar() {
if u.progress == nil {
func (u *UI) StopProgressBar(id uint) {
for try := 0; try < 10; try++ {
p := u.progressBars[id]
if p != nil {
p.Complete()
p.Close()
p = nil
return
}
u.progress.Complete()
u.progress.Close()
u.progress = nil
println("Progress dialog not found:", id, try)
time.Sleep(100 * time.Millisecond)
}
}

View File

@ -10,11 +10,13 @@ import (
type UI struct {
ctx context.Context
progress zenity.ProgressDialog
progressBars map[uint]zenity.ProgressDialog
}
func New() *UI {
return &UI{}
return &UI{
progressBars: make(map[uint]zenity.ProgressDialog),
}
}
func (u *UI) Startup(ctx context.Context) {