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 id="dialogoutlets"></div>
<script src="./src/main.js" type="module"></script>
</body>
</html>

View File

@ -1 +1 @@
499da1237327bf4ddd2de74bfd6635c7
5d3dd16f94d140a9f950a48981534162

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,11 +3,9 @@
import Modal from '$components/modal.svelte';
import input from '$lib/actions/input';
import settings from '$lib/stores/settings';
export let show = false;
</script>
<Modal title="Preferences" bind:show>
<Modal title="Preferences" on:close>
<div class="prefs">
<label for="defaultLimit">Initial number of items to retrieve using one query (limit):</label>
<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) {
return;
}
if (env && settings) {
else if (env && settings) {
Promise.all(listeners.map(l => l())).then(() => {
set(true);
alreadyInited = true;
// Remove loading spinner.
document.getElementById('app-loading')?.remove();
// Call hooks
listeners.forEach(l => l());
});
}
}, 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 { get, writable } from 'svelte/store';
@ -23,6 +25,11 @@ function forCollection(hostKey, dbKey, collKey) {
return Object.fromEntries(entries);
}
function openConfig(collection, firstItem = {}) {
const dialog = dialogs.new(ViewConfigDialog, { collection, firstItem });
return dialog;
}
reload();
subscribe(newViewStore => {
if (skipUpdate) {
@ -32,5 +39,5 @@ subscribe(newViewStore => {
UpdateViewStore(JSON.stringify(newViewStore));
});
const views = { reload, set, subscribe, forCollection };
const views = { reload, set, subscribe, forCollection, openConfig };
export default views;

View File

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

View File

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

View File

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

View File

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

View File

@ -4,13 +4,12 @@
import Icon from '$components/icon.svelte';
import Modal from '$components/modal.svelte';
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 { createEventDispatcher } from 'svelte';
export let queryToSave = undefined;
export let collection = {};
export let show = false;
const dispatch = createEventDispatcher();
let gridSelectedPath = [];
@ -22,23 +21,16 @@
queryToSave.dbKey = collection.dbKey;
queryToSave.collKey = collection.key;
const newId = queries.create(queryToSave);
if (newId) {
dispatch('created', newId);
queryToSave = undefined;
selectedKey = newId;
select();
}
dispatch('create', { query: queryToSave });
selectedKey = queryToSave.name;
}
else {
select();
selectActive();
}
}
function select() {
dispatch('select', selectedKey);
show = false;
function selectActive() {
dispatch('select', { query: $queries[selectedKey] });
}
function gridSelect(event) {
@ -53,7 +45,7 @@
function gridTrigger(event) {
gridSelect(event);
select();
selectActive();
}
async function gridRemove(event) {
@ -71,7 +63,7 @@
}
</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}>
{#if queryToSave}
<label class="field queryname">
@ -88,7 +80,11 @@
columns={[ { key: 'n', title: 'Query name' }, { key: 'h', title: 'Host' }, { key: 'ns', title: 'Namespace' } ]}
key="n"
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;
}, {})}
showHeaders={true}

View File

