feat 资产总览统计

This commit is contained in:
bwcx_jzy 2024-01-11 20:43:23 +08:00
parent f787c17f31
commit 7994b3b5b9
No known key found for this signature in database
GPG Key ID: 5E48E9372088B9E5
13 changed files with 393 additions and 66 deletions

View File

@ -2,6 +2,10 @@
### 2.11.0.12-beta
### 🐣 新增功能
1. 【server】新增 资产总览统计
### 🐞 解决BUG、优化功能
1. 【server】修复 mysql 存储使用游标查询报错(不使用游标)(感谢@🇩)

View File

@ -340,9 +340,11 @@ public class JpomApplicationEvent implements ApplicationListener<ApplicationEven
//
File file = FileUtil.file(JpomApplication.getInstance().getDataPath(), Const.REMOTE_VERSION);
SystemUtil.set("JPOM_REMOTE_VERSION_CACHE_FILE", file.getAbsolutePath());
SystemUtil.set("JPOM_IS_DEBUG", String.valueOf(JpomManifest.getInstance().isDebug()));
SystemUtil.set("JPOM_TYPE", JpomManifest.getInstance().getType().name());
SystemUtil.set("JPOM_VERSION", JpomManifest.getInstance().getVersion());
JpomManifest jpomManifest = JpomManifest.getInstance();
SystemUtil.set("JPOM_IS_DEBUG", String.valueOf(jpomManifest.isDebug()));
SystemUtil.set("JPOM_TYPE", jpomManifest.getType().name());
SystemUtil.set("JPOM_VERSION", jpomManifest.getVersion());
SystemUtil.set("JPOM_INSTALL_ID", jpomManifest.getInstallId());
// 检查目录权限
this.checkPath();
this.install();

View File

