Compare commits

...

8 Commits

Author SHA1 Message Date
lijiahangmax
770f54df96 Merge pull request #1 from lijiahangmax/dev
v1.0.0
2024-03-01 17:26:29 +08:00
lijiahang
4799936a5f 🐛 终端标签页颜色错误. 2024-03-01 17:24:43 +08:00
lijiahang
55373bb7dc 终端颜色配置. 2024-03-01 15:56:00 +08:00
lijiahang
7e91ef8386 🚀 修订版本号. 2024-03-01 13:54:09 +08:00
lijiahang
1b459beb84 终端颜色配置. 2024-03-01 12:32:06 +08:00
lijiahangmax
c48e48ab72 💄 修改主机配置模态框样式. 2024-03-01 00:35:55 +08:00
lijiahangmax
1c6a38d5d9 优化别名逻辑. 2024-02-29 23:25:00 +08:00
lijiahang
44dd5a9079 添加 tab 配色. 2024-02-29 19:15:08 +08:00
49 changed files with 760 additions and 553 deletions

View File

@@ -2,7 +2,7 @@
<img style="margin-right: 8px;" src="https://bjuimg.obs.cn-north-4.myhuaweicloud.com/images/2024/2/27/8c687ef1-5711-4a93-9db0-79c010af7902.png" width="32px" height="32px"/> orion-ops-pro 是什么
</h1>
`orion-ops-pro` 一款开箱即用的运维平台, 提供了资产管理、资产授权、Web终端、WebSftp、角色管理、系统管理等功能。为运维团队提供轻量化的运维治理平台。它是根据 `orion-ops`
`orion-ops-pro` 一款开箱即用的一站式智能运维平台, 提供了资产管理、资产授权、Web终端、WebSftp、角色管理、系统管理等功能。致力于简化运维团队的治理工作。它是根据 `orion-ops`
的产品思路完全重构的一套系统, 重新设计了架构并优化交互逻辑, 操作更快捷友好。
<p style="text-align: left">
@@ -28,7 +28,7 @@
<br/>
当前版本: **1.0.0-beta.1**
当前版本: **1.0.0**
github: https://github.com/lijiahangmax/orion-ops-pro
gitee: https://gitee.com/lijiahangmax/orion-ops-pro
文档: https://lijiahangmax.gitee.io/orion-ops-pro/#/
@@ -40,12 +40,12 @@ demo: http://101.43.254.243:1081/#/
## 特性
* 易用便捷: 极简配置, 开箱即用, 并兼容 Docker 部署方式。
* 易用便捷: 极简配置, 开箱即用, 支持 Docker 部署方式。
* 资产管理: 支持灵活配置主机分组, 统一管理主机、秘钥和身份。
* 资产授权: 可将资产数据授权给指定角色和用户。
* 权限控制: 全面管理用户角色, 支持动态菜单配置和强制下线等功能。
* 在线终端: 提供便捷的在线 Web 终端服务, 支持自定义快捷键和主题风格。
* 文件管理: 实现远程主机文件的批量上传、下载和在线编辑等操作。
* 在线终端: 提供便捷的在线 Web 终端服务, 支持快捷命令、自定义快捷键和主题风格。
* 文件管理: 实现远程主机文件的批量上传、下载和在线编辑等操作。
* 可扩展性: 前后端代码规范统一, 代码质量高、健壮且易于阅读和扩展。
[comment]: <> ( FIXME * 批量操作: 支持远程主机批量执行命令 以及 批量执行上传文件)

View File

@@ -1,7 +1,7 @@
version: '3.3'
services:
orion-ops-pro:
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-ops-pro:1.0.0-beta.1
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-ops-pro:1.0.0
ports:
- 1081:80
environment:

View File

@@ -1,3 +1,3 @@
mv ../../orion-ops-launch/target/orion-ops-launch.jar ./
mv ../../orion-ops-ui/dist ./dist
docker build -t orion-ops-pro:1.0.0-beta.1 .
docker build -t orion-ops-pro:1.0.0 .

View File

@@ -2,7 +2,7 @@
<img style="margin-right: 8px;" src="./assert/logo.svg" width="32px" height="32px"/> orion-ops-pro 是什么
</h1>
`orion-ops-pro` 一款开箱即用的运维平台, 提供了资产管理、资产授权、Web终端、WebSftp、角色管理、系统管理等功能。为运维团队提供轻量化的运维治理平台。它是根据 `orion-ops`
`orion-ops-pro` 一款开箱即用的一站式智能运维平台, 提供了资产管理、资产授权、Web终端、WebSftp、角色管理、系统管理等功能。致力于简化运维团队的治理工作。它是根据 `orion-ops`
的产品思路完全重构的一套系统, 重新设计了架构并优化交互逻辑, 操作更快捷友好。
<p style="text-align: left">
@@ -28,7 +28,7 @@
<br/>
当前版本: **1.0.0-beta.1**
当前版本: **1.0.0**
github: https://github.com/lijiahangmax/orion-ops-pro
gitee: https://gitee.com/lijiahangmax/orion-ops-pro
文档: https://lijiahangmax.gitee.io/orion-ops-pro/#/
@@ -40,12 +40,12 @@ demo: http://101.43.254.243:1081/#/
## 特性
* 易用便捷: 极简配置, 开箱即用, 并兼容 Docker 部署方式。
* 易用便捷: 极简配置, 开箱即用, 支持 Docker 部署方式。
* 资产管理: 支持灵活配置主机分组, 统一管理主机、秘钥和身份。
* 资产授权: 可将资产数据授权给指定角色和用户。
* 权限控制: 全面管理用户角色, 支持动态菜单配置和强制下线等功能。
* 在线终端: 提供便捷的在线 Web 终端服务, 支持自定义快捷键和主题风格。
* 文件管理: 实现远程主机文件的批量上传、下载和在线编辑等操作。
* 在线终端: 提供便捷的在线 Web 终端服务, 支持快捷命令、自定义快捷键和主题风格。
* 文件管理: 实现远程主机文件的批量上传、下载和在线编辑等操作。
* 可扩展性: 前后端代码规范统一, 代码质量高、健壮且易于阅读和扩展。
[comment]: <> ( FIXME * 批量操作: 支持远程主机批量执行命令 以及 批量执行上传文件)

View File

@@ -1,8 +1,8 @@
# orion-ops-pro <small>1.0.0-beta.1</small>
# orion-ops-pro <small>1.0.0</small>
> 一款开箱即用的运维平台。
- 易用 便捷
- 友好 易用
- 安全 稳定
- 智能 高效

View File

