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

Implement OOP hosttree (#32)

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
Romein van Buren 2023-06-18 21:31:55 +02:00 committed by GitHub
parent 3fe5f09163
commit a1456b3987
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 624 additions and 593 deletions

View File

@ -15,6 +15,7 @@
<div></div> <div></div>
</div> </div>
</div> </div>
<div id="dialogoutlets"></div>
<script src="./src/main.js" type="module"></script> <script src="./src/main.js" type="module"></script>
</body> </body>
</html> </html>

View File

@ -1 +1 @@
499da1237327bf4ddd2de74bfd6635c7 5d3dd16f94d140a9f950a48981534162

View File

@ -1,39 +1,44 @@
<script> <script>
import BlankState from '$components/blankstate.svelte'; import BlankState from '$components/blankstate.svelte';
import ContextMenu from '$components/contextmenu.svelte'; import ContextMenu from '$components/contextmenu.svelte';
import dialogs from '$lib/dialogs';
import contextMenu from '$lib/stores/contextmenu'; import contextMenu from '$lib/stores/contextmenu';
import environment from '$lib/stores/environment'; import environment from '$lib/stores/environment';
import hosts from '$lib/stores/hosts'; import hostTree from '$lib/stores/hosttree';
import applicationInited from '$lib/stores/inited'; import applicationInited from '$lib/stores/inited';
import windowTitle from '$lib/stores/windowtitle'; import windowTitle from '$lib/stores/windowtitle';
import About from '$organisms/about.svelte';
import Connection from '$organisms/connection/index.svelte'; import Connection from '$organisms/connection/index.svelte';
import Settings from '$organisms/settings/index.svelte';
import { EventsOn } from '$wails/runtime'; import { EventsOn } from '$wails/runtime';
import { tick } from 'svelte'; import { tick } from 'svelte';
import AboutDialog from './dialogs/about.svelte';
import SettingsDialog from './dialogs/settings/index.svelte';
const activeHostKey = '';
let activeDbKey = '';
let activeCollKey = '';
let settingsModalOpen = false;
let aboutModalOpen = false;
let connectionManager;
let showWelcomeScreen = undefined; let showWelcomeScreen = undefined;
hosts.subscribe(h => { applicationInited.defer(() => {
if (h && (showWelcomeScreen === undefined)) { hostTree.subscribe(hosts => {
showWelcomeScreen = !Object.keys($hosts || {}).length; if (hostTree.hasBeenInited() && (showWelcomeScreen === undefined)) {
} showWelcomeScreen = !Object.keys(hosts || {}).length;
}
});
}); });
async function createFirstHost() { async function createFirstHost() {
showWelcomeScreen = false; showWelcomeScreen = false;
await tick(); await tick();
connectionManager.createHost(); hostTree.newHost();
} }
EventsOn('OpenPreferences', () => settingsModalOpen = true); function showAboutDialog() {
EventsOn('OpenAboutModal', () => aboutModalOpen = true); dialogs.new(AboutDialog);
}
function showSettings() {
dialogs.new(SettingsDialog);
}
EventsOn('OpenPreferences', showSettings);
EventsOn('OpenAboutModal', showAboutDialog);
</script> </script>
<svelte:window on:contextmenu|preventDefault /> <svelte:window on:contextmenu|preventDefault />
@ -41,23 +46,20 @@
<div id="root" class="platform-{$environment?.platform}"> <div id="root" class="platform-{$environment?.platform}">
<div class="titlebar">{$windowTitle}</div> <div class="titlebar">{$windowTitle}</div>
{#if $applicationInited && $hosts && (showWelcomeScreen !== undefined)} {#if $applicationInited && (showWelcomeScreen !== undefined)}
<main class:empty={showWelcomeScreen}> <main class:empty={showWelcomeScreen}>
{#if showWelcomeScreen} {#if showWelcomeScreen}
<BlankState label="Welcome to Rolens!" image="/logo.png" pale={false} big={true}> <BlankState label="Welcome to Rolens!" image="/logo.png" pale={false} big={true}>
<button class="btn" on:click={createFirstHost}>Add your first host</button> <button class="btn" on:click={createFirstHost}>Add your first host</button>
</BlankState> </BlankState>
{:else} {:else}
<Connection {activeHostKey} bind:activeCollKey bind:activeDbKey bind:this={connectionManager} /> <Connection />
{/if} {/if}
</main> </main>
{#key $contextMenu} {#key $contextMenu}
<ContextMenu {...$contextMenu} on:close={contextMenu.hide} /> <ContextMenu {...$contextMenu} on:close={contextMenu.hide} />
{/key} {/key}
<Settings bind:show={settingsModalOpen} />
<About bind:show={aboutModalOpen} />
{/if} {/if}
</div> </div>

View File

@ -66,7 +66,7 @@
activeKey = itemKey; activeKey = itemKey;
activePath = [ ...path.slice(0, level), itemKey ]; activePath = [ ...path.slice(0, level), itemKey ];
dispatch('select', { level, itemKey, index }); dispatch('select', { level, itemKey, index, path: activePath });
} }
function closeAll() { function closeAll() {

View File

@ -4,15 +4,17 @@
<script> <script>
import { Beep } from '$wails/go/ui/UI'; import { Beep } from '$wails/go/ui/UI';
import { createEventDispatcher } from 'svelte';
import { fade, fly } from 'svelte/transition'; import { fade, fly } from 'svelte/transition';
import Icon from './icon.svelte'; import Icon from './icon.svelte';
export let show = false; export let show = true;
export let title = undefined; export let title = undefined;
export let contentPadding = true; export let contentPadding = true;
export let width = '80vw'; export let width = '80vw';
export let overflow = true; export let overflow = true;
const dispatch = createEventDispatcher();
const level = numberOfModalsOpen + 1; const level = numberOfModalsOpen + 1;
let isNew = true; let isNew = true;
@ -29,9 +31,13 @@
function keydown(event) { function keydown(event) {
if ((event.key === 'Escape') && (level === numberOfModalsOpen)) { if ((event.key === 'Escape') && (level === numberOfModalsOpen)) {
event.preventDefault(); event.preventDefault();
show = false; close();
} }
} }
function close() {
dispatch('close');
}
</script> </script>
<svelte:window on:keydown={keydown} /> <svelte:window on:keydown={keydown} />
@ -42,7 +48,7 @@
{#if title} {#if title}
<header> <header>
<div class="title">{title}</div> <div class="title">{title}</div>
<button class="btn close" on:click={() => show = false} title="close" type="button"> <button class="btn close" on:click={close} title="close" type="button">
<Icon name="x" /> <Icon name="x" />
</button> </button>
</header> </header>
@ -69,6 +75,7 @@
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
margin: 0; margin: 0;
padding-top: 50px; padding-top: 50px;
--wails-draggable: drag;
} }
:global(#root.platform-darwin) .outer { :global(#root.platform-darwin) .outer {
margin-top: var(--darwin-titlebar-height); margin-top: var(--darwin-titlebar-height);
@ -86,6 +93,7 @@
flex-flow: column; flex-flow: column;
cursor: auto; cursor: auto;
overflow: hidden; overflow: hidden;
--wails-draggable: nodrag;
} }
.inner > :global(*:first-child) { .inner > :global(*:first-child) {
margin-top: 0; margin-top: 0;

View File

@ -1,10 +1,10 @@
<script> <script>
import { looseJsonIsValid } from '$lib/strings'; import { looseJsonIsValid } from '$lib/strings';
import { EJSON } from 'bson';
import { createEventDispatcher, onDestroy } from 'svelte'; import { createEventDispatcher, onDestroy } from 'svelte';
import Icon from './icon.svelte'; import Icon from './icon.svelte';
import Modal from './modal.svelte'; import Modal from './modal.svelte';
import ObjectEditor from './objecteditor.svelte'; import ObjectEditor from './objecteditor.svelte';
import { EJSON } from 'bson';
export let data; export let data;
export let saveable = false; export let saveable = false;

View File

@ -2,11 +2,9 @@
import Modal from '$components/modal.svelte'; import Modal from '$components/modal.svelte';
import alink from '$lib/actions/alink'; import alink from '$lib/actions/alink';
import environment from '$lib/stores/environment'; import environment from '$lib/stores/environment';
export let show = true;
</script> </script>
<Modal bind:show width="400px" title=" "> <Modal width="400px" title=" " on:close>
<div class="brand"> <div class="brand">
<img src="/logo.png" alt="Rolens logo" /> <img src="/logo.png" alt="Rolens logo" />
<div> <div>

View File

@ -3,11 +3,9 @@
import Modal from '$components/modal.svelte'; import Modal from '$components/modal.svelte';
import input from '$lib/actions/input'; import input from '$lib/actions/input';
import settings from '$lib/stores/settings'; import settings from '$lib/stores/settings';
export let show = false;
</script> </script>
<Modal title="Preferences" bind:show> <Modal title="Preferences" on:close>
<div class="prefs"> <div class="prefs">
<label for="defaultLimit">Initial number of items to retrieve using one query (limit):</label> <label for="defaultLimit">Initial number of items to retrieve using one query (limit):</label>
<label class="field"> <label class="field">

View File

@ -0,0 +1,20 @@
function newDialog(dialogComponent, data = {}) {
const outlet = document.createElement('div');
outlet.className = 'dialogoutlet';
document.getElementById('dialogoutlets').appendChild(outlet);
const instance = new dialogComponent({ target: outlet, props: data });
instance.$close = function() {
instance.$destroy();
outlet.remove();
};
instance.$on('close', instance.$close);
return instance;
}
const dialogs = { new: newDialog };
export default dialogs;

View File

@ -1,5 +0,0 @@
import { writable } from 'svelte/store';
const connections = writable({});
export default connections;

View File

@ -1,11 +0,0 @@
import { Hosts } from '$wails/go/app/App';
import { writable } from 'svelte/store';
import applicationInited from './inited';
const { set, subscribe } = writable();
const update = async() => set(await Hosts());
applicationInited.defer(update);
const hosts = { update, subscribe };
export default hosts;

View File

@ -0,0 +1,312 @@
import dialogs from '$lib/dialogs';
import { startProgress } from '$lib/progress';
import { get, writable } from 'svelte/store';
import applicationInited from './inited';
import queries from './queries';
import windowTitle from './windowtitle';
import ExportDialog from '$organisms/connection/collection/dialogs/export.svelte';
import IndexDetailDialog from '$organisms/connection/collection/dialogs/indexdetail.svelte';
import QueryChooserDialog from '$organisms/connection/collection/dialogs/querychooser.svelte';
import DumpDialog from '$organisms/connection/database/dialogs/dump.svelte';
import HostDetailDialog from '$organisms/connection/host/dialogs/hostdetail.svelte';
import {
CreateIndex,
DropCollection,
DropDatabase,
DropIndex,
GetIndexes,
Hosts,
OpenCollection,
OpenConnection,
OpenDatabase,
PerformDump,
PerformFindExport,
RemoveHost,
RenameCollection,
TruncateCollection
} from '$wails/go/app/App';
const { set, subscribe } = writable({});
const getValue = () => get({ subscribe });
let hostTreeInited = false;
async function refresh() {
const hosts = await Hosts();
const hostTree = getValue();
for (const [ hostKey, hostDetails ] of Object.entries(hosts)) {
hostTree[hostKey] = hostTree[hostKey] || {};
const host = hostTree[hostKey];
host.key = hostKey;
host.name = hostDetails.name;
host.uri = hostDetails.uri;
host.open = async function() {
const progress = startProgress(`Connecting to "${hostKey}"…`);
const { databases: dbNames, status, systemInfo } = await OpenConnection(hostKey);
host.status = status;
host.systemInfo = systemInfo;
host.databases = host.databases || {};
if (!dbNames) {
return;
}
for (const dbKey of dbNames.sort((a, b) => a.localeCompare(b))) {
host.databases[dbKey] = host.databases[dbKey] || {};
}
for (const [ dbKey, database ] of Object.entries(host.databases)) {
if (!dbNames.includes(dbKey)) {
delete host.databases[dbKey];
}
database.key = dbKey;
database.hostKey = hostKey;
database.collections = database.collections || {};
database.open = async function() {
const progress = startProgress(`Opening database "${dbKey}"…`);
const { collections: collNames, stats } = await OpenDatabase(hostKey, dbKey);
database.stats = stats;
if (!collNames) {
return;
}
for (const collKey of collNames.sort((a, b) => a.localeCompare(b))) {
database.collections[collKey] = database.collections[collKey] || {};
}
for (const [ collKey, collection ] of Object.entries(database.collections)) {
if (!collNames.includes(collKey)) {
delete database.collections[collKey];
}
collection.key = collKey;
collection.dbKey = dbKey;
collection.hostKey = hostKey;
collection.indexes = collection.indexes || [];
collection.open = async function() {
const progress = startProgress(`Opening database "${dbKey}"…`);
const stats = await OpenCollection(hostKey, dbKey, collKey);
collection.stats = stats;
await refresh();
progress.end();
};
collection.rename = async function() {
const newCollKey = await dialogs.enterText('Rename collection', `Enter a new name for collection ${collKey}.`, collKey);
if (newCollKey && (newCollKey !== collKey)) {
const progress = startProgress(`Renaming collection "${collKey}" to "${newCollKey}"…`);
const ok = await RenameCollection(hostKey, dbKey, collKey, newCollKey);
await database.open();
progress.end();
return ok;
}
};
collection.export = function(query) {
const dialog = dialogs.new(ExportDialog, { collection, query });
return new Promise(resolve => {
dialog.$on('export', async event => {
const success = await PerformFindExport(hostKey, dbKey, collKey, JSON.stringify(event.detail.exportInfo));
if (success) {
dialog.$close();
resolve();
}
});
});
};
collection.dump = function() {
const dialog = dialogs.new(DumpDialog, { info: {
hostKey,
dbKey,
collKeys: [ collKey ],
} });
return new Promise(resolve => {
dialog.$on('dump', async event => {
const success = await PerformDump(JSON.stringify(event.detail.info));
if (success) {
dialog.$close();
resolve();
}
});
});
};
collection.truncate = async function() {
const progress = startProgress(`Truncating collection "${collKey}"…`);
await TruncateCollection(hostKey, dbKey, collKey);
await refresh();
progress.end();
};
collection.drop = async function() {
const progress = startProgress(`Dropping collection "${collKey}"…`);
const success = await DropCollection(hostKey, dbKey, collKey);
if (success) {
await refresh();
}
progress.end();
};
collection.getIndexes = async function() {
const progress = startProgress(`Retrieving indexes of "${collKey}"…`);
collection.indexes = [];
const indexes = await GetIndexes(hostKey, dbKey, collKey);
for (const indexDetails of indexes) {
const index = {
name: indexDetails.name,
background: indexDetails.background || false,
unique: indexDetails.unique || false,
sparse: indexDetails.sparse || false,
model: indexDetails.model,
};
index.drop = async function() {
const progress = startProgress(`Dropping index ${index.name}`);
const hasBeenDropped = await DropIndex(hostKey, dbKey, collKey, index.name);
progress.end();
return hasBeenDropped;
};
collection.indexes.push(index);
}
progress.end();
return collection.indexes;
};
collection.getIndexByName = function(indesName) {
return collection.indexes.find(idx => idx.name = indesName);
};
collection.newIndex = function() {
const dialog = dialogs.new(IndexDetailDialog, { collection });
return new Promise(resolve => {
dialog.$on('create', async event => {
const progress = startProgress('Creating index…');
const newIndexName = await CreateIndex(collection.hostKey, collection.dbKey, collection.key, JSON.stringify(event.detail.index));
if (newIndexName) {
dialog.$close();
}
progress.end();
resolve(newIndexName);
});
});
};
collection.openQueryChooser = function(queryToSave = undefined) {
const dialog = dialogs.new(QueryChooserDialog, { collection, queryToSave });
return new Promise(resolve => {
dialog.$on('select', async event => {
dialog.$close();
resolve(event.detail.query);
});
dialog.$on('create', async event => {
const ok = await queries.create(event.detail.query);
if (ok) {
dialog.$close();
resolve(event.detail.query);
}
});
});
};
}
await refresh();
progress.end();
windowTitle.setSegments(dbKey, host.name, 'Rolens');
};
database.dump = function() {
const dialog = dialogs.new(DumpDialog, { info: { hostKey, dbKey } });
return new Promise(resolve => {
dialog.$on('dump', async event => {
const success = await PerformDump(JSON.stringify(event.detail.info));
if (success) {
dialog.$close();
resolve();
}
});
});
};
database.drop = async function() {
const progress = startProgress(`Dropping database "${dbKey}"…`);
const success = await DropDatabase(hostKey, dbKey);
if (success) {
await refresh();
}
progress.end();
};
database.newCollection = async function() {
const name = await dialogs.enterText('Create a collection', 'Note: collections in MongoDB do not exist until they have at least one item. Your new collection will not persist on the server; fill it to have it created.', '');
if (name) {
database.collections[name] = {};
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] = {};
await refresh();
}
};
await refresh();
progress.end();
};
host.remove = async function() {
await RemoveHost(hostKey);
await refresh();
};
}
hostTreeInited = true;
set(hostTree);
}
function newHost() {
const dialog = dialogs.new(HostDetailDialog, { hostKey: '' });
return new Promise(resolve => {
dialog.$on('close', () => {
refresh().then(resolve);
});
});
}
applicationInited.defer(refresh);
const hostTree = {
refresh,
subscribe,
get: getValue,
newHost,
hasBeenInited: () => hostTreeInited,
};
export default hostTree;

View File

@ -18,16 +18,12 @@ const { subscribe } = derived([ environment, applicationSettings ], ([ env, sett
if (alreadyInited) { if (alreadyInited) {
return; return;
} }
else if (env && settings) {
if (env && settings) { Promise.all(listeners.map(l => l())).then(() => {
set(true); set(true);
alreadyInited = true; alreadyInited = true;
document.getElementById('app-loading')?.remove();
// Remove loading spinner. });
document.getElementById('app-loading')?.remove();
// Call hooks
listeners.forEach(l => l());
} }
}, false); }, false);

View File

@ -1,3 +1,5 @@
import dialogs from '$lib/dialogs';
import ViewConfigDialog from '$organisms/connection/collection/dialogs/viewconfig.svelte';
import { UpdateViewStore, Views } from '$wails/go/app/App'; import { UpdateViewStore, Views } from '$wails/go/app/App';
import { get, writable } from 'svelte/store'; import { get, writable } from 'svelte/store';
@ -23,6 +25,11 @@ function forCollection(hostKey, dbKey, collKey) {
return Object.fromEntries(entries); return Object.fromEntries(entries);
} }
function openConfig(collection, firstItem = {}) {
const dialog = dialogs.new(ViewConfigDialog, { collection, firstItem });
return dialog;
}
reload(); reload();
subscribe(newViewStore => { subscribe(newViewStore => {
if (skipUpdate) { if (skipUpdate) {
@ -32,5 +39,5 @@ subscribe(newViewStore => {
UpdateViewStore(JSON.stringify(newViewStore)); UpdateViewStore(JSON.stringify(newViewStore));
}); });
const views = { reload, set, subscribe, forCollection }; const views = { reload, set, subscribe, forCollection, openConfig };
export default views; export default views;

View File

@ -2,9 +2,9 @@
import Details from '$components/details.svelte'; import Details from '$components/details.svelte';
import Icon from '$components/icon.svelte'; import Icon from '$components/icon.svelte';
import Modal from '$components/modal.svelte'; import Modal from '$components/modal.svelte';
import Collation from '$lib/mongo/collation.svelte';
import ObjectEditor from '$components/objecteditor.svelte'; import ObjectEditor from '$components/objecteditor.svelte';
import { aggregationStageDocumentationURL, aggregationStages } from '$lib/mongo'; import { aggregationStageDocumentationURL, aggregationStages } from '$lib/mongo';
import Collation from '$lib/mongo/collation.svelte';
import { jsonLooseParse, looseJsonIsValid } from '$lib/strings'; import { jsonLooseParse, looseJsonIsValid } from '$lib/strings';
import { Aggregate } from '$wails/go/app/App'; import { Aggregate } from '$wails/go/app/App';
import { BrowserOpenURL } from '$wails/runtime/runtime'; import { BrowserOpenURL } from '$wails/runtime/runtime';

View File

@ -1,9 +1,9 @@
<script> <script>
import FormInput from '$components/forminput.svelte'; import FormInput from '$components/forminput.svelte';
import Hint from '$components/hint.svelte';
import Icon from '$components/icon.svelte'; import Icon from '$components/icon.svelte';
import { inputTypes } from '$lib/mongo'; import { inputTypes } from '$lib/mongo';
import { resolveKeypath, setValue } from '$lib/objects'; import { resolveKeypath, setValue } from '$lib/objects';
import Hint from '$components/hint.svelte';
export let item = {}; export let item = {};
export let view = {}; export let view = {};

View File

@ -2,43 +2,34 @@
import Icon from '$components/icon.svelte'; import Icon from '$components/icon.svelte';
import Modal from '$components/modal.svelte'; import Modal from '$components/modal.svelte';
import views from '$lib/stores/views'; import views from '$lib/stores/views';
import { PerformFindExport } from '$wails/go/app/App';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
export let info;
export let collection; export let collection;
export let query = undefined;
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
let viewKey = collection.viewKey; const exportInfo = { ...query, viewKey: collection.viewKey };
$: viewKey = collection.viewKey;
$: if (info) {
info.viewKey = viewKey;
}
async function performExport() { function submit() {
info.view = $views[viewKey]; exportInfo.view = $views[exportInfo.viewKey];
const success = await PerformFindExport(collection.hostKey, collection.dbKey, collection.key, JSON.stringify(info)); dispatch('export', { exportInfo });
if (success) {
info = undefined;
}
} }
</script> </script>
<Modal bind:show={info} title="Export results" width="450px"> <Modal title="Export results" width="450px" on:close>
<form on:submit|preventDefault={performExport}> <form on:submit|preventDefault={submit}>
<label class="field"> <label class="field">
<span class="label">Export</span> <span class="label">Export</span>
<select bind:value={info.contents}> <select bind:value={exportInfo.contents}>
<option value="all">all records</option> <option value="all">all records</option>
<option value="query">all records matching query</option> <option value="query" disabled={!query}>all records matching query</option>
<option value="querylimitskip">all records matching query, considering limit and skip</option> <option value="querylimitskip" disabled={!query}>all records matching query, considering limit and skip</option>
</select> </select>
</label> </label>
<label class="field"> <label class="field">
<span class="label">Format</span> <span class="label">Format</span>
<select bind:value={info.format}> <select bind:value={exportInfo.format}>
<option value="jsonarray">JSON array</option> <option value="jsonarray">JSON array</option>
<option value="ndjson">Newline delimited JSON</option> <option value="ndjson">Newline delimited JSON</option>
<option value="csv">CSV</option> <option value="csv">CSV</option>
@ -47,7 +38,7 @@
<label class="field"> <label class="field">
<span class="label">View to use</span> <span class="label">View to use</span>
<select bind:value={viewKey}> <select bind:value={exportInfo.viewKey}>
{#each Object.entries(views.forCollection(collection.hostKey, collection.dbKey, collection.key)) as [ key, { name } ]} {#each Object.entries(views.forCollection(collection.hostKey, collection.dbKey, collection.key)) as [ key, { name } ]}
<option value={key}>{name}</option> <option value={key}>{name}</option>
{/each} {/each}
@ -59,7 +50,7 @@
</form> </form>
<svelte:fragment slot="footer"> <svelte:fragment slot="footer">
<button class="btn" on:click={performExport}> <button class="btn" on:click={submit}>
<Icon name="play" /> Start export <Icon name="play" /> Start export
</button> </button>
</svelte:fragment> </svelte:fragment>

View File

@ -2,14 +2,12 @@
import Icon from '$components/icon.svelte'; import Icon from '$components/icon.svelte';
import Modal from '$components/modal.svelte'; import Modal from '$components/modal.svelte';
import input from '$lib/actions/input'; import input from '$lib/actions/input';
import { CreateIndex } from '$wails/go/app/App';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
export let collection = {}; export let collection;
export let creatingNewIndex = false;
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
let index = { model: [] }; const index = { model: [] };
function addRule() { function addRule() {
index.model = [ ...index.model, {} ]; index.model = [ ...index.model, {} ];
@ -21,16 +19,11 @@
} }
async function create() { async function create() {
const newIndexName = await CreateIndex(collection.hostKey, collection.dbKey, collection.key, JSON.stringify(index)); dispatch('create', { index });
if (newIndexName) {
creatingNewIndex = false;
index = { model: [] };
dispatch('reload');
}
} }
</script> </script>
<Modal title="Create new index {collection ? `on collection ${collection.key}` : ''}" bind:show={creatingNewIndex}> <Modal title="Create new index on {collection.key}" on:close>
<form on:submit|preventDefault={create}> <form on:submit|preventDefault={create}>
<label class="field name"> <label class="field name">
<span class="label">Name</span> <span class="label">Name</span>

View File

@ -4,13 +4,12 @@
import Icon from '$components/icon.svelte'; import Icon from '$components/icon.svelte';
import Modal from '$components/modal.svelte'; import Modal from '$components/modal.svelte';
import input from '$lib/actions/input'; import input from '$lib/actions/input';
import hosts from '$lib/stores/hosts'; import hostTree from '$lib/stores/hosttree';
import queries from '$lib/stores/queries'; import queries from '$lib/stores/queries';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
export let queryToSave = undefined; export let queryToSave = undefined;
export let collection = {}; export let collection = {};
export let show = false;
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
let gridSelectedPath = []; let gridSelectedPath = [];
@ -22,23 +21,16 @@
queryToSave.dbKey = collection.dbKey; queryToSave.dbKey = collection.dbKey;
queryToSave.collKey = collection.key; queryToSave.collKey = collection.key;
const newId = queries.create(queryToSave); dispatch('create', { query: queryToSave });
selectedKey = queryToSave.name;
if (newId) {
dispatch('created', newId);
queryToSave = undefined;
selectedKey = newId;
select();
}
} }
else { else {
select(); selectActive();
} }
} }
function select() { function selectActive() {
dispatch('select', selectedKey); dispatch('select', { query: $queries[selectedKey] });
show = false;
} }
function gridSelect(event) { function gridSelect(event) {
@ -53,7 +45,7 @@
function gridTrigger(event) { function gridTrigger(event) {
gridSelect(event); gridSelect(event);
select(); selectActive();
} }
async function gridRemove(event) { async function gridRemove(event) {
@ -71,7 +63,7 @@
} }
</script> </script>
<Modal bind:show title={queryToSave ? 'Save query' : 'Load query'} width="500px"> <Modal title={queryToSave ? 'Save query' : 'Load query'} width="500px" on:close>
<form on:submit|preventDefault={submit}> <form on:submit|preventDefault={submit}>
{#if queryToSave} {#if queryToSave}
<label class="field queryname"> <label class="field queryname">
@ -88,7 +80,11 @@
columns={[ { key: 'n', title: 'Query name' }, { key: 'h', title: 'Host' }, { key: 'ns', title: 'Namespace' } ]} columns={[ { key: 'n', title: 'Query name' }, { key: 'h', title: 'Host' }, { key: 'ns', title: 'Namespace' } ]}
key="n" key="n"
items={Object.entries($queries).reduce((object, [ name, query ]) => { items={Object.entries($queries).reduce((object, [ name, query ]) => {
object[query.name] = { n: name, h: $hosts[query.hostKey]?.name || '?', ns: `${query.dbKey}.${query.collKey}` }; object[query.name] = {
n: name,
h: $hostTree[query.hostKey]?.name || '?',
ns: `${query.dbKey}.${query.collKey}`,
};
return object; return object;
}, {})} }, {})}
showHeaders={true} showHeaders={true}

View File

@ -7,15 +7,18 @@
import views from '$lib/stores/views'; import views from '$lib/stores/views';
export let collection; export let collection;
export let show = false;
export let activeViewKey = 'list';
export let firstItem = {}; export let firstItem = {};
$: tabs = Object.entries(views.forCollection(collection.hostKey, collection.dbKey, collection.key)) let tabs = [];
.sort((a, b) => sortTabKeys(a[0], b[0])) $: $views && refresh();
.map(([ key, v ]) => {
return { key, title: v.name, closable: key !== 'list' }; function refresh() {
}); tabs = Object.entries(views.forCollection(collection.hostKey, collection.dbKey, collection.key))
.sort((a, b) => sortTabKeys(a[0], b[0]))
.map(([ key, v ]) => {
return { key, title: v.name, closable: key !== 'list' };
});
}
function sortTabKeys(a, b) { function sortTabKeys(a, b) {
if (a === 'list') { if (a === 'list') {
@ -39,29 +42,29 @@
type: 'table', type: 'table',
columns: [ { key: '_id', showInTable: true, inputType: 'objectid', mandatory: true } ], columns: [ { key: '_id', showInTable: true, inputType: 'objectid', mandatory: true } ],
}; };
activeViewKey = newViewKey; collection.viewKey = newViewKey;
} }
function removeView(viewKey) { function removeView(viewKey) {
const keys = Object.keys($views).sort(sortTabKeys); const keys = Object.keys($views).sort(sortTabKeys);
const oldIndex = keys.indexOf(viewKey); const oldIndex = keys.indexOf(viewKey);
const newKey = keys[oldIndex - 1]; const newKey = keys[oldIndex - 1];
activeViewKey = newKey; collection.viewKey = newKey;
delete $views[viewKey]; delete $views[viewKey];
$views = $views; $views = $views;
} }
function addColumn(before) { function addColumn(before) {
if (typeof before === 'number') { if (typeof before === 'number') {
$views[activeViewKey].columns = [ $views[collection.viewKey].columns = [
...$views[activeViewKey].columns.slice(0, before), ...$views[collection.viewKey].columns.slice(0, before),
{ showInTable: true, inputType: 'none' }, { showInTable: true, inputType: 'none' },
...$views[activeViewKey].columns.slice(before), ...$views[collection.viewKey].columns.slice(before),
]; ];
} }
else { else {
$views[activeViewKey].columns = [ $views[collection.viewKey].columns = [
...$views[activeViewKey].columns, ...$views[collection.viewKey].columns,
{ showInTable: true, inputType: 'none' }, { showInTable: true, inputType: 'none' },
]; ];
} }
@ -72,7 +75,7 @@
return; return;
} }
$views[activeViewKey].columns = Object.keys(firstItem).sort().map(key => { $views[collection.viewKey].columns = Object.keys(firstItem).sort().map(key => {
return { return {
key, key,
showInTable: true, showInTable: true,
@ -82,57 +85,57 @@
} }
function moveColumn(oldIndex, delta) { function moveColumn(oldIndex, delta) {
const column = $views[activeViewKey].columns[oldIndex]; const column = $views[collection.viewKey].columns[oldIndex];
const newIndex = oldIndex + delta; const newIndex = oldIndex + delta;
$views[activeViewKey].columns.splice(oldIndex, 1); $views[collection.viewKey].columns.splice(oldIndex, 1);
$views[activeViewKey].columns.splice(newIndex, 0, column); $views[collection.viewKey].columns.splice(newIndex, 0, column);
$views[activeViewKey].columns = $views[activeViewKey].columns; $views[collection.viewKey].columns = $views[collection.viewKey].columns;
} }
function removeColumn(index) { function removeColumn(index) {
$views[activeViewKey].columns.splice(index, 1); $views[collection.viewKey].columns.splice(index, 1);
$views[activeViewKey].columns = $views[activeViewKey].columns; $views[collection.viewKey].columns = $views[collection.viewKey].columns;
} }
</script> </script>
<Modal title="View configuration" bind:show contentPadding={false}> <Modal title="View configuration" contentPadding={false} on:close>
<TabBar <TabBar
{tabs} {tabs}
canAddTab={true} canAddTab={true}
on:addTab={createView} on:addTab={createView}
on:closeTab={e => removeView(e.detail)} on:closeTab={e => removeView(e.detail)}
bind:selectedKey={activeViewKey} bind:selectedKey={collection.viewKey}
/> />
<div class="options"> <div class="options">
{#if $views[activeViewKey]} {#if $views[collection.viewKey]}
<div class="meta"> <div class="meta">
{#key activeViewKey} {#key collection.viewKey}
<label class="field"> <label class="field">
<span class="label">View name</span> <span class="label">View name</span>
<input type="text" use:input={{ autofocus: true }} bind:value={$views[activeViewKey].name} disabled={activeViewKey === 'list'} /> <input type="text" use:input={{ autofocus: true }} bind:value={$views[collection.viewKey].name} disabled={collection.viewKey === 'list'} />
</label> </label>
{/key} {/key}
<label class="field"> <label class="field">
<span class="label">View type</span> <span class="label">View type</span>
<select bind:value={$views[activeViewKey].type} disabled> <select bind:value={$views[collection.viewKey].type} disabled>
<option value="list">List view</option> <option value="list">List view</option>
<option value="table">Table view</option> <option value="table">Table view</option>
</select> </select>
</label> </label>
</div> </div>
{#if $views[activeViewKey].type === 'list'} {#if $views[collection.viewKey].type === 'list'}
<div class="flex"> <div class="flex">
<input type="checkbox" id="hideObjectIndicators" bind:checked={$views[activeViewKey].hideObjectIndicators} /> <input type="checkbox" id="hideObjectIndicators" bind:checked={$views[collection.viewKey].hideObjectIndicators} />
<label for="hideObjectIndicators"> <label for="hideObjectIndicators">
Hide object indicators ({'{...}'} and [...]) in list view and show nothing instead Hide object indicators ({'{...}'} and [...]) in list view and show nothing instead
</label> </label>
</div> </div>
{:else if $views[activeViewKey].type === 'table'} {:else if $views[collection.viewKey].type === 'table'}
<div class="columns"> <div class="columns">
{#each $views[activeViewKey].columns as column, columnIndex} {#each $views[collection.viewKey].columns as column, columnIndex}
<div class="column"> <div class="column">
<label class="field"> <label class="field">
<input type="text" use:input bind:value={column.key} placeholder="Column keypath" /> <input type="text" use:input bind:value={column.key} placeholder="Column keypath" />
@ -188,7 +191,7 @@
<button class="btn" type="button" on:click={() => moveColumn(columnIndex, -1)} disabled={columnIndex === 0} title="Move column one position up"> <button class="btn" type="button" on:click={() => moveColumn(columnIndex, -1)} disabled={columnIndex === 0} title="Move column one position up">
<Icon name="chev-u" /> <Icon name="chev-u" />
</button> </button>
<button class="btn" type="button" on:click={() => moveColumn(columnIndex, 1)} disabled={columnIndex === $views[activeViewKey].columns.length - 1} title="Move column one position down"> <button class="btn" type="button" on:click={() => moveColumn(columnIndex, 1)} disabled={columnIndex === $views[collection.viewKey].columns.length - 1} title="Move column one position down">
<Icon name="chev-d" /> <Icon name="chev-d" />
</button> </button>
<button class="btn danger" type="button" on:click={() => removeColumn(columnIndex)} title="Remove this column"> <button class="btn danger" type="button" on:click={() => removeColumn(columnIndex)} title="Remove this column">

View File

@ -6,18 +6,15 @@
import input from '$lib/actions/input'; import input from '$lib/actions/input';
import { deepClone } from '$lib/objects'; import { deepClone } from '$lib/objects';
import { startProgress } from '$lib/progress'; import { startProgress } from '$lib/progress';
import queries from '$lib/stores/queries';
import applicationSettings from '$lib/stores/settings'; import applicationSettings from '$lib/stores/settings';
import views from '$lib/stores/views'; import views from '$lib/stores/views';
import { convertLooseJson } from '$lib/strings';
import { FindItems, RemoveItemById, UpdateFoundDocument } from '$wails/go/app/App'; import { FindItems, RemoveItemById, UpdateFoundDocument } from '$wails/go/app/App';
import { EJSON } from 'bson'; import { EJSON } from 'bson';
import { createEventDispatcher, onMount } from 'svelte'; import { onMount } from 'svelte';
import ExportInfo from './components/export.svelte';
import QueryChooser from './components/querychooser.svelte';
export let collection; export let collection;
const dispatch = createEventDispatcher();
const defaults = { const defaults = {
query: '{}', query: '{}',
sort: $applicationSettings.defaultSort || '{ "_id": 1 }', sort: $applicationSettings.defaultSort || '{ "_id": 1 }',
@ -32,17 +29,18 @@
let queryField; let queryField;
let activePath = []; let activePath = [];
let objectViewerData; let objectViewerData;
let queryToSave;
let showQueryChooser = false;
let exportInfo;
let querying = false; let querying = false;
let objectViewerSuccessMessage = ''; let objectViewerSuccessMessage = '';
let viewsForCollection = {};
// $: 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})` : ''};`; // $: 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})` : ''};`;
$: viewsForCollection = views.forCollection(collection.hostKey, collection.dbKey, collection.key);
$: lastPage = (submittedForm.limit && result?.results?.length) ? Math.max(0, Math.ceil((result.total - submittedForm.limit) / submittedForm.limit)) : 0; $: lastPage = (submittedForm.limit && result?.results?.length) ? Math.max(0, Math.ceil((result.total - submittedForm.limit) / submittedForm.limit)) : 0;
$: activePage = (submittedForm.limit && submittedForm.skip && result?.results?.length) ? submittedForm.skip / submittedForm.limit : 0; $: activePage = (submittedForm.limit && submittedForm.skip && result?.results?.length) ? submittedForm.skip / submittedForm.limit : 0;
$: if ($views) {
viewsForCollection = views.forCollection(collection.hostKey, collection.dbKey, collection.key);
}
async function submitQuery() { async function submitQuery() {
if (querying) { if (querying) {
return; return;
@ -70,19 +68,18 @@
} }
} }
function loadQuery() { async function loadQuery() {
queryToSave = undefined; const query = await collection.openQueryChooser();
showQueryChooser = true; if (query) {
form = { ...query };
submitQuery();
}
} }
function saveQuery() { async function saveQuery() {
queryToSave = form; const query = await collection.openQueryChooser(form);
showQueryChooser = true; if (query) {
} form = { ...query };
function queryChosen(event) {
if ($queries[event?.detail]) {
form = { ...$queries[event?.detail] };
submitQuery(); submitQuery();
} }
} }
@ -130,6 +127,10 @@
objectViewerData = item; objectViewerData = item;
} }
function openViewConfig() {
views.openConfig(collection, result.results?.[0] || {});
}
export function performQuery(q) { export function performQuery(q) {
form = { ...defaults, ...q }; form = { ...defaults, ...q };
submitQuery(); submitQuery();
@ -142,7 +143,7 @@
collection.dbKey, collection.dbKey,
collection.key, collection.key,
EJSON.stringify({ _id: event.detail.originalData._id }), EJSON.stringify({ _id: event.detail.originalData._id }),
event.detail.text convertLooseJson(event.detail.text)
); );
if (success) { if (success) {
@ -207,7 +208,7 @@
<button type="submit" class="btn" title="Run query"> <button type="submit" class="btn" title="Run query">
<Icon name="play" /> Run <Icon name="play" /> Run
</button> </button>
<button class="btn secondary" type="button" on:click={() => exportInfo = {}}> <button class="btn secondary" type="button" on:click={() => collection.export(form)}>
<Icon name="save" /> Export results… <Icon name="save" /> Export results…
</button> </button>
<div class="field"> <div class="field">
@ -259,7 +260,7 @@
<option value={key}>{view.name}</option> <option value={key}>{view.name}</option>
{/each} {/each}
</select> </select>
<button class="btn" on:click={() => dispatch('openViewConfig', { firstItem: result.results?.[0] })} title="Configure view"> <button class="btn" on:click={openViewConfig} title="Configure view">
<Icon name="cog" /> <Icon name="cog" />
</button> </button>
</label> </label>
@ -283,15 +284,6 @@
</div> </div>
</div> </div>
<QueryChooser
bind:queryToSave
bind:show={showQueryChooser}
on:select={queryChosen}
{collection}
/>
<ExportInfo on:openViewConfig bind:collection bind:info={exportInfo} />
{#if objectViewerData} {#if objectViewerData}
<!-- @todo Implement save --> <!-- @todo Implement save -->
<ObjectViewer bind:data={objectViewerData} saveable on:save={saveDocument} bind:successMessage={objectViewerSuccessMessage} /> <ObjectViewer bind:data={objectViewerData} saveable on:save={saveDocument} bind:successMessage={objectViewerSuccessMessage} />

View File

@ -3,8 +3,8 @@
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 { tick } from 'svelte'; import { tick } from 'svelte';
import Aggregate from './aggregate.svelte'; import Aggregate from './aggregate.svelte';
import ViewConfig from './components/viewconfig.svelte';
import Find from './find.svelte'; import Find from './find.svelte';
import Indexes from './indexes.svelte'; import Indexes from './indexes.svelte';
import Insert from './insert.svelte'; import Insert from './insert.svelte';
@ -19,8 +19,6 @@
let tab = 'find'; let tab = 'find';
let find; let find;
let viewConfigModalOpen = false;
let firstItem;
$: if (collection) { $: if (collection) {
collection.hostKey = hostKey; collection.hostKey = hostKey;
@ -39,11 +37,6 @@
await tick(); await tick();
find.performQuery(event.detail); find.performQuery(event.detail);
} }
function openViewConfig(event) {
firstItem = event.detail?.firstItem;
viewConfigModalOpen = true;
}
</script> </script>
<div class="view" class:empty={!collection}> <div class="view" class:empty={!collection}>
@ -62,8 +55,8 @@
<div class="container"> <div class="container">
{#if tab === 'stats'} <Stats {collection} /> {#if tab === 'stats'} <Stats {collection} />
{:else if tab === 'find'} <Find {collection} bind:this={find} on:openViewConfig={openViewConfig} /> {:else if tab === 'find'} <Find {collection} bind:this={find} />
{:else if tab === 'insert'} <Insert {collection} on:performFind={catchQuery} on:openViewConfig={openViewConfig} /> {:else if tab === 'insert'} <Insert {collection} on:performFind={catchQuery} />
{:else if tab === 'update'} <Update {collection} on:performFind={catchQuery} /> {:else if tab === 'update'} <Update {collection} on:performFind={catchQuery} />
{:else if tab === 'remove'} <Remove {collection} /> {:else if tab === 'remove'} <Remove {collection} />
{:else if tab === 'indexes'} <Indexes {collection} /> {:else if tab === 'indexes'} <Indexes {collection} />
@ -76,15 +69,6 @@
{/if} {/if}
</div> </div>
{#if collection}
<ViewConfig
bind:show={viewConfigModalOpen}
bind:activeViewKey={collection.viewKey}
{firstItem}
{collection}
/>
{/if}
<style> <style>
.view { .view {
height: 100%; height: 100%;

View File

@ -1,65 +1,72 @@
<script> <script>
import Icon from '$components/icon.svelte'; import Icon from '$components/icon.svelte';
import ObjectGrid from '$components/objectgrid.svelte'; import ObjectGrid from '$components/objectgrid.svelte';
import { DropIndex, GetIndexes } from '$wails/go/app/App'; import { onMount } from 'svelte';
import IndexDetail from './components/indexdetail.svelte';
export let collection; export let collection;
let indexes = [];
let activePath = []; let activePath = [];
let creatingNewIndex = false; let _indexes = [];
$: collection && getIndexes(); async function refresh() {
await collection.getIndexes();
_indexes = collection.indexes.map(idx => {
return {
name: idx.name,
background: idx.background || false,
unique: idx.unique || false,
sparse: idx.sparse || false,
model: idx.model,
};
});
}
async function getIndexes() { async function createIndex() {
const result = await GetIndexes(collection.hostKey, collection.dbKey, collection.key); const newIndexName = await collection.newIndex();
if (result) { if (newIndexName) {
indexes = result; await refresh();
} }
} }
function createIndex() { async function dropIndex(indexName) {
creatingNewIndex = true; if (typeof indexName !== 'string') {
} indexName = activePath[0];
async function drop(key) {
if (typeof key !== 'string') {
key = activePath[0];
} }
const success = await DropIndex(collection.hostKey, collection.dbKey, collection.key, key);
const success = await collection.getIndexByName(indexName).drop();
if (success) { if (success) {
await getIndexes();
activePath[0] = ''; activePath[0] = '';
await refresh();
} }
} }
onMount(refresh);
</script> </script>
<div class="indexes"> <div class="indexes">
<div class="grid"> <div class="grid">
<ObjectGrid <ObjectGrid
key="name" key="name"
data={indexes} data={_indexes}
getRootMenu={(_, idx) => [ { label: 'Drop this index', fn: () => drop(idx.name) } ]} getRootMenu={(_, idx) => [ { label: 'Drop this index', fn: () => dropIndex(idx.name) } ]}
bind:activePath bind:activePath
/> />
</div> </div>
<div class="actions"> <div class="actions">
<button class="btn" on:click={getIndexes}> <button class="btn" on:click={refresh}>
<Icon name="reload" /> Reload <Icon name="reload" /> Reload
</button> </button>
<button class="btn" on:click={createIndex}> <button class="btn" on:click={createIndex}>
<Icon name="+" /> Create index… <Icon name="+" /> Create index…
</button> </button>
<button class="btn danger" on:click={drop} disabled={!indexes?.length || !activePath[0]}> <button class="btn danger" on:click={dropIndex} disabled={!_indexes.length || !activePath[0]}>
<Icon name="x" /> Drop selected <Icon name="x" /> Drop selected
</button> </button>
</div> </div>
</div> </div>
<IndexDetail bind:creatingNewIndex {collection} on:reload={getIndexes} />
<style> <style>
.indexes { .indexes {
display: grid; display: grid;

View File

@ -2,6 +2,7 @@
import Details from '$components/details.svelte'; import Details from '$components/details.svelte';
import Grid from '$components/grid.svelte'; import Grid from '$components/grid.svelte';
import Icon from '$components/icon.svelte'; import Icon from '$components/icon.svelte';
import ObjectEditor from '$components/objecteditor.svelte';
import ObjectViewer from '$components/objectviewer.svelte'; import ObjectViewer from '$components/objectviewer.svelte';
import { randomString } from '$lib/math'; import { randomString } from '$lib/math';
import { inputTypes } from '$lib/mongo'; import { inputTypes } from '$lib/mongo';
@ -11,7 +12,6 @@
import { EJSON } from 'bson'; import { EJSON } from 'bson';
import { createEventDispatcher, onMount } from 'svelte'; import { createEventDispatcher, onMount } from 'svelte';
import Form from './components/form.svelte'; import Form from './components/form.svelte';
import ObjectEditor from '$components/objecteditor.svelte';
export let collection; export let collection;
@ -25,8 +25,8 @@
let objectViewerData = ''; let objectViewerData = '';
let viewType = 'form'; let viewType = 'form';
let allValid = false; let allValid = false;
let viewsForCollection = {};
$: viewsForCollection = views.forCollection(collection.hostKey, collection.dbKey, collection.key);
$: oppositeViewType = viewType === 'table' ? 'form' : 'table'; $: oppositeViewType = viewType === 'table' ? 'form' : 'table';
$: allValid = Object.values(formValidity).every(v => v !== false); $: allValid = Object.values(formValidity).every(v => v !== false);
@ -46,6 +46,10 @@
newItems = [ {} ]; newItems = [ {} ];
} }
$: if ($views) {
viewsForCollection = views.forCollection(collection.hostKey, collection.dbKey, collection.key);
}
async function insert() { async function insert() {
insertedIds = await InsertItems( insertedIds = await InsertItems(
collection.hostKey, collection.hostKey,
@ -95,6 +99,10 @@
newItems = newItems; newItems = newItems;
} }
function openViewConfig() {
views.openConfig(collection);
}
onMount(() => { onMount(() => {
if (collection.viewKey === 'list') { if (collection.viewKey === 'list') {
editor.dispatch({ editor.dispatch({
@ -190,7 +198,7 @@
<option value={key}>{key === 'list' ? 'Raw JSON' : view.name}</option> <option value={key}>{key === 'list' ? 'Raw JSON' : view.name}</option>
{/each} {/each}
</select> </select>
<button class="btn" type="button" on:click={() => dispatch('openViewConfig')} title="Configure view"> <button class="btn" type="button" on:click={openViewConfig} title="Configure view">
<Icon name="cog" /> <Icon name="cog" />
</button> </button>
</label> </label>

View File

@ -3,12 +3,14 @@
import Grid from '$components/grid.svelte'; import Grid from '$components/grid.svelte';
import Modal from '$components/modal.svelte'; import Modal from '$components/modal.svelte';
import { startProgress } from '$lib/progress'; import { startProgress } from '$lib/progress';
import connections from '$lib/stores/connections'; import hostTree from '$lib/stores/hosttree';
import hosts from '$lib/stores/hosts';
import applicationSettings from '$lib/stores/settings'; import applicationSettings from '$lib/stores/settings';
import { OpenConnection, OpenDatabase, PerformDump } from '$wails/go/app/App'; import { OpenConnection, OpenDatabase } from '$wails/go/app/App';
import { createEventDispatcher } from 'svelte';
export let info; export let info = {};
const dispatch = createEventDispatcher();
$: if (info) { $: if (info) {
info.outdir = info.outdir || $applicationSettings.defaultExportDirectory; info.outdir = info.outdir || $applicationSettings.defaultExportDirectory;
@ -24,10 +26,10 @@
const progress = startProgress(`Opening connection to host "${hostKey}"`); const progress = startProgress(`Opening connection to host "${hostKey}"`);
const databases = await OpenConnection(hostKey); const databases = await OpenConnection(hostKey);
if (databases && !$connections[hostKey]) { if (databases && !$hostTree[hostKey]) {
$connections[hostKey] = { databases: {} }; $hostTree[hostKey] = { databases: {} };
databases.sort().forEach(dbKey => { databases.sort().forEach(dbKey => {
$connections[hostKey].databases[dbKey] = $connections[hostKey].databases[dbKey] || { collections: {} }; $hostTree[hostKey].databases[dbKey] = $hostTree[hostKey].databases[dbKey] || { collections: {} };
}); });
} }
@ -44,26 +46,23 @@
const collections = await OpenDatabase(info.hostKey, dbKey); const collections = await OpenDatabase(info.hostKey, dbKey);
for (const collKey of collections?.sort() || []) { for (const collKey of collections?.sort() || []) {
$connections[info.hostKey].databases[dbKey].collections[collKey] = {}; $hostTree[info.hostKey].databases[dbKey].collections[collKey] = {};
} }
progress.end(); progress.end();
} }
} }
async function performDump() {
const ok = await PerformDump(JSON.stringify(info));
if (ok) {
info = undefined;
}
}
function selectCollection(collKey) { function selectCollection(collKey) {
info.collKeys = [ collKey ]; info.collKeys = [ collKey ];
} }
function performDump() {
dispatch('dump', { info });
}
</script> </script>
<Modal bind:show={info} title="Perform dump"> <Modal title="Perform dump" on:close>
<form on:submit|preventDefault={performDump}> <form on:submit|preventDefault={performDump}>
<label class="field"> <label class="field">
<span class="label">Output destination:</span> <span class="label">Output destination:</span>
@ -82,8 +81,8 @@
hideChildrenToggles hideChildrenToggles
items={[ items={[
{ id: undefined, name: '(localhost)' }, { id: undefined, name: '(localhost)' },
...Object.keys($hosts).map(id => { ...Object.keys($hostTree).map(id => {
return { id, name: $hosts[id]?.name }; return { id, name: $hostTree[id]?.name };
}), }),
]} ]}
on:select={e => selectHost(e.detail?.itemKey)} on:select={e => selectHost(e.detail?.itemKey)}
@ -98,7 +97,7 @@
hideChildrenToggles hideChildrenToggles
items={[ items={[
{ id: undefined, name: '(all databases)' }, { id: undefined, name: '(all databases)' },
...($connections[info.hostKey]?.databases ? Object.keys($connections[info.hostKey].databases).map(id => { ...($hostTree[info.hostKey]?.databases ? Object.keys($hostTree[info.hostKey].databases).map(id => {
return { id, name: id }; return { id, name: id };
}) : [] }) : []
), ),
@ -115,7 +114,7 @@
hideChildrenToggles hideChildrenToggles
items={[ items={[
{ id: undefined, name: '(all collections)' }, { id: undefined, name: '(all collections)' },
...($connections[info.hostKey]?.databases[info.dbKey]?.collections ? Object.keys($connections[info.hostKey].databases[info.dbKey].collections).map(id => { ...($hostTree[info.hostKey]?.databases[info.dbKey]?.collections ? Object.keys($hostTree[info.hostKey].databases[info.dbKey].collections).map(id => {
return { id, name: id }; return { id, name: id };
}) : [] }) : []
), ),

View File

@ -1,26 +1,17 @@
<script> <script>
import Modal from '$components/modal.svelte'; import Modal from '$components/modal.svelte';
import input from '$lib/actions/input'; import input from '$lib/actions/input';
import hosts from '$lib/stores/hosts'; import hostTree from '$lib/stores/hosttree';
import { AddHost, UpdateHost } from '$wails/go/app/App'; import { AddHost, UpdateHost } from '$wails/go/app/App';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher, onMount } from 'svelte';
export let show = false;
export let hostKey = ''; export let hostKey = '';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
let form = {}; let form = {};
let error = ''; let error = '';
$: valid = validate(form); $: valid = validate(form);
$: host = $hosts[hostKey]; $: host = $hostTree[hostKey];
$: if (show || !show) {
init();
}
function init() {
form = { ...(host || {}) };
}
function validate(form) { function validate(form) {
return form.name && form.uri && true; return form.name && form.uri && true;
@ -41,16 +32,20 @@
hostKey = newHostKey; hostKey = newHostKey;
} }
} }
show = false; dispatch('updated', form);
dispatch('reload'); dispatch('close');
} }
catch (e) { catch (e) {
error = e; error = e;
} }
} }
onMount(() => {
form = { ...(host || {}) };
});
</script> </script>
<Modal bind:show title={host ? `Edit ${host.name}` : 'Create a new host'}> <Modal title={host ? `Edit ${host.name}` : 'Create a new host'} on:close>
<form on:submit|preventDefault={submit}> <form on:submit|preventDefault={submit}>
<label class="field"> <label class="field">
<span class="label">Label</span> <span class="label">Label</span>

View File

@ -1,180 +1,70 @@
<script> <script>
import Grid from '$components/grid.svelte'; import Grid from '$components/grid.svelte';
import { startProgress } from '$lib/progress'; import hostTree from '$lib/stores/hosttree';
import connections from '$lib/stores/connections';
import { createEventDispatcher, tick } from 'svelte';
import { DropCollection, DropDatabase, OpenCollection, OpenConnection, OpenDatabase, RemoveHost, TruncateCollection } from '../../../wailsjs/go/app/App';
import hosts from '$lib/stores/hosts';
import windowTitle from '$lib/stores/windowtitle';
export let activeHostKey = ''; export let path = [];
export let activeDbKey = '';
export let activeCollKey = '';
const dispatch = createEventDispatcher();
let activeGridPath = [];
// $: activeGridPath[0] = activeHostKey || undefined;
// $: activeGridPath[1] = activeDbKey || undefined;
// $: activeGridPath[2] = activeCollKey || undefined;
$: connection = $connections[activeHostKey];
export async function reload() {
activeHostKey && await openConnection(activeHostKey);
activeDbKey && await openDatabase(activeDbKey);
activeCollKey && await openCollection(activeCollKey);
}
async function openConnection(hostKey) {
const progress = startProgress(`Connecting to "${hostKey}"…`);
activeCollKey = '';
activeDbKey = '';
activeHostKey = hostKey;
const { databases, status, systemInfo } = await OpenConnection(hostKey);
if (databases) {
$connections[hostKey] = $connections[hostKey] || {};
$connections[hostKey].status = status;
$connections[hostKey].systemInfo = systemInfo;
$connections[hostKey].databases = $connections[hostKey].databases || {};
databases.forEach(dbKey => {
$connections[hostKey].databases[dbKey] =
$connections[hostKey].databases[dbKey] || { collections: {} };
});
activeHostKey = hostKey;
dispatch('connected', hostKey);
}
progress.end();
if (databases) {
windowTitle.setSegments($hosts[activeHostKey].name, 'Rolens');
}
}
async function removeHost(hostKey) {
activeCollKey = '';
activeDbKey = '';
activeHostKey = '';
await tick();
await RemoveHost(hostKey);
await reload();
await hosts.update();
}
async function openDatabase(dbKey) {
const progress = startProgress(`Opening database "${dbKey}"…`);
const { collections, stats } = await OpenDatabase(activeHostKey, dbKey);
activeDbKey = dbKey;
activeCollKey = '';
$connections[activeHostKey].databases[dbKey].stats = stats;
for (const collKey of collections || []) {
$connections[activeHostKey].databases[dbKey].collections[collKey] =
$connections[activeHostKey].databases[dbKey].collections[collKey] || {};
}
progress.end();
windowTitle.setSegments(activeDbKey, $hosts[activeHostKey].name, 'Rolens');
}
async function dropDatabase(dbKey) {
const progress = startProgress(`Dropping database "${dbKey}"…`);
const success = await DropDatabase(activeHostKey, dbKey);
if (success) {
activeCollKey = '';
activeDbKey = '';
await reload();
}
progress.end();
}
async function openCollection(collKey) {
const progress = startProgress(`Opening collection "${collKey}"…`);
const stats = await OpenCollection(activeHostKey, activeDbKey, collKey);
activeCollKey = collKey;
$connections[activeHostKey].databases[activeDbKey].collections[collKey] = $connections[activeHostKey].databases[activeDbKey].collections[collKey] || {};
$connections[activeHostKey].databases[activeDbKey].collections[collKey].stats = stats;
progress.end();
windowTitle.setSegments(activeDbKey + '.' + activeCollKey, $hosts[activeHostKey].name, 'Rolens');
}
async function truncateCollection(dbKey, collKey) {
const progress = startProgress(`Truncating collection "${collKey}"…`);
await TruncateCollection(activeHostKey, dbKey, collKey);
await reload();
progress.end();
}
async function dropCollection(dbKey, collKey) {
const progress = startProgress(`Dropping collection "${collKey}"…`);
const success = await DropCollection(activeHostKey, dbKey, collKey);
if (success) {
activeCollKey = '';
await reload();
}
progress.end();
}
</script> </script>
<Grid <Grid
striped={false} striped={false}
columns={[ { key: 'name' }, { key: 'count', right: true } ]} columns={[ { key: 'name' }, { key: 'count', right: true } ]}
bind:activePath={activeGridPath} items={Object.values($hostTree || {}).map(host => {
items={Object.keys($hosts).map(hostKey => {
return { return {
id: hostKey, id: host.key,
name: $hosts[hostKey].name, name: host.name,
icon: 'server', icon: 'server',
children: Object.keys(connection?.databases || {}).sort().map(dbKey => { children: Object.values(host.databases || {})
return { .sort((a, b) => a.key.localeCompare(b))
id: dbKey, .map(database => {
name: dbKey, return {
icon: 'db', id: database.key,
count: Object.keys(connection.databases[dbKey].collections || {}).length || '', name: database.key,
children: Object.keys(connection.databases[dbKey].collections).sort().map(collKey => { icon: 'db',
return { count: Object.keys(database.collections || {}).length || '',
id: collKey, children: Object.values(database.collections)
name: collKey, .sort((a, b) => a.key.localeCompare(b))
icon: 'list', .map(collection => {
menu: [ return {
{ label: 'Export collection (JSON, CSV)…', fn: () => dispatch('exportCollection', collKey) }, id: collection.key,
{ label: 'Dump collection (BSON via mongodump)…', fn: () => dispatch('dumpCollection', collKey) }, name: collection.key,
{ separator: true }, icon: 'list',
{ label: 'Rename collection…', fn: () => dispatch('renameCollection', collKey) }, menu: [
{ label: 'Truncate collection…', fn: () => truncateCollection(dbKey, collKey) }, { label: 'Export collection…', fn: collection.export },
{ label: 'Drop collection…', fn: () => dropCollection(dbKey, collKey) }, { label: 'Dump collection (BSON via mongodump)…', fn: collection.dump },
{ separator: true }, { separator: true },
{ label: 'New collection…', fn: () => dispatch('newCollection') }, { label: 'Rename collection…', fn: collection.rename },
], { label: 'Truncate collection…', fn: collection.truncate },
}; { label: 'Drop collection…', fn: collection.drop },
}) || [], { separator: true },
menu: [ { label: 'New collection…', fn: database.newCollection },
{ label: 'Drop database…', fn: () => dropDatabase(dbKey) }, ],
{ separator: true }, };
{ label: 'New database…', fn: () => dispatch('newDatabase') }, }) || [],
{ label: 'New collection…', fn: () => dispatch('newCollection') }, menu: [
], { label: 'Dump database (BSON via mongodump)…', fn: database.dump },
}; { label: 'Drop database…', fn: database.drop },
}), { separator: true },
{ label: 'New database…', fn: host.newDatabase },
{ label: 'New collection…', fn: database.newCollection },
],
};
}),
menu: [ menu: [
{ label: 'New database…', fn: () => dispatch('newDatabase') }, { label: 'New database…', fn: host.newDatabase },
{ separator: true }, { separator: true },
{ label: `Edit host ${$hosts[hostKey].name}`, fn: () => dispatch('editHost', hostKey) }, { label: `Edit host ${host.name}`, fn: host.edit },
{ label: 'Remove host…', fn: () => removeHost(hostKey) }, { label: 'Remove host…', fn: host.remove },
], ],
}; };
})} })}
on:select={e => { on:select={e => {
const key = e.detail.itemKey; let level;
switch (e.detail?.level) { ({ path, level } = e.detail);
case 0: return openConnection(key);
case 1: return openDatabase(key); switch (level) {
case 2: return openCollection(key); case 0: return $hostTree[path[0]].open();
case 1: return $hostTree[path[0]].databases[path[1]].open();
case 2: return $hostTree[path[0]].databases[path[1]].collections[path[2]].open();
} }
}} }}
/> />

View File

@ -1,144 +1,53 @@
<script> <script>
import { startProgress } from '$lib/progress';
import connections from '$lib/stores/connections';
import { RenameCollection } from '$wails/go/app/App';
import { EnterText } from '$wails/go/ui/UI';
import { EventsOn } from '$wails/runtime/runtime';
import HostView from './host/index.svelte';
import DatabaseView from './database/index.svelte';
import CollectionView from './collection/index.svelte';
import DumpInfo from './dump.svelte';
import HostDetail from './hostdetail.svelte';
import HostTree from './hosttree.svelte';
import sharedState from '$lib/stores/sharedstate';
import Icon from '$components/icon.svelte'; import Icon from '$components/icon.svelte';
import hosts from '$lib/stores/hosts'; import hostTree from '$lib/stores/hosttree';
import sharedState from '$lib/stores/sharedstate';
import CollectionView from './collection/index.svelte';
import DatabaseView from './database/index.svelte';
import HostView from './host/index.svelte';
import HostTree from './hosttree.svelte';
export let activeHostKey = ''; let path = [];
export let activeDbKey = '';
export let activeCollKey = '';
let hostTree; $: activeHostKey = path[0];
let showHostDetail = false; $: activeDbKey = path[1];
let hostDetailKey = ''; $: activeCollKey = path[2];
let exportInfo;
$: sharedState.currentHost.set(activeHostKey); $: sharedState.currentHost.set(activeHostKey);
$: sharedState.currentDb.set(activeDbKey); $: sharedState.currentDb.set(activeDbKey);
$: sharedState.currentColl.set(activeCollKey); $: sharedState.currentColl.set(activeCollKey);
export function createHost() {
hostDetailKey = '';
showHostDetail = true;
}
function editHost(hostKey) {
hostDetailKey = hostKey;
showHostDetail = true;
}
export async function createDatabase() {
const name = await 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) {
$connections[activeHostKey].databases[name] = { collections: {} };
}
}
async function renameCollection(oldCollKey) {
const newCollKey = await EnterText('Rename collection', `Enter a new name for collection ${oldCollKey}.`, oldCollKey);
if (newCollKey && (newCollKey !== oldCollKey)) {
const progress = startProgress(`Renaming collection "${oldCollKey}" to "${newCollKey}"…`);
const ok = await RenameCollection(activeHostKey, activeDbKey, oldCollKey, newCollKey);
if (ok) {
activeCollKey = newCollKey;
await hostTree.reload();
}
progress.end();
}
}
export async function createCollection() {
const name = await EnterText('Create a collection', 'Note: collections in MongoDB do not exist until they have at least one item. Your new collection will not persist on the server; fill it to have it created.');
if (name) {
$connections[activeHostKey].databases[activeDbKey].collections[name] = {};
}
}
function exportCollection(collKey) {
exportInfo = {
type: 'export',
filetype: 'json',
hostKey: activeHostKey,
dbKey: activeDbKey,
collKeys: [ collKey ],
};
}
function dumpCollection(collKey) {
exportInfo = {
type: 'dump',
filetype: 'bson',
hostKey: activeHostKey,
dbKey: activeDbKey,
collKeys: [ collKey ],
};
}
EventsOn('CreateHost', createHost);
EventsOn('CreateDatabase', createDatabase);
EventsOn('CreateCollection', createCollection);
</script> </script>
<div class="tree"> <div class="tree">
<div class="tree-buttons"> <div class="tree-buttons">
<button class="button-small" on:click={createHost}> <button class="button-small" on:click={hostTree.newHost}>
<Icon name="+" /> New host <Icon name="+" /> New host
</button> </button>
</div> </div>
<HostTree <HostTree bind:path />
bind:activeHostKey
bind:activeCollKey
bind:activeDbKey
bind:this={hostTree}
on:newHost={createHost}
on:newDatabase={createDatabase}
on:newCollection={createCollection}
on:editHost={e => editHost(e.detail)}
on:renameCollection={e => renameCollection(e.detail)}
on:exportCollection={e => exportCollection(e.detail)}
on:dumpCollection={e => dumpCollection(e.detail)}
/>
</div> </div>
{#if activeCollKey} {#if activeCollKey}
<CollectionView <CollectionView
collection={$connections[activeHostKey]?.databases[activeDbKey]?.collections?.[activeCollKey]} collection={$hostTree[activeHostKey]?.databases[activeDbKey]?.collections?.[activeCollKey]}
hostKey={activeHostKey} hostKey={activeHostKey}
dbKey={activeDbKey} dbKey={activeDbKey}
collKey={activeCollKey} collKey={activeCollKey}
/> />
{:else if activeDbKey} {:else if activeDbKey}
<DatabaseView <DatabaseView
database={$connections[activeHostKey]?.databases[activeDbKey]} database={$hostTree[activeHostKey]?.databases[activeDbKey]}
hostKey={activeHostKey} hostKey={activeHostKey}
dbKey={activeDbKey} dbKey={activeDbKey}
/> />
{:else if activeHostKey} {:else if activeHostKey}
<HostView <HostView
host={$connections[activeHostKey]} host={$hostTree[activeHostKey]}
hostKey={activeHostKey} hostKey={activeHostKey}
/> />
{/if} {/if}
<HostDetail
bind:show={showHostDetail}
on:reload={hosts.update}
hostKey={hostDetailKey}
/>
<DumpInfo bind:info={exportInfo} />
<style> <style>
.tree { .tree {
padding: 0.5rem; padding: 0.5rem;

View File

@ -1,60 +0,0 @@
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function createHost() {
dispatch('createHost');
}
</script>
<div class="welcome">
<div class="brand">
<img src="/logo.png" alt="" class="logo" />
<div class="text">
<div class="name">Welcome to Rolens!</div>
<div class="subtitle">A modest MongoDB client</div>
</div>
</div>
<button class="btn" on:click={createHost}>Create your first host</button>
</div>
<style>
.welcome {
/* transform: translateY(-80px); */
margin-top: -90px;
padding: 2rem;
}
.brand {
display: flex;
}
.brand .logo {
height: 200px;
}
.brand .text {
align-self: flex-end;
margin: 0 0 4rem 1rem;
}
.brand .text .name {
font-size: 2.5rem;
margin-bottom: 1.5rem;
font-weight: 600;
}
.brand .text .subtitle {
font-size: 1.5rem;
}
.logo {
height: 250px;
}
/* .title {
font-weight: 600;
font-size: 1.5rem;
} */
.btn {
margin-top: 2rem;
}
</style>

View File

@ -36,8 +36,6 @@ func (a *App) Menu() *menu.Menu {
fileMenu := appMenu.AddSubmenu("File") fileMenu := appMenu.AddSubmenu("File")
fileMenu.AddText("New host…", keys.CmdOrCtrl("y"), menuCallbackEmit(a, "CreateHost")) fileMenu.AddText("New host…", keys.CmdOrCtrl("y"), menuCallbackEmit(a, "CreateHost"))
fileMenu.AddText("New database", keys.CmdOrCtrl("y"), menuCallbackEmit(a, "CreateDatabase"))
fileMenu.AddText("New collection…", keys.CmdOrCtrl("i"), menuCallbackEmit(a, "CreateCollection"))
fileMenu.AddSeparator() fileMenu.AddSeparator()
fileMenu.AddText("Stats", keys.Combo("h", keys.CmdOrCtrlKey, keys.OptionOrAltKey), menuCallbackEmit(a, "OpenCollectionTab", "stats")) fileMenu.AddText("Stats", keys.Combo("h", keys.CmdOrCtrlKey, keys.OptionOrAltKey), menuCallbackEmit(a, "OpenCollectionTab", "stats"))
fileMenu.AddText("Find", keys.Combo("f", keys.CmdOrCtrlKey, keys.OptionOrAltKey), menuCallbackEmit(a, "OpenCollectionTab", "find")) fileMenu.AddText("Find", keys.Combo("f", keys.CmdOrCtrlKey, keys.OptionOrAltKey), menuCallbackEmit(a, "OpenCollectionTab", "find"))

View File

@ -134,9 +134,9 @@ func (a *App) UpdateFoundDocument(hostKey, dbKey, collKey, idJson, newDocJson st
} }
defer close() defer close()
if _, err := client.Database(dbKey).Collection(collKey).UpdateOne(ctx, id, bson.M{"$set": newDoc}); err != nil { if _, err := client.Database(dbKey).Collection(collKey).ReplaceOne(ctx, id, newDoc); err != nil {
runtime.LogInfof(a.ctx, "Error while performing find/update: %s", err.Error()) runtime.LogInfof(a.ctx, "Error while replacing document: %s", err.Error())
zenity.Error(err.Error(), zenity.Title("Unable to perform update"), zenity.ErrorIcon) zenity.Error(err.Error(), zenity.Title("Unable to replace document"), zenity.ErrorIcon)
return false return false
} }

View File

@ -246,7 +246,7 @@ func (a *App) PerformFindExport(hostKey, dbKey, collKey, settingsJson string) bo
continue continue
} }
csvItem = append(csvItem, string(v.(string))) csvItem = append(csvItem, fmt.Sprintf("%v", v))
} }
default: default:

View File

@ -21,13 +21,14 @@ type HostInfo struct {
func (a *App) connectToHost(hostKey string) (*mongo.Client, context.Context, func(), error) { func (a *App) connectToHost(hostKey string) (*mongo.Client, context.Context, func(), error) {
hosts, err := a.Hosts() hosts, err := a.Hosts()
if err != nil { if err != nil {
runtime.LogInfof(a.ctx, "Error while getting hosts: %s", err.Error())
zenity.Error(err.Error(), zenity.Title("Error while getting hosts"), zenity.ErrorIcon) zenity.Error(err.Error(), zenity.Title("Error while getting hosts"), zenity.ErrorIcon)
return nil, nil, nil, errors.New("could not retrieve hosts") return nil, nil, nil, errors.New("could not retrieve hosts")
} }
h := hosts[hostKey] h := hosts[hostKey]
if len(h.URI) == 0 { if len(h.URI) == 0 {
runtime.LogInfo(a.ctx, "Invalid URI (len == 0) for host "+hostKey) runtime.LogInfof(a.ctx, "Invalid URI (len == 0) for host %s", hostKey)
zenity.Warning("You haven't specified a valid uri for the selected host.", zenity.Title("Invalid query"), zenity.WarningIcon) zenity.Warning("You haven't specified a valid uri for the selected host.", zenity.Title("Invalid query"), zenity.WarningIcon)
return nil, nil, nil, errors.New("invalid uri") return nil, nil, nil, errors.New("invalid uri")
} }
@ -35,8 +36,7 @@ func (a *App) connectToHost(hostKey string) (*mongo.Client, context.Context, fun
client, err := mongo.NewClient(mongoOptions.Client().ApplyURI(h.URI)) client, err := mongo.NewClient(mongoOptions.Client().ApplyURI(h.URI))
if err != nil { if err != nil {
runtime.LogWarning(a.ctx, "Could not connect to host "+hostKey) runtime.LogWarningf(a.ctx, "Could not connect to host %s: %s", hostKey, err.Error())
runtime.LogWarning(a.ctx, err.Error())
zenity.Error(err.Error(), zenity.Title("Error while connecting to "+h.Name), zenity.ErrorIcon) zenity.Error(err.Error(), zenity.Title("Error while connecting to "+h.Name), zenity.ErrorIcon)
return nil, nil, nil, errors.New("could not establish a connection with " + h.Name) return nil, nil, nil, errors.New("could not establish a connection with " + h.Name)
} }