@ -22,12 +22,19 @@
*/
package org.dromara.jpom.controller;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.db.Entity;
import cn.keepbx.jpom.IJsonMessage;
import cn.keepbx.jpom.model.JsonMessage;
import lombok.extern.slf4j.Slf4j;
import org.dromara.jpom.func.assets.server.MachineDockerServer;
import org.dromara.jpom.func.assets.server.MachineNodeServer;
import org.dromara.jpom.func.assets.server.MachineSshServer;
import org.dromara.jpom.func.files.service.FileStorageService;
import org.dromara.jpom.func.files.service.StaticFileStorageService;
import org.dromara.jpom.func.system.service.ClusterInfoService;
import org.dromara.jpom.model.user.UserModel;
import org.dromara.jpom.permission.SystemPermission;
import org.dromara.jpom.service.docker.DockerInfoService;
import org.dromara.jpom.service.docker.DockerSwarmInfoService;
import org.dromara.jpom.service.node.NodeService;
@ -37,6 +44,8 @@ import org.dromara.jpom.service.node.ssh.SshCommandService;
import org.dromara.jpom.service.node.ssh.SshService;
import org.dromara.jpom.service.outgiving.OutGivingServer;
import org.dromara.jpom.service.script.ScriptServer;
import org.dromara.jpom.service.system.WorkspaceService;
import org.dromara.jpom.service.user.UserService;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@ -44,6 +53,7 @@ import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
@ -66,6 +76,12 @@ public class DataStatController {
private final FileStorageService fileStorageService;
private final StaticFileStorageService staticFileStorageService;
private final DockerSwarmInfoService dockerSwarmInfoService;
private final UserService userService;
private final WorkspaceService workspaceService;
private final ClusterInfoService clusterInfoService;
private final MachineNodeServer machineNodeServer;
private final MachineSshServer machineSshServer;
private final MachineDockerServer machineDockerServer;
public DataStatController(NodeService nodeService,
ProjectInfoCacheService projectInfoCacheService,
@ -77,7 +93,13 @@ public class DataStatController {
DockerInfoService dockerInfoService,
FileStorageService fileStorageService,
StaticFileStorageService staticFileStorageService,
DockerSwarmInfoService dockerSwarmInfoService) {
DockerSwarmInfoService dockerSwarmInfoService,
UserService userService,
WorkspaceService workspaceService,
ClusterInfoService clusterInfoService,
MachineNodeServer machineNodeServer,
MachineSshServer machineSshServer,
MachineDockerServer machineDockerServer) {
this.nodeService = nodeService;
this.projectInfoCacheService = projectInfoCacheService;
this.nodeScriptServer = nodeScriptServer;
@ -89,6 +111,12 @@ public class DataStatController {
this.fileStorageService = fileStorageService;
this.staticFileStorageService = staticFileStorageService;
this.dockerSwarmInfoService = dockerSwarmInfoService;
this.userService = userService;
this.workspaceService = workspaceService;
this.clusterInfoService = clusterInfoService;
this.machineNodeServer = machineNodeServer;
this.machineSshServer = machineSshServer;
this.machineDockerServer = machineDockerServer;
}
@RequestMapping(value = "workspace", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@ -110,4 +138,51 @@ public class DataStatController {
//map.put("staticFileCount", staticFileStorageService.count(entity));
return JsonMessage.success("", map);
}
@RequestMapping(value = "system", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@SystemPermission
public IJsonMessage<Map<String, Object>> system(HttpServletRequest request) {
Map<String, Object> map = new HashMap<>(10);
{
long count = userService.count();
map.put("userCount", count);
{
UserModel userModel = new UserModel();
userModel.setSystemUser(1);
long systemUserCount = userService.count(userModel);
map.put("systemUserCount", systemUserCount);
}
{
UserModel userModel = new UserModel();
userModel.setStatus(0);
long disableUserCount = userService.count(userModel);
map.put("disableUserCount", disableUserCount);
}
String sql = "select count(1) from " + userService.getTableName() + " where twoFactorAuthKey is null or twoFactorAuthKey=''";
Number closeTwoFactorAuth = ObjectUtil.defaultIfNull(userService.queryNumber(sql), 0);
map.put("openTwoFactorAuth", count - closeTwoFactorAuth.intValue());
}
{
long workspaceCount = workspaceService.count();
long clusterCount = clusterInfoService.count();
map.put("workspaceCount", workspaceCount);
map.put("clusterCount", clusterCount);
}
{
String sql = "select status,count(1) as count from " + machineDockerServer.getTableName() + " group by status";
List<Entity> query = machineDockerServer.query(sql);
map.put("dockerStat", query);
}
{
String sql = "select status,count(1) as count from " + machineSshServer.getTableName() + " group by status";
List<Entity> query = machineSshServer.query(sql);
map.put("sshStat", query);
}
{
String sql = "select status,count(1) as count from " + machineNodeServer.getTableName() + " group by status";
List<Entity> query = machineNodeServer.query(sql);
map.put("nodeStat", query);
}
return JsonMessage.success("", map);
}
}

View File

@ -122,7 +122,8 @@ public class CacheManageController extends BaseServerController implements ICach
map.put("timeZoneId", TimeZone.getDefault().getID());
map.put("errorWorkspace", dataInitEvent.getErrorWorkspaceTable());
map.put("clusterId", clusterConfig.getId());
map.put("installId", JpomManifest.getInstance().getInstallId());
JpomManifest jpomManifest = JpomManifest.getInstance();
map.put("installId", jpomManifest.getInstallId());
//
return JsonMessage.success("", map);
}

View File

@ -1,4 +1,9 @@
[
{
"title": "资产总览",
"icon_v3": "laptop",
"id": "system-overview"
},
{
"title": "资产管理",
"icon_v3": "hdd",

View File

@ -1,4 +1,4 @@
import axios from "@/api/config";
import axios from '@/api/config'
/**
*
@ -6,30 +6,30 @@ import axios from "@/api/config";
*/
export function dockerList(params) {
return axios({
url: "/system/assets/docker/list-data",
method: "post",
data: params,
});
url: '/system/assets/docker/list-data',
method: 'post',
data: params
})
}
export function dockerImportTls(formData) {
return axios({
url: "/system/assets/docker/import-tls",
url: '/system/assets/docker/import-tls',
headers: {
"Content-Type": "multipart/form-data;charset=UTF-8",
'Content-Type': 'multipart/form-data;charset=UTF-8'
},
method: "post",
method: 'post',
timeout: 0,
data: formData,
});
data: formData
})
}
export function editDocker(data) {
return axios({
url: "/system/assets/docker/edit",
method: "post",
data: data,
});
url: '/system/assets/docker/edit',
method: 'post',
data: data
})
}
/**
@ -40,10 +40,10 @@ export function editDocker(data) {
*/
export function tryLocalDocker(params) {
return axios({
url: "/system/assets/docker/try-local-docker",
method: "get",
params,
});
url: '/system/assets/docker/try-local-docker',
method: 'get',
params
})
}
/**
@ -54,42 +54,42 @@ export function tryLocalDocker(params) {
*/
export function deleteDcoker(params) {
return axios({
url: "/system/assets/docker/del",
method: "get",
params,
});
url: '/system/assets/docker/del',
method: 'get',
params
})
}
export function dockerListGroup(params) {
return axios({
url: "/system/assets/docker/list-group",
method: "get",
params: params,
});
url: '/system/assets/docker/list-group',
method: 'get',
params: params
})
}
export function initDockerSwarm(data) {
return axios({
url: "/system/assets/docker/init",
method: "post",
data: data,
});
url: '/system/assets/docker/init',
method: 'post',
data: data
})
}
export function joinDockerSwarm(data) {
return axios({
url: "/system/assets/docker/join",
method: "post",
data: data,
});
url: '/system/assets/docker/join',
method: 'post',
data: data
})
}
export function dockerSwarmListAll(params) {
return axios({
url: "/system/assets/docker/swarm/list-all",
method: "get",
params: params,
});
url: '/system/assets/docker/swarm/list-all',
method: 'get',
params: params
})
}
/**
@ -100,10 +100,10 @@ export function dockerSwarmListAll(params) {
*/
export function dcokerSwarmLeaveForce(params) {
return axios({
url: "/system/assets/docker/leave-force",
method: "get",
params,
});
url: '/system/assets/docker/leave-force',
method: 'get',
params
})
}
/**
@ -112,24 +112,29 @@ export function dcokerSwarmLeaveForce(params) {
*/
export function dockerSwarmNodeLeave(params) {
return axios({
url: "/system/assets/docker/leave-node",
method: "get",
params: params,
});
url: '/system/assets/docker/leave-node',
method: 'get',
params: params
})
}
export function machineDockerDistribute(params) {
return axios({
url: "/system/assets/docker/distribute",
method: "post",
data: params,
});
url: '/system/assets/docker/distribute',
method: 'post',
data: params
})
}
export function dockerListWorkspace(params) {
return axios({
url: "/system/assets/docker/list-workspace-docker",
method: "get",
params: params,
});
url: '/system/assets/docker/list-workspace-docker',
method: 'get',
params: params
})
}
export const statusMap = {
0: { desc: '无法连接', color: 'red' },
1: { desc: '正常连接', color: 'green' }
}

