1
0
mirror of https://github.com/garraflavatra/rolens.git synced 2025-07-19 22:18:03 +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:
2023-06-18 21:31:55 +02:00
committed by GitHub
parent 3fe5f09163
commit a1456b3987
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) {
set(true);
alreadyInited = true;
// Remove loading spinner.
document.getElementById('app-loading')?.remove();
// Call hooks
listeners.forEach(l => l());
else if (env && settings) {
Promise.all(listeners.map(l => l())).then(() => {
set(true);
alreadyInited = true;
document.getElementById('app-loading')?.remove();
});
}
}, 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))
.sort((a, b) => sortTabKeys(a[0], b[0]))
.map(([ key, v ]) => {
return { key, title: v.name, closable: key !== 'list' };
});
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 drop(key) {
if (typeof key !== 'string') {
key = activePath[0];
async function dropIndex(indexName) {
if (typeof indexName !== 'string') {
indexName = 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 => {
return {
id: dbKey,
name: dbKey,
icon: 'db',
count: Object.keys(connection.databases[dbKey].collections || {}).length || '',
children: Object.keys(connection.databases[dbKey].collections).sort().map(collKey => {
return {
id: collKey,
name: collKey,
icon: 'list',
menu: [
{ label: 'Export collection (JSON, CSV)…', fn: () => dispatch('exportCollection', collKey) },
{ label: 'Dump collection (BSON via mongodump)…', fn: () => dispatch('dumpCollection', collKey) },
{ separator: true },
{ label: 'Rename collection…', fn: () => dispatch('renameCollection', collKey) },
{ label: 'Truncate collection…', fn: () => truncateCollection(dbKey, collKey) },
{ label: 'Drop collection…', fn: () => dropCollection(dbKey, collKey) },
{ separator: true },
{ label: 'New collection…', fn: () => dispatch('newCollection') },
],
};
}) || [],
menu: [
{ label: 'Drop database…', fn: () => dropDatabase(dbKey) },
{ separator: true },
{ label: 'New database…', fn: () => dispatch('newDatabase') },
{ label: 'New collection…', fn: () => dispatch('newCollection') },
],
};
}),
children: Object.values(host.databases || {})
.sort((a, b) => a.key.localeCompare(b))
.map(database => {
return {
id: database.key,
name: database.key,
icon: 'db',
count: Object.keys(database.collections || {}).length || '',
children: Object.values(database.collections)
.sort((a, b) => a.key.localeCompare(b))
.map(collection => {
return {
id: collection.key,
name: collection.key,
icon: 'list',
menu: [
{ label: 'Export collection…', fn: collection.export },
{ label: 'Dump collection (BSON via mongodump)…', fn: collection.dump },
{ separator: true },
{ label: 'Rename collection…', fn: collection.rename },
{ label: 'Truncate collection…', fn: collection.truncate },
{ label: 'Drop collection…', fn: collection.drop },
{ separator: true },
{ label: 'New collection…', fn: database.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: [
{ 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>