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

@@ -494,6 +494,20 @@ module.exports = {
return res.result || null;
},
saveRedisData_meta: true,
async saveRedisData({ conid, database, changeSet }, req) {
await testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid, database);
const res = await this.sendRequest(opened, { msgtype: 'saveRedisData', changeSet });
if (res.errorMessage) {
return {
errorMessage: res.errorMessage,
};
}
return res.result || null;
},
status_meta: true,
async status({ conid, database }, req) {
if (!conid) {

View File

@@ -368,6 +368,90 @@ async function handleSaveTableData({ msgid, changeSet }) {
}
}
async function handleSaveRedisData({ msgid, changeSet }) {
try {
const driver = requireEngineDriver(storedConnection);
if (!changeSet || !changeSet.changes || !Array.isArray(changeSet.changes)) {
throw new Error('Invalid changeSet structure');
}
for (const change of changeSet.changes) {
if (change.type === 'string') {
await driver.query(dbhan, `SET "${change.key}" "${change.value}"`);
} else if (change.type === 'json') {
await driver.query(dbhan, `JSON.SET "${change.key}" $ '${change.value.replace(/'/g, "\\'")}'`);
} else if (change.type === 'hash') {
if (change.updates && Array.isArray(change.updates)) {
for (const update of change.updates) {
await driver.query(dbhan, `HSET "${change.key}" "${update.key}" "${update.value}"`);
if (update.ttl !== undefined && update.ttl !== null && update.ttl !== -1) {
try {
await dbhan.client.call('HEXPIRE', change.key, update.ttl, 'FIELDS', 1, update.key);
} catch (e) {}
}
}
}
if (change.inserts && Array.isArray(change.inserts)) {
for (const insert of change.inserts) {
await driver.query(dbhan, `HSET "${change.key}" "${insert.key}" "${insert.value}"`);
if (insert.ttl !== undefined && insert.ttl !== null && insert.ttl !== -1) {
try {
await dbhan.client.call('HEXPIRE', change.key, insert.ttl, 'FIELDS', 1, insert.key);
} catch (e) {}
}
}
}
} else if (change.type === 'zset') {
if (change.updates && Array.isArray(change.updates)) {
for (const update of change.updates) {
await driver.query(dbhan, `ZADD "${change.key}" ${update.score} "${update.member}"`);
}
}
if (change.inserts && Array.isArray(change.inserts)) {
for (const insert of change.inserts) {
await driver.query(dbhan, `ZADD "${change.key}" ${insert.score} "${insert.member}"`);
}
}
} else if (change.type === 'list') {
if (change.updates && Array.isArray(change.updates)) {
for (const update of change.updates) {
await driver.query(dbhan, `LSET "${change.key}" ${update.index} "${update.value}"`);
}
}
if (change.inserts && Array.isArray(change.inserts)) {
for (const insert of change.inserts) {
await driver.query(dbhan, `RPUSH "${change.key}" "${insert.value}"`);
}
}
} else if (change.type === 'set') {
if (change.inserts && Array.isArray(change.inserts)) {
for (const insert of change.inserts) {
await driver.query(dbhan, `SADD "${change.key}" "${insert.value}"`);
}
}
} else if (change.type === 'stream') {
if (change.inserts && Array.isArray(change.inserts)) {
for (const insert of change.inserts) {
const streamId = insert.id === '*' || !insert.id ? '*' : insert.id;
await driver.query(dbhan, `XADD "${change.key}" ${streamId} value "${insert.value}"`);
}
}
}
}
process.send({ msgtype: 'response', msgid });
} catch (err) {
process.send({
msgtype: 'response',
msgid,
errorMessage: extractErrorMessage(err, 'Error saving Redis data'),
});
}
}
async function handleSqlPreview({ msgid, objects, options }) {
await waitStructure();
const driver = requireEngineDriver(storedConnection);
@@ -501,6 +585,7 @@ const messageHandlers = {
schemaList: handleSchemaList,
executeSessionQuery: handleExecuteSessionQuery,
evalJsonScript: handleEvalJsonScript,
saveRedisData: handleSaveRedisData,
// runCommand: handleRunCommand,
};

View File

@@ -0,0 +1,55 @@
export interface ChangeSetRedis_String {
key: string;
type: 'string';
value: string;
}
export interface ChangeSetRedis_JSON {
key: string;
type: 'json';
value: string;
}
export interface ChangeSetRedis_Hash {
key: string;
type: 'hash';
inserts: { key: string; value: string, ttl: number }[];
updates: { key: string; value: string, ttl: number }[];
deletes: string[];
}
export interface ChangeSetRedis_List {
key: string;
type: 'list';
inserts: { index: number; value: string }[];
updates: { index: number; value: string }[];
deletes: number[];
}
export interface ChangeSetRedis_Set {
key: string;
type: 'set';
inserts: string[];
deletes: string[];
}
export interface ChangeSetRedis_ZSet {
key: string;
type: 'zset';
inserts: { member: string; score: number }[];
updates: { member: string; score: number }[];
deletes: string[];
}
export type ChangeSetRedisType =
| ChangeSetRedis_String
| ChangeSetRedis_JSON
| ChangeSetRedis_Hash
| ChangeSetRedis_List
| ChangeSetRedis_Set
| ChangeSetRedis_ZSet;
export interface ChangeSetRedis {
changes: ChangeSetRedisType[];
}

View File

@@ -25,3 +25,4 @@ export * from './CustomGridDisplay';
export * from './ScriptDrivedDeployer';
export * from './chartDefinitions';
export * from './chartProcessor';
export * from './ChangeSetRedis';

View File

@@ -504,6 +504,7 @@ export function getIconForRedisType(type) {
case 'binary':
return 'img type-binary';
case 'ReJSON-RL':
case 'JSON':
return 'img type-rejson';
default:
return null;

View File

@@ -9,6 +9,8 @@
export let database;
export let keyInfo;
export let onChangeSelected;
export let modifyRow = null;
export let changeSetRedis = null;
let rows = [];
let cursor = 0;
@@ -73,6 +75,12 @@
onMount(() => {
loadNextRows();
});
$: displayRows = modifyRow ? rows.map(row => modifyRow(row)) : rows;
$: {
changeSetRedis;
displayRows = modifyRow ? rows.map(row => modifyRow(row)) : rows;
}
</script>
<ScrollableTableControl
@@ -87,7 +95,7 @@
header: column.name,
})),
]}
{rows}
rows={displayRows}
onLoadNext={isLoadedAll ? null : loadNextRows}
selectable
singleLineRow

