From c06944b652798165322905500b0e1aa579e803d8 Mon Sep 17 00:00:00 2001 From: jiangweidong <1053570670@qq.com> Date: Wed, 5 Feb 2025 09:52:41 +0800 Subject: [PATCH 1/5] feat: Support right-click to copy SQL --- .../framework/console/DataViewConsole.java | 3 +- .../Main/Explore/DataView/DataView.vue | 95 ++++++++++++- .../Main/Explore/DataView/RightMenu.vue | 130 ++++++++++++++++++ .../components/Main/Explore/DataView/const.js | 12 ++ 4 files changed, 233 insertions(+), 7 deletions(-) create mode 100644 frontend/src/components/Main/Explore/DataView/RightMenu.vue create mode 100644 frontend/src/components/Main/Explore/DataView/const.js diff --git a/backend/framework/src/main/java/org/jumpserver/chen/framework/console/DataViewConsole.java b/backend/framework/src/main/java/org/jumpserver/chen/framework/console/DataViewConsole.java index 7e6bd3d..0f9c9eb 100644 --- a/backend/framework/src/main/java/org/jumpserver/chen/framework/console/DataViewConsole.java +++ b/backend/framework/src/main/java/org/jumpserver/chen/framework/console/DataViewConsole.java @@ -104,7 +104,8 @@ public void onConnect(Connect connect) { public void createDataView(String schemaName, String tableName) { var viewTitle = this.getTitle() + "child"; - this.getPacketIO().sendPacket("new_data_view", Map.of("title", viewTitle)); + var data = Map.of("title", viewTitle, "schema", this.schema, "table", this.table); + this.getPacketIO().sendPacket("new_data_view", data); var dataView = new DataView(viewTitle, this.getPacketIO(), this.getConsoleLogger()); var session = SessionManager.getCurrentSession(); diff --git a/frontend/src/components/Main/Explore/DataView/DataView.vue b/frontend/src/components/Main/Explore/DataView/DataView.vue index 3383327..703e59e 100644 --- a/frontend/src/components/Main/Explore/DataView/DataView.vue +++ b/frontend/src/components/Main/Explore/DataView/DataView.vue @@ -1,7 +1,8 @@ <template> - <div v-loading="state.loading" class="data-view" id="doc"> - <ExportDataDialog :visible.sync="exportDataDialogVisible" @submit="onExportSubmit"/> - <Toolbar :items="iToolBarItems"/> + <div id="doc" v-loading="state.loading" class="data-view" @contextmenu.prevent="preventDefaultContextMenu"> + <RightMenu ref="rightMenu" :menus="menus" /> + <ExportDataDialog :visible.sync="exportDataDialogVisible" @submit="onExportSubmit" /> + <Toolbar :items="iToolBarItems" /> <AgGridVue :rowData="rowData" :columnDefs="colDefs" @@ -9,22 +10,26 @@ style="height: 100%" :defaultColDef="defaultColDef" :gridOptions="gridOptions" + @cell-context-menu="showContextMenu" /> </div> </template> <script> +import store from '@/store' import Toolbar from '@/framework/components/Toolbar/index.vue' import { Subject } from 'rxjs' import ExportDataDialog from '@/components/Main/Explore/DataView/ExportDataDialog.vue' import { AgGridVue } from 'ag-grid-vue' +import RightMenu from '@/components/Main/Explore/DataView/RightMenu.vue' +import { SpecialCharacters, GeneralInsertSQL, GeneralUpdateSQL } from './const' import 'ag-grid-community/styles/ag-grid.css' import 'ag-grid-community/styles/ag-theme-balham.css' export default { name: 'DataView', - components: { ExportDataDialog, Toolbar, AgGridVue }, + components: { ExportDataDialog, Toolbar, AgGridVue, RightMenu }, props: { meta: { type: Object, @@ -164,7 +169,30 @@ export default { suppressMovableColumnsHints: true, suppressSortingHints: true }, - init: false + init: false, + currentRow: null, + menus: [ + { + name: 'copy', + title: this.$t('Copy'), + icon: 'el-icon-document-copy', + hidden: () => { return !store.getters.profile.canCopy }, + children: [ + { + name: 'copy-insert', + title: this.$t('InsertStatement'), + icon: 'el-icon-document-copy', + callback: () => this.handleCopy('insert') + }, + { + name: 'copy-update', + title: this.$t('UpdateStatement'), + icon: 'el-icon-document-copy', + callback: () => this.handleCopy('update') + } + ] + } + ] } }, computed: { @@ -231,9 +259,64 @@ export default { onExportSubmit(scope) { this.exportDataDialogVisible = false this.$emit('action', { action: 'export', data: scope }) + }, + showContextMenu(params) { + this.currentRow = params.data + this.$refs.rightMenu.show(params.event) + }, + preventDefaultContextMenu(event) { + event.preventDefault() + }, + wrap(str, specChar) { + const result = str ? str.trim() : '' + return `${specChar}${result}${specChar}` + }, + handleCopy(action) { + let sql = '' + let fields = '' + let values = '' + let updated_attrs = '' + let conditional_attrs = '' + let hasPrimary = false + const dbType = store.getters.profile.dbType + const { schema, table } = this.meta + const char = SpecialCharacters[dbType] + const tableName = `${this.wrap(schema, char)}.${this.wrap(table, char)}` + const primaryKeys = ['id'] + for (let i = 0; i < this.colDefs.length; i++) { + const fieldName = this.colDefs[i].field + const fieldValue = `'${(this.currentRow[fieldName] || '')}'` + if (action === 'insert') { + fields += (i > 0 ? ', ' : '') + this.wrap(fieldName, char) + values += (i > 0 ? ', ' : '') + `${fieldValue}` + sql = GeneralInsertSQL + .replace('{table_name}', tableName) + .replace('{fields}', fields) + .replace('{values}', values) + } else { + if (primaryKeys.includes(fieldName)) { + hasPrimary = true + } else { + updated_attrs += (i > 0 ? ', ' : '') + `${this.wrap(fieldName, char)} = ${fieldValue}` + } + if (hasPrimary) { + conditional_attrs = `${this.wrap('id', char)} = '${this.currentRow['id']}'` + } else { + conditional_attrs = `${updated_attrs} LIMIT 1` + } + sql = GeneralUpdateSQL + .replace('{table_name}', tableName) + .replace('{updated_attrs}', updated_attrs) + .replace('{conditional_attrs}', conditional_attrs) + } + } + navigator.clipboard.writeText(sql).then(() => { + this.$message.success(this.$t('CopySucceeded')) + }).catch((error) => { + this.$message.error(`${this.$t('CopyFailed')}: ${error}`) + }) } } - } </script> diff --git a/frontend/src/components/Main/Explore/DataView/RightMenu.vue b/frontend/src/components/Main/Explore/DataView/RightMenu.vue new file mode 100644 index 0000000..8c6fb82 --- /dev/null +++ b/frontend/src/components/Main/Explore/DataView/RightMenu.vue @@ -0,0 +1,130 @@ +<template> + <ul + v-show="menuVisible" + class="menus" + :style="{ top: menuTop, left: menuLeft }" + > + <li + v-for="menu in iMenus" + :key="menu.name" + :class="['menu', menu.children? '' : 'menu-hover']" + @click.stop="clickMenu(menu)" + > + <div @mouseenter="showSubMenu(menu)"> + <i :class="menu.icon" style="margin-right: 5px;" /> + <span>{{ menu.title }}</span> + <i v-if="menu.children" class="el-icon-arrow-right" style="margin-left: 5px;" /> + </div> + <ul v-show="subMenuVisible" class="submenu"> + <li + v-for="subMenu in menu.children" + :key="subMenu.name" + class="submenu-item menu-hover" + @click.stop="clickMenu(subMenu)" + > + <div> + <i :class="subMenu.icon" style="margin-right: 5px;" /> + <span>{{ subMenu.title }}</span> + </div> + </li> + </ul> + </li> + </ul> +</template> + +<script> +export default { + name: 'RightMenu', + props: { + menus: { + type: Array, + default: () => [] + } + }, + data() { + return { + menuTop: '0px', + menuLeft: '0px', + menuVisible: false, + subMenuVisible: false + } + }, + computed: { + iMenus() { + return this.menus.filter((m) => { + if (typeof m.hidden === 'function') { + return !m.hidden() + } + return !m.hidden + }) + } + }, + methods: { + clickMenu(menu) { + if (typeof menu.callback === 'function') { + menu.callback() + } + }, + show(event) { + if (this.iMenus.length === 0) { + return + } + this.menuVisible = true + const { clientX: x, clientY: y } = event + const { innerWidth: innerWidth, innerHeight: innerHeight } = window + const menuWidth = 180 + const menuHeight = this.iMenus.length * 30 + this.menuTop = (y + menuHeight > innerHeight ? innerHeight - menuHeight : y) + 'px' + this.menuLeft = (x + menuWidth > innerWidth ? innerWidth - menuWidth : x) + 'px' + document.addEventListener('mouseup', this.hide, false) + }, + hide(e) { + if (e.button === 0) { + this.menuVisible = false + this.subMenuVisible = false + document.removeEventListener('mouseup', this.hide) + } + }, + showSubMenu(menu) { + this.subMenuVisible = !!menu.children + } + } +} +</script> + +<style lang='scss' scoped> +.menu-hover:hover { + color: #2f65ca; +} + +.menus { + background: #fff; + border-radius: 4px; + list-style-type: none; + padding: 3px; + position: fixed; + z-index: 9999; + display: block; + + .menu { + padding: 6px 12px; + cursor: pointer; + + .submenu { + background: #fff; + list-style-type: none; + padding: 3px; + position: absolute; + top: 0; + left: 100%; + } + + .submenu-item { + display: block; + white-space: nowrap; + padding: 6px 12px; + cursor: pointer; + } + } +} +</style> diff --git a/frontend/src/components/Main/Explore/DataView/const.js b/frontend/src/components/Main/Explore/DataView/const.js new file mode 100644 index 0000000..d155671 --- /dev/null +++ b/frontend/src/components/Main/Explore/DataView/const.js @@ -0,0 +1,12 @@ +export const GeneralUpdateSQL = 'UPDATE {table_name} SET {updated_attrs} WHERE {conditional_attrs};' +export const GeneralInsertSQL = 'INSERT INTO {table_name} ({fields}) VALUES ({values});' + +export const SpecialCharacters = { + mysql: '`', + mariadb: '`', + postgresql: '"', + sqlserver: '', + oracle: '"', + dameng: '"', + db2: '' +} From 2e281a4c5a34d67535ab29aef98e3f10975a68fd Mon Sep 17 00:00:00 2001 From: jiangweidong <1053570670@qq.com> Date: Wed, 5 Feb 2025 10:48:25 +0800 Subject: [PATCH 2/5] perf: Optimization Tips --- frontend/src/components/Main/Explore/DataView/DataView.vue | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/src/components/Main/Explore/DataView/DataView.vue b/frontend/src/components/Main/Explore/DataView/DataView.vue index 703e59e..d147ece 100644 --- a/frontend/src/components/Main/Explore/DataView/DataView.vue +++ b/frontend/src/components/Main/Explore/DataView/DataView.vue @@ -310,6 +310,10 @@ export default { .replace('{conditional_attrs}', conditional_attrs) } } + if (!navigator.clipboard) { + this.$message.error(`${this.$t('NoPermissionError')}: clipboard`) + return + } navigator.clipboard.writeText(sql).then(() => { this.$message.success(this.$t('CopySucceeded')) }).catch((error) => { From 366871b8de54ee11f0607ab19d64088b9f1ee647 Mon Sep 17 00:00:00 2001 From: Aaron3S <chenyang@fit2cloud.com> Date: Thu, 6 Feb 2025 17:49:19 +0800 Subject: [PATCH 3/5] feat: export csv and excel --- backend/framework/pom.xml | 5 + .../framework/console/dataview/DataView.java | 98 +++---------- .../console/dataview/export/DataExport.java | 137 ++++++++++++++++++ .../dataview/export/DataExportInterface.java | 7 + .../Explore/DataView/ExportDataDialog.vue | 17 ++- 5 files changed, 186 insertions(+), 78 deletions(-) create mode 100644 backend/framework/src/main/java/org/jumpserver/chen/framework/console/dataview/export/DataExport.java create mode 100644 backend/framework/src/main/java/org/jumpserver/chen/framework/console/dataview/export/DataExportInterface.java diff --git a/backend/framework/pom.xml b/backend/framework/pom.xml index 8d1b25f..aaedccc 100644 --- a/backend/framework/pom.xml +++ b/backend/framework/pom.xml @@ -52,6 +52,11 @@ <artifactId>bcpkix-jdk15on</artifactId> <version>1.68</version> </dependency> + <dependency> + <groupId>org.dhatim</groupId> + <artifactId>fastexcel</artifactId> + <version>0.18.4</version> + </dependency> </dependencies> diff --git a/backend/framework/src/main/java/org/jumpserver/chen/framework/console/dataview/DataView.java b/backend/framework/src/main/java/org/jumpserver/chen/framework/console/dataview/DataView.java index f2d5e34..be4eb21 100644 --- a/backend/framework/src/main/java/org/jumpserver/chen/framework/console/dataview/DataView.java +++ b/backend/framework/src/main/java/org/jumpserver/chen/framework/console/dataview/DataView.java @@ -4,26 +4,18 @@ import lombok.EqualsAndHashCode; import org.jumpserver.chen.framework.console.action.DataViewAction; import org.jumpserver.chen.framework.console.component.Logger; +import org.jumpserver.chen.framework.console.dataview.export.DataExport; import org.jumpserver.chen.framework.console.entity.response.SQLResult; import org.jumpserver.chen.framework.console.state.DataViewState; import org.jumpserver.chen.framework.console.state.StateManager; -import org.jumpserver.chen.framework.datasource.entity.resource.Field; import org.jumpserver.chen.framework.datasource.sql.SQLQueryParams; import org.jumpserver.chen.framework.datasource.sql.SQLQueryResult; import org.jumpserver.chen.framework.jms.entity.CommandRecord; import org.jumpserver.chen.framework.session.SessionManager; -import org.jumpserver.chen.framework.utils.CodeUtils; import org.jumpserver.chen.framework.ws.io.PacketIO; -import java.io.BufferedWriter; -import java.io.IOException; -import java.nio.file.Files; -import java.sql.Clob; +import java.io.File; import java.sql.SQLException; -import java.text.SimpleDateFormat; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -74,7 +66,10 @@ public void doAction(DataViewAction action) throws SQLException { this.changeLimit((int) action.getData()); } case DataViewAction.ACTION_EXPORT -> { - this.export((String) action.getData()); + var data = (Map<String, String>) action.getData(); + var scope = data.get("scope"); + var format = data.get("format"); + this.export(scope, format); } } } @@ -134,84 +129,39 @@ private void fullDataViewData(DataViewData viewData, SQLQueryResult result) { } - private static void writeString(BufferedWriter writer, Object object) throws IOException { - var str = object.toString(); - - if (str.contains(",")) { - str = "\"" + str + "\""; - } - writer.write(str); - } - - private void writeCSVData(BufferedWriter writer, DataViewData viewData) throws IOException, SQLException { - - for (Field field : viewData.getFields()) { - writeString(writer, field.getName()); - writer.write(","); - } - for (Map<String, Object> row : viewData.getData()) { - for (Field field : viewData.getFields()) { - var obj = row.get(field.getName()); - if (obj == null) { - writer.write("NULL"); - writer.write(","); - } else if (obj instanceof Clob clob) { - writer.write(CodeUtils.escapeCsvValue(clob.getSubString(1, (int) clob.length()))); - writer.write(","); - } else if (obj instanceof Date) { - SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - writeString(writer, fmt.format(obj)); - } else { - writeString(writer, row.get(field.getName())); - writer.write(","); - } - } - writer.newLine(); - } - - writer.newLine(); - } - - public void export(String scope) throws SQLException { + public void export(String scope, String format) throws SQLException { var session = SessionManager.getCurrentSession(); - - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"); - String timestamp = LocalDateTime.now().format(formatter); - var f = session.createFile(String.format("data_%s.csv", timestamp)); - CommandRecord command = new CommandRecord(String.format("Export data: %s", this.title)); try { if (!SessionManager.getCurrentSession().canDownload()) { - session.getController().sendFile(f.getName()); return; } - var writer = Files.newBufferedWriter(f.toPath()); - - if (scope.equals("current")) { - this.writeCSVData(writer, this.data); - command.setOutput(String.format("%d rows exported", this.data.getData().size())); - } - - if (scope.equals("all")) { - SQLQueryParams queryParams = new SQLQueryParams(); - queryParams.setLimit(-1); - var result = this.loadDataInterface.loadData(queryParams); - var viewData = new DataViewData(); - this.fullDataViewData(viewData, result); - command.setOutput(String.format("%d rows exported", result.getData().size())); + File f = null; + switch (scope) { + case "current": + f = DataExport.export(format, this.data); + command.setOutput(String.format("%d rows exported", this.data.getData().size())); + break; + case "all": + SQLQueryParams queryParams = new SQLQueryParams(); + queryParams.setLimit(-1); + var result = this.loadDataInterface.loadData(queryParams); + var viewData = new DataViewData(); + this.fullDataViewData(viewData, result); + f = DataExport.export(format, viewData); + command.setOutput(String.format("%d rows exported", result.getData().size())); + break; } - writer.flush(); - writer.close(); this.consoleLogger.success(command.getOutput()); session.recordCommand(command); + session.getController().sendFile(f.getName()); - } catch (IOException e) { + } catch (Exception e) { throw new RuntimeException(e); } - session.getController().sendFile(f.getName()); } diff --git a/backend/framework/src/main/java/org/jumpserver/chen/framework/console/dataview/export/DataExport.java b/backend/framework/src/main/java/org/jumpserver/chen/framework/console/dataview/export/DataExport.java new file mode 100644 index 0000000..ef71526 --- /dev/null +++ b/backend/framework/src/main/java/org/jumpserver/chen/framework/console/dataview/export/DataExport.java @@ -0,0 +1,137 @@ +package org.jumpserver.chen.framework.console.dataview.export; + +import org.dhatim.fastexcel.Workbook; +import org.dhatim.fastexcel.Worksheet; +import org.jumpserver.chen.framework.console.dataview.DataViewData; +import org.jumpserver.chen.framework.datasource.entity.resource.Field; +import org.jumpserver.chen.framework.session.SessionManager; +import org.jumpserver.chen.framework.utils.CodeUtils; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.Clob; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Date; +import java.util.List; +import java.util.Map; + + +public class DataExport { + public static File export(String format, DataViewData data) throws Exception { + var session = SessionManager.getCurrentSession(); + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"); + String timestamp = LocalDateTime.now().format(formatter); + + String filename = String.format("data_%s", timestamp); + + File f; + + switch (format) { + case "excel": + f = session.createFile(String.format("%s.xlsx", filename)); + new DataExportExcel().exportData(f.toPath().toString(), data); + break; + case "csv": + f = session.createFile(String.format("%s.csv", filename)); + new DataExportCSV().exportData(f.toPath().toString(), data); + break; + default: + throw new Exception("unsupported format: " + format); + } + return f; + } +} + + +class DataExportExcel implements DataExportInterface { + @Override + public void exportData(String path, DataViewData data) throws Exception { + try (FileOutputStream fos = new FileOutputStream(path); + Workbook workbook = new Workbook(fos, "JumpServer", "4.0")) { + + Worksheet sheet = workbook.newWorksheet("Data"); + List<Field> fields = data.getFields(); + List<Map<String, Object>> rows = data.getData(); + + for (int col = 0; col < fields.size(); col++) { + sheet.value(0, col, fields.get(col).getName()); + } + + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + for (int rowIndex = 0; rowIndex < rows.size(); rowIndex++) { + Map<String, Object> row = rows.get(rowIndex); + + for (int col = 0; col < fields.size(); col++) { + Field field = fields.get(col); + Object obj = row.get(field.getName()); + + if (obj == null) { + sheet.value(rowIndex + 1, col, "NULL"); + } else if (obj instanceof Clob clob) { + try { + sheet.value(rowIndex + 1, col, clob.getSubString(1, (int) clob.length())); + } catch (Exception e) { + sheet.value(rowIndex + 1, col, "ERROR_CLOB"); + } + } else if (obj instanceof Date) { + sheet.value(rowIndex + 1, col, dateFormat.format(obj)); + } else { + sheet.value(rowIndex + 1, col, obj.toString()); + } + } + } + } + } +} + +class DataExportCSV implements DataExportInterface { + @Override + public void exportData(String path, DataViewData data) throws Exception { + var writer = Files.newBufferedWriter(Path.of(path)); + + for (Field field : data.getFields()) { + writeString(writer, field.getName()); + writer.write(","); + } + for (Map<String, Object> row : data.getData()) { + for (Field field : data.getFields()) { + var obj = row.get(field.getName()); + if (obj == null) { + writer.write("NULL"); + writer.write(","); + } else if (obj instanceof Clob clob) { + writer.write(CodeUtils.escapeCsvValue(clob.getSubString(1, (int) clob.length()))); + writer.write(","); + } else if (obj instanceof Date) { + SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + writeString(writer, fmt.format(obj)); + } else { + writeString(writer, row.get(field.getName())); + writer.write(","); + } + } + writer.newLine(); + } + + writer.newLine(); + writer.flush(); + writer.close(); + } + + private static void writeString(BufferedWriter writer, Object object) throws IOException { + var str = object.toString(); + + if (str.contains(",")) { + str = "\"" + str + "\""; + } + writer.write(str); + } +} \ No newline at end of file diff --git a/backend/framework/src/main/java/org/jumpserver/chen/framework/console/dataview/export/DataExportInterface.java b/backend/framework/src/main/java/org/jumpserver/chen/framework/console/dataview/export/DataExportInterface.java new file mode 100644 index 0000000..2bfb075 --- /dev/null +++ b/backend/framework/src/main/java/org/jumpserver/chen/framework/console/dataview/export/DataExportInterface.java @@ -0,0 +1,7 @@ +package org.jumpserver.chen.framework.console.dataview.export; + +import org.jumpserver.chen.framework.console.dataview.DataViewData; + +public interface DataExportInterface { + void exportData(String path, DataViewData data) throws Exception; +} \ No newline at end of file diff --git a/frontend/src/components/Main/Explore/DataView/ExportDataDialog.vue b/frontend/src/components/Main/Explore/DataView/ExportDataDialog.vue index c4029e4..0371bc7 100644 --- a/frontend/src/components/Main/Explore/DataView/ExportDataDialog.vue +++ b/frontend/src/components/Main/Explore/DataView/ExportDataDialog.vue @@ -6,8 +6,16 @@ width="40%" > <el-form ref="form" :model="form" label-width="80px"> - <el-radio v-model="form.scope" label="current">{{ $tc('ExportCurrent') }}</el-radio> - <el-radio v-model="form.scope" label="all">{{ $tc('ExportAll') }}</el-radio> + + <el-form-item :label="$tc('Scope')"> + <el-radio v-model="form.scope" label="current">{{ $tc('ExportCurrent') }}</el-radio> + <el-radio v-model="form.scope" label="all">{{ $tc('ExportAll') }}</el-radio> + </el-form-item> + + <el-form-item :label="$tc('Format')"> + <el-radio v-model="form.format" label="csv">CSV</el-radio> + <el-radio v-model="form.format" label="excel">Excel</el-radio> + </el-form-item> </el-form> <span slot="footer" class="dialog-footer"> @@ -35,7 +43,8 @@ export default { data() { return { form: { - scope: 'current' + scope: 'current', + format: 'csv' } } }, @@ -53,7 +62,7 @@ export default { }, methods: { onSubmit() { - this.$emit('submit', this.form.scope) + this.$emit('submit', { scope: this.form.scope, format: this.form.format }) } } } From 16264962e9087b9a83cdbb840b4f9364d81dd47f Mon Sep 17 00:00:00 2001 From: Aaron3S <chenyang@fit2cloud.com> Date: Tue, 18 Feb 2025 15:47:23 +0800 Subject: [PATCH 4/5] fix: export write new line --- .../chen/framework/console/dataview/export/DataExport.java | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/framework/src/main/java/org/jumpserver/chen/framework/console/dataview/export/DataExport.java b/backend/framework/src/main/java/org/jumpserver/chen/framework/console/dataview/export/DataExport.java index ef71526..a1255c7 100644 --- a/backend/framework/src/main/java/org/jumpserver/chen/framework/console/dataview/export/DataExport.java +++ b/backend/framework/src/main/java/org/jumpserver/chen/framework/console/dataview/export/DataExport.java @@ -101,6 +101,7 @@ public void exportData(String path, DataViewData data) throws Exception { writeString(writer, field.getName()); writer.write(","); } + writer.newLine(); for (Map<String, Object> row : data.getData()) { for (Field field : data.getFields()) { var obj = row.get(field.getName()); From 1267fb2fcc2cf86505a5e34096e31c50241d14ec Mon Sep 17 00:00:00 2001 From: Aaron3S <chenyang@fit2cloud.com> Date: Wed, 19 Feb 2025 18:54:32 +0800 Subject: [PATCH 5/5] feat: add health check api --- .../web/controller/HealthCheckController.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 backend/web/src/main/java/org/jumpserver/chen/web/controller/HealthCheckController.java diff --git a/backend/web/src/main/java/org/jumpserver/chen/web/controller/HealthCheckController.java b/backend/web/src/main/java/org/jumpserver/chen/web/controller/HealthCheckController.java new file mode 100644 index 0000000..3da55a1 --- /dev/null +++ b/backend/web/src/main/java/org/jumpserver/chen/web/controller/HealthCheckController.java @@ -0,0 +1,15 @@ +package org.jumpserver.chen.web.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/healthy") +public class HealthCheckController { + + @GetMapping("") + public String healthCheck() { + return "ok"; + } +}