mirror of
https://github.com/garraflavatra/rolens.git
synced 2025-07-20 06:28: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:
@ -0,0 +1,65 @@
|
||||
<script>
|
||||
import Icon from '$components/icon.svelte';
|
||||
import Modal from '$components/modal.svelte';
|
||||
import views from '$lib/stores/views';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let collection;
|
||||
export let query = undefined;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const exportInfo = { ...query, viewKey: collection.viewKey };
|
||||
|
||||
function submit() {
|
||||
exportInfo.view = $views[exportInfo.viewKey];
|
||||
dispatch('export', { exportInfo });
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal title="Export results" width="450px" on:close>
|
||||
<form on:submit|preventDefault={submit}>
|
||||
<label class="field">
|
||||
<span class="label">Export</span>
|
||||
<select bind:value={exportInfo.contents}>
|
||||
<option value="all">all records</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={exportInfo.format}>
|
||||
<option value="jsonarray">JSON array</option>
|
||||
<option value="ndjson">Newline delimited JSON</option>
|
||||
<option value="csv">CSV</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span class="label">View to use</span>
|
||||
<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}
|
||||
</select>
|
||||
<button class="btn" type="button" on:click={() => dispatch('openViewConfig')} title="Edit view">
|
||||
<Icon name="cog" />
|
||||
</button>
|
||||
</label>
|
||||
</form>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<button class="btn" on:click={submit}>
|
||||
<Icon name="play" /> Start export
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,127 @@
|
||||
<script>
|
||||
import Icon from '$components/icon.svelte';
|
||||
import Modal from '$components/modal.svelte';
|
||||
import input from '$lib/actions/input';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let collection;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const index = { model: [] };
|
||||
|
||||
function addRule() {
|
||||
index.model = [ ...index.model, {} ];
|
||||
}
|
||||
|
||||
function removeRule(ruleIndex) {
|
||||
index.model.splice(ruleIndex, 1);
|
||||
index.model = index.model;
|
||||
}
|
||||
|
||||
async function create() {
|
||||
dispatch('create', { index });
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal title="Create new index on {collection.key}" on:close>
|
||||
<form on:submit|preventDefault={create}>
|
||||
<label class="field name">
|
||||
<span class="label">Name</span>
|
||||
<input type="text" placeholder="Optional" bind:value={index.name} use:input={{ autofocus: true }} />
|
||||
</label>
|
||||
|
||||
<div class="toggles">
|
||||
<label class="field">
|
||||
<span class="label">Background (legacy)</span>
|
||||
<span class="checkbox">
|
||||
<input type="checkbox" bind:checked={index.background} />
|
||||
</span>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="label">Unique</span>
|
||||
<span class="checkbox">
|
||||
<input type="checkbox" bind:checked={index.unique} />
|
||||
</span>
|
||||
</label>
|
||||
<!-- <label class="field">
|
||||
<span class="label">Drop duplicates</span>
|
||||
<span class="checkbox">
|
||||
<input type="checkbox" bind:checked={index.dropDuplicates} />
|
||||
</span>
|
||||
</label> -->
|
||||
<label class="field">
|
||||
<span class="label">Sparse</span>
|
||||
<span class="checkbox">
|
||||
<input type="checkbox" bind:checked={index.sparse} />
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="model">
|
||||
{#each index.model as rule, ruleIndex}
|
||||
<div class="row">
|
||||
<label class="field">
|
||||
<span class="label">Key</span>
|
||||
<input type="text" placeholder="_id" bind:value={rule.key}>
|
||||
</label>
|
||||
<label class="field">
|
||||
<select bind:value={rule.sort}>
|
||||
<option value={1}>Ascending</option>
|
||||
<option value={-1}>Decending</option>
|
||||
<option value="hashed" disabled={index.model.length > 1}>Hashed</option>
|
||||
</select>
|
||||
</label>
|
||||
<button type="button" class="btn danger" on:click={() => removeRule(ruleIndex)} disabled={index.model.length < 2}>
|
||||
<Icon name="-" />
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
No rules
|
||||
{/each}
|
||||
</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>
|
||||
.field.name {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.toggles {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.model {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
.model .row {
|
||||
display: grid;
|
||||
grid-template: 1fr / 1fr auto auto;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.buttons:nth-child(2) {
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,137 @@
|
||||
<script>
|
||||
import Grid from '$components/grid.svelte';
|
||||
import Hint from '$components/hint.svelte';
|
||||
import Icon from '$components/icon.svelte';
|
||||
import Modal from '$components/modal.svelte';
|
||||
import input from '$lib/actions/input';
|
||||
import hostTree from '$lib/stores/hosttree';
|
||||
import queries from '$lib/stores/queries';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let queryToSave = undefined;
|
||||
export let collection = {};
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let gridSelectedPath = [];
|
||||
let selectedKey = '';
|
||||
|
||||
function submit() {
|
||||
if (queryToSave) {
|
||||
queryToSave.hostKey = collection.hostKey;
|
||||
queryToSave.dbKey = collection.dbKey;
|
||||
queryToSave.collKey = collection.key;
|
||||
|
||||
dispatch('create', { query: queryToSave });
|
||||
selectedKey = queryToSave.name;
|
||||
}
|
||||
else {
|
||||
selectActive();
|
||||
}
|
||||
}
|
||||
|
||||
function selectActive() {
|
||||
dispatch('select', { query: $queries[selectedKey] });
|
||||
}
|
||||
|
||||
function gridSelect(event) {
|
||||
if (event?.detail?.level === 0) {
|
||||
selectedKey = event.detail.itemKey;
|
||||
|
||||
if (queryToSave) {
|
||||
queryToSave.name = event.detail.itemKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function gridTrigger(event) {
|
||||
gridSelect(event);
|
||||
selectActive();
|
||||
}
|
||||
|
||||
async function gridRemove(event) {
|
||||
await queries.remove(event.detail);
|
||||
}
|
||||
|
||||
$: if (queryToSave && !queryToSave.name) {
|
||||
queryToSave.name = 'New query';
|
||||
}
|
||||
$: if (queryToSave?.name) {
|
||||
gridSelectedPath = [ queryToSave.name ];
|
||||
}
|
||||
$: if (selectedKey) {
|
||||
gridSelectedPath = [ selectedKey ];
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal title={queryToSave ? 'Save query' : 'Load query'} width="500px" on:close>
|
||||
<form on:submit|preventDefault={submit}>
|
||||
{#if queryToSave}
|
||||
<label class="field queryname">
|
||||
<span class="label">Query name</span>
|
||||
<input type="text" bind:value={queryToSave.name} use:input={{ autofocus: true }} />
|
||||
</label>
|
||||
<label class="field">
|
||||
<textarea bind:value={queryToSave.remarks} placeholder="Remarks…" use:input></textarea>
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
<div class="querylist">
|
||||
<Grid
|
||||
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: $hostTree[query.hostKey]?.name || '?',
|
||||
ns: `${query.dbKey}.${query.collKey}`,
|
||||
};
|
||||
return object;
|
||||
}, {})}
|
||||
showHeaders={true}
|
||||
canRemoveItems={true}
|
||||
bind:activePath={gridSelectedPath}
|
||||
on:select={gridSelect}
|
||||
on:trigger={gridTrigger}
|
||||
on:removeItem={gridRemove}
|
||||
/>
|
||||
</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" on:click={submit}>
|
||||
<Icon name="save" /> Save query
|
||||
</button>
|
||||
{:else}
|
||||
<button class="btn" on:click={submit} disabled={!selectedKey}>
|
||||
<Icon name="upload" /> Load query
|
||||
</button>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.field, .querylist {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 75px;
|
||||
}
|
||||
|
||||
.querylist {
|
||||
border: 1px solid #ccc;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.btn + :global(.hint) {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,249 @@
|
||||
<script>
|
||||
import Icon from '$components/icon.svelte';
|
||||
import Modal from '$components/modal.svelte';
|
||||
import TabBar from '$components/tabbar.svelte';
|
||||
import input from '$lib/actions/input';
|
||||
import { randomString } from '$lib/math';
|
||||
import views from '$lib/stores/views';
|
||||
|
||||
export let collection;
|
||||
export let firstItem = {};
|
||||
|
||||
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') {
|
||||
return -1;
|
||||
}
|
||||
if (b === 'list') {
|
||||
return 1;
|
||||
}
|
||||
else {
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
}
|
||||
|
||||
function createView() {
|
||||
const newViewKey = randomString();
|
||||
$views[newViewKey] = {
|
||||
name: 'Table view',
|
||||
host: collection.hostKey,
|
||||
database: collection.dbKey,
|
||||
collection: collection.key,
|
||||
type: 'table',
|
||||
columns: [ { key: '_id', showInTable: true, inputType: 'objectid', mandatory: true } ],
|
||||
};
|
||||
collection.viewKey = newViewKey;
|
||||
}
|
||||
|
||||
function removeView(viewKey) {
|
||||
const keys = Object.keys($views).sort(sortTabKeys);
|
||||
const oldIndex = keys.indexOf(viewKey);
|
||||
const newKey = keys[oldIndex - 1];
|
||||
collection.viewKey = newKey;
|
||||
delete $views[viewKey];
|
||||
$views = $views;
|
||||
}
|
||||
|
||||
function addColumn(before) {
|
||||
if (typeof before === 'number') {
|
||||
$views[collection.viewKey].columns = [
|
||||
...$views[collection.viewKey].columns.slice(0, before),
|
||||
{ showInTable: true, inputType: 'none' },
|
||||
...$views[collection.viewKey].columns.slice(before),
|
||||
];
|
||||
}
|
||||
else {
|
||||
$views[collection.viewKey].columns = [
|
||||
...$views[collection.viewKey].columns,
|
||||
{ showInTable: true, inputType: 'none' },
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
function addSuggestedColumns() {
|
||||
if ((typeof firstItem !== 'object') || (firstItem === null)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$views[collection.viewKey].columns = Object.keys(firstItem).sort().map(key => {
|
||||
return {
|
||||
key,
|
||||
showInTable: true,
|
||||
inputType: 'none',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function moveColumn(oldIndex, delta) {
|
||||
const column = $views[collection.viewKey].columns[oldIndex];
|
||||
const newIndex = oldIndex + delta;
|
||||
|
||||
$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[collection.viewKey].columns.splice(index, 1);
|
||||
$views[collection.viewKey].columns = $views[collection.viewKey].columns;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal title="View configuration" contentPadding={false} on:close>
|
||||
<TabBar
|
||||
{tabs}
|
||||
canAddTab={true}
|
||||
on:addTab={createView}
|
||||
on:closeTab={e => removeView(e.detail)}
|
||||
bind:selectedKey={collection.viewKey}
|
||||
/>
|
||||
|
||||
<div class="options">
|
||||
{#if $views[collection.viewKey]}
|
||||
<div class="meta">
|
||||
{#key collection.viewKey}
|
||||
<label class="field">
|
||||
<span class="label">View name</span>
|
||||
<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[collection.viewKey].type} disabled>
|
||||
<option value="list">List view</option>
|
||||
<option value="table">Table view</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{#if $views[collection.viewKey].type === 'list'}
|
||||
<div class="flex">
|
||||
<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[collection.viewKey].type === 'table'}
|
||||
<div class="columns">
|
||||
{#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" />
|
||||
</label>
|
||||
|
||||
<label class="field" title="Show column in table view">
|
||||
<span class="label">
|
||||
<Icon name="table" />
|
||||
</span>
|
||||
<span class="checkbox">
|
||||
<input type="checkbox" bind:checked={column.showInTable} />
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="field" title="Input type in form view">
|
||||
<span class="label">
|
||||
<Icon name="form" />
|
||||
</span>
|
||||
<select bind:value={column.inputType}>
|
||||
<option value="none">Hidden in form</option>
|
||||
<optgroup label="Strings">
|
||||
<option value="string">String</option>
|
||||
<option value="objectid">ObjectId</option>
|
||||
</optgroup>
|
||||
<optgroup label="Integers">
|
||||
<option value="int">Integer (32-bit, signed)</option>
|
||||
<option value="uint64">Integer (64-bit, unsigned)</option>
|
||||
<option value="long">Long (64-bit integer, signed)</option>
|
||||
</optgroup>
|
||||
<optgroup label="Floats">
|
||||
<option value="double">Double (64-bit)</option>
|
||||
<option value="decimal">Decimal (128-bit)</option>
|
||||
</optgroup>
|
||||
<optgroup label="Miscellaneous">
|
||||
<option value="bool">Boolean</option>
|
||||
<option value="date">Date</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="field" title="Mandatory (field must be valid in order to submit form)">
|
||||
<span class="label">
|
||||
<Icon name="target" />
|
||||
</span>
|
||||
<span class="checkbox">
|
||||
<input type="checkbox" bind:checked={column.mandatory} disabled={column.inputType === 'none'} />
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<button class="btn" type="button" on:click={() => addColumn(columnIndex)} title="Add column before this one">
|
||||
<Icon name="+" />
|
||||
</button>
|
||||
<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[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">
|
||||
<Icon name="x" />
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<p>No columns yet</p>
|
||||
{/each}
|
||||
</div>
|
||||
<button class="btn" on:click={addColumn}>
|
||||
<Icon name="+" /> Add column
|
||||
</button>
|
||||
<button class="btn" on:click={addSuggestedColumns} disabled={!Object.keys(firstItem || {}).length}>
|
||||
<Icon name="zap" /> Add suggested columns
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.options {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: grid;
|
||||
grid-template: 1fr / 1fr 1fr;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.columns {
|
||||
border: 1px solid #ccc;
|
||||
overflow: auto;
|
||||
padding: 0.5rem 0.5rem 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.columns p {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.columns .column {
|
||||
display: grid;
|
||||
grid-template: 1fr / 1fr repeat(7, auto);
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user