From 2d0e8f5445e33cdb07712c8a4687b1c3d350fd1f Mon Sep 17 00:00:00 2001
From: tiny-craft <137850705+tiny-craft@users.noreply.github.com>
Date: Tue, 25 Jul 2023 19:12:50 +0800
Subject: [PATCH] feat: add stream type support
---
backend/services/connection_service.go | 83 +++++--
backend/types/{zset.go => redis_wrapper.go} | 5 +
.../components/common/EditableTableColumn.vue | 3 +-
.../src/components/content/ContentPane.vue | 6 +-
.../content_value/ContentToolbar.vue | 17 +-
.../content_value/ContentValueStream.vue | 202 ++++++++++++++++++
.../components/dialogs/AddFieldsDialog.vue | 30 ++-
.../src/components/dialogs/NewKeyDialog.vue | 5 +-
.../src/components/new_value/AddZSetValue.vue | 5 +-
.../components/new_value/NewStreamValue.vue | 62 ++++++
frontend/src/consts/support_redis_type.js | 11 +-
frontend/src/stores/connections.js | 50 +++++
12 files changed, 456 insertions(+), 23 deletions(-)
rename backend/types/{zset.go => redis_wrapper.go} (50%)
create mode 100644 frontend/src/components/content_value/ContentValueStream.vue
create mode 100644 frontend/src/components/new_value/NewStreamValue.vue
diff --git a/backend/services/connection_service.go b/backend/services/connection_service.go
index 7ba560a..cb08ba1 100644
--- a/backend/services/connection_service.go
+++ b/backend/services/connection_service.go
@@ -497,6 +497,21 @@ func (c *connectionService) GetKeyValue(connName string, db int, key string) (re
}
}
value = items
+ case "stream":
+ var msgs []redis.XMessage
+ items := []types.StreamItem{}
+ msgs, err = rdb.XRevRange(ctx, key, "+", "-").Result()
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+ for _, msg := range msgs {
+ items = append(items, types.StreamItem{
+ ID: msg.ID,
+ Value: msg.Values,
+ })
+ }
+ value = items
}
if err != nil {
resp.Msg = err.Error()
@@ -538,13 +553,10 @@ func (c *connectionService) SetKeyValue(connName string, db int, key, keyType st
resp.Msg = "invalid list value"
return
} else {
- _, err = rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error {
- pipe.LPush(ctx, key, strs...)
- if expiration > 0 {
- pipe.Expire(ctx, key, expiration)
- }
- return nil
- })
+ err = rdb.LPush(ctx, key, strs...).Err()
+ if err == nil && expiration > 0 {
+ rdb.Expire(ctx, key, expiration)
+ }
}
case "hash":
if strs, ok := value.([]any); !ok {
@@ -552,11 +564,7 @@ func (c *connectionService) SetKeyValue(connName string, db int, key, keyType st
return
} else {
if len(strs) > 1 {
- kvs := map[string]any{}
- for i := 0; i < len(strs); i += 2 {
- kvs[strs[i].(string)] = strs[i+1]
- }
- err = rdb.HSet(ctx, key, kvs).Err()
+ err = rdb.HSet(ctx, key, strs).Err()
if err == nil && expiration > 0 {
rdb.Expire(ctx, key, expiration)
}
@@ -594,6 +602,22 @@ func (c *connectionService) SetKeyValue(connName string, db int, key, keyType st
}
}
}
+ case "stream":
+ if strs, ok := value.([]any); !ok {
+ resp.Msg = "invalid stream value"
+ return
+ } else {
+ if len(strs) > 2 {
+ err = rdb.XAdd(ctx, &redis.XAddArgs{
+ Stream: key,
+ ID: strs[0].(string),
+ Values: strs[1:],
+ }).Err()
+ if err == nil && expiration > 0 {
+ rdb.Expire(ctx, key, expiration)
+ }
+ }
+ }
}
if err != nil {
@@ -886,6 +910,41 @@ func (c *connectionService) AddZSetValue(connName string, db int, key string, ac
return
}
+// AddStreamValue add stream field
+func (c *connectionService) AddStreamValue(connName string, db int, key, ID string, fieldItems []any) (resp types.JSResp) {
+ rdb, ctx, err := c.getRedisClient(connName, db)
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ _, err = rdb.XAdd(ctx, &redis.XAddArgs{
+ Stream: key,
+ ID: ID,
+ Values: fieldItems,
+ }).Result()
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ resp.Success = true
+ return
+}
+
+// RemoveStreamValues remove stream values by id
+func (c *connectionService) RemoveStreamValues(connName string, db int, key string, IDs []string) (resp types.JSResp) {
+ rdb, ctx, err := c.getRedisClient(connName, db)
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ _, err = rdb.XDel(ctx, key, IDs...).Result()
+ resp.Success = true
+ return
+}
+
// SetKeyTTL set ttl of key
func (c *connectionService) SetKeyTTL(connName string, db int, key string, ttl int64) (resp types.JSResp) {
rdb, ctx, err := c.getRedisClient(connName, db)
diff --git a/backend/types/zset.go b/backend/types/redis_wrapper.go
similarity index 50%
rename from backend/types/zset.go
rename to backend/types/redis_wrapper.go
index f87cd86..1419136 100644
--- a/backend/types/zset.go
+++ b/backend/types/redis_wrapper.go
@@ -4,3 +4,8 @@ type ZSetItem struct {
Value string `json:"value"`
Score float64 `json:"score"`
}
+
+type StreamItem struct {
+ ID string `json:"id"`
+ Value map[string]any `json:"value"`
+}
diff --git a/frontend/src/components/common/EditableTableColumn.vue b/frontend/src/components/common/EditableTableColumn.vue
index 3c7b88a..11d3d41 100644
--- a/frontend/src/components/common/EditableTableColumn.vue
+++ b/frontend/src/components/common/EditableTableColumn.vue
@@ -8,6 +8,7 @@ import Save from '../icons/Save.vue'
const props = defineProps({
bindKey: String,
editing: Boolean,
+ readonly: Boolean,
})
const emit = defineEmits(['edit', 'delete', 'save', 'cancel'])
@@ -20,7 +21,7 @@ const emit = defineEmits(['edit', 'delete', 'save', 'cancel'])
-
+
diff --git a/frontend/src/components/content/ContentPane.vue b/frontend/src/components/content/ContentPane.vue
index 1b03fca..1cc07dc 100644
--- a/frontend/src/components/content/ContentPane.vue
+++ b/frontend/src/components/content/ContentPane.vue
@@ -13,6 +13,7 @@ import useConnectionStore from '../../stores/connections.js'
import { useI18n } from 'vue-i18n'
import { useConfirmDialog } from '../../utils/confirm_dialog.js'
import ContentServerStatus from '../content_value/ContentServerStatus.vue'
+import ContentValueStream from '../content_value/ContentValueStream.vue'
const serverInfo = ref({})
const autoRefresh = ref(false)
@@ -62,6 +63,7 @@ const valueComponents = {
[types.LIST]: ContentValueList,
[types.SET]: ContentValueSet,
[types.ZSET]: ContentValueZset,
+ [types.STREAM]: ContentValueStream,
}
const dialog = useDialog()
@@ -71,7 +73,7 @@ const tab = computed(() =>
map(tabStore.tabs, (item) => ({
key: item.name,
label: item.title,
- }))
+ })),
)
watch(
@@ -80,7 +82,7 @@ watch(
if (nav === 'browser') {
refreshInfo()
}
- }
+ },
)
const tabContent = computed(() => {
diff --git a/frontend/src/components/content_value/ContentToolbar.vue b/frontend/src/components/content_value/ContentToolbar.vue
index ce75739..88ce51a 100644
--- a/frontend/src/components/content_value/ContentToolbar.vue
+++ b/frontend/src/components/content_value/ContentToolbar.vue
@@ -11,6 +11,8 @@ import { useMessage } from 'naive-ui'
import IconButton from '../common/IconButton.vue'
import useConnectionStore from '../../stores/connections.js'
import { useConfirmDialog } from '../../utils/confirm_dialog.js'
+import Copy from '../icons/Copy.vue'
+import { ClipboardSetText } from '../../../wailsjs/runtime/runtime.js'
const props = defineProps({
server: String,
@@ -38,6 +40,18 @@ const onReloadKey = () => {
connectionStore.loadKeyValue(props.server, props.db, props.keyPath)
}
+const onCopyKey = () => {
+ ClipboardSetText(props.keyPath)
+ .then((succ) => {
+ if (succ) {
+ message.success(i18n.t('copy_succ'))
+ }
+ })
+ .catch((e) => {
+ message.error(e.message)
+ })
+}
+
const confirmDialog = useConfirmDialog()
const onDeleteKey = () => {
confirmDialog.warning(i18n.t('remove_tip', { name: props.keyPath }), () => {
@@ -53,12 +67,13 @@ const onDeleteKey = () => {
-
+
+
diff --git a/frontend/src/components/content_value/ContentValueStream.vue b/frontend/src/components/content_value/ContentValueStream.vue
new file mode 100644
index 0000000..ac7a37e
--- /dev/null
+++ b/frontend/src/components/content_value/ContentValueStream.vue
@@ -0,0 +1,202 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('add_row') }}
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/dialogs/AddFieldsDialog.vue b/frontend/src/components/dialogs/AddFieldsDialog.vue
index 547484f..16406ec 100644
--- a/frontend/src/components/dialogs/AddFieldsDialog.vue
+++ b/frontend/src/components/dialogs/AddFieldsDialog.vue
@@ -10,6 +10,8 @@ import AddListValue from '../new_value/AddListValue.vue'
import AddHashValue from '../new_value/AddHashValue.vue'
import AddZSetValue from '../new_value/AddZSetValue.vue'
import useConnectionStore from '../../stores/connections.js'
+import NewStreamValue from '../new_value/NewStreamValue.vue'
+import { size, slice } from 'lodash'
const i18n = useI18n()
const newForm = reactive({
@@ -29,6 +31,7 @@ const addValueComponent = {
[types.LIST]: AddListValue,
[types.SET]: NewSetValue,
[types.ZSET]: AddZSetValue,
+ [types.STREAM]: NewStreamValue,
}
const defaultValue = {
[types.STRING]: '',
@@ -36,6 +39,7 @@ const defaultValue = {
[types.LIST]: [],
[types.SET]: [],
[types.ZSET]: [],
+ [types.STREAM]: ['*'],
}
/**
@@ -52,6 +56,8 @@ const title = computed(() => {
return i18n.t('new_field')
case types.ZSET:
return i18n.t('new_field')
+ case types.STREAM:
+ return i18n.t('new_field')
}
return ''
})
@@ -69,7 +75,7 @@ watch(
newForm.opType = 0
newForm.value = null
}
- }
+ },
)
const connectionStore = useConnectionStore()
@@ -143,6 +149,28 @@ const onAdd = async () => {
}
}
break
+
+ case types.STREAM:
+ {
+ if (size(value) > 2) {
+ const { success, msg } = await connectionStore.addStreamValue(
+ server,
+ db,
+ key,
+ value[0],
+ slice(value, 1),
+ )
+ if (success) {
+ if (newForm.reload) {
+ connectionStore.loadKeyValue(server, db, key).then(() => {})
+ }
+ message.success(i18n.t('handle_succ'))
+ } else {
+ message.error(msg)
+ }
+ }
+ }
+ break
}
dialogStore.closeAddFieldsDialog()
} catch (e) {
diff --git a/frontend/src/components/dialogs/NewKeyDialog.vue b/frontend/src/components/dialogs/NewKeyDialog.vue
index 45d1cc9..aa43041 100644
--- a/frontend/src/components/dialogs/NewKeyDialog.vue
+++ b/frontend/src/components/dialogs/NewKeyDialog.vue
@@ -12,6 +12,7 @@ import { useI18n } from 'vue-i18n'
import useConnectionStore from '../../stores/connections.js'
import { NSpace, useMessage } from 'naive-ui'
import useTabStore from '../../stores/tab.js'
+import NewStreamValue from '../new_value/NewStreamValue.vue'
const i18n = useI18n()
const newForm = reactive({
@@ -52,6 +53,7 @@ const addValueComponent = {
[types.LIST]: NewListValue,
[types.SET]: NewSetValue,
[types.ZSET]: NewZSetValue,
+ [types.STREAM]: NewStreamValue,
}
const defaultValue = {
[types.STRING]: '',
@@ -59,6 +61,7 @@ const defaultValue = {
[types.LIST]: [],
[types.SET]: [],
[types.ZSET]: [],
+ [types.STREAM]: [],
}
const dialogStore = useDialog()
@@ -165,7 +168,7 @@ const onClose = () => {
-
+
diff --git a/frontend/src/components/new_value/AddZSetValue.vue b/frontend/src/components/new_value/AddZSetValue.vue
index 5d84aaf..48ea466 100644
--- a/frontend/src/components/new_value/AddZSetValue.vue
+++ b/frontend/src/components/new_value/AddZSetValue.vue
@@ -1,5 +1,5 @@
+
+
+
+
+
+
+
+
+ remove(index)" />
+ create(index)" />
+
+
+
+
+
+
diff --git a/frontend/src/consts/support_redis_type.js b/frontend/src/consts/support_redis_type.js
index 4993581..99e60fc 100644
--- a/frontend/src/consts/support_redis_type.js
+++ b/frontend/src/consts/support_redis_type.js
@@ -4,14 +4,16 @@ export const types = {
LIST: 'LIST',
SET: 'SET',
ZSET: 'ZSET',
+ STREAM: 'STREAM',
}
export const typesColor = {
[types.STRING]: '#8256DC',
- [types.HASH]: '#2983ED',
- [types.LIST]: '#26A15E',
- [types.SET]: '#EE9F33',
- [types.ZSET]: '#CE3352',
+ [types.HASH]: '#0171F5',
+ [types.LIST]: '#23C338',
+ [types.SET]: '#F29E33',
+ [types.ZSET]: '#F53227',
+ [types.STREAM]: '#F5C201',
}
export const typesBgColor = {
@@ -20,6 +22,7 @@ export const typesBgColor = {
[types.LIST]: '#E3F3EB',
[types.SET]: '#FDF1DF',
[types.ZSET]: '#FAEAED',
+ [types.STREAM]: '#FFF8DF',
}
// export const typesName = Object.fromEntries(Object.entries(types).map(([key, value]) => [key, value.name]))
diff --git a/frontend/src/stores/connections.js b/frontend/src/stores/connections.js
index 4f2f807..a4ec33e 100644
--- a/frontend/src/stores/connections.js
+++ b/frontend/src/stores/connections.js
@@ -3,6 +3,7 @@ import { endsWith, get, isEmpty, join, remove, size, slice, sortedIndexBy, split
import {
AddHashField,
AddListItem,
+ AddStreamValue,
AddZSetValue,
CloseConnection,
CreateGroup,
@@ -15,6 +16,7 @@ import {
ListConnection,
OpenConnection,
OpenDatabase,
+ RemoveStreamValues,
RenameGroup,
RenameKey,
SaveConnection,
@@ -1134,6 +1136,54 @@ const useConnectionStore = defineStore('connections', {
}
},
+ /**
+ * insert new stream field item
+ * @param {string} connName
+ * @param {number} db
+ * @param {string} key
+ * @param {string} id
+ * @param {string[]} values field1, value1, filed2, value2...
+ * @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>}
+ */
+ async addStreamValue(connName, db, key, id, values) {
+ try {
+ const { data = {}, success, msg } = await AddStreamValue(connName, db, key, id, values)
+ if (success) {
+ const { updated = {} } = data
+ return { success, updated }
+ } else {
+ return { success: false, msg }
+ }
+ } catch (e) {
+ return { success: false, msg: e.message }
+ }
+ },
+
+ /**
+ * remove stream field
+ * @param {string} connName
+ * @param {number} db
+ * @param {string} key
+ * @param {string[]|string} ids
+ * @returns {Promise<{[msg]: {}, success: boolean, [removed]: string[]}>}
+ */
+ async removeStreamValues(connName, db, key, ids) {
+ if (typeof ids === 'string') {
+ ids = [ids]
+ }
+ try {
+ const { data = {}, success, msg } = await RemoveStreamValues(connName, db, key, ids)
+ if (success) {
+ const { removed = [] } = data
+ return { success, removed }
+ } else {
+ return { success, msg }
+ }
+ } catch (e) {
+ return { success: false, msg: e.message }
+ }
+ },
+
/**
* reset key's ttl
* @param {string} connName