Merge pull request #1307 from dbgate/feature/redis-gui-refactor

Feature/redis gui refactor
This commit is contained in:
Jan Prochazka
2025-12-29 15:18:32 +01:00
committed by GitHub
22 changed files with 1812 additions and 114 deletions

View File

@@ -30,12 +30,22 @@
import InputTextModal from '../modals/InputTextModal.svelte';
import _ from 'lodash';
import DbKeyItemDetail from '../dbkeyvalue/DbKeyItemDetail.svelte';
import DbKeyValueListEdit from '../dbkeyvalue/DbKeyValueListEdit.svelte';
import DbKeyValueHashEdit from '../dbkeyvalue/DbKeyValueHashEdit.svelte';
import DbKeyValueZSetEdit from '../dbkeyvalue/DbKeyValueZSetEdit.svelte';
import DbKeyValueSetEdit from '../dbkeyvalue/DbKeyValueSetEdit.svelte';
import DbKeyValueStreamEdit from '../dbkeyvalue/DbKeyValueStreamEdit.svelte';
import DbKeyAddItemModal from '../modals/DbKeyAddItemModal.svelte';
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
import { changeTab } from '../utility/common';
import SelectField from '../forms/SelectField.svelte';
import DbKeyValueDetail from '../dbkeyvalue/DbKeyValueDetail.svelte';
import { _t } from '../translations';
import ToolStripContainer from '../buttons/ToolStripContainer.svelte';
import ToolStripButton from '../buttons/ToolStripButton.svelte';
import type { ChangeSetRedis, ChangeSetRedisType } from 'dbgate-datalib';
import useEditorData from '../query/useEditorData';
import { onDestroy } from 'svelte';
export let tabid;
export let conid;
@@ -44,18 +54,61 @@
export let isDefaultBrowser = false;
export const activator = createActivator('DbKeyDetailTab', true);
export function getChangeSetRedis(): ChangeSetRedis {
return changeSetRedis;
}
export function resetChangeSet() {
changeSetRedis = { changes: [] };
}
let currentRow;
let showAddForm = false;
let previousKey = null;
$: key = $activeDbKeysStore[`${conid}:${database}`];
let refreshToken = 0;
let editedValue = null;
const { editorState, editorValue, setEditorData } = useEditorData({
tabid,
onInitialData: value => {
if (value && value.changes) {
changeSetRedis = value;
}
},
});
let changeSetRedis: ChangeSetRedis = { changes: [] };
$: if ($editorValue && $editorValue.changes) {
changeSetRedis = $editorValue;
}
$: if (changeSetRedis && changeSetRedis.changes) {
setEditorData(changeSetRedis);
}
$: if (key !== previousKey && previousKey !== null && changeSetRedis.changes.length > 0) {
setEditorData(changeSetRedis);
previousKey = key;
} else if (key !== previousKey) {
previousKey = key;
}
$: hasChanges = changeSetRedis.changes.length > 0;
$: changeTab(tabid, tab => ({
...tab,
title: getKeyText(key),
}));
onDestroy(() => {
if (changeSetRedis && changeSetRedis.changes && changeSetRedis.changes.length > 0) {
setEditorData(changeSetRedis);
}
});
function handleChangeTtl(keyInfo) {
showModal(InputTextModal, {
value: keyInfo.ttl,
@@ -85,98 +138,373 @@
});
}
function refresh() {
editedValue = null;
refreshToken += 1;
}
async function saveString() {
await apiCall('database-connections/call-method', {
conid,
database,
method: 'set',
args: [key, editedValue],
});
refresh();
}
async function addItem(keyInfo) {
showModal(DbKeyAddItemModal, {
keyInfo,
onConfirm: async row => {
function handleKeyRename(keyInfo) {
showModal(InputTextModal, {
value: keyInfo.key,
label: 'New key name',
header: `Rename key ${keyInfo.key}`,
onConfirm: async value => {
const res = await apiCall('database-connections/call-method', {
conid,
database,
method: keyInfo.keyType.addMethod,
args: [keyInfo.key, ...keyInfo.keyType.dbKeyFields.map(col => row[col.name])],
method: 'rename',
args: [keyInfo.key, value],
});
if (res.errorMessage) {
showModal(ErrorMessageModal, { message: res.errorMessage });
return false;
return;
}
refresh();
return true;
activeDbKeysStore.update(store => ({
...store,
[`${conid}:${database}`]: value,
}));
},
});
}
function addOrUpdateChange(change: ChangeSetRedisType) {
const existingIndex = changeSetRedis.changes.findIndex(
c => c.key === change.key && c.type === change.type
);
if (existingIndex >= 0) {
changeSetRedis = {
...changeSetRedis,
changes: changeSetRedis.changes.map((c, idx) =>
idx === existingIndex ? change : c
)
};
} else {
changeSetRedis = {
...changeSetRedis,
changes: [...changeSetRedis.changes, change]
};
}
}
function getDisplayRow(row, keyInfo) {
if (!row) return row;
const existingChange = changeSetRedis.changes.find(
c => c.key === keyInfo.key && c.type === keyInfo.type
);
if (!existingChange) return row;
if (keyInfo.type === 'hash') {
// @ts-ignore
const update = existingChange.updates?.find(u => u.key === row.key);
if (update) {
return { ...row, value: update.value, TTL: update.ttl !== undefined ? update.ttl : row.TTL };
}
} else if (keyInfo.type === 'list') {
// @ts-ignore
const update = existingChange.updates?.find(u => u.index === row.rowNumber);
if (update) {
return { ...row, value: update.value };
}
} else if (keyInfo.type === 'zset') {
// @ts-ignore
const update = existingChange.updates?.find(u => u.member === row.member);
if (update) {
return { ...row, score: update.score };
}
}
return row;
}
function getDisplayValue(keyInfo) {
const existingChange = changeSetRedis.changes.find(
c => c.key === keyInfo.key && c.type === keyInfo.type
);
if (existingChange && (keyInfo.type === 'string' || keyInfo.type === 'JSON')) {
// @ts-ignore
return existingChange.value || keyInfo.value;
}
return keyInfo.value;
}
function getExistingInserts(keyInfo) {
const existingChange = changeSetRedis.changes.find(
c => c.key === keyInfo.key && c.type === keyInfo.type
);
let records = [];
// Add existing inserts if any
if (existingChange && existingChange.inserts) {
// @ts-ignore
records = existingChange.inserts.map(insert => {
if (keyInfo.type === 'hash') {
return { key: insert.key || '', value: insert.value || '', ttl: insert.ttl ? String(insert.ttl) : '' };
} else if (keyInfo.type === 'list' || keyInfo.type === 'set') {
return { value: insert.value || '' };
} else if (keyInfo.type === 'zset') {
return { member: insert.member || '', score: insert.score ? String(insert.score) : '' };
} else if (keyInfo.type === 'stream') {
return { id: insert.id || '', value: insert.value || '' };
}
return insert;
});
}
if (records.length === 0) {
if (keyInfo.type === 'hash') {
records.push({ key: '', value: '', ttl: '' });
} else if (keyInfo.type === 'list' || keyInfo.type === 'set') {
records.push({ value: '' });
} else if (keyInfo.type === 'zset') {
records.push({ member: '', score: '' });
} else if (keyInfo.type === 'stream') {
records.push({ id: '', value: '' });
}
}
return { records };
}
function refresh() {
changeSetRedis = { changes: [] };
setEditorData({ changes: [] });
refreshToken += 1;
}
async function saveAll() {
await apiCall('database-connections/save-redis-data', {
conid,
database,
changeSet: changeSetRedis,
});
changeSetRedis = { changes: [] };
setEditorData({ changes: [] });
refreshToken += 1;
}
</script>
{#await apiCall('database-connections/load-key-info', { conid, database, key, refreshToken })}
<LoadingInfo message="Loading key details" wrapper />
{:then keyInfo}
<div class="container">
<div class="top-panel">
<div class="type">
<FontIcon icon={getIconForRedisType(keyInfo.type)} padRight />
{keyInfo.type}
<ToolStripContainer>
<div class="container">
<div class="top-panel">
<div class="type">
<FontIcon icon={getIconForRedisType(keyInfo.type)} padRight />
{keyInfo.keyType?.label || keyInfo.type}
</div>
<div class="key-name">
<TextField value={key} readOnly />
</div>
<FormStyledButton value="Rename Key" on:click={() => handleKeyRename(keyInfo)} />
<FormStyledButton value={`TTL:${keyInfo.ttl}`} on:click={() => handleChangeTtl(keyInfo)} />
</div>
<div class="key-name">
<TextField value={key} readOnly />
</div>
<FormStyledButton value={`TTL:${keyInfo.ttl}`} on:click={() => handleChangeTtl(keyInfo)} />
{#if keyInfo.type == 'string'}
<FormStyledButton
value={_t('common.save', { defaultMessage: 'Save' })}
on:click={saveString}
disabled={!editedValue}
/>
{/if}
{#if keyInfo.keyType?.addMethod && keyInfo.keyType?.showItemList}
<FormStyledButton value="Add item" on:click={() => addItem(keyInfo)} />
{/if}
<FormStyledButton value={_t('common.refresh', { defaultMessage: 'Refresh' })} on:click={refresh} />
</div>
<div class="content">
{#if keyInfo.keyType?.dbKeyFields && keyInfo.keyType?.showItemList}
<VerticalSplitter>
<svelte:fragment slot="1">
<DbKeyTableControl
{conid}
{database}
{keyInfo}
onChangeSelected={row => {
currentRow = row;
<div class="content">
{#if keyInfo.keyType?.dbKeyFields && keyInfo.keyType?.showItemList}
<VerticalSplitter>
<svelte:fragment slot="1">
<DbKeyTableControl
{conid}
{database}
{keyInfo}
{changeSetRedis}
onChangeSelected={row => {
currentRow = row;
showAddForm = false;
}}
modifyRow={row => getDisplayRow(row, keyInfo)}
/>
</svelte:fragment>
<svelte:fragment slot="2">
{#if showAddForm}
{#if keyInfo.type === 'list'}
<DbKeyValueListEdit
dbKeyFields={keyInfo.keyType.dbKeyFields}
item={getExistingInserts(keyInfo)}
keyColumn={null}
onChangeItem={item => {
if (item && item.records && item.records.length > 0) {
const existingChange = changeSetRedis.changes.find(
c => c.key === keyInfo.key && c.type === keyInfo.type
);
// @ts-ignore
const listChange = existingChange || { key: keyInfo.key, type: 'list', inserts: [], updates: [], deletes: [] };
// @ts-ignore
listChange.inserts = item.records.filter(r => r.value.trim() !== '').map(r => ({ value: r.value }));
addOrUpdateChange(listChange);
}
}}
/>
{:else if keyInfo.type === 'hash'}
<DbKeyValueHashEdit
dbKeyFields={keyInfo.keyType.dbKeyFields}
item={getExistingInserts(keyInfo)}
keyColumn={null}
onChangeItem={item => {
if (item && item.records && item.records.length > 0) {
const existingChange = changeSetRedis.changes.find(
c => c.key === keyInfo.key && c.type === keyInfo.type
);
// @ts-ignore
const hashChange = existingChange || { key: keyInfo.key, type: 'hash', inserts: [], updates: [], deletes: [] };
// @ts-ignore
hashChange.inserts = item.records.filter(r => r.key.trim() !== '' && r.value.trim() !== '').map(r => ({ key: r.key, value: r.value, ttl: r.ttl ? parseInt(r.ttl) : undefined }));
addOrUpdateChange(hashChange);
}
}}
/>
{:else if keyInfo.type === 'zset'}
<DbKeyValueZSetEdit
dbKeyFields={keyInfo.keyType.dbKeyFields}
item={getExistingInserts(keyInfo)}
keyColumn={null}
onChangeItem={item => {
if (item && item.records && item.records.length > 0) {
const existingChange = changeSetRedis.changes.find(
c => c.key === keyInfo.key && c.type === keyInfo.type
);
// @ts-ignore
const zsetChange = existingChange || { key: keyInfo.key, type: 'zset', inserts: [], updates: [], deletes: [] };
// @ts-ignore
zsetChange.inserts = item.records.filter(r => r.member.trim() !== '' && r.score.trim() !== '').map(r => ({ member: r.member, score: parseFloat(r.score) }));
addOrUpdateChange(zsetChange);
}
}}
/>
{:else if keyInfo.type === 'set'}
<DbKeyValueSetEdit
dbKeyFields={keyInfo.keyType.dbKeyFields}
item={getExistingInserts(keyInfo)}
keyColumn={null}
onChangeItem={item => {
if (item && item.records && item.records.length > 0) {
const existingChange = changeSetRedis.changes.find(
c => c.key === keyInfo.key && c.type === keyInfo.type
);
// @ts-ignore
const setChange = existingChange || { key: keyInfo.key, type: 'set', inserts: [], updates: [], deletes: [] };
// @ts-ignore
setChange.inserts = item.records.filter(r => r.value.trim() !== '').map(r => ({ value: r.value }));
addOrUpdateChange(setChange);
}
}}
/>
{:else if keyInfo.type === 'stream'}
<DbKeyValueStreamEdit
dbKeyFields={keyInfo.keyType.dbKeyFields}
item={getExistingInserts(keyInfo)}
keyColumn={null}
onChangeItem={item => {
if (item && item.records && item.records.length > 0) {
const existingChange = changeSetRedis.changes.find(
c => c.key === keyInfo.key && c.type === keyInfo.type
);
// @ts-ignore
const streamChange = existingChange || { key: keyInfo.key, type: 'stream', inserts: [], updates: [], deletes: [] };
// @ts-ignore
streamChange.inserts = item.records.filter(r => r.value.trim() !== '').map(r => ({ id: r.id.trim() || '*', value: r.value }));
addOrUpdateChange(streamChange);
}
}}
/>
{/if}
{:else}
<DbKeyItemDetail
dbKeyFields={keyInfo.keyType.dbKeyFields}
item={getDisplayRow(currentRow, keyInfo)}
onChangeItem={item => {
const existingChange = changeSetRedis.changes.find(
c => c.key === keyInfo.key && c.type === keyInfo.type
);
if (keyInfo.type === 'hash') {
// @ts-ignore
const hashChange = existingChange || { key: keyInfo.key, type: 'hash', inserts: [], updates: [], deletes: [] };
// @ts-ignore
const updateIndex = hashChange.updates?.findIndex(u => u.key === item.key) ?? -1;
if (updateIndex >= 0) {
// @ts-ignore
hashChange.updates[updateIndex] = { key: item.key, value: item.value, ttl: item.TTL };
} else {
// @ts-ignore
hashChange.updates = [...(hashChange.updates || []), { key: item.key, value: item.value, ttl: item.TTL }];
}
addOrUpdateChange(hashChange);
} else if (keyInfo.type === 'list') {
// @ts-ignore
const listChange = existingChange || { key: keyInfo.key, type: 'list', inserts: [], updates: [], deletes: [] };
// @ts-ignore
const updateIndex = listChange.updates?.findIndex(u => u.index === item.rowNumber) ?? -1;
if (updateIndex >= 0) {
// @ts-ignore
listChange.updates[updateIndex] = { index: item.rowNumber, value: item.value };
} else {
// @ts-ignore
listChange.updates = [...(listChange.updates || []), { index: item.rowNumber, value: item.value }];
}
addOrUpdateChange(listChange);
} else if (keyInfo.type === 'zset') {
// @ts-ignore
const zsetChange = existingChange || { key: keyInfo.key, type: 'zset', inserts: [], updates: [], deletes: [] };
// @ts-ignore
const updateIndex = zsetChange.updates?.findIndex(u => u.member === item.member) ?? -1;
if (updateIndex >= 0) {
// @ts-ignore
zsetChange.updates[updateIndex] = { member: item.member, score: item.score };
} else {
// @ts-ignore
zsetChange.updates = [...(zsetChange.updates || []), { member: item.member, score: item.score }];
}
addOrUpdateChange(zsetChange);
}
}}
/>
{/if}
</svelte:fragment>
</VerticalSplitter>
{:else}
<div class="value-holder">
<DbKeyValueDetail
columnTitle="Value"
value={getDisplayValue(keyInfo)}
keyType={keyInfo.type}
onChangeValue={value => {
if (keyInfo.type === 'string') {
addOrUpdateChange({
key: key,
type: 'string',
value: value,
});
} else if (keyInfo.type === 'JSON') {
addOrUpdateChange({
key: key,
type: 'json',
value: value,
});
}
}}
/>
</svelte:fragment>
<svelte:fragment slot="2">
<DbKeyItemDetail dbKeyFields={keyInfo.keyType.dbKeyFields} item={currentRow} />
</svelte:fragment>
</VerticalSplitter>
{:else}
<div class="value-holder">
<DbKeyValueDetail
columnTitle="Value"
value={editedValue || keyInfo.value}
onChangeValue={value => {
editedValue = value;
}}
/>
</div>
{/if}
</div>
{/if}
</div>
</div>
</div>
<svelte:fragment slot="toolstrip">
<ToolStripButton
icon="icon save"
on:click={saveAll}
disabled={!hasChanges}
>{_t('common.save', { defaultMessage: 'Save' })}</ToolStripButton>
{#if keyInfo.keyType?.addMethod && keyInfo.keyType?.showItemList}
<ToolStripButton icon="icon add" on:click={() => { showAddForm = true; }}>Add field</ToolStripButton>
{/if}
<ToolStripButton icon="icon refresh" on:click={refresh}>{_t('common.refresh', { defaultMessage: 'Refresh' })}</ToolStripButton>
</svelte:fragment>
</ToolStripContainer>
{/await}
<style>

View File

@@ -0,0 +1,264 @@
<script lang="ts">
import DbKeyValueDetail from '../dbkeyvalue/DbKeyValueDetail.svelte';
import DbKeyValueHashEdit from '../dbkeyvalue/DbKeyValueHashEdit.svelte';
import DbKeyValueListEdit from '../dbkeyvalue/DbKeyValueListEdit.svelte';
import DbKeyValueSetEdit from '../dbkeyvalue/DbKeyValueSetEdit.svelte';
import DbKeyValueZSetEdit from '../dbkeyvalue/DbKeyValueZSetEdit.svelte';
import DbKeyValueStreamEdit from '../dbkeyvalue/DbKeyValueStreamEdit.svelte';
import FormFieldTemplateLarge from "../forms/FormFieldTemplateLarge.svelte";
import FormProvider from '../forms/FormProvider.svelte';
import SelectField from '../forms/SelectField.svelte';
import TextField from "../forms/TextField.svelte";
import ToolStripContainer from '../buttons/ToolStripContainer.svelte';
import ToolStripButton from '../buttons/ToolStripButton.svelte';
import { _t } from '../translations';
import { apiCall } from '../utility/api';
import { showSnackbarSuccess } from '../utility/snackbar';
import { findEngineDriver } from 'dbgate-tools';
import { activeDbKeysStore, getExtensions, openedTabs } from '../stores';
import { useConnectionInfo } from '../utility/metadataLoaders';
import openNewTab from '../utility/openNewTab';
export let conid;
export let database;
export let tabid;
export let initialKeyName = '';
$: connection = useConnectionInfo({ conid });
$: driver = $connection && findEngineDriver($connection, getExtensions());
let item = {};
let keyName = initialKeyName || '';
$: type = driver?.supportedKeyTypes?.[0]?.name || '';
$: console.log('DbKeyTab debug:', { conid, database, connection: $connection, driver, hasTypes: driver?.supportedKeyTypes?.length });
async function handleSave() {
if (!driver) return;
const typeConfig = driver.supportedKeyTypes.find(x => x.name == type);
if (type === 'hash' && item.records && Array.isArray(item.records)) {
for (const record of item.records) {
if (record.key && record.value) {
await apiCall('database-connections/call-method', {
conid,
database,
method: typeConfig.addMethod,
args: [keyName, record.key, record.value],
});
}
}
} else if (type === 'list' && item.records && Array.isArray(item.records)) {
const values = item.records
.map(record => record.value)
.filter(value => value);
if (values.length > 0) {
await apiCall('database-connections/call-method', {
conid,
database,
method: typeConfig.addMethod,
args: [keyName, ...values],
});
}
} else if (type === 'set' && item.records && Array.isArray(item.records)) {
const values = item.records
.map(record => record.value)
.filter(value => value);
if (values.length > 0) {
await apiCall('database-connections/call-method', {
conid,
database,
method: typeConfig.addMethod,
args: [keyName, ...values],
});
}
} else if (type === 'zset' && item.records && Array.isArray(item.records)) {
for (const record of item.records) {
if (record.member && record.score) {
await apiCall('database-connections/call-method', {
conid,
database,
method: typeConfig.addMethod,
args: [keyName, record.member, parseFloat(record.score)],
});
}
}
} else if (type === 'stream' && item.records && Array.isArray(item.records)) {
for (const record of item.records) {
if (record.value) {
const streamId = record.id || '*';
await apiCall('database-connections/call-method', {
conid,
database,
method: typeConfig.addMethod,
args: [keyName, streamId, record.value],
});
}
}
} else {
await apiCall('database-connections/call-method', {
conid,
database,
method: typeConfig.addMethod,
args: [keyName, ...typeConfig.dbKeyFields.map(fld => item[fld.name])],
});
}
showSnackbarSuccess('Key created successfully');
$activeDbKeysStore = {
...$activeDbKeysStore,
[`${conid}:${database}`]: keyName,
};
openedTabs.update(tabs =>
tabs.map(tab =>
tab.tabid === tabid
? { ...tab, closedTime: new Date().getTime(), selected: false }
: tab
)
);
openNewTab({
tabComponent: 'DbKeyDetailTab',
title: keyName || '(no name)',
icon: 'img keydb',
props: {
isDefaultBrowser: true,
conid,
database,
},
});
}
</script>
{#if driver && driver.supportedKeyTypes && driver.supportedKeyTypes.length > 0}
<FormProvider>
<ToolStripContainer>
<div class="container">
<div class="flex flex-gap">
<div class="col-9">
<FormFieldTemplateLarge label={_t('addDbKeyModal.key', { defaultMessage: 'Key' })} type="text" noMargin>
<TextField
value={keyName}
on:change={e => {
// @ts-ignore
keyName = e.target.value;
}}
/>
</FormFieldTemplateLarge>
</div>
<div class="col-3">
<FormFieldTemplateLarge label={_t('addDbKeyModal.type', { defaultMessage: 'Type' })} type="combo" noMargin>
<SelectField
options={driver.supportedKeyTypes.map(t => ({ value: t.name, label: t.label }))}
value={type}
isNative
on:change={e => {
type = e.detail;
}}
/>
</FormFieldTemplateLarge>
</div>
</div>
{#if type === 'hash'}
<DbKeyValueHashEdit
dbKeyFields={driver.supportedKeyTypes.find(x => x.name == type).dbKeyFields}
{item}
onChangeItem={value => {
item = value;
}}
/>
{:else if type === 'list'}
<DbKeyValueListEdit
dbKeyFields={driver.supportedKeyTypes.find(x => x.name == type).dbKeyFields}
{item}
onChangeItem={value => {
item = value;
}}
/>
{:else if type === 'set'}
<DbKeyValueSetEdit
dbKeyFields={driver.supportedKeyTypes.find(x => x.name == type).dbKeyFields}
{item}
onChangeItem={value => {
item = value;
}}
/>
{:else if type === 'zset'}
<DbKeyValueZSetEdit
dbKeyFields={driver.supportedKeyTypes.find(x => x.name == type).dbKeyFields}
{item}
onChangeItem={value => {
item = value;
}}
/>
{:else if type === 'stream'}
<DbKeyValueStreamEdit
dbKeyFields={driver.supportedKeyTypes.find(x => x.name == type).dbKeyFields}
{item}
onChangeItem={value => {
item = value;
}}
/>
{:else}
<DbKeyValueDetail
columnTitle="Value"
value={item.value}
onChangeValue={value => {
item = { ...item, value };
}}
/>
{/if}
</div>
<svelte:fragment slot="toolstrip">
<ToolStripButton
icon="icon save"
on:click={handleSave}
disabled={!keyName || keyName.trim() === ''}
>{_t('common.save', { defaultMessage: 'Save' })}</ToolStripButton>
</svelte:fragment>
</ToolStripContainer>
</FormProvider>
{:else}
<div class="wrapper">
<div class="container">
<div class="loading">Loading...</div>
</div>
</div>
{/if}
<style>
.wrapper {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.container {
flex: 1;
display: flex;
flex-direction: column;
padding: 20px;
overflow: hidden;
gap: 10px;
}
.button-container {
display: flex;
gap: 10px;
margin-top: 10px;
}
.flex-gap {
gap: 10px;
padding-bottom: 10px;
}
</style>

View File

@@ -18,6 +18,7 @@ import * as JsonTab from './JsonTab.svelte';
import * as ChangelogTab from './ChangelogTab.svelte';
import * as DiagramTab from './DiagramTab.svelte';
import * as DbKeyDetailTab from './DbKeyDetailTab.svelte';
import * as DbKeyTab from './DbKeyTab.svelte';
import * as QueryDataTab from './QueryDataTab.svelte';
import * as ConnectionTab from './ConnectionTab.svelte';
import * as MapTab from './MapTab.svelte';
@@ -50,6 +51,7 @@ export default {
ChangelogTab,
DiagramTab,
DbKeyDetailTab,
DbKeyTab,
QueryDataTab,
ConnectionTab,
MapTab,