@@ -1,6 +1,15 @@
> 版本号严格遵循 Semver 规范。
[//]: # (🌈添加 🔨优化 🐞修复 [如何升级]&#40;/about/update.md?id=_100&#41;)
[//]: # (🐞修复)
## 1.0.0
`2024-03-01` `release`
🌈 用户自定义终端标签颜色
🔨 拓展数据模块添加缓存
[如何升级](/about/update.md?id=_100)
## 1.0.0-beta.1

View File

@@ -9,4 +9,4 @@
* 站内消息
* 后端配置动态配置
* 终端背景图片
* 资产数据 UI 改版
* 资产授权 UI 改版

View File

@@ -1,9 +1,19 @@
⚡ 注意: 应用不支持跨版本升级, 可以进行多次升级
## 1.0.0-beta.1
## 1.0.0
> sql 脚本
```sql
INSERT INTO `dict_key` VALUES (32, 'terminalTabColor', 'COLOR', '[]', '终端标签页颜色', '2024-03-01 15:01:44', '2024-03-01 15:01:44', '1', '1', 0);
INSERT INTO `dict_value` VALUES (203, 32, 'terminalTabColor', 'rgb(var(--red-6))', '红色', '{}', 10, '2024-03-01 15:07:41', '2024-03-01 15:07:41', '1', '1', 0);
INSERT INTO `dict_value` VALUES (204, 32, 'terminalTabColor', 'rgb(var(--orange-6))', '橙色', '{}', 20, '2024-03-01 15:07:55', '2024-03-01 15:07:55', '1', '1', 0);
INSERT INTO `dict_value` VALUES (205, 32, 'terminalTabColor', 'rgb(var(--yellow-6))', '黄色', '{}', 30, '2024-03-01 15:08:13', '2024-03-01 15:08:13', '1', '1', 0);
INSERT INTO `dict_value` VALUES (206, 32, 'terminalTabColor', 'rgb(var(--green-6))', '绿色', '{}', 40, '2024-03-01 15:08:23', '2024-03-01 15:08:23', '1', '1', 0);
INSERT INTO `dict_value` VALUES (207, 32, 'terminalTabColor', 'rgb(var(--cyan-6))', '青色', '{}', 50, '2024-03-01 15:08:46', '2024-03-01 15:08:46', '1', '1', 0);
INSERT INTO `dict_value` VALUES (208, 32, 'terminalTabColor', 'rgb(var(--blue-6))', '浅蓝', '{}', 60, '2024-03-01 15:11:01', '2024-03-01 15:11:01', '1', '1', 0);
INSERT INTO `dict_value` VALUES (209, 32, 'terminalTabColor', 'rgb(var(--arcoblue-6))', '蓝色', '{}', 70, '2024-03-01 15:11:11', '2024-03-01 15:11:11', '1', '1', 0);
INSERT INTO `dict_value` VALUES (210, 32, 'terminalTabColor', 'rgb(var(--purple-6))', '紫色', '{}', 80, '2024-03-01 15:11:20', '2024-03-01 15:11:20', '1', '1', 0);
INSERT INTO `dict_value` VALUES (211, 32, 'terminalTabColor', 'rgb(var(--pinkpurple-6))', '粉紫', '{}', 90, '2024-03-01 15:11:41', '2024-03-01 15:11:41', '1', '1', 0);
INSERT INTO `dict_value` VALUES (213, 32, 'terminalTabColor', 'rgb(var(--gray-6))', '灰色', '{}', 100, '2024-03-01 15:12:01', '2024-03-01 15:39:34', '1', '1', 0);
```

View File

@@ -2,8 +2,12 @@
主机终端页面 支持 SSH, SFTP。
打开后默认会进入新建连接页面, 页面的主机数据是用户授权的资产数据。
鼠标移入列表内的主机上时, 右侧会出现 `打开 SSH` `打开 SFTP` `连接设置` `收藏` 的按钮。
点击 `连接设置` 后, 会弹出模态框, 可以自定义配置连接主机的 密码/秘钥/身份, 仅对自己生效, 不会修改全局配置, `秘钥` `身份` 数据是用户授权的资产数据。
鼠标移入列表内的主机上时, 右侧会出现 `打开 SSH` `打开 SFTP` `主机设置` `收藏` 的按钮。
> 主机设置
* SSH 配置: 可以自定义配置连接主机的 密码/秘钥/身份, 仅对自己生效, 不会修改全局配置, `秘钥` `身份` 数据是用户授权的资产数据
* 标签颜色: 自定义配置标签的颜色, 可以用来区分环境等
> 顶部状态栏

View File

@@ -14,7 +14,7 @@
<url>https://github.com/lijiahangmax/orion-ops-pro</url>
<properties>
<revision>1.0.0-beta.1</revision>
<revision>1.0.0</revision>
<spring.boot.version>2.7.17</spring.boot.version>
<spring.boot.admin.version>2.7.15</spring.boot.admin.version>
<flatten.maven.plugin.version>1.5.0</flatten.maven.plugin.version>

View File

@@ -12,7 +12,7 @@ public interface OrionOpsProConst {
/**
* 同 ${orion.version} 迭代时候需要手动更改
*/
String VERSION = "1.0.0-beta.1";
String VERSION = "1.0.0";
String GITHUB = "https://github.com/lijiahangmax/orion-ops-pro";

View File

@@ -84,6 +84,7 @@ public class WebSockets {
try {
Threads.sleep(delay);
session.sendMessage(new TextMessage(message));
log.info("消息重发成功");
Threads.sleep(delay);
} catch (Exception ex) {
throw Exceptions.ioRuntime(ex);

View File

@@ -64,4 +64,7 @@ public class HostVO implements Serializable {
@Schema(description = "别名")
private String alias;
@Schema(description = "颜色")
private String color;
}

View File

@@ -3,7 +3,9 @@ package com.orion.ops.module.asset.enums;
import com.orion.ops.framework.common.handler.data.GenericsDataDefinition;
import com.orion.ops.framework.common.handler.data.model.GenericsDataModel;
import com.orion.ops.framework.common.handler.data.strategy.MapDataStrategy;
import com.orion.ops.module.asset.handler.host.extra.model.HostColorExtraModel;
import com.orion.ops.module.asset.handler.host.extra.model.HostSshExtraModel;
import com.orion.ops.module.asset.handler.host.extra.strategy.HostColorExtraStrategy;
import com.orion.ops.module.asset.handler.host.extra.strategy.HostSshExtraStrategy;
import lombok.AllArgsConstructor;
import lombok.Getter;
@@ -24,6 +26,11 @@ public enum HostExtraItemEnum implements GenericsDataDefinition {
*/
SSH("ssh", HostSshExtraModel.class, HostSshExtraStrategy.class),
/**
* 颜色额外配置
*/
COLOR("color", HostColorExtraModel.class, HostColorExtraStrategy.class),
;
private final String item;

View File

@@ -0,0 +1,27 @@
package com.orion.ops.module.asset.handler.host.extra.model;
import com.orion.ops.framework.common.handler.data.model.GenericsDataModel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 主机拓展信息 - color 模型
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/2/29 23:16
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(name = "HostExtraSshModel", description = "主机拓展信息 - color 模型")
public class HostColorExtraModel implements GenericsDataModel {
@Schema(description = "颜色")
private String color;
}

View File

@@ -0,0 +1,37 @@
package com.orion.ops.module.asset.handler.host.extra.strategy;
import com.orion.ops.framework.common.handler.data.strategy.MapDataStrategy;
import com.orion.ops.module.asset.handler.host.extra.model.HostColorExtraModel;
import org.springframework.stereotype.Component;
/**
* 主机拓展信息 - 颜色 模型处理策略
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/2/29 23:16
*/
@Component
public class HostColorExtraStrategy implements MapDataStrategy<HostColorExtraModel> {
@Override
public HostColorExtraModel getDefault() {
return HostColorExtraModel.builder()
// 默认透明
.color("")
.build();
}
@Override
public void updateFill(HostColorExtraModel beforeModel, HostColorExtraModel afterModel) {
}
@Override
public void preValid(HostColorExtraModel model) {
}
@Override
public void valid(HostColorExtraModel model) {
}
}

View File

@@ -1,6 +1,8 @@
package com.orion.ops.module.asset.service.impl;
import com.alibaba.fastjson.JSON;
import com.orion.lang.function.Functions;
import com.orion.lang.utils.Refs;
import com.orion.lang.utils.collect.Lists;
import com.orion.lang.utils.collect.Maps;
import com.orion.ops.framework.common.constant.Const;
@@ -10,8 +12,10 @@ import com.orion.ops.module.asset.convert.HostGroupConvert;
import com.orion.ops.module.asset.entity.request.asset.AssetAuthorizedDataQueryRequest;
import com.orion.ops.module.asset.entity.vo.*;
import com.orion.ops.module.asset.enums.HostConnectTypeEnum;
import com.orion.ops.module.asset.handler.host.extra.model.HostColorExtraModel;
import com.orion.ops.module.asset.service.*;
import com.orion.ops.module.infra.api.*;
import com.orion.ops.module.infra.constant.DataExtraItems;
import com.orion.ops.module.infra.entity.dto.data.DataGroupDTO;
import com.orion.ops.module.infra.entity.dto.tag.TagDTO;
import com.orion.ops.module.infra.enums.*;
@@ -67,7 +71,7 @@ public class AssetAuthorizedDataServiceImpl implements AssetAuthorizedDataServic
private TagRelApi tagRelApi;
@Resource
private DataAliasApi dataAliasApi;
private DataExtraApi dataExtraApi;
@Override
public List<Long> getAuthorizedDataRelId(DataPermissionTypeEnum type, AssetAuthorizedDataQueryRequest request) {
@@ -179,8 +183,10 @@ public class AssetAuthorizedDataServiceImpl implements AssetAuthorizedDataServic
Future<List<Long>> favoriteResult = favoriteApi.getFavoriteRelIdListAsync(FavoriteTypeEnum.HOST, userId);
// 查询最近连接的主机
Future<List<Long>> latestConnectHostIdList = hostConnectLogService.getLatestConnectHostIdAsync(HostConnectTypeEnum.SSH, userId);
// 查询数据别名
Future<Map<Long, String>> dataAliasResult = dataAliasApi.getDataAliasAsync(userId, DataExtraTypeEnum.HOST);
// 查询别名
Future<Map<Long, String>> dataAliasResult = dataExtraApi.getExtraItemValuesByCacheAsync(userId, DataExtraTypeEnum.HOST, DataExtraItems.ALIAS);
// 查询颜色
Future<Map<Long, String>> dataColorResult = dataExtraApi.getExtraItemValuesByCacheAsync(userId, DataExtraTypeEnum.HOST, DataExtraItems.COLOR);
// 查询分组
List<DataGroupDTO> dataGroup = dataGroupApi.getDataGroupList(DataGroupTypeEnum.HOST);
// 查询分组引用
@@ -207,7 +213,8 @@ public class AssetAuthorizedDataServiceImpl implements AssetAuthorizedDataServic
// 设置主机拓展信息
this.getAuthorizedHostExtra(wrapper.getHostList(),
favoriteResult.get(),
dataAliasResult.get());
dataAliasResult.get(),
dataColorResult.get());
// 设置最近连接的主机
wrapper.setLatestHosts(new LinkedHashSet<>(latestConnectHostIdList.get()));
return wrapper;
@@ -292,10 +299,12 @@ public class AssetAuthorizedDataServiceImpl implements AssetAuthorizedDataServic
* @param hosts hosts
* @param favorite favorite
* @param aliasMap aliasMap
* @param colorMap colorMap
*/
private void getAuthorizedHostExtra(List<HostVO> hosts,
List<Long> favorite,
Map<Long, String> aliasMap) {
Map<Long, String> aliasMap,
Map<Long, String> colorMap) {
if (Lists.isEmpty(hosts)) {
return;
}
@@ -313,7 +322,21 @@ public class AssetAuthorizedDataServiceImpl implements AssetAuthorizedDataServic
}
// 设置主机别名
if (!Maps.isEmpty(aliasMap)) {
hosts.forEach(s -> s.setAlias(aliasMap.get(s.getId())));
hosts.forEach(s -> {
String alias = aliasMap.get(s.getId());
if (alias != null) {
s.setAlias(Refs.unrefToString(alias));
}
});
}
// 设置主机颜色
if (!Maps.isEmpty(colorMap)) {
hosts.forEach(s -> {
HostColorExtraModel color = JSON.parseObject(colorMap.get(s.getId()), HostColorExtraModel.class);
if (color != null) {
s.setColor(color.getColor());
}
});
}
}

View File

@@ -1,6 +1,7 @@
package com.orion.ops.module.asset.service.impl;
import com.orion.lang.function.Functions;
import com.orion.lang.utils.Refs;
import com.orion.lang.utils.collect.Maps;
import com.orion.ops.framework.common.constant.ErrorMessage;
import com.orion.ops.framework.common.handler.data.model.GenericsDataModel;
@@ -12,9 +13,8 @@ import com.orion.ops.module.asset.entity.request.host.HostExtraQueryRequest;
import com.orion.ops.module.asset.entity.request.host.HostExtraUpdateRequest;
import com.orion.ops.module.asset.enums.HostExtraItemEnum;
import com.orion.ops.module.asset.service.HostExtraService;
import com.orion.ops.module.infra.api.DataAliasApi;
import com.orion.ops.module.infra.api.DataExtraApi;
import com.orion.ops.module.infra.entity.dto.data.DataAliasUpdateDTO;
import com.orion.ops.module.infra.constant.DataExtraItems;
import com.orion.ops.module.infra.entity.dto.data.DataExtraDTO;
import com.orion.ops.module.infra.entity.dto.data.DataExtraQueryDTO;
import com.orion.ops.module.infra.entity.dto.data.DataExtraSetDTO;
@@ -36,20 +36,18 @@ import java.util.stream.Collectors;
@Service
public class HostExtraServiceImpl implements HostExtraService {
@Resource
private DataAliasApi dataAliasApi;
@Resource
private DataExtraApi dataExtraApi;
@Override
public Integer updateHostAlias(HostAliasUpdateRequest request) {
DataAliasUpdateDTO update = DataAliasUpdateDTO.builder()
DataExtraSetDTO update = DataExtraSetDTO.builder()
.userId(SecurityUtils.getLoginUserId())
.item(DataExtraItems.ALIAS)
.relId(request.getId())
.alias(request.getName())
.value(Refs.json(request.getName()))
.build();
return dataAliasApi.updateDataAlias(update, DataExtraTypeEnum.HOST);
return dataExtraApi.setExtraItem(update, DataExtraTypeEnum.HOST);
}
@Override

View File

@@ -1,55 +0,0 @@
package com.orion.ops.module.infra.api;
import com.orion.ops.module.infra.entity.dto.data.DataAliasUpdateDTO;
import com.orion.ops.module.infra.enums.DataExtraTypeEnum;
import java.util.Map;
import java.util.concurrent.Future;
/**
* 数据别名 对外服务类
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023-12-18 17:37
*/
public interface DataAliasApi {
/**
* 更新数据别名
*
* @param dto dto
* @param type type
* @return effect
*/
Integer updateDataAlias(DataAliasUpdateDTO dto, DataExtraTypeEnum type);
/**
* 查询数据别名
*
* @param userId userId
* @param type type
* @param relId relId
* @return aliasName
*/
String getDataAlias(Long userId, DataExtraTypeEnum type, Long relId);
/**
* 查询数据别名
*
* @param userId userId
* @param type type
* @return relId:aliasName
*/
Map<Long, String> getDataAlias(Long userId, DataExtraTypeEnum type);
/**
* 异步查询数据别名
*
* @param userId userId
* @param type type
* @return relId:aliasName
*/
Future<Map<Long, String>> getDataAliasAsync(Long userId, DataExtraTypeEnum type);
}

View File

@@ -7,6 +7,7 @@ import com.orion.ops.module.infra.enums.DataExtraTypeEnum;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
/**
* 数据拓展信息 对外服务类
@@ -69,6 +70,37 @@ public interface DataExtraApi {
*/
Map<Long, String> getExtraItemValues(DataExtraQueryDTO dto, DataExtraTypeEnum type);
/**
* 查询额外配置项 (查询缓存)
*
* @param userId userId
* @param type type
* @param item item
* @param relId relId
* @return value
*/
String getExtraItemValueByCache(Long userId, DataExtraTypeEnum type, String item, Long relId);
/**
* 查询额外配置项 (查询缓存)
*
* @param userId userId
* @param type type
* @param item item
* @return relId:value
*/
Map<Long, String> getExtraItemValuesByCache(Long userId, DataExtraTypeEnum type, String item);
/**
* 异步查询额外配置项 (查询缓存)
*
* @param userId userId
* @param type type
* @param item item
* @return value
*/
Future<Map<Long, String>> getExtraItemValuesByCacheAsync(Long userId, DataExtraTypeEnum type, String item);
/**
* 查询额外配置
*

View File

@@ -11,4 +11,6 @@ public interface DataExtraItems {
String ALIAS = "alias";
String COLOR = "color";
}

View File

@@ -1,42 +0,0 @@
package com.orion.ops.module.infra.entity.dto.data;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.io.Serializable;
/**
* 数据别名 更新请求业务对象
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023-12-18 17:37
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(name = "DataAliasUpdateDTO", description = "数据别名 创建请求业务对象")
public class DataAliasUpdateDTO implements Serializable {
private static final long serialVersionUID = 1L;
@NotNull
@Schema(description = "用户id")
private Long userId;
@NotNull
@Schema(description = "数据id")
private Long relId;
@NotNull
@Size(max = 32)
@Schema(description = "别名")
private String alias;
}

View File

@@ -1,63 +0,0 @@
package com.orion.ops.module.infra.api.impl;
import com.orion.ops.framework.common.utils.Valid;
import com.orion.ops.module.infra.api.DataAliasApi;
import com.orion.ops.module.infra.entity.dto.data.DataAliasUpdateDTO;
import com.orion.ops.module.infra.entity.request.data.DataAliasUpdateRequest;
import com.orion.ops.module.infra.enums.DataExtraTypeEnum;
import com.orion.ops.module.infra.service.DataAliasService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
/**
* 数据别名 对外服务实现类
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023-12-18 17:37
*/
@Slf4j
@Service
public class DataAliasApiImpl implements DataAliasApi {
@Resource
private DataAliasService dataAliasService;
@Override
public Integer updateDataAlias(DataAliasUpdateDTO dto, DataExtraTypeEnum type) {
Valid.valid(dto);
DataAliasUpdateRequest update = DataAliasUpdateRequest.builder()
.userId(dto.getUserId())
.type(type.name())
.relId(dto.getRelId())
.alias(dto.getAlias())
.build();
return dataAliasService.updateDataAlias(update);
}
@Override
public String getDataAlias(Long userId, DataExtraTypeEnum type, Long relId) {
Valid.allNotNull(userId, relId);
return dataAliasService.getDataAlias(userId, type.name(), relId);
}
@Override
public Map<Long, String> getDataAlias(Long userId, DataExtraTypeEnum type) {
Valid.notNull(userId);
return dataAliasService.getDataAlias(userId, type.name());
}
@Override
@Async("asyncExecutor")
public Future<Map<Long, String>> getDataAliasAsync(Long userId, DataExtraTypeEnum type) {
Valid.notNull(userId);
return CompletableFuture.completedFuture(dataAliasService.getDataAlias(userId, type.name()));
}
}

View File

@@ -18,6 +18,8 @@ import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.stream.Collectors;
/**
@@ -83,6 +85,23 @@ public class DataExtraApiImpl implements DataExtraApi {
return dataExtraService.getExtraItemValues(request);
}
@Override
public String getExtraItemValueByCache(Long userId, DataExtraTypeEnum type, String item, Long relId) {
Valid.allNotNull(userId, type, item, relId);
return dataExtraService.getExtraItemValueByCache(userId, type.name(), item, relId);
}
@Override
public Map<Long, String> getExtraItemValuesByCache(Long userId, DataExtraTypeEnum type, String item) {
Valid.allNotNull(userId, type, item);
return dataExtraService.getExtraItemValuesByCache(userId, type.name(), item);
}
@Override
public Future<Map<Long, String>> getExtraItemValuesByCacheAsync(Long userId, DataExtraTypeEnum type, String item) {
return CompletableFuture.completedFuture(this.getExtraItemValuesByCache(userId, type, item));
}
@Override
public DataExtraDTO getExtraItem(DataExtraQueryDTO dto, DataExtraTypeEnum type) {
Valid.allNotNull(dto.getUserId(), dto.getRelId(), dto.getItem());

View File

@@ -15,9 +15,9 @@ import java.util.concurrent.TimeUnit;
*/
public interface DataExtraCacheKeyDefine {
CacheKeyDefine DATA_ALIAS = new CacheKeyBuilder()
.key("data:alias:{}:{}")
.desc("数据别名 ${userId} ${type}")
CacheKeyDefine DATA_EXTRA = new CacheKeyBuilder()
.key("data:extra:{}:{}:{}")
.desc("数据拓展信息 ${userId} ${type} ${item}")
.type(String.class)
.struct(RedisCacheStruct.HASH)
.timeout(1, TimeUnit.DAYS)

View File

@@ -1,47 +0,0 @@
package com.orion.ops.module.infra.entity.request.data;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.io.Serializable;
/**
* 数据别名 更新请求对象
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023-12-18 17:37
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(name = "DataAliasUpdateRequest", description = "数据别名 更新请求对象")
public class DataAliasUpdateRequest implements Serializable {
private static final long serialVersionUID = 1L;
@NotNull
@Schema(description = "用户id")
private Long userId;
@NotNull
@Schema(description = "数据id")
private Long relId;
@NotBlank
@Size(max = 32)
@Schema(description = "数据类型")
private String type;
@Size(max = 32)
@Schema(description = "别名")
private String alias;
}

View File

@@ -1,43 +0,0 @@
package com.orion.ops.module.infra.service;
import com.orion.ops.module.infra.entity.request.data.DataAliasUpdateRequest;
import java.util.Map;
/**
* 数据别名 服务类
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023-12-18 17:37
*/
public interface DataAliasService {
/**
* 更新数据别名
*
* @param request request
* @return effect
*/
Integer updateDataAlias(DataAliasUpdateRequest request);
/**
* 查询数据别名
*
* @param userId userId
* @param type type
* @param relId relId
* @return aliasName
*/
String getDataAlias(Long userId, String type, Long relId);
/**
* 查询数据别名
*
* @param userId userId
* @param type type
* @return relId:aliasName
*/
Map<Long, String> getDataAlias(Long userId, String type);
}

View File

@@ -64,6 +64,27 @@ public interface DataExtraService {
*/
Map<Long, String> getExtraItemValues(DataExtraQueryRequest request);
/**
* 查询额外配置项 (查询缓存)
*
* @param userId userId
* @param type type
* @param item item
* @param relId relId
* @return value
*/
String getExtraItemValueByCache(Long userId, String type, String item, Long relId);
/**
* 查询额外配置项 (查询缓存)
*
* @param userId userId
* @param type type
* @param item item
* @return relId:value
*/
Map<Long, String> getExtraItemValuesByCache(Long userId, String type, String item);
/**
* 查询额外配置
*

View File

@@ -1,82 +0,0 @@
package com.orion.ops.module.infra.service.impl;
import com.orion.lang.utils.Refs;
import com.orion.lang.utils.collect.Maps;
import com.orion.ops.framework.redis.core.utils.RedisMaps;
import com.orion.ops.framework.redis.core.utils.barrier.CacheBarriers;
import com.orion.ops.module.infra.constant.DataExtraItems;
import com.orion.ops.module.infra.define.cache.DataExtraCacheKeyDefine;
import com.orion.ops.module.infra.entity.request.data.DataAliasUpdateRequest;
import com.orion.ops.module.infra.entity.request.data.DataExtraQueryRequest;
import com.orion.ops.module.infra.entity.request.data.DataExtraSetRequest;
import com.orion.ops.module.infra.service.DataAliasService;
import com.orion.ops.module.infra.service.DataExtraService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Map;
import java.util.function.Function;
/**
* 数据别名 服务实现类
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023-12-18 17:37
*/
@Slf4j
@Service
public class DataAliasServiceImpl implements DataAliasService {
@Resource
private DataExtraService dataExtraService;
@Override
public Integer updateDataAlias(DataAliasUpdateRequest request) {
Long userId = request.getUserId();
String type = request.getType();
// 更新
DataExtraSetRequest update = new DataExtraSetRequest();
update.setUserId(userId);
update.setRelId(request.getRelId());
update.setType(type);
update.setItem(DataExtraItems.ALIAS);
update.setValue(Refs.json(request.getAlias()));
Integer effect = dataExtraService.setExtraItem(update);
// 删除缓存
RedisMaps.delete(DataExtraCacheKeyDefine.DATA_ALIAS.format(userId, type));
return effect;
}
@Override
public String getDataAlias(Long userId, String type, Long relId) {
return this.getDataAlias(userId, type).get(relId);
}
@Override
public Map<Long, String> getDataAlias(Long userId, String type) {
// 查询缓存
String key = DataExtraCacheKeyDefine.DATA_ALIAS.format(userId, type);
Map<String, String> entities = RedisMaps.entities(key);
if (Maps.isEmpty(entities)) {
// 查询数据库
DataExtraQueryRequest request = DataExtraQueryRequest.builder()
.userId(userId)
.type(type)
.item(DataExtraItems.ALIAS)
.build();
Map<Long, String> extras = dataExtraService.getExtraItemValues(request);
entities = Maps.map(extras, String::valueOf, Refs::unrefToString);
// 设置屏障 防止穿透
CacheBarriers.MAP.check(entities);
// 设置缓存
RedisMaps.putAll(key, DataExtraCacheKeyDefine.DATA_ALIAS, entities);
}
// 删除屏障
CacheBarriers.MAP.remove(entities);
// 转换
return Maps.map(entities, Long::valueOf, Function.identity());
}
}

View File

@@ -2,9 +2,14 @@ package com.orion.ops.module.infra.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.orion.lang.function.Functions;
import com.orion.lang.utils.collect.Lists;
import com.orion.lang.utils.collect.Maps;
import com.orion.ops.framework.common.constant.Const;
import com.orion.ops.framework.mybatis.core.query.ThenLambdaWrapper;
import com.orion.ops.framework.redis.core.utils.RedisMaps;
import com.orion.ops.framework.redis.core.utils.barrier.CacheBarriers;
import com.orion.ops.module.infra.dao.DataExtraDAO;
import com.orion.ops.module.infra.define.cache.DataExtraCacheKeyDefine;
import com.orion.ops.module.infra.entity.domain.DataExtraDO;
import com.orion.ops.module.infra.entity.request.data.DataExtraQueryRequest;
import com.orion.ops.module.infra.entity.request.data.DataExtraSetRequest;
@@ -15,6 +20,7 @@ import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
@@ -62,15 +68,29 @@ public class DataExtraServiceImpl implements DataExtraService {
insert.setItem(request.getItem());
insert.setValue(request.getValue());
dataExtraDAO.insert(insert);
// 删除缓存
RedisMaps.delete(DataExtraCacheKeyDefine.DATA_EXTRA.format(request.getUserId(), request.getType(), request.getItem()));
return insert.getId();
}
@Override
public Integer updateExtraValue(Long id, String value) {
// 查询数据
DataExtraDO data = this.getCacheSelectWrapper()
.eq(DataExtraDO::getId, id)
.then()
.get();
if (data == null) {
return Const.N_0;
}
DataExtraDO update = new DataExtraDO();
update.setId(id);
update.setValue(value);
return dataExtraDAO.updateById(update);
// 更新
int effect = dataExtraDAO.updateById(update);
// 删除缓存
RedisMaps.delete(DataExtraCacheKeyDefine.DATA_EXTRA.format(data.getUserId(), data.getType(), data.getItem()));
return effect;
}
@Override
@@ -78,8 +98,16 @@ public class DataExtraServiceImpl implements DataExtraService {
if (Maps.isEmpty(map)) {
return;
}
// 查询数据
List<DataExtraDO> list = this.getCacheSelectWrapper()
.in(DataExtraDO::getId, map.keySet())
.then()
.list();
if (list.isEmpty()) {
return;
}
// 批量更新
List<DataExtraDO> list = map.entrySet()
List<DataExtraDO> update = map.entrySet()
.stream()
.map(s -> {
DataExtraDO extra = new DataExtraDO();
@@ -87,7 +115,9 @@ public class DataExtraServiceImpl implements DataExtraService {
extra.setValue(s.getValue());
return extra;
}).collect(Collectors.toList());
dataExtraDAO.updateBatch(list);
dataExtraDAO.updateBatch(update);
// 删除缓存
this.deleteCache(list);
}
@Override
@@ -110,6 +140,36 @@ public class DataExtraServiceImpl implements DataExtraService {
);
}
@Override
public String getExtraItemValueByCache(Long userId, String type, String item, Long relId) {
return this.getExtraItemValuesByCache(userId, type, item).get(relId);
}
@Override
public Map<Long, String> getExtraItemValuesByCache(Long userId, String type, String item) {
// 查询缓存
String key = DataExtraCacheKeyDefine.DATA_EXTRA.format(userId, type, item);
Map<String, String> entities = RedisMaps.entities(key);
if (Maps.isEmpty(entities)) {
// 查询数据库
DataExtraQueryRequest request = DataExtraQueryRequest.builder()
.userId(userId)
.type(type)
.item(item)
.build();
Map<Long, String> extras = this.getExtraItemValues(request);
entities = Maps.map(extras, String::valueOf, String::valueOf);
// 设置屏障 防止穿透
CacheBarriers.MAP.check(entities);
// 设置缓存
RedisMaps.putAll(key, DataExtraCacheKeyDefine.DATA_EXTRA, entities);
}
// 删除屏障
CacheBarriers.MAP.remove(entities);
// 转换
return Maps.map(entities, Long::valueOf, Function.identity());
}
@Override
public DataExtraDO getExtraItem(DataExtraQueryRequest request) {
return dataExtraDAO.of()
@@ -126,12 +186,62 @@ public class DataExtraServiceImpl implements DataExtraService {
@Override
public Integer deleteByUserId(Long userId) {
return dataExtraDAO.deleteByUserId(userId);
List<DataExtraDO> list = this.getCacheSelectWrapper()
.eq(DataExtraDO::getUserId, userId)
.then()
.list();
if (list.isEmpty()) {
return Const.N_0;
}
// 删除数据
int effect = dataExtraDAO.deleteByUserId(userId);
// 删除缓存
this.deleteCache(list);
return effect;
}
@Override
public Integer deleteByRelId(String type, Long relId) {
return dataExtraDAO.deleteByRelId(type, relId);
List<DataExtraDO> list = this.getCacheSelectWrapper()
.eq(DataExtraDO::getType, type)
.eq(DataExtraDO::getRelId, relId)
.then()
.list();
if (list.isEmpty()) {
return Const.N_0;
}
// 删除数据
int effect = dataExtraDAO.deleteByRelId(type, relId);
// 删除缓存
this.deleteCache(list);
return effect;
}
/**
* 获取查询缓存参数 wrapper 不查询 longtext 加速查询
*
* @return wrapper
*/
private ThenLambdaWrapper<DataExtraDO> getCacheSelectWrapper() {
return dataExtraDAO.of()
.createWrapper()
.select(DataExtraDO::getId, DataExtraDO::getUserId, DataExtraDO::getType, DataExtraDO::getItem);
}
/**
* 删除缓存
*
* @param list list
*/
private void deleteCache(List<DataExtraDO> list) {
if (Lists.isEmpty(list)) {
return;
}
List<String> keys = list.stream()
.map(s -> DataExtraCacheKeyDefine.DATA_EXTRA.format(s.getUserId(), s.getType(), s.getItem()))
.distinct()
.collect(Collectors.toList());
RedisMaps.delete(keys);
}
/**

View File

@@ -1,4 +1,4 @@
VITE_API_BASE_URL= 'http://127.0.0.1:9200/orion/api'
VITE_WS_BASE_URL= 'ws://127.0.0.1:9200/orion/keep-alive'
VITE_APP_VERSION= '1.0.0-beta.1'
VITE_APP_VERSION= '1.0.0'
VITE_SFTP_PREVIEW_MB= 2

View File

@@ -1,4 +1,4 @@
VITE_API_BASE_URL= '/orion/api'
VITE_WS_BASE_URL= '/orion/keep-alive'
VITE_APP_VERSION= '1.0.0-beta.1'
VITE_APP_VERSION= '1.0.0'
VITE_SFTP_PREVIEW_MB= 2

View File

@@ -1,7 +1,7 @@
{
"name": "orion-ops-pro-ui",
"description": "Orion Ops Pro for Vue",
"version": "1.0.0-beta.1",
"version": "1.0.0",
"private": true,
"author": "Jiahang Li",
"license": "Apache 2.0",

View File

@@ -36,7 +36,7 @@ export interface HostQueryRequest extends Pagination {
/**
* 主机查询响应
*/
export interface HostQueryResponse extends TableData {
export interface HostQueryResponse extends TableData, HostQueryResponseExtra {
id: number;
name: string;
code: string;
@@ -47,9 +47,15 @@ export interface HostQueryResponse extends TableData {
updater: string;
favorite: boolean;
alias: string;
color: string;
tags: Array<{ id: number, name: string }>;
groupIdList: Array<number>;
}
/**
* 主机操作拓展
*/
export interface HostQueryResponseExtra {
editable: boolean;
loading: boolean;
modCount: number;

View File

@@ -160,7 +160,7 @@
</li>
<!-- 用户信息 -->
<li>
<a-dropdown trigger="click">
<a-dropdown trigger="click" position="br">
<!-- 头像 -->
<a-avatar draggable="false"
:size="32"
@@ -256,10 +256,10 @@
const localeRef = ref();
// 打开应用设置
const openAppSetting = inject<() => void>(openAppSettingKey);
const openAppSetting = inject(openAppSettingKey) as () => void;
// 注入收缩菜单
const toggleDrawerMenu = inject<() => void>(toggleDrawerMenuKey);
const toggleDrawerMenu = inject(toggleDrawerMenuKey) as () => void;
// 切换主题
const handleToggleTheme = () => {

View File

@@ -157,6 +157,7 @@ export default defineStore('terminal', {
title: `(${nextSeq}) ${record.alias || record.name}`,
hostId: record.id,
address: record.address,
color: record.color,
icon: session.icon,
type: session.type
});

View File

@@ -7,7 +7,7 @@ export default {
'login.form.userName.placeholder': '用户名',
'login.form.password.placeholder': '密码',
'login.form.login': '登录',
'login.banner.slogan1': '开箱即用的运维平台',
'login.banner.slogan1': '开箱即用的一站式智能运维平台',
'login.banner.subSlogan1': '一站式操作 智能运维 让运维变得更简单',
'login.banner.slogan2': '内置权限角色管理',
'login.banner.subSlogan2': '让每一次操作都安全可控可追溯',

View File

@@ -170,7 +170,6 @@
margin: 0 !important;
padding: 0 !important;
color: var(--color-header-text-1);
background: var(--color-bg-header-tabs);
&:hover {
color: var(--color-header-text-2);
@@ -232,11 +231,10 @@
}
:deep(.arco-tabs-tab-active) {
background: var(--color-bg-header-tabs-active);
color: var(--color-header-text-2) !important;
.arco-tabs-tab-title {
background: var(--color-bg-header-tabs-active);
background: var(--color-bg-header-tabs-active) !important;
}
&:hover::after {

View File

@@ -21,7 +21,8 @@
:key="tab.key">
<!-- 标题 -->
<template #title>
<span class="tab-title-wrapper">
<span class="tab-title-wrapper"
:style="{ 'border-bottom': `2px ${tab.color || 'transparent'} solid` }">
<span class="tab-title-icon">
<component :is="tab.icon" />
</span>
@@ -101,8 +102,12 @@
}
.tab-title-wrapper {
width: 100%;
height: 100%;
display: flex;
align-items: center;
padding: 11px 18px 9px 14px;
margin: 0 2px;
.tab-title-icon {
font-size: 16px;
@@ -214,13 +219,11 @@
}
.arco-tabs-tab-title {
padding: 11px 18px 7px 14px;
background: var(--color-bg-panel-tabs);
font-size: 14px;
height: var(--panel-nav-height);
display: flex;
align-items: center;
border-bottom: 4px transparent solid;
&::before {
display: none;
@@ -253,7 +256,7 @@
}
:deep(.arco-tabs-tab-active) {
background: var(--color-bg-panel-tabs-active);
background: var(--color-bg-panel-tabs-active) !important;
color: var(--color-panel-text-2) !important;
.arco-tabs-tab-title {

View File

@@ -137,13 +137,13 @@
</div>
</div>
</a-tooltip>
<!-- 连接设置 -->
<!-- 主机设置 -->
<a-tooltip position="top"
:mini="true"
:auto-fix-position="false"
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="连接设置">
content="主机设置">
<div class="terminal-sidebar-icon-wrapper">
<div class="terminal-sidebar-icon" @click="openSetting(item)">
<icon-settings />
@@ -185,7 +185,7 @@
import { dataColor } from '@/utils';
import { tagColor } from '@/views/asset/host-list/types/const';
import { updateHostAlias } from '@/api/asset/host-extra';
import { openSshSettingModalKey, PanelSessionType } from '../../types/terminal.const';
import { openSettingModalKey, PanelSessionType } from '../../types/terminal.const';
import { useTerminalStore } from '@/store';
const props = defineProps<{
@@ -231,7 +231,7 @@
};
// 打开配置
const openSetting = inject(openSshSettingModalKey) as (record: HostQueryResponse) => void;
const openSetting = inject(openSettingModalKey) as (record: HostQueryResponse) => void;
// 设置收藏
const setFavorite = async (item: HostQueryResponse) => {

View File

@@ -21,8 +21,8 @@
class="list-view-container"
:hostList="hostList"
empty-value="暂无连接记录, 快去体验吧!" />
<!-- 修改主机设置模态框 -->
<ssh-extra-modal ref="sshModal" />
<!-- 主机设置模态框 -->
<host-setting-modal ref="settingModal" />
</div>
</template>
@@ -34,12 +34,12 @@
<script lang="ts" setup>
import { onMounted, provide, ref, watch } from 'vue';
import { NewConnectionType, openSshSettingModalKey } from '../../types/terminal.const';
import { NewConnectionType, openSettingModalKey } from '../../types/terminal.const';
import { AuthorizedHostQueryResponse } from '@/api/asset/asset-authorized-data';
import { HostQueryResponse } from '@/api/asset/host';
import HostGroupView from './host-group-view.vue';
import HostListView from './host-list-view.vue';
import SshExtraModal from '../setting/ssh-extra-modal.vue';
import HostSettingModal from '../setting/extra/host-setting-modal.vue';
const props = defineProps<{
hosts: AuthorizedHostQueryResponse,
@@ -54,11 +54,11 @@
? props.hosts.groupTree[0].key
: 0
);
const sshModal = ref();
const settingModal = ref();
// 暴露打开 ssh 配置模态框
provide(openSshSettingModalKey, (record: any) => {
sshModal.value?.open(record);
provide(openSettingModalKey, (record: any) => {
settingModal.value?.open(record);
});
// 主机数据处理

View File

@@ -0,0 +1,136 @@
<template>
<div>
<a-alert class="mb16">刷新页面后生效</a-alert>
<!-- 颜色选择 -->
<div class="color-setting-container">
<!-- 透明色 -->
<div class="color-wrapper"
:style="{ '--color': 'transparent' }"
@click="clickColor('')">
<div class="color-item">
<span class="color-item-cancel">
<icon-stop />
</span>
</div>
</div>
<!-- 其他颜色 -->
<template v-for="color in toOptions(terminalTabColorKey)">
<div class="color-wrapper"
:class="[formModel.color === color.value ? 'selected-color' : '']"
:style="{ '--color': `${color.value}` }"
@click="clickColor(color.value as string)">
<div class="color-item">
<div class="color-item-dot" />
</div>
</div>
</template>
</div>
</div>
</template>
<script lang="ts">
export default {
name: 'colorSettingForm'
};
</script>
<script lang="ts" setup>
import type { ColorExtraSettingModel } from '../../../types/terminal.type';
import { onMounted, ref } from 'vue';
import { terminalTabColorKey } from '../../../types/terminal.const';
import { getHostExtraItem } from '@/api/asset/host-extra';
import { useDictStore } from '@/store';
const props = defineProps<{
hostId: number,
item: string
}>();
const { toOptions } = useDictStore();
const formModel = ref<ColorExtraSettingModel>({
color: ''
});
// 渲染表单
const renderForm = async () => {
const { data } = await getHostExtraItem<ColorExtraSettingModel>({ hostId: props.hostId, item: props.item });
formModel.value = data;
};
// 设置颜色
const clickColor = (color: string) => {
formModel.value.color = color;
};
// 获取值
const getValue = async () => {
return JSON.stringify(formModel.value);
};
defineExpose({ getValue });
onMounted(renderForm);
</script>
<style lang="less" scoped>
.color-setting-container {
display: flex;
}
.color-wrapper {
margin-right: 8px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
transition: all .3s ease-in-out;
cursor: pointer;
border-radius: 4px;
&:hover {
background: var(--color-fill-3);
}
.color-item {
width: 20px;
height: 20px;
border-radius: 100%;
display: flex;
align-items: center;
justify-content: center;
&-dot {
width: 100%;
height: 100%;
border-radius: 100%;
background: var(--color);
}
&-cancel {
color: var(--color-content-text-1);
font-size: 22px;
line-height: 1;
}
}
}
.color-wrapper.selected-color {
.color-item {
border: 2px solid var(--color);
&-dot {
width: 12px;
height: 12px;
box-sizing: border-box;
background: var(--color);
padding: 4px;
position: relative;
}
}
}
</style>

View File

@@ -0,0 +1,120 @@
<template>
<a-modal v-model:visible="visible"
title-align="start"
:title="title"
:top="180"
:width="600"
:align-center="false"
:draggable="true"
:mask-closable="false"
:unmount-on-close="true"
ok-text="保存当前页"
cancel-text="关闭"
:ok-button-props="{ disabled: loading }"
:cancel-button-props="{ disabled: loading }"
:on-before-ok="handlerOk"
@close="handleClose">
<a-spin class="full" :loading="loading">
<a-tabs v-model:active-key="activeItem"
position="left"
type="rounded"
:lazy-load="true">
<!-- SSH 配置 -->
<a-tab-pane :key="ExtraSettingItems.SSH" title="SSH 配置">
<ssh-setting-form ref="sshForm"
:host-id="hostId as number"
:item="ExtraSettingItems.SSH" />
</a-tab-pane>
<!-- 标签颜色 -->
<a-tab-pane :key="ExtraSettingItems.COLOR" title="标签颜色">
<color-setting-form ref="colorForm"
:host-id="hostId as number"
:item="ExtraSettingItems.COLOR" />
</a-tab-pane>
</a-tabs>
</a-spin>
</a-modal>
</template>
<script lang="ts">
export default {
name: 'hostSettingModal'
};
</script>
<script lang="ts" setup>
import type { HostQueryResponse } from '@/api/asset/host';
import { ref } from 'vue';
import useLoading from '@/hooks/loading';
import useVisible from '@/hooks/visible';
import { ExtraSettingItems } from '../../../types/terminal.const';
import { updateHostExtra } from '@/api/asset/host-extra';
import { Message } from '@arco-design/web-vue';
import SshSettingForm from './ssh-setting-form.vue';
import ColorSettingForm from './color-setting-form.vue';
const { visible, setVisible } = useVisible();
const { loading, setLoading } = useLoading();
const activeItem = ref<string>(ExtraSettingItems.SSH);
const title = ref<string>();
const hostId = ref<number>();
const sshForm = ref();
const colorForm = ref();
// 打开配置
const open = (record: HostQueryResponse) => {
hostId.value = record.id;
title.value = record.alias || `${record.name} (${record.code})`;
setVisible(true);
};
defineExpose({ open });
// 确定
const handlerOk = async () => {
setLoading(true);
try {
let value;
if (activeItem.value === ExtraSettingItems.SSH) {
// SSH 配置
value = await sshForm.value.getValue();
} else if (activeItem.value === ExtraSettingItems.COLOR) {
// 颜色配置
value = await colorForm.value.getValue();
}
if (!value) {
return false;
}
// 保存
await updateHostExtra({
hostId: hostId.value,
item: activeItem.value,
extra: value as string
});
Message.success('保存成功');
} catch (e) {
} finally {
setLoading(false);
}
return false;
};
// 关闭
const handleClose = () => {
handlerClear();
};
// 清空
const handlerClear = () => {
setLoading(false);
};
</script>
<style lang="less" scoped>
:deep(.arco-tabs-pane) {
border-left: 1px var(--color-neutral-3) solid;
padding-left: 16px;
}
</style>

View File

@@ -0,0 +1,91 @@
<template>
<a-form :model="formModel"
ref="formRef"
label-align="right"
:style="{ width: '460px' }"
:label-col-props="{ span: 6 }"
:wrapper-col-props="{ span: 18 }"
:rules="{}">
<!-- 验证方式 -->
<a-form-item field="authType" label="验证方式">
<a-radio-group type="button"
v-model="formModel.authType"
:options="toRadioOptions(extraSshAuthTypeKey)" />
</a-form-item>
<!-- 用户名 -->
<a-form-item v-if="formModel.authType === ExtraSshAuthType.CUSTOM_KEY"
field="username"
label="用户名">
<a-input v-model="formModel.username" placeholder="请输入用户名" />
</a-form-item>
<!-- 主机秘钥 -->
<a-form-item v-if="formModel.authType === ExtraSshAuthType.CUSTOM_KEY"
field="keyId"
label="主机秘钥"
:rules="{ required: true, message: '请选择主机秘钥' }">
<host-key-selector v-model="formModel.keyId"
:authorized="true" />
</a-form-item>
<!-- 主机身份 -->
<a-form-item v-if="formModel.authType === ExtraSshAuthType.CUSTOM_IDENTITY"
field="identityId"
label="主机身份"
:rules="{ required: true, message: '请选择主机身份' }">
<host-identity-selector v-model="formModel.identityId"
:authorized="true" />
</a-form-item>
</a-form>
</template>
<script lang="ts">
export default {
name: 'sshSettingForm'
};
</script>
<script lang="ts" setup>
import type { SshExtraSettingModel } from '../../../types/terminal.type';
import { onMounted, ref } from 'vue';
import { getHostExtraItem } from '@/api/asset/host-extra';
import { ExtraSshAuthType, extraSshAuthTypeKey } from '../../../types/terminal.const';
import { useDictStore } from '@/store';
import HostKeySelector from '@/components/asset/host-key/host-key-selector.vue';
import HostIdentitySelector from '@/components/asset/host-identity/host-identity-selector.vue';
const props = defineProps<{
hostId: number,
item: string
}>();
const { toRadioOptions } = useDictStore();
const formRef = ref();
const formModel = ref<SshExtraSettingModel>({
authType: ExtraSshAuthType.DEFAULT
});
// 渲染表单
const renderForm = async () => {
const { data } = await getHostExtraItem<SshExtraSettingModel>({ hostId: props.hostId, item: props.item });
formModel.value = data;
};
// 获取值
const getValue = async () => {
// 验证参数
const error = await formRef.value.validate();
if (error) {
return false;
}
return JSON.stringify(formModel.value);
};
defineExpose({ getValue });
onMounted(renderForm);
</script>
<style lang="less" scoped>
</style>

View File

@@ -1,146 +0,0 @@
<template>
<a-modal v-model:visible="visible"
body-class="modal-form"
title-align="start"
:title="title"
:top="80"
:align-center="false"
:draggable="true"
:mask-closable="false"
:unmount-on-close="true"
ok-text="保存"
:ok-button-props="{ disabled: loading }"
:cancel-button-props="{ disabled: loading }"
:on-before-ok="handlerOk"
@close="handleClose">
<a-spin class="full" :loading="loading">
<a-form :model="formModel"
ref="formRef"
label-align="right"
:style="{ width: '460px' }"
:label-col-props="{ span: 6 }"
:wrapper-col-props="{ span: 18 }"
:rules="{}">
<!-- 验证方式 -->
<a-form-item field="authType" label="验证方式">
<a-radio-group type="button"
v-model="formModel.authType"
:options="toRadioOptions(extraSshAuthTypeKey)" />
</a-form-item>
<!-- 用户名 -->
<a-form-item v-if="formModel.authType === ExtraSshAuthType.CUSTOM_KEY"
field="username"
label="用户名">
<a-input v-model="formModel.username" placeholder="请输入用户名" />
</a-form-item>
<!-- 主机秘钥 -->
<a-form-item v-if="formModel.authType === ExtraSshAuthType.CUSTOM_KEY"
field="keyId"
label="主机秘钥"
:rules="{ required: true, message: '请选择主机秘钥' }">
<host-key-selector v-model="formModel.keyId"
:authorized="true" />
</a-form-item>
<!-- 主机身份 -->
<a-form-item v-if="formModel.authType === ExtraSshAuthType.CUSTOM_IDENTITY"
field="identityId"
label="主机身份"
:rules="{ required: true, message: '请选择主机身份' }">
<host-identity-selector v-model="formModel.identityId"
:authorized="true" />
</a-form-item>
</a-form>
</a-spin>
</a-modal>
</template>
<script lang="ts">
export default {
name: 'sshExtraModal'
};
</script>
<script lang="ts" setup>
import type { HostQueryResponse } from '@/api/asset/host';
import type { SshExtraModel } from '../../types/terminal.type';
import { ref } from 'vue';
import useLoading from '@/hooks/loading';
import useVisible from '@/hooks/visible';
import { Message } from '@arco-design/web-vue';
import { getHostExtraItem, updateHostExtra } from '@/api/asset/host-extra';
import { ExtraSshAuthType, extraSshAuthTypeKey } from '../../types/terminal.const';
import { useDictStore } from '@/store';
import HostIdentitySelector from '@/components/asset/host-identity/host-identity-selector.vue';
import HostKeySelector from '@/components/asset/host-key/host-key-selector.vue';
const { toRadioOptions } = useDictStore();
const { visible, setVisible } = useVisible();
const { loading, setLoading } = useLoading();
const title = ref<string>();
const hostId = ref<number>();
const formRef = ref();
const formModel = ref<SshExtraModel>({});
// 打开配置
const open = (record: HostQueryResponse) => {
hostId.value = record.id;
title.value = record.alias || `${record.name} (${record.code})`;
renderForm();
setVisible(true);
};
defineExpose({ open });
// 渲染表单
const renderForm = async () => {
try {
setLoading(true);
const { data } = await getHostExtraItem<SshExtraModel>({ hostId: hostId.value, item: 'ssh' });
formModel.value = data;
} catch (e) {
} finally {
setLoading(false);
}
};
// 确定
const handlerOk = async () => {
setLoading(true);
try {
// 验证参数
const error = await formRef.value.validate();
if (error) {
return false;
}
// 修改
await updateHostExtra({
hostId: hostId.value,
item: 'ssh',
extra: JSON.stringify(formModel.value)
});
Message.success('保存成功');
// 清空
handlerClear();
} catch (e) {
return false;
} finally {
setLoading(false);
}
};
// 关闭
const handleClose = () => {
handlerClear();
};
// 清空
const handlerClear = () => {
setLoading(false);
};
</script>
<style lang="less" scoped>
</style>

View File

@@ -42,6 +42,12 @@ export const NewConnectionType = {
LATEST: 'latest'
};
// 主机额外配置项
export const ExtraSettingItems = {
SSH: 'ssh',
COLOR: 'color'
};
// 主机额外配置 ssh 认证方式
export const ExtraSshAuthType = {
// 使用默认认证方式
@@ -318,8 +324,8 @@ export const TransferReceiverType = {
DOWNLOAD_ERROR: 'downloadError',
};
// 打开 sshSettingModal key
export const openSshSettingModalKey = Symbol();
// 打开 settingModal key
export const openSettingModalKey = Symbol();
// 打开 sftpCreateModal key
export const openSftpCreateModalKey = Symbol();
@@ -360,10 +366,14 @@ export const connectStatusKey = 'terminalConnectStatus';
// 终端类型
export const terminalEmulationTypeKey = 'terminalEmulationType';
// 终端标签颜色
export const terminalTabColorKey = 'terminalTabColor';
// 加载的字典值
export const dictKeys = [
fontFamilyKey, fontSizeKey,
fontWeightKey, cursorStyleKey,
newConnectionTypeKey, extraSshAuthTypeKey,
connectStatusKey, terminalEmulationTypeKey
connectStatusKey, terminalEmulationTypeKey,
terminalTabColorKey
];

View File

@@ -23,6 +23,7 @@ export interface TerminalPanelTabItem extends TerminalTabItem {
hostId: number;
address: string;
type: string;
color?: string;
}
// sidebar 操作类型
@@ -59,13 +60,18 @@ export interface ShortcutKeyItem {
}
// ssh 额外配置
export interface SshExtraModel {
export interface SshExtraSettingModel {
authType?: string;
username?: string;
keyId?: number;
identityId?: number;
}
// 颜色 额外配置
export interface ColorExtraSettingModel {
color: string;
}
// session tab
export interface PanelSessionTab {
type: string;

View File

@@ -22,7 +22,7 @@
</modules>
<properties>
<revision>1.0.0-beta.1</revision>
<revision>1.0.0</revision>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<maven.surefire.plugin.version>3.0.0-M5</maven.surefire.plugin.version>

View File

@@ -108,6 +108,7 @@ INSERT INTO `dict_key` VALUES (27, 'hostConnectType', 'STRING', '[]', '主机连
INSERT INTO `dict_key` VALUES (28, 'hostConnectStatus', 'STRING', '[{\"name\": \"color\", \"type\": \"COLOR\"}]', '主机连接状态', '2023-12-26 23:23:51', '2023-12-26 23:28:15', '1', '1', 0);
INSERT INTO `dict_key` VALUES (29, 'terminalConnectStatus', 'INTEGER', '[{\"name\": \"status\", \"type\": \"STRING\"}]', '终端连接状态', '2024-01-09 00:32:00', '2024-01-09 00:32:16', '1', '1', 0);
INSERT INTO `dict_key` VALUES (31, 'terminalEmulationType', 'STRING', '[]', '伪终端类型', '2024-01-11 23:35:01', '2024-01-11 23:35:01', '1', '1', 0);
INSERT INTO `dict_key` VALUES (32, 'terminalTabColor', 'COLOR', '[]', '终端标签页颜色', '2024-03-01 15:01:44', '2024-03-01 15:01:44', '1', '1', 0);
-- 字典值
INSERT INTO `dict_value` VALUES (3, 4, 'systemMenuType', '1', '父菜单', '{}', 10, '2023-10-26 15:58:59', '2023-10-26 15:58:59', '1', '1', 0);
@@ -258,3 +259,13 @@ INSERT INTO `dict_value` VALUES (199, 2, 'operatorLogType', 'host-terminal:sftp-
INSERT INTO `dict_value` VALUES (200, 2, 'operatorLogType', 'host-terminal:sftp-set-content', '修改文件内容', '{}', 80, '2024-02-23 17:54:37', '2024-02-23 17:54:37', '1', '1', 0);
INSERT INTO `dict_value` VALUES (201, 2, 'operatorLogType', 'host-terminal:sftp-upload', '上传文件', '{}', 90, '2024-02-23 17:54:52', '2024-02-23 17:54:52', '1', '1', 0);
INSERT INTO `dict_value` VALUES (202, 2, 'operatorLogType', 'host-terminal:sftp-download', '下载文件', '{}', 100, '2024-02-23 17:55:03', '2024-02-23 17:55:03', '1', '1', 0);
INSERT INTO `dict_value` VALUES (203, 32, 'terminalTabColor', 'rgb(var(--red-6))', '红色', '{}', 10, '2024-03-01 15:07:41', '2024-03-01 15:07:41', '1', '1', 0);
INSERT INTO `dict_value` VALUES (204, 32, 'terminalTabColor', 'rgb(var(--orange-6))', '橙色', '{}', 20, '2024-03-01 15:07:55', '2024-03-01 15:07:55', '1', '1', 0);
INSERT INTO `dict_value` VALUES (205, 32, 'terminalTabColor', 'rgb(var(--yellow-6))', '黄色', '{}', 30, '2024-03-01 15:08:13', '2024-03-01 15:08:13', '1', '1', 0);
INSERT INTO `dict_value` VALUES (206, 32, 'terminalTabColor', 'rgb(var(--green-6))', '绿色', '{}', 40, '2024-03-01 15:08:23', '2024-03-01 15:08:23', '1', '1', 0);
INSERT INTO `dict_value` VALUES (207, 32, 'terminalTabColor', 'rgb(var(--cyan-6))', '青色', '{}', 50, '2024-03-01 15:08:46', '2024-03-01 15:08:46', '1', '1', 0);
INSERT INTO `dict_value` VALUES (208, 32, 'terminalTabColor', 'rgb(var(--blue-6))', '浅蓝', '{}', 60, '2024-03-01 15:11:01', '2024-03-01 15:11:01', '1', '1', 0);
INSERT INTO `dict_value` VALUES (209, 32, 'terminalTabColor', 'rgb(var(--arcoblue-6))', '蓝色', '{}', 70, '2024-03-01 15:11:11', '2024-03-01 15:11:11', '1', '1', 0);
INSERT INTO `dict_value` VALUES (210, 32, 'terminalTabColor', 'rgb(var(--purple-6))', '紫色', '{}', 80, '2024-03-01 15:11:20', '2024-03-01 15:11:20', '1', '1', 0);
INSERT INTO `dict_value` VALUES (211, 32, 'terminalTabColor', 'rgb(var(--pinkpurple-6))', '粉紫', '{}', 90, '2024-03-01 15:11:41', '2024-03-01 15:11:41', '1', '1', 0);
INSERT INTO `dict_value` VALUES (213, 32, 'terminalTabColor', 'rgb(var(--gray-6))', '灰色', '{}', 100, '2024-03-01 15:12:01', '2024-03-01 15:39:34', '1', '1', 0);