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

Made harsh loading/error experience friendlier

This commit is contained in:
Romein van Buren 2023-06-23 17:22:47 +02:00
parent 964e66e8a3
commit dc0094b27c
Signed by: romein
GPG Key ID: 0EFF8478ADDF6C49
21 changed files with 262 additions and 253 deletions

View File

@ -5,6 +5,7 @@
* Added meaningful window titles, and actually show these in the title bar (macOS). * Added meaningful window titles, and actually show these in the title bar (macOS).
* Corrected link to documentation in the about box (#30). * Corrected link to documentation in the about box (#30).
* Fixed host/database selection bug in grid (#31, #32), involving a frontend refactoring. * Fixed host/database selection bug in grid (#31, #32), involving a frontend refactoring.
* Replaced (some) harsh loading dialogs with smooth spinners, and replaced (some) capricious error dialogs with friendly error messages.
## [v0.2.0] ## [v0.2.0]

View File

@ -1,14 +1,25 @@
<script> <script>
import Icon from './icon.svelte';
export let title = '';
export let label = 'No items'; export let label = 'No items';
export let image = '/empty.svg'; export let image = '/empty.svg';
export let icon = '';
export let pale = true; export let pale = true;
export let big = false; export let big = false;
</script> </script>
<div class="blankstate" class:pale class:big> <div class="blankstate" class:pale class:big>
<div class="content"> <div class="content">
<img src={image} alt="" /> {#if icon}
<Icon name={icon} />
{:else if image}
<img src={image} alt="" />
{/if}
<p class="title">{title}</p>
<p>{label}</p> <p>{label}</p>
<slot /> <slot />
</div> </div>
</div> </div>
@ -27,12 +38,20 @@
height: 150px; height: 150px;
width: auto; width: auto;
} }
.content :global(svg) {
height: 40px;
width: auto;
}
p { p {
margin: 2.85rem 0; margin: 2.85rem 0;
font-size: 1.25rem; font-size: 1.25rem;
line-height: 1.25rem; line-height: 1.25rem;
} }
p.title {
font-weight: 700;
margin-bottom: -1.85rem;
}
.blankstate :global(.btn) { .blankstate :global(.btn) {
font-size: 1.35rem; font-size: 1.35rem;
@ -48,7 +67,7 @@
filter: grayscale(1); filter: grayscale(1);
opacity: 0.4; opacity: 0.4;
} }
.pale p { .pale {
color: #777; color: #777;
} }
</style> </style>

View File

@ -1,4 +1,5 @@
<script> <script>
import BlankState from './blankstate.svelte';
import GridItems from './grid-items.svelte'; import GridItems from './grid-items.svelte';
export let columns = []; export let columns = [];
@ -12,7 +13,9 @@
export let canSelect = true; export let canSelect = true;
export let canRemoveItems = false; export let canRemoveItems = false;
export let inputsValid = false; export let inputsValid = false;
// export let actions = []; export let errorTitle = '';
export let errorDescription = '';
export let busy = false;
</script> </script>
<div class="grid"> <div class="grid">
@ -27,45 +30,51 @@
</div> </div>
{/if} --> {/if} -->
<table> {#if busy}
{#if showHeaders && columns.some(col => col.title)} <BlankState label={(busy === true) ? 'Loading…' : busy} icon="loading" />
<thead> {:else if errorTitle || errorDescription}
<tr> <BlankState title={errorTitle} label={errorDescription} icon="!" />
{#if !hideChildrenToggles} {:else}
<th class="has-toggle"></th> <table>
{/if} {#if showHeaders && columns.some(col => col.title)}
<thead>
<tr>
{#if !hideChildrenToggles}
<th class="has-toggle"></th>
{/if}
<th class="has-icon"></th> <th class="has-icon"></th>
{#each columns as column} {#each columns as column}
<th scope="col">{column.title || ''}</th> <th scope="col">{column.title || ''}</th>
{/each} {/each}
{#if canRemoveItems} {#if canRemoveItems}
<th class="has-button"></th> <th class="has-button"></th>
{/if} {/if}
</tr> </tr>
</thead> </thead>
{/if} {/if}
<tbody> <tbody>
<GridItems <GridItems
{items} {items}
{columns} {columns}
{key} {key}
{striped} {striped}
{canSelect} {canSelect}
{canRemoveItems} {canRemoveItems}
{hideObjectIndicators} {hideObjectIndicators}
{hideChildrenToggles} {hideChildrenToggles}
bind:activePath bind:activePath
bind:inputsValid bind:inputsValid
on:select on:select
on:trigger on:trigger
on:removeItem on:removeItem
/> />
</tbody> </tbody>
</table> </table>
{/if}
</div> </div>
<style> <style>
@ -75,15 +84,6 @@
background-color: #fff; background-color: #fff;
} }
/* .actions {
margin-bottom: 0.5rem;
padding: 0.5rem;
border-bottom: 1px solid #ccc;
}
.actions button {
margin-right: 0.2rem;
} */
table { table {
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
@ -99,7 +99,20 @@
padding: 2px; padding: 2px;
} }
.grid :global(.blankstate) {
height: 100%;
padding: 1rem;
}
/* tfoot button { /* tfoot button {
margin-top: 0.5rem; margin-top: 0.5rem;
}
.actions {
margin-bottom: 0.5rem;
padding: 0.5rem;
border-bottom: 1px solid #ccc;
}
.actions button {
margin-right: 0.2rem;
} */ } */
</style> </style>

View File

@ -14,9 +14,19 @@
width: auto; width: auto;
vertical-align: bottom; vertical-align: bottom;
} }
@keyframes spinning {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
svg.spinning {
animation: spinning 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
}
</style> </style>
<svg xmlns="http://www.w3.org/2000/svg" <svg
xmlns="http://www.w3.org/2000/svg"
width="24" width="24"
height="24" height="24"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@ -24,7 +34,9 @@
stroke="currentColor" stroke="currentColor"
stroke-width="2" stroke-width="2"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round"> stroke-linejoin="round"
class:spinning={name === 'loading'}
>
{#if name === 'radio'} {#if name === 'radio'}
<circle cx="12" cy="12" r="2"></circle><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"></path> <circle cx="12" cy="12" r="2"></circle><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"></path>
{:else if name === 'chev-l'} {:else if name === 'chev-l'}
@ -126,5 +138,9 @@
<path d="M18 20V10M12 20V4M6 20v-6" /> <path d="M18 20V10M12 20V4M6 20v-6" />
{:else if name === '?'} {:else if name === '?'}
<circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line> <circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line>
{:else if name === '!'}
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0zM12 9v4M12 17h.01" />
{:else if name === 'loading'}
<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" />
{/if} {/if}
</svg> </svg>

View File

@ -8,6 +8,9 @@
export let activePath = []; export let activePath = [];
export let hideObjectIndicators = false; export let hideObjectIndicators = false;
export let getRootMenu = () => undefined; export let getRootMenu = () => undefined;
export let errorTitle = '';
export let errorDescription = '';
export let busy = false;
const columns = [ const columns = [
{ key: 'key', label: 'Key' }, { key: 'key', label: 'Key' },
@ -116,4 +119,7 @@
{columns} {columns}
{items} {items}
{hideObjectIndicators} {hideObjectIndicators}
{errorTitle}
{errorDescription}
{busy}
/> />

View File

@ -36,3 +36,9 @@
<button on:click={close} class="btn secondary">Cancel</button> <button on:click={close} class="btn secondary">Cancel</button>
</svelte:fragment> </svelte:fragment>
</Modal> </Modal>
<style>
p {
line-height: 1.25;
}
</style>

View File

@ -44,10 +44,11 @@ async function refresh() {
host.uri = hostDetails.uri; host.uri = hostDetails.uri;
host.open = async function() { host.open = async function() {
const progress = startProgress(`Connecting to "${hostKey}"…`); const { databases: dbNames, status, statusError, systemInfo, systemInfoError } = await OpenConnection(hostKey);
const { databases: dbNames, status, systemInfo } = await OpenConnection(hostKey);
host.status = status; host.status = status;
host.statusError = statusError;
host.systemInfo = systemInfo; host.systemInfo = systemInfo;
host.systemInfoError = systemInfoError;
host.databases = host.databases || {}; host.databases = host.databases || {};
if (!dbNames) { if (!dbNames) {
@ -71,9 +72,9 @@ async function refresh() {
delete database.new; delete database.new;
database.open = async function() { database.open = async function() {
const progress = startProgress(`Opening database "${dbKey}"…`); const { collections: collNames, stats, statsError } = await OpenDatabase(hostKey, dbKey);
const { collections: collNames, stats } = await OpenDatabase(hostKey, dbKey);
database.stats = stats; database.stats = stats;
database.statsError = statsError;
if (!collNames) { if (!collNames) {
return; return;
@ -97,11 +98,12 @@ async function refresh() {
delete collection.new; delete collection.new;
collection.open = async function() { collection.open = async function() {
const progress = startProgress(`Opening database "${dbKey}"…`); const { stats, statsError } = await OpenCollection(hostKey, dbKey, collKey);
const stats = await OpenCollection(hostKey, dbKey, collKey);
collection.stats = stats; collection.stats = stats;
collection.statsError = statsError;
await refresh(); await refresh();
progress.end();
}; };
collection.rename = async function() { collection.rename = async function() {
@ -168,7 +170,12 @@ async function refresh() {
collection.getIndexes = async function() { collection.getIndexes = async function() {
const progress = startProgress(`Retrieving indexes of "${collKey}"…`); const progress = startProgress(`Retrieving indexes of "${collKey}"…`);
collection.indexes = []; collection.indexes = [];
const indexes = await GetIndexes(hostKey, dbKey, collKey); const { indexes, error } = await GetIndexes(hostKey, dbKey, collKey);
if (error) {
progress.end();
return error;
}
for (const indexDetails of indexes) { for (const indexDetails of indexes) {
const index = { const index = {
@ -190,7 +197,6 @@ async function refresh() {
} }
progress.end(); progress.end();
return collection.indexes;
}; };
collection.getIndexByName = function(indesName) { collection.getIndexByName = function(indesName) {
@ -236,7 +242,6 @@ async function refresh() {
} }
await refresh(); await refresh();
progress.end();
windowTitle.setSegments(dbKey, host.name, 'Rolens'); windowTitle.setSegments(dbKey, host.name, 'Rolens');
}; };
@ -283,7 +288,6 @@ async function refresh() {
}; };
await refresh(); await refresh();
progress.end();
}; };
host.remove = async function() { host.remove = async function() {

View File

@ -46,8 +46,7 @@
return; return;
} }
querying = true; querying = `Querying ${collection.key}…`;
const progress = startProgress('Performing query…');
activePath = []; activePath = [];
const newResult = await FindItems(collection.hostKey, collection.dbKey, collection.key, JSON.stringify(form)); const newResult = await FindItems(collection.hostKey, collection.dbKey, collection.key, JSON.stringify(form));
@ -57,7 +56,6 @@
submittedForm = deepClone(form); submittedForm = deepClone(form);
} }
progress.end();
resetFocus(); resetFocus();
querying = false; querying = false;
} }
@ -231,6 +229,9 @@
hideObjectIndicators={$views[collection.viewKey]?.hideObjectIndicators} hideObjectIndicators={$views[collection.viewKey]?.hideObjectIndicators}
bind:activePath bind:activePath
on:trigger={e => openJson(e.detail?.index)} on:trigger={e => openJson(e.detail?.index)}
errorTitle={result.errorTitle}
errorDescription={result.errorDescription}
busy={querying}
/> />
{:else} {:else}
<Grid <Grid
@ -242,6 +243,9 @@
items={result.results ? result.results.map(r => EJSON.deserialize(r)) : []} items={result.results ? result.results.map(r => EJSON.deserialize(r)) : []}
bind:activePath bind:activePath
on:trigger={e => openJson(e.detail?.index)} on:trigger={e => openJson(e.detail?.index)}
errorTitle={result.errorTitle}
errorDescription={result.errorDescription}
busy={querying}
/> />
{/if} {/if}
{/key} {/key}

View File

@ -7,18 +7,21 @@
let activePath = []; let activePath = [];
let _indexes = []; let _indexes = [];
let error = '';
async function refresh() { async function refresh() {
await collection.getIndexes(); error = await collection.getIndexes();
_indexes = collection.indexes.map(idx => { if (!error) {
return { _indexes = collection.indexes.map(idx => {
name: idx.name, return {
background: idx.background || false, name: idx.name,
unique: idx.unique || false, background: idx.background || false,
sparse: idx.sparse || false, unique: idx.unique || false,
model: idx.model, sparse: idx.sparse || false,
}; model: idx.model,
}); };
});
}
} }
async function createIndex() { async function createIndex() {
@ -50,6 +53,8 @@
key="name" key="name"
data={_indexes} data={_indexes}
getRootMenu={(_, idx) => [ { label: 'Drop this index', fn: () => dropIndex(idx.name) } ]} getRootMenu={(_, idx) => [ { label: 'Drop this index', fn: () => dropIndex(idx.name) } ]}
errorTitle={error ? 'Error while getting indexes' : ''}
errorDescription={error}
bind:activePath bind:activePath
/> />
</div> </div>

View File

@ -18,11 +18,16 @@
<!-- <CodeExample code="db.stats()" /> --> <!-- <CodeExample code="db.stats()" /> -->
<div class="grid"> <div class="grid">
<ObjectGrid data={collection.stats} /> <ObjectGrid
data={collection.stats}
errorTitle={collection.statsError ? 'Error fetching collection stats' : ''}
errorDescription={collection.statsError}
busy={!collection.stats && !collection.statsError && `Fetching stats for ${collection.key}`}
/>
</div> </div>
<div class="buttons"> <div class="buttons">
<button class="btn secondary" on:click={copy}> <button class="btn secondary" on:click={copy} disabled={!collection.stats}>
<Icon name={copySucceeded ? 'check' : 'clipboard'} /> <Icon name={copySucceeded ? 'check' : 'clipboard'} />
Copy JSON Copy JSON
</button> </button>

View File

@ -18,11 +18,16 @@
<!-- <CodeExample code="db.stats()" /> --> <!-- <CodeExample code="db.stats()" /> -->
<div class="grid"> <div class="grid">
<ObjectGrid data={database.stats} /> <ObjectGrid
data={database.stats}
errorTitle={database.statsError ? 'Error fetching database stats' : ''}
errorDescription={database.statsError}
busy={!database.stats && !database.statsError && `Fetching stats for ${database.key}`}
/>
</div> </div>
<div class="buttons"> <div class="buttons">
<button class="btn secondary" on:click={copy}> <button class="btn secondary" on:click={copy} disabled={!database.stats}>
<Icon name={copySucceeded ? 'check' : 'clipboard'} /> <Icon name={copySucceeded ? 'check' : 'clipboard'} />
Copy JSON Copy JSON
</button> </button>

View File

@ -18,11 +18,16 @@
<!-- <CodeExample code="db.stats()" /> --> <!-- <CodeExample code="db.stats()" /> -->
<div class="grid"> <div class="grid">
<ObjectGrid data={host.status} /> <ObjectGrid
data={host.status || {}}
errorTitle={host.statusError ? 'Error fetching server status' : ''}
errorDescription={host.statusError}
busy={!host.status && !host.statusError && 'Fetching server status…'}
/>
</div> </div>
<div class="buttons"> <div class="buttons">
<button class="btn secondary" on:click={copy}> <button class="btn secondary" on:click={copy} disabled={!host.status}>
<Icon name={copySucceeded ? 'check' : 'clipboard'} /> <Icon name={copySucceeded ? 'check' : 'clipboard'} />
Copy JSON Copy JSON
</button> </button>

View File

@ -18,11 +18,16 @@
<!-- <CodeExample code="db.stats()" /> --> <!-- <CodeExample code="db.stats()" /> -->
<div class="grid"> <div class="grid">
<ObjectGrid data={host.systemInfo} /> <ObjectGrid
data={host.systemInfo}
errorTitle={host.systemInfoError ? 'Error fetching system info' : ''}
errorDescription={host.systemInfoError}
busy={!host.systemInfo && !host.systemInfoError && 'Fetching system info…'}
/>
</div> </div>
<div class="buttons"> <div class="buttons">
<button class="btn secondary" on:click={copy}> <button class="btn secondary" on:click={copy} disabled={!host.systemInfo}>
<Icon name={copySucceeded ? 'check' : 'clipboard'} /> <Icon name={copySucceeded ? 'check' : 'clipboard'} />
Copy JSON Copy JSON
</button> </button>

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

@ -1,7 +1,6 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT // This file is automatically generated. DO NOT EDIT
import {app} from '../models'; import {app} from '../models';
import {primitive} from '../models';
import {map[string]app} from '../models'; import {map[string]app} from '../models';
import {menu} from '../models'; import {menu} from '../models';
import {context} from '../models'; import {context} from '../models';
@ -21,9 +20,9 @@ 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 FindItems(arg1:string,arg2:string,arg3:string,arg4:string):Promise<app.QueryResult>; export function FindItems(arg1:string,arg2:string,arg3:string,arg4:string):Promise<app.FindItemsResult>;
export function GetIndexes(arg1:string,arg2:string,arg3:string):Promise<Array<primitive.M>>; export function GetIndexes(arg1:string,arg2:string,arg3:string):Promise<app.GetIndexesResult>;
export function Hosts():Promise<map[string]app.Host>; export function Hosts():Promise<map[string]app.Host>;
@ -31,11 +30,11 @@ export function InsertItems(arg1:string,arg2:string,arg3:string,arg4:string):Pro
export function Menu():Promise<menu.Menu>; export function Menu():Promise<menu.Menu>;
export function OpenCollection(arg1:string,arg2:string,arg3:string):Promise<primitive.M>; export function OpenCollection(arg1:string,arg2:string,arg3:string):Promise<app.OpenCollectionResult>;
export function OpenConnection(arg1:string):Promise<app.HostInfo>; export function OpenConnection(arg1:string):Promise<app.OpenConnectionResult>;
export function OpenDatabase(arg1:string,arg2:string):Promise<app.DatabaseInfo>; export function OpenDatabase(arg1:string,arg2:string):Promise<app.OpenDatabaseResult>;
export function PerformDump(arg1:string):Promise<boolean>; export function PerformDump(arg1:string):Promise<boolean>;

View File

@ -1,97 +0,0 @@
export namespace app {
export class DatabaseInfo {
collections: string[];
stats: {[key: string]: any};
static createFrom(source: any = {}) {
return new DatabaseInfo(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.collections = source["collections"];
this.stats = source["stats"];
}
}
export class EnvironmentInfo {
arch: string;
buildType: string;
platform: string;
version: string;
hasMongoExport: boolean;
hasMongoDump: boolean;
homeDirectory: string;
dataDirectory: string;
logDirectory: string;
downloadDirectory: string;
static createFrom(source: any = {}) {
return new EnvironmentInfo(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.arch = source["arch"];
this.buildType = source["buildType"];
this.platform = source["platform"];
this.version = source["version"];
this.hasMongoExport = source["hasMongoExport"];
this.hasMongoDump = source["hasMongoDump"];
this.homeDirectory = source["homeDirectory"];
this.dataDirectory = source["dataDirectory"];
this.logDirectory = source["logDirectory"];
this.downloadDirectory = source["downloadDirectory"];
}
}
export class HostInfo {
databases: string[];
status: {[key: string]: any};
systemInfo: {[key: string]: any};
static createFrom(source: any = {}) {
return new HostInfo(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.databases = source["databases"];
this.status = source["status"];
this.systemInfo = source["systemInfo"];
}
}
export class QueryResult {
total: number;
results: string[];
static createFrom(source: any = {}) {
return new QueryResult(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.total = source["total"];
this.results = source["results"];
}
}
export class Settings {
defaultLimit: number;
defaultSort: string;
autosubmitQuery: boolean;
defaultExportDirectory: string;
static createFrom(source: any = {}) {
return new Settings(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.defaultLimit = source["defaultLimit"];
this.defaultSort = source["defaultSort"];
this.autosubmitQuery = source["autosubmitQuery"];
this.defaultExportDirectory = source["defaultExportDirectory"];
}
}
}

View File

@ -8,23 +8,27 @@ import (
"go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson"
) )
func (a *App) OpenCollection(hostKey, dbKey, collKey string) (result bson.M) { type OpenCollectionResult struct {
Stats bson.M `json:"stats"`
StatsError string `json:"statsError"`
}
func (a *App) OpenCollection(hostKey, dbKey, collKey string) (result OpenCollectionResult) {
client, ctx, close, err := a.connectToHost(hostKey) client, ctx, close, err := a.connectToHost(hostKey)
if err != nil { if err != nil {
return nil return
} }
defer close()
command := bson.M{"collStats": collKey} command := bson.M{"collStats": collKey}
err = client.Database(dbKey).RunCommand(ctx, command).Decode(&result) err = client.Database(dbKey).RunCommand(ctx, command).Decode(&result.Stats)
if err != nil { if err != nil {
runtime.LogWarning(a.ctx, "Could not retrieve collection stats for "+collKey) runtime.LogWarning(a.ctx, "Could not retrieve collection stats for "+collKey)
runtime.LogWarning(a.ctx, err.Error()) runtime.LogWarning(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Could not get stats"), zenity.ErrorIcon) result.StatsError = err.Error()
return nil
} }
defer close() return
return result
} }
func (a *App) RenameCollection(hostKey, dbKey, collKey, newCollKey string) bool { func (a *App) RenameCollection(hostKey, dbKey, collKey, newCollKey string) bool {
@ -32,6 +36,7 @@ func (a *App) RenameCollection(hostKey, dbKey, collKey, newCollKey string) bool
if err != nil { if err != nil {
return false return false
} }
defer close()
var result bson.M var result bson.M
command := bson.D{ command := bson.D{
@ -46,7 +51,6 @@ func (a *App) RenameCollection(hostKey, dbKey, collKey, newCollKey string) bool
return false return false
} }
defer close()
return true return true
} }
@ -60,6 +64,7 @@ func (a *App) TruncateCollection(hostKey, dbKey, collKey string) bool {
if err != nil { if err != nil {
return false return false
} }
defer close()
_, err = client.Database(dbKey).Collection(collKey).DeleteMany(ctx, bson.D{}) _, err = client.Database(dbKey).Collection(collKey).DeleteMany(ctx, bson.D{})
if err != nil { if err != nil {
@ -69,7 +74,6 @@ func (a *App) TruncateCollection(hostKey, dbKey, collKey string) bool {
return false return false
} }
defer close()
return true return true
} }
@ -83,6 +87,7 @@ func (a *App) DropCollection(hostKey, dbKey, collKey string) bool {
if err != nil { if err != nil {
return false return false
} }
defer close()
err = client.Database(dbKey).Collection(collKey).Drop(ctx) err = client.Database(dbKey).Collection(collKey).Drop(ctx)
if err != nil { if err != nil {
@ -92,6 +97,5 @@ func (a *App) DropCollection(hostKey, dbKey, collKey string) bool {
return false return false
} }
defer close()
return true return true
} }

View File

@ -17,13 +17,14 @@ type Query struct {
Sort string `json:"sort"` Sort string `json:"sort"`
} }
type QueryResult struct { type FindItemsResult struct {
Total int64 `json:"total"` Total int64 `json:"total"`
Results []string `json:"results"` Results []string `json:"results"`
ErrorTitle string `json:"errorTitle"`
ErrorDescription string `json:"errorDescription"`
} }
func (a *App) FindItems(hostKey, dbKey, collKey, formJson string) QueryResult { func (a *App) FindItems(hostKey, dbKey, collKey, formJson string) (result FindItemsResult) {
var out QueryResult
var form Query var form Query
err := json.Unmarshal([]byte(formJson), &form) err := json.Unmarshal([]byte(formJson), &form)
@ -31,15 +32,15 @@ func (a *App) FindItems(hostKey, dbKey, collKey, formJson string) QueryResult {
runtime.LogError(a.ctx, "Could not parse find form:") runtime.LogError(a.ctx, "Could not parse find form:")
runtime.LogError(a.ctx, err.Error()) runtime.LogError(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Could not parse form"), zenity.ErrorIcon) zenity.Error(err.Error(), zenity.Title("Could not parse form"), zenity.ErrorIcon)
return out return
} }
client, ctx, close, err := a.connectToHost(hostKey) client, ctx, close, err := a.connectToHost(hostKey)
if err != nil { if err != nil {
return out return
} }
defer close() defer close()
var query bson.M var query bson.M
var projection bson.M var projection bson.M
var sort bson.M var sort bson.M
@ -47,22 +48,25 @@ func (a *App) FindItems(hostKey, dbKey, collKey, formJson string) QueryResult {
err = bson.UnmarshalExtJSON([]byte(form.Query), true, &query) err = bson.UnmarshalExtJSON([]byte(form.Query), true, &query)
if err != nil { if err != nil {
runtime.LogInfof(a.ctx, "Invalid find query: %s", err.Error()) runtime.LogInfof(a.ctx, "Invalid find query: %s", err.Error())
zenity.Error(err.Error(), zenity.Title("Invalid query"), zenity.ErrorIcon) result.ErrorTitle = "Invalid query"
return out result.ErrorDescription = err.Error()
return
} }
err = json.Unmarshal([]byte(form.Fields), &projection) err = json.Unmarshal([]byte(form.Fields), &projection)
if err != nil { if err != nil {
runtime.LogInfof(a.ctx, "Invalid find projection: %s", err.Error()) runtime.LogInfof(a.ctx, "Invalid find projection: %s", err.Error())
zenity.Error(err.Error(), zenity.Title("Invalid projection"), zenity.ErrorIcon) result.ErrorTitle = "Invalid projection"
return out result.ErrorDescription = err.Error()
return
} }
err = json.Unmarshal([]byte(form.Sort), &sort) err = json.Unmarshal([]byte(form.Sort), &sort)
if err != nil { if err != nil {
runtime.LogInfof(a.ctx, "Invalid find sort: %s", err.Error()) runtime.LogInfof(a.ctx, "Invalid find sort: %s", err.Error())
zenity.Error(err.Error(), zenity.Title("Invalid sort"), zenity.ErrorIcon) result.ErrorTitle = "Invalid sort"
return out result.ErrorDescription = err.Error()
return
} }
opt := mongoOptions.FindOptions{ opt := mongoOptions.FindOptions{
@ -75,15 +79,17 @@ func (a *App) FindItems(hostKey, dbKey, collKey, formJson string) QueryResult {
total, err := client.Database(dbKey).Collection(collKey).CountDocuments(ctx, query, nil) total, err := client.Database(dbKey).Collection(collKey).CountDocuments(ctx, query, nil)
if err != nil { if err != nil {
runtime.LogWarningf(a.ctx, "Encountered an error while counting documents: %s", err.Error()) runtime.LogWarningf(a.ctx, "Encountered an error while counting documents: %s", err.Error())
zenity.Error(err.Error(), zenity.Title("Error while counting docs"), zenity.ErrorIcon) result.ErrorTitle = "Error while counting documents"
return out result.ErrorDescription = err.Error()
return
} }
cur, err := client.Database(dbKey).Collection(collKey).Find(ctx, query, &opt) cur, err := client.Database(dbKey).Collection(collKey).Find(ctx, query, &opt)
if err != nil { if err != nil {
runtime.LogWarningf(a.ctx, "Encountered an error while performing query: %s", err.Error()) runtime.LogWarningf(a.ctx, "Encountered an error while performing query: %s", err.Error())
zenity.Error(err.Error(), zenity.Title("Error while querying"), zenity.ErrorIcon) result.ErrorTitle = "Error while querying"
return out result.ErrorDescription = err.Error()
return
} }
defer cur.Close(ctx) defer cur.Close(ctx)
@ -92,25 +98,27 @@ func (a *App) FindItems(hostKey, dbKey, collKey, formJson string) QueryResult {
if err != nil { if err != nil {
runtime.LogWarningf(a.ctx, "Encountered an error while performing query: %s", err.Error()) runtime.LogWarningf(a.ctx, "Encountered an error while performing query: %s", err.Error())
zenity.Error(err.Error(), zenity.Title("Error while querying"), zenity.ErrorIcon) result.ErrorTitle = "Error while querying"
return out result.ErrorDescription = err.Error()
return
} }
out.Total = total result.Total = total
out.Results = make([]string, 0) result.Results = make([]string, 0)
for _, r := range results { for _, r := range results {
marshalled, err := bson.MarshalExtJSON(r, true, true) marshalled, err := bson.MarshalExtJSON(r, true, true)
if err != nil { if err != nil {
runtime.LogError(a.ctx, "Failed to marshal find BSON:") runtime.LogError(a.ctx, "Failed to marshal find BSON:")
runtime.LogError(a.ctx, err.Error()) runtime.LogError(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Failed to marshal JSON"), zenity.ErrorIcon) result.ErrorTitle = "Failed to marshal JSON"
return out result.ErrorDescription = err.Error()
return
} }
out.Results = append(out.Results, string(marshalled)) result.Results = append(result.Results, string(marshalled))
} }
return out return
} }
func (a *App) UpdateFoundDocument(hostKey, dbKey, collKey, idJson, newDocJson string) bool { func (a *App) UpdateFoundDocument(hostKey, dbKey, collKey, idJson, newDocJson string) bool {

View File

@ -11,10 +11,15 @@ import (
"go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/mongo/options"
) )
func (a *App) GetIndexes(hostKey, dbKey, collKey string) []bson.M { type GetIndexesResult struct {
Indexes []bson.M `json:"indexes"`
Error string `json:"error"`
}
func (a *App) GetIndexes(hostKey, dbKey, collKey string) (result GetIndexesResult) {
client, ctx, close, err := a.connectToHost(hostKey) client, ctx, close, err := a.connectToHost(hostKey)
if err != nil { if err != nil {
return nil return
} }
defer close() defer close()
@ -22,20 +27,18 @@ func (a *App) GetIndexes(hostKey, dbKey, collKey string) []bson.M {
if err != nil { if err != nil {
runtime.LogWarning(a.ctx, "Encountered an error while creating index cursor:") runtime.LogWarning(a.ctx, "Encountered an error while creating index cursor:")
runtime.LogWarning(a.ctx, err.Error()) runtime.LogWarning(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Error while creating cursor"), zenity.ErrorIcon) result.Error = err.Error()
return nil return
} }
var results []bson.M err = cur.All(ctx, &result.Indexes)
err = cur.All(ctx, &results)
if err != nil { if err != nil {
runtime.LogWarning(a.ctx, "Encountered an error while executing index cursor:") runtime.LogWarning(a.ctx, "Encountered an error while executing index cursor:")
runtime.LogWarning(a.ctx, err.Error()) runtime.LogWarning(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Error while running cursor"), zenity.ErrorIcon) result.Error = err.Error()
return nil
} }
return results return
} }
func (a *App) CreateIndex(hostKey, dbKey, collKey, jsonData string) string { func (a *App) CreateIndex(hostKey, dbKey, collKey, jsonData string) string {

View File

@ -28,8 +28,8 @@ func (a *App) InsertItems(hostKey, dbKey, collKey, jsonData string) interface{}
if err != nil { if err != nil {
return nil return nil
} }
defer close() defer close()
res, err := client.Database(dbKey).Collection(collKey).InsertMany(ctx, data) res, err := client.Database(dbKey).Collection(collKey).InsertMany(ctx, data)
if err != nil { if err != nil {
runtime.LogWarning(a.ctx, "Encountered an error while performing insert:") runtime.LogWarning(a.ctx, "Encountered an error while performing insert:")

View File

@ -12,10 +12,12 @@ import (
mongoOptions "go.mongodb.org/mongo-driver/mongo/options" mongoOptions "go.mongodb.org/mongo-driver/mongo/options"
) )
type HostInfo struct { type OpenConnectionResult struct {
Databases []string `json:"databases"` Databases []string `json:"databases"`
Status bson.M `json:"status"` Status bson.M `json:"status"`
SystemInfo bson.M `json:"systemInfo"` StatusError string `json:"statusError"`
SystemInfo bson.M `json:"systemInfo"`
SystemInfoError string `json:"systemInfoError"`
} }
func (a *App) connectToHost(hostKey string) (*mongo.Client, context.Context, func(), error) { func (a *App) connectToHost(hostKey string) (*mongo.Client, context.Context, func(), error) {
@ -49,38 +51,35 @@ func (a *App) connectToHost(hostKey string) (*mongo.Client, context.Context, fun
}, nil }, nil
} }
func (a *App) OpenConnection(hostKey string) (info HostInfo) { func (a *App) OpenConnection(hostKey string) (result OpenConnectionResult) {
client, ctx, close, err := a.connectToHost(hostKey) client, ctx, close, err := a.connectToHost(hostKey)
if err != nil { if err != nil {
return return
} }
defer close()
info.Databases, err = client.ListDatabaseNames(ctx, bson.M{}) result.Databases, err = client.ListDatabaseNames(ctx, bson.M{})
if err != nil { if err != nil {
runtime.LogWarning(a.ctx, "Could not retrieve database names for host "+hostKey) runtime.LogWarning(a.ctx, "Could not retrieve database names for host "+hostKey)
runtime.LogWarning(a.ctx, err.Error()) runtime.LogWarning(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Error while getting databases"), zenity.ErrorIcon) zenity.Error(err.Error(), zenity.Title("Error while getting databases"), zenity.ErrorIcon)
return
} }
command := bson.M{"serverStatus": 1} command := bson.M{"serverStatus": 1}
err = client.Database("admin").RunCommand(ctx, command).Decode(&info.Status) err = client.Database("admin").RunCommand(ctx, command).Decode(&result.Status)
if err != nil { if err != nil {
runtime.LogWarning(a.ctx, "Could not retrieve server status") runtime.LogWarning(a.ctx, "Could not retrieve server status")
runtime.LogWarning(a.ctx, err.Error()) runtime.LogWarning(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Could not get server status"), zenity.ErrorIcon) result.StatusError = err.Error()
return
} }
command = bson.M{"hostInfo": 1} command = bson.M{"hostInfo": 1}
err = client.Database("admin").RunCommand(ctx, command).Decode(&info.SystemInfo) err = client.Database("admin").RunCommand(ctx, command).Decode(&result.SystemInfo)
if err != nil { if err != nil {
runtime.LogWarning(a.ctx, "Could not retrieve system info") runtime.LogWarning(a.ctx, "Could not retrieve system info")
runtime.LogWarning(a.ctx, err.Error()) runtime.LogWarning(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Could not get system info"), zenity.ErrorIcon) result.SystemInfoError = err.Error()
return
} }
defer close()
return return
} }

View File

@ -6,35 +6,34 @@ import (
"go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson"
) )
type DatabaseInfo struct { type OpenDatabaseResult struct {
Collections []string `json:"collections"` Collections []string `json:"collections"`
Stats bson.M `json:"stats"` Stats bson.M `json:"stats"`
StatsError string `json:"statsError"`
} }
func (a *App) OpenDatabase(hostKey, dbKey string) (info DatabaseInfo) { func (a *App) OpenDatabase(hostKey, dbKey string) (result OpenDatabaseResult) {
client, ctx, close, err := a.connectToHost(hostKey) client, ctx, close, err := a.connectToHost(hostKey)
if err != nil { if err != nil {
return return
} }
defer close()
command := bson.M{"dbStats": 1} command := bson.M{"dbStats": 1}
err = client.Database(dbKey).RunCommand(ctx, command).Decode(&info.Stats) err = client.Database(dbKey).RunCommand(ctx, command).Decode(&result.Stats)
if err != nil { if err != nil {
runtime.LogWarning(a.ctx, "Could not retrieve database stats for "+dbKey) runtime.LogWarning(a.ctx, "Could not retrieve database stats for "+dbKey)
runtime.LogWarning(a.ctx, err.Error()) runtime.LogWarning(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Could not get stats"), zenity.ErrorIcon) result.StatsError = err.Error()
return
} }
info.Collections, err = client.Database(dbKey).ListCollectionNames(ctx, bson.D{}) result.Collections, err = client.Database(dbKey).ListCollectionNames(ctx, bson.D{})
if err != nil { if err != nil {
runtime.LogWarning(a.ctx, "Could not retrieve collection list for db "+dbKey) runtime.LogWarning(a.ctx, "Could not retrieve collection list for db "+dbKey)
runtime.LogWarning(a.ctx, err.Error()) runtime.LogWarning(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Error while getting collections"), zenity.ErrorIcon) zenity.Error(err.Error(), zenity.Title("Error while getting collections"), zenity.ErrorIcon)
return
} }
defer close()
return return
} }
@ -48,6 +47,7 @@ func (a *App) DropDatabase(hostKey, dbKey string) bool {
if err != nil { if err != nil {
return false return false
} }
defer close()
err = client.Database(dbKey).Drop(ctx) err = client.Database(dbKey).Drop(ctx)
if err != nil { if err != nil {
@ -57,6 +57,5 @@ func (a *App) DropDatabase(hostKey, dbKey string) bool {
return false return false
} }
defer close()
return true return true
} }