Merge branch 'redis'

This commit is contained in:
Jan Prochazka
2022-03-16 18:41:17 +01:00
51 changed files with 1446 additions and 65 deletions

View File

@@ -240,6 +240,7 @@ module.exports = {
get_meta: true,
async get({ conid }) {
if (!conid) return null;
if (portalConnections) return portalConnections.find(x => x._id == conid) || null;
const res = await this.datastore.get(conid);
return res || null;

View File

@@ -151,6 +151,46 @@ module.exports = {
return res.result;
},
loadKeys_meta: true,
async loadKeys({ conid, database, root }) {
const opened = await this.ensureOpened(conid, database);
const res = await this.sendRequest(opened, { msgtype: 'loadKeys', root });
if (res.errorMessage) {
console.error(res.errorMessage);
}
return res.result || null;
},
loadKeyInfo_meta: true,
async loadKeyInfo({ conid, database, key }) {
const opened = await this.ensureOpened(conid, database);
const res = await this.sendRequest(opened, { msgtype: 'loadKeyInfo', key });
if (res.errorMessage) {
console.error(res.errorMessage);
}
return res.result || null;
},
loadKeyTableRange_meta: true,
async loadKeyTableRange({ conid, database, key, cursor, count }) {
const opened = await this.ensureOpened(conid, database);
const res = await this.sendRequest(opened, { msgtype: 'loadKeyTableRange', key, cursor, count });
if (res.errorMessage) {
console.error(res.errorMessage);
}
return res.result || null;
},
callMethod_meta: true,
async callMethod({ conid, database, method, args }) {
const opened = await this.ensureOpened(conid, database);
const res = await this.sendRequest(opened, { msgtype: 'callMethod', method, args });
if (res.errorMessage) {
console.error(res.errorMessage);
}
return res.result || null;
},
updateCollection_meta: true,
async updateCollection({ conid, database, changeSet }) {
const opened = await this.ensureOpened(conid, database);

View File

@@ -183,6 +183,50 @@ async function handleCollectionData({ msgid, options }) {
}
}
async function handleLoadKeys({ msgid, root }) {
await waitConnected();
const driver = requireEngineDriver(storedConnection);
try {
const result = await driver.loadKeys(systemConnection, root);
process.send({ msgtype: 'response', msgid, result });
} catch (err) {
process.send({ msgtype: 'response', msgid, errorMessage: err.message });
}
}
async function handleLoadKeyInfo({ msgid, key }) {
await waitConnected();
const driver = requireEngineDriver(storedConnection);
try {
const result = await driver.loadKeyInfo(systemConnection, key);
process.send({ msgtype: 'response', msgid, result });
} catch (err) {
process.send({ msgtype: 'response', msgid, errorMessage: err.message });
}
}
async function handleCallMethod({ msgid, method, args }) {
await waitConnected();
const driver = requireEngineDriver(storedConnection);
try {
const result = await driver.callMethod(systemConnection, method, args);
process.send({ msgtype: 'response', msgid, result });
} catch (err) {
process.send({ msgtype: 'response', msgid, errorMessage: err.message });
}
}
async function handleLoadKeyTableRange({ msgid, key, cursor, count }) {
await waitConnected();
const driver = requireEngineDriver(storedConnection);
try {
const result = await driver.loadKeyTableRange(systemConnection, key, cursor, count);
process.send({ msgtype: 'response', msgid, result });
} catch (err) {
process.send({ msgtype: 'response', msgid, errorMessage: err.message });
}
}
async function handleUpdateCollection({ msgid, changeSet }) {
await waitConnected();
const driver = requireEngineDriver(storedConnection);
@@ -248,6 +292,10 @@ const messageHandlers = {
runScript: handleRunScript,
updateCollection: handleUpdateCollection,
collectionData: handleCollectionData,
loadKeys: handleLoadKeys,
loadKeyInfo: handleLoadKeyInfo,
callMethod: handleCallMethod,
loadKeyTableRange: handleLoadKeyTableRange,
sqlPreview: handleSqlPreview,
ping: handlePing,
syncModel: handleSyncModel,

View File

@@ -9,7 +9,7 @@ async function tableReader({ connection, pureName, schemaName }) {
const fullName = { pureName, schemaName };
if (driver.dialect.nosql) {
if (driver.databaseEngineTypes.includes('document')) {
// @ts-ignore
console.log(`Reading collection ${fullNameToString(fullName)}`);
// @ts-ignore

View File

@@ -14,7 +14,7 @@ export function createBulkInsertStreamBase(driver, stream, pool, name, options):
writable.buffer = [];
writable.structure = null;
writable.columnNames = null;
writable.requireFixedStructure = !driver.dialect.nosql;
writable.requireFixedStructure = driver.databaseEngineTypes.includes('sql');
writable.addRow = async row => {
if (writable.structure) {

View File

@@ -20,6 +20,7 @@ export const driverBase = {
analyserClass: null,
dumperClass: SqlDumper,
dialect,
databaseEngineTypes: ['sql'],
async analyseFull(pool, version) {
const analyser = new this.analyserClass(pool, this, version);
@@ -45,7 +46,7 @@ export const driverBase = {
}
},
getNewObjectTemplates() {
if (!this.dialect?.nosql) {
if (this.databaseEngineTypes.includes('sql')) {
return [{ label: 'New view', sql: 'CREATE VIEW myview\nAS\nSELECT * FROM table1' }];
}
return [];

View File

@@ -59,3 +59,28 @@ export function safeJsonParse(json, defaultValue?, logError = false) {
export function isJsonLikeLongString(value) {
return _isString(value) && value.length > 100 && value.match(/^\s*\{.*\}\s*$|^\s*\[.*\]\s*$/);
}
export function getIconForRedisType(type) {
switch (type) {
case 'dir':
return 'img folder';
case 'string':
return 'img type-string';
case 'hash':
return 'img type-hash';
case 'set':
return 'img type-set';
case 'list':
return 'img type-list';
case 'zset':
return 'img type-zset';
case 'stream':
return 'img type-stream';
case 'binary':
return 'img type-binary';
case 'ReJSON-RL':
return 'img type-rejson';
default:
return null;
}
}

View File

@@ -10,7 +10,6 @@ export interface SqlDialect {
explicitDropConstraint?: boolean;
anonymousPrimaryKey?: boolean;
enableConstraintsPerTable?: boolean;
nosql?: boolean; // mongo
dropColumnDependencies?: string[];
changeColumnDependencies?: string[];

View File

@@ -46,6 +46,8 @@ export interface EngineDriver {
engine: string;
title: string;
defaultPort?: number;
databaseEngineTypes: string[];
supportedKeyTypes: { name: string; label: string }[];
supportsDatabaseUrl?: boolean;
isElectronOnly?: boolean;
showConnectionField?: (field: string, values: any) => boolean;
@@ -74,6 +76,9 @@ export interface EngineDriver {
name: string;
}[]
>;
loadKeys(pool, root: string): Promise;
loadKeyInfo(pool, key): Promise;
loadKeyTableRange(pool, key, cursor, count): Promise;
analyseFull(pool: any, serverVersion): Promise<DatabaseInfo>;
analyseIncremental(pool: any, structure: DatabaseInfo, serverVersion): Promise<DatabaseInfo>;
dialect: SqlDialect;
@@ -87,6 +92,8 @@ export interface EngineDriver {
getQuerySplitterOptions(usage: 'stream' | 'script'): any;
script(pool: any, sql: string): Promise;
getNewObjectTemplates(): NewObjectTemplate[];
// direct call of pool method, only some methods could be supported, on only some drivers
callMethod(pool, method, args);
analyserClass?: any;
dumperClass?: any;

View File

@@ -8,8 +8,8 @@
export let icon;
export let title;
export let data;
export let module;
export let data = null;
export let module = null;
export let isBold = false;
export let isBusy = false;
@@ -24,8 +24,10 @@
export let onPin = null;
export let onUnpin = null;
export let showPinnedInsteadOfUnpin = false;
export let indentLevel = 0;
$: isChecked = checkedObjectsStore && $checkedObjectsStore.find(x => module.extractKey(data) == module.extractKey(x));
$: isChecked =
checkedObjectsStore && $checkedObjectsStore.find(x => module?.extractKey(data) == module?.extractKey(x));
function handleExpand() {
dispatch('expand');
@@ -33,7 +35,7 @@
function handleClick() {
if (checkedObjectsStore) {
if (isChecked) {
checkedObjectsStore.update(x => x.filter(y => module.extractKey(data) != module.extractKey(y)));
checkedObjectsStore.update(x => x.filter(y => module?.extractKey(data) != module?.extractKey(y)));
} else {
checkedObjectsStore.update(x => [...x, data]);
}
@@ -52,12 +54,14 @@
function setChecked(value) {
if (!value && isChecked) {
checkedObjectsStore.update(x => x.filter(y => module.extractKey(data) != module.extractKey(y)));
checkedObjectsStore.update(x => x.filter(y => module?.extractKey(data) != module?.extractKey(y)));
}
if (value && !isChecked) {
checkedObjectsStore.update(x => [...x, data]);
}
}
// $: console.log(title, indentLevel);
</script>
<div
@@ -85,6 +89,9 @@
<FontIcon icon={expandIcon} />
</span>
{/if}
{#if indentLevel}
<span style:margin-right={`${indentLevel * 16}px`} />
{/if}
{#if isBusy}
<FontIcon icon="icon loading" />
{:else}
@@ -188,5 +195,4 @@
float: right;
color: var(--theme-font-2);
}
</style>

View File

@@ -174,8 +174,8 @@
return [
{ onClick: handleNewQuery, text: 'New query', isNewQuery: true },
!driver?.dialect?.nosql && { onClick: handleNewTable, text: 'New table' },
driver?.dialect?.nosql && { onClick: handleNewCollection, text: 'New collection' },
driver?.databaseEngineTypes?.includes('sql') && { onClick: handleNewTable, text: 'New table' },
driver?.databaseEngineTypes?.includes('document') && { onClick: handleNewCollection, text: 'New collection' },
{ divider: true },
{ onClick: handleImport, text: 'Import' },
{ onClick: handleExport, text: 'Export' },
@@ -256,6 +256,7 @@
{...$$restProps}
{data}
title={data.name}
extInfo={data.extInfo}
icon="img database"
colorMark={passProps?.connectionColorFactory &&
passProps?.connectionColorFactory({ conid: _.get(data.connection, '_id'), database: data.name }, null, null, false)}

View File

@@ -15,7 +15,7 @@
<AppObjectList
list={_.sortBy(
($databases || []).filter(x => filterName(filter, x.name)),
'name'
x => x.sortOrder ?? x.name
).map(db => ({ ...db, connection: data }))}
module={databaseAppObject}
{passProps}

View File

@@ -4,6 +4,7 @@ import { get } from 'svelte/store';
import { ThemeDefinition } from 'dbgate-types';
import ConnectionModal from '../modals/ConnectionModal.svelte';
import AboutModal from '../modals/AboutModal.svelte';
import AddDbKeyModal from '../modals/AddDbKeyModal.svelte';
import SettingsModal from '../settings/SettingsModal.svelte';
import ImportExportModal from '../modals/ImportExportModal.svelte';
import SqlGeneratorModal from '../modals/SqlGeneratorModal.svelte';
@@ -158,7 +159,7 @@ registerCommand({
toolbarName: 'New table',
testEnabled: () => {
const driver = findEngineDriver(get(currentDatabase)?.connection, getExtensions());
return !!get(currentDatabase) && !driver?.dialect?.nosql;
return !!get(currentDatabase) && driver?.databaseEngineTypes?.includes('sql');
},
onClick: () => {
const $currentDatabase = get(currentDatabase);
@@ -196,7 +197,7 @@ registerCommand({
toolbarName: 'New collection',
testEnabled: () => {
const driver = findEngineDriver(get(currentDatabase)?.connection, getExtensions());
return !!get(currentDatabase) && driver?.dialect?.nosql;
return !!get(currentDatabase) && driver?.databaseEngineTypes?.includes('document');
},
onClick: async () => {
const $currentDatabase = get(currentDatabase);
@@ -217,6 +218,30 @@ registerCommand({
},
});
registerCommand({
id: 'new.dbKey',
category: 'New',
name: 'Key',
toolbar: true,
toolbarName: 'New key',
testEnabled: () => {
const driver = findEngineDriver(get(currentDatabase)?.connection, getExtensions());
return !!get(currentDatabase) && driver?.databaseEngineTypes?.includes('keyvalue');
},
onClick: async () => {
const $currentDatabase = get(currentDatabase);
const connection = _.get($currentDatabase, 'connection') || {};
const database = _.get($currentDatabase, 'name');
const driver = findEngineDriver(get(currentDatabase)?.connection, getExtensions());
showModal(AddDbKeyModal, {
conid: connection._id,
database,
driver,
});
},
});
registerCommand({
id: 'new.markdown',
category: 'New',

View File

@@ -266,7 +266,7 @@
class:isOk
placeholder="Filter"
/>
{#if conid && database && driver && !driver?.dialect?.nosql}
{#if conid && database && driver && driver?.databaseEngineTypes?.includes('sql')}
{#if foreignKey}
<InlineButton on:click={handleShowDictionary} narrow square>
<FontIcon icon="icon dots-horizontal" />

View File

@@ -0,0 +1,95 @@
<script lang="ts">
import { onMount, tick } from 'svelte';
import ScrollableTableControl from '../elements/ScrollableTableControl.svelte';
import { apiCall } from '../utility/api';
import createRef from '../utility/createRef';
export let conid;
export let database;
export let keyInfo;
export let onChangeSelected;
let rows = [];
let cursor = 0;
let isLoading = false;
let loadNextNeeded = false;
let isLoadedAll = false;
let selectedIndex;
const oldIndexRef = createRef(null);
async function loadNextRows() {
if (isLoadedAll) {
return;
}
if (isLoading) {
// console.log('ALREADY LOADING');
loadNextNeeded = true;
return;
}
isLoading = true;
try {
const res = await apiCall('database-connections/load-key-table-range', {
conid,
database,
key: keyInfo.key,
cursor,
count: 10,
});
const newRows = [...rows];
for (const row of res.items) {
if (keyInfo.keyColumn && newRows.find(x => x[keyInfo.keyColumn] == row[keyInfo.keyColumn])) {
continue;
}
newRows.push({ rowNumber: newRows.length + 1, ...row });
}
rows = newRows;
cursor = res.cursor;
isLoadedAll = cursor == 0;
} finally {
isLoading = false;
}
if (loadNextNeeded) {
loadNextNeeded = false;
await tick();
loadNextRows();
}
}
$: {
if (onChangeSelected && rows[selectedIndex]) {
if (oldIndexRef.get() != selectedIndex) {
oldIndexRef.set(selectedIndex);
onChangeSelected(rows[selectedIndex]);
}
}
}
$: {
keyInfo;
}
onMount(() => {
loadNextRows();
});
</script>
<ScrollableTableControl
columns={[
{
fieldName: 'rowNumber',
header: 'num',
width: '60px',
},
...keyInfo.keyType.dbKeyFields.map(column => ({
fieldName: column.name,
header: column.name,
})),
]}
{rows}
onLoadNext={isLoadedAll ? null : loadNextRows}
selectable
singleLineRow
bind:selectedIndex
/>

View File

@@ -0,0 +1,44 @@
<script lang="ts">
import AceEditor from '../query/AceEditor.svelte';
export let dbKeyFields;
export let item;
export let onChangeItem = null;
</script>
<div class="props">
{#each dbKeyFields as column}
<div class="colname">{column.name}</div>
<div class="colvalue">
<AceEditor
readOnly={!onChangeItem}
value={item && item[column.name]}
on:input={e => {
if (onChangeItem) {
onChangeItem({
...item,
[column.name]: e.detail,
});
}
}}
/>
</div>
{/each}
</div>
<style>
.props {
flex: 1;
display: flex;
flex-direction: column;
}
.colname {
margin: 10px;
}
.colvalue {
position: relative;
flex: 1;
}
</style>

View File

@@ -15,7 +15,7 @@
<script lang="ts">
import _ from 'lodash';
import { onMount } from 'svelte';
import { onDestroy, onMount } from 'svelte';
import keycodes from '../utility/keycodes';
import { createEventDispatcher } from 'svelte';
import resizeObserver from '../utility/resizeObserver';
@@ -27,6 +27,8 @@
export let selectedIndex = 0;
export let clickable = false;
export let disableFocusOutline = false;
export let onLoadNext = null;
export let singleLineRow = false;
export let domTable = undefined;
@@ -34,6 +36,36 @@
let headerHeight = 0;
let domBody;
let domLoadNext;
let observer;
function startObserver(dom) {
if (observer) {
// console.log('STOP OBSERVE');
observer.disconnect();
observer = null;
}
if (dom) {
// console.log('OBSERVE');
observer = new IntersectionObserver(entries => {
// console.log('ENTRIES', entries);
if (entries.find(x => x.isIntersecting)) {
// console.log('INVOKE LOAD NEXT');
onLoadNext();
}
});
observer.observe(dom);
}
}
$: startObserver(domLoadNext);
onDestroy(() => {
if (observer) {
observer.disconnect();
}
});
const dispatch = createEventDispatcher();
$: columnList = _.compact(_.flatten(columns));
@@ -86,6 +118,7 @@
bind:this={domTable}
class:selectable
class:disableFocusOutline
class:singleLineRow
on:keydown
tabindex={selectable ? -1 : undefined}
on:keydown={handleKeyDown}
@@ -163,6 +196,13 @@
{/each}
</tr>
{/each}
{#if onLoadNext}
{#key rows}
<tr>
<td colspan={columnList.length} bind:this={domLoadNext}> Loading next rows... </td>
</tr>
{/key}
{/if}
</tbody>
</table>
</div>
@@ -193,6 +233,11 @@
table tbody tr td {
overflow: hidden;
}
table.singleLineRow tbody tr td {
white-space: nowrap;
}
table tbody {
display: block;
overflow-y: scroll;

View File

@@ -8,4 +8,13 @@
if (focused) onMount(() => domEditor.focus());
</script>
<input type="text" {...$$restProps} bind:value on:change on:input bind:this={domEditor} on:keydown autocomplete="new-password" />
<input
type="text"
{...$$restProps}
bind:value
on:change
on:input
bind:this={domEditor}
on:keydown
autocomplete="new-password"
/>

View File

@@ -167,6 +167,16 @@
'img link': 'mdi mdi-link',
'img filter': 'mdi mdi-filter',
'img group': 'mdi mdi-group',
'img folder': 'mdi mdi-folder color-icon-yellow',
'img type-string': 'mdi mdi-alphabetical color-icon-blue',
'img type-hash': 'mdi mdi-pound color-icon-blue',
'img type-set': 'mdi mdi-format-list-bulleted color-icon-blue',
'img type-list': 'mdi mdi-format-list-numbered color-icon-blue',
'img type-zset': 'mdi mdi-format-list-checks color-icon-blue',
'img type-stream': 'mdi mdi-view-stream color-icon-blue',
'img type-binary': 'mdi mdi-file color-icon-blue',
'img type-rejson': 'mdi mdi-color-json color-icon-blue',
};
</script>

View File

@@ -0,0 +1,58 @@
<script lang="ts">
import FormStyledButton from '../buttons/FormStyledButton.svelte';
import DbKeyItemDetail from '../dbkeyvalue/DbKeyItemDetail.svelte';
import FormProvider from '../forms/FormProvider.svelte';
import SelectField from '../forms/SelectField.svelte';
import ModalBase from './ModalBase.svelte';
import { closeCurrentModal } from './modalTools';
export let conid;
export let database;
export let driver;
export let onConfirm;
let item = {};
let type = driver.supportedKeyTypes[0].name;
const handleSubmit = async () => {
closeCurrentModal();
onConfirm(item);
};
</script>
<FormProvider>
<ModalBase {...$$restProps}>
<svelte:fragment slot="header">Add item</svelte:fragment>
<div class="container">
<SelectField
options={driver.supportedKeyTypes.map(t => ({ value: t.name, label: t.label }))}
value={type}
on:change={e => {
type = e.detail;
}}
/>
<DbKeyItemDetail
dbKeyFields={driver.supportedKeyTypes.find(x => x.name == type).dbKeyFields}
{item}
onChangeItem={value => {
item = value;
}}
/>
</div>
<svelte:fragment slot="footer">
<FormStyledButton value="OK" on:click={e => handleSubmit()} />
<FormStyledButton type="button" value="Cancel" on:click={closeCurrentModal} />
</svelte:fragment>
</ModalBase>
</FormProvider>
<style>
.container {
display: flex;
height: 30vh;
}
</style>

View File

@@ -26,6 +26,9 @@
$: disabledFields = (currentAuthType ? currentAuthType.disabledFields : null) || [];
$: driver = $extensions.drivers.find(x => x.engine == engine);
$: defaultDatabase = $values.defaultDatabase;
$: showUser = !driver?.showConnectionField || driver.showConnectionField('user', $values);
$: showPassword = !driver?.showConnectionField || driver.showConnectionField('password', $values);
</script>
<FormSelectField
@@ -100,17 +103,19 @@
</div>
{/if}
{#if !driver?.showConnectionField || driver.showConnectionField('user', $values)}
{#if showUser && showPassword}
<div class="row">
<div class="col-6 mr-1">
<FormTextField
label="User"
name="user"
disabled={disabledFields.includes('user')}
templateProps={{ noMargin: true }}
/>
</div>
{#if !driver?.showConnectionField || driver.showConnectionField('password', $values)}
{#if showUser}
<div class="col-6 mr-1">
<FormTextField
label="User"
name="user"
disabled={disabledFields.includes('user')}
templateProps={{ noMargin: true }}
/>
</div>
{/if}
{#if showPassword}
<div class="col-6 mr-1">
<FormPasswordField
label="Password"
@@ -122,8 +127,14 @@
{/if}
</div>
{/if}
{#if showUser && !showPassword}
<FormTextField label="User" name="user" disabled={disabledFields.includes('user')} />
{/if}
{#if !showUser && showPassword}
<FormPasswordField label="Password" name="password" disabled={disabledFields.includes('password')} />
{/if}
{#if !disabledFields.includes('password') && (!driver?.showConnectionField || driver.showConnectionField('password', $values))}
{#if !disabledFields.includes('password') && showPassword}
<FormSelectField
label="Password mode"
isNative

View File

@@ -0,0 +1,47 @@
<script lang="ts">
import FormStyledButton from '../buttons/FormStyledButton.svelte';
import DbKeyItemDetail from '../dbkeyvalue/DbKeyItemDetail.svelte';
import FormProvider from '../forms/FormProvider.svelte';
import ModalBase from './ModalBase.svelte';
import { closeCurrentModal } from './modalTools';
export let keyInfo;
export let label;
export let onConfirm;
let item = {};
const handleSubmit = async () => {
closeCurrentModal();
onConfirm(item);
};
</script>
<FormProvider>
<ModalBase {...$$restProps}>
<svelte:fragment slot="header">Add item</svelte:fragment>
<div class="container">
<DbKeyItemDetail
dbKeyFields={keyInfo.keyType.dbKeyFields}
{item}
onChangeItem={value => {
item = value;
}}
/>
</div>
<svelte:fragment slot="footer">
<FormStyledButton value="OK" on:click={e => handleSubmit()} />
<FormStyledButton type="button" value="Cancel" on:click={closeCurrentModal} />
</svelte:fragment>
</ModalBase>
</FormProvider>
<style>
.container {
display: flex;
height: 30vh;
}
</style>

View File

@@ -93,6 +93,7 @@ export const loadingPluginStore = writable({
loaded: false,
loadingPackageName: null,
});
export const activeDbKeysStore = writableWithStorage({}, 'activeDbKeysStore');
export const currentThemeDefinition = derived([currentTheme, extensions], ([$currentTheme, $extensions]) =>
$extensions.themes.find(x => x.themeClassName == $currentTheme)

View File

@@ -0,0 +1,183 @@
<script lang="ts" context="module">
import createActivator, { getActiveComponent } from '../utility/createActivator';
const getCurrentEditor = () => getActiveComponent('DbKeyDetailTab');
export const matchingProps = ['conid', 'database', 'isDefaultBrowser'];
export const allowAddToFavorites = props => true;
</script>
<script lang="ts">
import { activeDbKeysStore } from '../stores';
import { apiCall } from '../utility/api';
import LoadingInfo from '../elements/LoadingInfo.svelte';
import ScrollableTableControl from '../elements/ScrollableTableControl.svelte';
import AceEditor from '../query/AceEditor.svelte';
import VerticalSplitter from '../elements/VerticalSplitter.svelte';
import FormStyledButton from '../buttons/FormStyledButton.svelte';
import FontIcon from '../icons/FontIcon.svelte';
import { getIconForRedisType } from 'dbgate-tools';
import TextField from '../forms/TextField.svelte';
import DbKeyTableControl from '../datagrid/DbKeyTableControl.svelte';
import { showModal } from '../modals/modalTools';
import InputTextModal from '../modals/InputTextModal.svelte';
import _ from 'lodash';
import DbKeyItemDetail from '../dbkeyvalue/DbKeyItemDetail.svelte';
import DbKeyAddItemModal from '../modals/DbKeyAddItemModal.svelte';
export let conid;
export let database;
export let key;
export let isDefaultBrowser = false;
export const activator = createActivator('DbKeyDetailTab', true);
let currentRow;
$: key = $activeDbKeysStore[`${conid}:${database}`];
let refreshToken = 0;
let editedValue = null;
function handleChangeTtl(keyInfo) {
showModal(InputTextModal, {
value: keyInfo.ttl,
label: 'New TTL value (-1=key never expires)',
header: `Set TTL for key ${keyInfo.key}`,
onConfirm: async value => {
const ttl = parseInt(value);
if (_.isNumber(ttl)) {
if (ttl < 0) {
await apiCall('database-connections/call-method', {
conid,
database,
method: 'persist',
args: [keyInfo.key],
});
} else {
await apiCall('database-connections/call-method', {
conid,
database,
method: 'expire',
args: [keyInfo.key, ttl],
});
}
refresh();
}
},
});
}
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 => {
await apiCall('database-connections/call-method', {
conid,
database,
method: keyInfo.keyType.addMethod,
args: [keyInfo.key, ...keyInfo.keyType.dbKeyFields.map(col => row[col.name])],
});
refresh();
},
});
}
</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}
</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="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="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;
}}
/>
</svelte:fragment>
<svelte:fragment slot="2">
<DbKeyItemDetail dbKeyFields={keyInfo.keyType.dbKeyFields} item={currentRow} />
</svelte:fragment>
</VerticalSplitter>
{:else}
<AceEditor
value={editedValue || keyInfo.value}
on:input={e => {
editedValue = e.detail;
}}
/>
{/if}
</div>
</div>
{/await}
<style>
.container {
display: flex;
flex-direction: column;
flex: 1;
}
.content {
flex: 1;
position: relative;
}
.top-panel {
display: flex;
background: var(--theme-bg-2);
}
.type {
font-weight: bold;
margin-right: 10px;
align-self: center;
}
.key-name {
flex-grow: 1;
display: flex;
}
.key-name :global(input) {
flex-grow: 1;
}
</style>

View File

@@ -124,7 +124,7 @@
}
export function isSqlEditor() {
return !driver?.dialect?.nosql;
return driver?.databaseEngineTypes?.includes('sql');
}
export function canKill() {
@@ -281,7 +281,7 @@
<ToolStripContainer>
<VerticalSplitter isSplitter={visibleResultTabs}>
<svelte:fragment slot="1">
{#if driver?.dialect?.nosql}
{#if driver?.databaseEngineTypes?.includes('document')}
<AceEditor
mode="javascript"
value={$editorState.value || ''}

View File

@@ -21,6 +21,7 @@ import * as CompareModelTab from './CompareModelTab.svelte';
import * as JsonTab from './JsonTab.svelte';
import * as ChangelogTab from './ChangelogTab.svelte';
import * as DiagramTab from './DiagramTab.svelte';
import * as DbKeyDetailTab from './DbKeyDetailTab.svelte';
export default {
TableDataTab,
@@ -46,4 +47,5 @@ export default {
JsonTab,
ChangelogTab,
DiagramTab,
DbKeyDetailTab,
};

View File

@@ -78,6 +78,12 @@ const databaseListLoader = ({ conid }) => ({
},
});
const databaseKeysLoader = ({ conid, database, root }) => ({
url: 'database-connections/load-keys',
params: { conid, database, root },
reloadTrigger: `database-keys-changed-${conid}-${database}`,
});
const serverVersionLoader = ({ conid }) => ({
url: 'server-connections/version',
params: { conid },
@@ -429,3 +435,10 @@ export function getAuthTypes(args) {
export function useAuthTypes(args) {
return useCore(authTypesLoader, args);
}
export function getDatabaseKeys(args) {
return getCore(databaseKeysLoader, args);
}
export function useDatabaseKeys(args) {
return useCore(databaseKeysLoader, args);
}

View File

@@ -39,7 +39,7 @@
];
function autodetect(selection) {
if (selection[0]?.engine?.dialect?.nosql) {
if (selection[0]?.engine?.databaseEngineTypes?.includes('document')) {
return 'jsonRow';
}
const value = selection.length == 1 ? selection[0].value : null;

View File

@@ -5,10 +5,13 @@
import ConnectionList from './ConnectionList.svelte';
import PinnedObjectsList from './PinnedObjectsList.svelte';
import SqlObjectListWrapper from './SqlObjectListWrapper.svelte';
import ErrorInfo from '../elements/ErrorInfo.svelte';
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
import WidgetColumnBar from './WidgetColumnBar.svelte';
import WidgetColumnBarItem from './WidgetColumnBarItem.svelte';
import SqlObjectList from './SqlObjectList.svelte';
import DbKeysTree from './DbKeysTree.svelte';
export let hidden = false;
@@ -16,6 +19,8 @@
$: connection = useConnectionInfo({ conid });
$: driver = findEngineDriver($connection, $extensions);
$: config = useConfig();
$: singleDatabase = $currentDatabase?.connection?.singleDatabase;
$: database = $currentDatabase?.name;
</script>
<WidgetColumnBar {hidden}>
@@ -34,11 +39,26 @@
>
<PinnedObjectsList />
</WidgetColumnBarItem>
<WidgetColumnBarItem
title={driver?.dialect?.nosql ? 'Collections' : 'Tables, views, functions'}
name="dbObjects"
storageName="dbObjectsWidget"
>
<SqlObjectListWrapper />
</WidgetColumnBarItem>
{#if conid && (database || singleDatabase)}
{#if driver?.databaseEngineTypes?.includes('sql') || driver?.databaseEngineTypes?.includes('document')}
<WidgetColumnBarItem
title={driver?.databaseEngineTypes?.includes('document') ? 'Collections' : 'Tables, views, functions'}
name="dbObjects"
storageName="dbObjectsWidget"
>
<SqlObjectList {conid} {database} />
</WidgetColumnBarItem>
{:else if driver?.databaseEngineTypes?.includes('keyvalue')}
<WidgetColumnBarItem title={'Keys'} name="dbObjects" storageName="dbObjectsWidget">
<DbKeysTree {conid} {database} />
</WidgetColumnBarItem>
{/if}
{:else}
<WidgetColumnBarItem title="Database content" name="dbObjects" storageName="dbObjectsWidget">
<WidgetsInnerContainer>
<ErrorInfo message="Database not selected" icon="img alert" />
</WidgetsInnerContainer>
</WidgetColumnBarItem>
{/if}
</WidgetColumnBar>

View File

@@ -0,0 +1,34 @@
<script lang="ts">
import AppObjectCore from '../appobj/AppObjectCore.svelte';
const SHOW_INCREMENT = 500;
import { useDatabaseKeys } from '../utility/metadataLoaders';
import DbKeysTreeNode from './DbKeysTreeNode.svelte';
export let conid;
export let database;
export let root;
export let indentLevel = 0;
let maxShowCount = SHOW_INCREMENT;
$: items = useDatabaseKeys({ conid, database, root });
</script>
{#each ($items || []).slice(0, maxShowCount) as item}
<DbKeysTreeNode {conid} {database} {root} {item} {indentLevel} />
{/each}
{#if ($items || []).length > maxShowCount}
<AppObjectCore
{indentLevel}
title="Show more..."
icon="icon dots-horizontal"
expandIcon="icon invisible-box"
on:click={() => {
maxShowCount += SHOW_INCREMENT;
}}
/>
{/if}

View File

@@ -0,0 +1,33 @@
<script lang="ts">
import CloseSearchButton from '../buttons/CloseSearchButton.svelte';
import InlineButton from '../buttons/InlineButton.svelte';
import runCommand from '../commands/runCommand';
import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte';
import SearchInput from '../elements/SearchInput.svelte';
import FontIcon from '../icons/FontIcon.svelte';
import DbKeysSubTree from './DbKeysSubTree.svelte';
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
export let conid;
export let database;
let filter;
function handleRefreshDatabase() {}
</script>
<SearchBoxWrapper>
<SearchInput placeholder="Search keys" bind:value={filter} />
<CloseSearchButton bind:filter />
<InlineButton on:click={() => runCommand('new.dbKey')} title="Add new key">
<FontIcon icon="icon plus-thick" />
</InlineButton>
<InlineButton on:click={handleRefreshDatabase} title="Refresh key list">
<FontIcon icon="icon refresh" />
</InlineButton>
</SearchBoxWrapper>
<WidgetsInnerContainer>
<DbKeysSubTree {conid} {database} root="" />
</WidgetsInnerContainer>

View File

@@ -0,0 +1,64 @@
<script lang="ts">
import { getIconForRedisType } from 'dbgate-tools';
import AppObjectCore from '../appobj/AppObjectCore.svelte';
import { plusExpandIcon } from '../icons/expandIcons';
import FontIcon from '../icons/FontIcon.svelte';
import { activeDbKeysStore } from '../stores';
import openNewTab from '../utility/openNewTab';
import DbKeysSubTree from './DbKeysSubTree.svelte';
export let conid;
export let database;
export let root;
export let item;
export let indentLevel = 0;
let isExpanded;
// $: console.log(item.text, indentLevel);
</script>
<AppObjectCore
icon={getIconForRedisType(item.type)}
title={item.text}
expandIcon={item.type == 'dir' ? plusExpandIcon(isExpanded) : 'icon invisible-box'}
on:expand={() => {
if (item.type == 'dir') {
isExpanded = !isExpanded;
}
}}
on:click={() => {
if (item.type == 'dir') {
isExpanded = !isExpanded;
} else {
openNewTab({
tabComponent: 'DbKeyDetailTab',
title: 'Key: ' + database,
props: {
isDefaultBrowser: true,
conid,
database,
},
});
$activeDbKeysStore = {
...$activeDbKeysStore,
[`${conid}:${database}`]: item.key,
};
}
}}
extInfo={item.count ? `(${item.count})` : null}
{indentLevel}
/>
<!-- <div on:click={() => (isExpanded = !isExpanded)}>
<FontIcon icon={} />
{item.text}
</div> -->
{#if isExpanded}
<DbKeysSubTree {conid} {database} root={item.root} indentLevel={indentLevel + 1} />
{/if}

View File

@@ -67,9 +67,10 @@
function createAddMenu() {
const res = [];
if (driver?.dialect?.nosql) {
if (driver?.databaseEngineTypes?.includes('document')) {
res.push({ command: 'new.collection' });
} else {
}
if (driver?.databaseEngineTypes?.includes('sql')) {
res.push({ command: 'new.table' });
}
if (driver)
@@ -100,11 +101,11 @@
/>
<div class="m-1" />
<InlineButton on:click={handleRefreshDatabase}>Refresh</InlineButton>
{#if !driver?.dialect?.nosql}
{#if driver?.databaseEngineTypes?.includes('sql')}
<div class="m-1" />
<InlineButton on:click={() => runCommand('new.table')}>New table</InlineButton>
{/if}
{#if driver?.dialect?.nosql}
{#if driver?.databaseEngineTypes?.includes('document')}
<div class="m-1" />
<InlineButton on:click={() => runCommand('new.collection')}>New collection</InlineButton>
{/if}

View File

@@ -1,19 +0,0 @@
<script lang="ts">
import _ from 'lodash';
import { currentDatabase } from '../stores';
import ErrorInfo from '../elements/ErrorInfo.svelte';
import SqlObjectList from './SqlObjectList.svelte';
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
$: conid = _.get($currentDatabase, 'connection._id');
$: singleDatabase = _.get($currentDatabase, 'connection.singleDatabase');
$: database = _.get($currentDatabase, 'name');
</script>
{#if conid && (database || singleDatabase)}
<SqlObjectList {conid} {database} />
{:else}
<WidgetsInnerContainer>
<ErrorInfo message="Database not selected" icon="img alert" />
</WidgetsInnerContainer>
{/if}