@ -7,15 +7,18 @@
import views from '$lib/stores/views';
export let collection;
export let show = false;
export let activeViewKey = 'list';
export let firstItem = {};
$: tabs = Object.entries(views.forCollection(collection.hostKey, collection.dbKey, collection.key))
let tabs = [];
$: $views && refresh();
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) {
if (a === 'list') {
@ -39,29 +42,29 @@
type: 'table',
columns: [ { key: '_id', showInTable: true, inputType: 'objectid', mandatory: true } ],
};
activeViewKey = newViewKey;
collection.viewKey = newViewKey;
}
function removeView(viewKey) {
const keys = Object.keys($views).sort(sortTabKeys);
const oldIndex = keys.indexOf(viewKey);
const newKey = keys[oldIndex - 1];
activeViewKey = newKey;
collection.viewKey = newKey;
delete $views[viewKey];
$views = $views;
}
function addColumn(before) {
if (typeof before === 'number') {
$views[activeViewKey].columns = [
...$views[activeViewKey].columns.slice(0, before),
$views[collection.viewKey].columns = [
...$views[collection.viewKey].columns.slice(0, before),
{ showInTable: true, inputType: 'none' },
...$views[activeViewKey].columns.slice(before),
...$views[collection.viewKey].columns.slice(before),
];
}
else {
$views[activeViewKey].columns = [
...$views[activeViewKey].columns,
$views[collection.viewKey].columns = [
...$views[collection.viewKey].columns,
{ showInTable: true, inputType: 'none' },
];
}
@ -72,7 +75,7 @@
return;
}
$views[activeViewKey].columns = Object.keys(firstItem).sort().map(key => {
$views[collection.viewKey].columns = Object.keys(firstItem).sort().map(key => {
return {
key,
showInTable: true,
@ -82,57 +85,57 @@
}
function moveColumn(oldIndex, delta) {
const column = $views[activeViewKey].columns[oldIndex];
const column = $views[collection.viewKey].columns[oldIndex];
const newIndex = oldIndex + delta;
$views[activeViewKey].columns.splice(oldIndex, 1);
$views[activeViewKey].columns.splice(newIndex, 0, column);
$views[activeViewKey].columns = $views[activeViewKey].columns;
$views[collection.viewKey].columns.splice(oldIndex, 1);
$views[collection.viewKey].columns.splice(newIndex, 0, column);
$views[collection.viewKey].columns = $views[collection.viewKey].columns;
}
function removeColumn(index) {
$views[activeViewKey].columns.splice(index, 1);
$views[activeViewKey].columns = $views[activeViewKey].columns;
$views[collection.viewKey].columns.splice(index, 1);
$views[collection.viewKey].columns = $views[collection.viewKey].columns;
}
</script>
<Modal title="View configuration" bind:show contentPadding={false}>
<Modal title="View configuration" contentPadding={false} on:close>
<TabBar
{tabs}
canAddTab={true}
on:addTab={createView}
on:closeTab={e => removeView(e.detail)}
bind:selectedKey={activeViewKey}
bind:selectedKey={collection.viewKey}
/>
<div class="options">
{#if $views[activeViewKey]}
{#if $views[collection.viewKey]}
<div class="meta">
{#key activeViewKey}
{#key collection.viewKey}
<label class="field">
<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>
{/key}
<label class="field">
<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="table">Table view</option>
</select>
</label>
</div>
{#if $views[activeViewKey].type === 'list'}
{#if $views[collection.viewKey].type === 'list'}
<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">
Hide object indicators ({'{...}'} and [...]) in list view and show nothing instead
</label>
</div>
{:else if $views[activeViewKey].type === 'table'}
{:else if $views[collection.viewKey].type === 'table'}
<div class="columns">
{#each $views[activeViewKey].columns as column, columnIndex}
{#each $views[collection.viewKey].columns as column, columnIndex}
<div class="column">
<label class="field">
<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">
<Icon name="chev-u" />
</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" />
</button>
<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 { deepClone } from '$lib/objects';
import { startProgress } from '$lib/progress';
import queries from '$lib/stores/queries';
import applicationSettings from '$lib/stores/settings';
import views from '$lib/stores/views';
import { convertLooseJson } from '$lib/strings';
import { FindItems, RemoveItemById, UpdateFoundDocument } from '$wails/go/app/App';
import { EJSON } from 'bson';
import { createEventDispatcher, onMount } from 'svelte';
import ExportInfo from './components/export.svelte';
import QueryChooser from './components/querychooser.svelte';
import { onMount } from 'svelte';
export let collection;
const dispatch = createEventDispatcher();
const defaults = {
query: '{}',
sort: $applicationSettings.defaultSort || '{ "_id": 1 }',
@ -32,17 +29,18 @@
let queryField;
let activePath = [];
let objectViewerData;
let queryToSave;
let showQueryChooser = false;
let exportInfo;
let querying = false;
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})` : ''};`;
$: 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;
$: 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() {
if (querying) {
return;
@ -70,19 +68,18 @@
}
}
function loadQuery() {
queryToSave = undefined;
showQueryChooser = true;
async function loadQuery() {
const query = await collection.openQueryChooser();
if (query) {
form = { ...query };
submitQuery();
}
}
function saveQuery() {
queryToSave = form;
showQueryChooser = true;
}
function queryChosen(event) {
if ($queries[event?.detail]) {
form = { ...$queries[event?.detail] };
async function saveQuery() {
const query = await collection.openQueryChooser(form);
if (query) {
form = { ...query };
submitQuery();
}
}
@ -130,6 +127,10 @@
objectViewerData = item;
}
function openViewConfig() {
views.openConfig(collection, result.results?.[0] || {});
}
export function performQuery(q) {
form = { ...defaults, ...q };
submitQuery();
@ -142,7 +143,7 @@
collection.dbKey,
collection.key,
EJSON.stringify({ _id: event.detail.originalData._id }),
event.detail.text
convertLooseJson(event.detail.text)
);
if (success) {
@ -207,7 +208,7 @@
<button type="submit" class="btn" title="Run query">
<Icon name="play" /> Run
</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…
</button>
<div class="field">
@ -259,7 +260,7 @@
<option value={key}>{view.name}</option>
{/each}
</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" />
</button>
</label>
@ -283,15 +284,6 @@
</div>
</div>
<QueryChooser
bind:queryToSave
bind:show={showQueryChooser}
on:select={queryChosen}
{collection}
/>
<ExportInfo on:openViewConfig bind:collection bind:info={exportInfo} />
{#if objectViewerData}
<!-- @todo Implement save -->
<ObjectViewer bind:data={objectViewerData} saveable on:save={saveDocument} bind:successMessage={objectViewerSuccessMessage} />

View File

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

View File

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

View File

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

View File

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

View File

@ -1,26 +1,17 @@
<script>
import Modal from '$components/modal.svelte';
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 { createEventDispatcher } from 'svelte';
import { createEventDispatcher, onMount } from 'svelte';
export let show = false;
export let hostKey = '';
const dispatch = createEventDispatcher();
let form = {};
let error = '';
$: valid = validate(form);
$: host = $hosts[hostKey];
$: if (show || !show) {
init();
}
function init() {
form = { ...(host || {}) };
}
$: host = $hostTree[hostKey];
function validate(form) {
return form.name && form.uri && true;
@ -41,16 +32,20 @@
hostKey = newHostKey;
}
}
show = false;
dispatch('reload');
dispatch('updated', form);
dispatch('close');
}
catch (e) {
error = e;
}
}
onMount(() => {
form = { ...(host || {}) };
});
</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}>
<label class="field">
<span class="label">Label</span>

View File

@ -1,180 +1,70 @@
<script>
import Grid from '$components/grid.svelte';
import { startProgress } from '$lib/progress';
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';
import hostTree from '$lib/stores/hosttree';
export let activeHostKey = '';
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();
}
export let path = [];
</script>
<Grid
striped={false}
columns={[ { key: 'name' }, { key: 'count', right: true } ]}
bind:activePath={activeGridPath}
items={Object.keys($hosts).map(hostKey => {
items={Object.values($hostTree || {}).map(host => {
return {
id: hostKey,
name: $hosts[hostKey].name,
id: host.key,
name: host.name,
icon: 'server',
children: Object.keys(connection?.databases || {}).sort().map(dbKey => {
children: Object.values(host.databases || {})
.sort((a, b) => a.key.localeCompare(b))
.map(database => {
return {
id: dbKey,
name: dbKey,
id: database.key,
name: database.key,
icon: 'db',
count: Object.keys(connection.databases[dbKey].collections || {}).length || '',
children: Object.keys(connection.databases[dbKey].collections).sort().map(collKey => {
count: Object.keys(database.collections || {}).length || '',
children: Object.values(database.collections)
.sort((a, b) => a.key.localeCompare(b))
.map(collection => {
return {
id: collKey,
name: collKey,
id: collection.key,
name: collection.key,
icon: 'list',
menu: [
{ label: 'Export collection (JSON, CSV)…', fn: () => dispatch('exportCollection', collKey) },
{ label: 'Dump collection (BSON via mongodump)…', fn: () => dispatch('dumpCollection', collKey) },
{ label: 'Export collection…', fn: collection.export },
{ label: 'Dump collection (BSON via mongodump)…', fn: collection.dump },
{ separator: true },
{ label: 'Rename collection…', fn: () => dispatch('renameCollection', collKey) },
{ label: 'Truncate collection…', fn: () => truncateCollection(dbKey, collKey) },
{ label: 'Drop collection…', fn: () => dropCollection(dbKey, collKey) },
{ label: 'Rename collection…', fn: collection.rename },
{ label: 'Truncate collection…', fn: collection.truncate },
{ label: 'Drop collection…', fn: collection.drop },
{ separator: true },
{ label: 'New collection…', fn: () => dispatch('newCollection') },
{ label: 'New collection…', fn: database.newCollection },
],
};
}) || [],
menu: [
{ label: 'Drop database…', fn: () => dropDatabase(dbKey) },
{ label: 'Dump database (BSON via mongodump)…', fn: database.dump },
{ label: 'Drop database…', fn: database.drop },
{ separator: true },
{ label: 'New database…', fn: () => dispatch('newDatabase') },
{ label: 'New collection…', fn: () => dispatch('newCollection') },
{ label: 'New database…', fn: host.newDatabase },
{ label: 'New collection…', fn: database.newCollection },
],
};
}),
menu: [
{ label: 'New database…', fn: () => dispatch('newDatabase') },
{ label: 'New database…', fn: host.newDatabase },
{ separator: true },
{ label: `Edit host ${$hosts[hostKey].name}`, fn: () => dispatch('editHost', hostKey) },
{ label: 'Remove host…', fn: () => removeHost(hostKey) },
{ label: `Edit host ${host.name}`, fn: host.edit },
{ label: 'Remove host…', fn: host.remove },
],
};
})}
on:select={e => {
const key = e.detail.itemKey;
switch (e.detail?.level) {
case 0: return openConnection(key);
case 1: return openDatabase(key);
case 2: return openCollection(key);
let level;
({ path, level } = e.detail);
switch (level) {
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>
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 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 = '';
export let activeDbKey = '';
export let activeCollKey = '';
let path = [];
let hostTree;
let showHostDetail = false;
let hostDetailKey = '';
let exportInfo;
$: activeHostKey = path[0];
$: activeDbKey = path[1];
$: activeCollKey = path[2];
$: sharedState.currentHost.set(activeHostKey);
$: sharedState.currentDb.set(activeDbKey);
$: 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>
<div class="tree">
<div class="tree-buttons">
<button class="button-small" on:click={createHost}>
<button class="button-small" on:click={hostTree.newHost}>
<Icon name="+" /> New host
</button>
</div>
<HostTree
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)}
/>
<HostTree bind:path />
</div>
{#if activeCollKey}
<CollectionView
collection={$connections[activeHostKey]?.databases[activeDbKey]?.collections?.[activeCollKey]}
collection={$hostTree[activeHostKey]?.databases[activeDbKey]?.collections?.[activeCollKey]}
hostKey={activeHostKey}
dbKey={activeDbKey}
collKey={activeCollKey}
/>
{:else if activeDbKey}
<DatabaseView
database={$connections[activeHostKey]?.databases[activeDbKey]}
database={$hostTree[activeHostKey]?.databases[activeDbKey]}
hostKey={activeHostKey}
dbKey={activeDbKey}
/>
{:else if activeHostKey}
<HostView
host={$connections[activeHostKey]}
host={$hostTree[activeHostKey]}
hostKey={activeHostKey}
/>
{/if}
<HostDetail
bind:show={showHostDetail}
on:reload={hosts.update}
hostKey={hostDetailKey}
/>
<DumpInfo bind:info={exportInfo} />
<style>
.tree {
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.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.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"))

View File

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

View File

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

View File

@ -21,13 +21,14 @@ type HostInfo struct {
func (a *App) connectToHost(hostKey string) (*mongo.Client, context.Context, func(), error) {
hosts, err := a.Hosts()
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)
return nil, nil, nil, errors.New("could not retrieve hosts")
}
h := hosts[hostKey]
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)
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))
if err != nil {
runtime.LogWarning(a.ctx, "Could not connect to host "+hostKey)
runtime.LogWarning(a.ctx, err.Error())
runtime.LogWarningf(a.ctx, "Could not connect to host %s: %s", hostKey, err.Error())
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)
}