mirror of
https://github.com/garraflavatra/rolens.git
synced 2025-07-20 06:28:04 +00:00
Initial commit
This commit is contained in:
27
frontend/src/actions.js
Normal file
27
frontend/src/actions.js
Normal file
@ -0,0 +1,27 @@
|
||||
export function input(node, { json } = { json: false }) {
|
||||
const handleInput = () => {
|
||||
if (json) {
|
||||
try {
|
||||
JSON.parse(node.value);
|
||||
node.classList.remove('invalid');
|
||||
}
|
||||
catch {
|
||||
node.classList.add('invalid');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleFocus = () => {
|
||||
node.select();
|
||||
};
|
||||
|
||||
node.addEventListener('focus', handleFocus);
|
||||
node.addEventListener('input', handleInput);
|
||||
|
||||
return {
|
||||
destroy: () => {
|
||||
node.removeEventListener('focus', handleFocus);
|
||||
node.removeEventListener('input', handleInput);
|
||||
},
|
||||
};
|
||||
}
|
120
frontend/src/app.svelte
Normal file
120
frontend/src/app.svelte
Normal file
@ -0,0 +1,120 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { Hosts, OpenCollection, OpenConnection, OpenDatabase } from '../wailsjs/go/main/App';
|
||||
import AddressBar from './organisms/addressbar/index.svelte';
|
||||
import Grid from './components/grid.svelte';
|
||||
import CollectionDetail from './organisms/collection-detail/index.svelte';
|
||||
import { busy } from './stores';
|
||||
|
||||
const connections = {};
|
||||
let hosts = {};
|
||||
let activeHostKey = '';
|
||||
let activeDbKey = '';
|
||||
let activeCollKey = '';
|
||||
let addressBarModalOpen = false;
|
||||
|
||||
$: host = hosts[activeHostKey];
|
||||
$: connection = connections[activeHostKey];
|
||||
$: database = connection?.databases[activeDbKey];
|
||||
$: collection = database?.collections?.[activeCollKey];
|
||||
|
||||
async function openConnection(hostKey) {
|
||||
$busy = true;
|
||||
const databases = await OpenConnection(hostKey);
|
||||
|
||||
if (databases) {
|
||||
connections[hostKey] = { databases: {} };
|
||||
databases.forEach(dbKey => {
|
||||
connections[hostKey].databases[dbKey] = { collections: {} };
|
||||
});
|
||||
activeHostKey = hostKey;
|
||||
addressBarModalOpen = false;
|
||||
window.runtime.WindowSetTitle(`${host.name} - Mongodup`);
|
||||
}
|
||||
|
||||
$busy = false;
|
||||
}
|
||||
|
||||
async function openDatabase(dbKey) {
|
||||
$busy = true;
|
||||
const collections = await OpenDatabase(activeHostKey, dbKey);
|
||||
|
||||
for (const collKey of collections || []) {
|
||||
connections[activeHostKey].databases[dbKey].collections[collKey] = {};
|
||||
}
|
||||
|
||||
$busy = false;
|
||||
}
|
||||
|
||||
async function openCollection(collKey) {
|
||||
$busy = true;
|
||||
const stats = await OpenCollection(activeHostKey, activeDbKey, collKey);
|
||||
connections[activeHostKey].databases[activeDbKey].collections[collKey].stats = stats;
|
||||
$busy = false;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
Hosts().then(h => hosts = h);
|
||||
});
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<AddressBar {hosts} bind:activeHostKey on:select={e => openConnection(e.detail)} bind:modalOpen={addressBarModalOpen} />
|
||||
|
||||
<div class="columns">
|
||||
{#if host && connection}
|
||||
<div class="hostlist">
|
||||
<Grid
|
||||
columns={[ { key: 'id' }, { key: 'collCount', right: true } ]}
|
||||
items={Object.keys(connection.databases).map(id => ({
|
||||
id,
|
||||
collCount: Object.keys(connection.databases[id].collections || {}).length,
|
||||
children: connection.databases[id].collections || [],
|
||||
}))}
|
||||
bind:activeKey={activeDbKey}
|
||||
bind:activeChildKey={activeCollKey}
|
||||
on:select={e => openDatabase(e.detail)}
|
||||
on:selectChild={e => openCollection(e.detail)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="collection">
|
||||
<CollectionDetail
|
||||
{collection}
|
||||
hostKey={activeHostKey}
|
||||
dbKey={activeDbKey}
|
||||
collectionKey={activeCollKey}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
main {
|
||||
height: 100vh;
|
||||
max-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.columns {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex: 1;
|
||||
}
|
||||
.columns > :global(*) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.hostlist {
|
||||
flex: 0 0 250px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
.collection {
|
||||
flex: 1;
|
||||
width: auto;
|
||||
}
|
||||
</style>
|
28
frontend/src/components/code-example.svelte
Normal file
28
frontend/src/components/code-example.svelte
Normal file
@ -0,0 +1,28 @@
|
||||
<script>
|
||||
export let code = '';
|
||||
</script>
|
||||
|
||||
<div class="examplecode">
|
||||
<strong>CLI command</strong>
|
||||
<code>{code}</code>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.examplecode {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 10px;
|
||||
padding: 0.5rem;
|
||||
opacity: 0.6;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
strong {
|
||||
display: inline-block;
|
||||
margin-right: 1rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
code {
|
||||
user-select: all;
|
||||
}
|
||||
</style>
|
167
frontend/src/components/grid.svelte
Normal file
167
frontend/src/components/grid.svelte
Normal file
@ -0,0 +1,167 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import Icon from './icon.svelte';
|
||||
|
||||
export let columns = [];
|
||||
export let items = [];
|
||||
export let key = 'id';
|
||||
export let activeKey = '';
|
||||
export let activeChildKey = '';
|
||||
export let showHeaders = true;
|
||||
export let level = 0;
|
||||
export let contained = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const childrenOpen = {};
|
||||
|
||||
$: _items = objectToArray(items).map(item => {
|
||||
item.children = objectToArray(item.children);
|
||||
return item;
|
||||
});
|
||||
|
||||
function objectToArray(obj) {
|
||||
if (Array.isArray(obj)) {
|
||||
return obj;
|
||||
}
|
||||
else if (typeof obj === 'object') {
|
||||
return Object.entries(obj).map(([ k, item ]) => ({ ...item, [key]: k }));
|
||||
}
|
||||
else {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
function select(itemKey) {
|
||||
activeKey = itemKey;
|
||||
activeChildKey = '';
|
||||
dispatch('select', itemKey);
|
||||
}
|
||||
|
||||
function selectChild(itemKey, childKey) {
|
||||
select(itemKey);
|
||||
activeChildKey = childKey;
|
||||
dispatch('selectChild', childKey);
|
||||
}
|
||||
|
||||
function toggleChildren(itemKey) {
|
||||
childrenOpen[itemKey] = !childrenOpen[itemKey];
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class:grid={level === 0} class:subgrid={level > 0} class:contained>
|
||||
<table>
|
||||
{#if showHeaders && columns.some(col => col.title)}
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="has-toggle"></th>
|
||||
{#each columns as column}
|
||||
<th scope="col">{column.title || ''}</th>
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
{/if}
|
||||
|
||||
<tbody>
|
||||
{#each _items as item (item[key])}
|
||||
<tr on:click={() => select(item[key])} class:selected={activeKey === item[key] && !activeChildKey}>
|
||||
<td class="has-toggle">
|
||||
{#if item.children}
|
||||
<button class="toggle" on:click={() => toggleChildren(item[key])}>
|
||||
<Icon name={childrenOpen[item[key]] ? 'chev-d' : 'chev-r'} />
|
||||
</button>
|
||||
{/if}
|
||||
</td>
|
||||
|
||||
{#each columns as column}
|
||||
{@const value = item[column.key]}
|
||||
<td class:right={column.right}>
|
||||
{#if typeof value !== 'object'}
|
||||
{value || ''}
|
||||
{/if}
|
||||
</td>
|
||||
{/each}
|
||||
</tr>
|
||||
|
||||
{#if item.children && childrenOpen[item[key]]}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td colspan={columns.length + 1} class="subgrid-parent">
|
||||
<svelte:self
|
||||
{columns}
|
||||
{key}
|
||||
bind:activeKey={activeChildKey}
|
||||
showHeaders={false}
|
||||
items={item.children}
|
||||
level={level + 1}
|
||||
on:select={e => selectChild(item[key], e.detail)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.grid {
|
||||
background-color: #fff;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: scroll;
|
||||
}
|
||||
.grid.contained {
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
.subgrid {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table thead {
|
||||
border-bottom: 2px solid #ccc;
|
||||
}
|
||||
|
||||
table th {
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
td, th {
|
||||
padding: 0.3rem;
|
||||
height: 100%;
|
||||
}
|
||||
td.has-toggle {
|
||||
width: calc(20px + 0.3rem);
|
||||
}
|
||||
td.subgrid-parent {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
table tbody tr.selected td {
|
||||
background-color: #00008b;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
button.toggle {
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
}
|
||||
button.toggle :global(svg) {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.right {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
15
frontend/src/components/icon.svelte
Normal file
15
frontend/src/components/icon.svelte
Normal file
@ -0,0 +1,15 @@
|
||||
<script>
|
||||
export let name;
|
||||
</script>
|
||||
|
||||
{#if name === 'radio'}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-radio"><circle cx="12" cy="12" r="2"></circle><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"></path></svg>
|
||||
{:else if name === 'chev-r'}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-right"><polyline points="9 18 15 12 9 6"></polyline></svg>
|
||||
{:else if name === 'chev-d'}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-down"><polyline points="6 9 12 15 18 9"></polyline></svg>
|
||||
{:else if name === 'db'}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-database"><ellipse cx="12" cy="5" rx="9" ry="3"></ellipse><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path></svg>
|
||||
{:else if name === 'x'}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||
{/if}
|
105
frontend/src/components/modal.svelte
Normal file
105
frontend/src/components/modal.svelte
Normal file
@ -0,0 +1,105 @@
|
||||
<script>
|
||||
import { fade, fly } from 'svelte/transition';
|
||||
import Icon from './icon.svelte';
|
||||
|
||||
export let show = false;
|
||||
export let title = undefined;
|
||||
export let contentPadding = true;
|
||||
</script>
|
||||
|
||||
{#if show}
|
||||
<div class="modal outer" on:mousedown|self={close} transition:fade>
|
||||
<div class="inner" transition:fly={{ y: 100 }}>
|
||||
<header>
|
||||
{#if title}
|
||||
<div class="title">{title}</div>
|
||||
{/if}
|
||||
<button class="btn close" on:click={() => show = false} title="close">
|
||||
<Icon name="x" />
|
||||
</button>
|
||||
</header>
|
||||
<div class="slot content" class:p-0={!contentPadding}> <slot /> </div>
|
||||
{#if $$slots.footerLeft || $$slots.footerRight}
|
||||
<footer>
|
||||
<slot name="footer" />
|
||||
</footer>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.outer {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
z-index: 100;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
margin: 0;
|
||||
padding-top: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.inner {
|
||||
max-width: 80vw;
|
||||
max-height: 80vh;
|
||||
background-color: #fff;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-bottom: auto;
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
cursor: auto;
|
||||
}
|
||||
.inner > :global(*:first-child) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.inner > :global(*:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
header {
|
||||
border-bottom: 1px solid #ccc;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
header .title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.close {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 1rem;
|
||||
border-top: 1px solid #ccc;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.outer {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.inner {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
width: 100%;
|
||||
margin-top: auto;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
64
frontend/src/components/objectgrid.svelte
Normal file
64
frontend/src/components/objectgrid.svelte
Normal file
@ -0,0 +1,64 @@
|
||||
<script>
|
||||
import Grid from './grid.svelte';
|
||||
|
||||
export let data = [];
|
||||
export let key = '_id';
|
||||
export let showHeaders = false;
|
||||
export let contained = true;
|
||||
|
||||
console.log(data);
|
||||
|
||||
let items = [];
|
||||
|
||||
$: if (data) {
|
||||
items = [];
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
for (const item of data) {
|
||||
const _item = {};
|
||||
_item.key = item[key];
|
||||
_item.children = dissectObject(item);
|
||||
items = [ ...items, _item ];
|
||||
}
|
||||
}
|
||||
else {
|
||||
items = dissectObject(data);
|
||||
console.log(items);
|
||||
}
|
||||
}
|
||||
|
||||
function getType(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return `array (${value.length} item${value.length === 1 ? '' : 's'})`;
|
||||
}
|
||||
else if (new Date(value).toString() !== 'Invalid Date') {
|
||||
return 'date';
|
||||
}
|
||||
else if (typeof value === 'object') {
|
||||
const keys = Object.keys(value);
|
||||
return `object (${keys.length} item${keys.length === 1 ? '' : 's'})`;
|
||||
}
|
||||
else {
|
||||
return typeof value;
|
||||
}
|
||||
}
|
||||
|
||||
function dissectObject(object) {
|
||||
return Object.entries(object).map(([ key, value ]) => {
|
||||
const type = getType(value);
|
||||
const child = { key, value, type };
|
||||
|
||||
if (type.startsWith('object')) {
|
||||
child.children = dissectObject(value);
|
||||
}
|
||||
|
||||
return child;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<Grid columns={[
|
||||
{ key: 'key', label: 'Key' },
|
||||
{ key: 'value', label: 'Value' },
|
||||
{ key: 'type', label: 'Type' },
|
||||
]} {items} {showHeaders} {contained} key="key" />
|
54
frontend/src/components/tabbar.svelte
Normal file
54
frontend/src/components/tabbar.svelte
Normal file
@ -0,0 +1,54 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let tabs = [];
|
||||
export let selectedKey = {};
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function select(tabKey) {
|
||||
selectedKey = tabKey;
|
||||
dispatch('select', tabKey);
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav class="tabs">
|
||||
<ul>
|
||||
{#each tabs as tab (tab.key)}
|
||||
<li class="tab" class:active={tab.key === selectedKey}>
|
||||
<button on:click={() => select(tab.key)}>{tab.title}</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
.tabs {
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
.tabs ul {
|
||||
overflow-x: scroll;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
list-style: none;
|
||||
}
|
||||
.tabs li {
|
||||
display: inline-block;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.tabs li button {
|
||||
width: 100%;
|
||||
padding: 0.7rem 1rem;
|
||||
border: 1px solid #ccc;
|
||||
border-bottom: none;
|
||||
border-radius: 5px 5px 0 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tabs li.active button {
|
||||
color: #fff;
|
||||
background-color: #00008b;
|
||||
border-color: #00008b;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
7
frontend/src/main.js
Normal file
7
frontend/src/main.js
Normal file
@ -0,0 +1,7 @@
|
||||
import './reset.css';
|
||||
import './style.css';
|
||||
import App from './app.svelte';
|
||||
|
||||
const app = new App({ target: document.getElementById('app') });
|
||||
|
||||
export default app;
|
78
frontend/src/organisms/addressbar/index.svelte
Normal file
78
frontend/src/organisms/addressbar/index.svelte
Normal file
@ -0,0 +1,78 @@
|
||||
<script>
|
||||
import Modal from '../../components/modal.svelte';
|
||||
import Icon from '../../components/icon.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let hosts = {};
|
||||
export let activeHostKey = '';
|
||||
export let modalOpen = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
$: host = hosts?.[activeHostKey];
|
||||
|
||||
function select(hostKey) {
|
||||
activeHostKey = hostKey;
|
||||
dispatch('select', hostKey);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="addressbar">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="address" class:empty={!host?.uri} on:click={() => modalOpen = true}>
|
||||
{host?.uri || 'No host selected'}
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="btn" on:click={() => modalOpen = true}>
|
||||
<Icon name="db" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal bind:show={modalOpen} title="Hosts">
|
||||
{#if Object.keys(hosts).length}
|
||||
<ul class="hosts">
|
||||
{#each Object.entries(hosts) as [hostKey, host]}
|
||||
<li>
|
||||
<button on:click={() => select(hostKey)}>
|
||||
{host.name}
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.addressbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 1rem;
|
||||
padding: 0.5rem 0.5rem 0.5rem 1rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.address.empty {
|
||||
font-style: italic;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.hosts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.hosts li button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 1rem;
|
||||
background-color: #ddd;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.hosts li button:hover {
|
||||
background-color: #ccc;
|
||||
}
|
||||
</style>
|
93
frontend/src/organisms/collection-detail/find.svelte
Normal file
93
frontend/src/organisms/collection-detail/find.svelte
Normal file
@ -0,0 +1,93 @@
|
||||
<script>
|
||||
import { PerformFind } from '../../../wailsjs/go/main/App';
|
||||
import CodeExample from '../../components/code-example.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { input } from '../../actions';
|
||||
import ObjectGrid from '../../components/objectgrid.svelte';
|
||||
|
||||
export let collection;
|
||||
|
||||
const defaults = {
|
||||
query: '{}',
|
||||
sort: '{ "_id": 1 }',
|
||||
fields: '{}',
|
||||
skip: 0,
|
||||
limit: 30,
|
||||
};
|
||||
|
||||
const form = {
|
||||
query: '{}',
|
||||
sort: '{ "_id": 1 }',
|
||||
fields: '{}',
|
||||
skip: 0,
|
||||
limit: 30,
|
||||
};
|
||||
|
||||
let result = [];
|
||||
let queryField;
|
||||
$: 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})` : ''};`;
|
||||
|
||||
$: if (collection) {
|
||||
result = [];
|
||||
}
|
||||
|
||||
async function submitQuery() {
|
||||
result = await PerformFind(collection.hostKey, collection.dbKey, collection.key, JSON.stringify(form));
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
queryField?.focus();
|
||||
queryField?.select();
|
||||
});
|
||||
</script>
|
||||
|
||||
<form on:submit|preventDefault={submitQuery}>
|
||||
<div class="row-one">
|
||||
<label class="field">
|
||||
<span class="label">Query or id</span>
|
||||
<input type="text" class="code" bind:this={queryField} bind:value={form.query} use:input={{ json: true }} placeholder={defaults.query} />
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span class="label">Sort</span>
|
||||
<input type="text" class="code" bind:value={form.sort} use:input={{ json: true }} placeholder={defaults.sort} />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="row-two">
|
||||
<label class="field">
|
||||
<span class="label">Fields</span>
|
||||
<input type="text" class="code" bind:value={form.fields} use:input={{ json: true }} placeholder={defaults.fields} />
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span class="label">Skip</span>
|
||||
<input type="number" min="0" bind:value={form.skip} use:input placeholder={defaults.skip} />
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span class="label">Limit</span>
|
||||
<input type="number" min="0" bind:value={form.limit} use:input placeholder={defaults.limit} />
|
||||
</label>
|
||||
|
||||
<button type="submit" class="btn">Run</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<CodeExample {code} />
|
||||
<ObjectGrid data={result} />
|
||||
|
||||
<style>
|
||||
.row-one {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
grid-template-columns: 3fr 2fr;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.row-two {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
grid-template-columns: 5fr 1fr 1fr 1fr;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
</style>
|
68
frontend/src/organisms/collection-detail/index.svelte
Normal file
68
frontend/src/organisms/collection-detail/index.svelte
Normal file
@ -0,0 +1,68 @@
|
||||
<script>
|
||||
import ObjectGrid from '../../components/objectgrid.svelte';
|
||||
import CodeExample from '../../components/code-example.svelte';
|
||||
import TabBar from '../../components/tabbar.svelte';
|
||||
import Find from './find.svelte';
|
||||
import Indexes from './indexes.svelte';
|
||||
import Insert from './insert.svelte';
|
||||
import Remove from './remove.svelte';
|
||||
|
||||
export let collection;
|
||||
export let hostKey;
|
||||
export let dbKey;
|
||||
export let collectionKey;
|
||||
|
||||
let tab = 'find';
|
||||
|
||||
$: if (collection) {
|
||||
collection.hostKey = hostKey;
|
||||
collection.dbKey = dbKey;
|
||||
collection.key = collectionKey;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="collection" class:empty={!collection}>
|
||||
{#if collection}
|
||||
<TabBar tabs={[
|
||||
{ key: 'stats', title: 'Stats' },
|
||||
{ key: 'find', title: 'Find' },
|
||||
{ key: 'insert', title: 'Insert' },
|
||||
{ key: 'update', title: 'Update' },
|
||||
{ key: 'remove', title: 'Remove' },
|
||||
{ key: 'indexes', title: 'Indexes' },
|
||||
]} bind:selectedKey={tab} />
|
||||
|
||||
<div class="container">
|
||||
{#if tab === 'stats'}
|
||||
<CodeExample code="db.stats()" />
|
||||
<ObjectGrid data={collection.stats} />
|
||||
{:else if tab === 'find'}
|
||||
<Find {collection} />
|
||||
{:else if tab === 'insert'}
|
||||
<Insert {collection} />
|
||||
{:else if tab === 'remove'}
|
||||
<Remove {collection} />
|
||||
{:else if tab === 'indexes'}
|
||||
<Indexes {collection} />
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
No collection selected
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.collection {
|
||||
margin: 1rem 1rem 1rem 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0.5rem 0.5rem 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
13
frontend/src/organisms/collection-detail/indexes.svelte
Normal file
13
frontend/src/organisms/collection-detail/indexes.svelte
Normal file
@ -0,0 +1,13 @@
|
||||
<script>
|
||||
export let collection;
|
||||
|
||||
const indexes = [];
|
||||
|
||||
function getIndexes() {}
|
||||
</script>
|
||||
|
||||
<div class="buttons">
|
||||
<button class="btn">Get indexes</button>
|
||||
<button class="btn" disabled={!indexes?.length}>Drop selected</button>
|
||||
<button class="btn">Create…</button>
|
||||
</div>
|
39
frontend/src/organisms/collection-detail/insert.svelte
Normal file
39
frontend/src/organisms/collection-detail/insert.svelte
Normal file
@ -0,0 +1,39 @@
|
||||
<script>
|
||||
import { PerformInsert } from '../../../wailsjs/go/main/App';
|
||||
|
||||
export let collection;
|
||||
|
||||
let input = '';
|
||||
let insertedIds;
|
||||
|
||||
$: if (collection) {
|
||||
insertedIds = undefined;
|
||||
}
|
||||
|
||||
async function insert() {
|
||||
insertedIds = await PerformInsert(collection.hostKey, collection.dbKey, collection.key, input);
|
||||
}
|
||||
</script>
|
||||
|
||||
<form on:submit|preventDefault={insert}>
|
||||
<label class="field">
|
||||
<textarea cols="30" rows="10" bind:value={input} placeholder="[]" class="code"></textarea>
|
||||
</label>
|
||||
|
||||
<div class="flex">
|
||||
<div>
|
||||
{#if insertedIds}
|
||||
Success! {insertedIds.length} document{insertedIds.length > 1 ? 's' : ''} inserted
|
||||
{/if}
|
||||
</div>
|
||||
<button type="submit" class="btn">Insert</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<style>
|
||||
.flex {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
</style>
|
31
frontend/src/organisms/collection-detail/remove.svelte
Normal file
31
frontend/src/organisms/collection-detail/remove.svelte
Normal file
@ -0,0 +1,31 @@
|
||||
<script>
|
||||
import CodeExample from '../../components/code-example.svelte';
|
||||
|
||||
export let collection;
|
||||
|
||||
let remove = '';
|
||||
$: code = `db.${collection.key}.remove(${remove});`;
|
||||
|
||||
function insert() {}
|
||||
</script>
|
||||
|
||||
<form on:submit|preventDefault={insert}>
|
||||
<CodeExample {code} />
|
||||
|
||||
<label class="field">
|
||||
<textarea cols="30" rows="10" bind:value={remove} placeholder={'{}'} class="code"></textarea>
|
||||
</label>
|
||||
|
||||
<div class="flex">
|
||||
<div></div>
|
||||
<button type="submit" class="btn">Remove</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<style>
|
||||
.flex {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
</style>
|
66
frontend/src/reset.css
Normal file
66
frontend/src/reset.css
Normal file
@ -0,0 +1,66 @@
|
||||
/* http://meyerweb.com/eric/tools/css/reset/
|
||||
v2.0 | 20110126
|
||||
License: none (public domain)
|
||||
*/
|
||||
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite, code,
|
||||
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||
small, strike, strong, sub, sup, tt, var,
|
||||
b, u, i, center,
|
||||
dl, dt, dd, ol, ul, li,
|
||||
fieldset, form, label, legend,
|
||||
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||
article, aside, canvas, details, embed,
|
||||
figure, figcaption, footer, header, hgroup,
|
||||
menu, nav, output, ruby, section, summary,
|
||||
time, mark, audio, video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block;
|
||||
}
|
||||
body {
|
||||
line-height: 1;
|
||||
}
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
}
|
||||
blockquote, q {
|
||||
quotes: none;
|
||||
}
|
||||
blockquote:before, blockquote:after,
|
||||
q:before, q:after {
|
||||
content: '';
|
||||
content: none;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
button {
|
||||
font: inherit;
|
||||
border: none;
|
||||
background: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Inputs */
|
||||
input {
|
||||
font: inherit;
|
||||
min-width: 100px;
|
||||
width: 100%;
|
||||
}
|
11
frontend/src/stores.js
Normal file
11
frontend/src/stores.js
Normal file
@ -0,0 +1,11 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const busy = writable(false);
|
||||
busy.subscribe(isBusy => {
|
||||
if (isBusy) {
|
||||
document.body.classList.add('busy');
|
||||
}
|
||||
else {
|
||||
document.body.classList.remove('busy');
|
||||
}
|
||||
});
|
93
frontend/src/style.css
Normal file
93
frontend/src/style.css
Normal file
@ -0,0 +1,93 @@
|
||||
html,
|
||||
body {
|
||||
height: 100vh;
|
||||
max-height: 100vh;
|
||||
overflow: hidden;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
font-size: 15px;
|
||||
line-height: 15px;
|
||||
}
|
||||
|
||||
* {
|
||||
vertical-align: middle;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 0.7rem 0;
|
||||
}
|
||||
|
||||
.loading {
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
align-items: stretch;
|
||||
}
|
||||
.field > * {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ccc;
|
||||
border-right: none;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
}
|
||||
.field .label {
|
||||
background-color: #eee;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.field > :first-child {
|
||||
border-top-left-radius: 10px;
|
||||
border-bottom-left-radius: 10px;
|
||||
}
|
||||
.field > :last-child {
|
||||
border-right: 1px solid #ccc;
|
||||
border-top-right-radius: 10px;
|
||||
border-bottom-right-radius: 10px;
|
||||
}
|
||||
.field > input,
|
||||
.field > textarea {
|
||||
flex: 1;
|
||||
}
|
||||
.field > textarea:focus,
|
||||
.field > input:focus {
|
||||
outline: none;
|
||||
border-color: #00008b;
|
||||
box-shadow: 0 0 0 3px rgba(0, 0, 139, 0.2);
|
||||
}
|
||||
.field > input.invalid,
|
||||
.field > textarea.invalid {
|
||||
background-color: rgba(255, 80, 80, 0.3);
|
||||
border-color: rgb(255, 80, 80);
|
||||
}
|
||||
|
||||
.btn {
|
||||
background-color: #00008b;
|
||||
padding: 0.5rem;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
}
|
||||
.btn:focus {
|
||||
box-shadow: 0 0 0 3px rgba(0, 0, 139, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.btn svg {
|
||||
height: 15px;
|
||||
width: auto;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
code, .code {
|
||||
font-family: Menlo, monospace;
|
||||
}
|
2
frontend/src/vite-env.d.ts
vendored
Normal file
2
frontend/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/// <reference types="svelte" />
|
||||
/// <reference types="vite/client" />
|
Reference in New Issue
Block a user