1
0
mirror of https://github.com/garraflavatra/rolens.git synced 2025-07-20 14:38:04 +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

@ -1,72 +0,0 @@
<script>
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=" ">
<div class="brand">
<img src="/logo.png" alt="Rolens logo" />
<div>
<div class="title">
Rolens
<span class="version">{$environment.version}</span>
</div>
<div class="description">Intuitive MongoDB <br /> administration tool</div>
</div>
</div>
<hr />
<div class="info">
<p class="copy">© Romein van Buren, 2023.</p>
<p>
<a href="https://garraflavatra.github.io/rolens/" use:alink>Documentation</a> |
<a href="https://github.com/garraflavatra/rolens" use:alink>GitHub</a> |
<a href="https://github.com/garraflavatra/rolens/issues/new" use:alink>Report a bug</a> |
<a href="https://github.com/garraflavatra/rolens/blob/main/LICENSE" use:alink>License</a>
</p>
</div>
</Modal>
<style>
.brand {
display: flex;
align-items: center;
justify-content: space-around;
}
.brand > img {
width: 125px;
flex: 0 1 125px;
}
.brand .title {
font-size: 2.25rem;
font-weight: 600;
line-height: 2.5rem;
}
.brand .title .version {
font-size: 80%;
font-weight: 300;
opacity: 0.65;
}
.brand .description {
font-size: 1.5rem;
line-height: 1.6rem;
}
hr {
margin: 2rem 1rem;
}
.info {
font-size: 1.25rem;
line-height: 1.25rem;
margin: 0 1rem 1rem;
text-align: center;
}
.info .copy {
font-weight: 500;
}
</style>

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>

View File

@ -1,44 +0,0 @@
<script>
import DirectoryChooser from '$components/directorychooser.svelte';
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>
<div class="prefs">
<label for="defaultLimit">Initial number of items to retrieve using one query (limit):</label>
<label class="field">
<input type="number" bind:value={$settings.defaultLimit} id="defaultLimit" use:input={{ autofocus: true }} />
<span class="label">items</span>
</label>
<label for="defaultSort">Default sort query</label>
<label class="field">
<input type="text" class="code" bind:value={$settings.defaultSort} id="defaultSort" use:input={{ type: 'json' }} />
</label>
<label for="autosubmitQuery">Autosubmit query</label>
<span>
<input type="checkbox" id="autosubmitQuery" bind:checked={$settings.autosubmitQuery} />
<label for="autosubmitQuery">Query items automatically after opening a collection</label>
</span>
<label for="defaultExportDirectory">Default export directory</label>
<!-- svelte-ignore a11y-label-has-associated-control -->
<label class="field">
<DirectoryChooser id="defaultExportDirectory" bind:value={$settings.defaultExportDirectory} />
</label>
</div>
</Modal>
<style>
.prefs {
display: grid;
grid-template-columns: auto auto;
gap: 0.5rem;
align-items: center;
}
</style>