diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 new file mode 100755 index 0000000..e072450 --- /dev/null +++ b/frontend/package.json.md5 @@ -0,0 +1 @@ +e25516ba8abdacf5ab40609a5d48f542 \ No newline at end of file diff --git a/frontend/src/app.svelte b/frontend/src/app.svelte index 0e2a44e..cf45542 100644 --- a/frontend/src/app.svelte +++ b/frontend/src/app.svelte @@ -1,19 +1,15 @@ @@ -52,13 +29,7 @@ {#if $environment && $applicationSettings}
- openConnection(e.detail)} bind:modalOpen={addressBarModalOpen} /> - - {#if host && connection} - - {:else} - - {/if} +
{#key $contextMenu} @@ -82,16 +53,11 @@ main { height: 100vh; display: grid; - grid-template: 3rem auto / 250px 1fr; - gap: 0.5rem; - padding: 0.5rem; + grid-template: 1fr / 250px 1fr; } #root.platform-darwin main { height: calc(100vh - var(--darwin-titlebar-height)); } - main.empty { - grid-template: 3rem auto / 1fr; - } main > :global(*) { overflow: auto; diff --git a/frontend/src/components/contextmenu.svelte b/frontend/src/components/contextmenu.svelte index cd7c736..c4dbdb1 100644 --- a/frontend/src/components/contextmenu.svelte +++ b/frontend/src/components/contextmenu.svelte @@ -5,11 +5,16 @@ export let position = undefined; const dispatch = createEventDispatcher(); - let selected = -1; const buttons = []; + let selected = -1; + + $: if (items && position) { + selected = 0; + } function close() { dispatch('close'); + selected = -1; } function click(fn) { diff --git a/frontend/src/components/icon.svelte b/frontend/src/components/icon.svelte index 2d9e77a..78fdbb1 100644 --- a/frontend/src/components/icon.svelte +++ b/frontend/src/components/icon.svelte @@ -31,6 +31,8 @@ {:else if name === 'chevs-r'} + {:else if name === 'arr-d'} + {:else if name === 'db'} {:else if name === 'x'} @@ -55,5 +57,7 @@ {:else if name === 'zap'} + {:else if name === 'server'} + {/if} diff --git a/frontend/src/organisms/addressbar/hostmodal.svelte b/frontend/src/organisms/addressbar/hostmodal.svelte deleted file mode 100644 index 5095bfc..0000000 --- a/frontend/src/organisms/addressbar/hostmodal.svelte +++ /dev/null @@ -1,148 +0,0 @@ - - - - {#if hostCount} -
-

- {#if error} - Oops! {error} - {:else} - {hostCount} host{hostCount === 1 ? '' : 's'} - {/if} -

- -
-
    - {#each Object.entries(hosts) as [hostKey, host]} -
  • -
    - - - -
    -
  • - {/each} -
- {:else} - - {/if} -
- - - - diff --git a/frontend/src/organisms/addressbar/index.svelte b/frontend/src/organisms/addressbar/index.svelte deleted file mode 100644 index 07a4602..0000000 --- a/frontend/src/organisms/addressbar/index.svelte +++ /dev/null @@ -1,49 +0,0 @@ - - -
- -
modalOpen = true}> - {#if host?.uri} - {@const split = host.uri.split('://').map(s => s.split('/')).flat()} - {split[0]}://{split[1]}{split.slice(2).join('/')} - {:else} - no host selected - {/if} -
- -
- -
-
- - - - diff --git a/frontend/src/organisms/addressbar/hostdetail.svelte b/frontend/src/organisms/connection/hostdetail.svelte similarity index 96% rename from frontend/src/organisms/addressbar/hostdetail.svelte rename to frontend/src/organisms/connection/hostdetail.svelte index 33683f5..7fba0f1 100644 --- a/frontend/src/organisms/addressbar/hostdetail.svelte +++ b/frontend/src/organisms/connection/hostdetail.svelte @@ -5,13 +5,14 @@ import Modal from '../../components/modal.svelte'; export let show = false; - export let host = undefined; export let hostKey = ''; + export let hosts = {}; const dispatch = createEventDispatcher(); - let form = { ...(host || {}) }; + let form = {}; let error = ''; $: valid = validate(form); + $: host = hosts[hostKey]; $: if (show || !show) { init(); diff --git a/frontend/src/organisms/connection/dblist.svelte b/frontend/src/organisms/connection/hosttree.svelte similarity index 50% rename from frontend/src/organisms/connection/dblist.svelte rename to frontend/src/organisms/connection/hosttree.svelte index 2f2c2ad..a058d1c 100644 --- a/frontend/src/organisms/connection/dblist.svelte +++ b/frontend/src/organisms/connection/hosttree.svelte @@ -3,7 +3,7 @@ import { createEventDispatcher } from 'svelte'; import { DropCollection, DropDatabase, OpenCollection, OpenConnection, OpenDatabase } from '../../../wailsjs/go/app/App'; import Grid from '../../components/grid.svelte'; - import { WindowSetTitle } from '../../../wailsjs/runtime'; + import { WindowSetTitle } from '../../../wailsjs/runtime/runtime'; export let hosts = {}; export let activeHostKey = ''; @@ -11,9 +11,10 @@ export let activeCollKey = ''; const dispatch = createEventDispatcher(); - let dbAndCollKeys = []; - $: activeDbKey = dbAndCollKeys[0]; - $: activeCollKey = dbAndCollKeys[1]; + let activeGridPath = []; + $: activeHostKey = activeGridPath[0] || activeHostKey; + $: activeDbKey = activeGridPath[1]; + $: activeCollKey = activeGridPath[2]; $: host = hosts[activeHostKey]; $: connection = $connections[activeHostKey]; $: database = connection?.databases[activeDbKey]; @@ -74,67 +75,64 @@ await reload(); busy.end(); } + + function buildMenu(hostKey, dbKey, collKey, context = [ 'new', 'edit', 'drop' ]) { + // context: n = new, d = drop + const menu = []; + + if (context.includes('edit')) { + hostKey && menu.push({ label: `Edit host ${hosts[hostKey].name}…`, fn: () => dispatch('editHost', hostKey) }); + collKey && menu.push({ label: `Rename collection ${collKey}…`, fn: () => dispatch('editCollection', collKey) }); + } + if (context.includes('drop')) { + menu.length && menu.push({ separator: true }); + dbKey && menu.push({ label: `Drop database ${dbKey}…`, fn: () => dropDatabase(dbKey) }); + collKey && menu.push({ label: `Drop collection ${collKey}…`, fn: () => dropCollection(dbKey, collKey) }); + } + if (context.includes('new')) { + menu.length && menu.push({ separator: true }); + hostKey && menu.push({ label: 'New database…', fn: () => dispatch('newDatabase') }); + dbKey && menu.push({ label: 'New collection…', fn: () => dispatch('newCollection') }); + } + + return menu; + } -{#if host && connection} - ({ + ({ + id: hostKey, + name: hosts[hostKey].name, + icon: 'server', + children: Object.keys(connection?.databases || {}).sort().map(dbKey => ({ id: dbKey, + name: dbKey, icon: 'db', - collCount: Object.keys(connection.databases[dbKey].collections || {}).length || '', + count: Object.keys(connection.databases[dbKey].collections || {}).length || '', children: Object.keys(connection.databases[dbKey].collections).sort().map(collKey => ({ id: collKey, + name: collKey, icon: 'list', - menu: [ - { label: `Drop ${collKey}…`, fn: () => dropCollection(dbKey, collKey) }, - { label: `Drop ${dbKey}…`, fn: () => dropDatabase(dbKey) }, - { separator: true }, - { label: 'New database…', fn: () => dispatch('newDatabase') }, - { label: 'New collection…', fn: () => dispatch('newCollection') }, - ], + menu: buildMenu(hostKey, dbKey, collKey), })) || [], - menu: [ - { label: `Drop ${dbKey}…`, fn: () => dropDatabase(dbKey) }, - { separator: true }, - { label: 'New database…', fn: () => dispatch('newDatabase') }, - { label: 'New collection…', fn: () => dispatch('newCollection') }, - ], - }))} - actions={[ - { icon: 'reload', fn: reload }, - { icon: '+', fn: evt => { - if (activeDbKey) { - contextMenu.show(evt, [ - { label: 'New database…', fn: () => dispatch('newDatabase') }, - { label: 'New collection…', fn: () => dispatch('newCollection') }, - ]); - } - else { - dispatch('newDatabase'); - } - } }, - { icon: '-', fn: evt => { - if (activeCollKey) { - contextMenu.show(evt, [ - { label: 'Drop database…', fn: () => dropDatabase(activeDbKey) }, - { label: 'Drop collection…', fn: () => dropCollection(activeDbKey, activeCollKey) }, - ]); - } - else { - dropDatabase(activeDbKey); - } - }, disabled: !activeDbKey }, - ]} - bind:activePath={dbAndCollKeys} - on:select={e => { - if (e.detail?.level === 0) { - openDatabase(e.detail.itemKey); - } - else if (e.detail?.level === 1) { - openCollection(e.detail.itemKey); - } - }} - /> -{/if} + menu: buildMenu(hostKey, dbKey), + })), + }))} + actions={[ + { icon: 'reload', fn: reload }, + { icon: '+', fn: evt => contextMenu.show(evt, buildMenu(activeHostKey, activeDbKey, activeCollKey, 'new')) }, + { icon: 'edit', fn: evt => contextMenu.show(evt, buildMenu(activeHostKey, activeDbKey, activeCollKey, 'edit')), disabled: !activeHostKey }, + { icon: '-', fn: evt => contextMenu.show(evt, buildMenu(activeHostKey, activeDbKey, activeCollKey, 'drop')), disabled: !activeDbKey }, + ]} + bind:activePath={activeGridPath} + 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); + } + }} +/> diff --git a/frontend/src/organisms/connection/index.svelte b/frontend/src/organisms/connection/index.svelte index 80b8140..24d99f0 100644 --- a/frontend/src/organisms/connection/index.svelte +++ b/frontend/src/organisms/connection/index.svelte @@ -1,34 +1,66 @@ - addressBarModalOpen = false} + bind:this={hostTree} + on:newHost={createHost} on:newDatabase={() => newDb = {}} on:newCollection={() => newColl = {}} + on:editHost={e => editHost(e.detail)} + on:editCollection={e => openEditCollModal(e.detail)} /> + + {#if newDb}

Create a database

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.

Create a collections

+

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.

Renaming collection {collToRename}
+ +
+ + + +
+ +
+{/if} + diff --git a/frontend/src/organisms/addressbar/welcome.svelte b/frontend/src/organisms/connection/welcome.svelte similarity index 100% rename from frontend/src/organisms/addressbar/welcome.svelte rename to frontend/src/organisms/connection/welcome.svelte diff --git a/frontend/src/stores.js b/frontend/src/stores.js index d2e5c5f..e6b79a4 100644 --- a/frontend/src/stores.js +++ b/frontend/src/stores.js @@ -25,7 +25,7 @@ export const contextMenu = (() => { const { set, subscribe } = writable(); return { - show: (evt, menu) => set(menu ? { + show: (evt, menu) => set(Object.keys(menu || {}).length ? { position: [ evt.clientX, evt.clientY ], items: menu, } : undefined), diff --git a/frontend/src/style.css b/frontend/src/style.css index 3d57dd3..251390f 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -19,7 +19,6 @@ body { } * { - vertical-align: middle; box-sizing: border-box; overscroll-behavior: none; -webkit-font-smoothing: antialiased; diff --git a/frontend/wailsjs/go/app/App.d.ts b/frontend/wailsjs/go/app/App.d.ts index f371d5b..373847c 100755 --- a/frontend/wailsjs/go/app/App.d.ts +++ b/frontend/wailsjs/go/app/App.d.ts @@ -37,6 +37,8 @@ export function RemoveItemById(arg1:string,arg2:string,arg3:string,arg4:string): export function RemoveItems(arg1:string,arg2:string,arg3:string,arg4:string,arg5:boolean):Promise; +export function RenameCollection(arg1:string,arg2:string,arg3:string,arg4:string):Promise; + export function Settings():Promise; export function UpdateHost(arg1:string,arg2:string):Promise; diff --git a/frontend/wailsjs/go/app/App.js b/frontend/wailsjs/go/app/App.js index 4de6ef0..5bb6c4f 100755 --- a/frontend/wailsjs/go/app/App.js +++ b/frontend/wailsjs/go/app/App.js @@ -66,6 +66,10 @@ export function RemoveItems(arg1, arg2, arg3, arg4, arg5) { return window['go']['app']['App']['RemoveItems'](arg1, arg2, arg3, arg4, arg5); } +export function RenameCollection(arg1, arg2, arg3, arg4) { + return window['go']['app']['App']['RenameCollection'](arg1, arg2, arg3, arg4); +} + export function Settings() { return window['go']['app']['App']['Settings'](); } diff --git a/internal/app/app.go b/internal/app/app.go index 94beb5a..3d45771 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -36,10 +36,10 @@ func (a *App) Menu() *menu.Menu { wailsRuntime.Quit(a.ctx) }) - fileMenu := appMenu.AddSubmenu("File") - fileMenu.AddText("Hosts…", keys.CmdOrCtrl("k"), func(cd *menu.CallbackData) { - wailsRuntime.EventsEmit(a.ctx, "OpenHostsModal") - }) + // fileMenu := appMenu.AddSubmenu("File") + // fileMenu.AddText("Hosts…", keys.CmdOrCtrl("k"), func(cd *menu.CallbackData) { + // wailsRuntime.EventsEmit(a.ctx, "OpenHostsModal") + // }) if runtime.GOOS == "darwin" { appMenu.Append(menu.EditMenu()) diff --git a/internal/app/collection.go b/internal/app/collection.go index e84551e..48e4abc 100644 --- a/internal/app/collection.go +++ b/internal/app/collection.go @@ -13,21 +13,50 @@ func (a *App) OpenCollection(hostKey, dbKey, collKey string) (result bson.M) { fmt.Println(err.Error()) return nil } + command := bson.M{"collStats": collKey} err = client.Database(dbKey).RunCommand(ctx, command).Decode(&result) if err != nil { fmt.Println(err.Error()) runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ Type: runtime.ErrorDialog, - Title: "Could not retrieve collection list for " + dbKey, + Title: "Could not retrieve collection stats for " + collKey, Message: err.Error(), }) return nil } + defer close() return result } +func (a *App) RenameCollection(hostKey, dbKey, collKey, newCollKey string) bool { + client, ctx, close, err := a.connectToHost(hostKey) + if err != nil { + fmt.Println(err.Error()) + return false + } + + var result bson.M + command := bson.D{ + bson.E{Key: "renameCollection", Value: fmt.Sprintf("%v.%v", dbKey, collKey)}, + bson.E{Key: "to", Value: fmt.Sprintf("%v.%v", dbKey, newCollKey)}, + } + err = client.Database("admin").RunCommand(ctx, command).Decode(&result) + if err != nil { + fmt.Println(err.Error()) + runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ + Type: runtime.ErrorDialog, + Title: "Could not rename " + collKey, + Message: err.Error(), + }) + return false + } + + defer close() + return true +} + func (a *App) DropCollection(hostKey, dbKey, collKey string) bool { sure, _ := runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ Title: "Confirm", @@ -50,7 +79,7 @@ func (a *App) DropCollection(hostKey, dbKey, collKey string) bool { fmt.Println(err.Error()) runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ Type: runtime.ErrorDialog, - Title: "Could not drop " + dbKey, + Title: "Could not drop " + collKey, Message: err.Error(), }) return false diff --git a/internal/app/connection.go b/internal/app/connection.go index 32ef687..d993fca 100644 --- a/internal/app/connection.go +++ b/internal/app/connection.go @@ -1,12 +1,57 @@ package app import ( + "context" + "errors" "fmt" + "time" "github.com/wailsapp/wails/v2/pkg/runtime" "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + mongoOptions "go.mongodb.org/mongo-driver/mongo/options" ) +func (a *App) connectToHost(hostKey string) (*mongo.Client, context.Context, func(), error) { + hosts, err := a.Hosts() + if err != nil { + runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ + Type: runtime.InfoDialog, + Title: "Could not retrieve hosts", + }) + return nil, nil, nil, errors.New("could not retrieve hosts") + } + + h := hosts[hostKey] + if len(h.URI) == 0 { + runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ + Type: runtime.InfoDialog, + Title: "Invalid uri", + Message: "You haven't specified a valid uri for the selected host.", + }) + return nil, nil, nil, errors.New("invalid uri") + } + + client, err := mongo.NewClient(mongoOptions.Client().ApplyURI(h.URI)) + + if err != nil { + fmt.Println(err.Error()) + runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ + Type: runtime.ErrorDialog, + Title: "Could not connect to " + h.Name, + Message: err.Error(), + }) + return nil, nil, nil, errors.New("could not establish a connection with " + h.Name) + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + client.Connect(ctx) + return client, ctx, func() { + client.Disconnect(ctx) + cancel() + }, nil +} + func (a *App) OpenConnection(hostKey string) (databases []string) { client, ctx, close, err := a.connectToHost(hostKey) if err != nil { diff --git a/internal/app/database.go b/internal/app/database.go index f9707ba..1267956 100644 --- a/internal/app/database.go +++ b/internal/app/database.go @@ -13,6 +13,7 @@ func (a *App) OpenDatabase(hostKey, dbKey string) (collections []string) { fmt.Println(err.Error()) return nil } + collections, err = client.Database(dbKey).ListCollectionNames(ctx, bson.D{}) if err != nil { fmt.Println(err.Error()) @@ -23,6 +24,7 @@ func (a *App) OpenDatabase(hostKey, dbKey string) (collections []string) { }) return nil } + defer close() return collections } @@ -44,6 +46,7 @@ func (a *App) DropDatabase(hostKey, dbKey string) bool { fmt.Println(err.Error()) return false } + err = client.Database(dbKey).Drop(ctx) if err != nil { fmt.Println(err.Error()) @@ -54,6 +57,7 @@ func (a *App) DropDatabase(hostKey, dbKey string) bool { }) return false } + defer close() return true } diff --git a/internal/app/hosts.go b/internal/app/hosts.go index 1c797a4..fc9b9ea 100644 --- a/internal/app/hosts.go +++ b/internal/app/hosts.go @@ -1,18 +1,14 @@ package app import ( - "context" "encoding/json" "errors" "fmt" "io/ioutil" "os" - "time" "github.com/google/uuid" "github.com/wailsapp/wails/v2/pkg/runtime" - "go.mongodb.org/mongo-driver/mongo" - mongoOptions "go.mongodb.org/mongo-driver/mongo/options" ) type Host struct { @@ -183,43 +179,3 @@ func (a *App) RemoveHost(key string) error { } return nil } - -func (a *App) connectToHost(hostKey string) (*mongo.Client, context.Context, func(), error) { - hosts, err := a.Hosts() - if err != nil { - runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ - Type: runtime.InfoDialog, - Title: "Could not retrieve hosts", - }) - return nil, nil, nil, errors.New("could not retrieve hosts") - } - - h := hosts[hostKey] - if len(h.URI) == 0 { - runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ - Type: runtime.InfoDialog, - Title: "Invalid uri", - Message: "You haven't specified a valid uri for the selected host.", - }) - return nil, nil, nil, errors.New("invalid uri") - } - - client, err := mongo.NewClient(mongoOptions.Client().ApplyURI(h.URI)) - - if err != nil { - fmt.Println(err.Error()) - runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ - Type: runtime.ErrorDialog, - Title: "Could not connect to " + h.Name, - Message: err.Error(), - }) - return nil, nil, nil, errors.New("could not establish a connection with " + h.Name) - } - - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - client.Connect(ctx) - return client, ctx, func() { - client.Disconnect(ctx) - cancel() - }, nil -}