💄 优化表格样式.

This commit is contained in:
lijiahangmax
2024-03-04 22:27:39 +08:00
parent 462e77f936
commit f1ade4e182
30 changed files with 166 additions and 66 deletions

View File

@@ -1,12 +1,25 @@
> 版本号严格遵循 Semver 规范。
[//]: # (🐞修复)
## v1.0.1
`2024-03-` `release`
🐞 修复 用户操作日志条件重置后类型框数据不正常的问题
🩰 修改 主机连接日志 UI
🌈 新增 SFTP 使用日志列表
🌈 新增 主机连接日志强制下线会话
🌈 新增 主机连接日志删除/清理
🌈 新增 用户操作日志日志删除/清理
🌈 新增 用户操作日志日志删除/清理
🔨 优化 用户锁定次数/时间可配置
[如何升级](/about/update.md?id=_v101)
## v1.0.0
`2024-03-01` `release`
🌈 用户自定义终端标签颜色
🌈 新增 用户自定义终端标签颜色
🔨 拓展数据模块添加缓存
[如何升级](/about/update.md?id=_v100)

View File

@@ -80,7 +80,6 @@
</template>
<!-- table -->
<a-table row-key="id"
class="table-wrapper-8"
ref="tableRef"
:loading="loading"
:columns="columns"

View File

@@ -28,7 +28,7 @@ public enum OutputTypeEnum {
/**
* 关闭连接
*/
CLOSE("cl", "${type}|${sessionId}|${msg}"),
CLOSE("cl", "${type}|${sessionId}|${forceClose}|${msg}"),
/**
* pong

View File

@@ -23,6 +23,9 @@ import lombok.experimental.SuperBuilder;
@Schema(name = "TerminalCloseResponse", description = "主机连接关闭响应 实体对象")
public class TerminalCloseResponse extends TerminalBasePayload {
@Schema(description = "是否为强制关闭")
private Integer forceClose;
@Schema(description = "关闭信息")
private String msg;

View File

@@ -4,6 +4,7 @@ import com.orion.lang.utils.io.Streams;
import com.orion.net.host.SessionStore;
import com.orion.net.host.ssh.shell.ShellExecutor;
import com.orion.ops.framework.common.constant.Const;
import com.orion.ops.framework.common.enums.BooleanBit;
import com.orion.ops.framework.websocket.core.utils.WebSockets;
import com.orion.ops.module.asset.define.AssetThreadPools;
import com.orion.ops.module.asset.handler.host.terminal.constant.TerminalMessage;
@@ -53,7 +54,7 @@ public class SshSession extends TerminalSession implements ISshSession {
executor.size(cols, rows);
executor.terminalType(terminalType);
executor.streamHandler(this::streamHandler);
executor.callback(this::eofCallback);
executor.callback(this::close);
executor.connect();
// 开始监听输出
AssetThreadPools.TERMINAL_STDOUT.execute(executor);
@@ -122,20 +123,4 @@ public class SshSession extends TerminalSession implements ISshSession {
}
}
/**
* eof 回调
*/
private void eofCallback() {
log.info("terminal eof回调 {}, forClose: {}, forceOffline: {}", sessionId, this.close, this.forceOffline);
// 发送关闭信息
TerminalCloseResponse resp = TerminalCloseResponse.builder()
.type(OutputTypeEnum.CLOSE.getType())
.sessionId(this.sessionId)
.msg(this.forceOffline ? TerminalMessage.FORCED_OFFLINE : TerminalMessage.CLOSED_CONNECTION)
.build();
WebSockets.sendText(channel, OutputTypeEnum.CLOSE.format(resp));
// 需要调用关闭 - 可能是 logout 需要手动触发
this.close();
}
}

View File

@@ -1,7 +1,12 @@
package com.orion.ops.module.asset.handler.host.terminal.session;
import com.orion.ops.framework.common.enums.BooleanBit;
import com.orion.ops.framework.websocket.core.utils.WebSockets;
import com.orion.ops.module.asset.enums.HostConnectStatusEnum;
import com.orion.ops.module.asset.handler.host.terminal.constant.TerminalMessage;
import com.orion.ops.module.asset.handler.host.terminal.enums.OutputTypeEnum;
import com.orion.ops.module.asset.handler.host.terminal.model.TerminalConfig;
import com.orion.ops.module.asset.handler.host.terminal.model.response.TerminalCloseResponse;
import com.orion.ops.module.asset.service.HostConnectLogService;
import com.orion.spring.SpringHolder;
import lombok.Getter;
@@ -41,6 +46,21 @@ public abstract class TerminalSession implements ITerminalSession {
*/
protected abstract void releaseResource();
/**
* 发送关闭消息
*/
protected void sendCloseMessage() {
log.info("TerminalSession close {}, forClose: {}, forceOffline: {}", sessionId, this.close, this.forceOffline);
// 发送关闭信息
TerminalCloseResponse resp = TerminalCloseResponse.builder()
.type(OutputTypeEnum.CLOSE.getType())
.sessionId(this.sessionId)
.forceClose(BooleanBit.of(this.forceOffline).getValue())
.msg(this.forceOffline ? TerminalMessage.FORCED_OFFLINE : TerminalMessage.CLOSED_CONNECTION)
.build();
WebSockets.sendText(channel, OutputTypeEnum.CLOSE.format(resp));
}
@Override
public void close() {
log.info("terminal close {}", sessionId);
@@ -56,6 +76,7 @@ public abstract class TerminalSession implements ITerminalSession {
public void forceOffline() {
log.info("terminal forceOffline {}", sessionId);
this.forceOffline = true;
// 关闭
this.checkAndClose();
}
@@ -75,6 +96,12 @@ public abstract class TerminalSession implements ITerminalSession {
} catch (Exception e) {
log.error("terminal release error {}", sessionId, e);
}
// 发送关闭信息
try {
this.sendCloseMessage();
} catch (Exception e) {
log.error("terminal send close error {}", sessionId, e);
}
return true;
}

View File

@@ -85,12 +85,25 @@ public class HostConnectLogServiceImpl implements HostConnectLogService {
HostConnectLogDO record = hostConnectLogDAO.of()
.createWrapper()
.eq(HostConnectLogDO::getToken, token)
.orderByDesc(HostConnectLogDO::getId)
.then()
.getOne();
if (record == null) {
log.info("HostConnectLogService-updateStatusByToken no record token: {}", token);
return Const.N_0;
}
return this.updateStatus(record, status, partial);
}
/**
* 更新状态
*
* @param record record
* @param status status
* @param partial partial
* @return effect
*/
private int updateStatus(HostConnectLogDO record, HostConnectStatusEnum status, Map<String, Object> partial) {
// 更新
HostConnectLogDO update = new HostConnectLogDO();
update.setId(record.getId());
@@ -151,20 +164,20 @@ public class HostConnectLogServiceImpl implements HostConnectLogService {
public Integer forceOffline(HostConnectLogQueryRequest request) {
Long id = request.getId();
// 查询数据是否存在
HostConnectLogDO connect = hostConnectLogDAO.selectById(id);
Valid.notNull(connect, ErrorMessage.LOG_ABSENT);
Valid.eq(connect.getStatus(), HostConnectStatusEnum.CONNECTING.name(), ErrorMessage.ILLEGAL_STATUS);
HostConnectLogDO record = hostConnectLogDAO.selectById(id);
Valid.notNull(record, ErrorMessage.LOG_ABSENT);
Valid.eq(record.getStatus(), HostConnectStatusEnum.CONNECTING.name(), ErrorMessage.ILLEGAL_STATUS);
// 设置日志参数
OperatorLogs.add(OperatorLogs.HOST_NAME, connect.getHostName());
OperatorLogs.add(OperatorLogs.HOST_NAME, record.getHostName());
// 获取会话
HostConnectLogExtraDTO extra = JSON.parseObject(connect.getExtraInfo(), HostConnectLogExtraDTO.class);
HostConnectLogExtraDTO extra = JSON.parseObject(record.getExtraInfo(), HostConnectLogExtraDTO.class);
ITerminalSession session = terminalManager.getSession(extra.getChannelId(), extra.getSessionId());
if (session != null) {
// 关闭会话
session.forceOffline();
}
// 更新状态
return this.updateStatusByToken(connect.getToken(), HostConnectStatusEnum.FORCE_OFFLINE, null);
return this.updateStatus(record, HostConnectStatusEnum.FORCE_OFFLINE, null);
}
/**

View File

@@ -6,7 +6,6 @@ import com.orion.ops.module.infra.entity.request.operator.OperatorLogQueryReques
import com.orion.ops.module.infra.entity.vo.LoginHistoryVO;
import com.orion.ops.module.infra.entity.vo.OperatorLogVO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
/**
@@ -25,7 +24,6 @@ public interface OperatorLogConvert {
OperatorLogDO to(OperatorLogQueryRequest request);
@Mapping(target = "extra", ignore = true)
OperatorLogVO to(OperatorLogDO domain);
LoginHistoryVO toLoginHistory(OperatorLogDO domain);

View File

@@ -8,7 +8,6 @@ import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Date;
import java.util.Map;
/**
* 操作日志 视图响应对象
@@ -60,7 +59,7 @@ public class OperatorLogVO implements Serializable {
private String logInfo;
@Schema(description = "参数")
private Map<String, Object> extra;
private String extra;
@Schema(description = "操作结果 0失败 1成功")
private Integer result;

View File

@@ -50,11 +50,7 @@ public class OperatorLogServiceImpl implements OperatorLogService {
// 查询
return operatorLogDAO.of(wrapper)
.page(request)
.dataGrid(s -> {
OperatorLogVO vo = OperatorLogConvert.MAPPER.to(s);
vo.setExtra(JSON.parseObject(s.getExtra()));
return vo;
});
.dataGrid(OperatorLogConvert.MAPPER::to);
}
@Override

View File

@@ -29,7 +29,6 @@
</template>
<!-- table -->
<a-table row-key="id"
class="table-wrapper-8"
ref="tableRef"
:loading="loading"
:columns="columns"

View File

@@ -72,7 +72,6 @@
</template>
<!-- table -->
<a-table row-key="id"
class="table-wrapper-8"
ref="tableRef"
:loading="loading"
:columns="columns"

View File

@@ -64,7 +64,6 @@
</template>
<!-- table -->
<a-table row-key="id"
class="table-wrapper-8"
ref="tableRef"
:loading="loading"
:columns="columns"

View File

@@ -91,7 +91,6 @@
</template>
<!-- table -->
<a-table row-key="id"
class="table-wrapper-8"
ref="tableRef"
:loading="loading"
:columns="columns"

View File

@@ -89,7 +89,6 @@
</template>
<!-- table -->
<a-table row-key="id"
class="table-wrapper-8"
ref="tableRef"
:loading="loading"
v-model:selected-keys="selectedKeys"

View File

@@ -44,8 +44,12 @@
</a-breadcrumb>
</div>
</div>
<!-- 已关闭-右侧操作 -->
<div v-if="isClose" class="sftp-table-header-right">
<span class="close-message">{{ closeMessage }}</span>
</div>
<!-- 路径编辑模式-右侧操作 -->
<a-space v-if="pathEditable" class="sftp-table-header-right">
<a-space v-else-if="pathEditable" class="sftp-table-header-right">
<!-- 进入 -->
<a-tooltip position="top"
:mini="true"
@@ -185,9 +189,11 @@
import { openSftpCreateModalKey, openSftpUploadModalKey } from '../../types/terminal.const';
const props = defineProps<{
isClose: boolean;
closeMessage: string | undefined;
currentPath: string;
session: ISftpSession | undefined,
selectedFiles: Array<string>
session: ISftpSession | undefined;
selectedFiles: Array<string>;
}>();
const emits = defineEmits(['update:selectedFiles', 'loadFile', 'download']);
@@ -322,6 +328,14 @@
}
}
.close-message {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
padding-left: 16px;
color: rgb(var(--red-6));
}
.header-action-icon {
font-size: 16px;
padding: 4px;

View File

@@ -12,6 +12,8 @@
<!-- 表头 -->
<sftp-table-header class="sftp-table-header"
v-model:selected-files="selectFiles"
:is-close="closed"
:close-message="closeMessage"
:current-path="currentPath"
:session="session"
@load-file="loadFiles"
@@ -89,6 +91,8 @@
const fileList = ref<Array<SftpFile>>([]);
const selectFiles = ref<Array<string>>([]);
const splitSize = ref(1);
const closed = ref(false);
const closeMessage = ref('');
const editorView = ref(false);
const editorRef = ref();
const editorFileName = ref('');
@@ -184,6 +188,14 @@
return success;
};
// 关闭回调
const onClose = (forceClose: string, msg: string) => {
console.log(forceClose);
console.log(msg);
closed.value = true;
closeMessage.value = msg;
};
// 接收列表回调
const resolveList = (result: string, path: string, list: Array<SftpFile>) => {
setTableLoading(false);
@@ -240,6 +252,7 @@
session.value = await sessionManager.openSftp(props.tab, {
setLoading: setTableLoading,
connectCallback,
onClose,
resolveList,
resolveSftpMkdir: resolveFileAction,
resolveSftpTouch: resolveFileAction,

View File

@@ -81,9 +81,9 @@ export default class TerminalOutputProcessor implements ITerminalOutputProcessor
}
// 处理关闭消息
processClose({ sessionId, msg }: OutputPayload): void {
processClose({ sessionId, msg, forceClose }: OutputPayload): void {
const session = this.sessionManager.getSession(sessionId);
// 无需处理 (直接关闭 tab )
// 无需处理 (直接关闭 tab)
if (!session) {
return;
}
@@ -98,6 +98,7 @@ export default class TerminalOutputProcessor implements ITerminalOutputProcessor
} else if (session instanceof SftpSession) {
// sftp 设置状态
session.connected = false;
session.resolver?.onClose(forceClose, msg);
}
}

View File

@@ -94,7 +94,7 @@ export const OutputProtocol = {
// 主机连接关闭
CLOSE: {
type: 'cl',
template: ['type', 'sessionId', 'msg'],
template: ['type', 'sessionId', 'forceClose', 'msg'],
processMethod: 'processClose'
},
// pong

View File

@@ -347,6 +347,8 @@ export interface ISftpSessionResolver {
setLoading: (loading: boolean) => void;
// 连接后回调
connectCallback: () => void;
// 关闭回调
onClose: (forceClose: string, msg: string) => void;
// 接受文件列表响应
resolveList: (result: string, path: string, list: Array<SftpFile>) => void;
// 接收创建文件夹响应

View File

@@ -57,7 +57,6 @@
</template>
<!-- table -->
<a-table row-key="id"
class="table-wrapper-8"
ref="tableRef"
:loading="loading"
:columns="columns"

View File

@@ -65,7 +65,6 @@
</template>
<!-- table -->
<a-table row-key="id"
class="table-wrapper-8"
ref="tableRef"
:loading="loading"
:columns="columns"

View File

@@ -33,7 +33,6 @@
:allow-search="true"
:filter-option="labelFilter"
placeholder="请选择操作模块"
@change="m => selectedModule(m as string)"
allow-clear />
</a-form-item>
<!-- 操作类型 -->
@@ -81,7 +80,7 @@
<script lang="ts" setup>
import type { SelectOptionData } from '@arco-design/web-vue/es/select/interface';
import type { OperatorLogQueryRequest } from '@/api/user/operator-log';
import { ref } from 'vue';
import { ref, watch } from 'vue';
import useLoading from '@/hooks/loading';
import useVisible from '@/hooks/visible';
import { getOperatorLogCount, clearOperatorLog } from '@/api/user/operator-log';
@@ -123,8 +122,8 @@
defineExpose({ open });
// 选择类型
const selectedModule = (module: string) => {
// 监听类型变化
watch(() => formModel.value.module, (module: string | undefined) => {
if (!module) {
// 不选择则重置 options
typeOptions.value = toOptions(operatorLogTypeKey);
@@ -138,7 +137,7 @@
if (formModel.value.type && !formModel.value.type.startsWith(modulePrefix)) {
formModel.value.type = undefined;
}
};
});
// 确定
const handlerOk = async () => {

View File

@@ -21,7 +21,6 @@
:allow-search="true"
:filter-option="labelFilter"
placeholder="请选择操作模块"
@change="m => selectedModule(m as string)"
allow-clear />
</a-form-item>
<!-- 操作类型 -->
@@ -67,12 +66,12 @@
<script lang="ts" setup>
import type { OperatorLogQueryRequest } from '@/api/user/operator-log';
import type { SelectOptionData } from '@arco-design/web-vue/es/select/interface';
import { reactive, ref } from 'vue';
import { reactive, ref, watch } from 'vue';
import useLoading from '@/hooks/loading';
import { useDictStore } from '@/store';
import UserSelector from '@/components/user/user/user-selector.vue';
import { operatorLogModuleKey, operatorLogTypeKey, operatorRiskLevelKey, operatorLogResultKey } from '../types/const';
import { labelFilter } from '@/types/form';
import UserSelector from '@/components/user/user/user-selector.vue';
const emits = defineEmits(['submit']);
const props = defineProps({
@@ -94,8 +93,8 @@
startTimeRange: undefined,
});
// 选择类型
const selectedModule = (module: string) => {
// 监听类型变化
watch(() => formModel.module, (module: string | undefined) => {
if (!module) {
// 不选择则重置 options
typeOptions.value = toOptions(operatorLogTypeKey);
@@ -109,7 +108,7 @@
if (formModel.type && !formModel.type.startsWith(modulePrefix)) {
formModel.type = undefined;
}
};
});
// 切换页码
const submit = () => {

View File

@@ -1,7 +1,6 @@
<template>
<!-- 表格 -->
<a-table row-key="id"
class="table-wrapper-8"
ref="tableRef"
:loading="loading"
:columns="tableColumns"
@@ -33,6 +32,19 @@
<icon-copy class="copy-left" @click="copy(record.originLogInfo, '已复制')" />
<span v-html="replaceHtmlTag(record.logInfo)" />
</template>
<!-- 操作地址 -->
<template #address="{ record }">
<span class="operator-location" :title="record.location">
{{ record.location }}
</span>
<br>
<span class="copy-left" title="复制" @click="copy(record.address)">
<icon-copy />
</span>
<span class="operator-address" :title="record.address">
{{ record.address }}
</span>
</template>
<!-- 操作 -->
<template #handle="{ record }">
<div class="table-handle-wrapper">
@@ -144,7 +156,7 @@
// 初始化
onMounted(() => {
let cols = [...columns].map(s => {
let cols = columns.map(s => {
return { ...s };
}).filter(s => s.dataIndex !== 'username');
if (props.handleColumn) {
@@ -164,4 +176,13 @@
</script>
<style lang="less" scoped>
.operator-location {
color: var(--color-text-2);
}
.operator-address {
margin-top: 4px;
display: inline-block;
color: var(--color-text-3);
}
</style>

View File

@@ -46,7 +46,6 @@
</template>
<!-- 表格 -->
<a-table row-key="id"
class="table-wrapper-8"
ref="tableRef"
:loading="loading"
v-model:selected-keys="selectedKeys"
@@ -80,6 +79,19 @@
<icon-copy class="copy-left" @click="copy(record.originLogInfo, '已复制')" />
<span v-html="replaceHtmlTag(record.logInfo)" />
</template>
<!-- 操作地址 -->
<template #address="{ record }">
<span class="operator-location" :title="record.location">
{{ record.location }}
</span>
<br>
<span class="copy-left" title="复制" @click="copy(record.address)">
<icon-copy />
</span>
<span class="operator-address" :title="record.address">
{{ record.address }}
</span>
</template>
<!-- 操作 -->
<template #handle="{ record }">
<div class="table-handle-wrapper">
@@ -220,5 +232,13 @@
</script>
<style lang="less" scoped>
.operator-location {
color: var(--color-text-2);
}
.operator-address {
margin-top: 4px;
display: inline-block;
color: var(--color-text-3);
}
</style>

View File

@@ -19,7 +19,7 @@ export const getLogDetail = (record: OperatorLogQueryResponse): Record<string, a
detail.duration = `${record.duration} ms`;
detail.startTime = dateFormat(new Date(record.startTime));
detail.endTime = dateFormat(new Date(record.endTime));
detail.extra = record?.extra;
detail.extra = JSON.parse(record?.extra);
detail.returnValue = JSON.parse(record?.returnValue);
return detail;
} catch (e) {

View File

@@ -32,6 +32,13 @@ const columns = [
slotName: 'originLogInfo',
ellipsis: true,
tooltip: true,
}, {
title: '操作地址',
dataIndex: 'address',
slotName: 'address',
width: 156,
align: 'left',
ellipsis: true,
}, {
title: '操作时间',
dataIndex: 'createTime',

View File

@@ -50,7 +50,6 @@
</template>
<!-- table -->
<a-table row-key="id"
class="table-wrapper-8"
ref="tableRef"
:loading="loading"
:columns="columns"

View File

@@ -65,7 +65,6 @@
</template>
<!-- table -->
<a-table row-key="id"
class="table-wrapper-8"
ref="tableRef"
:loading="loading"
:columns="columns"