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

Multiple improvements to the frontend

* Consistent usage of modal footer
* Remove hosts
* Moved hosts to dedicated store
This commit is contained in:
Romein van Buren 2023-05-31 20:20:39 +02:00
parent 27dc1f9117
commit 415efe9ac4
Signed by: romein
GPG Key ID: 0EFF8478ADDF6C49
17 changed files with 163 additions and 112 deletions

View File

@ -1,6 +1,4 @@
---
title: Installation
---
# Installing Rolens
## System requirements

View File

@ -1,9 +1,10 @@
<script>
import BlankState from '$components/blankstate.svelte';
import ContextMenu from '$components/contextmenu.svelte';
import { connections } from '$lib/stores/connections';
import connections from '$lib/stores/connections';
import contextMenu from '$lib/stores/contextmenu';
import environment from '$lib/stores/environment';
import hosts from '$lib/stores/hosts';
import applicationInited from '$lib/stores/inited';
import About from '$organisms/about.svelte';
import Connection from '$organisms/connection/index.svelte';
@ -11,18 +12,22 @@
import { EventsEmit, EventsOn } from '$wails/runtime';
import { tick } from 'svelte';
const hosts = {};
const activeHostKey = '';
let activeDbKey = '';
let activeCollKey = '';
let settingsModalOpen = false;
let aboutModalOpen = false;
let connectionManager;
let showWelcomeScreen = false;
let showWelcomeScreen = undefined;
$: host = hosts[activeHostKey];
$: connection = $connections[activeHostKey];
$: showWelcomeScreen = !Object.keys(hosts).length;
hosts.subscribe(h => {
if (h && (showWelcomeScreen === undefined)) {
showWelcomeScreen = !Object.keys($hosts || {}).length;
}
});
async function createFirstHost() {
showWelcomeScreen = false;
@ -39,14 +44,14 @@
<div id="root" class="platform-{$environment?.platform}">
<div class="titlebar"></div>
{#if $applicationInited}
{#if $applicationInited && $hosts && (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 {hosts} {activeHostKey} bind:activeCollKey bind:activeDbKey bind:this={connectionManager} />
<Connection {activeHostKey} bind:activeCollKey bind:activeDbKey bind:this={connectionManager} />
{/if}
</main>

View File

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

View File

@ -0,0 +1,11 @@
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

@ -2,13 +2,35 @@ import { derived } from 'svelte/store';
import environment from './environment';
import applicationSettings from './settings';
const applicationInited = derived([ environment, applicationSettings ], ([ env, settings ], set) => {
let alreadyInited = false;
const listeners = [];
const defer = listener => {
if (alreadyInited) {
listener();
}
else {
listeners.push(listener)
}
};
const { subscribe } = derived([ environment, applicationSettings ], ([ env, settings ], set) => {
if (alreadyInited) {
return;
}
if (env && settings) {
set(true);
alreadyInited = true;
// Remove loading spinner.
document.getElementById('app-loading')?.remove();
// Call hooks
listeners.forEach(l => l());
}
}, false);
const applicationInited = { defer, subscribe };
export default applicationInited;

View File

@ -56,12 +56,13 @@
<Icon name="cog" />
</button>
</label>
<button class="btn" type="submit">
<Icon name="play" />
Start export
</button>
</form>
<svelte:fragment slot="footer">
<button class="btn" on:click={performExport}>
<Icon name="play" /> Start export
</button>
</svelte:fragment>
</Modal>
<style>

View File

@ -86,16 +86,16 @@
No rules
{/each}
</div>
<div class="buttons">
<button class="btn" type="button" on:click={addRule} disabled={index.model.some(r => r.sort === 'hashed')}>
<Icon name="+" /> Add rule
</button>
<button class="btn" type="submit" disabled={!index.model.length || index.model.some(r => !r.key)}>
<Icon name="+" /> Create index
</button>
</div>
</form>
<div class="buttons" slot="footer">
<button class="btn" on:click={addRule} disabled={index.model.some(r => r.sort === 'hashed')}>
<Icon name="+" /> Add rule
</button>
<button class="btn" on:click={create} disabled={!index.model.length || index.model.some(r => !r.key)}>
<Icon name="+" /> Create index
</button>
</div>
</Modal>
<style>
@ -128,7 +128,7 @@
display: flex;
gap: 0.5rem;
}
.buttons button[type="submit"] {
.buttons:nth-child(2) {
margin-left: auto;
}
</style>

View File

@ -4,13 +4,13 @@
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 queries from '$lib/stores/queries';
import { createEventDispatcher } from 'svelte';
export let queryToSave = undefined;
export let collection = {};
export let show = false;
export let hosts = {};
const dispatch = createEventDispatcher();
let gridSelectedPath = [];
@ -88,7 +88,7 @@
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: $hosts[query.hostKey]?.name || '?', ns: `${query.dbKey}.${query.collKey}` };
return object;
}, {})}
showHeaders={true}
@ -100,23 +100,25 @@
/>
</div>
{#if queryToSave && Object.keys($queries).includes(queryToSave.name)}
<Hint>
You are about to <strong>overwrite</strong> a saved query. Give it
another name if you do not want to overwrite.
</Hint>
{/if}
</form>
<svelte:fragment slot="footer">
{#if queryToSave}
<button class="btn" type="submit">
<button class="btn" on:click={submit}>
<Icon name="save" /> Save query
</button>
{#if Object.keys($queries).includes(queryToSave.name)}
<Hint>
You are about to <strong>overwrite</strong> a saved query. Give it
another name if you do not want to overwrite.
</Hint>
{/if}
{:else}
<button class="btn" type="submit" disabled={!selectedKey}>
<button class="btn" on:click={submit} disabled={!selectedKey}>
<Icon name="upload" /> Load query
</button>
{/if}
</form>
</svelte:fragment>
</Modal>
<style>

View File

@ -16,7 +16,6 @@
import QueryChooser from './components/querychooser.svelte';
export let collection;
export let hosts = {};
const dispatch = createEventDispatcher();
const defaults = {
@ -252,7 +251,6 @@
bind:queryToSave
bind:show={showQueryChooser}
on:select={queryChosen}
{hosts}
{collection}
/>

View File

@ -16,7 +16,6 @@
export let hostKey;
export let dbKey;
export let collectionKey;
export let hosts = {};
let tab = 'find';
let find;
@ -62,7 +61,7 @@
<div class="container">
{#if tab === 'stats'} <Stats {collection} />
{:else if tab === 'find'} <Find {collection} {hosts} bind:this={find} on:openViewConfig={openViewConfig} />
{: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 === 'update'} <Update {collection} on:performFind={catchQuery} />
{:else if tab === 'remove'} <Remove {collection} />

View File

@ -3,12 +3,12 @@
import Grid from '$components/grid.svelte';
import Modal from '$components/modal.svelte';
import { startProgress } from '$lib/progress';
import { connections } from '$lib/stores/connections';
import connections from '$lib/stores/connections';
import hosts from '$lib/stores/hosts';
import applicationSettings from '$lib/stores/settings';
import { OpenConnection, OpenDatabase, PerformDump } from '$wails/go/app/App';
export let info;
export let hosts = {};
$: if (info) {
info.outdir = info.outdir || $applicationSettings.defaultExportDirectory;
@ -83,7 +83,7 @@
hideChildrenToggles
items={[
{ id: undefined, name: '(localhost)' },
...Object.keys(hosts).map(id => ({ id, name: hosts[id]?.name })),
...Object.keys($hosts).map(id => ({ id, name: $hosts[id]?.name })),
]}
on:select={e => selectHost(e.detail?.itemKey)}
/>
@ -123,11 +123,11 @@
/>
</div>
</div>
<div>
<button type="submit" class="btn">Perform dump</button>
</div>
</form>
<svelte:fragment slot="footer">
<button class="btn" on:click={performDump}>Perform dump</button>
</svelte:fragment>
</Modal>
<style>

View File

@ -1,18 +1,18 @@
<script>
import Modal from '$components/modal.svelte';
import input from '$lib/actions/input';
import hosts from '$lib/stores/hosts';
import { AddHost, UpdateHost } from '$wails/go/app/App';
import { createEventDispatcher } from 'svelte';
export let show = false;
export let hostKey = '';
export let hosts = {};
const dispatch = createEventDispatcher();
let form = {};
let error = '';
$: valid = validate(form);
$: host = hosts[hostKey];
$: host = $hosts[hostKey];
$: if (show || !show) {
init();
@ -36,7 +36,10 @@
await UpdateHost(hostKey, JSON.stringify(form));
}
else {
await AddHost(JSON.stringify(form));
const newHostKey = await AddHost(JSON.stringify(form));
if (newHostKey) {
hostKey = newHostKey;
}
}
show = false;
dispatch('reload');
@ -58,18 +61,18 @@
<span class="label">Connection string</span>
<input type="text" placeholder="mongodb://..." bind:value={form.uri} spellcheck="false" use:input />
</label>
<div class="result">
<div>
{#if error}
<div class="error">{error}</div>
{/if}
</div>
<button class="btn" disabled={!valid} type="submit">
{host ? 'Save' : 'Create'}
</button>
</div>
</form>
<div class="result" slot="footer">
<div>
{#if error}
<div class="error">{error}</div>
{/if}
</div>
<button class="btn" disabled={!valid} on:click={submit}>
{host ? 'Save' : 'Create'}
</button>
</div>
</Modal>
<style>

View File

@ -1,12 +1,13 @@
<script>
import Grid from '$components/grid.svelte';
import { startProgress } from '$lib/progress';
import { connections } from '$lib/stores/connections';
import connections from '$lib/stores/connections';
import { WindowSetTitle } from '$wails/runtime/runtime';
import { createEventDispatcher } from 'svelte';
import { DropCollection, DropDatabase, OpenCollection, OpenConnection, OpenDatabase, TruncateCollection } from '../../../wailsjs/go/app/App';
import { DropCollection, DropDatabase, OpenCollection, OpenConnection, OpenDatabase, RemoveHost, TruncateCollection } from '../../../wailsjs/go/app/App';
import hosts from '$lib/stores/hosts';
import { tick } from 'svelte';
export let hosts = {};
export let activeHostKey = '';
export let activeDbKey = '';
export let activeCollKey = '';
@ -16,7 +17,7 @@
$: activeHostKey = activeGridPath[0] || activeHostKey;
$: activeDbKey = activeGridPath[1];
$: activeCollKey = activeGridPath[2];
$: host = hosts[activeHostKey];
$: host = $hosts[activeHostKey];
$: connection = $connections[activeHostKey];
$: database = connection?.databases[activeDbKey];
$: collection = database?.collections?.[activeCollKey];
@ -38,12 +39,21 @@
});
activeHostKey = hostKey;
dispatch('connected', hostKey);
WindowSetTitle(`${hosts[activeHostKey].name} - Rolens`);
WindowSetTitle(`${$hosts[activeHostKey].name} - Rolens`);
}
progress.end();
}
async function removeHost(hostKey) {
activeCollKey = '';
activeDbKey = '';
activeHostKey = '';
await tick();
await RemoveHost(hostKey);
hosts.update();
}
async function openDatabase(dbKey) {
const progress = startProgress(`Opening database "${dbKey}"…`);
const collections = await OpenDatabase(activeHostKey, dbKey);
@ -88,9 +98,9 @@
<Grid
striped={false}
columns={[ { key: 'name' }, { key: 'count', right: true } ]}
items={Object.keys(hosts).map(hostKey => ({
items={Object.keys($hosts).map(hostKey => ({
id: hostKey,
name: hosts[hostKey].name,
name: $hosts[hostKey].name,
icon: 'server',
children: Object.keys(connection?.databases || {}).sort().map(dbKey => ({
id: dbKey,
@ -122,7 +132,8 @@
menu: [
{ label: 'New database…', fn: () => dispatch('newDatabase') },
{ separator: true },
{ label: `Edit host ${hosts[hostKey].name}`, fn: () => dispatch('editHost', hostKey) },
{ label: `Edit host ${$hosts[hostKey].name}`, fn: () => dispatch('editHost', hostKey) },
{ label: `Remove host…`, fn: () => removeHost(hostKey) },
],
}))}
bind:activePath={activeGridPath}

View File

@ -1,6 +1,6 @@
<script>
import { startProgress } from '$lib/progress';
import { connections } from '$lib/stores/connections';
import connections from '$lib/stores/connections';
import { Hosts, RenameCollection } from '$wails/go/app/App';
import { EnterText } from '$wails/go/ui/UI';
import { EventsOn } from '$wails/runtime/runtime';
@ -10,8 +10,9 @@
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';
export let hosts = {};
export let activeHostKey = '';
export let activeDbKey = '';
export let activeCollKey = '';
@ -25,10 +26,6 @@
$: sharedState.currentDb.set(activeDbKey);
$: sharedState.currentColl.set(activeCollKey);
async function getHosts() {
hosts = await Hosts();
}
export function createHost() {
hostDetailKey = '';
showHostDetail = true;
@ -89,12 +86,16 @@
EventsOn('CreateHost', createHost);
EventsOn('CreateDatabase', createDatabase);
EventsOn('CreateCollection', createCollection);
onMount(getHosts);
</script>
<div class="tree">
<div class="tree-buttons">
<button class="button-small" on:click={createHost}>
<Icon name="+" /> New host
</button>
</div>
<HostTree
{hosts}
bind:activeHostKey
bind:activeCollKey
bind:activeDbKey
@ -114,21 +115,22 @@
hostKey={activeHostKey}
dbKey={activeDbKey}
collectionKey={activeCollKey}
{hosts}
/>
<HostDetail
bind:show={showHostDetail}
on:reload={getHosts}
on:reload={hosts.update}
hostKey={activeHostKey}
{hosts}
/>
<DumpInfo bind:info={exportInfo} {hosts} />
<DumpInfo bind:info={exportInfo} />
<style>
.tree {
padding: 0.5rem;
background-color: #fff;
}
.tree-buttons {
margin-bottom: 1rem;
}
</style>

6
frontend/wailsjs/go/app/App.d.ts generated vendored
View File

@ -7,7 +7,7 @@ import {menu} from '../models';
import {context} from '../models';
import {ui} from '../models';
export function AddHost(arg1:string):Promise<void>;
export function AddHost(arg1:string):Promise<string>;
export function Aggregate(arg1:string,arg2:string,arg3:string,arg4:string,arg5:string):Promise<void>;
@ -43,7 +43,7 @@ export function PerformFindExport(arg1:string,arg2:string,arg3:string,arg4:strin
export function PurgeLogDirectory():Promise<void>;
export function RemoveHost(arg1:string):Promise<void>;
export function RemoveHost(arg1:string):Promise<boolean>;
export function RemoveItemById(arg1:string,arg2:string,arg3:string,arg4:string):Promise<boolean>;
@ -67,7 +67,7 @@ export function Startup(arg1:context.Context,arg2:ui.UI):Promise<void>;
export function TruncateCollection(arg1:string,arg2:string,arg3:string):Promise<boolean>;
export function UpdateHost(arg1:string,arg2:string):Promise<void>;
export function UpdateHost(arg1:string,arg2:string):Promise<boolean>;
export function UpdateItems(arg1:string,arg2:string,arg3:string,arg4:string):Promise<number>;

View File

@ -45,6 +45,7 @@ func (a *App) Menu() *menu.Menu {
fileMenu.AddText("Update", keys.Combo("u", keys.CmdOrCtrlKey, keys.OptionOrAltKey), menuCallbackEmit(a, "OpenCollectionTab", "update"))
fileMenu.AddText("Remove", keys.Combo("r", keys.CmdOrCtrlKey, keys.OptionOrAltKey), menuCallbackEmit(a, "OpenCollectionTab", "remove"))
fileMenu.AddText("Indexes", keys.Combo("x", keys.CmdOrCtrlKey, keys.OptionOrAltKey), menuCallbackEmit(a, "OpenCollectionTab", "indexes"))
fileMenu.AddText("Aggregate", keys.Combo("a", keys.CmdOrCtrlKey, keys.OptionOrAltKey), menuCallbackEmit(a, "OpenCollectionTab", "aggregate"))
if runtime.GOOS == "darwin" {
appMenu.Append(menu.EditMenu())

View File

@ -34,8 +34,7 @@ func (a *App) Hosts() (map[string]Host, error) {
if err != nil {
// It's ok if the file cannot be opened, for example if it is not accessible.
// Therefore no error is returned.
runtime.LogInfo(a.ctx, "Could not open hosts.json")
runtime.LogInfo(a.ctx, err.Error())
runtime.LogInfof(a.ctx, "Could not open hosts.json (%s), trying to create it.", err.Error())
return make(map[string]Host, 0), nil
}
@ -46,84 +45,80 @@ func (a *App) Hosts() (map[string]Host, error) {
err = json.Unmarshal(jsonData, &hosts)
if err != nil {
runtime.LogInfo(a.ctx, "host.json file contains malformatted JSON data")
runtime.LogInfo(a.ctx, err.Error())
runtime.LogInfof(a.ctx, "host.json file contains malformatted JSON data: %s", err.Error())
return nil, errors.New("host.json file contains malformatted JSON data")
}
return hosts, nil
}
}
func (a *App) AddHost(jsonData string) error {
func (a *App) AddHost(jsonData string) string {
hosts, err := a.Hosts()
if err != nil {
zenity.Error(err.Error(), zenity.Title("Error while retrieving hosts"), zenity.ErrorIcon)
return errors.New("could not retrieve existing host list")
return ""
}
var newHost Host
err = json.Unmarshal([]byte(jsonData), &newHost)
if err != nil {
runtime.LogError(a.ctx, "Add host: malformed form")
runtime.LogError(a.ctx, err.Error())
runtime.LogErrorf(a.ctx, "Add host: malformed form: %s", err.Error())
zenity.Error(err.Error(), zenity.Title("Could not parse JSON"), zenity.ErrorIcon)
return errors.New("invalid JSON")
return ""
}
id, err := uuid.NewRandom()
if err != nil {
runtime.LogError(a.ctx, "Add host: failed to generate a UUID")
runtime.LogError(a.ctx, err.Error())
runtime.LogErrorf(a.ctx, "Add host: failed to generate a UUID: %s", err.Error())
zenity.Error(err.Error(), zenity.Title("Error while generating UUID"), zenity.ErrorIcon)
return errors.New("could not generate a UUID")
return ""
}
hosts[id.String()] = newHost
err = updateHostsFile(a, hosts)
if err != nil {
zenity.Error(err.Error(), zenity.Title("Error while updating host list"), zenity.ErrorIcon)
return errors.New("could not update host list")
return ""
}
return nil
return id.String()
}
func (a *App) UpdateHost(hostKey string, jsonData string) error {
func (a *App) UpdateHost(hostKey string, jsonData string) bool {
hosts, err := a.Hosts()
if err != nil {
zenity.Error(err.Error(), zenity.Title("Error while getting hosts"), zenity.ErrorIcon)
return errors.New("could not retrieve existing host list")
return false
}
var host Host
err = json.Unmarshal([]byte(jsonData), &host)
if err != nil {
runtime.LogError(a.ctx, "Could not parse update host JSON")
runtime.LogError(a.ctx, err.Error())
runtime.LogErrorf(a.ctx, "Could not parse update host JSON: %s", err.Error())
zenity.Error(err.Error(), zenity.Title("Could not parse JSON"), zenity.ErrorIcon)
return errors.New("invalid JSON")
return false
}
hosts[hostKey] = host
err = updateHostsFile(a, hosts)
if err != nil {
zenity.Error(err.Error(), zenity.Title("Error while updating hosts"), zenity.ErrorIcon)
return errors.New("could not update host list")
return false
}
return nil
return true
}
func (a *App) RemoveHost(key string) error {
func (a *App) RemoveHost(key string) bool {
hosts, err := a.Hosts()
if err != nil {
zenity.Error(err.Error(), zenity.Title("Error while retrieving hosts"), zenity.ErrorIcon)
return errors.New("could not retrieve existing host list")
return false
}
err = zenity.Question("Are you sure you want to remove "+hosts[key].Name+"?", zenity.Title("Confirm"), zenity.WarningIcon)
if err == zenity.ErrCanceled {
return errors.New("operation aborted")
return false
}
delete(hosts, key)
@ -131,7 +126,8 @@ func (a *App) RemoveHost(key string) error {
if err != nil {
zenity.Error(err.Error(), zenity.Title("Error while updating hosts"), zenity.ErrorIcon)
return errors.New("could not update host list")
return false
}
return nil
return true
}