Skip to content

Commit 21d9deb

Browse files
authored
Merge pull request #82 from jumpserver/pr@dev@feat_right_menu_copy
feat: Support right-click to copy SQL
2 parents 169411f + 2e281a4 commit 21d9deb

File tree

4 files changed

+237
-7
lines changed

4 files changed

+237
-7
lines changed

backend/framework/src/main/java/org/jumpserver/chen/framework/console/DataViewConsole.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ public void onConnect(Connect connect) {
104104
public void createDataView(String schemaName, String tableName) {
105105

106106
var viewTitle = this.getTitle() + "child";
107-
this.getPacketIO().sendPacket("new_data_view", Map.of("title", viewTitle));
107+
var data = Map.of("title", viewTitle, "schema", this.schema, "table", this.table);
108+
this.getPacketIO().sendPacket("new_data_view", data);
108109
var dataView = new DataView(viewTitle, this.getPacketIO(), this.getConsoleLogger());
109110

110111
var session = SessionManager.getCurrentSession();

frontend/src/components/Main/Explore/DataView/DataView.vue

+93-6
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,35 @@
11
<template>
2-
<div v-loading="state.loading" class="data-view" id="doc">
3-
<ExportDataDialog :visible.sync="exportDataDialogVisible" @submit="onExportSubmit"/>
4-
<Toolbar :items="iToolBarItems"/>
2+
<div id="doc" v-loading="state.loading" class="data-view" @contextmenu.prevent="preventDefaultContextMenu">
3+
<RightMenu ref="rightMenu" :menus="menus" />
4+
<ExportDataDialog :visible.sync="exportDataDialogVisible" @submit="onExportSubmit" />
5+
<Toolbar :items="iToolBarItems" />
56
<AgGridVue
67
:rowData="rowData"
78
:columnDefs="colDefs"
89
class="ag-theme-balham-dark"
910
style="height: 100%"
1011
:defaultColDef="defaultColDef"
1112
:gridOptions="gridOptions"
13+
@cell-context-menu="showContextMenu"
1214
/>
1315
</div>
1416
</template>
1517

1618
<script>
19+
import store from '@/store'
1720
import Toolbar from '@/framework/components/Toolbar/index.vue'
1821
import { Subject } from 'rxjs'
1922
import ExportDataDialog from '@/components/Main/Explore/DataView/ExportDataDialog.vue'
2023
import { AgGridVue } from 'ag-grid-vue'
24+
import RightMenu from '@/components/Main/Explore/DataView/RightMenu.vue'
25+
import { SpecialCharacters, GeneralInsertSQL, GeneralUpdateSQL } from './const'
2126
2227
import 'ag-grid-community/styles/ag-grid.css'
2328
import 'ag-grid-community/styles/ag-theme-balham.css'
2429
2530
export default {
2631
name: 'DataView',
27-
components: { ExportDataDialog, Toolbar, AgGridVue },
32+
components: { ExportDataDialog, Toolbar, AgGridVue, RightMenu },
2833
props: {
2934
meta: {
3035
type: Object,
@@ -164,7 +169,30 @@ export default {
164169
suppressMovableColumnsHints: true,
165170
suppressSortingHints: true
166171
},
167-
init: false
172+
init: false,
173+
currentRow: null,
174+
menus: [
175+
{
176+
name: 'copy',
177+
title: this.$t('Copy'),
178+
icon: 'el-icon-document-copy',
179+
hidden: () => { return !store.getters.profile.canCopy },
180+
children: [
181+
{
182+
name: 'copy-insert',
183+
title: this.$t('InsertStatement'),
184+
icon: 'el-icon-document-copy',
185+
callback: () => this.handleCopy('insert')
186+
},
187+
{
188+
name: 'copy-update',
189+
title: this.$t('UpdateStatement'),
190+
icon: 'el-icon-document-copy',
191+
callback: () => this.handleCopy('update')
192+
}
193+
]
194+
}
195+
]
168196
}
169197
},
170198
computed: {
@@ -231,9 +259,68 @@ export default {
231259
onExportSubmit(scope) {
232260
this.exportDataDialogVisible = false
233261
this.$emit('action', { action: 'export', data: scope })
262+
},
263+
showContextMenu(params) {
264+
this.currentRow = params.data
265+
this.$refs.rightMenu.show(params.event)
266+
},
267+
preventDefaultContextMenu(event) {
268+
event.preventDefault()
269+
},
270+
wrap(str, specChar) {
271+
const result = str ? str.trim() : ''
272+
return `${specChar}${result}${specChar}`
273+
},
274+
handleCopy(action) {
275+
let sql = ''
276+
let fields = ''
277+
let values = ''
278+
let updated_attrs = ''
279+
let conditional_attrs = ''
280+
let hasPrimary = false
281+
const dbType = store.getters.profile.dbType
282+
const { schema, table } = this.meta
283+
const char = SpecialCharacters[dbType]
284+
const tableName = `${this.wrap(schema, char)}.${this.wrap(table, char)}`
285+
const primaryKeys = ['id']
286+
for (let i = 0; i < this.colDefs.length; i++) {
287+
const fieldName = this.colDefs[i].field
288+
const fieldValue = `'${(this.currentRow[fieldName] || '')}'`
289+
if (action === 'insert') {
290+
fields += (i > 0 ? ', ' : '') + this.wrap(fieldName, char)
291+
values += (i > 0 ? ', ' : '') + `${fieldValue}`
292+
sql = GeneralInsertSQL
293+
.replace('{table_name}', tableName)
294+
.replace('{fields}', fields)
295+
.replace('{values}', values)
296+
} else {
297+
if (primaryKeys.includes(fieldName)) {
298+
hasPrimary = true
299+
} else {
300+
updated_attrs += (i > 0 ? ', ' : '') + `${this.wrap(fieldName, char)} = ${fieldValue}`
301+
}
302+
if (hasPrimary) {
303+
conditional_attrs = `${this.wrap('id', char)} = '${this.currentRow['id']}'`
304+
} else {
305+
conditional_attrs = `${updated_attrs} LIMIT 1`
306+
}
307+
sql = GeneralUpdateSQL
308+
.replace('{table_name}', tableName)
309+
.replace('{updated_attrs}', updated_attrs)
310+
.replace('{conditional_attrs}', conditional_attrs)
311+
}
312+
}
313+
if (!navigator.clipboard) {
314+
this.$message.error(`${this.$t('NoPermissionError')}: clipboard`)
315+
return
316+
}
317+
navigator.clipboard.writeText(sql).then(() => {
318+
this.$message.success(this.$t('CopySucceeded'))
319+
}).catch((error) => {
320+
this.$message.error(`${this.$t('CopyFailed')}: ${error}`)
321+
})
234322
}
235323
}
236-
237324
}
238325
</script>
239326

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<template>
2+
<ul
3+
v-show="menuVisible"
4+
class="menus"
5+
:style="{ top: menuTop, left: menuLeft }"
6+
>
7+
<li
8+
v-for="menu in iMenus"
9+
:key="menu.name"
10+
:class="['menu', menu.children? '' : 'menu-hover']"
11+
@click.stop="clickMenu(menu)"
12+
>
13+
<div @mouseenter="showSubMenu(menu)">
14+
<i :class="menu.icon" style="margin-right: 5px;" />
15+
<span>{{ menu.title }}</span>
16+
<i v-if="menu.children" class="el-icon-arrow-right" style="margin-left: 5px;" />
17+
</div>
18+
<ul v-show="subMenuVisible" class="submenu">
19+
<li
20+
v-for="subMenu in menu.children"
21+
:key="subMenu.name"
22+
class="submenu-item menu-hover"
23+
@click.stop="clickMenu(subMenu)"
24+
>
25+
<div>
26+
<i :class="subMenu.icon" style="margin-right: 5px;" />
27+
<span>{{ subMenu.title }}</span>
28+
</div>
29+
</li>
30+
</ul>
31+
</li>
32+
</ul>
33+
</template>
34+
35+
<script>
36+
export default {
37+
name: 'RightMenu',
38+
props: {
39+
menus: {
40+
type: Array,
41+
default: () => []
42+
}
43+
},
44+
data() {
45+
return {
46+
menuTop: '0px',
47+
menuLeft: '0px',
48+
menuVisible: false,
49+
subMenuVisible: false
50+
}
51+
},
52+
computed: {
53+
iMenus() {
54+
return this.menus.filter((m) => {
55+
if (typeof m.hidden === 'function') {
56+
return !m.hidden()
57+
}
58+
return !m.hidden
59+
})
60+
}
61+
},
62+
methods: {
63+
clickMenu(menu) {
64+
if (typeof menu.callback === 'function') {
65+
menu.callback()
66+
}
67+
},
68+
show(event) {
69+
if (this.iMenus.length === 0) {
70+
return
71+
}
72+
this.menuVisible = true
73+
const { clientX: x, clientY: y } = event
74+
const { innerWidth: innerWidth, innerHeight: innerHeight } = window
75+
const menuWidth = 180
76+
const menuHeight = this.iMenus.length * 30
77+
this.menuTop = (y + menuHeight > innerHeight ? innerHeight - menuHeight : y) + 'px'
78+
this.menuLeft = (x + menuWidth > innerWidth ? innerWidth - menuWidth : x) + 'px'
79+
document.addEventListener('mouseup', this.hide, false)
80+
},
81+
hide(e) {
82+
if (e.button === 0) {
83+
this.menuVisible = false
84+
this.subMenuVisible = false
85+
document.removeEventListener('mouseup', this.hide)
86+
}
87+
},
88+
showSubMenu(menu) {
89+
this.subMenuVisible = !!menu.children
90+
}
91+
}
92+
}
93+
</script>
94+
95+
<style lang='scss' scoped>
96+
.menu-hover:hover {
97+
color: #2f65ca;
98+
}
99+
100+
.menus {
101+
background: #fff;
102+
border-radius: 4px;
103+
list-style-type: none;
104+
padding: 3px;
105+
position: fixed;
106+
z-index: 9999;
107+
display: block;
108+
109+
.menu {
110+
padding: 6px 12px;
111+
cursor: pointer;
112+
113+
.submenu {
114+
background: #fff;
115+
list-style-type: none;
116+
padding: 3px;
117+
position: absolute;
118+
top: 0;
119+
left: 100%;
120+
}
121+
122+
.submenu-item {
123+
display: block;
124+
white-space: nowrap;
125+
padding: 6px 12px;
126+
cursor: pointer;
127+
}
128+
}
129+
}
130+
</style>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export const GeneralUpdateSQL = 'UPDATE {table_name} SET {updated_attrs} WHERE {conditional_attrs};'
2+
export const GeneralInsertSQL = 'INSERT INTO {table_name} ({fields}) VALUES ({values});'
3+
4+
export const SpecialCharacters = {
5+
mysql: '`',
6+
mariadb: '`',
7+
postgresql: '"',
8+
sqlserver: '',
9+
oracle: '"',
10+
dameng: '"',
11+
db2: ''
12+
}

0 commit comments

Comments
 (0)