View File

@@ -10,9 +10,9 @@
<div class="props">
{#each dbKeyFields as column}
<DbKeyValueDetail
value={item && item[column.name]}
value={item && item[column.name] != null ? String(item[column.name]) : ''}
columnTitle={_.startCase(column.name)}
onChangeValue={onChangeItem
onChangeValue={onChangeItem && !column.readOnly
? value => {
onChangeItem?.({
...item,
@@ -29,5 +29,8 @@
flex: 1;
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px;
overflow: auto;
}
</style>

View File

@@ -0,0 +1,53 @@
<script lang="ts">
import _ from 'lodash';
import DbKeyValueDetail from './DbKeyValueDetail.svelte';
export let dbKeyFields;
export let item;
export let onChangeItem = null;
export let keyColumn = null;
$: console.log('DbKeyItemEdit', { item, dbKeyFields, keyColumn, onChangeItem: !!onChangeItem });
function getValueAsString(value) {
if (value === null || value === undefined) return undefined;
if (typeof value === 'string') return value;
if (typeof value === 'number') return String(value);
return JSON.stringify(value);
}
</script>
<div class="props">
{#each dbKeyFields as column}
<div class="field-wrapper">
<DbKeyValueDetail
value={getValueAsString(item?.[column.name])}
columnTitle={_.startCase(column.name)}
onChangeValue={onChangeItem && column.name !== keyColumn
? value => {
onChangeItem?.({
...item,
[column.name]: value,
});
}
: null}
/>
</div>
{/each}
</div>
<style>
.props {
flex: 1;
gap: 10px;
padding: 10px;
overflow: hidden;
}
.field-wrapper {
flex: 1;
min-width: 0;
overflow: hidden;
max-height: 100px;
}
</style>

View File

@@ -12,6 +12,7 @@
export let columnTitle;
export let value;
export let onChangeValue = null;
export let keyType = null;
</script>
<div class="colnamewrap">
@@ -30,13 +31,16 @@
</div>
<div class="colvalue">
{#if display == 'text'}
<div class="editor-wrapper">
<AceEditor
readOnly={!onChangeValue}
{value}
value={value != null ? String(value) : ''}
mode={keyType === 'JSON' ? 'json' : undefined}
on:input={e => {
onChangeValue?.(e.detail);
}}
/>
</div>
{/if}
{#if display == 'json'}
<div class="outer">
@@ -64,6 +68,13 @@
justify-content: space-between;
}
.editor-wrapper {
flex: 1;
position: relative;
min-height: 60px;
max-height: 1000px;
}
.outer {
flex: 1;
position: relative;

View File

@@ -0,0 +1,173 @@
<script lang="ts">
import _ from 'lodash';
import TextField from '../forms/TextField.svelte';
import FormFieldTemplateLarge from '../forms/FormFieldTemplateLarge.svelte';
import FontIcon from '../icons/FontIcon.svelte';
export let dbKeyFields;
export let item;
export let onChangeItem = null;
export let keyColumn = null;
let records = [{ key: '', value: '', ttl: '' }];
let lastItem = null;
$: if (item !== lastItem) {
if (item?.records && Array.isArray(item.records)) {
records = [...item.records];
} else if (!item) {
records = [{ key: '', value: '', ttl: '' }];
}
lastItem = item;
}
$: console.log('DbKeyItemEdit', { item, dbKeyFields, keyColumn, onChangeItem: !!onChangeItem });
function getValueAsString(value) {
if (value === null || value === undefined) return '';
if (typeof value === 'string') return value;
if (typeof value === 'number') return String(value);
return JSON.stringify(value);
}
function handleFieldChange(index, fieldName, value) {
records = records.map((record, idx) =>
idx === index ? { ...record, [fieldName]: value } : record
);
if (onChangeItem && fieldName !== keyColumn) {
onChangeItem?.({
...item,
records: records,
});
}
}
function addRecord() {
records = [...records, { key: '', value: '', ttl: '' }];
if (onChangeItem) {
onChangeItem({
...item,
records: records,
});
}
}
</script>
<div class="container">
{#each records as record, index}
<div class="props flex">
<div class="field-wrapper col-3">
<FormFieldTemplateLarge label="Key" type="text" noMargin>
<TextField
value={record.key}
on:change={e => handleFieldChange(index, 'key', e.target.value)}
disabled={keyColumn === 'key'}
/>
</FormFieldTemplateLarge>
</div>
<div class="field-wrapper col-6">
<FormFieldTemplateLarge label="Value" type="text" noMargin>
<TextField
value={record.value}
on:change={e => handleFieldChange(index, 'value', e.target.value)}
disabled={keyColumn === 'value'}
/>
</FormFieldTemplateLarge>
</div>
<div class="field-wrapper col-2">
<FormFieldTemplateLarge label="TTL" type="text" noMargin>
<TextField
value={record.ttl}
on:change={e => handleFieldChange(index, 'ttl', e.target.value)}
disabled={keyColumn === 'ttl'}
/>
</FormFieldTemplateLarge>
</div>
<div class="delete-wrapper col-1">
<button class="delete-button" on:click={() => {
records = records.filter((_, idx) => idx !== index);
if (onChangeItem) {
onChangeItem({
...item,
records: records,
});
}
}}>
<FontIcon icon="icon delete" />
</button>
</div>
</div>
{/each}
<div class="add-button-wrapper">
<button class="add-button" on:click={addRecord}>
<FontIcon icon="icon add" />
</button>
</div>
</div>
<style>
.container {
flex: 1;
display: flex;
flex-direction: column;
gap: 10px;
overflow-y: auto;
}
.props {
display: flex;
flex-direction: row;
gap: 10px;
align-items: center;
}
.field-wrapper {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.delete-wrapper {
display: flex;
align-items: center;
justify-content: center;
margin-top: 10px;
}
.delete-button {
background: none;
border: none;
padding: 0;
cursor: pointer;
color: var(--theme-font-3);
transition: color 0.2s;
font-size: 24px;
display: flex;
align-items: center;
justify-content: center;
margin-top: 10px;
}
.add-button-wrapper {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.add-button {
background: none;
border: none;
padding: 0;
cursor: pointer;
color: var(--theme-font-3);
transition: color 0.2s;
font-size: 24px;
}
.add-button:hover {
color: var(--theme-font-hover);
}
</style>

View File

@@ -0,0 +1,151 @@
<script lang="ts">
import _ from 'lodash';
import TextField from '../forms/TextField.svelte';
import FormFieldTemplateLarge from '../forms/FormFieldTemplateLarge.svelte';
import FontIcon from '../icons/FontIcon.svelte';
export let dbKeyFields;
export let item;
export let onChangeItem = null;
export let keyColumn = null;
let records = [{ value: '' }];
let lastItem = null;
$: if (item !== lastItem) {
if (item?.records && Array.isArray(item.records)) {
records = [...item.records];
} else if (!item) {
records = [{ value: '' }];
}
lastItem = item;
}
$: console.log('DbKeyValueListEdit', { item, dbKeyFields, keyColumn, onChangeItem: !!onChangeItem });
function handleFieldChange(index, fieldName, value) {
records = records.map((record, idx) =>
idx === index ? { ...record, [fieldName]: value } : record
);
if (onChangeItem && fieldName !== keyColumn) {
onChangeItem?.({
...item,
records: records,
});
}
}
function addRecord() {
records = [...records, { value: '' }];
if (onChangeItem) {
onChangeItem({
...item,
records: records,
});
}
}
</script>
<div class="container">
{#each records as record, index}
<div class="props flex">
<div class="field-wrapper col-11">
<FormFieldTemplateLarge label="Value" type="text" noMargin>
<TextField
value={record.value}
on:change={e => handleFieldChange(index, 'value', e.target.value)}
disabled={keyColumn === 'value'}
/>
</FormFieldTemplateLarge>
</div>
<div class="delete-wrapper col-1">
<button class="delete-button" on:click={() => {
records = records.filter((_, idx) => idx !== index);
if (onChangeItem) {
onChangeItem({
...item,
records: records,
});
}
}}>
<FontIcon icon="icon delete" />
</button>
</div>
</div>
{/each}
<div class="add-button-wrapper">
<button class="add-button" on:click={addRecord}>
<FontIcon icon="icon add" />
</button>
</div>
</div>
<style>
.container {
flex: 1;
display: flex;
flex-direction: column;
gap: 10px;
overflow-y: auto;
}
.props {
display: flex;
flex-direction: row;
gap: 10px;
}
.field-wrapper {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.delete-wrapper {
display: flex;
align-items: center;
justify-content: center;
margin-top: 10px;
}
.delete-button {
background: none;
border: none;
padding: 0;
cursor: pointer;
color: var(--theme-font-3);
transition: color 0.2s;
font-size: 24px;
display: flex;
align-items: center;
justify-content: center;
margin-top: 10px;
}
.delete-button:hover {
color: var(--theme-font-hover);
}
.add-button-wrapper {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.add-button {
background: none;
border: none;
padding: 0;
cursor: pointer;
color: var(--theme-font-3);
transition: color 0.2s;
font-size: 24px;
}
.add-button:hover {
color: var(--theme-font-hover);
}
</style>

View File

@@ -0,0 +1,151 @@
<script lang="ts">
import _ from 'lodash';
import TextField from '../forms/TextField.svelte';
import FormFieldTemplateLarge from '../forms/FormFieldTemplateLarge.svelte';
import FontIcon from '../icons/FontIcon.svelte';
export let dbKeyFields;
export let item;
export let onChangeItem = null;
export let keyColumn = null;
let records = [{ value: '' }];
let lastItem = null;
$: if (item !== lastItem) {
if (item?.records && Array.isArray(item.records)) {
records = [...item.records];
} else if (!item) {
records = [{ value: '' }];
}
lastItem = item;
}
$: console.log('DbKeyValueSetEdit', { item, dbKeyFields, keyColumn, onChangeItem: !!onChangeItem });
function handleFieldChange(index, fieldName, value) {
records = records.map((record, idx) =>
idx === index ? { ...record, [fieldName]: value } : record
);
if (onChangeItem && fieldName !== keyColumn) {
onChangeItem?.({
...item,
records: records,
});
}
}
function addRecord() {
records = [...records, { value: '' }];
if (onChangeItem) {
onChangeItem({
...item,
records: records,
});
}
}
</script>
<div class="container">
{#each records as record, index}
<div class="props flex">
<div class="field-wrapper col-11">
<FormFieldTemplateLarge label="Value" type="text" noMargin>
<TextField
value={record.value}
on:change={e => handleFieldChange(index, 'value', e.target.value)}
disabled={keyColumn === 'value'}
/>
</FormFieldTemplateLarge>
</div>
<div class="delete-wrapper col-1">
<button class="delete-button" on:click={() => {
records = records.filter((_, idx) => idx !== index);
if (onChangeItem) {
onChangeItem({
...item,
records: records,
});
}
}}>
<FontIcon icon="icon delete" />
</button>
</div>
</div>
{/each}
<div class="add-button-wrapper">
<button class="add-button" on:click={addRecord}>
<FontIcon icon="icon add" />
</button>
</div>
</div>
<style>
.container {
flex: 1;
display: flex;
flex-direction: column;
gap: 10px;
overflow-y: auto;
}
.props {
display: flex;
flex-direction: row;
gap: 10px;
}
.field-wrapper {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.delete-wrapper {
display: flex;
align-items: center;
justify-content: center;
margin-top: 10px;
}
.delete-button {
background: none;
border: none;
padding: 0;
cursor: pointer;
color: var(--theme-font-3);
transition: color 0.2s;
font-size: 24px;
display: flex;
align-items: center;
justify-content: center;
margin-top: 10px;
}
.delete-button:hover {
color: var(--theme-font-hover);
}
.add-button-wrapper {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.add-button {
background: none;
border: none;
padding: 0;
cursor: pointer;
color: var(--theme-font-3);
transition: color 0.2s;
font-size: 24px;
}
.add-button:hover {
color: var(--theme-font-hover);
}
</style>

View File

@@ -0,0 +1,166 @@
<script lang="ts">
import _ from 'lodash';
import TextField from '../forms/TextField.svelte';
import FormFieldTemplateLarge from '../forms/FormFieldTemplateLarge.svelte';
import FontIcon from '../icons/FontIcon.svelte';
export let dbKeyFields;
export let item;
export let onChangeItem = null;
export let keyColumn = null;
let records = [{ id: '', value: '' }];
let lastItem = null;
$: if (item !== lastItem) {
if (item?.records && Array.isArray(item.records)) {
records = [...item.records];
} else if (!item) {
records = [{ id: '', value: '' }];
}
lastItem = item;
}
$: console.log('DbKeyValueStreamEdit', { item, dbKeyFields, keyColumn, onChangeItem: !!onChangeItem });
function getValueAsString(value) {
if (value === null || value === undefined) return '';
if (typeof value === 'string') return value;
if (typeof value === 'number') return String(value);
return JSON.stringify(value);
}
function handleFieldChange(index, fieldName, value) {
records = records.map((record, idx) => (idx === index ? { ...record, [fieldName]: value } : record));
if (onChangeItem && fieldName !== keyColumn) {
onChangeItem?.({
...item,
records: records,
});
}
}
function addRecord() {
records = [...records, { id: '', value: '' }];
if (onChangeItem) {
onChangeItem({
...item,
records: records,
});
}
}
</script>
<div class="container">
{#each records as record, index}
<div class="props flex">
<div class="field-wrapper col-3">
<FormFieldTemplateLarge label="ID" type="text" noMargin>
<TextField
value={record.id}
on:change={e => handleFieldChange(index, 'id', e.target.value)}
disabled={keyColumn === 'id'}
placeholder="* for auto"
/>
</FormFieldTemplateLarge>
</div>
<div class="field-wrapper col-8">
<FormFieldTemplateLarge label="Value" type="text" noMargin>
<TextField
value={record.value}
on:change={e => handleFieldChange(index, 'value', e.target.value)}
disabled={keyColumn === 'value'}
/>
</FormFieldTemplateLarge>
</div>
<div class="delete-wrapper col-1">
<button class="delete-button" on:click={() => {
records = records.filter((_, idx) => idx !== index);
if (onChangeItem) {
onChangeItem({
...item,
records: records,
});
}
}}>
<FontIcon icon="icon delete" />
</button>
</div>
</div>
{/each}
<div class="add-button-wrapper">
<button class="add-button" on:click={addRecord}>
<FontIcon icon="icon add" />
</button>
</div>
</div>
<style>
.container {
flex: 1;
display: flex;
flex-direction: column;
gap: 10px;
overflow-y: auto;
}
.props {
display: flex;
flex-direction: row;
gap: 10px;
}
.field-wrapper {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.delete-wrapper {
display: flex;
align-items: center;
justify-content: center;
margin-top: 10px;
}
.delete-button {
background: none;
border: none;
padding: 0;
cursor: pointer;
color: var(--theme-font-3);
transition: color 0.2s;
font-size: 24px;
display: flex;
align-items: center;
justify-content: center;
margin-top: 10px;
}
.delete-button:hover {
color: var(--theme-font-hover);
}
.add-button-wrapper {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.add-button {
background: none;
border: none;
padding: 0;
cursor: pointer;
color: var(--theme-font-3);
transition: color 0.2s;
font-size: 24px;
}
.add-button:hover {
color: var(--theme-font-hover);
}
</style>

View File

@@ -0,0 +1,165 @@
<script lang="ts">
import _ from 'lodash';
import TextField from '../forms/TextField.svelte';
import FormFieldTemplateLarge from '../forms/FormFieldTemplateLarge.svelte';
import FontIcon from '../icons/FontIcon.svelte';
export let dbKeyFields;
export let item;
export let onChangeItem = null;
export let keyColumn = null;
let records = [{ member: '', score: '' }];
let lastItem = null;
$: if (item !== lastItem) {
if (item?.records && Array.isArray(item.records)) {
records = [...item.records];
} else if (!item) {
records = [{ member: '', score: '' }];
}
lastItem = item;
}
$: console.log('DbKeyValueZSetEdit', { item, dbKeyFields, keyColumn, onChangeItem: !!onChangeItem });
function getValueAsString(value) {
if (value === null || value === undefined) return '';
if (typeof value === 'string') return value;
if (typeof value === 'number') return String(value);
return JSON.stringify(value);
}
function handleFieldChange(index, fieldName, value) {
records = records.map((record, idx) => (idx === index ? { ...record, [fieldName]: value } : record));
if (onChangeItem && fieldName !== keyColumn) {
onChangeItem?.({
...item,
records: records,
});
}
}
function addRecord() {
records = [...records, { member: '', score: '' }];
if (onChangeItem) {
onChangeItem({
...item,
records: records,
});
}
}
</script>
<div class="container">
{#each records as record, index}
<div class="props flex">
<div class="field-wrapper col-7">
<FormFieldTemplateLarge label="Member" type="text" noMargin>
<TextField
value={record.member}
on:change={e => handleFieldChange(index, 'member', e.target.value)}
disabled={keyColumn === 'member'}
/>
</FormFieldTemplateLarge>
</div>
<div class="field-wrapper col-4">
<FormFieldTemplateLarge label="Score" type="text" noMargin>
<TextField
value={record.score}
on:change={e => handleFieldChange(index, 'score', e.target.value)}
disabled={keyColumn === 'score'}
/>
</FormFieldTemplateLarge>
</div>
<div class="delete-wrapper col-1">
<button class="delete-button" on:click={() => {
records = records.filter((_, idx) => idx !== index);
if (onChangeItem) {
onChangeItem({
...item,
records: records,
});
}
}}>
<FontIcon icon="icon delete" />
</button>
</div>
</div>
{/each}
<div class="add-button-wrapper">
<button class="add-button" on:click={addRecord}>
<FontIcon icon="icon add" />
</button>
</div>
</div>
<style>
.container {
flex: 1;
display: flex;
flex-direction: column;
gap: 10px;
overflow-y: auto;
}
.props {
display: flex;
flex-direction: row;
gap: 10px;
}
.field-wrapper {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.delete-wrapper {
display: flex;
align-items: center;
justify-content: center;
margin-top: 10px;
}
.delete-button {
background: none;
border: none;
padding: 0;
cursor: pointer;
color: var(--theme-font-3);
transition: color 0.2s;
font-size: 24px;
display: flex;
align-items: center;
justify-content: center;
margin-top: 10px;
}
.delete-button:hover {
color: var(--theme-font-hover);
}
.add-button-wrapper {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.add-button {
background: none;
border: none;
padding: 0;
cursor: pointer;
color: var(--theme-font-3);
transition: color 0.2s;
font-size: 24px;
}
.add-button:hover {
color: var(--theme-font-hover);
}
</style>

View File

@@ -337,7 +337,7 @@
'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',
'img type-rejson': 'mdi mdi-code-json color-icon-blue',
'img keydb': 'mdi mdi-key color-icon-blue',
'img replicator': 'mdi mdi-content-duplicate color-icon-green',

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import FormStyledButton from '../buttons/FormStyledButton.svelte';
import DbKeyItemDetail from '../dbkeyvalue/DbKeyItemDetail.svelte';
import DbKeyValueHashEdit from '../dbkeyvalue/DbKeyValueHashEdit.svelte';
import FormFieldTemplateLarge from '../forms/FormFieldTemplateLarge.svelte';
import FormProvider from '../forms/FormProvider.svelte';
@@ -53,6 +54,15 @@
/>
</FormFieldTemplateLarge>
{#if type === 'hash'}
<DbKeyValueHashEdit
dbKeyFields={driver.supportedKeyTypes.find(x => x.name == type).dbKeyFields}
{item}
onChangeItem={value => {
item = value;
}}
/>
{:else}
<DbKeyItemDetail
dbKeyFields={driver.supportedKeyTypes.find(x => x.name == type).dbKeyFields}
{item}
@@ -60,6 +70,7 @@
item = value;
}}
/>
{/if}
</div>
<svelte:fragment slot="footer">

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;
@@ -45,17 +55,60 @@
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,66 +138,169 @@
});
}
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}
<ToolStripContainer>
<div class="container">
<div class="top-panel">
<div class="type">
<FontIcon icon={getIconForRedisType(keyInfo.type)} padRight />
{keyInfo.type}
{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)} />
{#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">
@@ -155,28 +311,200 @@
{conid}
{database}
{keyInfo}
{changeSetRedis}
onChangeSelected={row => {
currentRow = row;
showAddForm = false;
}}
modifyRow={row => getDisplayRow(row, keyInfo)}
/>
</svelte:fragment>
<svelte:fragment slot="2">
<DbKeyItemDetail dbKeyFields={keyInfo.keyType.dbKeyFields} item={currentRow} />
{#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={editedValue || keyInfo.value}
value={getDisplayValue(keyInfo)}
keyType={keyInfo.type}
onChangeValue={value => {
editedValue = value;
if (keyInfo.type === 'string') {
addOrUpdateChange({
key: key,
type: 'string',
value: value,
});
} else if (keyInfo.type === 'JSON') {
addOrUpdateChange({
key: key,
type: 'json',
value: value,
});
}
}}
/>
</div>
{/if}
</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,

View File

@@ -51,25 +51,48 @@
function handleAddKey() {
const connection = $currentDatabase?.connection;
const database = $currentDatabase?.name;
const driver = findEngineDriver(connection, getExtensions());
const focusedKey = $focusedTreeDbKey;
showModal(AddDbKeyModal, {
conid: connection._id,
let initialKeyName = '';
if (focusedKey) {
if (focusedKey.type === 'dir' && focusedKey.key) {
initialKeyName = focusedKey.key + treeKeySeparator;
} else if (focusedKey.key) {
const lastSeparatorIndex = focusedKey.key.lastIndexOf(treeKeySeparator);
if (lastSeparatorIndex !== -1) {
initialKeyName = focusedKey.key.substring(0, lastSeparatorIndex + 1);
}
}
}
openNewTab({
tabComponent: 'DbKeyTab',
title: 'Add key',
icon: 'img keydb',
props: {
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();
initialKeyName,
},
});
// 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 =

View File

@@ -459,6 +459,15 @@ const driver = {
case 'string':
res.value = await dbhan.client.get(key);
break;
case 'ReJSON-RL':
res.type = 'JSON';
try {
const jsonData = await dbhan.client.call('JSON.GET', key);
res.value = JSON.stringify(JSON.parse(jsonData), null, 2);
} catch (e) {
res.value = '';
}
break;
// case 'list':
// res.tableColumns = [{ name: 'value' }];
// res.addMethod = 'rpush';
@@ -495,6 +504,10 @@ const driver = {
switch (method) {
case 'mdel':
return await this.deleteBranch(dbhan, args[0]);
case 'zadd':
return await dbhan.client.zadd(args[0], args[2], args[1]);
case 'json.set':
return await dbhan.client.call('JSON.SET', args[0], '$', args[1]);
case 'xaddjson':
let json;
try {
@@ -528,14 +541,28 @@ const driver = {
const res = await dbhan.client.zscan(key, cursor, 'COUNT', count);
return {
cursor: parseInt(res[0]),
items: _.chunk(res[1], 2).map((item) => ({ value: item[0], score: item[1] })),
items: _.chunk(res[1], 2).map((item) => ({ member: item[0], score: item[1] })),
};
}
case 'hash': {
const res = await dbhan.client.hscan(key, cursor, 'COUNT', count);
const fields = _.chunk(res[1], 2);
// Get TTL for each hash field (Redis 7.4+)
const items = await Promise.all(
fields.map(async ([fieldKey, fieldValue]) => {
try {
const ttl = await dbhan.client.call('HTTL', key, 'FIELDS', 1, fieldKey);
return { key: fieldKey, value: fieldValue, TTL: ttl && ttl[0] !== undefined ? ttl[0] : null };
} catch (e) {
return { key: fieldKey, value: fieldValue };
}
})
);
return {
cursor: parseInt(res[0]),
items: _.chunk(res[1], 2).map((item) => ({ key: item[0], value: item[1] })),
items,
};
}
case 'stream': {

View File

@@ -49,7 +49,7 @@ const driver = {
{
name: 'set',
label: 'Set',
dbKeyFields: [{ name: 'value' }],
dbKeyFields: [{ name: 'value', readOnly: true }],
keyColumn: 'value',
addMethod: 'sadd',
showItemList: true,
@@ -57,15 +57,15 @@ const driver = {
{
name: 'zset',
label: 'Sorted Set',
dbKeyFields: [{ name: 'score' }, { name: 'value' }],
keyColumn: 'value',
dbKeyFields: [{ name: 'member', readOnly: true }, { name: 'score' }],
keyColumn: 'member',
addMethod: 'zadd',
showItemList: true,
},
{
name: 'hash',
label: 'Hash',
dbKeyFields: [{ name: 'key' }, { name: 'value' }],
dbKeyFields: [{ name: 'key', readOnly: true }, { name: 'value' }, { name: 'TTL' }],
keyColumn: 'key',
addMethod: 'hset',
showItemList: true,
@@ -73,11 +73,17 @@ const driver = {
{
name: 'stream',
label: 'Stream',
dbKeyFields: [{ name: 'id' }, { name: 'value' }],
dbKeyFields: [{ name: 'id', readOnly: true }, { name: 'value', readOnly: true }],
keyColumn: 'id',
addMethod: 'xaddjson',
showItemList: true,
},
{
name: 'json',
label: 'JSON',
dbKeyFields: [{ name: 'value' }],
addMethod: 'json.set',
}
],
showConnectionField: (field, values) => {