View File

@ -251,6 +251,14 @@ export function statWorkspace() {
})
}
export function statSystemOverview() {
return axios({
url: '/stat/system',
method: 'get',
params: {}
})
}
/**
*
*

View File

@ -11,6 +11,7 @@ declare module 'vue' {
AAlert: typeof import('ant-design-vue/es')['Alert']
AAutoComplete: typeof import('ant-design-vue/es')['AutoComplete']
ABackTop: typeof import('ant-design-vue/es')['BackTop']
ABadge: typeof import('ant-design-vue/es')['Badge']
AButton: typeof import('ant-design-vue/es')['Button']
AButtonGroup: typeof import('ant-design-vue/es')['ButtonGroup']
ACard: typeof import('ant-design-vue/es')['Card']

View File

@ -117,7 +117,7 @@ export default {
this.$nextTick(() => {
this.mangerMenuOpenkeys = []
this.$router.push({
path: this.mode == 'normal' ? '/system/management' : '/overview'
path: this.mode == 'normal' ? '/system/overview' : '/overview'
})
})
},

View File

@ -128,9 +128,8 @@
</template>
<template v-else-if="column.dataIndex === 'status'">
<a-tag color="green" v-if="record.status === 1">正常</a-tag>
<a-tooltip v-else :title="record.failureMsg">
<a-tag color="red">无法连接</a-tag>
<a-tooltip :title="record.failureMsg">
<a-tag :color="statusMap[record.status].color">{{ statusMap[record.status].desc || '未知' }}</a-tag>
</a-tooltip>
</template>
<template v-else-if="column.dataIndex === 'operation'">
@ -556,7 +555,8 @@ import {
dcokerSwarmLeaveForce,
machineDockerDistribute,
dockerListWorkspace,
dockerListGroup
dockerListGroup,
statusMap
} from '@/api/system/assets-docker'
import { machineSshListData } from '@/api/system/assets-ssh'
import { CHANGE_PAGE, COMPUTED_PAGINATION, PAGE_DEFAULT_LIST_QUERY, parseTime } from '@/utils/const'
@ -579,6 +579,7 @@ export default {
return {
loading: false,
listQuery: Object.assign({}, PAGE_DEFAULT_LIST_QUERY),
statusMap,
list: [],
groupList: [],
temp: {},

View File

@ -0,0 +1,219 @@
<template>
<div>
<a-page-header :backIcon="false">
<template #title> 欢迎{{ getUserInfo.name }}您来到系统管理中心</template>
<template #subTitle>当前区域为系统管理资产管理中心 </template>
<template v-slot:tags>
<a-tag color="blue">
<template v-if="getUserInfo.demoUser">演示账号</template>
<template v-else-if="getUserInfo.superSystemUser">超级管理员</template>
<template v-else-if="getUserInfo.systemUser">管理员</template>
<template v-else>普通用户</template>
</a-tag>
</template>
<template v-slot:extra>
<a-button @click="init" :loading="loading">
<template #icon><ReloadOutlined /></template>
</a-button>
</template>
<a-space>
<span> 工作空间总数 <a-badge color="blue" :count="statData['workspaceCount'] || '0'" showZero /> </span>
<span>集群数<a-badge color="cyan" :count="statData['clusterCount'] || '0'" showZero /></span>
</a-space>
</a-page-header>
<a-divider dashed />
<a-row :gutter="[16, 16]">
<a-col :span="6">
<a-card size="small">
<template #title> 机器节点 </template>
<a-list :data-source="['all', ...Object.keys(nodeStatusMap)]">
<template #renderItem="{ item }">
<a-list-item v-if="item === 'all'">
总数<a-badge
:color="item.color"
:count="
(statData.nodeStat &&
statData.nodeStat.reduce(function (sum, item2) {
return sum + Number(item2.count)
}, 0)) ||
'0'
"
showZero
/>
</a-list-item>
<a-list-item v-else>
{{ nodeStatusMap[item] }}<a-badge
:color="Number(item) === 1 ? 'green' : ''"
:count="
(statData.nodeStat &&
statData.nodeStat.find((item2) => {
return item2.status === Number(item)
}) &&
statData.nodeStat.find((item2) => {
return item2.status === Number(item)
}).count) ||
'0'
"
showZero
/>
</a-list-item>
</template>
</a-list>
</a-card>
</a-col>
<a-col :span="6">
<a-card size="small">
<template #title> 机器SSH </template>
<a-list :data-source="['all', ...Object.keys(sshStatusMap)]">
<template #renderItem="{ item }">
<a-list-item v-if="item === 'all'">
总数<a-badge
:color="item.color"
:count="
(statData.sshStat &&
statData.sshStat.reduce(function (sum, item2) {
return sum + Number(item2.count)
}, 0)) ||
'0'
"
showZero
/>
</a-list-item>
<a-list-item v-else>
{{ sshStatusMap[item].desc }}<a-badge
:color="sshStatusMap[item].color"
:count="
(statData.sshStat &&
statData.sshStat.find((item2) => {
return item2.status === Number(item)
}) &&
statData.sshStat.find((item2) => {
return item2.status === Number(item)
}).count) ||
'0'
"
showZero
/>
</a-list-item>
</template>
</a-list>
</a-card>
</a-col>
<a-col :span="6">
<a-card size="small">
<template #title> 机器DOCKER </template>
<a-list :data-source="['all', ...Object.keys(dockerStatusMap)]">
<template #renderItem="{ item }">
<a-list-item v-if="item === 'all'">
总数<a-badge
:color="item.color"
:count="
(statData.dockerStat &&
statData.dockerStat.reduce(function (sum, item2) {
return sum + Number(item2.count)
}, 0)) ||
'0'
"
showZero
/>
</a-list-item>
<a-list-item v-else>
{{ dockerStatusMap[item].desc }}<a-badge
:color="dockerStatusMap[item].color"
:count="
(statData.dockerStat &&
statData.dockerStat.find((item2) => {
return item2.status === Number(item)
}) &&
statData.dockerStat.find((item2) => {
return item2.status === Number(item)
}).count) ||
'0'
"
showZero
/>
</a-list-item>
</template>
</a-list>
</a-card>
</a-col>
<a-col :span="6">
<a-card size="small">
<template #title> 用户数据 </template>
<a-list
:data-source="[
{ name: '用户总数', field: 'userCount', color: 'red' },
{ name: '管理员数', field: 'systemUserCount', color: 'pink' },
{ name: '开启MFA数', field: 'openTwoFactorAuth', color: 'green' },
{ name: '禁用数量', field: 'disableUserCount', color: 'yellow' }
]"
>
<template #renderItem="{ item }">
<a-list-item>
{{ item.name }}<a-badge :color="item.color" :count="statData[item.field] || '0'" showZero />
</a-list-item>
</template>
</a-list>
</a-card>
</a-col>
</a-row>
</div>
</template>
<script>
import { statSystemOverview } from '@/api/user/user'
import { statusMap as nodeStatusMap } from '@/api/system/assets-machine'
import { statusMap as sshStatusMap } from '@/api/system/assets-ssh'
import { statusMap as dockerStatusMap } from '@/api/system/assets-docker'
import { useUserStore } from '@/stores/user'
import { mapState } from 'pinia'
import { Empty } from 'ant-design-vue'
export default {
components: {},
computed: {},
data() {
return {
Empty,
dockerStatusMap,
nodeStatusMap,
sshStatusMap,
loading: true,
statData: {}
}
},
computed: {
...mapState(useUserStore, ['getUserInfo'])
},
created() {
this.init()
},
methods: {
init() {
//
this.loading = true
statSystemOverview()
.then((res) => {
if (res.code == 200 && res.data) {
this.statData = res.data || {}
}
})
.finally(() => {
this.loading = false
})
}
}
}
</script>
<style scoped>
:deep(.ant-divider-horizontal) {
margin: 5px 0;
}
</style>

View File

@ -147,6 +147,11 @@ const children = [
]
const management = [
{
path: '/system/overview',
name: 'system-overview',
component: () => import('../pages/system/overview.vue')
},
{
path: '/system/assets/machine-list',
name: 'system-machine-list',
@ -302,7 +307,7 @@ const router = createRouter({
path: '/system/management',
name: 'sys-management',
component: () => import('../pages/layout/management.vue'),
redirect: '/system/workspace',
redirect: '/system/overview',
children: management.map((item: any) => {
const props = item.props || {}
props.routerUrl = item.path

View File

@ -55,7 +55,8 @@ const routeMenuMap: Record<string, string> = {
fileReleaseTask: '/file-manager/release-task',
certificate: '/certificate/list',
authConfig: '/system/oauth-config',
overview: '/overview'
overview: '/overview',
'system-overview': '/system/overview'
}
export default routeMenuMap