Compare commits
105 Commits
Author | SHA1 | Date |
---|---|---|
|
3860415429 | |
|
dec906a69b | |
|
118b9b8870 | |
|
864bd74029 | |
|
aedf4d4222 | |
|
a4d7c598f9 | |
|
480e4cc7fd | |
|
1016df4a25 | |
|
46d7ba2a9f | |
|
19c8368dff | |
|
1c322fdac5 | |
|
53e8c26380 | |
|
400a908cf9 | |
|
edd77182a5 | |
|
6e44c64441 | |
|
51beb7249f | |
|
79fd2a6d39 | |
|
30c3decd65 | |
|
23fc32e92f | |
|
f458a1a0e4 | |
|
52aaad6339 | |
|
dd70d6b595 | |
|
5d425aadb1 | |
|
c02a24ee94 | |
|
e03fc8ad28 | |
|
3367f13d80 | |
|
b601ba255b | |
|
ccb4bb85ae | |
|
b0dfe348bd | |
|
a3a1a17af3 | |
|
ca9f0a08e1 | |
|
3f5b63a36f | |
|
ab3560fc2b | |
|
cb428747e2 | |
|
aa3383db43 | |
|
026591c8d4 | |
|
a70b5b56ff | |
|
eaa68df583 | |
|
2388f309d8 | |
|
a9c7cb1cd2 | |
|
b506e8a6a4 | |
|
c082a0c41f | |
|
970ebcf902 | |
|
b223feb441 | |
|
469a62333f | |
|
c38944e948 | |
|
5efbd4d316 | |
|
c54567115e | |
|
a14e7e947e | |
|
868b0c81b6 | |
|
a3cb09863a | |
|
b26f5d2bde | |
|
0038092193 | |
|
0739cb8b68 | |
|
70c38d9aa7 | |
|
237b25086c | |
|
71dbda01da | |
|
5deb6ce443 | |
|
ee398d4d98 | |
|
29b51f836f | |
|
ea8ceba32a | |
|
9bec3934bb | |
|
fdfd04d4bf | |
|
410dcd9e57 | |
|
ea44253c02 | |
|
e2093a89bf | |
|
908d4c7007 | |
|
6843314bad | |
|
bdfa31e4b6 | |
|
aa8c5495c1 | |
|
65cfdd1bcc | |
|
6bd1b23a64 | |
|
8c30daec15 | |
|
86f42fcc10 | |
|
1bcde26e35 | |
|
bf71c6db0e | |
|
88e2c6cb43 | |
|
6eeb701439 | |
|
028a240f49 | |
|
eefa7b1346 | |
|
3321fbf6fd | |
|
4ed93902a6 | |
|
b4405eb7db | |
|
04bc103583 | |
|
f17bb744f4 | |
|
152fbe962f | |
|
a2b0fc183f | |
|
f536b0f23b | |
|
3c43f960c3 | |
|
78bfaf6e16 | |
|
50bec33870 | |
|
f0c9b74545 | |
|
e5fed29427 | |
|
4dd52a8c8e | |
|
abf5534165 | |
|
455a911154 | |
|
e2264b33b0 | |
|
84b493b26a | |
|
c9e618d418 | |
|
e8f76ce8ae | |
|
70354c14ec | |
|
d472836d5f | |
|
f00a959db3 | |
|
aa98da31d6 | |
|
18ba04a5b1 |
|
@ -82,7 +82,7 @@ jobs:
|
|||
shell: bash
|
||||
run: |
|
||||
CGO_ENABLED=1 wails build -platform ${{ matrix.platform }} \
|
||||
-ldflags "-X main.version=${{ github.event.release.tag_name }} -X main.gaMeasurementID=${{ secrets.GA_MEASUREMENT_ID }} -X main.gaSecretKey=${{ secrets.MAC_GA_SECRET }}"
|
||||
-ldflags "-X main.version=${{ steps.normalise_version.outputs.version }} -X main.gaMeasurementID=${{ secrets.GA_MEASUREMENT_ID }} -X main.gaSecretKey=${{ secrets.MAC_GA_SECRET }}"
|
||||
|
||||
# - name: Notarise macOS app + create dmg
|
||||
# shell: bash
|
||||
|
|
|
@ -105,6 +105,7 @@ jobs:
|
|||
-ldflags "-X main.version=v${{ steps.normalise_version.outputs.version }}"
|
||||
|
||||
- name: Codesign Windows NSIS installer
|
||||
shell: powershell
|
||||
working-directory: ./build/bin
|
||||
run: |
|
||||
echo "Creating certificate file"
|
||||
|
@ -112,7 +113,7 @@ jobs:
|
|||
Set-Content -Path certificate\certificate.txt -Value '${{ secrets.WIN_SIGNING_CERT }}'
|
||||
certutil -decode certificate\certificate.txt certificate\certificate.pfx
|
||||
echo "Signing TinyRDM installer"
|
||||
& 'C:/Program Files (x86)/Windows Kits/10/bin/10.0.17763.0/x86/signtool.exe' sign /fd sha256 /tr http://ts.ssl.com /f certificate\certificate.pfx /p '${{ secrets.WIN_SIGNING_CERT_PASSWORD }}' TinyRDM-${{ steps.normalise_platform_name.outputs.pname }}-installer.exe
|
||||
& 'C:/Program Files (x86)/Windows Kits/10/bin/10.0.17763.0/x86/signtool.exe' sign /fd sha256 /tr http://timestamp.digicert.com /f certificate\certificate.pfx /p '${{ secrets.WIN_SIGNING_CERT_PASSWORD }}' TinyRDM-${{ steps.normalise_platform_name.outputs.pname }}-installer.exe
|
||||
|
||||
- name: Rename installer
|
||||
working-directory: ./build/bin
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
</div>
|
||||
<h1 align="center">Tiny RDM</h1>
|
||||
<h4 align="center"><strong>English</strong> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_zh.md">
|
||||
简体中文</a></h4>
|
||||
简体中文</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_ja.md">日本語</a></h4>
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/tiny-craft/tiny-rdm/blob/main/LICENSE)
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
<div align="center">
|
||||
<a href="https://github.com/tiny-craft/tiny-rdm/"><img src="build/appicon.png" width="120"/></a>
|
||||
</div>
|
||||
<h1 align="center">Tiny RDM</h1>
|
||||
<h4 align="center"><strong><a href="/">English</a></strong> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_zh.md">简体中文</a> | 日本語</h4>
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/tiny-craft/tiny-rdm/blob/main/LICENSE)
|
||||
[](https://github.com/tiny-craft/tiny-rdm/releases)
|
||||

|
||||
[](https://github.com/tiny-craft/tiny-rdm/stargazers)
|
||||
[](https://github.com/tiny-craft/tiny-rdm/fork)
|
||||
[](https://discord.gg/VTFbBMGjWh)
|
||||
[](https://twitter.com/Lykin53448)
|
||||
|
||||
<strong>Tiny RDMは、Mac、Windows、Linuxで利用可能な、モダンで軽量なクロスプラットフォームのRedisデスクトップマネージャーです。</strong>
|
||||
</div>
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="screenshots/dark_en.png">
|
||||
<source media="(prefers-color-scheme: light)" srcset="screenshots/light_en.png">
|
||||
<img alt="screenshot" src="screenshots/dark_en.png">
|
||||
</picture>
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="screenshots/dark_en2.png">
|
||||
<source media="(prefers-color-scheme: light)" srcset="screenshots/light_en2.png">
|
||||
<img alt="screenshot" src="screenshots/dark_en2.png">
|
||||
</picture>
|
||||
|
||||
## 特徴
|
||||
|
||||
* 超軽量、Webview2をベースにしており、埋め込みブラウザなし([Wails](https://github.com/wailsapp/wails)に感謝)。
|
||||
* 視覚的でユーザーフレンドリーなUI、ライトとダークテーマを提供([Naive UI](https://github.com/tusen-ai/naive-ui)と[IconPark](https://iconpark.oceanengine.com)に感謝)。
|
||||
* 多言語サポート([もっと多くの言語が必要ですか?ここをクリックして貢献してください](.github/CONTRIBUTING.md))。
|
||||
* より良い接続管理:SSHトンネル/SSL/センチネルモード/クラスターモード/HTTPプロキシ/SOCKS5プロキシをサポート。
|
||||
* キー値操作の可視化、リスト、ハッシュ、文字列、セット、ソートセット、ストリームのCRUDサポート。
|
||||
* 複数のデータ表示形式とデコード/解凍方法をサポート。
|
||||
* SCANを使用してセグメント化された読み込みを行い、数百万のキーを簡単にリスト化。
|
||||
* コマンド操作履歴のログリスト。
|
||||
* コマンドラインモードを提供。
|
||||
* スローログリストを提供。
|
||||
* リスト/ハッシュ/セット/ソートセットのセグメント化された読み込みとクエリ。
|
||||
* リスト/ハッシュ/セット/ソートセットの値のデコード/解凍を提供。
|
||||
* Monaco Editorと統合。
|
||||
* リアルタイムコマンド監視をサポート。
|
||||
* データのインポート/エクスポートをサポート。
|
||||
* パブリッシュ/サブスクライブをサポート。
|
||||
* 接続プロファイルのインポート/エクスポートをサポート。
|
||||
* 値表示のためのカスタムデータエンコーダーとデコーダーをサポート([こちらが手順です](https://redis.tinycraft.cc/guide/custom-decoder/))。
|
||||
|
||||
## インストール
|
||||
|
||||
[こちら](https://github.com/tiny-craft/tiny-rdm/releases)から無料でダウンロードできます。
|
||||
|
||||
> macOSにインストール後に開けない場合は、以下のコマンドを実行してから再度開いてください:
|
||||
> ``` shell
|
||||
> sudo xattr -d com.apple.quarantine /Applications/Tiny\ RDM.app
|
||||
> ```
|
||||
|
||||
## ビルドガイドライン
|
||||
|
||||
### 前提条件
|
||||
|
||||
* Go(最新バージョン)
|
||||
* Node.js >= 16
|
||||
* NPM >= 9
|
||||
|
||||
### Wailsのインストール
|
||||
|
||||
```bash
|
||||
go install github.com/wailsapp/wails/v2/cmd/wails@latest
|
||||
```
|
||||
|
||||
### コードの取得
|
||||
|
||||
```bash
|
||||
git clone https://github.com/tiny-craft/tiny-rdm --depth=1
|
||||
```
|
||||
|
||||
### フロントエンドのビルド
|
||||
|
||||
```bash
|
||||
npm install --prefix ./frontend
|
||||
```
|
||||
|
||||
または
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
```
|
||||
|
||||
### コンパイルと実行
|
||||
|
||||
```bash
|
||||
wails dev
|
||||
```
|
||||
## について
|
||||
|
||||
### Wechat公式アカウント
|
||||
|
||||
<img src="docs/images/wechat_official.png" alt="wechat" width="360" />
|
||||
|
||||
### スポンサー
|
||||
|
||||
このプロジェクトが役立つ場合は、コーヒーを一杯おごってください ☕️。
|
||||
|
||||
* Wechatスポンサー
|
||||
|
||||
<img src="docs/images/wechat_sponsor.jpg" alt="wechat" width="200" />
|
|
@ -2,7 +2,7 @@
|
|||
<a href="https://github.com/tiny-craft/tiny-rdm/"><img src="build/appicon.png" width="120"/></a>
|
||||
</div>
|
||||
<h1 align="center">Tiny RDM</h1>
|
||||
<h4 align="center"><strong><a href="/">English</a></strong> | 简体中文</h4>
|
||||
<h4 align="center"><strong><a href="/">English</a></strong> | 简体中文 | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_ja.md">日本語</a></h4>
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/tiny-craft/tiny-rdm/blob/main/LICENSE)
|
||||
|
@ -106,6 +106,10 @@ wails dev
|
|||
|
||||
<img src="docs/images/wechat_official.png" alt="wechat" width="360" />
|
||||
|
||||
### B站官方账号
|
||||
|
||||
<img src="docs/images/bilibili_official.png" alt="bilibili" width="360" />
|
||||
|
||||
### 独立开发互助QQ群
|
||||
|
||||
```
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
"tinyrdm/backend/types"
|
||||
"tinyrdm/backend/utils/coll"
|
||||
convutil "tinyrdm/backend/utils/convert"
|
||||
maputil "tinyrdm/backend/utils/map"
|
||||
redis2 "tinyrdm/backend/utils/redis"
|
||||
sliceutil "tinyrdm/backend/utils/slice"
|
||||
strutil "tinyrdm/backend/utils/string"
|
||||
|
@ -98,9 +99,9 @@ func (b *browserService) OpenConnection(name string) (resp types.JSResp) {
|
|||
selConn := Connection().getConnection(name)
|
||||
// correct last database index
|
||||
lastDB := selConn.LastDB
|
||||
if selConn.DBFilterType == "show" && !sliceutil.Contains(selConn.DBFilterList, lastDB) {
|
||||
if selConn.DBFilterType == "show" && !slices.Contains(selConn.DBFilterList, lastDB) {
|
||||
lastDB = selConn.DBFilterList[0]
|
||||
} else if selConn.DBFilterType == "hide" && sliceutil.Contains(selConn.DBFilterList, lastDB) {
|
||||
} else if selConn.DBFilterType == "hide" && slices.Contains(selConn.DBFilterList, lastDB) {
|
||||
lastDB = selConn.DBFilterList[0]
|
||||
}
|
||||
if lastDB != selConn.LastDB {
|
||||
|
@ -155,12 +156,13 @@ func (b *browserService) OpenConnection(name string) (resp types.JSResp) {
|
|||
} else {
|
||||
// get database info
|
||||
var res string
|
||||
res, err = client.Info(ctx, "keyspace").Result()
|
||||
if err != nil {
|
||||
resp.Msg = "get server info fail:" + err.Error()
|
||||
return
|
||||
info := map[string]map[string]string{}
|
||||
if res, err = client.Info(ctx, "keyspace").Result(); err != nil {
|
||||
//resp.Msg = "get server info fail:" + err.Error()
|
||||
//return
|
||||
} else {
|
||||
info = b.parseInfo(res)
|
||||
}
|
||||
info := b.parseInfo(res)
|
||||
|
||||
if totaldb <= 0 {
|
||||
// cannot retrieve the database count by "CONFIG GET databases", try to get max index from keyspace
|
||||
|
@ -222,24 +224,32 @@ func (b *browserService) OpenConnection(name string) (resp types.JSResp) {
|
|||
}
|
||||
}
|
||||
|
||||
// get redis server version
|
||||
var version string
|
||||
if res, err := client.Info(ctx, "server").Result(); err == nil || errors.Is(err, redis.Nil) {
|
||||
info := b.parseInfo(res)
|
||||
serverInfo := maputil.Get(info, "Server", map[string]string{})
|
||||
version = maputil.Get(serverInfo, "redis_version", "1.0.0")
|
||||
}
|
||||
|
||||
resp.Success = true
|
||||
resp.Data = map[string]any{
|
||||
"db": dbs,
|
||||
"view": selConn.KeyView,
|
||||
"lastDB": selConn.LastDB,
|
||||
"db": dbs,
|
||||
"view": selConn.KeyView,
|
||||
"lastDB": selConn.LastDB,
|
||||
"version": version,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CloseConnection close redis server connection
|
||||
func (b *browserService) CloseConnection(name string) (resp types.JSResp) {
|
||||
item, ok := b.connMap[name]
|
||||
if ok {
|
||||
if item, ok := b.connMap[name]; ok {
|
||||
delete(b.connMap, name)
|
||||
if item.cancelFunc != nil {
|
||||
item.cancelFunc()
|
||||
}
|
||||
if item.client != nil {
|
||||
if item.cancelFunc != nil {
|
||||
item.cancelFunc()
|
||||
}
|
||||
item.client.Close()
|
||||
}
|
||||
}
|
||||
|
@ -247,7 +257,7 @@ func (b *browserService) CloseConnection(name string) (resp types.JSResp) {
|
|||
return
|
||||
}
|
||||
|
||||
func (b *browserService) createRedisClient(selConn types.ConnectionConfig) (client redis.UniversalClient, err error) {
|
||||
func (b *browserService) createRedisClient(ctx context.Context, selConn types.ConnectionConfig) (client redis.UniversalClient, err error) {
|
||||
hook := redis2.NewHook(selConn.Name, func(cmd string, cost int64) {
|
||||
now := time.Now()
|
||||
//last := strings.LastIndex(cmd, ":")
|
||||
|
@ -268,10 +278,10 @@ func (b *browserService) createRedisClient(selConn types.ConnectionConfig) (clie
|
|||
return
|
||||
}
|
||||
|
||||
_ = client.Do(b.ctx, "CLIENT", "SETNAME", url.QueryEscape(selConn.Name)).Err()
|
||||
_ = client.Do(ctx, "CLIENT", "SETNAME", url.QueryEscape(selConn.Name)).Err()
|
||||
// add hook to each node in cluster mode
|
||||
if cluster, ok := client.(*redis.ClusterClient); ok {
|
||||
err = cluster.ForEachShard(b.ctx, func(ctx context.Context, cli *redis.Client) error {
|
||||
err = cluster.ForEachShard(ctx, func(ctx context.Context, cli *redis.Client) error {
|
||||
cli.AddHook(hook)
|
||||
return nil
|
||||
})
|
||||
|
@ -283,15 +293,15 @@ func (b *browserService) createRedisClient(selConn types.ConnectionConfig) (clie
|
|||
client.AddHook(hook)
|
||||
}
|
||||
|
||||
if _, err = client.Ping(b.ctx).Result(); err != nil && !errors.Is(err, redis.Nil) {
|
||||
if _, err = client.Ping(ctx).Result(); err != nil && !errors.Is(err, redis.Nil) {
|
||||
err = errors.New("can not connect to redis server:" + err.Error())
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// get a redis client from local cache or create a new open
|
||||
// if db >= 0, will also switch to db index
|
||||
// get a redis client from local cache or create a new one
|
||||
// if db >= 0, it will also switch to target database index
|
||||
func (b *browserService) getRedisClient(server string, db int) (item *connectionItem, err error) {
|
||||
b.mutex.Lock()
|
||||
defer b.mutex.Unlock()
|
||||
|
@ -316,15 +326,22 @@ func (b *browserService) getRedisClient(server string, db int) (item *connection
|
|||
selConn := Connection().getConnection(server)
|
||||
if selConn == nil {
|
||||
err = fmt.Errorf("no match connection \"%s\"", server)
|
||||
delete(b.connMap, server)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancelFunc := context.WithCancel(b.ctx)
|
||||
b.connMap[server] = &connectionItem{
|
||||
ctx: ctx,
|
||||
cancelFunc: cancelFunc,
|
||||
}
|
||||
var connConfig = selConn.ConnectionConfig
|
||||
connConfig.LastDB = db
|
||||
client, err = b.createRedisClient(connConfig)
|
||||
client, err = b.createRedisClient(ctx, connConfig)
|
||||
if err != nil {
|
||||
delete(b.connMap, server)
|
||||
return
|
||||
}
|
||||
ctx, cancelFunc := context.WithCancel(b.ctx)
|
||||
item = &connectionItem{
|
||||
client: client,
|
||||
ctx: ctx,
|
||||
|
@ -445,7 +462,7 @@ func (b *browserService) scanKeys(ctx context.Context, client redis.UniversalCli
|
|||
filterType := len(keyType) > 0
|
||||
scanSize := int64(Preferences().GetScanSize())
|
||||
// define sub scan function
|
||||
scan := func(ctx context.Context, cli redis.UniversalClient, appendFunc func(k []any)) error {
|
||||
scan := func(ctx context.Context, cli redis.UniversalClient, count int64, appendFunc func(k []any)) error {
|
||||
var loadedKey []string
|
||||
var scanCount int64
|
||||
for {
|
||||
|
@ -475,16 +492,22 @@ func (b *browserService) scanKeys(ctx context.Context, client redis.UniversalCli
|
|||
if cluster, ok := client.(*redis.ClusterClient); ok {
|
||||
// cluster mode
|
||||
var mutex sync.Mutex
|
||||
var totalMaster int64
|
||||
cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error {
|
||||
totalMaster += 1
|
||||
return nil
|
||||
})
|
||||
partCount := count / max(totalMaster, 1)
|
||||
err = cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error {
|
||||
// FIXME: BUG? can not fully load in cluster mode? maybe remove the shared "cursor"
|
||||
return scan(ctx, cli, func(k []any) {
|
||||
return scan(ctx, cli, partCount, func(k []any) {
|
||||
mutex.Lock()
|
||||
keys = append(keys, k...)
|
||||
mutex.Unlock()
|
||||
})
|
||||
})
|
||||
} else {
|
||||
err = scan(ctx, client, func(k []any) {
|
||||
err = scan(ctx, client, count, func(k []any) {
|
||||
keys = append(keys, k...)
|
||||
})
|
||||
}
|
||||
|
@ -853,7 +876,8 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
|
|||
continue
|
||||
}
|
||||
items = append(items, types.ListEntryItem{
|
||||
Value: val,
|
||||
Index: len(items),
|
||||
Value: strutil.EncodeRedisKey(val),
|
||||
})
|
||||
if doConvert {
|
||||
if dv, _, _ := convutil.ConvertTo(val, param.Decode, param.Format, decoder); dv != val {
|
||||
|
@ -970,7 +994,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
|
|||
}
|
||||
for _, val := range loadedKey {
|
||||
items = append(items, types.SetEntryItem{
|
||||
Value: val,
|
||||
Value: strutil.EncodeRedisKey(val),
|
||||
})
|
||||
if doConvert {
|
||||
if dv, _, _ := convutil.ConvertTo(val, param.Decode, param.Format, decoder); dv != val {
|
||||
|
@ -991,7 +1015,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
|
|||
loadedKey, cursor, subErr = client.SScan(ctx, key, cursor, matchPattern, scanSize).Result()
|
||||
items = make([]types.SetEntryItem, len(loadedKey))
|
||||
for i, val := range loadedKey {
|
||||
items[i].Value = val
|
||||
items[i].Value = strutil.EncodeRedisKey(val)
|
||||
if doConvert {
|
||||
if dv, _, _ := convutil.ConvertTo(val, param.Decode, param.Format, decoder); dv != val {
|
||||
items[i].DisplayValue = dv
|
||||
|
@ -1037,7 +1061,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
|
|||
for i := 0; i < len(loadedVal); i += 2 {
|
||||
if score, err = strconv.ParseFloat(loadedVal[i+1], 64); err == nil {
|
||||
items = append(items, types.ZSetEntryItem{
|
||||
Value: loadedVal[i],
|
||||
Value: strutil.EncodeRedisKey(loadedVal[i]),
|
||||
Score: score,
|
||||
})
|
||||
if doConvert {
|
||||
|
@ -1071,7 +1095,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
|
|||
continue
|
||||
}
|
||||
entry := types.ZSetEntryItem{
|
||||
Value: val,
|
||||
Value: strutil.EncodeRedisKey(val),
|
||||
}
|
||||
if math.IsInf(z.Score, 1) {
|
||||
entry.ScoreStr = "+inf"
|
||||
|
@ -1317,7 +1341,10 @@ func (b *browserService) SetKeyValue(param types.SetKeyParam) (resp types.JSResp
|
|||
if err == nil && expiration > 0 {
|
||||
client.Expire(ctx, key, expiration)
|
||||
}
|
||||
savedValue = param.Value
|
||||
var ok bool
|
||||
if savedValue, ok = param.Value.(string); !ok {
|
||||
savedValue = ""
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
@ -1325,9 +1352,50 @@ func (b *browserService) SetKeyValue(param types.SetKeyParam) (resp types.JSResp
|
|||
return
|
||||
}
|
||||
resp.Success = true
|
||||
resp.Data = map[string]any{
|
||||
"value": savedValue,
|
||||
respData := map[string]any{}
|
||||
if val, ok := savedValue.(string); ok {
|
||||
respData["value"] = strutil.EncodeRedisKey(val)
|
||||
}
|
||||
resp.Data = respData
|
||||
return
|
||||
}
|
||||
|
||||
// GetHashValue get hash field
|
||||
func (b *browserService) GetHashValue(param types.GetHashParam) (resp types.JSResp) {
|
||||
item, err := b.getRedisClient(param.Server, param.DB)
|
||||
if err != nil {
|
||||
resp.Msg = err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
client, ctx := item.client, item.ctx
|
||||
key := strutil.DecodeRedisKey(param.Key)
|
||||
val, err := client.HGet(ctx, key, param.Field).Result()
|
||||
if errors.Is(err, redis.Nil) {
|
||||
resp.Msg = "field in key not found"
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
resp.Msg = err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
var displayVal string
|
||||
if (len(param.Decode) > 0 && param.Decode != types.DECODE_NONE) ||
|
||||
(len(param.Format) > 0 && param.Format != types.FORMAT_RAW) {
|
||||
decoder := Preferences().GetDecoder()
|
||||
displayVal, _, _ = convutil.ConvertTo(val, param.Decode, param.Format, decoder)
|
||||
if displayVal == val {
|
||||
displayVal = ""
|
||||
}
|
||||
}
|
||||
|
||||
resp.Data = types.HashEntryItem{
|
||||
Key: param.Field,
|
||||
Value: val,
|
||||
DisplayValue: displayVal,
|
||||
}
|
||||
resp.Success = true
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -1547,10 +1615,11 @@ func (b *browserService) SetListItem(param types.SetListParam) (resp types.JSRes
|
|||
client, ctx := item.client, item.ctx
|
||||
key := strutil.DecodeRedisKey(param.Key)
|
||||
str := strutil.DecodeRedisKey(param.Value)
|
||||
index := int64(param.Index)
|
||||
var replaced, removed []types.ListReplaceItem
|
||||
if len(str) <= 0 {
|
||||
// remove from list
|
||||
err = client.LSet(ctx, key, param.Index, "---VALUE_REMOVED_BY_TINY_RDM---").Err()
|
||||
err = client.LSet(ctx, key, index, "---VALUE_REMOVED_BY_TINY_RDM---").Err()
|
||||
if err != nil {
|
||||
resp.Msg = err.Error()
|
||||
return
|
||||
|
@ -1572,7 +1641,7 @@ func (b *browserService) SetListItem(param types.SetListParam) (resp types.JSRes
|
|||
resp.Msg = fmt.Sprintf(`save to type "%s" fail: %s`, param.Format, err.Error())
|
||||
return
|
||||
}
|
||||
err = client.LSet(ctx, key, param.Index, saveStr).Err()
|
||||
err = client.LSet(ctx, key, index, saveStr).Err()
|
||||
if err != nil {
|
||||
resp.Msg = err.Error()
|
||||
return
|
||||
|
@ -1964,21 +2033,13 @@ func (b *browserService) SetKeyTTL(server string, db int, k any, ttl int64) (res
|
|||
|
||||
// BatchSetTTL batch set ttl
|
||||
func (b *browserService) BatchSetTTL(server string, db int, ks []any, ttl int64, serialNo string) (resp types.JSResp) {
|
||||
conf := Connection().getConnection(server)
|
||||
if conf == nil {
|
||||
resp.Msg = fmt.Sprintf("no connection profile named: %s", server)
|
||||
return
|
||||
}
|
||||
var client redis.UniversalClient
|
||||
var err error
|
||||
var connConfig = conf.ConnectionConfig
|
||||
connConfig.LastDB = db
|
||||
if client, err = b.createRedisClient(connConfig); err != nil {
|
||||
item, err := b.getRedisClient(server, db)
|
||||
if err != nil {
|
||||
resp.Msg = err.Error()
|
||||
return
|
||||
}
|
||||
client := item.client
|
||||
ctx, cancelFunc := context.WithCancel(b.ctx)
|
||||
defer client.Close()
|
||||
defer cancelFunc()
|
||||
|
||||
//cancelEvent := "ttling:stop:" + serialNo
|
||||
|
@ -2002,7 +2063,7 @@ func (b *browserService) BatchSetTTL(server string, db int, ks []any, ttl int64,
|
|||
//}
|
||||
if i >= total-1 || time.Now().Sub(startTime).Milliseconds() > 100 {
|
||||
startTime = time.Now()
|
||||
//runtime.EventsEmit(b.ctx, processEvent, param)
|
||||
//runtime.EventsEmit(ctx, processEvent, param)
|
||||
// do some sleep to prevent blocking the Redis server
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
@ -2172,22 +2233,13 @@ func (b *browserService) DeleteOneKey(server string, db int, k any) (resp types.
|
|||
|
||||
// DeleteKeys delete keys sync with notification
|
||||
func (b *browserService) DeleteKeys(server string, db int, ks []any, serialNo string) (resp types.JSResp) {
|
||||
// connect a new connection to export keys
|
||||
conf := Connection().getConnection(server)
|
||||
if conf == nil {
|
||||
resp.Msg = fmt.Sprintf("no connection profile named: %s", server)
|
||||
return
|
||||
}
|
||||
var client redis.UniversalClient
|
||||
var err error
|
||||
var connConfig = conf.ConnectionConfig
|
||||
connConfig.LastDB = db
|
||||
if client, err = b.createRedisClient(connConfig); err != nil {
|
||||
item, err := b.getRedisClient(server, db)
|
||||
if err != nil {
|
||||
resp.Msg = err.Error()
|
||||
return
|
||||
}
|
||||
client := item.client
|
||||
ctx, cancelFunc := context.WithCancel(b.ctx)
|
||||
defer client.Close()
|
||||
defer cancelFunc()
|
||||
|
||||
cancelEvent := "delete:stop:" + serialNo
|
||||
|
@ -2247,24 +2299,85 @@ func (b *browserService) DeleteKeys(server string, db int, ks []any, serialNo st
|
|||
return
|
||||
}
|
||||
|
||||
// ExportKey export keys
|
||||
func (b *browserService) ExportKey(server string, db int, ks []any, path string, includeExpire bool) (resp types.JSResp) {
|
||||
// connect a new connection to export keys
|
||||
conf := Connection().getConnection(server)
|
||||
if conf == nil {
|
||||
resp.Msg = fmt.Sprintf("no connection profile named: %s", server)
|
||||
return
|
||||
}
|
||||
var client redis.UniversalClient
|
||||
var err error
|
||||
var connConfig = conf.ConnectionConfig
|
||||
connConfig.LastDB = db
|
||||
if client, err = b.createRedisClient(connConfig); err != nil {
|
||||
// DeleteKeysByPattern delete keys by pattern
|
||||
func (b *browserService) DeleteKeysByPattern(server string, db int, pattern string) (resp types.JSResp) {
|
||||
item, err := b.getRedisClient(server, db)
|
||||
if err != nil {
|
||||
resp.Msg = err.Error()
|
||||
return
|
||||
}
|
||||
client := item.client
|
||||
ctx, cancelFunc := context.WithCancel(b.ctx)
|
||||
defer cancelFunc()
|
||||
|
||||
var ks []any
|
||||
ks, _, err = b.scanKeys(ctx, client, pattern, "", 0, 0)
|
||||
if err != nil {
|
||||
resp.Msg = err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
total := len(ks)
|
||||
var canceled bool
|
||||
var deletedKeys = make([]any, 0, total)
|
||||
var mutex sync.Mutex
|
||||
del := func(ctx context.Context, cli redis.UniversalClient) error {
|
||||
const batchSize = 1000
|
||||
for i := 0; i < total; i += batchSize {
|
||||
pipe := cli.Pipeline()
|
||||
for j := 0; j < batchSize; j++ {
|
||||
if i+j < total {
|
||||
pipe.Del(ctx, strutil.DecodeRedisKey(ks[i+j]))
|
||||
}
|
||||
}
|
||||
cmders, delErr := pipe.Exec(ctx)
|
||||
for j, cmder := range cmders {
|
||||
if cmder.(*redis.IntCmd).Val() == 1 {
|
||||
// save deleted key
|
||||
mutex.Lock()
|
||||
deletedKeys = append(deletedKeys, ks[i+j])
|
||||
mutex.Unlock()
|
||||
}
|
||||
}
|
||||
if errors.Is(delErr, context.Canceled) || canceled {
|
||||
canceled = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if cluster, ok := client.(*redis.ClusterClient); ok {
|
||||
// cluster mode
|
||||
err = cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error {
|
||||
return del(ctx, cli)
|
||||
})
|
||||
} else {
|
||||
err = del(ctx, client)
|
||||
}
|
||||
|
||||
resp.Success = true
|
||||
resp.Data = struct {
|
||||
Canceled bool `json:"canceled"`
|
||||
Deleted any `json:"deleted"`
|
||||
Failed int `json:"failed"`
|
||||
}{
|
||||
Canceled: canceled,
|
||||
Deleted: deletedKeys,
|
||||
Failed: len(ks) - len(deletedKeys),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ExportKey export keys
|
||||
func (b *browserService) ExportKey(server string, db int, ks []any, path string, includeExpire bool) (resp types.JSResp) {
|
||||
item, err := b.getRedisClient(server, db)
|
||||
if err != nil {
|
||||
resp.Msg = err.Error()
|
||||
return
|
||||
}
|
||||
client := item.client
|
||||
ctx, cancelFunc := context.WithCancel(b.ctx)
|
||||
defer client.Close()
|
||||
defer cancelFunc()
|
||||
|
||||
file, err := os.Create(path)
|
||||
|
@ -2333,22 +2446,13 @@ func (b *browserService) ExportKey(server string, db int, ks []any, path string,
|
|||
|
||||
// ImportCSV import data from csv file
|
||||
func (b *browserService) ImportCSV(server string, db int, path string, conflict int, ttl int64) (resp types.JSResp) {
|
||||
// connect a new connection to export keys
|
||||
conf := Connection().getConnection(server)
|
||||
if conf == nil {
|
||||
resp.Msg = fmt.Sprintf("no connection profile named: %s", server)
|
||||
return
|
||||
}
|
||||
var client redis.UniversalClient
|
||||
var err error
|
||||
var connConfig = conf.ConnectionConfig
|
||||
connConfig.LastDB = db
|
||||
if client, err = b.createRedisClient(connConfig); err != nil {
|
||||
item, err := b.getRedisClient(server, db)
|
||||
if err != nil {
|
||||
resp.Msg = err.Error()
|
||||
return
|
||||
}
|
||||
client := item.client
|
||||
ctx, cancelFunc := context.WithCancel(b.ctx)
|
||||
defer client.Close()
|
||||
defer cancelFunc()
|
||||
|
||||
file, err := os.Open(path)
|
||||
|
@ -2430,7 +2534,7 @@ func (b *browserService) ImportCSV(server string, db int, path string, conflict
|
|||
"ignored": ignored,
|
||||
//"processing": string(key),
|
||||
}
|
||||
runtime.EventsEmit(b.ctx, processEvent, param)
|
||||
runtime.EventsEmit(ctx, processEvent, param)
|
||||
// do some sleep to prevent blocking the Redis server
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/klauspost/compress/zip"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/vrischmann/userdir"
|
||||
|
@ -65,7 +64,7 @@ func (c *connectionService) buildOption(config types.ConnectionConfig) (*redis.O
|
|||
} else if config.Proxy.Type == 2 {
|
||||
// use custom proxy
|
||||
proxyUrl := url.URL{
|
||||
Host: fmt.Sprintf("%s:%d", config.Proxy.Addr, config.Proxy.Port),
|
||||
Host: net.JoinHostPort(config.Proxy.Addr, strconv.Itoa(config.Proxy.Port)),
|
||||
}
|
||||
if len(config.Proxy.Username) > 0 {
|
||||
proxyUrl.User = url.UserPassword(config.Proxy.Username, config.Proxy.Password)
|
||||
|
@ -111,7 +110,7 @@ func (c *connectionService) buildOption(config types.ConnectionConfig) (*redis.O
|
|||
return nil, errors.New("invalid login type")
|
||||
}
|
||||
|
||||
sshAddr = fmt.Sprintf("%s:%d", config.SSH.Addr, config.SSH.Port)
|
||||
sshAddr = net.JoinHostPort(config.SSH.Addr, strconv.Itoa(config.SSH.Port))
|
||||
}
|
||||
|
||||
var tlsConfig *tls.Config
|
||||
|
@ -168,9 +167,9 @@ func (c *connectionService) buildOption(config types.ConnectionConfig) (*redis.O
|
|||
port = config.Port
|
||||
}
|
||||
if len(config.Addr) <= 0 {
|
||||
option.Addr = fmt.Sprintf("127.0.0.1:%d", port)
|
||||
option.Addr = net.JoinHostPort("127.0.0.1", strconv.Itoa(port))
|
||||
} else {
|
||||
option.Addr = fmt.Sprintf("%s:%d", config.Addr, port)
|
||||
option.Addr = net.JoinHostPort(config.Addr, strconv.Itoa(port))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -224,9 +223,13 @@ func (c *connectionService) createRedisClient(config types.ConnectionConfig) (re
|
|||
if len(addr) < 2 {
|
||||
return nil, errors.New("cannot get master address")
|
||||
}
|
||||
option.Addr = fmt.Sprintf("%s:%s", addr[0], addr[1])
|
||||
option.Addr = net.JoinHostPort(addr[0], addr[1])
|
||||
option.Username = config.Sentinel.Username
|
||||
option.Password = config.Sentinel.Password
|
||||
if option.Dialer != nil {
|
||||
option.ReadTimeout = -2
|
||||
option.WriteTimeout = -2
|
||||
}
|
||||
}
|
||||
|
||||
if config.LastDB > 0 {
|
||||
|
@ -310,7 +313,7 @@ func (c *connectionService) ListSentinelMasters(config types.ConnectionConfig) (
|
|||
if infoMap, ok := info.(map[any]any); ok {
|
||||
retInfo = append(retInfo, map[string]string{
|
||||
"name": infoMap["name"].(string),
|
||||
"addr": fmt.Sprintf("%s:%s", infoMap["ip"].(string), infoMap["port"].(string)),
|
||||
"addr": net.JoinHostPort(infoMap["ip"].(string), infoMap["port"].(string)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -89,7 +89,7 @@ func (c *monitorService) StartMonitor(server string) (resp types.JSResp) {
|
|||
item.cmd = item.client.Monitor(c.ctx, item.ch)
|
||||
item.cmd.Start()
|
||||
|
||||
go c.processMonitor(&item.mutex, item.ch, item.closeCh, item.eventName)
|
||||
go c.processMonitor(&item.mutex, item.ch, item.closeCh, item.cmd, item.eventName)
|
||||
resp.Success = true
|
||||
resp.Data = struct {
|
||||
EventName string `json:"eventName"`
|
||||
|
@ -99,7 +99,7 @@ func (c *monitorService) StartMonitor(server string) (resp types.JSResp) {
|
|||
return
|
||||
}
|
||||
|
||||
func (c *monitorService) processMonitor(mutex *sync.Mutex, ch <-chan string, closeCh <-chan struct{}, eventName string) {
|
||||
func (c *monitorService) processMonitor(mutex *sync.Mutex, ch <-chan string, closeCh <-chan struct{}, cmd *redis.MonitorCmd, eventName string) {
|
||||
lastEmitTime := time.Now().Add(-1 * time.Minute)
|
||||
cache := make([]string, 0, 1000)
|
||||
for {
|
||||
|
@ -120,6 +120,7 @@ func (c *monitorService) processMonitor(mutex *sync.Mutex, ch <-chan string, clo
|
|||
|
||||
case <-closeCh:
|
||||
// monitor stopped
|
||||
cmd.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -136,8 +137,8 @@ func (c *monitorService) StopMonitor(server string) (resp types.JSResp) {
|
|||
return
|
||||
}
|
||||
|
||||
item.cmd.Stop()
|
||||
//close(item.ch)
|
||||
item.client.Close()
|
||||
close(item.closeCh)
|
||||
delete(c.items, server)
|
||||
resp.Success = true
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"github.com/adrg/sysfont"
|
||||
runtime2 "github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
@ -50,6 +51,7 @@ func (p *preferencesService) SetPreferences(pf types.Preferences) (resp types.JS
|
|||
return
|
||||
}
|
||||
|
||||
p.UpdateEnv()
|
||||
resp.Success = true
|
||||
return
|
||||
}
|
||||
|
@ -114,6 +116,11 @@ func (p *preferencesService) GetBuildInDecoder() (resp types.JSResp) {
|
|||
return
|
||||
}
|
||||
|
||||
func (p *preferencesService) GetLanguage() string {
|
||||
pref := p.pref.GetPreferences()
|
||||
return pref.General.Language
|
||||
}
|
||||
|
||||
func (p *preferencesService) SetAppVersion(ver string) {
|
||||
if !strings.HasPrefix(ver, "v") {
|
||||
p.clientVersion = "v" + ver
|
||||
|
@ -215,22 +222,31 @@ func (p *preferencesService) GetDecoder() []convutil.CmdConvert {
|
|||
})
|
||||
}
|
||||
|
||||
type latestRelease struct {
|
||||
Name string `json:"name"`
|
||||
TagName string `json:"tag_name"`
|
||||
Url string `json:"url"`
|
||||
HtmlUrl string `json:"html_url"`
|
||||
type sponsorItem struct {
|
||||
Name string `json:"name"`
|
||||
Link string `json:"link"`
|
||||
Region []string `json:"region"`
|
||||
}
|
||||
|
||||
type upgradeInfo struct {
|
||||
Version string `json:"version"`
|
||||
Changelog map[string]string `json:"changelog"`
|
||||
Description map[string]string `json:"description"`
|
||||
DownloadURl map[string]string `json:"download_url"`
|
||||
DownloadPage map[string]string `json:"download_page"`
|
||||
Sponsor []sponsorItem `json:"sponsor,omitempty"`
|
||||
}
|
||||
|
||||
func (p *preferencesService) CheckForUpdate() (resp types.JSResp) {
|
||||
// request latest version
|
||||
res, err := http.Get("https://api.github.com/repos/tiny-craft/tiny-rdm/releases/latest")
|
||||
//res, err := http.Get("https://api.github.com/repos/tiny-craft/tiny-rdm/releases/latest")
|
||||
res, err := http.Get("https://redis.tinycraft.cc/client_version.json")
|
||||
if err != nil || res.StatusCode != http.StatusOK {
|
||||
resp.Msg = "network error"
|
||||
return
|
||||
}
|
||||
|
||||
var respObj latestRelease
|
||||
var respObj upgradeInfo
|
||||
err = json.NewDecoder(res.Body).Decode(&respObj)
|
||||
if err != nil {
|
||||
resp.Msg = "invalid content"
|
||||
|
@ -240,9 +256,20 @@ func (p *preferencesService) CheckForUpdate() (resp types.JSResp) {
|
|||
// compare with current version
|
||||
resp.Success = true
|
||||
resp.Data = map[string]any{
|
||||
"version": p.clientVersion,
|
||||
"latest": respObj.TagName,
|
||||
"page_url": respObj.HtmlUrl,
|
||||
"version": p.clientVersion,
|
||||
"latest": respObj.Version,
|
||||
"description": respObj.Description,
|
||||
"download_page": respObj.DownloadPage,
|
||||
"sponsor": respObj.Sponsor,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateEnv Update System Environment
|
||||
func (p *preferencesService) UpdateEnv() {
|
||||
if p.GetLanguage() == "zh" {
|
||||
os.Setenv("LANG", "zh_CN.UTF-8")
|
||||
} else {
|
||||
os.Unsetenv("LANG")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,10 +3,10 @@ package storage
|
|||
import (
|
||||
"errors"
|
||||
"gopkg.in/yaml.v3"
|
||||
"slices"
|
||||
"sync"
|
||||
"tinyrdm/backend/consts"
|
||||
"tinyrdm/backend/types"
|
||||
sliceutil "tinyrdm/backend/utils/slice"
|
||||
)
|
||||
|
||||
type ConnectionsStorage struct {
|
||||
|
@ -256,10 +256,10 @@ func (c *ConnectionsStorage) SaveSortedConnection(sortedConns types.Connections)
|
|||
|
||||
conns := c.GetConnectionsFlat()
|
||||
takeConn := func(name string) (types.Connection, bool) {
|
||||
idx, ok := sliceutil.Find(conns, func(i int) bool {
|
||||
return conns[i].Name == name
|
||||
idx := slices.IndexFunc(conns, func(connection types.Connection) bool {
|
||||
return connection.Name == name
|
||||
})
|
||||
if ok {
|
||||
if idx >= 0 {
|
||||
ret := conns[idx]
|
||||
conns = append(conns[:idx], conns[idx+1:]...)
|
||||
return ret, true
|
||||
|
|
|
@ -56,7 +56,7 @@ type SetListParam struct {
|
|||
Server string `json:"server"`
|
||||
DB int `json:"db"`
|
||||
Key any `json:"key"`
|
||||
Index int64 `json:"index"`
|
||||
Index int `json:"index"`
|
||||
Value any `json:"value"`
|
||||
Format string `json:"format,omitempty"`
|
||||
Decode string `json:"decode,omitempty"`
|
||||
|
@ -101,3 +101,12 @@ type SetZSetParam struct {
|
|||
RetFormat string `json:"retFormat,omitempty"`
|
||||
RetDecode string `json:"retDecode,omitempty"`
|
||||
}
|
||||
|
||||
type GetHashParam struct {
|
||||
Server string `json:"server"`
|
||||
DB int `json:"db"`
|
||||
Key any `json:"key"`
|
||||
Field string `json:"field,omitempty"`
|
||||
Format string `json:"format,omitempty"`
|
||||
Decode string `json:"decode,omitempty"`
|
||||
}
|
||||
|
|
|
@ -27,11 +27,12 @@ func NewPreferences() Preferences {
|
|||
AllowTrack: true,
|
||||
},
|
||||
Editor: PreferencesEditor{
|
||||
FontSize: consts.DEFAULT_FONT_SIZE,
|
||||
ShowLineNum: true,
|
||||
ShowFolding: true,
|
||||
DropText: true,
|
||||
Links: true,
|
||||
FontSize: consts.DEFAULT_FONT_SIZE,
|
||||
ShowLineNum: true,
|
||||
ShowFolding: true,
|
||||
DropText: true,
|
||||
Links: true,
|
||||
EntryTextAlign: 0,
|
||||
},
|
||||
Cli: PreferencesCli{
|
||||
FontSize: consts.DEFAULT_FONT_SIZE,
|
||||
|
@ -67,13 +68,14 @@ type PreferencesGeneral struct {
|
|||
}
|
||||
|
||||
type PreferencesEditor struct {
|
||||
Font string `json:"font" yaml:"font,omitempty"`
|
||||
FontFamily []string `json:"fontFamily" yaml:"font_family,omitempty"`
|
||||
FontSize int `json:"fontSize" yaml:"font_size"`
|
||||
ShowLineNum bool `json:"showLineNum" yaml:"show_line_num"`
|
||||
ShowFolding bool `json:"showFolding" yaml:"show_folding"`
|
||||
DropText bool `json:"dropText" yaml:"drop_text"`
|
||||
Links bool `json:"links" yaml:"links"`
|
||||
Font string `json:"font" yaml:"font,omitempty"`
|
||||
FontFamily []string `json:"fontFamily" yaml:"font_family,omitempty"`
|
||||
FontSize int `json:"fontSize" yaml:"font_size"`
|
||||
ShowLineNum bool `json:"showLineNum" yaml:"show_line_num"`
|
||||
ShowFolding bool `json:"showFolding" yaml:"show_folding"`
|
||||
DropText bool `json:"dropText" yaml:"drop_text"`
|
||||
Links bool `json:"links" yaml:"links"`
|
||||
EntryTextAlign int `json:"entryTextAlign" yaml:"entry_text_align"`
|
||||
}
|
||||
|
||||
type PreferencesCli struct {
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
package types
|
||||
|
||||
type ListEntryItem struct {
|
||||
Index int `json:"index"`
|
||||
Value any `json:"v"`
|
||||
DisplayValue string `json:"dv,omitempty"`
|
||||
}
|
||||
|
||||
type ListReplaceItem struct {
|
||||
Index int64 `json:"index"`
|
||||
Index int `json:"index"`
|
||||
Value any `json:"v,omitempty"`
|
||||
DisplayValue string `json:"dv,omitempty"`
|
||||
}
|
||||
|
@ -32,7 +33,7 @@ type SetEntryItem struct {
|
|||
type ZSetEntryItem struct {
|
||||
Score float64 `json:"s"`
|
||||
ScoreStr string `json:"ss,omitempty"`
|
||||
Value string `json:"v"`
|
||||
Value any `json:"v"`
|
||||
DisplayValue string `json:"dv,omitempty"`
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ const DECODE_BASE64 = "Base64"
|
|||
const DECODE_GZIP = "GZip"
|
||||
const DECODE_DEFLATE = "Deflate"
|
||||
const DECODE_ZSTD = "ZStd"
|
||||
const DECODE_LZ4 = "LZ4"
|
||||
const DECODE_BROTLI = "Brotli"
|
||||
const DECODE_MSGPACK = "Msgpack"
|
||||
const DECODE_PHP = "PHP"
|
||||
|
|
|
@ -24,7 +24,7 @@ func (BinaryConvert) Encode(str string) (string, bool) {
|
|||
|
||||
func (BinaryConvert) Decode(str string) (string, bool) {
|
||||
var binary strings.Builder
|
||||
for _, char := range str {
|
||||
for _, char := range []byte(str) {
|
||||
binary.WriteString(fmt.Sprintf("%08b", int(char)))
|
||||
}
|
||||
return binary.String(), true
|
||||
|
|
|
@ -24,6 +24,7 @@ var (
|
|||
gzipConv GZipConvert
|
||||
deflateConv DeflateConvert
|
||||
zstdConv ZStdConvert
|
||||
lz4Conv LZ4Convert
|
||||
brotliConv BrotliConvert
|
||||
msgpackConv MsgpackConvert
|
||||
phpConv = NewPhpConvert()
|
||||
|
@ -44,6 +45,7 @@ var BuildInDecoders = map[string]DataConvert{
|
|||
types.DECODE_GZIP: gzipConv,
|
||||
types.DECODE_DEFLATE: deflateConv,
|
||||
types.DECODE_ZSTD: zstdConv,
|
||||
types.DECODE_LZ4: lz4Conv,
|
||||
types.DECODE_BROTLI: brotliConv,
|
||||
types.DECODE_MSGPACK: msgpackConv,
|
||||
types.DECODE_PHP: phpConv,
|
||||
|
@ -138,6 +140,11 @@ func autoDecode(str string, customDecoder []CmdConvert) (value, resultDecode str
|
|||
return
|
||||
}
|
||||
|
||||
if value, ok = lz4Conv.Decode(str); ok {
|
||||
resultDecode = types.DECODE_LZ4
|
||||
return
|
||||
}
|
||||
|
||||
// FIXME: skip decompress with brotli due to incorrect format checking
|
||||
//if value, ok = decodeBrotli(str); ok {
|
||||
// resultDecode = types.DECODE_BROTLI
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
package convutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
strutil "tinyrdm/backend/utils/string"
|
||||
)
|
||||
|
||||
type JsonConvert struct{}
|
||||
|
@ -16,18 +15,11 @@ func (JsonConvert) Decode(str string) (string, bool) {
|
|||
trimedStr := strings.TrimSpace(str)
|
||||
if (strings.HasPrefix(trimedStr, "{") && strings.HasSuffix(trimedStr, "}")) ||
|
||||
(strings.HasPrefix(trimedStr, "[") && strings.HasSuffix(trimedStr, "]")) {
|
||||
var out bytes.Buffer
|
||||
if err := json.Indent(&out, []byte(trimedStr), "", " "); err == nil {
|
||||
return out.String(), true
|
||||
}
|
||||
return strutil.JSONBeautify(trimedStr, " "), true
|
||||
}
|
||||
return str, false
|
||||
}
|
||||
|
||||
func (JsonConvert) Encode(str string) (string, bool) {
|
||||
var dst bytes.Buffer
|
||||
if err := json.Compact(&dst, []byte(str)); err != nil {
|
||||
return str, false
|
||||
}
|
||||
return dst.String(), true
|
||||
return strutil.JSONMinify(str), true
|
||||
}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
package convutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/pierrec/lz4/v4"
|
||||
"io"
|
||||
)
|
||||
|
||||
type LZ4Convert struct{}
|
||||
|
||||
func (LZ4Convert) Enable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (LZ4Convert) Encode(str string) (string, bool) {
|
||||
var compress = func(b []byte) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
writer := lz4.NewWriter(&buf)
|
||||
if _, err := writer.Write([]byte(str)); err != nil {
|
||||
writer.Close()
|
||||
return "", err
|
||||
}
|
||||
writer.Close()
|
||||
return string(buf.Bytes()), nil
|
||||
}
|
||||
|
||||
if gzipStr, err := compress([]byte(str)); err == nil {
|
||||
return gzipStr, true
|
||||
}
|
||||
return str, false
|
||||
}
|
||||
|
||||
func (LZ4Convert) Decode(str string) (string, bool) {
|
||||
reader := lz4.NewReader(bytes.NewReader([]byte(str)))
|
||||
if decompressed, err := io.ReadAll(reader); err == nil {
|
||||
return string(decompressed), true
|
||||
}
|
||||
return str, false
|
||||
}
|
|
@ -11,9 +11,12 @@ func (MsgpackConvert) Enable() bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func (MsgpackConvert) Encode(str string) (string, bool) {
|
||||
func (c MsgpackConvert) Encode(str string) (string, bool) {
|
||||
var obj map[string]any
|
||||
if err := json.Unmarshal([]byte(str), &obj); err == nil {
|
||||
for k, v := range obj {
|
||||
obj[k] = c.TryFloatToInt(v)
|
||||
}
|
||||
if b, err := msgpack.Marshal(obj); err == nil {
|
||||
return string(b), true
|
||||
}
|
||||
|
@ -43,3 +46,25 @@ func (MsgpackConvert) Decode(str string) (string, bool) {
|
|||
|
||||
return str, false
|
||||
}
|
||||
|
||||
func (c MsgpackConvert) TryFloatToInt(input any) any {
|
||||
switch val := input.(type) {
|
||||
case map[string]any:
|
||||
for k, v := range val {
|
||||
val[k] = c.TryFloatToInt(v)
|
||||
}
|
||||
return val
|
||||
case []any:
|
||||
for i, v := range val {
|
||||
val[i] = c.TryFloatToInt(v)
|
||||
}
|
||||
return val
|
||||
case float64:
|
||||
if val == float64(int(val)) {
|
||||
return int(val)
|
||||
}
|
||||
return val
|
||||
default:
|
||||
return val
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package convutil
|
||||
|
||||
import "os/exec"
|
||||
import (
|
||||
"os/exec"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
type PickleConvert struct {
|
||||
CmdConvert
|
||||
|
@ -49,6 +52,14 @@ func NewPickleConvert() *PickleConvert {
|
|||
}
|
||||
}
|
||||
// check if pickle available
|
||||
if runtime.GOOS == "darwin" {
|
||||
// the xcode-select installation prompt may appear on macOS
|
||||
// so check it manually in advance
|
||||
if _, err = exec.LookPath("xcode-select"); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = runCommand(c.DecodePath, "-c", "import pickle"); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -2,9 +2,9 @@ package convutil
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
strutil "tinyrdm/backend/utils/string"
|
||||
"unicode"
|
||||
"unicode/utf16"
|
||||
"unicode/utf8"
|
||||
|
@ -20,22 +20,16 @@ func (UnicodeJsonConvert) Decode(str string) (string, bool) {
|
|||
trimedStr := strings.TrimSpace(str)
|
||||
if (strings.HasPrefix(trimedStr, "{") && strings.HasSuffix(trimedStr, "}")) ||
|
||||
(strings.HasPrefix(trimedStr, "[") && strings.HasSuffix(trimedStr, "]")) {
|
||||
var out bytes.Buffer
|
||||
if err := json.Indent(&out, []byte(trimedStr), "", " "); err == nil {
|
||||
if quoteStr, ok := UnquoteUnicodeJson(out.Bytes()); ok {
|
||||
return string(quoteStr), true
|
||||
}
|
||||
resultStr := strutil.JSONBeautify(trimedStr, " ")
|
||||
if quoteStr, ok := UnquoteUnicodeJson([]byte(resultStr)); ok {
|
||||
return string(quoteStr), true
|
||||
}
|
||||
}
|
||||
return str, false
|
||||
}
|
||||
|
||||
func (UnicodeJsonConvert) Encode(str string) (string, bool) {
|
||||
var dst bytes.Buffer
|
||||
if err := json.Compact(&dst, []byte(str)); err != nil {
|
||||
return str, false
|
||||
}
|
||||
return dst.String(), true
|
||||
return strutil.JSONMinify(str), true
|
||||
}
|
||||
|
||||
func UnquoteUnicodeJson(s []byte) ([]byte, bool) {
|
||||
|
|
|
@ -107,27 +107,6 @@ func Merge[M ~map[K]V, K Hashable, V any](mapArr ...M) M {
|
|||
return result
|
||||
}
|
||||
|
||||
// DeepMerge 深度递归覆盖src值到dst中
|
||||
// 将返回新的值
|
||||
func DeepMerge[M ~map[K]any, K Hashable](src1, src2 M) M {
|
||||
out := make(map[K]any, len(src1))
|
||||
for k, v := range src1 {
|
||||
out[k] = v
|
||||
}
|
||||
for k, v := range src2 {
|
||||
if v1, ok := v.(map[K]any); ok {
|
||||
if bv, ok := out[k]; ok {
|
||||
if bv1, ok := bv.(map[K]any); ok {
|
||||
out[k] = DeepMerge(bv1, v1)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Omit 根据条件省略指定元素
|
||||
func Omit[M ~map[K]V, K Hashable, V any](m M, omitFunc func(k K, v V) bool) (M, []K) {
|
||||
result := M{}
|
||||
|
|
|
@ -1,137 +1,11 @@
|
|||
package sliceutil
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
. "tinyrdm/backend/utils"
|
||||
)
|
||||
|
||||
// Get 获取指定索引的值, 如果不存在则返回默认值
|
||||
func Get[S ~[]T, T any](arr S, index int, defaultVal T) T {
|
||||
if index < 0 || index >= len(arr) {
|
||||
return defaultVal
|
||||
}
|
||||
return arr[index]
|
||||
}
|
||||
|
||||
// Remove 删除指定索引的元素
|
||||
func Remove[S ~[]T, T any](arr S, index int) S {
|
||||
return append(arr[:index], arr[index+1:]...)
|
||||
}
|
||||
|
||||
// RemoveIf 移除指定条件的元素
|
||||
func RemoveIf[S ~[]T, T any](arr S, cond func(T) bool) S {
|
||||
l := len(arr)
|
||||
if l <= 0 {
|
||||
return arr
|
||||
}
|
||||
for i := l - 1; i >= 0; i-- {
|
||||
if cond(arr[i]) {
|
||||
arr = append(arr[:i], arr[i+1:]...)
|
||||
}
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
// RemoveRange 删除从[from, to]部分元素
|
||||
func RemoveRange[S ~[]T, T any](arr S, from, to int) S {
|
||||
return append(arr[:from], arr[to:]...)
|
||||
}
|
||||
|
||||
// Find 查找指定条件的元素第一个出现位置
|
||||
func Find[S ~[]T, T any](arr S, matchFunc func(int) bool) (int, bool) {
|
||||
total := len(arr)
|
||||
for i := 0; i < total; i++ {
|
||||
if matchFunc(i) {
|
||||
return i, true
|
||||
}
|
||||
}
|
||||
return -1, false
|
||||
}
|
||||
|
||||
// AnyMatch 判断是否有任意元素符合条件
|
||||
func AnyMatch[S ~[]T, T any](arr S, matchFunc func(int) bool) bool {
|
||||
total := len(arr)
|
||||
if total > 0 {
|
||||
for i := 0; i < total; i++ {
|
||||
if matchFunc(i) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// AllMatch 判断是否所有元素都符合条件
|
||||
func AllMatch[S ~[]T, T any](arr S, matchFunc func(int) bool) bool {
|
||||
total := len(arr)
|
||||
for i := 0; i < total; i++ {
|
||||
if !matchFunc(i) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Equals 比较两个切片内容是否完全一致
|
||||
func Equals[S ~[]T, T comparable](arr1, arr2 S) bool {
|
||||
if &arr1 == &arr2 {
|
||||
return true
|
||||
}
|
||||
|
||||
len1, len2 := len(arr1), len(arr2)
|
||||
if len1 != len2 {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len1; i++ {
|
||||
if arr1[i] != arr2[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Contains 判断数组是否包含指定元素
|
||||
func Contains[S ~[]T, T Hashable](arr S, elem T) bool {
|
||||
return AnyMatch(arr, func(idx int) bool {
|
||||
return arr[idx] == elem
|
||||
})
|
||||
}
|
||||
|
||||
// ContainsAny 判断数组是否包含任意指定元素
|
||||
func ContainsAny[S ~[]T, T Hashable](arr S, elems ...T) bool {
|
||||
for _, elem := range elems {
|
||||
if Contains(arr, elem) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ContainsAll 判断数组是否包含所有指定元素
|
||||
func ContainsAll[S ~[]T, T Hashable](arr S, elems ...T) bool {
|
||||
for _, elem := range elems {
|
||||
if !Contains(arr, elem) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Filter 筛选出符合指定条件的所有元素
|
||||
func Filter[S ~[]T, T any](arr S, filterFunc func(int) bool) []T {
|
||||
total := len(arr)
|
||||
var result []T
|
||||
for i := 0; i < total; i++ {
|
||||
if filterFunc(i) {
|
||||
result = append(result, arr[i])
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Map 数组映射转换
|
||||
// Map map items to new array
|
||||
func Map[S ~[]T, T any, R any](arr S, mappingFunc func(int) R) []R {
|
||||
total := len(arr)
|
||||
result := make([]R, total)
|
||||
|
@ -141,7 +15,7 @@ func Map[S ~[]T, T any, R any](arr S, mappingFunc func(int) R) []R {
|
|||
return result
|
||||
}
|
||||
|
||||
// FilterMap 数组过滤和映射转换
|
||||
// FilterMap filter and map items to new array
|
||||
func FilterMap[S ~[]T, T any, R any](arr S, mappingFunc func(int) (R, bool)) []R {
|
||||
total := len(arr)
|
||||
result := make([]R, 0, total)
|
||||
|
@ -155,68 +29,7 @@ func FilterMap[S ~[]T, T any, R any](arr S, mappingFunc func(int) (R, bool)) []R
|
|||
return result
|
||||
}
|
||||
|
||||
// ToMap 数组转键值对
|
||||
func ToMap[S ~[]T, T any, K Hashable, V any](arr S, mappingFunc func(int) (K, V)) map[K]V {
|
||||
total := len(arr)
|
||||
result := map[K]V{}
|
||||
for i := 0; i < total; i++ {
|
||||
key, val := mappingFunc(i)
|
||||
result[key] = val
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Flat 二维数组扁平化
|
||||
func Flat[T any](arr [][]T) []T {
|
||||
total := len(arr)
|
||||
var result []T
|
||||
for i := 0; i < total; i++ {
|
||||
subTotal := len(arr[i])
|
||||
for j := 0; j < subTotal; j++ {
|
||||
result = append(result, arr[i][j])
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// FlatMap 二维数组扁平化映射
|
||||
func FlatMap[T any, R any](arr [][]T, mappingFunc func(int, int) R) []R {
|
||||
total := len(arr)
|
||||
var result []R
|
||||
for i := 0; i < total; i++ {
|
||||
subTotal := len(arr[i])
|
||||
for j := 0; j < subTotal; j++ {
|
||||
result = append(result, mappingFunc(i, j))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func FlatValueMap[T Hashable](arr [][]T) []T {
|
||||
return FlatMap(arr, func(i, j int) T {
|
||||
return arr[i][j]
|
||||
})
|
||||
}
|
||||
|
||||
// Reduce 数组累计
|
||||
func Reduce[S ~[]T, T any, R any](arr S, init R, reduceFunc func(R, T) R) R {
|
||||
result := init
|
||||
for _, item := range arr {
|
||||
result = reduceFunc(result, item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Reverse 反转数组(会修改原数组)
|
||||
func Reverse[S ~[]T, T any](arr S) S {
|
||||
total := len(arr)
|
||||
for i := 0; i < total/2; i++ {
|
||||
arr[i], arr[total-i-1] = arr[total-i-1], arr[i]
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
// Join 数组拼接转字符串
|
||||
// Join join any array to a single string by custom function
|
||||
func Join[S ~[]T, T any](arr S, sep string, toStringFunc func(int) string) string {
|
||||
total := len(arr)
|
||||
if total <= 0 {
|
||||
|
@ -236,21 +49,14 @@ func Join[S ~[]T, T any](arr S, sep string, toStringFunc func(int) string) strin
|
|||
return sb.String()
|
||||
}
|
||||
|
||||
// JoinString 字符串数组拼接成字符串
|
||||
// JoinString join string array to a single string
|
||||
func JoinString(arr []string, sep string) string {
|
||||
return Join(arr, sep, func(idx int) string {
|
||||
return arr[idx]
|
||||
})
|
||||
}
|
||||
|
||||
// JoinInt 整形数组拼接转字符串
|
||||
func JoinInt(arr []int, sep string) string {
|
||||
return Join(arr, sep, func(idx int) string {
|
||||
return strconv.Itoa(arr[idx])
|
||||
})
|
||||
}
|
||||
|
||||
// Unique 数组去重
|
||||
// Unique filter unique item
|
||||
func Unique[S ~[]T, T Hashable](arr S) S {
|
||||
result := make(S, 0, len(arr))
|
||||
uniKeys := map[T]struct{}{}
|
||||
|
@ -263,136 +69,3 @@ func Unique[S ~[]T, T Hashable](arr S) S {
|
|||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// UniqueEx 数组去重(任意类型)
|
||||
// @param toKeyFunc 数组元素转为唯一标识字符串函数, 如转为哈希值等
|
||||
func UniqueEx[S ~[]T, T any](arr S, toKeyFunc func(i int) string) S {
|
||||
result := make(S, 0, len(arr))
|
||||
keyArr := Map(arr, toKeyFunc)
|
||||
uniKeys := map[string]struct{}{}
|
||||
var exists bool
|
||||
for i, item := range arr {
|
||||
if _, exists = uniKeys[keyArr[i]]; !exists {
|
||||
uniKeys[keyArr[i]] = struct{}{}
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Sort 顺序排序(会修改原数组)
|
||||
func Sort[S ~[]T, T Hashable](arr S) S {
|
||||
sort.Slice(arr, func(i, j int) bool {
|
||||
return arr[i] <= arr[j]
|
||||
})
|
||||
return arr
|
||||
}
|
||||
|
||||
// SortDesc 倒序排序(会修改原数组)
|
||||
func SortDesc[S ~[]T, T Hashable](arr S) S {
|
||||
sort.Slice(arr, func(i, j int) bool {
|
||||
return arr[i] > arr[j]
|
||||
})
|
||||
return arr
|
||||
}
|
||||
|
||||
// Union 返回两个切片共同拥有的元素
|
||||
func Union[S ~[]T, T Hashable](arr1 S, arr2 S) S {
|
||||
hashArr, compArr := arr1, arr2
|
||||
if len(arr1) < len(arr2) {
|
||||
hashArr, compArr = compArr, hashArr
|
||||
}
|
||||
hash := map[T]struct{}{}
|
||||
for _, item := range hashArr {
|
||||
hash[item] = struct{}{}
|
||||
}
|
||||
|
||||
uniq := map[T]struct{}{}
|
||||
ret := make(S, 0, len(compArr))
|
||||
exists := false
|
||||
for _, item := range compArr {
|
||||
if _, exists = hash[item]; exists {
|
||||
if _, exists = uniq[item]; !exists {
|
||||
ret = append(ret, item)
|
||||
uniq[item] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// Exclude 返回不包含的元素
|
||||
func Exclude[S ~[]T, T Hashable](arr1 S, arr2 S) S {
|
||||
diff := make([]T, 0, len(arr1))
|
||||
hash := map[T]struct{}{}
|
||||
for _, item := range arr2 {
|
||||
hash[item] = struct{}{}
|
||||
}
|
||||
|
||||
for _, item := range arr1 {
|
||||
if _, exists := hash[item]; !exists {
|
||||
diff = append(diff, item)
|
||||
}
|
||||
}
|
||||
return diff
|
||||
}
|
||||
|
||||
// PadLeft 左边填充指定数量
|
||||
func PadLeft[S ~[]T, T any](arr S, val T, count int) S {
|
||||
prefix := make(S, count)
|
||||
for i := 0; i < count; i++ {
|
||||
prefix[i] = val
|
||||
}
|
||||
arr = append(prefix, arr...)
|
||||
return arr
|
||||
}
|
||||
|
||||
// PadRight 右边填充指定数量
|
||||
func PadRight[S ~[]T, T any](arr S, val T, count int) S {
|
||||
for i := 0; i < count; i++ {
|
||||
arr = append(arr, val)
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
// RemoveLeft 移除左侧相同元素
|
||||
func RemoveLeft[S ~[]T, T comparable](arr S, val T) S {
|
||||
for len(arr) > 0 && arr[0] == val {
|
||||
arr = arr[1:]
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
// RemoveRight 移除右侧相同元素
|
||||
func RemoveRight[S ~[]T, T comparable](arr S, val T) S {
|
||||
for {
|
||||
length := len(arr)
|
||||
if length > 0 && arr[length-1] == val {
|
||||
arr = arr[:length]
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
// Count 统计制定条件元素数量
|
||||
func Count[S ~[]T, T any](arr S, filter func(int) bool) int {
|
||||
count := 0
|
||||
for i := range arr {
|
||||
if filter(i) {
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// Group 根据分组函数对数组进行分组汇总
|
||||
func Group[S ~[]T, T any, K Hashable, R any](arr S, groupFunc func(int) (K, R)) map[K][]R {
|
||||
ret := map[K][]R{}
|
||||
for i := range arr {
|
||||
key, val := groupFunc(i)
|
||||
ret[key] = append(ret[key], val)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
|
|
@ -135,29 +135,38 @@ func SplitCmd(cmd string) []string {
|
|||
var result []string
|
||||
var curStr strings.Builder
|
||||
var preChar int32
|
||||
inQuotes := false
|
||||
var quotesChar int32
|
||||
|
||||
for _, char := range []rune(cmd) {
|
||||
if char == '"' && preChar != '\\' {
|
||||
inQuotes = !inQuotes
|
||||
} else if char == ' ' && !inQuotes {
|
||||
if curStr.Len() > 0 {
|
||||
if part, e := strconv.Unquote(`"` + curStr.String() + `"`); e == nil {
|
||||
result = append(result, part)
|
||||
}
|
||||
curStr.Reset()
|
||||
cmdRune := []rune(cmd)
|
||||
for _, char := range cmdRune {
|
||||
if (char == '"' || char == '\'') && preChar != '\\' && (quotesChar == 0 || quotesChar == char) {
|
||||
if quotesChar != 0 {
|
||||
quotesChar = 0
|
||||
} else {
|
||||
quotesChar = char
|
||||
}
|
||||
} else if char == ' ' && quotesChar == 0 {
|
||||
result = append(result, curStr.String())
|
||||
curStr.Reset()
|
||||
} else {
|
||||
curStr.WriteRune(char)
|
||||
}
|
||||
preChar = char
|
||||
}
|
||||
result = append(result, curStr.String())
|
||||
|
||||
if curStr.Len() > 0 {
|
||||
if part, e := strconv.Unquote(`"` + curStr.String() + `"`); e == nil {
|
||||
result = append(result, part)
|
||||
result = sliceutil.FilterMap(result, func(i int) (string, bool) {
|
||||
var part = strings.TrimSpace(result[i])
|
||||
if len(part) <= 0 {
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
if strings.Contains(part, "\\") {
|
||||
if unquotePart, e := strconv.Unquote(`"` + part + `"`); e == nil {
|
||||
return unquotePart, true
|
||||
}
|
||||
}
|
||||
return part, true
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -0,0 +1,158 @@
|
|||
package strutil
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// Convert from https://github.com/ObuchiYuki/SwiftJSONFormatter
|
||||
|
||||
// ArrayIterator defines the iterator for an array
|
||||
type ArrayIterator[T any] struct {
|
||||
array []T
|
||||
head int
|
||||
}
|
||||
|
||||
// NewArrayIterator initializes a new ArrayIterator with the given array
|
||||
func NewArrayIterator[T any](array []T) *ArrayIterator[T] {
|
||||
return &ArrayIterator[T]{
|
||||
array: array,
|
||||
head: -1,
|
||||
}
|
||||
}
|
||||
|
||||
// HasNext returns true if there are more elements to iterate over
|
||||
func (it *ArrayIterator[T]) HasNext() bool {
|
||||
return it.head+1 < len(it.array)
|
||||
}
|
||||
|
||||
// PeekNext returns the next element without advancing the iterator
|
||||
func (it *ArrayIterator[T]) PeekNext() *T {
|
||||
if it.head+1 < len(it.array) {
|
||||
return &it.array[it.head+1]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Next returns the next element and advances the iterator
|
||||
func (it *ArrayIterator[T]) Next() *T {
|
||||
defer func() {
|
||||
it.head++
|
||||
}()
|
||||
return it.PeekNext()
|
||||
}
|
||||
|
||||
// JSONBeautify formats a JSON string with indentation
|
||||
func JSONBeautify(value string, indent string) string {
|
||||
if len(indent) <= 0 {
|
||||
indent = " "
|
||||
}
|
||||
return format(value, indent, "\n", " ")
|
||||
}
|
||||
|
||||
// JSONMinify formats a JSON string by removing all unnecessary whitespace
|
||||
func JSONMinify(value string) string {
|
||||
return format(value, "", "", "")
|
||||
}
|
||||
|
||||
// format applies the specified formatting to a JSON string
|
||||
func format(value string, indent string, newLine string, separator string) string {
|
||||
var formatted strings.Builder
|
||||
chars := NewArrayIterator([]rune(value))
|
||||
indentLevel := 0
|
||||
|
||||
for chars.HasNext() {
|
||||
if char := chars.Next(); char != nil {
|
||||
switch *char {
|
||||
case '{', '[':
|
||||
formatted.WriteRune(*char)
|
||||
consumeWhitespaces(chars)
|
||||
peeked := chars.PeekNext()
|
||||
if peeked != nil && (*peeked == '}' || *peeked == ']') {
|
||||
chars.Next()
|
||||
formatted.WriteRune(*peeked)
|
||||
} else {
|
||||
indentLevel++
|
||||
formatted.WriteString(newLine)
|
||||
formatted.WriteString(strings.Repeat(indent, indentLevel))
|
||||
}
|
||||
case '}', ']':
|
||||
indentLevel--
|
||||
formatted.WriteString(newLine)
|
||||
formatted.WriteString(strings.Repeat(indent, max(0, indentLevel)))
|
||||
formatted.WriteRune(*char)
|
||||
case '"':
|
||||
str := consumeString(chars)
|
||||
//str = convertUnicodeString(str)
|
||||
formatted.WriteString(str)
|
||||
case ',':
|
||||
consumeWhitespaces(chars)
|
||||
formatted.WriteRune(',')
|
||||
peeked := chars.PeekNext()
|
||||
if peeked != nil && *peeked != '}' && *peeked != ']' {
|
||||
formatted.WriteString(newLine)
|
||||
formatted.WriteString(strings.Repeat(indent, max(0, indentLevel)))
|
||||
}
|
||||
case ':':
|
||||
formatted.WriteString(":" + separator)
|
||||
default:
|
||||
if !unicode.IsSpace(*char) {
|
||||
formatted.WriteRune(*char)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return formatted.String()
|
||||
}
|
||||
|
||||
// consumeWhitespaces advances the iterator past any whitespace characters
|
||||
func consumeWhitespaces(iter *ArrayIterator[rune]) {
|
||||
for iter.HasNext() {
|
||||
if peeked := iter.PeekNext(); peeked != nil && unicode.IsSpace(*peeked) {
|
||||
iter.Next()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// consumeString consumes a JSON string value from the iterator
|
||||
func consumeString(iter *ArrayIterator[rune]) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteRune('"')
|
||||
escaping := false
|
||||
|
||||
for iter.HasNext() {
|
||||
if char := iter.Next(); char != nil {
|
||||
if *char == '\n' {
|
||||
return sb.String() // Unterminated string
|
||||
}
|
||||
|
||||
sb.WriteRune(*char)
|
||||
|
||||
if escaping {
|
||||
escaping = false
|
||||
} else {
|
||||
if *char == '\\' {
|
||||
escaping = true
|
||||
}
|
||||
if *char == '"' {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func convertUnicodeString(str string) string {
|
||||
// TODO: quote UTF-16 characters
|
||||
//if len(str) > 2 {
|
||||
// if unqStr, err := strconv.Unquote(str); err == nil {
|
||||
// return strconv.Quote(unqStr)
|
||||
// }
|
||||
//}
|
||||
return str
|
||||
}
|
|
@ -18,7 +18,7 @@
|
|||
<key>CFBundleIconFile</key>
|
||||
<string>iconfile</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.13.0</string>
|
||||
<string>11.7.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>true</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
<key>CFBundleIconFile</key>
|
||||
<string>iconfile</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.13.0</string>
|
||||
<string>11.7.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>true</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
File diff suppressed because it is too large
Load Diff
|
@ -9,25 +9,26 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"chart.js": "^4.4.2",
|
||||
"dayjs": "^1.11.10",
|
||||
"chart.js": "^4.4.8",
|
||||
"copy-text-to-clipboard": "^3.2.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"lodash": "^4.17.21",
|
||||
"monaco-editor": "^0.48.0",
|
||||
"pinia": "^2.1.7",
|
||||
"sass": "^1.75.0",
|
||||
"vue": "^3.4.25",
|
||||
"vue-chartjs": "^5.3.1",
|
||||
"vue-i18n": "^9.13.1",
|
||||
"monaco-editor": "^0.47.0",
|
||||
"pinia": "^3.0.1",
|
||||
"sass": "^1.85.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-chartjs": "^5.3.2",
|
||||
"vue-i18n": "^11.1.2",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-addon-fit": "^0.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"naive-ui": "^2.38.1",
|
||||
"prettier": "^3.2.5",
|
||||
"unplugin-auto-import": "^0.17.5",
|
||||
"unplugin-icons": "^0.18.5",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"vite": "^5.2.10"
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"naive-ui": "^2.41.0",
|
||||
"prettier": "^3.5.3",
|
||||
"unplugin-auto-import": "^19.1.1",
|
||||
"unplugin-icons": "^22.1.0",
|
||||
"unplugin-vue-components": "^28.4.1",
|
||||
"vite": "^6.2.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
015d99ba38ab1210c611b0adecf20175
|
||||
47ebcfd89e9e219e5b4ccf43ca2aa197
|
|
@ -1,7 +1,7 @@
|
|||
<script setup>
|
||||
import ContentPane from './components/content/ContentPane.vue'
|
||||
import BrowserPane from './components/sidebar/BrowserPane.vue'
|
||||
import { computed, onMounted, reactive, ref, watchEffect } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, reactive, ref, watchEffect } from 'vue'
|
||||
import { debounce } from 'lodash'
|
||||
import { useThemeVars } from 'naive-ui'
|
||||
import Ribbon from './components/sidebar/Ribbon.vue'
|
||||
|
@ -13,7 +13,7 @@ import ContentLogPane from './components/content/ContentLogPane.vue'
|
|||
import ContentValueTab from '@/components/content/ContentValueTab.vue'
|
||||
import ToolbarControlWidget from '@/components/common/ToolbarControlWidget.vue'
|
||||
import { EventsOn, WindowIsFullscreen, WindowIsMaximised, WindowToggleMaximise } from 'wailsjs/runtime/runtime.js'
|
||||
import { isMacOS } from '@/utils/platform.js'
|
||||
import { isMacOS, isWindows } from '@/utils/platform.js'
|
||||
import iconUrl from '@/assets/images/icon.png'
|
||||
import ResizeableWrapper from '@/components/common/ResizeableWrapper.vue'
|
||||
import { extraTheme } from '@/utils/extra_theme.js'
|
||||
|
@ -57,6 +57,9 @@ const logoPaddingLeft = ref(10)
|
|||
const maximised = ref(false)
|
||||
const hideRadius = ref(false)
|
||||
const wrapperStyle = computed(() => {
|
||||
if (isWindows()) {
|
||||
return {}
|
||||
}
|
||||
return hideRadius.value
|
||||
? {}
|
||||
: {
|
||||
|
@ -65,6 +68,11 @@ const wrapperStyle = computed(() => {
|
|||
}
|
||||
})
|
||||
const spinStyle = computed(() => {
|
||||
if (isWindows()) {
|
||||
return {
|
||||
backgroundColor: themeVars.value.bodyColor,
|
||||
}
|
||||
}
|
||||
return hideRadius.value
|
||||
? {
|
||||
backgroundColor: themeVars.value.bodyColor,
|
||||
|
@ -109,7 +117,28 @@ onMounted(async () => {
|
|||
onToggleFullscreen(fullscreen === true)
|
||||
const maximised = await WindowIsMaximised()
|
||||
onToggleMaximize(maximised)
|
||||
window.addEventListener('keydown', onKeyShortcut)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('keydown', onKeyShortcut)
|
||||
})
|
||||
|
||||
const onKeyShortcut = (e) => {
|
||||
const isCtrlOn = isMacOS() ? e.metaKey : e.ctrlKey
|
||||
switch (e.key) {
|
||||
case 'w':
|
||||
if (isCtrlOn) {
|
||||
// close current tab
|
||||
const tabStore = useTabStore()
|
||||
const currentTab = tabStore.currentTab
|
||||
if (currentTab != null) {
|
||||
tabStore.closeTab(currentTab.name)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -145,7 +174,7 @@ onMounted(async () => {
|
|||
<div v-show="tabStore.nav === 'browser'" class="app-toolbar-tab flex-item-expand">
|
||||
<content-value-tab />
|
||||
</div>
|
||||
<div class="flex-item-expand"></div>
|
||||
<div class="flex-item-expand" style="min-width: 15px"></div>
|
||||
<!-- simulate window control buttons -->
|
||||
<toolbar-control-widget
|
||||
v-if="!isMacOS()"
|
||||
|
@ -233,6 +262,7 @@ onMounted(async () => {
|
|||
align-self: flex-end;
|
||||
margin-bottom: -1px;
|
||||
margin-left: 3px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#app-content {
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
|
@ -5,14 +5,16 @@ import Edit from '@/components/icons/Edit.vue'
|
|||
import Close from '@/components/icons/Close.vue'
|
||||
import Save from '@/components/icons/Save.vue'
|
||||
import Copy from '@/components/icons/Copy.vue'
|
||||
import Refresh from '@/components/icons/Refresh.vue'
|
||||
|
||||
const props = defineProps({
|
||||
bindKey: String,
|
||||
editing: Boolean,
|
||||
readonly: Boolean,
|
||||
canRefresh: Boolean,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['edit', 'delete', 'copy', 'save', 'cancel'])
|
||||
const emit = defineEmits(['edit', 'delete', 'copy', 'refresh', 'save', 'cancel'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -22,6 +24,7 @@ const emit = defineEmits(['edit', 'delete', 'copy', 'save', 'cancel'])
|
|||
</div>
|
||||
<div v-else class="flex-box-h edit-column-func">
|
||||
<icon-button :icon="Copy" :title="$t('interface.copy_value')" @click="emit('copy')" />
|
||||
<icon-button v-if="props.canRefresh" :icon="Refresh" :title="$t('interface.reload')" @click="emit('refresh')" />
|
||||
<icon-button v-if="!props.readonly" :icon="Edit" :title="$t('interface.edit_row')" @click="emit('edit')" />
|
||||
<n-popconfirm
|
||||
:negative-text="$t('common.cancel')"
|
||||
|
|
|
@ -43,7 +43,7 @@ const hasTooltip = computed(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<n-tooltip v-if="hasTooltip" :delay="tooltipDelay" :show-arrow="false">
|
||||
<n-tooltip v-if="hasTooltip" :delay="tooltipDelay" :keep-alive-on-hover="false" :show-arrow="false">
|
||||
<template #trigger>
|
||||
<n-button
|
||||
:class="props.buttonClass"
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { computed, h } from 'vue'
|
||||
import { NSpace, useThemeVars } from 'naive-ui'
|
||||
import { types, typesBgColor, typesColor, typesShortName } from '@/consts/support_redis_type.js'
|
||||
import { get, map, toUpper } from 'lodash'
|
||||
import { get, isEmpty, map, toUpper } from 'lodash'
|
||||
import RedisTypeTag from '@/components/common/RedisTypeTag.vue'
|
||||
|
||||
const props = defineProps({
|
||||
|
@ -14,6 +14,14 @@ const props = defineProps({
|
|||
type: String,
|
||||
default: 'bottom-start',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disableTip: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:value', 'select'])
|
||||
|
@ -82,22 +90,42 @@ const handleSelect = (select) => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<n-dropdown
|
||||
:options="options"
|
||||
:placement="props.placement"
|
||||
:render-icon="renderIcon"
|
||||
:render-label="renderLabel"
|
||||
show-arrow
|
||||
@select="handleSelect">
|
||||
<n-tag
|
||||
:bordered="true"
|
||||
:color="{ color: backgroundColor, textColor: fontColor }"
|
||||
class="redis-tag"
|
||||
size="medium"
|
||||
strong>
|
||||
{{ displayValue }}
|
||||
</n-tag>
|
||||
</n-dropdown>
|
||||
<template v-if="props.disabled">
|
||||
<n-tooltip :disabled="isEmpty(props.disableTip)">
|
||||
<div>{{ props.disableTip }}</div>
|
||||
<template #trigger>
|
||||
<n-tag
|
||||
:bordered="true"
|
||||
:color="{ color: backgroundColor, textColor: fontColor }"
|
||||
class="redis-tag"
|
||||
disabled
|
||||
size="medium"
|
||||
strong>
|
||||
{{ displayValue }}
|
||||
</n-tag>
|
||||
</template>
|
||||
</n-tooltip>
|
||||
</template>
|
||||
<template v-else>
|
||||
<n-dropdown
|
||||
:disabled="props.disabled"
|
||||
:options="options"
|
||||
:placement="props.placement"
|
||||
:render-icon="renderIcon"
|
||||
:render-label="renderLabel"
|
||||
show-arrow
|
||||
@select="handleSelect">
|
||||
<n-tag
|
||||
:bordered="true"
|
||||
:color="{ color: backgroundColor, textColor: fontColor }"
|
||||
:disabled="props.disabled"
|
||||
class="redis-tag"
|
||||
size="medium"
|
||||
strong>
|
||||
{{ displayValue }}
|
||||
</n-tag>
|
||||
</n-dropdown>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -68,6 +68,7 @@ const label = computed(() => {
|
|||
'redis-type-tag-small': !props.short && props.size === 'small',
|
||||
'redis-type-tag-round': props.round,
|
||||
'redis-type-tag-loading': props.loading,
|
||||
'redis-type-tag': props.short,
|
||||
}"
|
||||
:color="{ color: backgroundColor, textColor: fontColor }"
|
||||
:size="props.size"
|
||||
|
@ -112,4 +113,13 @@ const label = computed(() => {
|
|||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
.redis-type-tag {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -12,6 +12,10 @@ const props = defineProps({
|
|||
},
|
||||
icons: Array,
|
||||
tTooltips: Array,
|
||||
tTooltipPlacement: {
|
||||
type: String,
|
||||
default: 'bottom',
|
||||
},
|
||||
iconSize: {
|
||||
type: [Number, String],
|
||||
default: 20,
|
||||
|
@ -45,6 +49,7 @@ const handleSwitch = (idx) => {
|
|||
v-for="(icon, i) in props.icons"
|
||||
:key="i"
|
||||
:disabled="!(props.tTooltips && props.tTooltips[i])"
|
||||
:placement="props.tTooltipPlacement"
|
||||
:show-arrow="false">
|
||||
<template #trigger>
|
||||
<n-button :focusable="false" :size="props.size" :tertiary="i !== props.value" @click="handleSwitch(i)">
|
||||
|
|
|
@ -168,5 +168,5 @@ defineExpose({
|
|||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/content';
|
||||
@use '@/styles/content';
|
||||
</style>
|
||||
|
|
|
@ -70,7 +70,7 @@ const isBlankValue = computed(() => {
|
|||
})
|
||||
|
||||
const selectedSubTab = computed(() => {
|
||||
const { subTab = 'status' } = tabStore.currentTab || {}
|
||||
const { subTab = BrowserTabType.Status } = tabStore.currentTab || {}
|
||||
return subTab
|
||||
})
|
||||
|
||||
|
@ -104,7 +104,7 @@ watch(
|
|||
}"
|
||||
:value="selectedSubTab"
|
||||
class="content-sub-tab"
|
||||
default-value="status"
|
||||
:default-value="BrowserTabType.Status.toString()"
|
||||
pane-class="content-sub-tab-pane"
|
||||
placement="top"
|
||||
tab-style="padding-left: 10px; padding-right: 10px;"
|
||||
|
@ -212,7 +212,7 @@ watch(
|
|||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/content';
|
||||
@use '@/styles/content';
|
||||
|
||||
.content-container {
|
||||
//padding: 5px 5px 0;
|
||||
|
|
|
@ -1,8 +1,32 @@
|
|||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import AddLink from '@/components/icons/AddLink.vue'
|
||||
import useDialogStore from 'stores/dialog.js'
|
||||
import { NButton, useThemeVars } from 'naive-ui'
|
||||
import { BrowserOpenURL } from 'wailsjs/runtime/runtime.js'
|
||||
import { find, includes, isEmpty } from 'lodash'
|
||||
import usePreferencesStore from 'stores/preferences.js'
|
||||
|
||||
const themeVars = useThemeVars()
|
||||
const dialogStore = useDialogStore()
|
||||
const prefStore = usePreferencesStore()
|
||||
|
||||
const onOpenSponsor = (link) => {
|
||||
BrowserOpenURL(link)
|
||||
}
|
||||
|
||||
const sponsorAd = computed(() => {
|
||||
try {
|
||||
const content = localStorage.getItem('sponsor_ad')
|
||||
const ads = JSON.parse(content)
|
||||
const ad = find(ads, ({ region }) => {
|
||||
return isEmpty(region) || includes(region, prefStore.currentLanguage)
|
||||
})
|
||||
return ad || null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -18,16 +42,27 @@ const dialogStore = useDialogStore()
|
|||
</n-button>
|
||||
</template>
|
||||
</n-empty>
|
||||
|
||||
<n-button v-if="sponsorAd != null" class="sponsor-ad" style="" text @click="onOpenSponsor(sponsorAd.link)">
|
||||
{{ sponsorAd.name }}
|
||||
</n-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/content';
|
||||
@use '@/styles/content';
|
||||
|
||||
.content-container {
|
||||
justify-content: center;
|
||||
padding: 5px;
|
||||
box-sizing: border-box;
|
||||
|
||||
& > .sponsor-ad {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
vertical-align: bottom;
|
||||
color: v-bind('themeVars.textColor3');
|
||||
}
|
||||
}
|
||||
|
||||
.color-preset-item {
|
||||
|
|
|
@ -2,32 +2,24 @@
|
|||
import Server from '@/components/icons/Server.vue'
|
||||
import useTabStore from 'stores/tab.js'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { get, map } from 'lodash'
|
||||
import { useThemeVars } from 'naive-ui'
|
||||
import useConnectionStore from 'stores/connections.js'
|
||||
import { extraTheme } from '@/utils/extra_theme.js'
|
||||
import usePreferencesStore from 'stores/preferences.js'
|
||||
import useBrowserStore from 'stores/browser.js'
|
||||
|
||||
/**
|
||||
* Value content tab on head
|
||||
*/
|
||||
|
||||
const themeVars = useThemeVars()
|
||||
const i18n = useI18n()
|
||||
const tabStore = useTabStore()
|
||||
const connectionStore = useConnectionStore()
|
||||
const browserStore = useBrowserStore()
|
||||
const prefStore = usePreferencesStore()
|
||||
|
||||
const onCloseTab = (tabIndex) => {
|
||||
const tab = get(tabStore.tabs, tabIndex)
|
||||
if (tab != null) {
|
||||
$dialog.warning(i18n.t('dialogue.close_confirm', { name: tab.name }), () => {
|
||||
browserStore.closeConnection(tab.name)
|
||||
})
|
||||
}
|
||||
tabStore.closeTab(tab.name)
|
||||
}
|
||||
|
||||
const tabMarkColor = computed(() => {
|
||||
|
|
|
@ -253,6 +253,15 @@ const onTermKey = (e) => {
|
|||
}
|
||||
// block all ctrl key combinations input
|
||||
return false
|
||||
} else {
|
||||
switch (e.key) {
|
||||
case 'Home': // move to head of line
|
||||
moveInputCursorTo(0)
|
||||
return false
|
||||
case 'End': // move to tail of line
|
||||
moveInputCursorTo(Number.MAX_SAFE_INTEGER)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
@ -313,6 +322,8 @@ const updateInput = (data) => {
|
|||
if (data == null || data.length <= 0) {
|
||||
return
|
||||
}
|
||||
// replace (Non-Breaking Space) with normal blank space
|
||||
data = data.replace(/\u00A0/g, ' ')
|
||||
|
||||
if (termInst == null) {
|
||||
return
|
||||
|
|
|
@ -249,4 +249,8 @@ onUnmounted(() => {
|
|||
left: 2px;
|
||||
right: 2px;
|
||||
}
|
||||
|
||||
:deep(.line-numbers) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -23,7 +23,7 @@ const props = defineProps({
|
|||
type: [String, Number],
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
type: [String, Array],
|
||||
},
|
||||
fieldLabel: {
|
||||
type: String,
|
||||
|
|
|
@ -3,20 +3,19 @@ import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue'
|
|||
import { debounce, filter, get, includes, isEmpty, join } from 'lodash'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useThemeVars } from 'naive-ui'
|
||||
import useBrowserStore from 'stores/browser.js'
|
||||
import Play from '@/components/icons/Play.vue'
|
||||
import Pause from '@/components/icons/Pause.vue'
|
||||
import { ExportLog, StartMonitor, StopMonitor } from 'wailsjs/go/services/monitorService.js'
|
||||
import { ClipboardSetText, EventsOff, EventsOn } from 'wailsjs/runtime/runtime.js'
|
||||
import { EventsOff, EventsOn } from 'wailsjs/runtime/runtime.js'
|
||||
import Copy from '@/components/icons/Copy.vue'
|
||||
import Export from '@/components/icons/Export.vue'
|
||||
import Delete from '@/components/icons/Delete.vue'
|
||||
import IconButton from '@/components/common/IconButton.vue'
|
||||
import Bottom from '@/components/icons/Bottom.vue'
|
||||
import copy from 'copy-text-to-clipboard'
|
||||
|
||||
const themeVars = useThemeVars()
|
||||
|
||||
const browserStore = useBrowserStore()
|
||||
const i18n = useI18n()
|
||||
const props = defineProps({
|
||||
server: {
|
||||
|
@ -95,15 +94,8 @@ const onStopMonitor = async () => {
|
|||
}
|
||||
|
||||
const onCopyLog = async () => {
|
||||
try {
|
||||
const content = join(data.list, '\n')
|
||||
const succ = await ClipboardSetText(content)
|
||||
if (succ) {
|
||||
$message.success(i18n.t('interface.copy_succ'))
|
||||
}
|
||||
} catch (e) {
|
||||
$message.error(e.message)
|
||||
}
|
||||
copy(join(data.list, '\n'))
|
||||
$message.success(i18n.t('interface.copy_succ'))
|
||||
}
|
||||
|
||||
const onExportLog = () => {
|
||||
|
@ -189,7 +181,7 @@ const onCleanLog = () => {
|
|||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/content';
|
||||
@use '@/styles/content';
|
||||
|
||||
.line-item {
|
||||
margin-bottom: 5px;
|
||||
|
|
|
@ -281,7 +281,7 @@ const onPublish = async () => {
|
|||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/content';
|
||||
@use '@/styles/content';
|
||||
|
||||
.total-message {
|
||||
margin: 10px 0 0;
|
||||
|
|
|
@ -80,9 +80,11 @@ const refreshInfo = async (force) => {
|
|||
}
|
||||
if (!isEmpty(props.server) && browserStore.isConnected(props.server)) {
|
||||
try {
|
||||
const info = await browserStore.getServerInfo(props.server)
|
||||
serverInfo.value = info
|
||||
_updateChart(info)
|
||||
const info = await browserStore.getServerInfo(props.server, true)
|
||||
if (!isEmpty(info)) {
|
||||
serverInfo.value = info
|
||||
_updateChart(info)
|
||||
}
|
||||
} finally {
|
||||
pageState.loading = false
|
||||
pageState.autoLoading = false
|
||||
|
@ -92,7 +94,7 @@ const refreshInfo = async (force) => {
|
|||
|
||||
const _updateChart = (info) => {
|
||||
let timeLabels = toRaw(cmdRate.value.labels)
|
||||
timeLabels = timeLabels.concat(dayjs().format('hh:mm:ss'))
|
||||
timeLabels = timeLabels.concat(dayjs().format('HH:mm:ss'))
|
||||
timeLabels = slice(timeLabels, Math.max(0, timeLabels.length - statusHistory))
|
||||
|
||||
// commands per seconds
|
||||
|
@ -144,7 +146,7 @@ const _updateChart = (info) => {
|
|||
const _mockChart = () => {
|
||||
const timeLabels = []
|
||||
for (let i = 0; i < 5; i++) {
|
||||
timeLabels.push(dayjs().add(5, 'seconds').format('hh:mm:ss'))
|
||||
timeLabels.push(dayjs().add(5, 'seconds').format('HH:mm:ss'))
|
||||
}
|
||||
|
||||
// commands per seconds
|
||||
|
@ -774,7 +776,7 @@ const clientTableColumns = computed(() => {
|
|||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/content';
|
||||
@use '@/styles/content';
|
||||
|
||||
.line-chart {
|
||||
display: flex;
|
||||
|
|
|
@ -222,5 +222,5 @@ const onListLimitChanged = (limit) => {
|
|||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/content';
|
||||
@use '@/styles/content';
|
||||
</style>
|
||||
|
|
|
@ -9,12 +9,12 @@ import RedisTypeTag from '@/components/common/RedisTypeTag.vue'
|
|||
import { useI18n } from 'vue-i18n'
|
||||
import IconButton from '@/components/common/IconButton.vue'
|
||||
import Copy from '@/components/icons/Copy.vue'
|
||||
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
|
||||
import { computed, onMounted, onUnmounted, reactive, watch } from 'vue'
|
||||
import { NIcon, useThemeVars } from 'naive-ui'
|
||||
import { timeout } from '@/utils/promise.js'
|
||||
import AutoRefreshForm from '@/components/common/AutoRefreshForm.vue'
|
||||
import { toHumanReadable } from '@/utils/date.js'
|
||||
import copy from 'copy-text-to-clipboard'
|
||||
|
||||
const props = defineProps({
|
||||
server: String,
|
||||
|
@ -139,15 +139,8 @@ const onToggleRefresh = (on) => {
|
|||
}
|
||||
|
||||
const onCopyKey = () => {
|
||||
ClipboardSetText(props.keyPath)
|
||||
.then((succ) => {
|
||||
if (succ) {
|
||||
$message.success(i18n.t('interface.copy_succ'))
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
$message.error(e.message)
|
||||
})
|
||||
copy(props.keyPath)
|
||||
$message.success(i18n.t('interface.copy_succ'))
|
||||
}
|
||||
|
||||
const onTTL = () => {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script setup>
|
||||
import { computed, h, nextTick, reactive, ref } from 'vue'
|
||||
import { computed, h, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import AddLink from '@/components/icons/AddLink.vue'
|
||||
import { NButton, NIcon, useThemeVars } from 'naive-ui'
|
||||
|
@ -15,10 +15,14 @@ import IconButton from '@/components/common/IconButton.vue'
|
|||
import ContentEntryEditor from '@/components/content_value/ContentEntryEditor.vue'
|
||||
import Edit from '@/components/icons/Edit.vue'
|
||||
import FormatSelector from '@/components/content_value/FormatSelector.vue'
|
||||
import { decodeRedisKey } from '@/utils/key_convert.js'
|
||||
import { decodeRedisKey, nativeRedisKey } from '@/utils/key_convert.js'
|
||||
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
|
||||
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
|
||||
import { formatBytes } from '@/utils/byte_convert.js'
|
||||
import copy from 'copy-text-to-clipboard'
|
||||
import SwitchButton from '@/components/common/SwitchButton.vue'
|
||||
import AlignLeft from '@/components/icons/AlignLeft.vue'
|
||||
import AlignCenter from '@/components/icons/AlignCenter.vue'
|
||||
import { TextAlignType } from '@/consts/text_align_type.js'
|
||||
|
||||
const i18n = useI18n()
|
||||
const themeVars = useThemeVars()
|
||||
|
@ -36,7 +40,7 @@ const props = defineProps({
|
|||
default: -1,
|
||||
},
|
||||
value: {
|
||||
type: Array,
|
||||
type: [String, Array],
|
||||
default: () => [],
|
||||
},
|
||||
size: Number,
|
||||
|
@ -45,9 +49,10 @@ const props = defineProps({
|
|||
decode: String,
|
||||
end: Boolean,
|
||||
loading: Boolean,
|
||||
textAlign: Number,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'match'])
|
||||
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'match', 'update:textAlign'])
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -78,7 +83,7 @@ const fieldFilterOption = ref(null)
|
|||
const fieldColumn = computed(() => ({
|
||||
key: 'key',
|
||||
title: () => i18n.t('common.field'),
|
||||
align: 'center',
|
||||
align: props.textAlign !== TextAlignType.Left ? 'center' : 'left',
|
||||
titleAlign: 'center',
|
||||
resizable: true,
|
||||
ellipsis: {
|
||||
|
@ -89,14 +94,17 @@ const fieldColumn = computed(() => ({
|
|||
},
|
||||
scrollable: true,
|
||||
},
|
||||
lineClamp: 10,
|
||||
lineClamp: 1,
|
||||
},
|
||||
filterOptionValue: fieldFilterOption.value,
|
||||
className: inEdit.value ? 'clickable' : '',
|
||||
className: inEdit.value ? 'clickable wordline' : 'wordline',
|
||||
filter: (value, row) => {
|
||||
return !!~row.k.indexOf(value.toString())
|
||||
},
|
||||
render: (row) => {
|
||||
if (row.rm === true) {
|
||||
return h('s', {}, decodeRedisKey(row.k))
|
||||
}
|
||||
return decodeRedisKey(row.k)
|
||||
},
|
||||
}))
|
||||
|
@ -108,7 +116,7 @@ const isCode = computed(() => {
|
|||
const valueColumn = computed(() => ({
|
||||
key: 'value',
|
||||
title: () => i18n.t('common.value'),
|
||||
align: isCode.value ? 'left' : 'center',
|
||||
align: isCode.value ? 'left' : props.textAlign !== TextAlignType.Left ? 'center' : 'left',
|
||||
titleAlign: 'center',
|
||||
resizable: true,
|
||||
ellipsis: isCode.value
|
||||
|
@ -121,6 +129,7 @@ const valueColumn = computed(() => ({
|
|||
},
|
||||
scrollable: true,
|
||||
},
|
||||
lineClamp: 1,
|
||||
},
|
||||
// filterOptionValue: valueFilterOption.value,
|
||||
className: inEdit.value ? 'clickable' : '',
|
||||
|
@ -131,10 +140,14 @@ const valueColumn = computed(() => ({
|
|||
// return !!~row.v.indexOf(value.toString())
|
||||
// },
|
||||
render: (row) => {
|
||||
const val = row.dv || nativeRedisKey(row.v)
|
||||
if (isCode.value) {
|
||||
return h('pre', {}, row.dv || row.v)
|
||||
return h('pre', { class: 'pre-wrap' }, val)
|
||||
}
|
||||
return row.dv || row.v
|
||||
if (row.rm === true) {
|
||||
return h('s', {}, val)
|
||||
}
|
||||
return val
|
||||
},
|
||||
}))
|
||||
|
||||
|
@ -185,9 +198,9 @@ const resetEdit = () => {
|
|||
currentEditRow.no = 0
|
||||
currentEditRow.key = ''
|
||||
currentEditRow.value = null
|
||||
if (currentEditRow.format !== props.format || currentEditRow.decode !== props.decode) {
|
||||
nextTick(() => onFormatChanged(currentEditRow.decode, currentEditRow.format))
|
||||
}
|
||||
// if (currentEditRow.format !== props.format || currentEditRow.decode !== props.decode) {
|
||||
// nextTick(() => onFormatChanged(currentEditRow.decode, currentEditRow.format))
|
||||
// }
|
||||
// currentEditRow.format = formatTypes.RAW
|
||||
// currentEditRow.decode = decodeTypes.NONE
|
||||
}
|
||||
|
@ -203,16 +216,29 @@ const actionColumn = {
|
|||
return h(EditableTableColumn, {
|
||||
editing: false,
|
||||
bindKey: row.k,
|
||||
onCopy: async () => {
|
||||
try {
|
||||
const succ = await ClipboardSetText(row.v)
|
||||
if (succ) {
|
||||
$message.success(i18n.t('interface.copy_succ'))
|
||||
}
|
||||
} catch (e) {
|
||||
$message.error(e.message)
|
||||
canRefresh: true,
|
||||
onRefresh: async () => {
|
||||
const { updated, success, msg } = await browserStore.getHashField({
|
||||
server: props.name,
|
||||
db: props.db,
|
||||
key: keyName.value,
|
||||
field: row.k,
|
||||
decode: props.decode,
|
||||
format: props.format,
|
||||
})
|
||||
if (success) {
|
||||
delete props.value[index]['rm']
|
||||
$message.success(i18n.t('dialogue.reload_succ'))
|
||||
} else {
|
||||
// update fail, the key may have been deleted
|
||||
$message.error(msg)
|
||||
props.value[index]['rm'] = true
|
||||
}
|
||||
},
|
||||
onCopy: async () => {
|
||||
copy(row.v)
|
||||
$message.success(i18n.t('interface.copy_succ'))
|
||||
},
|
||||
onEdit: () => startEdit(index + 1, row.k, row.v),
|
||||
onDelete: async () => {
|
||||
try {
|
||||
|
@ -342,6 +368,15 @@ defineExpose({
|
|||
@match-changed="onMatchInput" />
|
||||
</div>
|
||||
<div class="flex-item-expand"></div>
|
||||
<switch-button
|
||||
:icons="[AlignCenter, AlignLeft]"
|
||||
:stroke-width="3.5"
|
||||
:t-tooltips="['interface.text_align_center', 'interface.text_align_left']"
|
||||
:value="props.textAlign"
|
||||
size="medium"
|
||||
unselect-stroke-width="3"
|
||||
@update:value="(val) => emit('update:textAlign', val)" />
|
||||
<n-divider vertical />
|
||||
<n-button-group>
|
||||
<icon-button
|
||||
:disabled="props.end || props.loading"
|
||||
|
|
|
@ -5,13 +5,13 @@ import Copy from '@/components/icons/Copy.vue'
|
|||
import Save from '@/components/icons/Save.vue'
|
||||
import { useThemeVars } from 'naive-ui'
|
||||
import { types as redisTypes } from '@/consts/support_redis_type.js'
|
||||
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
|
||||
import { isEmpty, toLower } from 'lodash'
|
||||
import useBrowserStore from 'stores/browser.js'
|
||||
import { decodeRedisKey } from '@/utils/key_convert.js'
|
||||
import ContentEditor from '@/components/content_value/ContentEditor.vue'
|
||||
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
|
||||
import { formatBytes } from '@/utils/byte_convert.js'
|
||||
import copy from 'copy-text-to-clipboard'
|
||||
|
||||
const props = defineProps({
|
||||
name: String,
|
||||
|
@ -62,15 +62,8 @@ const showMemoryUsage = computed(() => {
|
|||
* Copy value
|
||||
*/
|
||||
const onCopyValue = () => {
|
||||
ClipboardSetText(displayValue.value)
|
||||
.then((succ) => {
|
||||
if (succ) {
|
||||
$message.success(i18n.t('interface.copy_succ'))
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
$message.error(e.message)
|
||||
})
|
||||
copy(displayValue.value)
|
||||
$message.success(i18n.t('interface.copy_succ'))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script setup>
|
||||
import { computed, h, nextTick, reactive, ref } from 'vue'
|
||||
import { computed, h, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import AddLink from '@/components/icons/AddLink.vue'
|
||||
import { NButton, NIcon, useThemeVars } from 'naive-ui'
|
||||
|
@ -16,8 +16,13 @@ import ContentEntryEditor from '@/components/content_value/ContentEntryEditor.vu
|
|||
import FormatSelector from '@/components/content_value/FormatSelector.vue'
|
||||
import Edit from '@/components/icons/Edit.vue'
|
||||
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
|
||||
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
|
||||
import { formatBytes } from '@/utils/byte_convert.js'
|
||||
import copy from 'copy-text-to-clipboard'
|
||||
import { TextAlignType } from '@/consts/text_align_type.js'
|
||||
import AlignLeft from '@/components/icons/AlignLeft.vue'
|
||||
import AlignCenter from '@/components/icons/AlignCenter.vue'
|
||||
import SwitchButton from '@/components/common/SwitchButton.vue'
|
||||
import { nativeRedisKey } from '@/utils/key_convert.js'
|
||||
|
||||
const i18n = useI18n()
|
||||
const themeVars = useThemeVars()
|
||||
|
@ -51,9 +56,10 @@ const props = defineProps({
|
|||
},
|
||||
end: Boolean,
|
||||
loading: Boolean,
|
||||
textAlign: Number,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'match'])
|
||||
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'match', 'update:textAlign'])
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -84,7 +90,7 @@ const valueFilterOption = ref(null)
|
|||
const valueColumn = computed(() => ({
|
||||
key: 'value',
|
||||
title: () => i18n.t('common.value'),
|
||||
align: isCode.value ? 'left' : 'center',
|
||||
align: isCode.value ? 'left' : props.textAlign !== TextAlignType.Left ? 'center' : 'left',
|
||||
titleAlign: 'center',
|
||||
ellipsis: isCode.value
|
||||
? false
|
||||
|
@ -96,20 +102,20 @@ const valueColumn = computed(() => ({
|
|||
},
|
||||
scrollable: true,
|
||||
},
|
||||
lineClamp: 1,
|
||||
},
|
||||
filterOptionValue: valueFilterOption.value,
|
||||
className: inEdit.value ? 'clickable' : '',
|
||||
filter: (value, row) => {
|
||||
if (row.dv) {
|
||||
return !!~row.dv.indexOf(value.toString())
|
||||
}
|
||||
return !!~row.v.indexOf(value.toString())
|
||||
filter: (filterValue, row) => {
|
||||
const val = row.dv || nativeRedisKey(row.v)
|
||||
return !!~val.indexOf(filterValue.toString())
|
||||
},
|
||||
render: (row) => {
|
||||
const val = row.dv || nativeRedisKey(row.v)
|
||||
if (isCode.value) {
|
||||
return h('pre', {}, row.dv || row.v)
|
||||
return h('pre', { class: 'pre-wrap' }, val)
|
||||
}
|
||||
return row.dv || row.v
|
||||
return val
|
||||
},
|
||||
}))
|
||||
|
||||
|
@ -144,7 +150,7 @@ const saveEdit = async (pos, value, decode, format) => {
|
|||
server: props.name,
|
||||
db: props.db,
|
||||
key: keyName.value,
|
||||
index,
|
||||
index: row.index,
|
||||
value,
|
||||
decode,
|
||||
format,
|
||||
|
@ -162,9 +168,9 @@ const saveEdit = async (pos, value, decode, format) => {
|
|||
const resetEdit = () => {
|
||||
currentEditRow.no = 0
|
||||
currentEditRow.value = null
|
||||
if (currentEditRow.format !== props.format || currentEditRow.decode !== props.decode) {
|
||||
nextTick(() => onFormatChanged(currentEditRow.decode, currentEditRow.format))
|
||||
}
|
||||
// if (currentEditRow.format !== props.format || currentEditRow.decode !== props.decode) {
|
||||
// nextTick(() => onFormatChanged(currentEditRow.decode, currentEditRow.format))
|
||||
// }
|
||||
}
|
||||
|
||||
const actionColumn = {
|
||||
|
@ -174,22 +180,16 @@ const actionColumn = {
|
|||
align: 'center',
|
||||
titleAlign: 'center',
|
||||
fixed: 'right',
|
||||
render: (row, index) => {
|
||||
render: ({ index, v }, _) => {
|
||||
return h(EditableTableColumn, {
|
||||
editing: false,
|
||||
bindKey: `#${index + 1}`,
|
||||
onCopy: async () => {
|
||||
try {
|
||||
const succ = await ClipboardSetText(row.v)
|
||||
if (succ) {
|
||||
$message.success(i18n.t('interface.copy_succ'))
|
||||
}
|
||||
} catch (e) {
|
||||
$message.error(e.message)
|
||||
}
|
||||
copy(v)
|
||||
$message.success(i18n.t('interface.copy_succ'))
|
||||
},
|
||||
onEdit: () => {
|
||||
startEdit(index + 1, row.v)
|
||||
startEdit(index + 1, v)
|
||||
},
|
||||
onDelete: async () => {
|
||||
try {
|
||||
|
@ -221,7 +221,7 @@ const columns = computed(() => {
|
|||
width: 80,
|
||||
align: 'center',
|
||||
titleAlign: 'center',
|
||||
render: (row, index) => {
|
||||
render: ({ index }, _) => {
|
||||
return index + 1
|
||||
},
|
||||
},
|
||||
|
@ -236,7 +236,7 @@ const columns = computed(() => {
|
|||
width: 80,
|
||||
align: 'center',
|
||||
titleAlign: 'center',
|
||||
render: (row, index) => {
|
||||
render: ({ index }, _) => {
|
||||
if (index + 1 === currentEditRow.no) {
|
||||
// editing row, show edit state
|
||||
return h(NIcon, { size: 16, color: 'red' }, () => h(Edit, { strokeWidth: 5 }))
|
||||
|
@ -250,12 +250,12 @@ const columns = computed(() => {
|
|||
}
|
||||
})
|
||||
|
||||
const rowProps = (row, index) => {
|
||||
const rowProps = ({ index, v }, _) => {
|
||||
return {
|
||||
onClick: () => {
|
||||
// in edit mode, switch edit row by click
|
||||
if (inEdit.value) {
|
||||
startEdit(index + 1, row.v)
|
||||
startEdit(index + 1, v)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -316,6 +316,15 @@ defineExpose({
|
|||
@match-changed="onMatchInput" />
|
||||
</div>
|
||||
<div class="flex-item-expand"></div>
|
||||
<switch-button
|
||||
:icons="[AlignCenter, AlignLeft]"
|
||||
:stroke-width="3.5"
|
||||
:t-tooltips="['interface.text_align_center', 'interface.text_align_left']"
|
||||
:value="props.textAlign"
|
||||
size="medium"
|
||||
unselect-stroke-width="3"
|
||||
@update:value="(val) => emit('update:textAlign', val)" />
|
||||
<n-divider vertical />
|
||||
<n-button-group>
|
||||
<icon-button
|
||||
:disabled="props.end || props.loading"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script setup>
|
||||
import { computed, h, nextTick, reactive, ref } from 'vue'
|
||||
import { computed, h, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import AddLink from '@/components/icons/AddLink.vue'
|
||||
import { NButton, NIcon, useThemeVars } from 'naive-ui'
|
||||
|
@ -16,8 +16,13 @@ import Edit from '@/components/icons/Edit.vue'
|
|||
import ContentEntryEditor from '@/components/content_value/ContentEntryEditor.vue'
|
||||
import FormatSelector from '@/components/content_value/FormatSelector.vue'
|
||||
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
|
||||
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
|
||||
import { formatBytes } from '@/utils/byte_convert.js'
|
||||
import copy from 'copy-text-to-clipboard'
|
||||
import AlignLeft from '@/components/icons/AlignLeft.vue'
|
||||
import AlignCenter from '@/components/icons/AlignCenter.vue'
|
||||
import SwitchButton from '@/components/common/SwitchButton.vue'
|
||||
import { TextAlignType } from '@/consts/text_align_type.js'
|
||||
import { nativeRedisKey } from '@/utils/key_convert.js'
|
||||
|
||||
const i18n = useI18n()
|
||||
const themeVars = useThemeVars()
|
||||
|
@ -50,9 +55,10 @@ const props = defineProps({
|
|||
},
|
||||
end: Boolean,
|
||||
loading: Boolean,
|
||||
textAlign: Number,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'match'])
|
||||
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'match', 'update:textAlign'])
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -83,7 +89,7 @@ const valueFilterOption = ref(null)
|
|||
const valueColumn = computed(() => ({
|
||||
key: 'value',
|
||||
title: () => i18n.t('common.value'),
|
||||
align: isCode.value ? 'left' : 'center',
|
||||
align: isCode.value ? 'left' : props.textAlign !== TextAlignType.Left ? 'center' : 'left',
|
||||
titleAlign: 'center',
|
||||
ellipsis: isCode.value
|
||||
? false
|
||||
|
@ -95,20 +101,20 @@ const valueColumn = computed(() => ({
|
|||
},
|
||||
scrollable: true,
|
||||
},
|
||||
lineClamp: 1,
|
||||
},
|
||||
filterOptionValue: valueFilterOption.value,
|
||||
className: inEdit.value ? 'clickable' : '',
|
||||
filter: (value, row) => {
|
||||
if (row.dv) {
|
||||
return !!~row.dv.indexOf(value.toString())
|
||||
}
|
||||
return !!~row.v.indexOf(value.toString())
|
||||
filter: (filterValue, row) => {
|
||||
const val = row.dv || nativeRedisKey(row.v)
|
||||
return !!~val.indexOf(filterValue.toString())
|
||||
},
|
||||
render: (row) => {
|
||||
const val = row.dv || nativeRedisKey(row.v)
|
||||
if (isCode.value) {
|
||||
return h('pre', {}, row.dv || row.v)
|
||||
return h('pre', { class: 'pre-wrap' }, val)
|
||||
}
|
||||
return row.dv || row.v
|
||||
return val
|
||||
},
|
||||
}))
|
||||
|
||||
|
@ -159,9 +165,9 @@ const saveEdit = async (pos, value, decode, format) => {
|
|||
const resetEdit = () => {
|
||||
currentEditRow.no = 0
|
||||
currentEditRow.value = null
|
||||
if (currentEditRow.format !== props.format || currentEditRow.decode !== props.decode) {
|
||||
nextTick(() => onFormatChanged(currentEditRow.decode, currentEditRow.format))
|
||||
}
|
||||
// if (currentEditRow.format !== props.format || currentEditRow.decode !== props.decode) {
|
||||
// nextTick(() => onFormatChanged(currentEditRow.decode, currentEditRow.format))
|
||||
// }
|
||||
}
|
||||
|
||||
const actionColumn = {
|
||||
|
@ -176,14 +182,8 @@ const actionColumn = {
|
|||
editing: false,
|
||||
bindKey: `#${index + 1}`,
|
||||
onCopy: async () => {
|
||||
try {
|
||||
const succ = await ClipboardSetText(row.v)
|
||||
if (succ) {
|
||||
$message.success(i18n.t('interface.copy_succ'))
|
||||
}
|
||||
} catch (e) {
|
||||
$message.error(e.message)
|
||||
}
|
||||
copy(row.v)
|
||||
$message.success(i18n.t('interface.copy_succ'))
|
||||
},
|
||||
onEdit: () => {
|
||||
startEdit(index + 1, row.v)
|
||||
|
@ -313,6 +313,15 @@ defineExpose({
|
|||
@match-changed="onMatchInput" />
|
||||
</div>
|
||||
<div class="flex-item-expand"></div>
|
||||
<switch-button
|
||||
:icons="[AlignCenter, AlignLeft]"
|
||||
:stroke-width="3.5"
|
||||
:t-tooltips="['interface.text_align_center', 'interface.text_align_left']"
|
||||
:value="props.textAlign"
|
||||
size="medium"
|
||||
unselect-stroke-width="3"
|
||||
@update:value="(val) => emit('update:textAlign', val)" />
|
||||
<n-divider vertical />
|
||||
<n-button-group>
|
||||
<icon-button
|
||||
:disabled="props.end || props.loading"
|
||||
|
|
|
@ -13,8 +13,8 @@ import LoadList from '@/components/icons/LoadList.vue'
|
|||
import LoadAll from '@/components/icons/LoadAll.vue'
|
||||
import IconButton from '@/components/common/IconButton.vue'
|
||||
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
|
||||
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
|
||||
import { formatBytes } from '@/utils/byte_convert.js'
|
||||
import copy from 'copy-text-to-clipboard'
|
||||
|
||||
const i18n = useI18n()
|
||||
const themeVars = useThemeVars()
|
||||
|
@ -93,7 +93,7 @@ const valueColumn = computed(() => ({
|
|||
},
|
||||
// sorter: (row1, row2) => row1.value - row2.value,
|
||||
render: (row) => {
|
||||
return h('pre', {}, row.dv)
|
||||
return h('pre', { class: 'pre-wrap' }, row.dv)
|
||||
},
|
||||
}))
|
||||
const actionColumn = {
|
||||
|
@ -108,14 +108,8 @@ const actionColumn = {
|
|||
bindKey: row.id,
|
||||
readonly: true,
|
||||
onCopy: async () => {
|
||||
try {
|
||||
const succ = await ClipboardSetText(JSON.stringify(row.v))
|
||||
if (succ) {
|
||||
$message.success(i18n.t('interface.copy_succ'))
|
||||
}
|
||||
} catch (e) {
|
||||
$message.error(e.message)
|
||||
}
|
||||
copy(JSON.stringify(row.v))
|
||||
$message.success(i18n.t('interface.copy_succ'))
|
||||
},
|
||||
onDelete: async () => {
|
||||
try {
|
||||
|
|
|
@ -6,13 +6,13 @@ import Save from '@/components/icons/Save.vue'
|
|||
import { useThemeVars } from 'naive-ui'
|
||||
import { formatTypes } from '@/consts/value_view_type.js'
|
||||
import { types as redisTypes } from '@/consts/support_redis_type.js'
|
||||
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
|
||||
import { isEmpty, toLower } from 'lodash'
|
||||
import useBrowserStore from 'stores/browser.js'
|
||||
import { decodeRedisKey } from '@/utils/key_convert.js'
|
||||
import FormatSelector from '@/components/content_value/FormatSelector.vue'
|
||||
import ContentEditor from '@/components/content_value/ContentEditor.vue'
|
||||
import { formatBytes } from '@/utils/byte_convert.js'
|
||||
import copy from 'copy-text-to-clipboard'
|
||||
|
||||
const props = defineProps({
|
||||
name: String,
|
||||
|
@ -121,15 +121,8 @@ const onFormatChanged = async (decode = '', format = '') => {
|
|||
* Copy value
|
||||
*/
|
||||
const onCopyValue = () => {
|
||||
ClipboardSetText(displayValue.value)
|
||||
.then((succ) => {
|
||||
if (succ) {
|
||||
$message.success(i18n.t('interface.copy_succ'))
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
$message.error(e.message)
|
||||
})
|
||||
copy(displayValue.value)
|
||||
$message.success(i18n.t('interface.copy_succ'))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -14,11 +14,14 @@ import useDialogStore from 'stores/dialog.js'
|
|||
import { useI18n } from 'vue-i18n'
|
||||
import ContentToolbar from '@/components/content_value/ContentToolbar.vue'
|
||||
import ContentValueJson from '@/components/content_value/ContentValueJson.vue'
|
||||
import usePreferencesStore from 'stores/preferences.js'
|
||||
import { TextAlignType } from '@/consts/text_align_type.js'
|
||||
import { isMacOS } from '@/utils/platform.js'
|
||||
|
||||
const themeVars = useThemeVars()
|
||||
const browserStore = useBrowserStore()
|
||||
const dialogStore = useDialogStore()
|
||||
const prefStore = usePreferencesStore()
|
||||
|
||||
const props = defineProps({
|
||||
blank: Boolean,
|
||||
|
@ -128,28 +131,19 @@ const onReload = async (selDecode, selFormat) => {
|
|||
}
|
||||
|
||||
const onKeyShortcut = (e) => {
|
||||
// console.log(e)
|
||||
const isCtrlOn = isMacOS() ? e.metaKey : e.ctrlKey
|
||||
switch (e.key) {
|
||||
case 'Delete':
|
||||
onDelete()
|
||||
return
|
||||
|
||||
case 'd':
|
||||
if (e.metaKey) {
|
||||
onDelete()
|
||||
}
|
||||
return
|
||||
|
||||
case 'F5':
|
||||
onReload()
|
||||
return
|
||||
|
||||
case 'r':
|
||||
if (e.metaKey && isMacOS()) {
|
||||
if (isCtrlOn) {
|
||||
onReload()
|
||||
}
|
||||
return
|
||||
|
||||
case 'F2':
|
||||
onRename()
|
||||
return
|
||||
|
@ -188,6 +182,11 @@ const onMatch = (match) => {
|
|||
loadData(true, false, match || '')
|
||||
}
|
||||
|
||||
const onEntryTextAlignChanged = (align) => {
|
||||
prefStore.editor.entryTextAlign = align !== TextAlignType.Left ? TextAlignType.Center : TextAlignType.Left
|
||||
prefStore.savePreferences()
|
||||
}
|
||||
|
||||
const contentRef = ref(null)
|
||||
const initContent = async () => {
|
||||
// onReload()
|
||||
|
@ -219,8 +218,6 @@ watch(() => data.value?.keyPath, initContent)
|
|||
<!-- FIXME: keep alive may cause virtual list null value error. -->
|
||||
<!-- <keep-alive v-else> -->
|
||||
<component
|
||||
tabindex="0"
|
||||
@keydown="onKeyShortcut"
|
||||
:is="valueComponents[data.type]"
|
||||
v-else
|
||||
ref="contentRef"
|
||||
|
@ -236,11 +233,15 @@ watch(() => data.value?.keyPath, initContent)
|
|||
:size="data.size"
|
||||
:ttl="data.ttl"
|
||||
:value="data.value"
|
||||
tabindex="0"
|
||||
:text-align="prefStore.entryTextAlign"
|
||||
@delete="onDelete"
|
||||
@keydown="onKeyShortcut"
|
||||
@loadall="onLoadAll"
|
||||
@loadmore="onLoadMore"
|
||||
@match="onMatch"
|
||||
@reload="onReload">
|
||||
@reload="onReload"
|
||||
@update:text-align="onEntryTextAlignChanged">
|
||||
<template #toolbar>
|
||||
<content-toolbar
|
||||
:db="data.db"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script setup>
|
||||
import { computed, h, nextTick, reactive, ref } from 'vue'
|
||||
import { computed, h, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import AddLink from '@/components/icons/AddLink.vue'
|
||||
import { NButton, NIcon, useThemeVars } from 'naive-ui'
|
||||
|
@ -16,8 +16,13 @@ import ContentEntryEditor from '@/components/content_value/ContentEntryEditor.vu
|
|||
import FormatSelector from '@/components/content_value/FormatSelector.vue'
|
||||
import Edit from '@/components/icons/Edit.vue'
|
||||
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
|
||||
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
|
||||
import { formatBytes } from '@/utils/byte_convert.js'
|
||||
import copy from 'copy-text-to-clipboard'
|
||||
import { TextAlignType } from '@/consts/text_align_type.js'
|
||||
import AlignLeft from '@/components/icons/AlignLeft.vue'
|
||||
import AlignCenter from '@/components/icons/AlignCenter.vue'
|
||||
import SwitchButton from '@/components/common/SwitchButton.vue'
|
||||
import { nativeRedisKey } from '@/utils/key_convert.js'
|
||||
|
||||
const i18n = useI18n()
|
||||
const themeVars = useThemeVars()
|
||||
|
@ -50,9 +55,10 @@ const props = defineProps({
|
|||
},
|
||||
end: Boolean,
|
||||
loading: Boolean,
|
||||
textAlign: Number,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'match'])
|
||||
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'match', 'update:textAlign'])
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -82,7 +88,7 @@ const fullEdit = ref(false)
|
|||
const scoreColumn = computed(() => ({
|
||||
key: 'score',
|
||||
title: () => i18n.t('common.score'),
|
||||
align: 'center',
|
||||
align: props.textAlign !== TextAlignType.Left ? 'center' : 'left',
|
||||
titleAlign: 'center',
|
||||
resizable: true,
|
||||
sorter: (row1, row2) => row1.s - row2.s,
|
||||
|
@ -131,7 +137,7 @@ const valueFilterOption = ref(null)
|
|||
const valueColumn = computed(() => ({
|
||||
key: 'value',
|
||||
title: () => i18n.t('common.value'),
|
||||
align: isCode.value ? 'left' : 'center',
|
||||
align: isCode.value ? 'left' : props.textAlign !== TextAlignType.Left ? 'center' : 'left',
|
||||
titleAlign: 'center',
|
||||
resizable: true,
|
||||
ellipsis: isCode.value
|
||||
|
@ -144,21 +150,21 @@ const valueColumn = computed(() => ({
|
|||
},
|
||||
scrollable: true,
|
||||
},
|
||||
lineClamp: 1,
|
||||
},
|
||||
filterOptionValue: valueFilterOption.value,
|
||||
className: inEdit.value ? 'clickable' : '',
|
||||
filter(value, row) {
|
||||
if (row.dv) {
|
||||
return !!~row.dv.indexOf(value.toString())
|
||||
}
|
||||
return !!~row.v.indexOf(value.toString())
|
||||
filter(filterValue, row) {
|
||||
const val = row.dv || nativeRedisKey(row.v)
|
||||
return !!~val.indexOf(filterValue.toString())
|
||||
},
|
||||
// sorter: (row1, row2) => row1.value - row2.value,
|
||||
render: (row) => {
|
||||
const val = row.dv || nativeRedisKey(row.v)
|
||||
if (isCode.value) {
|
||||
return h('pre', {}, row.dv || row.v)
|
||||
return h('pre', { class: 'pre-wrap' }, val)
|
||||
}
|
||||
return row.dv || row.v
|
||||
return val
|
||||
},
|
||||
}))
|
||||
|
||||
|
@ -206,9 +212,9 @@ const resetEdit = () => {
|
|||
currentEditRow.no = 0
|
||||
currentEditRow.score = 0
|
||||
currentEditRow.value = null
|
||||
if (currentEditRow.format !== props.format || currentEditRow.decode !== props.decode) {
|
||||
nextTick(() => onFormatChanged(currentEditRow.decode, currentEditRow.format))
|
||||
}
|
||||
// if (currentEditRow.format !== props.format || currentEditRow.decode !== props.decode) {
|
||||
// nextTick(() => onFormatChanged(currentEditRow.decode, currentEditRow.format))
|
||||
// }
|
||||
}
|
||||
|
||||
const actionColumn = {
|
||||
|
@ -223,14 +229,8 @@ const actionColumn = {
|
|||
editing: false,
|
||||
bindKey: row.v,
|
||||
onCopy: async () => {
|
||||
try {
|
||||
const succ = await ClipboardSetText(row.v)
|
||||
if (succ) {
|
||||
$message.success(i18n.t('interface.copy_succ'))
|
||||
}
|
||||
} catch (e) {
|
||||
$message.error(e.message)
|
||||
}
|
||||
copy(row.v)
|
||||
$message.success(i18n.t('interface.copy_succ'))
|
||||
},
|
||||
onEdit: () => startEdit(index + 1, row.s, row.v),
|
||||
onDelete: async () => {
|
||||
|
@ -348,6 +348,15 @@ defineExpose({
|
|||
@match-changed="onMatchInput" />
|
||||
</div>
|
||||
<div class="flex-item-expand"></div>
|
||||
<switch-button
|
||||
:icons="[AlignCenter, AlignLeft]"
|
||||
:stroke-width="3.5"
|
||||
:t-tooltips="['interface.text_align_center', 'interface.text_align_left']"
|
||||
:value="props.textAlign"
|
||||
size="medium"
|
||||
unselect-stroke-width="3"
|
||||
@update:value="(val) => emit('update:textAlign', val)" />
|
||||
<n-divider vertical />
|
||||
<n-button-group>
|
||||
<icon-button
|
||||
:disabled="props.end || props.loading"
|
||||
|
|
|
@ -93,7 +93,7 @@ const onDecodeMenu = (key) => {
|
|||
:icon="Code"
|
||||
:options="formatTypeOption"
|
||||
:tooltip="$t('interface.view_as')"
|
||||
:value="props.format"
|
||||
:value="props.format || formatTypes.RAW"
|
||||
@update:value="(f) => onFormatChanged(props.decode, f)" />
|
||||
<n-divider vertical />
|
||||
<dropdown-selector
|
||||
|
@ -103,7 +103,7 @@ const onDecodeMenu = (key) => {
|
|||
:menu-option="decodeMenuOption"
|
||||
:options="decodeTypeOption"
|
||||
:tooltip="$t('interface.decode_with')"
|
||||
:value="props.decode"
|
||||
:value="props.decode || decodeTypes.NONE"
|
||||
@menu="onDecodeMenu"
|
||||
@update:value="(d) => onFormatChanged(d, '')" />
|
||||
</n-space>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script setup>
|
||||
import { every, get, includes, isEmpty, map, reject, sortBy, toNumber } from 'lodash'
|
||||
import { every, get, includes, isEmpty, map, reject, sortBy, toNumber, trim } from 'lodash'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ListSentinelMasters, TestConnection } from 'wailsjs/go/services/connectionService.js'
|
||||
|
@ -169,9 +169,11 @@ const onSaveConnection = async () => {
|
|||
generalForm.value.network = 'unix'
|
||||
generalForm.value.addr = ''
|
||||
generalForm.value.port = 0
|
||||
generalForm.value.sock = trim(generalForm.value.sock)
|
||||
} else {
|
||||
generalForm.value.network = 'tcp'
|
||||
generalForm.value.sock = ''
|
||||
generalForm.value.addr = trim(generalForm.value.addr)
|
||||
}
|
||||
|
||||
// trim advance data
|
||||
|
@ -505,7 +507,7 @@ const pasteFromClipboard = async () => {
|
|||
</n-radio-group>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi
|
||||
v-show="generalForm.dbFilterType !== 'none'"
|
||||
v-if="generalForm.dbFilterType !== 'none'"
|
||||
:label="$t('dialogue.connection.advn.dbfilter_input')"
|
||||
:span="24">
|
||||
<n-select
|
||||
|
|
|
@ -203,5 +203,5 @@ const onClose = () => {}
|
|||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/content';
|
||||
@use '@/styles/content';
|
||||
</style>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<script setup>
|
||||
import { computed, nextTick, reactive, ref, watchEffect } from 'vue'
|
||||
import useDialog from 'stores/dialog'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { isEmpty, map, size } from 'lodash'
|
||||
import useBrowserStore from 'stores/browser.js'
|
||||
import { decodeRedisKey } from '@/utils/key_convert.js'
|
||||
|
@ -14,6 +13,7 @@ const deleteForm = reactive({
|
|||
loadingAffected: false,
|
||||
affectedKeys: [],
|
||||
async: true,
|
||||
direct: false,
|
||||
})
|
||||
|
||||
const dialogStore = useDialog()
|
||||
|
@ -68,7 +68,6 @@ const keyLines = computed(() => {
|
|||
return map(deleteForm.affectedKeys, (k) => decodeRedisKey(k))
|
||||
})
|
||||
|
||||
const i18n = useI18n()
|
||||
const onConfirmDelete = async () => {
|
||||
try {
|
||||
deleting.value = true
|
||||
|
@ -84,6 +83,21 @@ const onConfirmDelete = async () => {
|
|||
dialogStore.closeDeleteKeyDialog()
|
||||
}
|
||||
|
||||
const onConfirmDirectDelete = async () => {
|
||||
try {
|
||||
deleting.value = true
|
||||
const { server, db, key } = deleteForm
|
||||
await nextTick()
|
||||
browserStore.deleteByPattern(server, db, key).catch((e) => {})
|
||||
} catch (e) {
|
||||
$message.error(e.message)
|
||||
return
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
dialogStore.closeDeleteKeyDialog()
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
dialogStore.closeDeleteKeyDialog()
|
||||
}
|
||||
|
@ -116,9 +130,9 @@ const onClose = () => {
|
|||
required>
|
||||
<n-input v-model:value="deleteForm.key" placeholder="" @input="resetAffected" />
|
||||
</n-form-item>
|
||||
<!-- <n-checkbox v-model:checked="deleteForm.async">-->
|
||||
<!-- {{ $t('dialogue.key.silent') }}-->
|
||||
<!-- </n-checkbox>-->
|
||||
<n-checkbox v-if="!deleteForm.showAffected" v-model:checked="deleteForm.direct">
|
||||
{{ $t('dialogue.key.direct_delete') }}
|
||||
</n-checkbox>
|
||||
<n-card
|
||||
v-if="deleteForm.showAffected"
|
||||
:title="$t('dialogue.key.affected_key') + `(${size(deleteForm.affectedKeys)})`"
|
||||
|
@ -140,22 +154,32 @@ const onClose = () => {
|
|||
<div class="flex-item n-dialog__action">
|
||||
<n-button :disabled="loading" :focusable="false" @click="onClose">{{ $t('common.cancel') }}</n-button>
|
||||
<n-button
|
||||
v-if="!deleteForm.showAffected"
|
||||
v-if="deleteForm.direct"
|
||||
:focusable="false"
|
||||
:loading="loading"
|
||||
type="primary"
|
||||
@click="scanAffectedKey">
|
||||
{{ $t('dialogue.key.show_affected_key') }}
|
||||
</n-button>
|
||||
<n-button
|
||||
v-else
|
||||
:disabled="isEmpty(deleteForm.affectedKeys)"
|
||||
:focusable="false"
|
||||
:loading="loading"
|
||||
type="primary"
|
||||
@click="onConfirmDelete">
|
||||
{{ $t('dialogue.key.confirm_delete_key', { num: size(deleteForm.affectedKeys) }) }}
|
||||
@click="onConfirmDirectDelete">
|
||||
{{ $t('dialogue.key.confirm_delete') }}
|
||||
</n-button>
|
||||
<template v-else>
|
||||
<n-button
|
||||
v-if="!deleteForm.showAffected"
|
||||
:focusable="false"
|
||||
:loading="loading"
|
||||
type="primary"
|
||||
@click="scanAffectedKey">
|
||||
{{ $t('dialogue.key.show_affected_key') }}
|
||||
</n-button>
|
||||
<n-button
|
||||
v-else
|
||||
:disabled="isEmpty(deleteForm.affectedKeys)"
|
||||
:focusable="false"
|
||||
:loading="loading"
|
||||
type="primary"
|
||||
@click="onConfirmDelete">
|
||||
{{ $t('dialogue.key.confirm_delete_key', { num: size(deleteForm.affectedKeys) }) }}
|
||||
</n-button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
|
|
@ -154,7 +154,11 @@ const onAdd = async () => {
|
|||
value = defaultValue[type]
|
||||
}
|
||||
// await browserStore.reloadKey({server, db, key: trim(key)})
|
||||
const { success, msg, nodeKey } = await browserStore.setKey({
|
||||
const {
|
||||
success,
|
||||
msg,
|
||||
nodeKey = '',
|
||||
} = await browserStore.setKey({
|
||||
server,
|
||||
db,
|
||||
key: trim(key),
|
||||
|
@ -165,12 +169,15 @@ const onAdd = async () => {
|
|||
if (success) {
|
||||
// select current key
|
||||
await nextTick()
|
||||
tabStore.setSelectedKeys(server, nodeKey)
|
||||
browserStore.reloadKey({ server, db, key })
|
||||
const selectedDB = browserStore.getSelectedDB(server)
|
||||
if (selectedDB === db) {
|
||||
tabStore.setSelectedKeys(server, nodeKey)
|
||||
browserStore.reloadKey({ server, db, key })
|
||||
}
|
||||
dialogStore.closeNewKeyDialog()
|
||||
} else if (!isEmpty(msg)) {
|
||||
$message.error(msg)
|
||||
}
|
||||
dialogStore.closeNewKeyDialog()
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
<script setup>
|
||||
const props = defineProps({
|
||||
strokeWidth: {
|
||||
type: [Number, String],
|
||||
default: 3,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
:stroke-width="props.strokeWidth"
|
||||
d="M36 19H12"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path
|
||||
:stroke-width="props.strokeWidth"
|
||||
d="M42 9H6"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path
|
||||
:stroke-width="props.strokeWidth"
|
||||
d="M42 29H6"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path
|
||||
:stroke-width="props.strokeWidth"
|
||||
d="M36 39H12"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,39 @@
|
|||
<script setup>
|
||||
const props = defineProps({
|
||||
strokeWidth: {
|
||||
type: [Number, String],
|
||||
default: 3,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
:stroke-width="props.strokeWidth"
|
||||
d="M42 9H6"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path
|
||||
:stroke-width="props.strokeWidth"
|
||||
d="M34 19H6"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path
|
||||
:stroke-width="props.strokeWidth"
|
||||
d="M42 29H6"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path
|
||||
:stroke-width="props.strokeWidth"
|
||||
d="M34 39H6"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
|
@ -39,18 +39,6 @@ const props = defineProps({
|
|||
stroke="currentColor"
|
||||
stroke-linejoin="round" />
|
||||
<path :stroke-width="props.strokeWidth" d="M43 22H5" stroke="currentColor" stroke-linejoin="round" />
|
||||
<path
|
||||
:stroke-width="props.strokeWidth"
|
||||
d="M5 16V28"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path
|
||||
:stroke-width="props.strokeWidth"
|
||||
d="M43 16V28"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -37,18 +37,6 @@ const props = defineProps({
|
|||
d="M26 38H38"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path
|
||||
:stroke-width="props.strokeWidth"
|
||||
d="M44 37V27"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path
|
||||
:stroke-width="props.strokeWidth"
|
||||
d="M4 37V27"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ import { ConnectionType } from '@/consts/connection_type.js'
|
|||
import Import from '@/components/icons/Import.vue'
|
||||
import Checkbox from '@/components/icons/Checkbox.vue'
|
||||
import Timer from '@/components/icons/Timer.vue'
|
||||
import { toVersionArray } from '@/utils/version.js'
|
||||
|
||||
const props = defineProps({
|
||||
server: String,
|
||||
|
@ -78,6 +79,12 @@ const dbSelectOptions = computed(() => {
|
|||
})
|
||||
})
|
||||
|
||||
const showTypeFilter = computed(() => {
|
||||
const version = browserStore.getServerVersion(props.server)
|
||||
const verArr = toVersionArray(version)
|
||||
return verArr[0] > 5
|
||||
})
|
||||
|
||||
const moreOptions = [
|
||||
{ key: 'import', label: 'interface.import_key', icon: Import },
|
||||
{ key: 'divider1', type: 'divider' },
|
||||
|
@ -301,7 +308,11 @@ watch(
|
|||
@filter-changed="onFilterInput"
|
||||
@match-changed="onMatchInput">
|
||||
<template #prepend>
|
||||
<redis-type-selector v-model:value="filterForm.type" @update:value="onSelectFilterType" />
|
||||
<redis-type-selector
|
||||
v-model:value="filterForm.type"
|
||||
:disabled="!showTypeFilter"
|
||||
:disable-tip="$t('dialogue.filter.filter_type_not_support')"
|
||||
@update:value="onSelectFilterType" />
|
||||
</template>
|
||||
</content-search-input>
|
||||
<n-button-group>
|
||||
|
@ -447,7 +458,7 @@ watch(
|
|||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/style';
|
||||
@use '@/styles/style' as style;
|
||||
|
||||
:deep(.toggle-btn) {
|
||||
border-style: solid;
|
||||
|
@ -472,7 +483,7 @@ watch(
|
|||
}
|
||||
|
||||
.nav-pane-bottom {
|
||||
@include top-shadow(0.1);
|
||||
@include style.top-shadow(0.1);
|
||||
color: v-bind('themeVars.iconColor');
|
||||
border-top: v-bind('themeVars.borderColor') 1px solid;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { NIcon, NSpace, NText, useThemeVars } from 'naive-ui'
|
|||
import Key from '@/components/icons/Key.vue'
|
||||
import Binary from '@/components/icons/Binary.vue'
|
||||
import Database from '@/components/icons/Database.vue'
|
||||
import { filter, find, get, includes, isEmpty, map, size, toUpper } from 'lodash'
|
||||
import { filter, find, first, get, includes, isEmpty, last, map, size, toUpper } from 'lodash'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Refresh from '@/components/icons/Refresh.vue'
|
||||
import CopyLink from '@/components/icons/CopyLink.vue'
|
||||
|
@ -13,7 +13,6 @@ import Add from '@/components/icons/Add.vue'
|
|||
import Layer from '@/components/icons/Layer.vue'
|
||||
import Delete from '@/components/icons/Delete.vue'
|
||||
import useDialogStore from 'stores/dialog.js'
|
||||
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
|
||||
import useConnectionStore from 'stores/connections.js'
|
||||
import useTabStore from 'stores/tab.js'
|
||||
import IconButton from '@/components/common/IconButton.vue'
|
||||
|
@ -26,6 +25,8 @@ import RedisTypeTag from '@/components/common/RedisTypeTag.vue'
|
|||
import usePreferencesStore from 'stores/preferences.js'
|
||||
import { typesIconStyle } from '@/consts/support_redis_type.js'
|
||||
import { nativeRedisKey } from '@/utils/key_convert.js'
|
||||
import copy from 'copy-text-to-clipboard'
|
||||
import { isMacOS } from '@/utils/platform.js'
|
||||
|
||||
const props = defineProps({
|
||||
server: String,
|
||||
|
@ -153,6 +154,212 @@ const menuOptions = {
|
|||
],
|
||||
}
|
||||
|
||||
const handleKeyUp = () => {
|
||||
const selectedKey = get(selectedKeys.value, 0)
|
||||
if (selectedKey == null) {
|
||||
return
|
||||
}
|
||||
let node = browserStore.getNode(selectedKey)
|
||||
if (node == null) {
|
||||
return
|
||||
}
|
||||
|
||||
let parentNode = browserStore.getParentNode(selectedKey)
|
||||
if (parentNode == null) {
|
||||
return
|
||||
}
|
||||
const nodeIndex = parentNode.children.indexOf(node)
|
||||
if (nodeIndex <= 0) {
|
||||
if (parentNode.type === ConnectionType.RedisKey || parentNode.type === ConnectionType.RedisValue) {
|
||||
onUpdateSelectedKeys([parentNode.key])
|
||||
updateKeyDetail(parentNode)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// try select pre node
|
||||
let preNode = parentNode.children[nodeIndex - 1]
|
||||
while (preNode.expanded && !isEmpty(preNode.children)) {
|
||||
preNode = last(preNode.children)
|
||||
}
|
||||
onUpdateSelectedKeys([preNode.key])
|
||||
updateKeyDetail(preNode)
|
||||
}
|
||||
|
||||
const handleKeyDown = () => {
|
||||
const selectedKey = get(selectedKeys.value, 0)
|
||||
if (selectedKey == null) {
|
||||
return
|
||||
}
|
||||
let node = browserStore.getNode(selectedKey)
|
||||
if (node == null) {
|
||||
return
|
||||
}
|
||||
// try select first child if expanded
|
||||
if (node.expanded && !isEmpty(node.children)) {
|
||||
const childNode = get(node.children, 0)
|
||||
onUpdateSelectedKeys([childNode.key])
|
||||
updateKeyDetail(childNode)
|
||||
return
|
||||
}
|
||||
|
||||
let travelCount = 0
|
||||
let childKey = selectedKey
|
||||
do {
|
||||
if (travelCount++ > 20) {
|
||||
return
|
||||
}
|
||||
// find out parent node
|
||||
const parentNode = browserStore.getParentNode(childKey)
|
||||
if (parentNode == null) {
|
||||
return
|
||||
}
|
||||
const nodeIndex = parentNode.children.indexOf(node)
|
||||
if (nodeIndex < 0 || nodeIndex >= parentNode.children.length - 1) {
|
||||
// last child, try select parent's neighbor node
|
||||
childKey = parentNode.key
|
||||
node = parentNode
|
||||
} else {
|
||||
// select next node
|
||||
const childNode = parentNode.children[nodeIndex + 1]
|
||||
onUpdateSelectedKeys([childNode.key])
|
||||
updateKeyDetail(childNode)
|
||||
return
|
||||
}
|
||||
} while (true)
|
||||
}
|
||||
|
||||
const handleKeyLeft = () => {
|
||||
const selectedKey = get(selectedKeys.value, 0)
|
||||
if (selectedKey == null) {
|
||||
return
|
||||
}
|
||||
let node = browserStore.getNode(selectedKey)
|
||||
if (node == null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (node.type === ConnectionType.RedisKey) {
|
||||
if (node.expanded) {
|
||||
// try collapse
|
||||
onUpdateExpanded([node.key], null, { node, action: 'collapse' })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// try select parent node
|
||||
let parentNode = browserStore.getParentNode(selectedKey)
|
||||
if (parentNode == null || parentNode.type !== ConnectionType.RedisKey) {
|
||||
return
|
||||
}
|
||||
onUpdateSelectedKeys([parentNode.key])
|
||||
updateKeyDetail(parentNode)
|
||||
}
|
||||
|
||||
const handleKeyRight = () => {
|
||||
const selectedKey = get(selectedKeys.value, 0)
|
||||
if (selectedKey == null) {
|
||||
return
|
||||
}
|
||||
let node = browserStore.getNode(selectedKey)
|
||||
if (node == null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (node.type === ConnectionType.RedisKey) {
|
||||
if (!node.expanded) {
|
||||
// try expand
|
||||
onUpdateExpanded([node.key], null, { node, action: 'expand' })
|
||||
} else if (!isEmpty(node.children)) {
|
||||
// try select first child
|
||||
const childNode = first(node.children)
|
||||
onUpdateSelectedKeys([childNode.key])
|
||||
updateKeyDetail(childNode)
|
||||
}
|
||||
} else if (node.type === ConnectionType.RedisValue) {
|
||||
handleKeyDown()
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDelete = () => {
|
||||
const selectedKey = get(selectedKeys.value, 0)
|
||||
if (selectedKey == null) {
|
||||
return
|
||||
}
|
||||
let node = browserStore.getNode(selectedKey)
|
||||
if (node == null) {
|
||||
return
|
||||
}
|
||||
|
||||
const { db = 0, key: nodeKey, redisKey: rk = '', redisKeyCode: rkc, label } = node || {}
|
||||
const redisKey = rkc || rk
|
||||
const redisKeyName = !!rkc ? label : redisKey
|
||||
switch (node.type) {
|
||||
case ConnectionType.RedisKey:
|
||||
dialogStore.openDeleteKeyDialog(props.server, db, isEmpty(redisKey) ? '*' : redisKey + ':*')
|
||||
break
|
||||
case ConnectionType.RedisValue:
|
||||
$dialog.warning(i18n.t('dialogue.remove_tip', { name: redisKeyName }), () => {
|
||||
browserStore.deleteKey(props.server, db, redisKey).then((success) => {
|
||||
if (success) {
|
||||
$message.success(i18n.t('dialogue.delete.success', { key: redisKeyName }))
|
||||
}
|
||||
})
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyCopy = () => {
|
||||
const selectedKey = get(selectedKeys.value, 0)
|
||||
if (selectedKey == null) {
|
||||
return
|
||||
}
|
||||
let node = browserStore.getNode(selectedKey)
|
||||
if (node == null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (node.type === ConnectionType.RedisValue) {
|
||||
copy(nativeRedisKey(node.redisKeyCode || node.redisKey))
|
||||
$message.success(i18n.t('interface.copy_succ'))
|
||||
}
|
||||
}
|
||||
|
||||
const onKeyShortcut = (e) => {
|
||||
const isCtrlOn = isMacOS() ? e.metaKey : e.ctrlKey
|
||||
switch (e.key) {
|
||||
case 'ArrowUp':
|
||||
handleKeyUp()
|
||||
break
|
||||
case 'ArrowDown':
|
||||
handleKeyDown()
|
||||
break
|
||||
case 'ArrowLeft':
|
||||
handleKeyLeft()
|
||||
break
|
||||
case 'ArrowRight':
|
||||
handleKeyRight()
|
||||
break
|
||||
case 'c':
|
||||
if (isCtrlOn) {
|
||||
handleKeyCopy()
|
||||
}
|
||||
break
|
||||
case 'Delete':
|
||||
handleKeyDelete()
|
||||
break
|
||||
case 'F5':
|
||||
handleSelectContextMenu('value_reload')
|
||||
break
|
||||
case 'r':
|
||||
if (isCtrlOn) {
|
||||
handleSelectContextMenu('value_reload')
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectContextMenu = (action) => {
|
||||
contextMenuParam.show = false
|
||||
const selectedKey = get(selectedKeys.value, 0)
|
||||
|
@ -200,15 +407,8 @@ const handleSelectContextMenu = (action) => {
|
|||
break
|
||||
case 'key_copy':
|
||||
case 'value_copy':
|
||||
ClipboardSetText(nativeRedisKey(redisKey))
|
||||
.then((succ) => {
|
||||
if (succ) {
|
||||
$message.success(i18n.t('interface.copy_succ'))
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
$message.error(e.message)
|
||||
})
|
||||
copy(nativeRedisKey(redisKey))
|
||||
$message.success(i18n.t('interface.copy_succ'))
|
||||
break
|
||||
case 'db_loadall':
|
||||
if (node != null && !!!node.loading) {
|
||||
|
@ -240,29 +440,35 @@ const onUpdateSelectedKeys = (keys, options) => {
|
|||
}
|
||||
|
||||
const onUpdateExpanded = (value, option, meta) => {
|
||||
tabStore.setExpandedKeys(props.server, value)
|
||||
if (!meta.node) {
|
||||
const expand = meta.action === 'expand'
|
||||
if (expand) {
|
||||
tabStore.addExpandedKey(props.server, value)
|
||||
} else {
|
||||
tabStore.removeExpandedKey(props.server, value)
|
||||
}
|
||||
let node = meta.node
|
||||
if (!node) {
|
||||
return
|
||||
}
|
||||
|
||||
// keep expand or collapse children while they own more than 1 child
|
||||
let node = meta.node
|
||||
while (node != null && size(node.children) === 1) {
|
||||
const key = node.children[0].key
|
||||
switch (meta.action) {
|
||||
case 'expand':
|
||||
do {
|
||||
const key = node.key
|
||||
if (expand) {
|
||||
if (node.type === ConnectionType.RedisKey) {
|
||||
node.expanded = true
|
||||
if (!includes(value, key)) {
|
||||
tabStore.addExpandedKey(props.server, key)
|
||||
}
|
||||
break
|
||||
case 'collapse':
|
||||
node.expanded = false
|
||||
tabStore.removeExpandedKey(props.server, key)
|
||||
break
|
||||
tabStore.addExpandedKey(props.server, key)
|
||||
}
|
||||
} else {
|
||||
node.expanded = false
|
||||
tabStore.removeExpandedKey(props.server, key)
|
||||
}
|
||||
node = node.children[0]
|
||||
}
|
||||
if (size(node.children) === 1) {
|
||||
node = node.children[0]
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} while (true)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -356,7 +562,7 @@ const renderLabel = ({ option }) => {
|
|||
if (option.label === '') {
|
||||
// blank label name
|
||||
return h('div', [
|
||||
h(NText, { italic: true, depth: 3 }, () => '[Empty]'),
|
||||
h(NText, { italic: true, depth: 3 }, () => '[NO NAME]'),
|
||||
h('span', () => ` (${option.keyCount || 0})`),
|
||||
])
|
||||
}
|
||||
|
@ -465,20 +671,40 @@ const renderSuffix = ({ option }) => {
|
|||
return null
|
||||
}
|
||||
|
||||
const lastLoadKey = ref(0)
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {RedisNodeItem} node
|
||||
*/
|
||||
const updateKeyDetail = (node) => {
|
||||
if (node.type === ConnectionType.RedisValue) {
|
||||
const preK = tabStore.getActivatedKey(props.server)
|
||||
if (!isEmpty(preK) && preK === node.key && Date.now() - lastLoadKey.value > 1000) {
|
||||
// reload key already activated
|
||||
lastLoadKey.value = Date.now()
|
||||
const { db, redisKey, redisKeyCode } = node
|
||||
browserStore.reloadKey({
|
||||
server: props.server,
|
||||
db,
|
||||
key: redisKeyCode || redisKey,
|
||||
})
|
||||
} else if (tabStore.setActivatedKey(props.server, node.key)) {
|
||||
const { db, redisKey, redisKeyCode } = node
|
||||
browserStore.loadKeySummary({
|
||||
server: props.server,
|
||||
db,
|
||||
key: redisKeyCode || redisKey,
|
||||
clearValue: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const nodeProps = ({ option }) => {
|
||||
return {
|
||||
onClick: () => {
|
||||
if (option.type === ConnectionType.RedisValue) {
|
||||
if (tabStore.setActivatedKey(props.server, option.key)) {
|
||||
const { db, redisKey, redisKeyCode } = option
|
||||
browserStore.loadKeySummary({
|
||||
server: props.server,
|
||||
db,
|
||||
key: redisKeyCode || redisKey,
|
||||
clearValue: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
updateKeyDetail(option)
|
||||
},
|
||||
onDblclick: () => {
|
||||
if (props.loading) {
|
||||
|
@ -588,6 +814,7 @@ defineExpose({
|
|||
:expand-on-click="false"
|
||||
:expanded-keys="expandedKeys"
|
||||
:filter="(pattern, node) => includes(node.redisKey, pattern)"
|
||||
:keyboard="false"
|
||||
:node-props="nodeProps"
|
||||
:pattern="props.pattern"
|
||||
:render-label="renderLabel"
|
||||
|
@ -598,7 +825,7 @@ defineExpose({
|
|||
check-strategy="child"
|
||||
class="fill-height"
|
||||
virtual-scroll
|
||||
@keydown.delete="handleSelectContextMenu('value_remove')"
|
||||
@keydown="onKeyShortcut"
|
||||
@update:selected-keys="onUpdateSelectedKeys"
|
||||
@update:expanded-keys="onUpdateExpanded"
|
||||
@update:checked-keys="onUpdateCheckedKeys">
|
||||
|
@ -623,7 +850,7 @@ defineExpose({
|
|||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/content';
|
||||
@use '@/styles/content';
|
||||
|
||||
.browser-tree-wrapper {
|
||||
height: 100%;
|
||||
|
|
|
@ -295,6 +295,7 @@ const openConnection = async (name) => {
|
|||
tabStore.upsertTab({
|
||||
server: name,
|
||||
db: browserStore.getSelectedDB(name),
|
||||
forceSwitch: true,
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
|
@ -437,7 +438,7 @@ const findSiblingsAndIndex = (node, nodes) => {
|
|||
}
|
||||
|
||||
// delay save until drop stopped after 2 seconds
|
||||
const saveSort = debounce(connectionStore.saveConnectionSorted, 2000, { trailing: true })
|
||||
const saveSort = debounce(connectionStore.saveConnectionSorted, 1500, { trailing: true })
|
||||
const handleDrop = ({ node, dragNode, dropPosition }) => {
|
||||
const [dragNodeSiblings, dragNodeIndex] = findSiblingsAndIndex(dragNode, connectionStore.connections)
|
||||
if (dragNodeSiblings === null || dragNodeIndex === null) {
|
||||
|
@ -542,7 +543,7 @@ const onCancelOpen = () => {
|
|||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/content';
|
||||
@use '@/styles/content';
|
||||
|
||||
.connection-tree-wrapper {
|
||||
height: 100%;
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
import { computed, ref } from 'vue'
|
||||
import { NIcon, useThemeVars } from 'naive-ui'
|
||||
import Database from '@/components/icons/Database.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Server from '@/components/icons/Server.vue'
|
||||
import IconButton from '@/components/common/IconButton.vue'
|
||||
import Config from '@/components/icons/Config.vue'
|
||||
|
@ -15,6 +14,7 @@ import { extraTheme } from '@/utils/extra_theme.js'
|
|||
import useBrowserStore from 'stores/browser.js'
|
||||
import { useRender } from '@/utils/render.js'
|
||||
import wechatUrl from '@/assets/images/wechat_official.png'
|
||||
import bilibiliUrl from '@/assets/images/bilibili_official.png'
|
||||
import QRCode from '@/components/icons/QRCode.vue'
|
||||
import Twitter from '@/components/icons/Twitter.vue'
|
||||
import { trackEvent } from '@/utils/analytics.js'
|
||||
|
@ -38,7 +38,6 @@ const emit = defineEmits(['update:value'])
|
|||
const iconSize = computed(() => Math.floor(props.width * 0.45))
|
||||
|
||||
const browserStore = useBrowserStore()
|
||||
const i18n = useI18n()
|
||||
const showWechat = ref(false)
|
||||
const menuOptions = computed(() => {
|
||||
return [
|
||||
|
@ -172,7 +171,6 @@ const exThemeVars = computed(() => {
|
|||
:options="preferencesOptions"
|
||||
:render-icon="({ icon }) => render.renderIcon(icon)"
|
||||
:render-label="({ label }) => render.renderLabel($t(label), { class: 'context-menu-item' })"
|
||||
content-class="nav-menu-button"
|
||||
trigger="click"
|
||||
@select="onSelectPreferenceMenu">
|
||||
<icon-button :icon="Config" :size="iconSize" :stroke-width="3" />
|
||||
|
@ -182,7 +180,6 @@ const exThemeVars = computed(() => {
|
|||
:icon="QRCode"
|
||||
:size="iconSize"
|
||||
:tooltip-delay="100"
|
||||
class="nav-menu-button"
|
||||
t-tooltip="ribbon.wechat_official"
|
||||
@click="openWechatOfficial" />
|
||||
<icon-button
|
||||
|
@ -191,21 +188,22 @@ const exThemeVars = computed(() => {
|
|||
:icon="Twitter"
|
||||
:size="iconSize"
|
||||
:tooltip-delay="100"
|
||||
class="nav-menu-button"
|
||||
t-tooltip="ribbon.follow_x"
|
||||
@click="openX" />
|
||||
<icon-button
|
||||
:icon="Github"
|
||||
:size="iconSize"
|
||||
:tooltip-delay="100"
|
||||
class="nav-menu-button"
|
||||
t-tooltip="ribbon.github"
|
||||
@click="openGithub" />
|
||||
</div>
|
||||
|
||||
<!-- wechat official modal -->
|
||||
<n-modal v-model:show="showWechat" close-on-esc mask-closable transform-origin="center">
|
||||
<n-image :src="wechatUrl" :width="400" preview-disabled />
|
||||
<n-flex vertical>
|
||||
<n-image :src="wechatUrl" :width="400" preview-disabled />
|
||||
<n-image :src="bilibiliUrl" :width="400" preview-disabled />
|
||||
</n-flex>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -280,15 +278,10 @@ const exThemeVars = computed(() => {
|
|||
.nav-menu-item {
|
||||
align-items: center;
|
||||
padding: 10px 0 15px;
|
||||
gap: 20px;
|
||||
--wails-draggable: none;
|
||||
|
||||
.nav-menu-button {
|
||||
margin-bottom: 6px;
|
||||
|
||||
:hover {
|
||||
color: v-bind('themeVars.primaryColor');
|
||||
}
|
||||
button {
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* all types of text alignment
|
||||
* @enum {number}
|
||||
*/
|
||||
export const TextAlignType = {
|
||||
Center: 0,
|
||||
Left: 1,
|
||||
}
|
|
@ -22,6 +22,7 @@ export const decodeTypes = {
|
|||
GZIP: 'GZip',
|
||||
DEFLATE: 'Deflate',
|
||||
ZSTD: 'ZStd',
|
||||
LZ4: 'LZ4',
|
||||
BROTLI: 'Brotli',
|
||||
MSGPACK: 'Msgpack',
|
||||
PHP: 'PHP',
|
||||
|
|
|
@ -126,6 +126,8 @@
|
|||
"length": "Length",
|
||||
"entries": "Entries",
|
||||
"memory_usage": "Memory Usage",
|
||||
"text_align_left": "Text Align Left",
|
||||
"text_align_center": "Text Align Center",
|
||||
"view_as": "View As",
|
||||
"decode_with": "Decode / Decompress",
|
||||
"custom_decoder": "New Custom Decoder",
|
||||
|
@ -296,6 +298,8 @@
|
|||
"affected_key": "Affected Keys",
|
||||
"show_affected_key": "Show Affected Keys",
|
||||
"confirm_delete_key": "Confirm delete {num} key(s)",
|
||||
"direct_delete": "Delete match pattern directly",
|
||||
"confirm_delete": "Confirm Delete",
|
||||
"async_delete": "Async Execution",
|
||||
"async_delete_title": "Don't wait for result",
|
||||
"confirm_flush": "I know what I'm doing!",
|
||||
|
@ -329,7 +333,8 @@
|
|||
"set_key_filter": "Set Key Filter",
|
||||
"filter_pattern": "Pattern",
|
||||
"filter_pattern_tip": "Filter by directly input, and scan by press 'Enter'.\n\n* matches 0 or more chars, e.g. 'key*' \n? matches single char, e.g. 'key?'\n[] matches range, e.g. 'key[1-3]'\n\\ escapes special chars",
|
||||
"exact_match_tip": "Exact Match"
|
||||
"exact_match_tip": "Exact Match",
|
||||
"filter_type_not_support": "Type filtering is not supported for Redis 5.x and below."
|
||||
},
|
||||
"export": {
|
||||
"name": "Export Data",
|
||||
|
|
|
@ -126,6 +126,8 @@
|
|||
"length": "Longitud",
|
||||
"entries": "Entradas",
|
||||
"memory_usage": "Uso de memoria",
|
||||
"text_align_left": "Alinear a la izquierda",
|
||||
"text_align_center": "Centrar",
|
||||
"view_as": "Ver como",
|
||||
"decode_with": "Decodificar / Descomprimir",
|
||||
"custom_decoder": "Nuevo decodificador personalizado",
|
||||
|
@ -296,6 +298,8 @@
|
|||
"affected_key": "Claves afectadas",
|
||||
"show_affected_key": "Mostrar claves afectadas",
|
||||
"confirm_delete_key": "Confirmar eliminar {num} clave(s)",
|
||||
"direct_delete": "Eliminar el patrón coincidente directamente",
|
||||
"confirm_delete": "Confirmar eliminación",
|
||||
"async_delete": "Ejecución asíncrona",
|
||||
"async_delete_title": "No esperar el resultado",
|
||||
"confirm_flush": "¡Sé lo que estoy haciendo!",
|
||||
|
@ -329,7 +333,8 @@
|
|||
"set_key_filter": "Establecer filtro de clave",
|
||||
"filter_pattern": "Patrón",
|
||||
"filter_pattern_tip": "Filtre la lista actual ingresando directamente, y escanee el servidor presionando 'Ingresar'.\n\n* coincide con 0 o más caracteres, ej. 'key*'\n? coincide con un carácter, ej. 'key?'\n[] coincide con un rango, ej. 'key[1-3]'\n\\ escapa caracteres especiales",
|
||||
"exact_match_tip": "Coincidencia exacta"
|
||||
"exact_match_tip": "Coincidencia exacta",
|
||||
"filter_type_not_support": "El filtrado por tipo no es compatible con Redis 5.x y versiones anteriores"
|
||||
},
|
||||
"export": {
|
||||
"name": "Exportar datos",
|
||||
|
|
|
@ -126,6 +126,8 @@
|
|||
"length": "Longueur",
|
||||
"entries": "Entrées",
|
||||
"memory_usage": "Utilisation de la mémoire",
|
||||
"text_align_left": "Aligner à gauche",
|
||||
"text_align_center": "Centrer",
|
||||
"view_as": "Voir comme",
|
||||
"decode_with": "Décoder / Décompresser",
|
||||
"custom_decoder": "Nouveau décodeur personnalisé",
|
||||
|
@ -296,6 +298,8 @@
|
|||
"affected_key": "Clés affectées",
|
||||
"show_affected_key": "Afficher les clés affectées",
|
||||
"confirm_delete_key": "Confirmer la suppression de {num} clé(s)",
|
||||
"direct_delete": "Supprimer le modèle correspondant directement",
|
||||
"confirm_delete": "Confirmer la suppression",
|
||||
"async_delete": "Exécution asynchrone",
|
||||
"async_delete_title": "Ne pas attendre le résultat",
|
||||
"confirm_flush": "Je sais ce que je fais !",
|
||||
|
@ -329,7 +333,8 @@
|
|||
"set_key_filter": "Définir le filtre de clé",
|
||||
"filter_pattern": "Modèle",
|
||||
"filter_pattern_tip": "Filtrez la liste actuelle en saisissant directement, et scannez le serveur en appuyant sur 'Entrée'.\n\n* correspond à 0 ou plusieurs caractères, ex : 'key*'\n? correspond à un seul caractère, ex : 'key?'\n[] correspond à une plage, ex : 'key[1-3]' échappe les caractères spéciaux",
|
||||
"exact_match_tip": "Correspondance exacte"
|
||||
"exact_match_tip": "Correspondance exacte",
|
||||
"filter_type_not_support": "Le filtrage par type n’est pas pris en charge pour Redis 5.x et les versions antérieures"
|
||||
},
|
||||
"export": {
|
||||
"name": "Exporter les données",
|
||||
|
|
|
@ -126,6 +126,8 @@
|
|||
"length": "長さ",
|
||||
"entries": "エントリ",
|
||||
"memory_usage": "メモリ使用量",
|
||||
"text_align_left": "左揃え",
|
||||
"text_align_center": "中央揃え",
|
||||
"view_as": "表示形式",
|
||||
"decode_with": "デコード/解凍",
|
||||
"custom_decoder": "新しいカスタムデコーダー",
|
||||
|
@ -296,6 +298,8 @@
|
|||
"affected_key": "影響を受けるキー",
|
||||
"show_affected_key": "影響を受けるキーを表示",
|
||||
"confirm_delete_key": "{num}個のキーを削除することを確認",
|
||||
"direct_delete": "一致するパターンを直接削除",
|
||||
"confirm_delete": "削除を確認",
|
||||
"async_delete": "非同期実行",
|
||||
"async_delete_title": "結果を待たない",
|
||||
"confirm_flush": "自分が実行しようとしている操作を理解しています!",
|
||||
|
@ -329,7 +333,8 @@
|
|||
"set_key_filter": "キーフィルターを設定",
|
||||
"filter_pattern": "パターン",
|
||||
"filter_pattern_tip": "直接入力して現在のリストをフィルタリングし、Enterキーを押すとサーバーをスキャンできます。\n\n*:0文字以上にマッチ。例:\"key*\"は\"key\"で始まるすべてのキーにマッチ\n?:1文字にマッチ。例:\"key?\"は\"key1\"、\"key2\"にマッチ\n[ ]:指定範囲の1文字にマッチ。例:\"key[1-3]\"は\"key1\"、\"key2\"、\"key3\"にマッチ\n\\:エスケープ文字。*、?、[、]をリテラルとして解釈したい場合は\"\\ \"をつける",
|
||||
"exact_match_tip": "完全一致"
|
||||
"exact_match_tip": "完全一致",
|
||||
"filter_type_not_support": "タイプフィルタリングは、Redis 5.x 以前のバージョンには対応していません"
|
||||
},
|
||||
"export": {
|
||||
"name": "データをエクスポート",
|
||||
|
|
|
@ -126,6 +126,8 @@
|
|||
"length": "길이",
|
||||
"entries": "항목 수",
|
||||
"memory_usage": "메모리 사용량",
|
||||
"text_align_left": "텍스트 왼쪽 정렬",
|
||||
"text_align_center": "텍스트 가운데 정렬",
|
||||
"view_as": "보기",
|
||||
"decode_with": "디코딩/압축 해제",
|
||||
"custom_decoder": "새 사용자 정의 디코더",
|
||||
|
@ -296,6 +298,8 @@
|
|||
"affected_key": "영향받는 키",
|
||||
"show_affected_key": "영향받는 키 표시",
|
||||
"confirm_delete_key": "{num}개의 키를 삭제하시겠습니까?",
|
||||
"direct_delete": "일치하는 패턴 직접 삭제",
|
||||
"confirm_delete": "삭제 확인",
|
||||
"async_delete": "비동기 실행",
|
||||
"async_delete_title": "결과를 기다리지 않음",
|
||||
"confirm_flush": "진행 중인 작업을 알고 있습니다!",
|
||||
|
@ -329,7 +333,8 @@
|
|||
"set_key_filter": "키 필터 설정",
|
||||
"filter_pattern": "패턴",
|
||||
"filter_pattern_tip": "직접 입력하여 현재 목록을 필터링하고, Enter키를 누르면 서버를 스캔할 수 있습니다.\n\n* 0개 이상의 문자 일치, 예) 'key*'\n? 단일 문자 일치, 예) 'key?'\n[] 범위 일치, 예) 'key[1-3]'\n\\ 특수문자 이스케이프",
|
||||
"exact_match_tip": "완전 일치"
|
||||
"exact_match_tip": "완전 일치",
|
||||
"filter_type_not_support": "타입 필터링은 Redis 5.x 및 이전 버전을 지원하지 않습니다"
|
||||
},
|
||||
"export": {
|
||||
"name": "데이터 내보내기",
|
||||
|
|
|
@ -126,6 +126,8 @@
|
|||
"length": "Tamanho",
|
||||
"entries": "Entradas",
|
||||
"memory_usage": "Uso de Memória",
|
||||
"text_align_left": "Alinhar à esquerda",
|
||||
"text_align_center": "Centralizar",
|
||||
"view_as": "Visualizar Como",
|
||||
"decode_with": "Decodificar / Descompressão",
|
||||
"custom_decoder": "Novo Decodificador Personalizado",
|
||||
|
@ -296,6 +298,8 @@
|
|||
"affected_key": "Chaves Afetadas",
|
||||
"show_affected_key": "Mostrar Chaves Afetadas",
|
||||
"confirm_delete_key": "Confirmar Exclusão de {num} Chave(s)",
|
||||
"direct_delete": "Excluir padrão correspondente diretamente",
|
||||
"confirm_delete": "Confirmar exclusão",
|
||||
"async_delete": "Execução Assíncrona",
|
||||
"async_delete_title": "Não esperar pelo resultado da operação",
|
||||
"confirm_flush": "Eu sei o que estou fazendo!",
|
||||
|
@ -329,7 +333,8 @@
|
|||
"set_key_filter": "Definir Filtro de Chave",
|
||||
"filter_pattern": "Padrão",
|
||||
"filter_pattern_tip": "Filtre a lista atual inserindo diretamente, e escaneie o servidor pressionando 'Enter'.\n\n* corresponde a 0 ou mais caracteres, ex: 'chave*'\n? corresponde a um único caractere, ex: 'chave?'\n[] corresponde a um intervalo, ex: 'chave[1-3]'\n\\ escapa caracteres especiais",
|
||||
"exact_match_tip": "Correspondência Exata"
|
||||
"exact_match_tip": "Correspondência Exata",
|
||||
"filter_type_not_support": "A filtragem por tipo não é suportada para Redis 5.x e versões anteriores"
|
||||
},
|
||||
"export": {
|
||||
"name": "Exportar Dados",
|
||||
|
|
|
@ -126,6 +126,8 @@
|
|||
"length": "Длина",
|
||||
"entries": "Записи",
|
||||
"memory_usage": "Использование памяти",
|
||||
"text_align_left": "Выравнивание по левому краю",
|
||||
"text_align_center": "Выравнивание по центру",
|
||||
"view_as": "Вид",
|
||||
"decode_with": "Декодировать/Распаковать",
|
||||
"custom_decoder": "Новый пользовательский декодер",
|
||||
|
@ -296,6 +298,8 @@
|
|||
"affected_key": "Затронутые ключи",
|
||||
"show_affected_key": "Показать затронутые ключи",
|
||||
"confirm_delete_key": "Подтвердить удаление {num} ключ(ей/ей)",
|
||||
"direct_delete": "Удалить совпадающий шаблон напрямую",
|
||||
"confirm_delete": "Подтвердить удаление",
|
||||
"async_delete": "Асинхронное выполнение",
|
||||
"async_delete_title": "Не ждать результата",
|
||||
"confirm_flush": "Я знаю, что делаю!",
|
||||
|
@ -329,7 +333,8 @@
|
|||
"set_key_filter": "Установить фильтр ключей",
|
||||
"filter_pattern": "Шаблон",
|
||||
"filter_pattern_tip": "Отфильтруйте текущий список, введя напрямую, и выполните сканирование сервера, нажав 'Enter'.\n\n* соответствует 0 или более символов, напр. 'key*'\n? соответствует одному символу, напр. 'key?'\n[] соответствует диапазону, напр. 'key[1-3]'\n\\ экранирует спецсимволы",
|
||||
"exact_match_tip": "Точное совпадение"
|
||||
"exact_match_tip": "Точное совпадение",
|
||||
"filter_type_not_support": "Фильтрация по типу не поддерживается для Redis версии 5.x и ниже"
|
||||
},
|
||||
"export": {
|
||||
"name": "Экспорт данных",
|
||||
|
|
|
@ -56,7 +56,7 @@
|
|||
"show_linenum": "显示行号",
|
||||
"show_folding": "启用代码折叠",
|
||||
"drop_text": "允许拖放文本",
|
||||
"links": "支持连接跳转"
|
||||
"links": "支持链接跳转"
|
||||
},
|
||||
"cli": {
|
||||
"name": "命令行",
|
||||
|
@ -126,6 +126,8 @@
|
|||
"length": "长度",
|
||||
"entries": "条目",
|
||||
"memory_usage": "内存占用",
|
||||
"text_align_left": "文本居左",
|
||||
"text_align_center": "文本居中",
|
||||
"view_as": "查看方式",
|
||||
"decode_with": "解码/解压方式",
|
||||
"custom_decoder": "添加自定义解码",
|
||||
|
@ -296,6 +298,8 @@
|
|||
"affected_key": "受影响的键名",
|
||||
"show_affected_key": "查看受影响的键名",
|
||||
"confirm_delete_key": "确认删除{num}个键",
|
||||
"direct_delete": "直接匹配删除",
|
||||
"confirm_delete": "确认删除",
|
||||
"async_delete": "异步执行",
|
||||
"async_delete_title": "不等待操作结果",
|
||||
"confirm_flush": "我知道我正在执行的操作!",
|
||||
|
@ -329,7 +333,8 @@
|
|||
"set_key_filter": "设置键过滤器",
|
||||
"filter_pattern": "过滤表达式",
|
||||
"filter_pattern_tip": "直接输入筛选当前列表,回车后可对服务器进行扫描。\n\n *:匹配零个或多个字符。例如:\"key*\"匹配到以\"key\"开头的所有键\n?:匹配单个字符。例如:\"key?\"匹配\"key1\"、\"key2\"\n[ ]:匹配指定范围内的单个字符。例如:\"key[1-3]\"可以匹配类似于 \"key1\"、\"key2\"、\"key3\" 的键\n\\:转义字符。如果想要匹配 *、?、[、或],需要使用反斜杠\"\\\"进行转义",
|
||||
"exact_match_tip": "完全匹配"
|
||||
"exact_match_tip": "完全匹配",
|
||||
"filter_type_not_support": "类型筛选不支持 Redis 5.x 及以下版本"
|
||||
},
|
||||
"export": {
|
||||
"name": "导出数据",
|
||||
|
|
|
@ -126,6 +126,8 @@
|
|||
"length": "長度",
|
||||
"entries": "條目",
|
||||
"memory_usage": "記憶體使用量",
|
||||
"text_align_left": "文字靠左",
|
||||
"text_align_center": "文字置中",
|
||||
"view_as": "檢視方式",
|
||||
"decode_with": "解碼/解壓方式",
|
||||
"custom_decoder": "新增自定義解碼",
|
||||
|
@ -296,6 +298,8 @@
|
|||
"affected_key": "受影響的鍵名",
|
||||
"show_affected_key": "檢視受影響的鍵名",
|
||||
"confirm_delete_key": "確認刪除{num}個鍵",
|
||||
"direct_delete": "直接匹配刪除",
|
||||
"confirm_delete": "確認刪除",
|
||||
"async_delete": "異步執行",
|
||||
"async_delete_title": "不等待操作結果",
|
||||
"confirm_flush": "我知道我正在執行的操作!",
|
||||
|
@ -329,7 +333,8 @@
|
|||
"set_key_filter": "設定鍵過濾器",
|
||||
"filter_pattern": "過濾表示式",
|
||||
"filter_pattern_tip": "直接鍵入篩選目前清單,按Enter鍵後可對伺服器進行掃描。\n\n*:匹配零個或多個字元。例如:\"key*\"匹配到以\"key\"開頭的所有鍵\n?:匹配單個字元。例如:\"key?\"匹配\"key1\", \"key2\"\n[ ]:匹配指定範圍內的單個字元。例如:\"key[1-3]\"可以匹配類似於 \"key1\", \"key2\", \"key3\" 的鍵\n\\:轉義字元。如果想要匹配 *, ?, [, 或],需要使用反斜線\"\\\"進行轉義",
|
||||
"exact_match_tip": "精準匹配"
|
||||
"exact_match_tip": "精準匹配",
|
||||
"filter_type_not_support": "類型篩選不支援 Redis 5.x 以下版本"
|
||||
},
|
||||
"export": {
|
||||
"name": "匯出資料",
|
||||
|
|
|
@ -28,6 +28,7 @@ export class RedisServerState {
|
|||
* @param {LoadingState} loadingState all loading state in opened connections map by server and LoadingState
|
||||
* @param {KeyViewType} viewType view type selection for all opened connections group by 'server'
|
||||
* @param {Map<string, RedisNodeItem>} nodeMap map nodes by "type#key"
|
||||
* @param {string} version redis server version
|
||||
*/
|
||||
constructor({
|
||||
name,
|
||||
|
@ -40,6 +41,7 @@ export class RedisServerState {
|
|||
loadingState = {},
|
||||
viewType = KeyViewType.Tree,
|
||||
nodeMap = new Map(),
|
||||
version = '',
|
||||
}) {
|
||||
this.name = name
|
||||
this.db = db
|
||||
|
@ -52,6 +54,7 @@ export class RedisServerState {
|
|||
this.loadingState = loadingState
|
||||
this.viewType = viewType
|
||||
this.nodeMap = nodeMap
|
||||
this.version = version
|
||||
this.decodeHistory = new Map()
|
||||
this.decodeHistoryLimit = 100
|
||||
this.getRoot()
|
||||
|
@ -293,6 +296,9 @@ export class RedisServerState {
|
|||
removeKeyNode(key, isLayer) {
|
||||
if (isLayer === true) {
|
||||
this.deleteChildrenKeyNodes(key)
|
||||
} else {
|
||||
const nodeKey = `${ConnectionType.RedisValue}/${key}`
|
||||
this.nodeMap.delete(nodeKey)
|
||||
}
|
||||
|
||||
const dbRoot = this.getRoot()
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { endsWith, get, isEmpty, map, now, size } from 'lodash'
|
||||
import { endsWith, get, isEmpty, join, map, now, size, slice, split } from 'lodash'
|
||||
import {
|
||||
AddHashField,
|
||||
AddListItem,
|
||||
|
@ -11,10 +11,12 @@ import {
|
|||
ConvertValue,
|
||||
DeleteKey,
|
||||
DeleteKeys,
|
||||
DeleteKeysByPattern,
|
||||
ExportKey,
|
||||
FlushDB,
|
||||
GetClientList,
|
||||
GetCmdHistory,
|
||||
GetHashValue,
|
||||
GetKeyDetail,
|
||||
GetKeySummary,
|
||||
GetKeyType,
|
||||
|
@ -109,7 +111,7 @@ const useBrowserStore = defineStore('browser', {
|
|||
|
||||
/**
|
||||
* get database info list
|
||||
* @param server
|
||||
* @param {string} server
|
||||
* @return {RedisDatabaseItem[]}
|
||||
*/
|
||||
getDBList(server) {
|
||||
|
@ -120,6 +122,18 @@ const useBrowserStore = defineStore('browser', {
|
|||
return []
|
||||
},
|
||||
|
||||
/**
|
||||
* get server version
|
||||
* @param {string} server
|
||||
*/
|
||||
getServerVersion(server) {
|
||||
const serverInst = this.servers[server]
|
||||
if (serverInst != null) {
|
||||
return serverInst.version
|
||||
}
|
||||
return '1.0.0'
|
||||
},
|
||||
|
||||
/**
|
||||
* get database by server name and database index
|
||||
* @param {string} server
|
||||
|
@ -238,7 +252,7 @@ const useBrowserStore = defineStore('browser', {
|
|||
// if (connNode == null) {
|
||||
// throw new Error('no such connection')
|
||||
// }
|
||||
const { db, view = KeyViewType.Tree, lastDB } = data
|
||||
const { db, view = KeyViewType.Tree, lastDB, version } = data
|
||||
if (isEmpty(db)) {
|
||||
throw new Error('no db loaded')
|
||||
}
|
||||
|
@ -247,6 +261,7 @@ const useBrowserStore = defineStore('browser', {
|
|||
separator: this.getSeparator(name),
|
||||
db: -1,
|
||||
viewType: view,
|
||||
version,
|
||||
})
|
||||
/** @type {Object.<number,RedisDatabaseItem>} **/
|
||||
const databases = {}
|
||||
|
@ -343,10 +358,11 @@ const useBrowserStore = defineStore('browser', {
|
|||
|
||||
/**
|
||||
*
|
||||
* @param server
|
||||
* @param {string} server
|
||||
* @param {boolean} mute
|
||||
* @returns {Promise<{}>}
|
||||
*/
|
||||
async getServerInfo(server) {
|
||||
async getServerInfo(server, mute) {
|
||||
try {
|
||||
const { success, data, msg } = await ServerInfo(server)
|
||||
if (success) {
|
||||
|
@ -356,7 +372,7 @@ const useBrowserStore = defineStore('browser', {
|
|||
serverInst.stats = data
|
||||
}
|
||||
return data
|
||||
} else if (!isEmpty(msg)) {
|
||||
} else if (!isEmpty(msg) && mute !== true) {
|
||||
$message.warning(msg)
|
||||
}
|
||||
} finally {
|
||||
|
@ -370,9 +386,10 @@ const useBrowserStore = defineStore('browser', {
|
|||
* @param {number} db
|
||||
* @param {string|number[]} [key] null or blank indicate that update tab to display normal content (blank content or server status)
|
||||
* @param {boolean} [clearValue]
|
||||
* @param {boolean} [redirect] redirect to key detail tab
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async loadKeySummary({ server, db, key, clearValue }) {
|
||||
async loadKeySummary({ server, db, key, clearValue, redirect = true }) {
|
||||
try {
|
||||
const tab = useTabStore()
|
||||
if (!isEmpty(key)) {
|
||||
|
@ -386,7 +403,7 @@ const useBrowserStore = defineStore('browser', {
|
|||
const k = nativeRedisKey(key)
|
||||
const binaryKey = k !== key
|
||||
tab.upsertTab({
|
||||
subTab: BrowserTabType.KeyDetail,
|
||||
subTab: redirect === false ? null : BrowserTabType.KeyDetail,
|
||||
server,
|
||||
db,
|
||||
type,
|
||||
|
@ -476,7 +493,7 @@ const useBrowserStore = defineStore('browser', {
|
|||
if (showLoading) {
|
||||
tab.updateLoading({ server, db, loading: true })
|
||||
}
|
||||
await this.loadKeySummary({ server, db, key, clearValue: true })
|
||||
await this.loadKeySummary({ server, db, key, clearValue: true, redirect: false })
|
||||
await this.loadKeyDetail({
|
||||
server,
|
||||
db,
|
||||
|
@ -692,17 +709,19 @@ const useBrowserStore = defineStore('browser', {
|
|||
}
|
||||
let match = prefix
|
||||
const separator = this.getSeparator(server)
|
||||
if (!endsWith(match, separator)) {
|
||||
match += separator + '*'
|
||||
} else {
|
||||
match += '*'
|
||||
if (!isEmpty(match)) {
|
||||
if (!endsWith(match, separator)) {
|
||||
match += separator + '*'
|
||||
} else {
|
||||
match += '*'
|
||||
}
|
||||
}
|
||||
// FIXME: ignore original match pattern due to redis not support combination matching
|
||||
const { match: originMatch, type: keyType, exact } = this.getKeyFilter(server)
|
||||
const { keys, maxKeys, success } = await this._loadKeys({
|
||||
server,
|
||||
db,
|
||||
match: originMatch,
|
||||
match: match || originMatch,
|
||||
exact: false,
|
||||
matchType: keyType,
|
||||
all: true,
|
||||
|
@ -773,6 +792,37 @@ const useBrowserStore = defineStore('browser', {
|
|||
return serverInst.nodeMap.get(keyPart)
|
||||
},
|
||||
|
||||
/**
|
||||
* get parent tree node by key name
|
||||
* @param key
|
||||
* @return {RedisNodeItem|null}
|
||||
*/
|
||||
getParentNode(key) {
|
||||
const i = key.indexOf('#')
|
||||
if (i < 0) {
|
||||
return null
|
||||
}
|
||||
const [server, db] = split(key.substring(0, i), '/')
|
||||
if (isEmpty(server) || isEmpty(db)) {
|
||||
return null
|
||||
}
|
||||
/** @type {RedisServerState} **/
|
||||
const serverInst = this.servers[server]
|
||||
if (serverInst == null) {
|
||||
return null
|
||||
}
|
||||
const separator = this.getSeparator(server)
|
||||
const keyPart = key.substring(i)
|
||||
const keyStartIdx = keyPart.indexOf('/')
|
||||
const redisKey = keyPart.substring(keyStartIdx + 1)
|
||||
const redisKeyParts = split(redisKey, separator)
|
||||
const parentKey = slice(redisKeyParts, 0, size(redisKeyParts) - 1)
|
||||
if (isEmpty(parentKey)) {
|
||||
return serverInst.getRoot()
|
||||
}
|
||||
return serverInst.nodeMap.get(`${ConnectionType.RedisKey}/${join(parentKey, separator)}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* set redis key
|
||||
* @param {string} server
|
||||
|
@ -800,7 +850,7 @@ const useBrowserStore = defineStore('browser', {
|
|||
if (success) {
|
||||
/** @type RedisServerState **/
|
||||
const serverInst = this.servers[server]
|
||||
if (serverInst != null) {
|
||||
if (serverInst != null && serverInst.db === db) {
|
||||
// const { value } = data
|
||||
// update tree view data
|
||||
const { newKey = 0 } = serverInst.addKeyNodes([key], true)
|
||||
|
@ -808,11 +858,12 @@ const useBrowserStore = defineStore('browser', {
|
|||
serverInst.tidyNode(key)
|
||||
serverInst.updateDBKeyCount(db, newKey)
|
||||
}
|
||||
}
|
||||
const { value: updatedValue } = data
|
||||
if (updatedValue != null) {
|
||||
const tab = useTabStore()
|
||||
tab.updateValue({ server, db, key, value: updatedValue })
|
||||
|
||||
const { value: updatedValue } = data
|
||||
if (updatedValue != null) {
|
||||
const tab = useTabStore()
|
||||
tab.updateValue({ server, db, key, value: updatedValue })
|
||||
}
|
||||
}
|
||||
// this.loadKeySummary({ server, db, key })
|
||||
return {
|
||||
|
@ -954,6 +1005,31 @@ const useBrowserStore = defineStore('browser', {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* get hash field
|
||||
* @param {string} server
|
||||
* @param {number} db
|
||||
* @param {string} key
|
||||
* @param {string} field
|
||||
* @param {decodeTypes} [decode]
|
||||
* @param {formatTypes} [format]
|
||||
* @return {Promise<{{msg: string, success: boolean, updated: HashEntryItem[]}>}
|
||||
*/
|
||||
async getHashField({ server, db, key, field, decode = decodeTypes.NONE, format = formatTypes.RAW }) {
|
||||
try {
|
||||
const { data, success, msg } = await GetHashValue({ server, db, key, field, decode, format })
|
||||
if (success && !isEmpty(data)) {
|
||||
const tab = useTabStore()
|
||||
tab.updateValueEntries({ server, db, key, type: 'hash', entries: [data] })
|
||||
return { success, updated: data }
|
||||
} else {
|
||||
return { success: false, msg }
|
||||
}
|
||||
} catch (e) {
|
||||
return { success: false, msg: e.message }
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* remove hash field
|
||||
* @param {string} server
|
||||
|
@ -1715,6 +1791,66 @@ const useBrowserStore = defineStore('browser', {
|
|||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* delete multiple keys by pattern
|
||||
* @param server
|
||||
* @param db
|
||||
* @param pattern
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async deleteByPattern(server, db, pattern) {
|
||||
const msgRef = $message.loading(i18nGlobal.t('dialogue.delete.deleting'), { duration: 0, closable: true })
|
||||
let deleted = []
|
||||
let failCount = 0
|
||||
let canceled = false
|
||||
try {
|
||||
const { success, msg, data } = await DeleteKeysByPattern(server, db, pattern)
|
||||
if (success) {
|
||||
canceled = get(data, 'canceled', false)
|
||||
deleted = get(data, 'deleted', [])
|
||||
failCount = get(data, 'failed', 0)
|
||||
} else {
|
||||
$message.error(msg)
|
||||
}
|
||||
} finally {
|
||||
msgRef.destroy()
|
||||
// clear checked keys
|
||||
const tab = useTabStore()
|
||||
tab.setCheckedKeys(server)
|
||||
}
|
||||
// refresh model data
|
||||
const deletedCount = size(deleted)
|
||||
if (canceled) {
|
||||
$message.info(i18nGlobal.t('dialogue.handle_cancel'))
|
||||
} else if (failCount <= 0) {
|
||||
// no fail
|
||||
$message.success(i18nGlobal.t('dialogue.delete.completed', { success: deletedCount, fail: failCount }))
|
||||
} else if (failCount >= deletedCount) {
|
||||
// all fail
|
||||
$message.error(i18nGlobal.t('dialogue.delete.completed', { success: deletedCount, fail: failCount }))
|
||||
} else {
|
||||
// some fail
|
||||
$message.warning(i18nGlobal.t('dialogue.delete.completed', { success: deletedCount, fail: failCount }))
|
||||
}
|
||||
// update ui
|
||||
timeout(100).then(async () => {
|
||||
/** @type RedisServerState **/
|
||||
const serverInst = this.servers[server]
|
||||
if (serverInst != null) {
|
||||
let start = now()
|
||||
for (let i = 0; i < deleted.length; i++) {
|
||||
serverInst.removeKeyNode(deleted[i], false)
|
||||
if (now() - start > 300) {
|
||||
await timeout(100)
|
||||
start = now()
|
||||
}
|
||||
}
|
||||
serverInst.tidyNode('', true)
|
||||
serverInst.updateDBKeyCount(db, -deletedCount)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* export multiple keys
|
||||
* @param {string} server
|
||||
|
@ -1957,7 +2093,7 @@ const useBrowserStore = defineStore('browser', {
|
|||
if (serverInst == null) {
|
||||
serverInst = new RedisServerState({
|
||||
name: server,
|
||||
separator: this.getSeparator(name),
|
||||
separator: this.getSeparator(server),
|
||||
})
|
||||
}
|
||||
return serverInst.getFilter()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { lang } from '@/langs/index.js'
|
||||
import { cloneDeep, findIndex, get, isEmpty, join, map, merge, pick, set, some, split } from 'lodash'
|
||||
import { cloneDeep, findIndex, get, isEmpty, join, map, pick, set, some, split } from 'lodash'
|
||||
import {
|
||||
CheckForUpdate,
|
||||
GetBuildInDecoder,
|
||||
|
@ -15,6 +15,7 @@ import { enUS, NButton, NSpace, useOsTheme, zhCN } from 'naive-ui'
|
|||
import { h, nextTick } from 'vue'
|
||||
import { compareVersion } from '@/utils/version.js'
|
||||
import { typesIconStyle } from '@/consts/support_redis_type.js'
|
||||
import { TextAlignType } from '@/consts/text_align_type.js'
|
||||
|
||||
const osTheme = useOsTheme()
|
||||
const usePreferencesStore = defineStore('preferences', {
|
||||
|
@ -63,6 +64,7 @@ const usePreferencesStore = defineStore('preferences', {
|
|||
showFolding: true,
|
||||
dropText: true,
|
||||
links: true,
|
||||
entryTextAlign: TextAlignType.Center,
|
||||
},
|
||||
cli: {
|
||||
fontFamily: [],
|
||||
|
@ -271,6 +273,10 @@ const usePreferencesStore = defineStore('preferences', {
|
|||
keyIconType() {
|
||||
return get(this.general, 'keyIconStyle', typesIconStyle.SHORT)
|
||||
},
|
||||
|
||||
entryTextAlign() {
|
||||
return get(this.editor, 'entryTextAlign', TextAlignType.Center)
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
_applyPreferences(data) {
|
||||
|
@ -421,15 +427,15 @@ const usePreferencesStore = defineStore('preferences', {
|
|||
return false
|
||||
}
|
||||
|
||||
this.decoder[idx] = merge(this.decoder[idx], {
|
||||
name: newName || name,
|
||||
enable,
|
||||
auto,
|
||||
encodePath,
|
||||
encodeArgs,
|
||||
decodePath,
|
||||
decodeArgs,
|
||||
})
|
||||
let selDecoder = this.decoder[idx]
|
||||
selDecoder.name = newName || name
|
||||
selDecoder.enable = enable
|
||||
selDecoder.auto = auto
|
||||
selDecoder.encodePath = encodePath
|
||||
selDecoder.encodeArgs = encodeArgs
|
||||
selDecoder.decodePath = decodePath
|
||||
selDecoder.decodeArgs = decodeArgs
|
||||
this.decoder[idx] = selDecoder
|
||||
return true
|
||||
},
|
||||
|
||||
|
@ -461,15 +467,27 @@ const usePreferencesStore = defineStore('preferences', {
|
|||
try {
|
||||
const { success, data = {} } = await CheckForUpdate()
|
||||
if (success) {
|
||||
const { version = 'v1.0.0', latest, page_url: pageUrl } = data
|
||||
const {
|
||||
version = 'v1.0.0',
|
||||
latest,
|
||||
download_page: pageUrl = {},
|
||||
description = {},
|
||||
sponsor = [],
|
||||
} = data
|
||||
const downUrl = pageUrl[this.currentLanguage] || pageUrl['en']
|
||||
const descStr = description[this.currentLanguage] || description['en']
|
||||
// save sponsor ad
|
||||
if (!isEmpty(sponsor)) {
|
||||
localStorage.setItem('sponsor_ad', JSON.stringify(sponsor))
|
||||
}
|
||||
if (
|
||||
(manual || latest > this.general.skipVersion) &&
|
||||
(manual || compareVersion(latest, this.general.skipVersion) !== 0) &&
|
||||
compareVersion(latest, version) > 0 &&
|
||||
!isEmpty(pageUrl)
|
||||
!isEmpty(downUrl)
|
||||
) {
|
||||
const notiRef = $notification.show({
|
||||
title: i18nGlobal.t('dialogue.upgrade.title'),
|
||||
content: i18nGlobal.t('dialogue.upgrade.new_version_tip', { ver: latest }),
|
||||
title: `${i18nGlobal.t('dialogue.upgrade.title')} - ${latest}`,
|
||||
content: descStr || i18nGlobal.t('dialogue.upgrade.new_version_tip', { ver: latest }),
|
||||
action: () =>
|
||||
h('div', { class: 'flex-box-h flex-item-expand' }, [
|
||||
h(NSpace, { wrapItem: false }, () => [
|
||||
|
@ -502,13 +520,13 @@ const usePreferencesStore = defineStore('preferences', {
|
|||
type: 'primary',
|
||||
size: 'small',
|
||||
secondary: true,
|
||||
onClick: () => BrowserOpenURL(pageUrl),
|
||||
onClick: () => BrowserOpenURL(downUrl),
|
||||
},
|
||||
() => i18nGlobal.t('dialogue.upgrade.download_now'),
|
||||
),
|
||||
]),
|
||||
]),
|
||||
onPositiveClick: () => BrowserOpenURL(pageUrl),
|
||||
onPositiveClick: () => BrowserOpenURL(downUrl),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { assign, find, findIndex, get, indexOf, isEmpty, pullAt, remove, set, size } from 'lodash'
|
||||
import { assign, find, findIndex, get, includes, indexOf, isEmpty, pullAt, remove, set, size } from 'lodash'
|
||||
import { defineStore } from 'pinia'
|
||||
import { TabItem } from '@/objects/tabItem.js'
|
||||
import useBrowserStore from 'stores/browser.js'
|
||||
import { i18nGlobal } from '@/utils/i18n.js'
|
||||
import { BrowserTabType } from '@/consts/browser_tab_type.js'
|
||||
|
||||
const useTabStore = defineStore('tab', {
|
||||
/**
|
||||
|
@ -132,6 +135,17 @@ const useTabStore = defineStore('tab', {
|
|||
this.upsertTab({ server, clearValue: true })
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} tabName
|
||||
*/
|
||||
closeTab(tabName) {
|
||||
$dialog.warning(i18nGlobal.t('dialogue.close_confirm', { name: tabName }), () => {
|
||||
const browserStore = useBrowserStore()
|
||||
browserStore.closeConnection(tabName)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* update or insert a new tab if not exists with the same name
|
||||
* @param {string} subTab
|
||||
|
@ -147,6 +161,7 @@ const useTabStore = defineStore('tab', {
|
|||
* @param {boolean} [clearValue]
|
||||
* @param {string} format
|
||||
* @param {string} decode
|
||||
* @param {boolean} forceSwitch
|
||||
* @param {*} [value]
|
||||
*/
|
||||
upsertTab({
|
||||
|
@ -163,9 +178,11 @@ const useTabStore = defineStore('tab', {
|
|||
clearValue,
|
||||
format = '',
|
||||
decode = '',
|
||||
forceSwitch = false,
|
||||
}) {
|
||||
let tabIndex = findIndex(this.tabList, { name: server })
|
||||
if (tabIndex === -1) {
|
||||
subTab = subTab || BrowserTabType.Status
|
||||
const tabItem = new TabItem({
|
||||
name: server,
|
||||
title: server,
|
||||
|
@ -185,10 +202,11 @@ const useTabStore = defineStore('tab', {
|
|||
})
|
||||
this.tabList.push(tabItem)
|
||||
tabIndex = this.tabList.length - 1
|
||||
this._setActivatedIndex(tabIndex, true, subTab)
|
||||
} else {
|
||||
const tab = this.tabList[tabIndex]
|
||||
tab.blank = false
|
||||
tab.subTab = subTab
|
||||
tab.subTab = subTab || tab.subTab
|
||||
// tab.title = db !== undefined ? `${server}/db${db}` : `${server}`
|
||||
tab.title = server
|
||||
tab.server = server
|
||||
|
@ -205,8 +223,10 @@ const useTabStore = defineStore('tab', {
|
|||
if (clearValue === true) {
|
||||
tab.value = undefined
|
||||
}
|
||||
if (forceSwitch === true) {
|
||||
this._setActivatedIndex(tabIndex, true, subTab)
|
||||
}
|
||||
}
|
||||
this._setActivatedIndex(tabIndex, true, subTab)
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -276,9 +296,25 @@ const useTabStore = defineStore('tab', {
|
|||
case 'list': // {v:string, dv:[string]}[]
|
||||
tab.value = tab.value || []
|
||||
if (prepend === true) {
|
||||
tab.value = [...entries, ...tab.value]
|
||||
const originList = tab.value
|
||||
const list = []
|
||||
let starIndex = 0
|
||||
for (const entry of entries) {
|
||||
entry.index = starIndex++
|
||||
list.push(entry)
|
||||
}
|
||||
for (const entry of originList) {
|
||||
entry.index = starIndex++
|
||||
list.push(entry)
|
||||
}
|
||||
tab.value = list
|
||||
} else {
|
||||
tab.value.push(...entries)
|
||||
const list = tab.value
|
||||
let starIndex = list.length
|
||||
for (const entry of entries) {
|
||||
entry.index = starIndex++
|
||||
list.push(entry)
|
||||
}
|
||||
}
|
||||
tab.length += size(entries)
|
||||
break
|
||||
|
@ -377,6 +413,7 @@ const useTabStore = defineStore('tab', {
|
|||
for (const entry of entries) {
|
||||
if (size(tab.value) > entry.index) {
|
||||
tab.value[entry.index] = {
|
||||
index: entry.index,
|
||||
v: entry.v,
|
||||
dv: entry.dv,
|
||||
}
|
||||
|
@ -658,12 +695,15 @@ const useTabStore = defineStore('tab', {
|
|||
/**
|
||||
* set expanded keys for server
|
||||
* @param {string} server
|
||||
* @param {string[]} keys
|
||||
* @param {string|string[]} keys
|
||||
*/
|
||||
setExpandedKeys(server, keys = []) {
|
||||
/** @type TabItem**/
|
||||
let tab = find(this.tabList, { name: server })
|
||||
if (tab != null) {
|
||||
if (typeof keys === 'string') {
|
||||
keys = [keys]
|
||||
}
|
||||
if (isEmpty(keys)) {
|
||||
tab.expandedKeys = []
|
||||
} else {
|
||||
|
@ -675,13 +715,20 @@ const useTabStore = defineStore('tab', {
|
|||
/**
|
||||
*
|
||||
* @param {string} server
|
||||
* @param {string} key
|
||||
* @param {string|string[]} keys
|
||||
*/
|
||||
addExpandedKey(server, key) {
|
||||
addExpandedKey(server, keys) {
|
||||
/** @type TabItem**/
|
||||
let tab = find(this.tabList, { name: server })
|
||||
if (tab != null) {
|
||||
tab.expandedKeys.push(key)
|
||||
if (typeof keys === 'string') {
|
||||
keys = [keys]
|
||||
}
|
||||
for (const k of keys) {
|
||||
if (!includes(tab.expandedKeys, k)) {
|
||||
tab.expandedKeys.push(k)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -770,9 +817,20 @@ const useTabStore = defineStore('tab', {
|
|||
},
|
||||
|
||||
/**
|
||||
* set activated key
|
||||
* get activated key
|
||||
* @param {string} server
|
||||
* @return {string|null}
|
||||
*/
|
||||
getActivatedKey(server) {
|
||||
let tab = find(this.tabList, { name: server })
|
||||
return get(tab, 'activatedKey')
|
||||
},
|
||||
|
||||
/**
|
||||
* set activated key and return current activatedKey
|
||||
* @param {string} server
|
||||
* @param {string} key
|
||||
* @return {boolean}
|
||||
*/
|
||||
setActivatedKey(server, key) {
|
||||
/** @type TabItem**/
|
||||
|
|
|
@ -63,6 +63,10 @@ body {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wordline {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
@extend .clickable;
|
||||
line-height: 100%;
|
||||
|
@ -170,7 +174,11 @@ body {
|
|||
}
|
||||
|
||||
.auto-rotate {
|
||||
animation: rotate 2s linear infinite;
|
||||
animation: rotate 2s steps(60) infinite;
|
||||
}
|
||||
|
||||
.pre-wrap {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
|
|
|
@ -13,7 +13,7 @@ export const joinCommand = (path, args = [], emptyContent = '-') => {
|
|||
if (!isEmpty(path)) {
|
||||
let containValuePlaceholder = false
|
||||
cmd = includes(path, ' ') ? `"${path}"` : path
|
||||
for (let part of args) {
|
||||
for (let part of args || []) {
|
||||
part = trim(part)
|
||||
if (isEmpty(part)) {
|
||||
continue
|
||||
|
|
|
@ -10,3 +10,7 @@ export async function loadEnvironment() {
|
|||
export function isMacOS() {
|
||||
return os === 'darwin'
|
||||
}
|
||||
|
||||
export function isWindows() {
|
||||
return os === 'windows'
|
||||
}
|
||||
|
|
|
@ -35,6 +35,9 @@ export const themeOverrides = {
|
|||
tabGapLargeCard: '2px',
|
||||
tabFontWeightActive: 450,
|
||||
},
|
||||
Tree: {
|
||||
nodeWrapperPadding: '0 3px',
|
||||
},
|
||||
Card: {
|
||||
colorEmbedded: '#FAFAFA',
|
||||
},
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import { get, isEmpty, map, size, split, trimStart } from 'lodash'
|
||||
|
||||
const toVerArr = (ver) => {
|
||||
/**
|
||||
* convert version string to number array
|
||||
* @param ver
|
||||
* @return {number[]}
|
||||
*/
|
||||
export const toVersionArray = (ver) => {
|
||||
const v = trimStart(ver, 'v')
|
||||
let vParts = split(v, '.')
|
||||
if (isEmpty(vParts)) {
|
||||
|
@ -20,8 +25,8 @@ const toVerArr = (ver) => {
|
|||
*/
|
||||
export const compareVersion = (v1, v2) => {
|
||||
if (v1 !== v2) {
|
||||
const v1Nums = toVerArr(v1)
|
||||
const v2Nums = toVerArr(v2)
|
||||
const v1Nums = toVersionArray(v1)
|
||||
const v2Nums = toVersionArray(v2)
|
||||
const length = Math.max(size(v1Nums), size(v2Nums))
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
|
|
|
@ -29,4 +29,11 @@ export default defineConfig({
|
|||
wailsjs: rootPath + 'wailsjs',
|
||||
},
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
api: 'modern-compiler',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
40
go.mod
40
go.mod
|
@ -1,53 +1,53 @@
|
|||
module tinyrdm
|
||||
|
||||
go 1.21
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/adrg/sysfont v0.1.2
|
||||
github.com/andybalholm/brotli v1.1.0
|
||||
github.com/andybalholm/brotli v1.1.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/klauspost/compress v1.17.8
|
||||
github.com/redis/go-redis/v9 v9.5.1
|
||||
github.com/klauspost/compress v1.18.0
|
||||
github.com/pierrec/lz4/v4 v4.1.22
|
||||
github.com/redis/go-redis/v9 v9.7.1
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1
|
||||
github.com/vrischmann/userdir v0.0.0-20151206171402-20f291cebd68
|
||||
github.com/wailsapp/wails/v2 v2.8.1
|
||||
golang.org/x/crypto v0.22.0
|
||||
golang.org/x/net v0.24.0
|
||||
github.com/wailsapp/wails/v2 v2.10.1
|
||||
golang.org/x/crypto v0.36.0
|
||||
golang.org/x/net v0.37.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/adrg/strutil v0.3.1 // indirect
|
||||
github.com/adrg/xdg v0.4.0 // indirect
|
||||
github.com/adrg/xdg v0.5.3 // indirect
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/labstack/echo/v4 v4.11.4 // indirect
|
||||
github.com/labstack/echo/v4 v4.13.3 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
||||
github.com/leaanthony/gosod v1.0.3 // indirect
|
||||
github.com/leaanthony/gosod v1.0.4 // indirect
|
||||
github.com/leaanthony/slicer v1.6.0 // indirect
|
||||
github.com/leaanthony/u v1.1.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/samber/lo v1.39.0 // indirect
|
||||
github.com/tkrajina/go-reflector v0.5.6 // indirect
|
||||
github.com/samber/lo v1.49.1 // indirect
|
||||
github.com/tkrajina/go-reflector v0.5.8 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/wailsapp/go-webview2 v1.0.10 // indirect
|
||||
github.com/wailsapp/go-webview2 v1.0.19 // indirect
|
||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||
golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 // indirect
|
||||
golang.org/x/sys v0.19.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
)
|
||||
|
||||
// replace github.com/wailsapp/wails/v2 v2.8.1 => ~/go/pkg/mod
|
||||
// install latest wails: go install github.com/wailsapp/wails/v2/cmd/wails@latest
|
||||
// replace github.com/wailsapp/wails/v2 v2.10.1 => ~/go/pkg/mod
|
||||
|
|
95
go.sum
95
go.sum
|
@ -4,19 +4,18 @@ github.com/adrg/strutil v0.3.1/go.mod h1:8h90y18QLrs11IBffcGX3NW/GFBXCMcNg4M7H6M
|
|||
github.com/adrg/sysfont v0.1.2 h1:MSU3KREM4RhsQ+7QgH7wPEPTgAgBIz0Hw6Nd4u7QgjE=
|
||||
github.com/adrg/sysfont v0.1.2/go.mod h1:6d3l7/BSjX9VaeXWJt9fcrftFaD/t7l11xgSywCPZGk=
|
||||
github.com/adrg/xdg v0.3.0/go.mod h1:7I2hH/IT30IsupOpKZ5ue7/qNi3CoKzD6tL3HwpaRMQ=
|
||||
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
|
||||
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
|
||||
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
@ -30,60 +29,61 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
|
||||
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=
|
||||
github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
|
||||
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
|
||||
github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
|
||||
github.com/leaanthony/gosod v1.0.3 h1:Fnt+/B6NjQOVuCWOKYRREZnjGyvg+mEhd1nkkA04aTQ=
|
||||
github.com/leaanthony/gosod v1.0.3/go.mod h1:BJ2J+oHsQIyIQpnLPjnqFGTMnOZXDbvWtRCSG7jGxs4=
|
||||
github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY=
|
||||
github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
|
||||
github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
|
||||
github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
|
||||
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
|
||||
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
||||
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
|
||||
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8=
|
||||
github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||
github.com/redis/go-redis/v9 v9.7.1 h1:4LhKRCIduqXqtvCUlaq9c8bdHOkICjDMrr1+Zb3osAc=
|
||||
github.com/redis/go-redis/v9 v9.7.1/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
|
||||
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
|
||||
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQSepKdE=
|
||||
github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
|
||||
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
|
@ -94,34 +94,41 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh
|
|||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
github.com/vrischmann/userdir v0.0.0-20151206171402-20f291cebd68 h1:Ah2/69Z24rwD6OByyOdpJDmttftz0FTF8Q4QZ/SF1E4=
|
||||
github.com/vrischmann/userdir v0.0.0-20151206171402-20f291cebd68/go.mod h1:EqKqAeKddSL9XSGnfXd/7iLncccKhR16HBKVva7ENw8=
|
||||
github.com/wailsapp/go-webview2 v1.0.10 h1:PP5Hug6pnQEAhfRzLCoOh2jJaPdrqeRgJKZhyYyDV/w=
|
||||
github.com/wailsapp/go-webview2 v1.0.10/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo=
|
||||
github.com/wailsapp/go-webview2 v1.0.19 h1:7U3QcDj1PrBPaxJNCui2k1SkWml+Q5kvFUFyTImA6NU=
|
||||
github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||
github.com/wailsapp/wails/v2 v2.8.1 h1:KAudNjlFaiXnDfFEfSNoLoibJ1ovoutSrJ8poerTPW0=
|
||||
github.com/wailsapp/wails/v2 v2.8.1/go.mod h1:EFUGWkUX3KofO4fmKR/GmsLy3HhPH7NbyOEaMt8lBF0=
|
||||
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 h1:/RIbNt/Zr7rVhIkQhooTxCxFcdWLGIKnZA4IXNFSrvo=
|
||||
golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
||||
github.com/wailsapp/wails/v2 v2.10.1 h1:QWHvWMXII2nI/nXz77gpPG8P3ehl6zKe+u4su5BWIns=
|
||||
github.com/wailsapp/wails/v2 v2.10.1/go.mod h1:zrebnFV6MQf9kx8HI4iAv63vsR5v67oS7GTEZ7Pz1TY=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
|
||||
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
|
|
23
main.go
23
main.go
|
@ -3,6 +3,7 @@ package main
|
|||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/menu"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
|
@ -25,6 +26,8 @@ var icon []byte
|
|||
var version = "0.0.0"
|
||||
var gaMeasurementID, gaSecretKey string
|
||||
|
||||
const appName = "Tiny RDM"
|
||||
|
||||
func main() {
|
||||
// Create an instance of the app structure
|
||||
sysSvc := services.System()
|
||||
|
@ -35,6 +38,7 @@ func main() {
|
|||
pubsubSvc := services.Pubsub()
|
||||
prefSvc := services.Preferences()
|
||||
prefSvc.SetAppVersion(version)
|
||||
prefSvc.UpdateEnv()
|
||||
windowWidth, windowHeight, maximised := prefSvc.GetWindowSize()
|
||||
windowStartState := options.Normal
|
||||
if maximised {
|
||||
|
@ -42,8 +46,9 @@ func main() {
|
|||
}
|
||||
|
||||
// menu
|
||||
isMacOS := runtime.GOOS == "darwin"
|
||||
appMenu := menu.NewMenu()
|
||||
if runtime.GOOS == "darwin" {
|
||||
if isMacOS {
|
||||
appMenu.Append(menu.AppMenu())
|
||||
appMenu.Append(menu.EditMenu())
|
||||
appMenu.Append(menu.WindowMenu())
|
||||
|
@ -51,19 +56,19 @@ func main() {
|
|||
|
||||
// Create application with options
|
||||
err := wails.Run(&options.App{
|
||||
Title: "Tiny RDM",
|
||||
Title: appName,
|
||||
Width: windowWidth,
|
||||
Height: windowHeight,
|
||||
MinWidth: consts.MIN_WINDOW_WIDTH,
|
||||
MinHeight: consts.MIN_WINDOW_HEIGHT,
|
||||
WindowStartState: windowStartState,
|
||||
Frameless: runtime.GOOS != "darwin",
|
||||
Frameless: !isMacOS,
|
||||
Menu: appMenu,
|
||||
EnableDefaultContextMenu: true,
|
||||
AssetServer: &assetserver.Options{
|
||||
Assets: assets,
|
||||
},
|
||||
BackgroundColour: options.NewRGBA(27, 38, 54, 0),
|
||||
BackgroundColour: options.NewRGBA(255, 255, 255, 0),
|
||||
StartHidden: true,
|
||||
OnStartup: func(ctx context.Context) {
|
||||
sysSvc.Start(ctx, version)
|
||||
|
@ -104,7 +109,7 @@ func main() {
|
|||
Mac: &mac.Options{
|
||||
TitleBar: mac.TitleBarHiddenInset(),
|
||||
About: &mac.AboutInfo{
|
||||
Title: "Tiny RDM " + version,
|
||||
Title: fmt.Sprintf("%s %s", appName, version),
|
||||
Message: "A modern lightweight cross-platform Redis desktop client.\n\nCopyright © 2024",
|
||||
Icon: icon,
|
||||
},
|
||||
|
@ -112,12 +117,12 @@ func main() {
|
|||
WindowIsTranslucent: false,
|
||||
},
|
||||
Windows: &windows.Options{
|
||||
WebviewIsTransparent: true,
|
||||
WindowIsTranslucent: true,
|
||||
DisableFramelessWindowDecorations: true,
|
||||
WebviewIsTransparent: false,
|
||||
WindowIsTranslucent: false,
|
||||
DisableFramelessWindowDecorations: false,
|
||||
},
|
||||
Linux: &linux.Options{
|
||||
ProgramName: "Tiny RDM",
|
||||
ProgramName: appName,
|
||||
Icon: icon,
|
||||
WebviewGpuPolicy: linux.WebviewGpuPolicyOnDemand,
|
||||
WindowIsTranslucent: true,
|
||||
|
|
Loading…
Reference in New Issue