Feature/load all keys (#770)

Load all keys support!
due to conflict, can only merge via "squash and merge"
Co-authored-by: Ricardo Tondello <rkdtondello@gmail.com>
This commit is contained in:
qii404 2022-01-24 21:34:03 +08:00 committed by GitHub
parent 0778323150
commit 261a7de2cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 261 additions and 127 deletions

View File

@ -58,6 +58,8 @@
## Feature Log
- 2022-01-24: Command Dump Support
- 2022-01-05: Support To Load All Keys
- 2022-01-01: Brotli\Gzip\Deflate Support && RedisJSON Support
- 2021-11-26: JSON Editable && Subscribe Support
- 2021-08-30: Execution log Support && Add Hot Keys

View File

@ -1,6 +1,6 @@
{
"name": "another-redis-desktop-manager",
"version": "1.5.1",
"version": "1.5.2",
"description": "A faster, better and more stable redis desktop manager.",
"author": "Another",
"private": true,

16
package-lock.json generated
View File

@ -322,6 +322,14 @@
"resolved": "https://registry.npmjs.org/@qii404/redis-splitargs/-/redis-splitargs-1.0.1.tgz",
"integrity": "sha512-7LvTGKoW0cvTNUQwzb+byHr4PBvlqizMumAiuR0GzASGV+BO8LQuMd6EW8ukmB6R/bQJq6zINyXtq1Oa3/mKBA=="
},
"@qii404/ztree_v3": {
"version": "3.5.48",
"resolved": "https://registry.npmjs.org/@qii404/ztree_v3/-/ztree_v3-3.5.48.tgz",
"integrity": "sha512-eKAQG88dkOwae3nvWI4m05P9a7eDZ0oyHCwz20UO2zXNsFfDiasvfYwCceytSY37U8MIL9iCuRuVN6UeC2duUQ==",
"requires": {
"jquery": ">=1.4.4"
}
},
"@sindresorhus/is": {
"version": "0.14.0",
"resolved": "https://registry.npm.taobao.org/@sindresorhus/is/download/@sindresorhus/is-0.14.0.tgz",
@ -726,14 +734,6 @@
"integrity": "sha1-0pHGpOl5ibXGHZrPOWrk/hM6cY0=",
"dev": true
},
"@ztree/ztree_v3": {
"version": "3.5.46",
"resolved": "https://registry.npm.taobao.org/@ztree/ztree_v3/download/@ztree/ztree_v3-3.5.46.tgz",
"integrity": "sha1-rt8tzz+ZlVD/evZ4b32sm4CgCQA=",
"requires": {
"jquery": ">=1.4.4"
}
},
"abbrev": {
"version": "1.1.1",
"resolved": "http://registry.npm.taobao.org/abbrev/download/abbrev-1.1.1.tgz",

View File

@ -29,7 +29,7 @@
"@msgpack/msgpack": "^2.3.0",
"@qii404/json-bigint": "^1.0.0",
"@qii404/redis-splitargs": "^1.0.1",
"@ztree/ztree_v3": "^3.5.46",
"@qii404/ztree_v3": "^3.5.48",
"element-ui": "^2.4.11",
"font-awesome": "^4.7.0",
"ioredis": "^4.27.0",

View File

@ -195,7 +195,7 @@ button, input, textarea, .vjs__tree {
}
.right-main-container .main-tabs-container {
overflow-y: hidden;
padding-top: 2px;
padding-top: 0px;
padding-right: 4px;
}

View File

@ -79,7 +79,7 @@ export default {
nodes.map(node => {
let scanOption = {
match: pattern + '*',
count: 7000,
count: 50000,
}
let stream = node.scanBufferStream(scanOption);

View File

@ -28,7 +28,6 @@
:content='content'
:name="selectedView"
:contentVisible='contentVisible'
:textrows='textrows'
:disabled='disabled'
:redisKey="redisKey"
:dataMap="dataMap">
@ -77,7 +76,6 @@ export default {
props: {
float: {default: 'right'},
content: {default: () => Buffer.from('')},
textrows: {default: 6},
disabled: {type: Boolean, default: false},
redisKey: {default: () => Buffer.from('')},
dataMap: {type: Object, default: () => {}},
@ -222,17 +220,21 @@ export default {
height: 22px !important;
}
/*outline same with text viewer's .el-textarea__inner*/
.text-formated-container {
border: 1px solid #dcdfe6;
min-height: 114px;
padding: 5px 15px;
line-height: 1.5;
border-radius: 5px;
padding: 5px 10px;
border-radius: 4px;
}
.dark-mode .text-formated-container {
border-color: #7f8ea5;
}
.format-viewer-container textarea {
min-height: 194px !important;
height: calc(100vh - 686px);
}
.collapse-container {
height: 27px;
}

View File

@ -7,7 +7,7 @@
</div>
<!-- monaco editor div -->
<div id="monaco-editor-con" ref="editor"></div>
<div class="monaco-editor-con" ref="editor"></div>
</div>
</template>
@ -148,9 +148,9 @@ export default {
</script>
<style type="text/css">
.text-formated-container #monaco-editor-con {
.text-formated-container .monaco-editor-con {
min-height: 150px;
max-height: 100vh;
height: calc(100vh - 730px);
clear: both;
overflow: hidden;
background: none;
@ -175,14 +175,14 @@ export default {
background: #565656;
}
.text-formated-container #monaco-editor-con .monaco-editor .slider {
.text-formated-container .monaco-editor-con .monaco-editor .slider {
border-radius: 4px;
background: #c1c1c1;
}
.dark-mode .text-formated-container #monaco-editor-con .monaco-editor .slider {
.dark-mode .text-formated-container .monaco-editor-con .monaco-editor .slider {
background: #5d676d;
}
.text-formated-container #monaco-editor-con .monaco-editor .slider:hover {
.text-formated-container .monaco-editor-con .monaco-editor .slider:hover {
background: #7d7d7d;
}
@ -190,39 +190,39 @@ export default {
.text-formated-container .monaco-editor .margin {
background-color: inherit;
}
#monaco-editor-con .monaco-editor, #monaco-editor-con .monaco-editor-background, #monaco-editor-con .monaco-editor .inputarea.ime-input {
.monaco-editor-con .monaco-editor, .monaco-editor-con .monaco-editor-background, .monaco-editor-con .monaco-editor .inputarea.ime-input {
background-color: inherit;
}
/*json key color*/
#monaco-editor-con .mtk4 {
.monaco-editor-con .mtk4 {
color: #111111;
}
.dark-mode #monaco-editor-con .mtk4 {
.dark-mode .monaco-editor-con .mtk4 {
color: #ebebec;
}
/*json val string color*/
#monaco-editor-con .mtk5 {
.monaco-editor-con .mtk5 {
color: #42b983;
}
/*json val number color*/
#monaco-editor-con .mtk6 {
.monaco-editor-con .mtk6 {
color: #fc1e70;
}
/*json bracket color*/
#monaco-editor-con .mtk9 {
.monaco-editor-con .mtk9 {
color: #111111;
}
/*json bracket color*/
.dark-mode #monaco-editor-con .mtk9 {
.dark-mode .monaco-editor-con .mtk9 {
color: #b6b6b9;
}
/* common string in json editor*/
#monaco-editor-con .mtk1 {
.monaco-editor-con .mtk1 {
color: #606266;
}
.dark-mode #monaco-editor-con .mtk1 {
.dark-mode .monaco-editor-con .mtk1 {
color: #f3f3f4;
}
</style>

View File

@ -31,6 +31,7 @@
<el-table
stripe
border
size='mini'
min-height=300
:data="hashData">
<el-table-column

View File

@ -27,6 +27,7 @@
<el-table
stripe
border
size='mini'
min-height=300
:data="listData">
<el-table-column

View File

@ -7,8 +7,7 @@
:content='content'
:binary='binary'
:redisKey='redisKey'
float=''
:textrows=12>
float=''>
</FormatViewer>
</el-form-item>
@ -116,12 +115,7 @@ export default {
height: calc(100vh - 286px);
}
/*json in monaco editor*/
.key-content-string #monaco-editor-con {
height: calc(100vh - 331px);
}
/*not text viewer box, such as json*/
.key-content-string .text-formated-container {
box-sizing: border-box;
min-height: calc(100vh - 286px);
.key-content-string .text-formated-container .monaco-editor-con {
height: calc(100vh - 330px);
}
</style>

View File

@ -27,6 +27,7 @@
<el-table
stripe
border
size='mini'
min-height=300
:data="setData">
<el-table-column

View File

@ -8,11 +8,11 @@
</el-form-item>
<!-- max value -->
<el-form-item label="Max">
<el-input v-model="maxId" @keyup.enter.native='initShow' type="primary" placeholder='Max ID, default +' :title='$t("message.enter_to_search")'>Max</el-input>
<el-input v-model="maxId" @keyup.enter.native='initShow' type="primary" placeholder='Max ID, default +' :title='$t("message.enter_to_search")' size='mini'>Max</el-input>
</el-form-item>
<!-- min value -->
<el-form-item label="Min">
<el-input v-model="minId" @keyup.enter.native='initShow' type="primary" placeholder='Min ID, default -' :title='$t("message.enter_to_search")'>Min</el-input>
<el-input v-model="minId" @keyup.enter.native='initShow' type="primary" placeholder='Min ID, default -' :title='$t("message.enter_to_search")' size='mini'>Min</el-input>
</el-form-item>
</el-form>
@ -39,6 +39,7 @@
<el-table
stripe
border
size='mini'
min-height=300
:data="lineData">
<el-table-column

View File

@ -7,8 +7,7 @@
:content='content'
:binary='binary'
:redisKey='redisKey'
float=''
:textrows=12>
float=''>
</FormatViewer>
</el-form-item>
@ -117,12 +116,7 @@ export default {
height: calc(100vh - 286px);
}
/*json in monaco editor*/
.key-content-string #monaco-editor-con {
height: calc(100vh - 331px);
}
/*not text viewer box, such as json*/
.key-content-string .text-formated-container {
box-sizing: border-box;
min-height: calc(100vh - 286px);
.key-content-string .text-formated-container .monaco-editor-con {
height: calc(100vh - 330px);
}
</style>

View File

@ -30,6 +30,7 @@
<el-table
stripe
border
size='mini'
min-height=300
:data="zsetData">
<el-table-column

View File

@ -7,15 +7,33 @@
:client="client"
:keyList="keyList">
</component>
<div class='keys-load-more-wrapper'>
<!-- load more -->
<el-button
ref='scanMoreBtn'
class='load-more-keys'
:disabled='scanMoreDisabled || searching'
@click='refreshKeyList(false)'>
{{ $t('message.load_more_keys') }}
</el-button>
<!-- load more -->
<el-button
ref='scanMoreBtn'
class='load-more-keys'
:disabled='scanMoreDisabled'
@click='refreshKeyList(false)'>
{{ $t('message.load_more_keys') }}
</el-button>
<!-- load all -->
<!-- fix el-tooltip 200ms delay when closing -->
<el-tooltip v-if='showLoadAllKeys' :disabled="!loadAllTooltip"
@mouseenter.native="loadAllTooltip=true" @mouseleave.native="loadAllTooltip=false"
effect="dark" :content="$t('message.load_all_keys_tip')"
placement="bottom" :open-delay=380 :enterable='false'>
<el-button
class='load-more-keys'
type= 'danger'
:icon="searching ? 'el-icon-loading' : ''"
:disabled='searching'
@click='loadAllKeys()'>
{{ $t('message.load_all_keys') }}
</el-button>
</el-tooltip>
</div>
</div>
</template>
@ -27,8 +45,6 @@ export default {
data() {
return {
keyList: [],
// keyListType: this.config.separator === '' ? 'KeyListNormal' : 'KeyListTree',
// keysPageSize: this.keyListType === 'KeyListNormal' ? 200 : 1000,
keyListType: 'KeyListTree',
searchPageSize: 10000,
scanStreams: [],
@ -37,6 +53,7 @@ export default {
onePageList: [],
onePageFinishedCount: 0,
firstPageFinished: false,
loadAllTooltip: true,
};
},
props: ['client', 'config', 'globalSettings'],
@ -46,6 +63,12 @@ export default {
let keysPageSize = parseInt(this.globalSettings['keysPageSize']);
return keysPageSize ? keysPageSize : 500;
},
showLoadAllKeys(){
return this.globalSettings['showLoadAllKeys'];
},
searching() {
return this.$parent.$parent.$parent.$refs.operateItem.searchIcon == 'el-icon-loading';
},
},
created() {
// add or remove key from key list directly
@ -99,15 +122,20 @@ export default {
}
}
},
initScanStreamsAndScan() {
// this.client.nodes: cluster
loadAllKeys(){
this.resetKeyList();
this.$parent.$parent.$parent.$refs.operateItem.searchIcon = 'el-icon-loading';
this.initScanStreamsAndScan(true);
},
initScanStreamsAndScan(loadAll = false) {
let nodes = this.client.nodes ? this.client.nodes('master') : [this.client];
let keysPageSize = loadAll ? 50000 : this.keysPageSize;
this.scanningCount = nodes.length;
nodes.map(node => {
let scanOption = {
match: this.getMatchMode(),
count: this.keysPageSize,
count: keysPageSize,
}
// scan count is bigger when in search mode
@ -120,7 +148,7 @@ export default {
this.onePageList = this.onePageList.concat(keys);
// scan once reaches page size
if (this.onePageList.length >= this.keysPageSize) {
if (this.onePageList.length >= keysPageSize && loadAll === false) {
// temp stop
stream.pause();
// search input icon recover
@ -129,13 +157,14 @@ export default {
// last node refresh keylist
if (++this.onePageFinishedCount >= this.scanningCount) {
// clear key list only after data scaned, to prevent list jitter
// empty keyList only when first click, if click 'load more' again, do not empty it
if (!this.firstPageFinished) {
this.firstPageFinished = true;
this.keyList = [];
}
// this page key list append to raw key list
this.keyList = this.keyList.concat(this.onePageList.sort());
this.keyList = this.keyList.concat(this.onePageList);
}
}
});
@ -171,14 +200,14 @@ export default {
// all nodes scan finished(cusor back to 0)
if (--this.scanningCount <= 0) {
// clear key list only after data scaned, to prevent list jitter
// empty keyList only when first click, if click 'load more' again, do not empty it
if (!this.firstPageFinished) {
this.firstPageFinished = true;
this.keyList = [];
}
// this page key list append to raw key list
this.keyList = this.keyList.concat(this.onePageList.sort());
this.keyList = this.keyList.concat(this.onePageList);
this.scanMoreDisabled = true;
// search input icon recover
this.$parent.$parent.$parent.$refs.operateItem.searchIcon = 'el-icon-search';
@ -187,6 +216,9 @@ export default {
});
},
resetKeyList(clearKeys = false) {
// cancel scanning
this.cancelScanning();
clearKeys && (this.keyList = []);
this.firstPageFinished = false;
this.scanStreams = [];
@ -205,6 +237,13 @@ export default {
this.scanMoreDisabled = true;
},
cancelScanning() {
if (this.scanStreams.length) {
for (let stream of this.scanStreams) {
stream.pause && stream.pause();
}
}
},
getMatchMode(fillStar = true) {
let match = this.$parent.$parent.$parent.$refs.operateItem.searchMatch;
@ -262,12 +301,16 @@ export default {
</script>
<style type="text/css">
.load-more-keys {
margin: 10px auto;
.keys-load-more-wrapper {
display: flex;
}
.keys-load-more-wrapper .load-more-keys {
margin: 10px 5px;
padding: 0;
display: block;
height: 20px;
height: 22px;
width: 100%;
font-size: 75%;
line-height: 1px;
}
</style>

View File

@ -38,7 +38,8 @@
import $ from "jquery";
if (!window.jQuery) window.jQuery = $;
import ("@ztree/ztree_v3/js/jquery.ztree.all.min.js");
// import ("@ztree/ztree_v3/js/jquery.ztree.all.js");
import ("@qii404/ztree_v3/js/jquery.ztree.all.js");
export default {
data() {
@ -47,13 +48,14 @@ export default {
openStatus: {},
rightClickNode: {},
multiOperating: false,
treeNodesOverflow: 20000,
setting: {
view: {
showIcon: true,
showLine: false,
selectedMulti: false,
dblClickExpand: false,
addDiyDom: this.addDiyDom,
// addDiyDom: this.addDiyDom,
expandSpeed: 'fast',
},
check: {
@ -69,6 +71,9 @@ export default {
// folder clicked
if (treeNode.children) {
// after folder expand, sorting keys
!treeNode.open && this.folderExpand(treeNode);
// toggle tree view
this.ztreeObj && this.ztreeObj.expandNode(treeNode, undefined, false, false);
@ -78,8 +83,12 @@ export default {
this.clickKey(Buffer.from(treeNode.nameBuffer.data), event);
},
onExpand: (event, treeId, treeNode) => {
return this.openStatus[treeNode.fullName] = treeNode.open;
beforeExpand: (treeId, treeNode) => {
// after folder expand, sorting keys
this.folderExpand(treeNode);
this.openStatus[treeNode.fullName] = !treeNode.open;
return true;
},
onCollapse: (event, treeId, treeNode) => {
return this.openStatus[treeNode.fullName] = treeNode.open;
@ -187,6 +196,25 @@ export default {
toggleCheckAll(checked) {
this.ztreeObj.checkAllNodes(checked);
},
folderExpand(treeNode) {
if (!treeNode.asyncOperated) {
// force cut nodes
if (treeNode.children.length > this.treeNodesOverflow) {
treeNode.children.splice(this.treeNodesOverflow);
this.$nextTick(() => {
this.$message.warning({
message: this.$t('message.tree_node_overflow', {num: this.treeNodesOverflow}),
duration: 4000
});
});
}
// sort nodes async, only after opened
this.$util.sortKeysAndFolder(treeNode.children);
treeNode.asyncOperated = true;
}
},
deleteBatch() {
let rule = {key: [], pattern: []};
let folderNodes = {};
@ -277,8 +305,6 @@ export default {
},
treeRefresh(nodes) {
this.ztreeObj && this.ztreeObj.destroy();
// folder keep in front
this.$util.sortNodes(nodes);
this.ztreeObj = $.fn.zTree.init(
$(`#${this.treeId}`),
@ -311,11 +337,26 @@ export default {
let checkedNodes = [];
this.ztreeObj && (checkedNodes = this.ztreeObj.getCheckedNodes(true));
this.treeRefresh(
this.separator ?
this.$util.keysToTree(newList, this.separator, this.openStatus) :
this.$util.keysToList(newList)
);
const keyNodes = this.separator ?
this.$util.keysToTree(newList, this.separator, this.openStatus, this.treeNodesOverflow) :
this.$util.keysToList(newList);
// nodes displayed in outermost layer
if (keyNodes.length > this.treeNodesOverflow) {
// force cut
keyNodes.splice(this.treeNodesOverflow);
// using nextTick to relieve msg missing caused by app stuck
this.$nextTick(() => {
this.$message.warning({
message: this.$t('message.tree_node_overflow', {num: this.treeNodesOverflow}),
duration: 6000,
});
});
}
// sort outermost layer nodes
this.$util.sortKeysAndFolder(keyNodes);
this.treeRefresh(keyNodes);
// remove animination when too many nodes
if (newList.length > 9000) {
@ -324,7 +365,6 @@ export default {
// recheck checked nodes
this.reCheckNodes(checkedNodes);
// only 1 key such as extract search, expand all
if (newList.length <= 1) {
this.ztreeObj && this.ztreeObj.expandAll(true);
@ -335,7 +375,7 @@ export default {
</script>
<style>
@import '@ztree/ztree_v3/css/zTreeStyle/zTreeStyle.css';
@import '@qii404/ztree_v3/css/zTreeStyle/zTreeStyle.css';
/*tree style*/
.ztree {

View File

@ -70,7 +70,7 @@
</el-form-item>
<!-- new key dialog -->
<el-dialog :title="$t('message.add_new_key')" :visible.sync="newKeyDialog">
<el-dialog :title="$t('message.add_new_key')" :visible.sync="newKeyDialog" :close-on-click-modal='false'>
<el-form label-position="top" size="mini">
<el-form-item :label="$t('message.key_name')">
<el-input v-model='newKeyName'></el-input>

View File

@ -45,26 +45,20 @@
timer = requestAnimationFrame(function fn() {
const nowTop = that.realDom.scrollTop;
if (nowTop > 5000) {
that.realDom.scrollTop -= 2500;
timer = requestAnimationFrame(fn);
// to top already
if (nowTop <= 0) {
cancelAnimationFrame(timer);
that.toTopShow = false;
}
else if (nowTop > 1000 && nowTop <= 5000) {
that.realDom.scrollTop -= 500;
timer = requestAnimationFrame(fn);
}
else if (nowTop > 200 && nowTop <= 1000) {
that.realDom.scrollTop -= 100;
timer = requestAnimationFrame(fn);
}
else if (nowTop > 0 && nowTop <= 200) {
that.realDom.scrollTop -= 15;
else if (nowTop < 50) {
that.realDom.scrollTop -= 5;
timer = requestAnimationFrame(fn);
}
else {
cancelAnimationFrame(timer);
that.toTopShow = false;
that.realDom.scrollTop -= nowTop * 0.2;
timer = requestAnimationFrame(fn);
}
});
}

View File

@ -36,7 +36,10 @@
:max=10000
:step=50
v-model='form.keysPageSize'>
</el-input-number>
</el-input-number>&nbsp;
<!-- load all switch -->
<el-switch v-model='form.showLoadAllKeys'></el-switch> {{ $t('message.show_load_all_keys') }}
<span slot="label">
{{ $t('message.keys_per_loading') }}
<el-popover
@ -133,7 +136,7 @@ export default {
data() {
return {
visible: false,
form: {fontFamily: '', zoomFactor: 1.0, keysPageSize: 500},
form: {fontFamily: '', zoomFactor: 1.0, keysPageSize: 500, showLoadAllKeys: false},
importConnectionVisible: false,
connectionFileContent: '',
appVersion: (new URL(window.location.href)).searchParams.get('version'),

View File

@ -1,13 +1,13 @@
<template>
<div>
<!-- </textarea> -->
<el-input ref='textInput' :disabled='disabled' type='textarea' :rows='textrows' :value='contentDisplay'></el-input>
<el-input ref='textInput' :disabled='disabled' type='textarea' :value='contentDisplay'></el-input>
</div>
</template>
<script type="text/javascript">
export default {
props: ['content', 'contentVisible', 'textrows', 'disabled'],
props: ['content', 'contentVisible', 'disabled'],
computed: {
contentDisplay() {
return this.$util.bufToBinary(this.content);

View File

@ -1,13 +1,13 @@
<template>
<div>
<!-- </textarea> -->
<el-input ref='textInput' :disabled='disabled' type='textarea' :rows='textrows' :value='contentDisplay'></el-input>
<el-input ref='textInput' :disabled='disabled' type='textarea' :value='contentDisplay'></el-input>
</div>
</template>
<script type="text/javascript">
export default {
props: ['content', 'contentVisible', 'textrows', 'disabled'],
props: ['content', 'contentVisible', 'disabled'],
computed: {
contentDisplay() {
return this.$util.bufToString(this.content);

View File

@ -5,7 +5,7 @@
:title='alertTitle'
type="error">
</el-alert>
<el-input :disabled='true' type='textarea' :rows='textrows' :value='contentDisplay'></el-input>
<el-input :disabled='true' type='textarea' :value='contentDisplay'></el-input>
</div>
</template>
@ -16,7 +16,7 @@ export default {
firstChars: 20000,
};
},
props: ['content', 'contentVisible', 'textrows', 'disabled'],
props: ['content', 'contentVisible', 'disabled'],
computed: {
contentDisplay() {
return this.$util.bufToString(this.content.slice(0, this.firstChars), false)

View File

@ -1,7 +1,7 @@
<template>
<div>
<!-- </textarea> -->
<el-input ref='textInput' :disabled='disabled' type='textarea' :rows='textrows' :value='contentDisplay' @input='inputContent'>
<el-input ref='textInput' :disabled='disabled' type='textarea' :value='contentDisplay' @input='inputContent'>
</el-input>
</div>
</template>
@ -13,7 +13,7 @@ export default {
confirmChange: false,
};
},
props: ['content', 'contentVisible', 'textrows', 'disabled'],
props: ['content', 'contentVisible', 'disabled'],
computed: {
contentDisplay() {
return this.content.toString();

View File

@ -146,6 +146,10 @@ const cn = {
hide_window: '隐藏窗口',
minimize_window: '最小化窗口',
maximize_window: '最大化窗口',
load_all_keys: '加载所有',
show_load_all_keys: '启用按钮以加载所有键',
load_all_keys_tip: '一次性加载所有key当key的数量过多时有可能会导致客户端卡顿请酌情使用',
tree_node_overflow: 'key或者文件夹数量过多仅保留{num}个进行展示。如未找到所需key建议使用模糊搜索或者设置分隔符来将key分散到文件夹中',
},
};

View File

@ -146,6 +146,10 @@ const de = {
hide_window: 'Fenster ausblenden',
minimize_window: 'Fenster minimieren',
maximize_window: 'Fenster maximieren',
load_all_keys: 'alle laden',
show_load_all_keys: 'Schaltfläche aktivieren, um alle Schlüssel zu laden',
load_all_keys_tip: 'Alle Schlüssel auf einmal laden. Wenn die Anzahl der Schlüssel zu groß ist, kann der Client stecken bleiben. Bitte verwenden Sie es richtig',
tree_node_overflow: 'Zu viele Schlüssel oder Ordner, behalten Sie nur {num} für die Anzeige. Wenn Ihr Schlüssel nicht hier ist, wird eine unscharfe Suche empfohlen, oder den Trenner setzen, um die Schlüssel in Ordner zu verteilen',
},
};

View File

@ -146,6 +146,10 @@ const en = {
hide_window: 'Hide Window',
minimize_window: 'Minimize window',
maximize_window: 'Maximize window',
load_all_keys: 'load all',
show_load_all_keys: 'Enable button to load all keys',
load_all_keys_tip: 'Load all keys at one time. If the number of keys is too large, the client may get stuck. Please use it correctly',
tree_node_overflow: 'Too many keys or folders , keep only {num} for display. If your key is not here, fuzzy search is recommended, or set the separator to spread the keys into folders',
},
};

View File

@ -146,6 +146,10 @@ const fr = {
hide_window: 'Masquer la fenêtre',
minimize_window: 'Réduire la fenêtre',
maximize_window: 'Agrandir la fenêtre',
load_all_keys: 'charger tout',
show_load_all_keys: 'Activer le bouton pour charger toutes les clés',
load_all_keys_tip: 'Chargez toutes les clés en même temps. Si le nombre de clés est trop important, le client peut rester bloqué. Veuillez l\'utiliser correctement',
tree_node_overflow: 'Il y a trop de touches ou de dossiers, ne laissant que {num} à afficher. Si votre clé n\'est pas ici, une recherche floue est recommandée, ou définissez un séparateur pour disperser la clé dans le dossier',
},
};

View File

@ -94,7 +94,7 @@ const it = {
private_key_faq: 'La chiave privata in formato RSA è compatibile e inizia con <pre>-----BEGIN RSA PRIVATE KEY-----</pre>\
ma se inizi con<pre>-----BEGIN OPENSSH PRIVATE KEY-----</pre>è necessario convertire il formato tramite <pre>ssh-keygen -p -m pem -f ~/.ssh/id_rsa</pre>Questa operazione non influirà sul precedente accesso con chiave privata',
dark_mode: 'Modalità scura',
load_more_keys: 'Carica più chiavi',
load_more_keys: 'carica di più',
key_name: 'Nome della chiave',
project_home: 'Progetto Home',
cluster_faq: 'Seleziona qualsiasi nodo nel cluster da compilare e gli altri nodi verranno identificati automaticamente.',
@ -146,6 +146,10 @@ const it = {
hide_window: 'Nascondi finestra',
minimize_window: 'Riduci finestra',
maximize_window: 'Massimizza finestra',
load_all_keys: 'carica tutto',
show_load_all_keys: 'Abilita pulsante per caricare tutte le chiavi',
load_all_keys_tip: 'Carica tutte le chiavi contemporaneamente. Se il numero di chiavi è troppo grande, il client potrebbe rimanere bloccato. Si prega di usarlo correttamente',
tree_node_overflow: 'Troppi tasti o cartelle, tenere solo {num} per la visualizzazione. Se la tua chiave non è qui, si raccomanda la ricerca sfocata, o impostare il separatore per distribuire le chiavi in cartelle',
},
};

View File

@ -94,7 +94,7 @@ const pt = {
private_key_faq: 'A chave privada de formato RSA é compatível e começa com <pre>-----BEGIN RSA PRIVATE KEY-----</pre>\
mas se começar com<pre>-----BEGIN OPENSSH PRIVATE KEY-----</pre>você precisa converter o formato via <pre>ssh-keygen -p -m pem -f ~/.ssh/id_rsa</pre>Esta operação não afetará o login de chave privada anterior',
dark_mode: 'Modo noturno',
load_more_keys: 'Carregar mais chaves',
load_more_keys: 'carregar mais',
key_name: 'Nome da Chave',
project_home: 'Home do Projeto',
cluster_faq: 'Selecione qualquer nó no cluster para preencher e outros nós serão identificados automaticamente.',
@ -146,6 +146,10 @@ const pt = {
hide_window: 'Ocultar janela',
minimize_window: 'Minimize a janela',
maximize_window: 'Maximize a janela',
load_all_keys: 'carregar tudo',
show_load_all_keys: 'Habilite o botão para carregar todas as chaves',
load_all_keys_tip: 'Carregue todas as chaves de uma vez. Se o número de chaves for muito grande, o cliente pode ficar preso. Por favor, use-o corretamente',
tree_node_overflow: 'Muitas teclas ou pastas, manter apenas {num} para exibição. Se a sua chave não está aqui, é recomendada a pesquisa difusa, ou configurar o separador para espalhar as chaves em pastas',
},
};

View File

@ -146,6 +146,10 @@ const ru = {
hide_window: 'Скрыть окно',
minimize_window: 'Свернуть окно',
maximize_window: 'Развернуть окно',
load_all_keys: 'загрузить все',
show_load_all_keys: 'Кнопка включения для загрузки всех ключей',
load_all_keys_tip: 'Загрузите все ключи одновременно. Если количество ключей слишком велико, клиент может застрять. Пожалуйста, используйте его правильно',
tree_node_overflow: 'Слишком много клавиш или папок, оставьте только {num} для отображения. Если вашего ключа здесь нет, рекомендуется использовать нечеткий поиск, или Настройка разделителя для разделения ключей в папку',
},
};

View File

@ -146,6 +146,10 @@ const tr = {
hide_window: 'Pencereyi gizle',
minimize_window: 'Pencereleri küçültür',
maximize_window: 'Pencereyi büyüt',
load_all_keys: 'hepsini yükle',
show_load_all_keys: 'Tüm anahtarları yüklemek için etkinleştir düğmesi',
load_all_keys_tip: 'Tüm anahtarları bir kerede yükleyin. Anahtar sayısı çok fazlaysa, istemci takılabilir. Lütfen doğru kullanın',
tree_node_overflow: 'Çok fazla anahtar veya klasör var, yalnızca {num} görüntüleme için ayrıldı. Anahtarınız burada değilse bulanık arama önerilir, veya anahtarı klasöre yaymak için bir ayırıcı ayarlayın',
},
};

View File

@ -146,6 +146,10 @@ const tw = {
hide_window: '隱藏窗口',
minimize_window: '最小化窗口',
maximize_window: '最大化窗口',
load_all_keys: '載入所有',
show_load_all_keys: '啟用按鈕以加載所有鍵',
load_all_keys_tip: '一次載入所有密鑰。如果密鑰數量過多,客戶端可能會卡住,請正確使用',
tree_node_overflow: '鍵或資料夾太多,僅保留{num}個以顯示。 如果您的鍵不在此處,建議使用模糊搜索,或設置分隔符以將密鑰分散到文件夾中',
},
};

View File

@ -146,6 +146,10 @@ const ua = {
hide_window: 'Сховати вікно',
minimize_window: 'Згорнути вікно',
maximize_window: 'Розгорнути вікно',
load_all_keys: 'завантажити все',
show_load_all_keys: 'Кнопка «Увімкнути», щоб завантажити всі ключі',
load_all_keys_tip: 'Завантажте всі ключі одночасно. Якщо кількість ключів занадто велика, клієнт може застрягти. Будь ласка, використовуйте його правильно',
tree_node_overflow: 'Занадто багато ключів або папок, тільки {num} дисплея. Якщо ваш ключ не тут, рекомендується використовувати нечіткий пошук, або встановити роздільник для поширення ключів у теки',
},
};

View File

@ -172,7 +172,7 @@ export default {
};
});
},
keysToTree(keys, separator = ':', openStatus = {}) {
keysToTree(keys, separator = ':', openStatus = {}, forceCut = 20000) {
let tree = {};
keys.forEach(key => {
let currentNode = tree;
@ -197,9 +197,10 @@ export default {
});
});
return this.formatTreeData(tree, '', openStatus, separator)
// to tree format
return this.formatTreeData(tree, '', openStatus, separator, forceCut);
},
formatTreeData(tree, previousKey = '', openStatus = {}, separator = ':') {
formatTreeData(tree, previousKey = '', openStatus = {}, separator = ':', forceCut = 20000) {
return Object.keys(tree).map(key => {
let node = { name: key ? key : '[Empty]'};
@ -207,10 +208,13 @@ export default {
if (!tree[key].keyNode && Object.keys(tree[key]).length > 0) {
let tillNowKeyName = previousKey + key + separator;
node.open = !!openStatus[tillNowKeyName];
node.children = this.formatTreeData(tree[key], tillNowKeyName, openStatus, separator);
// keep folder node in top of the tree(not include the outest list)
this.sortNodes(node.children);
node.children = this.formatTreeData(tree[key], tillNowKeyName, openStatus, separator, forceCut);
node.keyCount = node.children.reduce((a, b) => a + (b.keyCount || 0), 0);
// too many children, force cut, do not incluence keyCount display
node.open && node.children.length > forceCut && node.children.splice(forceCut);
// keep folder node in front of the tree and sorted(not include the outest list)
// async sort, only for opened folders
node.open && this.sortKeysAndFolder(node.children);
node.fullName = tillNowKeyName;
}
// key node
@ -223,14 +227,27 @@ export default {
return node;
});
},
// nodes is reference
sortNodes(nodes) {
// nodes is reference, keep folder in front and sorted,
// keep keys in tail and sorted
sortKeysAndFolder(nodes) {
nodes.sort(function(a, b) {
if (a.children && b.children) {
return 0;
// a & b are all keys
if (!a.children && !b.children) {
return a.name > b.name ? 1 : -1;
}
// a & b are all folder
else if (a.children && b.children) {
return a.name > b.name ? 1 : -1;
}
// a is folder, b is key
else if (a.children) {
return -1;
}
// a is key, b is folder
else {
return 1;
}
return a.children ? -1 : (b.children ? 1 : 0);
});
},
copyToClipboard(text) {