Files
dbgate/packages/web/src/widgets/DbKeysTree.svelte
2025-08-26 10:48:19 +02:00

218 lines
6.4 KiB
Svelte

<script lang="ts">
import {
dbKeys_clearLoadedData,
dbKeys_createNewModel,
dbKeys_getFlatList,
dbKeys_markNodeExpanded,
dbKeys_mergeNextPage,
findEngineDriver,
} from 'dbgate-tools';
import CloseSearchButton from '../buttons/CloseSearchButton.svelte';
import InlineButton from '../buttons/InlineButton.svelte';
import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte';
import SearchInput from '../elements/SearchInput.svelte';
import FontIcon from '../icons/FontIcon.svelte';
import AddDbKeyModal from '../modals/AddDbKeyModal.svelte';
import { showModal } from '../modals/modalTools';
import {
activeDbKeysStore,
currentDatabase,
focusedConnectionOrDatabase,
focusedTreeDbKey,
getExtensions,
getFocusedTreeDbKey,
} from '../stores';
import { apiCall } from '../utility/api';
import { useConnectionInfo } from '../utility/metadataLoaders';
import DbKeysSubTree from './DbKeysSubTree.svelte';
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
import FocusedConnectionInfoWidget from './FocusedConnectionInfoWidget.svelte';
import AppObjectListHandler from './AppObjectListHandler.svelte';
import { getOpenDetailOnArrowsSettings } from '../settings/settingsTools';
import openNewTab from '../utility/openNewTab';
import ConfirmModal from '../modals/ConfirmModal.svelte';
export let conid;
export let database;
export let treeKeySeparator = ':';
let domListHandler;
let domContainer = null;
let domFilter = null;
let filter;
let isLoading = false;
let model = dbKeys_createNewModel(treeKeySeparator);
function handleAddKey() {
const connection = $currentDatabase?.connection;
const database = $currentDatabase?.name;
const driver = findEngineDriver(connection, getExtensions());
showModal(AddDbKeyModal, {
conid: connection._id,
database,
driver,
onConfirm: async item => {
const type = driver.supportedKeyTypes.find(x => x.name == item.type);
await apiCall('database-connections/call-method', {
conid: connection._id,
database,
method: type.addMethod,
args: [item.keyName, ...type.dbKeyFields.map(fld => item[fld.name])],
});
reloadModel();
},
});
}
$: differentFocusedDb =
$focusedConnectionOrDatabase &&
($focusedConnectionOrDatabase.conid != conid ||
($focusedConnectionOrDatabase?.database && $focusedConnectionOrDatabase?.database != database));
$: connection = useConnectionInfo({ conid });
function changeModel(modelUpdate, loadNext) {
if (modelUpdate) model = modelUpdate(model);
if (loadNext) loadNextPage();
}
async function loadNextPage(skipCount = false) {
isLoading = true;
const nextScan = await apiCall('database-connections/scan-keys', {
conid,
database,
pattern: filter,
cursor: model.cursor,
count: skipCount ? undefined : model.loadCount,
});
model = dbKeys_mergeNextPage(model, nextScan);
isLoading = false;
}
async function loadAll() {
showModal(ConfirmModal, {
message:
'This will scan all keys in the database, which could affect server performance. Do you want to continue?',
onConfirm: () => {
loadNextPage(true);
},
});
}
function reloadModel() {
changeModel(model => dbKeys_clearLoadedData(model), true);
}
$: {
conid;
database;
filter;
reloadModel();
}
// $: console.log('DbKeysTree MODEL', model);
</script>
<SearchBoxWrapper noMargin>
<SearchInput
placeholder="Redis pattern or key part"
bind:value={filter}
isDebounced
bind:this={domFilter}
onFocusFilteredList={() => {
domListHandler?.focusFirst();
}}
/>
<CloseSearchButton bind:filter />
<InlineButton on:click={handleAddKey} title="Add new key">
<FontIcon icon="icon plus-thick" />
</InlineButton>
<InlineButton on:click={reloadModel} title="Refresh key list">
<FontIcon icon="icon refresh" />
</InlineButton>
</SearchBoxWrapper>
{#if !model?.loadedAll}
<div class="space-between align-items-center ml-1">
{#if model}
<div>
{#if isLoading}
Loading...
{:else}
Scanned {Math.min(model?.scannedKeys, model?.dbsize) ?? '???'}/{model?.dbsize ?? '???'}
{/if}
</div>
{/if}
{#if isLoading}
<div style="margin: 3px; margin-bottom: 2px">
<FontIcon icon="icon loading" />
</div>
{:else}
<div class="flex">
<InlineButton on:click={() => loadNextPage()} title="Scan more keys">
<FontIcon icon="icon more" /> Scan more
</InlineButton>
<InlineButton on:click={() => loadAll()} title="Scan all keys">
<FontIcon icon="icon warn" /> Scan all
</InlineButton>
</div>
{/if}
</div>
{/if}
{#if differentFocusedDb}
<FocusedConnectionInfoWidget {conid} {database} connection={$connection} />
{/if}
<WidgetsInnerContainer hideContent={differentFocusedDb} bind:this={domContainer}>
<AppObjectListHandler
bind:this={domListHandler}
list={dbKeys_getFlatList(model)}
selectedObjectStore={focusedTreeDbKey}
getSelectedObject={getFocusedTreeDbKey}
selectedObjectMatcher={(o1, o2) => o1?.key == o2?.key && o1?.type == o2?.type}
handleObjectClick={(data, clickAction) => {
focusedTreeDbKey.set(data);
const openDetailOnArrows = getOpenDetailOnArrowsSettings();
if (data.key && ((openDetailOnArrows && clickAction == 'keyArrow') || clickAction == 'keyEnter')) {
openNewTab({
tabComponent: 'DbKeyDetailTab',
title: data.text || '(no name)',
icon: 'img keydb',
props: {
isDefaultBrowser: true,
conid,
database,
},
});
$activeDbKeysStore = {
...$activeDbKeysStore,
[`${conid}:${database}`]: data.key,
};
}
if (data.key && clickAction == 'keyEnter') {
changeModel(model => dbKeys_markNodeExpanded(model, data.key, !model.dirsByKey[data.key]?.isExpanded), false);
}
}}
handleExpansion={(data, value) => {
changeModel(model => dbKeys_markNodeExpanded(model, data.key, value), false);
}}
onScrollTop={() => {
domContainer?.scrollTop();
}}
onFocusFilterBox={text => {
domFilter?.focus(text);
}}
>
<DbKeysSubTree key="" {filter} {model} {changeModel} {conid} {database} {connection} />
</AppObjectListHandler>
</WidgetsInnerContainer>