🔨 监控逻辑.
This commit is contained in:
102
orion-visor-ui/src/api/asset/host-agent.ts
Normal file
102
orion-visor-ui/src/api/asset/host-agent.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import axios from 'axios';
|
||||
import qs from 'query-string';
|
||||
|
||||
/**
|
||||
* 主机探针状态
|
||||
*/
|
||||
export interface HostAgentStatusResponse {
|
||||
id: number;
|
||||
agentVersion: string;
|
||||
latestVersion: string;
|
||||
agentInstallStatus: number;
|
||||
agentOnlineStatus: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 探针日志
|
||||
*/
|
||||
export interface HostAgentLogResponse {
|
||||
id: number;
|
||||
hostId: number;
|
||||
agentKey: string;
|
||||
type: string;
|
||||
status: string;
|
||||
message: string;
|
||||
createTime: number;
|
||||
updateTime: number;
|
||||
creator: string;
|
||||
updater: string;
|
||||
agentStatus: HostAgentStatusResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装探针请求
|
||||
*/
|
||||
export interface HostInstallAgentRequest {
|
||||
idList?: Array<number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 主机安装探针更新状态请求
|
||||
*/
|
||||
export interface HostAgentInstallStatusUpdateRequest {
|
||||
id?: number;
|
||||
status?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装主机探针
|
||||
*/
|
||||
export function installHostAgent(request: HostInstallAgentRequest) {
|
||||
return axios.post('/asset/host-agent/install', request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改探针安装状态
|
||||
*/
|
||||
export function updateAgentInstallStatus(request: HostAgentInstallStatusUpdateRequest) {
|
||||
return axios.put('/asset/host-agent/update-install-status', request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询主机探针状态
|
||||
*/
|
||||
export function getHostAgentStatus(idList: Array<number>) {
|
||||
return axios.get<Array<HostAgentStatusResponse>>('/asset/host-agent/status', {
|
||||
params: { idList },
|
||||
promptBizErrorMessage: false,
|
||||
promptRequestErrorMessage: false,
|
||||
paramsSerializer: params => {
|
||||
return qs.stringify(params, { arrayFormat: 'comma' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询探针安装状态
|
||||
*/
|
||||
export function getAgentInstallLogStatus(idList: Array<number>) {
|
||||
return axios.get<Array<HostAgentLogResponse>>('/asset/host-agent/install-status', {
|
||||
params: { idList },
|
||||
promptBizErrorMessage: false,
|
||||
promptRequestErrorMessage: false,
|
||||
paramsSerializer: params => {
|
||||
return qs.stringify(params, { arrayFormat: 'comma' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传探针发布包
|
||||
*/
|
||||
export function uploadAgentRelease(file: File) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
return axios.post<string>('/asset/host-agent/upload-agent-release', formData, {
|
||||
timeout: 120000,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -56,7 +56,6 @@ export interface HostVncConfig extends HostBaseConfig {
|
||||
identityId?: number;
|
||||
noUsername?: boolean;
|
||||
noPassword?: boolean;
|
||||
portForwardId?: number;
|
||||
timezone?: string;
|
||||
clipboardEncoding?: string;
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ export interface HostSpecExtraModel {
|
||||
outBandwidth: number;
|
||||
publicIpAddresses: Array<string>;
|
||||
privateIpAddresses: Array<string>;
|
||||
chargePerson: string;
|
||||
ownerPerson: string;
|
||||
createdTime: number;
|
||||
expiredTime: number;
|
||||
items: Array<{
|
||||
|
||||
@@ -69,6 +69,11 @@ export interface HostQueryBaseResponse {
|
||||
code: string;
|
||||
address: string;
|
||||
status: string;
|
||||
agentKey: string;
|
||||
agentVersion: string;
|
||||
agentInstallStatus: number;
|
||||
agentOnlineStatus: number;
|
||||
agentOnlineChangeTime: number;
|
||||
description: string;
|
||||
createTime: number;
|
||||
updateTime: number;
|
||||
@@ -143,8 +148,8 @@ export function updateHostSpec(request: Partial<HostExtraUpdateRequest>) {
|
||||
/**
|
||||
* 查询主机
|
||||
*/
|
||||
export function getHost(id: number) {
|
||||
return axios.get<HostQueryResponse>('/asset/host/get', { params: { id } });
|
||||
export function getHost(id: number, base = false) {
|
||||
return axios.get<HostQueryResponse>('/asset/host/get', { params: { id, base } });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,7 @@ import { getToken } from '@/utils/auth';
|
||||
import { httpBaseUrl } from '@/utils/env';
|
||||
import { reLoginTipsKey } from '@/types/symbol';
|
||||
|
||||
axios.defaults.timeout = 10000;
|
||||
axios.defaults.timeout = 15000;
|
||||
axios.defaults.setAuthorization = true;
|
||||
axios.defaults.promptBizErrorMessage = true;
|
||||
axios.defaults.promptRequestErrorMessage = true;
|
||||
|
||||
95
orion-visor-ui/src/api/monitor/metrics.ts
Normal file
95
orion-visor-ui/src/api/monitor/metrics.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import type { TableData } from '@arco-design/web-vue';
|
||||
import type { DataGrid, OrderDirection, Pagination } from '@/types/global';
|
||||
import axios from 'axios';
|
||||
|
||||
/**
|
||||
* 监控指标创建请求
|
||||
*/
|
||||
export interface MetricsCreateRequest {
|
||||
name?: string;
|
||||
measurement?: string;
|
||||
value?: string;
|
||||
unit?: string;
|
||||
suffix?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 监控指标更新请求
|
||||
*/
|
||||
export interface MetricsUpdateRequest extends MetricsCreateRequest {
|
||||
id?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 监控指标查询请求
|
||||
*/
|
||||
export interface MetricsQueryRequest extends Pagination, OrderDirection {
|
||||
searchValue?: string;
|
||||
id?: number;
|
||||
name?: string;
|
||||
measurement?: string;
|
||||
value?: string;
|
||||
unit?: string;
|
||||
suffix?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 监控指标查询响应
|
||||
*/
|
||||
export interface MetricsQueryResponse extends TableData {
|
||||
id: number;
|
||||
name: string;
|
||||
measurement: string;
|
||||
value: string;
|
||||
unit: string;
|
||||
suffix: string;
|
||||
description: string;
|
||||
createTime: number;
|
||||
updateTime: number;
|
||||
creator: string;
|
||||
updater: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建监控指标
|
||||
*/
|
||||
export function createMetrics(request: MetricsCreateRequest) {
|
||||
return axios.post<number>('/monitor/monitor-metrics/create', request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新监控指标
|
||||
*/
|
||||
export function updateMetrics(request: MetricsUpdateRequest) {
|
||||
return axios.put<number>('/monitor/monitor-metrics/update', request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询监控指标
|
||||
*/
|
||||
export function getMetrics(id: number) {
|
||||
return axios.get<MetricsQueryResponse>('/monitor/monitor-metrics/get', { params: { id } });
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询全部监控指标
|
||||
*/
|
||||
export function getMetricsList() {
|
||||
return axios.get<Array<MetricsQueryResponse>>('/monitor/monitor-metrics/list');
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询监控指标
|
||||
*/
|
||||
export function getMetricsPage(request: MetricsQueryRequest) {
|
||||
return axios.post<DataGrid<MetricsQueryResponse>>('/monitor/monitor-metrics/query', request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除监控指标
|
||||
*/
|
||||
export function deleteMetrics(id: number) {
|
||||
return axios.delete<number>('/monitor/monitor-metrics/delete', { params: { id } });
|
||||
}
|
||||
168
orion-visor-ui/src/api/monitor/monitor-host.ts
Normal file
168
orion-visor-ui/src/api/monitor/monitor-host.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import type { TableData } from '@arco-design/web-vue';
|
||||
import type { DataGrid, Pagination, TimeChartSeries } from '@/types/global';
|
||||
import type { HostAgentLogResponse } from '@/api/asset/host-agent';
|
||||
import axios from 'axios';
|
||||
|
||||
/**
|
||||
* 监控主机更新请求
|
||||
*/
|
||||
export interface MonitorHostUpdateRequest {
|
||||
id?: number;
|
||||
policyId?: number;
|
||||
alarmSwitch?: number;
|
||||
ownerUserId?: number;
|
||||
cpuName?: string;
|
||||
diskName?: string;
|
||||
networkName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 监控主机更新请求
|
||||
*/
|
||||
export interface MonitorHostSwitchUpdateRequest {
|
||||
id?: number;
|
||||
alarmSwitch?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 监控主机查询请求
|
||||
*/
|
||||
export interface MonitorHostQueryRequest extends Pagination {
|
||||
agentKeyList?: Array<string>;
|
||||
searchValue?: string;
|
||||
alarmSwitch?: number;
|
||||
policyId?: number;
|
||||
ownerUserId?: number;
|
||||
name?: string;
|
||||
code?: string;
|
||||
address?: string;
|
||||
agentKey?: string;
|
||||
agentInstallStatus?: number;
|
||||
agentOnlineStatus?: number;
|
||||
description?: string;
|
||||
tags?: Array<number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 监控主机图表查询请求
|
||||
*/
|
||||
export interface MonitorHostChartRequest {
|
||||
agentKeys?: Array<string>;
|
||||
measurement?: string;
|
||||
fields?: Array<string>;
|
||||
window?: string;
|
||||
aggregate?: string;
|
||||
range?: string;
|
||||
start?: string;
|
||||
end?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 监控主机查询响应
|
||||
*/
|
||||
export interface MonitorHostQueryResponse extends TableData {
|
||||
id: number;
|
||||
hostId: number;
|
||||
policyId: number;
|
||||
policyName: string;
|
||||
osType: string;
|
||||
name: string;
|
||||
code: string;
|
||||
address: string;
|
||||
status: string;
|
||||
agentKey: string;
|
||||
agentVersion: string;
|
||||
latestVersion: string;
|
||||
agentInstallStatus: number;
|
||||
agentOnlineStatus: number;
|
||||
agentOnlineChangeTime: number;
|
||||
alarmSwitch: number;
|
||||
ownerUserId: number;
|
||||
ownerUsername: string;
|
||||
tags: Array<{ id: number, name: string }>;
|
||||
meta: MonitorHostMeta;
|
||||
config: MonitorHostConfig;
|
||||
metricsData: MonitorHostMetricsData;
|
||||
installLog: HostAgentLogResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* 监控元数据
|
||||
*/
|
||||
export interface MonitorHostMeta {
|
||||
cpus: Array<string>;
|
||||
disks: Array<string>;
|
||||
nets: Array<string>;
|
||||
memoryBytes: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 监控配置
|
||||
*/
|
||||
export interface MonitorHostConfig {
|
||||
cpuName: string;
|
||||
diskName: string;
|
||||
networkName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 监控数据
|
||||
*/
|
||||
export interface MonitorHostMetricsData {
|
||||
agentKey: string;
|
||||
noData: boolean;
|
||||
timestamp: number;
|
||||
cpuName: string;
|
||||
diskName: string;
|
||||
networkName: string;
|
||||
cpuUsagePercent: number;
|
||||
memoryUsagePercent: number;
|
||||
memoryUsageBytes: number;
|
||||
load1: number;
|
||||
load5: number;
|
||||
load15: number;
|
||||
diskUsagePercent: number;
|
||||
diskUsageBytes: number;
|
||||
networkSentPreBytes: number;
|
||||
networkRecvPreBytes: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询监控主机指标
|
||||
*/
|
||||
export function getMonitorHostMetrics(agentKeyList: Array<string>) {
|
||||
return axios.post<Array<MonitorHostMetricsData>>('/monitor/monitor-host/metrics', {
|
||||
agentKeyList
|
||||
}, {
|
||||
promptBizErrorMessage: false,
|
||||
promptRequestErrorMessage: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询监控主机图表
|
||||
*/
|
||||
export function getMonitorHostChart(request: MonitorHostChartRequest) {
|
||||
return axios.post<Array<TimeChartSeries>>('/monitor/monitor-host/chart', request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询监控主机
|
||||
*/
|
||||
export function getMonitorHostPage(request: MonitorHostQueryRequest) {
|
||||
return axios.post<DataGrid<MonitorHostQueryResponse>>('/monitor/monitor-host/query', request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新监控主机
|
||||
*/
|
||||
export function updateMonitorHost(request: MonitorHostUpdateRequest) {
|
||||
return axios.put<number>('/monitor/monitor-host/update', request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新监控主机告警开关
|
||||
*/
|
||||
export function updateMonitorHostAlarmSwitch(request: MonitorHostSwitchUpdateRequest) {
|
||||
return axios.put<number>('/monitor/monitor-host/update-switch', request);
|
||||
}
|
||||
@@ -20,7 +20,7 @@ export function setSftpFileContent(token: string, content: string) {
|
||||
formData.append('token', token);
|
||||
formData.append('file', new File([content], Date.now() + '', { type: 'text/plain' }));
|
||||
return axios.post<boolean>('/terminal/terminal-sftp/set-content', formData, {
|
||||
timeout: 60000,
|
||||
timeout: 120000,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
|
||||
@@ -31,19 +31,19 @@
|
||||
background-color: rgb(var(--blue-6));
|
||||
|
||||
&.normal {
|
||||
color: rgb(var(--arcoblue-6));
|
||||
background-color: rgb(var(--arcoblue-6));
|
||||
}
|
||||
|
||||
&.pass {
|
||||
color: rgb(var(--green-6));
|
||||
background-color: rgb(var(--green-6));
|
||||
}
|
||||
|
||||
&.warn {
|
||||
color: rgb(var(--orange-6));
|
||||
background-color: rgb(var(--orange-6));
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: rgb(var(--red-6));
|
||||
background-color: rgb(var(--red-6));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
49
orion-visor-ui/src/assets/style/chart.less
Normal file
49
orion-visor-ui/src/assets/style/chart.less
Normal file
@@ -0,0 +1,49 @@
|
||||
// tooltip
|
||||
.chart-tooltip-wrapper {
|
||||
// 时间
|
||||
.chart-tooltip-time {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
// 表头
|
||||
.chart-tooltip-header {
|
||||
margin-top: 5px;
|
||||
font-size: 12px;
|
||||
line-height: 1.3;
|
||||
|
||||
&-grid {
|
||||
display: grid;
|
||||
gap: 0 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&-item {
|
||||
font-weight: bold;
|
||||
padding: 2px 0;
|
||||
white-space: nowrap;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
}
|
||||
|
||||
// 圆点
|
||||
.chart-tooltip-dot {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
// 数据行
|
||||
.chart-tooltip-col {
|
||||
font-weight: 500;
|
||||
color: #000;
|
||||
padding: 3px 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -282,10 +282,18 @@ body {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.mr2 {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.mr4 {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.mt2 {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.mt4 {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@
|
||||
}
|
||||
|
||||
& > .arco-card-body {
|
||||
padding: 0 16px 16px 16px;
|
||||
padding: 0 16px 12px 16px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
@@ -230,6 +230,10 @@
|
||||
}
|
||||
|
||||
// -- doption
|
||||
.arco-dropdown-option-disabled .arco-dropdown-option-content .more-doption {
|
||||
color: var(--color-text-4);
|
||||
}
|
||||
|
||||
.more-doption {
|
||||
min-width: 42px;
|
||||
padding: 0 4px;
|
||||
|
||||
@@ -152,6 +152,15 @@
|
||||
defaultVal: appStore.hostIdentityView,
|
||||
options: cardOptions,
|
||||
},
|
||||
{
|
||||
name: '主机监控',
|
||||
key: 'monitorHostView',
|
||||
type: 'radio-group',
|
||||
margin: '0 0 4px 0',
|
||||
permission: ['monitor:monitor-host:query'],
|
||||
defaultVal: appStore.monitorHostView,
|
||||
options: cardOptions,
|
||||
},
|
||||
]);
|
||||
|
||||
// 是否展示创建 PWA 应用
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<div class="tags-wrap">
|
||||
<tab-item v-for="(tag, index) in tagList"
|
||||
:key="tag.fullPath"
|
||||
:closeable="tagList.length > 1"
|
||||
:index="index"
|
||||
:item-data="tag" />
|
||||
</div>
|
||||
@@ -18,7 +19,6 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { RouteLocationNormalized } from 'vue-router';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { computed, onUnmounted, ref, watch } from 'vue';
|
||||
import { getRouteTag, getRouteTitle } from '@/router';
|
||||
import { listenerRouteChange, removeRouteListener } from '@/utils/route-listener';
|
||||
@@ -26,8 +26,6 @@
|
||||
import qs from 'query-string';
|
||||
import TabItem from './tab-item.vue';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const appStore = useAppStore();
|
||||
const tabBarStore = useTabBarStore();
|
||||
|
||||
@@ -50,12 +48,23 @@
|
||||
if (route.meta.noAffix) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tag = tagList.value.find((tag) => tag.path === route.path);
|
||||
if (tag) {
|
||||
// 找到 更新信息
|
||||
tag.fullPath = route.fullPath;
|
||||
tag.query = qs.parseUrl(route.fullPath).query;
|
||||
tag.title = getRouteTitle(route);
|
||||
if (route.meta.multipleTab) {
|
||||
// 找到 支持多标签 通过全路径去找
|
||||
const fullTag = tagList.value.find((tag) => tag.fullPath === route.fullPath);
|
||||
if (fullTag) {
|
||||
return;
|
||||
}
|
||||
// 没有通过全路径找到则打开新的页签
|
||||
tabBarStore.addTab(getRouteTag(route), route.meta?.ignoreCache as unknown as boolean);
|
||||
} else {
|
||||
// 找到 更新信息
|
||||
tag.fullPath = route.fullPath;
|
||||
tag.query = qs.parseUrl(route.fullPath).query;
|
||||
tag.title = getRouteTitle(route);
|
||||
}
|
||||
} else {
|
||||
// 未找到 添加标签
|
||||
tabBarStore.addTab(getRouteTag(route), route.meta?.ignoreCache as unknown as boolean);
|
||||
@@ -96,12 +105,6 @@
|
||||
margin-right: 6px;
|
||||
cursor: pointer;
|
||||
|
||||
&:first-child {
|
||||
.arco-tag-close-btn {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-link {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
:popup-max-height="false"
|
||||
@select="actionSelect">
|
||||
<span class="arco-tag arco-tag-size-medium arco-tag-checked"
|
||||
:class="{ 'link-activated': itemData?.path === route.path }"
|
||||
:class="{ 'link-activated': itemData?.fullPath === route.fullPath }"
|
||||
@click="goto(itemData as TagProps)">
|
||||
<span class="tag-link">
|
||||
{{ itemData.title }}
|
||||
</span>
|
||||
<span class="arco-icon-hover arco-tag-icon-hover arco-icon-hover-size-medium arco-tag-close-btn"
|
||||
<span v-if="closeable"
|
||||
class="arco-icon-hover arco-tag-icon-hover arco-icon-hover-size-medium arco-tag-close-btn"
|
||||
@click.stop="tagClose(itemData as TagProps, index)">
|
||||
<icon-close />
|
||||
</span>
|
||||
@@ -67,21 +68,23 @@
|
||||
const props = defineProps<{
|
||||
index: number;
|
||||
itemData: TagProps;
|
||||
closeable: boolean;
|
||||
}>();
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const tabBarStore = useTabBarStore();
|
||||
|
||||
const goto = (tag: TagProps) => {
|
||||
router.push({ ...tag });
|
||||
const goto = async (tag: TagProps) => {
|
||||
await router.push({ ...tag });
|
||||
};
|
||||
|
||||
const tagList = computed(() => {
|
||||
return tabBarStore.getTabList;
|
||||
});
|
||||
|
||||
const disabledReload = computed(() => {
|
||||
return props.itemData.path !== route.path;
|
||||
return props.itemData.fullPath !== route.fullPath;
|
||||
});
|
||||
|
||||
const disabledCurrent = computed(() => {
|
||||
@@ -97,18 +100,19 @@
|
||||
});
|
||||
|
||||
// 关闭 tag
|
||||
const tagClose = (tag: TagProps, idx: number) => {
|
||||
const tagClose = async (tag: TagProps, idx: number) => {
|
||||
tabBarStore.deleteTab(idx, tag);
|
||||
if (props.itemData.path === route.path) {
|
||||
|
||||
if (props.itemData.fullPath === route.fullPath) {
|
||||
// 获取队列的前一个 tab
|
||||
const latest = tagList.value[idx - 1];
|
||||
router.push({ name: latest.name });
|
||||
await goto(latest);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取当前路由索引
|
||||
const findCurrentRouteIndex = () => {
|
||||
return tagList.value.findIndex((el) => el.path === route.path);
|
||||
return tagList.value.findIndex((el) => el.fullPath === route.fullPath);
|
||||
};
|
||||
|
||||
// 选择操作
|
||||
@@ -121,10 +125,10 @@
|
||||
} else if (value === Option.left) {
|
||||
// 关闭左侧
|
||||
const currentRouteIdx = findCurrentRouteIndex();
|
||||
copyTagList.splice(1, props.index - 1);
|
||||
copyTagList.splice(0, props.index - 1);
|
||||
tabBarStore.freshTabList(copyTagList);
|
||||
if (currentRouteIdx < index) {
|
||||
await router.push({ name: itemData.name });
|
||||
await goto(itemData);
|
||||
}
|
||||
} else if (value === Option.right) {
|
||||
// 关闭右侧
|
||||
@@ -132,15 +136,15 @@
|
||||
copyTagList.splice(props.index + 1);
|
||||
tabBarStore.freshTabList(copyTagList);
|
||||
if (currentRouteIdx > index) {
|
||||
await router.push({ name: itemData.name });
|
||||
await goto(itemData);
|
||||
}
|
||||
} else if (value === Option.others) {
|
||||
// 关闭其他
|
||||
const filterList = tagList.value.filter((el, idx) => {
|
||||
return idx === 0 || idx === props.index;
|
||||
return idx === props.index;
|
||||
});
|
||||
tabBarStore.freshTabList(filterList);
|
||||
await router.push({ name: itemData.name });
|
||||
await goto(itemData);
|
||||
} else if (value === Option.reload) {
|
||||
// 重新加载
|
||||
tabBarStore.deleteCache(itemData);
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
padding-right: 8px;
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,9 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EChartsOption } from 'echarts';
|
||||
import { nextTick, ref } from 'vue';
|
||||
import { useAppStore } from '@/store';
|
||||
import VCharts from 'vue-echarts';
|
||||
|
||||
const props = withDefaults(defineProps<Partial<{
|
||||
withDefaults(defineProps<Partial<{
|
||||
options: EChartsOption;
|
||||
autoResize: boolean;
|
||||
width: string;
|
||||
@@ -25,8 +24,6 @@
|
||||
height: '100%',
|
||||
});
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
const renderChart = ref(false);
|
||||
|
||||
nextTick(() => {
|
||||
|
||||
@@ -49,6 +49,9 @@ export const builtinParams: Array<TemplateParam> = [
|
||||
}, {
|
||||
name: 'hostUsername',
|
||||
desc: '执行主机用户名'
|
||||
}, {
|
||||
name: 'agentKey',
|
||||
desc: 'agentKey'
|
||||
}, {
|
||||
name: 'osType',
|
||||
desc: '执行主机系统类型'
|
||||
|
||||
@@ -10,6 +10,7 @@ import './mock';
|
||||
// 样式通过 arco-plugin 插件导入 详见目录文件 config/plugin/arcoStyleImport.ts
|
||||
import '@/assets/style/global.less';
|
||||
import '@/assets/style/layout.less';
|
||||
import '@/assets/style/chart.less';
|
||||
import '@/assets/style/arco-extends.less';
|
||||
import '@/api/interceptor';
|
||||
import App from './App.vue';
|
||||
|
||||
@@ -59,6 +59,7 @@ export const UPDATE_PASSWORD_ROUTE: RouteRecordRaw = {
|
||||
name: UPDATE_PASSWORD_ROUTE_NAME,
|
||||
component: () => import('@/views/base/update-password/index.vue'),
|
||||
meta: {
|
||||
noAffix: true,
|
||||
locale: '修改密码'
|
||||
},
|
||||
};
|
||||
@@ -69,6 +70,7 @@ export const FORBIDDEN_ROUTE: RouteRecordRaw = {
|
||||
name: FORBIDDEN_ROUTER_NAME,
|
||||
component: () => import('@/views/base/status/forbidden/index.vue'),
|
||||
meta: {
|
||||
noAffix: true,
|
||||
locale: '403'
|
||||
},
|
||||
};
|
||||
@@ -80,6 +82,7 @@ export const NOT_FOUND_ROUTE: RouteRecordRaw = {
|
||||
name: NOT_FOUND_ROUTER_NAME,
|
||||
component: () => import('@/views/base/status/not-found/index.vue'),
|
||||
meta: {
|
||||
noAffix: true,
|
||||
locale: '404'
|
||||
},
|
||||
};
|
||||
|
||||
38
orion-visor-ui/src/router/routes/modules/monitor.ts
Normal file
38
orion-visor-ui/src/router/routes/modules/monitor.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { AppRouteRecordRaw } from '../types';
|
||||
import type { RouteLocationNormalized } from 'vue-router';
|
||||
import { DEFAULT_LAYOUT } from '../base';
|
||||
|
||||
const MONITOR: AppRouteRecordRaw = {
|
||||
name: 'monitorModule',
|
||||
path: '/monitor-module',
|
||||
component: DEFAULT_LAYOUT,
|
||||
children: [
|
||||
{
|
||||
name: 'metrics',
|
||||
path: '/monitor/metrics',
|
||||
component: () => import('@/views/monitor/metrics/index.vue'),
|
||||
},
|
||||
{
|
||||
name: 'monitorHost',
|
||||
path: '/monitor/monitor-host',
|
||||
component: () => import('@/views/monitor/monitor-host/index.vue'),
|
||||
},
|
||||
{
|
||||
name: 'monitorDetail',
|
||||
path: '/monitor/detail',
|
||||
meta: {
|
||||
// 固定到 tab
|
||||
noAffix: false,
|
||||
// 是否允许打开多个 tab
|
||||
multipleTab: true,
|
||||
// 名称模板
|
||||
localeTemplate: (route: RouteLocationNormalized) => {
|
||||
return `${route.meta.locale} - ${route.query.name || ''}`;
|
||||
},
|
||||
},
|
||||
component: () => import('@/views/monitor/monitor-detail/index.vue'),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default MONITOR;
|
||||
4
orion-visor-ui/src/router/typings.d.ts
vendored
4
orion-visor-ui/src/router/typings.d.ts
vendored
@@ -22,7 +22,9 @@ declare module 'vue-router' {
|
||||
newWindow?: boolean;
|
||||
// 是否活跃
|
||||
activeMenu?: string;
|
||||
// 是否允许打开多个 tag
|
||||
multipleTab?: boolean;
|
||||
// 名称模板
|
||||
localeTemplate?: (key: RouteLocationNormalized) => string;
|
||||
localeTemplate?: (route: RouteLocationNormalized) => string | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ const defaultConfig: AppState = {
|
||||
hostView: 'table',
|
||||
hostKeyView: 'table',
|
||||
hostIdentityView: 'table',
|
||||
monitorHostView: 'table',
|
||||
};
|
||||
|
||||
export default defineStore('app', {
|
||||
|
||||
@@ -47,4 +47,5 @@ export interface UserPreferenceView {
|
||||
hostView: ViewType;
|
||||
hostKeyView: ViewType;
|
||||
hostIdentityView: ViewType;
|
||||
monitorHostView: ViewType;
|
||||
}
|
||||
|
||||
@@ -1,45 +1,68 @@
|
||||
import type { LineSeriesOption } from 'echarts';
|
||||
import type { BarSeriesOption, LineSeriesOption } from 'echarts';
|
||||
import type { LineChartData, Options } from '@/types/global';
|
||||
|
||||
type TimeSeriesType = 'line' | 'bar';
|
||||
|
||||
/**
|
||||
* 折线图系列定义
|
||||
* 时序系列定义
|
||||
*/
|
||||
export interface LineSeriesColor {
|
||||
export interface TimeSeriesColor {
|
||||
lineColor: string;
|
||||
itemBorderColor: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 折线图系列常量
|
||||
* 时间系列配置
|
||||
*/
|
||||
export const LineSeriesColors: Record<string, LineSeriesColor> = {
|
||||
export interface TimeSeriesOption {
|
||||
name: string;
|
||||
type: TimeSeriesType;
|
||||
area: boolean;
|
||||
lineColor: string;
|
||||
itemBorderColor: string;
|
||||
data: any[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 时序系列常量
|
||||
*/
|
||||
export const TimeSeriesColors: Record<string, TimeSeriesColor> = {
|
||||
BLUE: {
|
||||
lineColor: '#4263EB',
|
||||
itemBorderColor: '#DBE4FF',
|
||||
},
|
||||
CYAN: {
|
||||
lineColor: '#1098AD',
|
||||
itemBorderColor: '#C5F6FA',
|
||||
},
|
||||
GREEN: {
|
||||
lineColor: '#37B24D',
|
||||
itemBorderColor: '#D3F9D8',
|
||||
},
|
||||
CYAN: {
|
||||
lineColor: '#1098AD',
|
||||
itemBorderColor: '#C5F6FA',
|
||||
},
|
||||
YELLOW: {
|
||||
lineColor: '#F59F00',
|
||||
itemBorderColor: '#FFF3BF',
|
||||
},
|
||||
PURPLE: {
|
||||
lineColor: '#AE3EC9',
|
||||
itemBorderColor: '#F3D9FA',
|
||||
},
|
||||
ORANGE: {
|
||||
lineColor: '#F76707',
|
||||
itemBorderColor: '#FFF3BF',
|
||||
},
|
||||
VIOLET: {
|
||||
lineColor: '#7048E8',
|
||||
itemBorderColor: '#E5DBFF',
|
||||
},
|
||||
YELLOW: {
|
||||
lineColor: '#F59F00',
|
||||
LIME: {
|
||||
lineColor: '#74B816',
|
||||
itemBorderColor: '#E9FAC8',
|
||||
},
|
||||
ORANGE: {
|
||||
lineColor: '#F76707',
|
||||
itemBorderColor: '#FFF3BF',
|
||||
},
|
||||
INDIGO: {
|
||||
lineColor: '#4263EB',
|
||||
itemBorderColor: '#DBE4FF',
|
||||
},
|
||||
TEAL: {
|
||||
lineColor: '#0CA678',
|
||||
itemBorderColor: '#C3FAE8',
|
||||
@@ -51,38 +74,80 @@ export const LineSeriesColors: Record<string, LineSeriesColor> = {
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成折线图系列
|
||||
* 生成时序系列
|
||||
*/
|
||||
export const createLineSeries = (name: string,
|
||||
lineColor: string,
|
||||
itemBorderColor: string,
|
||||
data: number[]): LineSeriesOption => {
|
||||
export const generateTimeSeriesArr = (options: Array<Options>,
|
||||
chartData: LineChartData,
|
||||
type: TimeSeriesType = 'line'): Array<LineSeriesOption | BarSeriesOption> => {
|
||||
const arr = [];
|
||||
const optionLen = options.length;
|
||||
for (let i = 0; i < optionLen; i++) {
|
||||
// 选项
|
||||
const option = options[i];
|
||||
// 获取颜色
|
||||
let color;
|
||||
if (option.seriesColor) {
|
||||
color = TimeSeriesColors[option.seriesColor as keyof typeof TimeSeriesColors];
|
||||
}
|
||||
if (!color) {
|
||||
color = Object.values(TimeSeriesColors)[i % Object.keys(TimeSeriesColors).length];
|
||||
}
|
||||
// 获取数据
|
||||
const data = chartData.data[option.value as keyof typeof chartData.data] || [];
|
||||
// 生成系列
|
||||
arr.push(createTimeSeries({
|
||||
name: option.label,
|
||||
area: true,
|
||||
type,
|
||||
lineColor: color.lineColor,
|
||||
itemBorderColor: color.itemBorderColor,
|
||||
data,
|
||||
}));
|
||||
}
|
||||
return arr as Array<LineSeriesOption>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建时序系列
|
||||
*/
|
||||
export const createTimeSeries = (option: Partial<TimeSeriesOption>): LineSeriesOption | BarSeriesOption => {
|
||||
// 设置默认值
|
||||
if (option.area === undefined) {
|
||||
option.area = true;
|
||||
}
|
||||
if (option.lineColor === undefined) {
|
||||
option.lineColor = TimeSeriesColors.BLUE.lineColor;
|
||||
}
|
||||
if (option.itemBorderColor === undefined) {
|
||||
option.itemBorderColor = TimeSeriesColors.BLUE.itemBorderColor;
|
||||
}
|
||||
// 配置项
|
||||
return {
|
||||
name,
|
||||
data,
|
||||
type: 'line',
|
||||
name: option.name,
|
||||
data: option.data || [],
|
||||
type: option.type || 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 10,
|
||||
itemStyle: {
|
||||
color: lineColor,
|
||||
color: option.lineColor,
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series',
|
||||
itemStyle: {
|
||||
color: lineColor,
|
||||
color: option.lineColor,
|
||||
borderWidth: 2,
|
||||
borderColor: itemBorderColor,
|
||||
borderColor: option.itemBorderColor,
|
||||
},
|
||||
},
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: lineColor,
|
||||
color: option.lineColor,
|
||||
},
|
||||
showSymbol: data.length === 1,
|
||||
areaStyle: {
|
||||
showSymbol: option.data?.length === 1,
|
||||
areaStyle: option.area ? {
|
||||
opacity: 0.1,
|
||||
color: lineColor,
|
||||
},
|
||||
color: option.lineColor,
|
||||
} : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -83,3 +83,10 @@ export interface PieChartData {
|
||||
export interface BarSingleChartData {
|
||||
data: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface TimeChartSeries {
|
||||
name: string;
|
||||
color: string;
|
||||
tags: Record<string, any>;
|
||||
data: Array<[number, number]>;
|
||||
}
|
||||
|
||||
10
orion-visor-ui/src/utils/charts.ts
Normal file
10
orion-visor-ui/src/utils/charts.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// 获取百分比进度状态
|
||||
export const getPercentProgressColor = (percent: number) => {
|
||||
if (percent < 0.6) {
|
||||
return 'rgb(var(--green-6))';
|
||||
} else if (percent < 0.8) {
|
||||
return 'rgb(var(--orange-6))';
|
||||
} else {
|
||||
return 'rgb(var(--red-6))';
|
||||
}
|
||||
};
|
||||
@@ -207,7 +207,7 @@ export function getFileSize(size: number, scale: number = 2) {
|
||||
result = (size / 1024).toFixed(scale);
|
||||
unit = 'KB';
|
||||
} else {
|
||||
result = size;
|
||||
result = size.toFixed(scale);
|
||||
unit = 'B';
|
||||
}
|
||||
return `${result} ${unit}`;
|
||||
|
||||
216
orion-visor-ui/src/utils/metrics.ts
Normal file
216
orion-visor-ui/src/utils/metrics.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import { dateFormat } from '@/utils/index';
|
||||
|
||||
// 监控指标单位
|
||||
export const MetricsUnit = {
|
||||
BYTES: 'BYTES',
|
||||
BITS: 'BITS',
|
||||
COUNT: 'COUNT',
|
||||
PER: 'PER',
|
||||
SECONDS: 'SECONDS',
|
||||
BYTES_S: 'BYTES_S',
|
||||
BITS_S: 'BITS_S',
|
||||
COUNT_S: 'COUNT_S',
|
||||
TEXT: 'TEXT',
|
||||
NONE: 'NONE',
|
||||
};
|
||||
|
||||
// 指标单位类型
|
||||
export type MetricUnitType = keyof typeof MetricsUnit;
|
||||
|
||||
// 窗口单位
|
||||
export type WindowUnit =
|
||||
| 'SEC'
|
||||
| 'MIN'
|
||||
| 'HOUR'
|
||||
| 'DAY';
|
||||
|
||||
// 指标单位格式化选项
|
||||
export interface MetricUnitFormatOptions {
|
||||
// 小数位
|
||||
digit?: number;
|
||||
// 后缀
|
||||
suffix?: string;
|
||||
// 空转0
|
||||
createEmpty?: number;
|
||||
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// 指标单位格式化函数
|
||||
type MetricUnitFormatterFn = (value: number, option?: MetricUnitFormatOptions) => string;
|
||||
|
||||
// 指标单位格式化配置
|
||||
type WindowTimeFormatterOption = {
|
||||
// 单位
|
||||
unit: string;
|
||||
// 窗口间隔 (ms)
|
||||
windowInterval: (window: number) => number;
|
||||
// 窗口格式化
|
||||
windowFormatter: (window: number) => string;
|
||||
// 标签格式化
|
||||
labelFormatter: (time: number) => string;
|
||||
// 时间格式化
|
||||
dateFormatter: (time: number) => string;
|
||||
};
|
||||
|
||||
// 指标单位格式化
|
||||
export const MetricUnitFormatter: Record<MetricUnitType, MetricUnitFormatterFn> = {
|
||||
// 字节
|
||||
BYTES: formatBytes,
|
||||
// 比特
|
||||
BITS: formatBits,
|
||||
// 次数
|
||||
COUNT: formatCount,
|
||||
// 秒
|
||||
SECONDS: formatSeconds,
|
||||
// 百分比
|
||||
PER: formatPer,
|
||||
// 字节/秒
|
||||
BYTES_S: (value, option) => formatBytes(value, option) + '/s',
|
||||
// 比特/秒
|
||||
BITS_S: (value, option) => formatBits(value, option) + 'ps',
|
||||
// 次数/秒
|
||||
COUNT_S: (value, option) => formatCount(value, option) + '/s',
|
||||
// 文本
|
||||
TEXT: formatText,
|
||||
// 无单位
|
||||
NONE: (value, option) => formatNumber(value, option),
|
||||
};
|
||||
|
||||
// 窗口单位格式化
|
||||
export const WindowUnitFormatter: Record<WindowUnit, WindowTimeFormatterOption> = {
|
||||
// 秒
|
||||
SEC: {
|
||||
unit: 's',
|
||||
windowInterval: (window: number) => window * 1000,
|
||||
windowFormatter: (window: number) => `${window}${WindowUnitFormatter.SEC.unit}`,
|
||||
labelFormatter: (time: number) => `${time}秒`,
|
||||
dateFormatter: (date: number) => dateFormat(new Date(date), 'mm:ss'),
|
||||
},
|
||||
// 分钟
|
||||
MIN: {
|
||||
unit: 'm',
|
||||
windowInterval: (window: number) => window * 60 * 1000,
|
||||
windowFormatter: (window: number) => `${window}${WindowUnitFormatter.MIN.unit}`,
|
||||
labelFormatter: (time: number) => `${time}分钟`,
|
||||
dateFormatter: (date: number) => dateFormat(new Date(date), 'HH:mm'),
|
||||
},
|
||||
// 小时
|
||||
HOUR: {
|
||||
unit: 'h',
|
||||
windowInterval: (window: number) => window * 60 * 60 * 1000,
|
||||
windowFormatter: (window: number) => `${window}${WindowUnitFormatter.HOUR.unit}`,
|
||||
labelFormatter: (time: number) => `${time}小时`,
|
||||
dateFormatter: (date: number) => dateFormat(new Date(date), 'dd mm'),
|
||||
},
|
||||
// 天
|
||||
DAY: {
|
||||
unit: 'd',
|
||||
windowInterval: (window: number) => window * 24 * 60 * 60 * 1000,
|
||||
windowFormatter: (window: number) => `${window}${WindowUnitFormatter.DAY.unit}`,
|
||||
labelFormatter: (time: number) => `${time}天`,
|
||||
dateFormatter: (date: number) => dateFormat(new Date(date), 'MM-dd'),
|
||||
}
|
||||
};
|
||||
|
||||
// 解析窗口单位
|
||||
export const parseWindowUnit = (windowValue: string): [number, WindowUnit] => {
|
||||
const value = Number.parseInt(windowValue);
|
||||
const item = Object.entries(WindowUnitFormatter).find((item) => windowValue.includes(item[1].unit));
|
||||
if (item) {
|
||||
return [value, item[0] as WindowUnit];
|
||||
} else {
|
||||
return [value, 'MIN'];
|
||||
}
|
||||
};
|
||||
|
||||
// 安全取小数位
|
||||
function getFixed(option?: MetricUnitFormatOptions, defaultValue = 2): number {
|
||||
return typeof option?.digit === 'number' ? option.digit : defaultValue;
|
||||
}
|
||||
|
||||
// 格式化数字
|
||||
function formatNumber(value: number, option?: MetricUnitFormatOptions): string {
|
||||
const fixed = getFixed(option, 2);
|
||||
const abs = Math.abs(value);
|
||||
let result: string;
|
||||
|
||||
if (abs >= 1e9) {
|
||||
result = (value / 1e9).toFixed(fixed);
|
||||
} else if (abs >= 1_000_000) {
|
||||
result = (value / 1_000_000).toFixed(fixed);
|
||||
} else if (abs >= 1_000) {
|
||||
result = (value / 1_000).toFixed(fixed);
|
||||
} else {
|
||||
result = value.toFixed(fixed);
|
||||
}
|
||||
|
||||
return parseFloat(result).toString();
|
||||
}
|
||||
|
||||
// 格式化百分比
|
||||
function formatPer(value: number, option?: MetricUnitFormatOptions): string {
|
||||
const fixed = getFixed(option, 2);
|
||||
return parseFloat((value).toFixed(fixed)) + '%';
|
||||
}
|
||||
|
||||
// 格式化字节
|
||||
function formatBytes(value: number, option?: MetricUnitFormatOptions): string {
|
||||
const fixed = getFixed(option, 2);
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
let v = Math.abs(value);
|
||||
let i = 0;
|
||||
while (v >= 1024 && i < units.length - 1) {
|
||||
v /= 1024;
|
||||
i++;
|
||||
}
|
||||
const signedValue = value < 0 ? -v : v;
|
||||
const formattedNum = parseFloat(signedValue.toFixed(i < 3 ? 0 : fixed));
|
||||
return `${formattedNum} ${units[i]}`;
|
||||
}
|
||||
|
||||
// 格式化比特
|
||||
function formatBits(value: number, option?: MetricUnitFormatOptions): string {
|
||||
const fixed = getFixed(option, 2);
|
||||
const units = ['b', 'Kb', 'Mb', 'Gb'];
|
||||
let v = Math.abs(value);
|
||||
let i = 0;
|
||||
while (v >= 1000 && i < units.length - 1) {
|
||||
v /= 1000;
|
||||
i++;
|
||||
}
|
||||
const signedValue = value < 0 ? -v : v;
|
||||
const formattedNum = parseFloat(signedValue.toFixed(i < 2 ? 0 : fixed));
|
||||
return `${formattedNum} ${units[i]}`;
|
||||
}
|
||||
|
||||
// 格式化次数
|
||||
function formatCount(value: number, option?: MetricUnitFormatOptions): string {
|
||||
const fixed = getFixed(option, 2);
|
||||
const abs = Math.abs(value);
|
||||
if (abs >= 1_000_000) {
|
||||
return parseFloat((value / 1_000_000).toFixed(fixed)) + 'M';
|
||||
} else if (abs >= 1_000) {
|
||||
return parseFloat((value / 1_000).toFixed(fixed)) + 'K';
|
||||
}
|
||||
return value.toFixed(0);
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
function formatSeconds(value: number, option?: MetricUnitFormatOptions): string {
|
||||
const fixed = getFixed(option, 2);
|
||||
if (value >= 3600) {
|
||||
return parseFloat((value / 3600).toFixed(fixed)) + 'h';
|
||||
} else if (value >= 60) {
|
||||
return parseFloat((value / 60).toFixed(fixed)) + 'm';
|
||||
}
|
||||
return parseFloat(value.toFixed(fixed)) + 's';
|
||||
}
|
||||
|
||||
// 格式化文本
|
||||
function formatText(value: number, option?: MetricUnitFormatOptions): string {
|
||||
const fixed = getFixed(option, 2);
|
||||
const unitText = option?.suffix || '';
|
||||
const numStr = value.toFixed(fixed);
|
||||
return unitText ? `${numStr} ${unitText}` : numStr;
|
||||
}
|
||||
@@ -188,7 +188,7 @@
|
||||
:wrap="true">
|
||||
<template v-for="groupId in record.groupIdList"
|
||||
:key="groupId">
|
||||
<a-tag>{{ hostGroupList.find(s => s.key === groupId)?.title || groupId }}</a-tag>
|
||||
<a-tag>{{ hostGroupList.find((s: HostGroupQueryResponse) => s.key === groupId)?.title || groupId }}</a-tag>
|
||||
</template>
|
||||
</a-space>
|
||||
</template>
|
||||
@@ -211,6 +211,7 @@
|
||||
<!-- 单协议连接 -->
|
||||
<a-button v-if="record.types?.length === 1"
|
||||
size="mini"
|
||||
type="text"
|
||||
v-permission="['terminal:terminal:access']"
|
||||
@click="openNewRoute({ name: 'terminal', query: { connect: record.id, type: record.types[0] } })">
|
||||
连接
|
||||
@@ -219,7 +220,9 @@
|
||||
<a-popover v-if="(record.types?.length || 0) > 1"
|
||||
:title="undefined"
|
||||
:content-style="{ padding: '8px' }">
|
||||
<a-button v-permission="['terminal:terminal:access']" size="mini">
|
||||
<a-button v-permission="['terminal:terminal:access']"
|
||||
type="text"
|
||||
size="mini">
|
||||
连接
|
||||
</a-button>
|
||||
<template #content>
|
||||
@@ -282,15 +285,15 @@
|
||||
import { addSuffix, dataColor, objectTruthKeyCount, resetObject } from '@/utils';
|
||||
import { deleteHost, getHostPage, updateHostStatus } from '@/api/asset/host';
|
||||
import { Message, Modal } from '@arco-design/web-vue';
|
||||
import { getHostOsIcon, hostOsTypeKey, hostArchTypeKey, hostStatusKey, HostType, hostTypeKey, tagColor, TableName } from '../types/const';
|
||||
import { getHostOsIcon, hostOsTypeKey, hostArchTypeKey, hostStatusKey, hostTypeKey, tagColor, TableName } from '../types/const';
|
||||
import { copy } from '@/hooks/copy';
|
||||
import { useCacheStore, useDictStore } from '@/store';
|
||||
import { useQueryOrder, ASC } from '@/hooks/query-order';
|
||||
import { GrantKey, GrantRouteName } from '@/views/asset/grant/types/const';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { openNewRoute } from '@/router';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import fieldConfig from '../types/card.fields';
|
||||
import { openNewRoute } from '@/router';
|
||||
import TagMultiSelector from '@/components/meta/tag/multi-selector/index.vue';
|
||||
|
||||
const emits = defineEmits(['openAdd', 'openUpdate', 'openHostGroup', 'openCopy']);
|
||||
|
||||
@@ -77,8 +77,8 @@
|
||||
</a-input-number>
|
||||
<span v-else class="text">{{ addSuffix(formModel.memorySize, 'G') }}</span>
|
||||
</a-descriptions-item>
|
||||
<!-- 硬盘 -->
|
||||
<a-descriptions-item label="硬盘">
|
||||
<!-- 磁盘 -->
|
||||
<a-descriptions-item label="磁盘">
|
||||
<a-input-number v-if="editing"
|
||||
v-model="formModel.diskSize"
|
||||
class="input"
|
||||
@@ -138,11 +138,11 @@
|
||||
<!-- 负责人 -->
|
||||
<a-descriptions-item label="负责人" :span="2">
|
||||
<a-input v-if="editing"
|
||||
v-model="formModel.chargePerson"
|
||||
v-model="formModel.ownerPerson"
|
||||
class="input"
|
||||
size="mini"
|
||||
allow-clear />
|
||||
<span v-else class="text">{{ formModel.chargePerson }}</span>
|
||||
<span v-else class="text">{{ formModel.ownerPerson }}</span>
|
||||
</a-descriptions-item>
|
||||
<!-- 创建时间 -->
|
||||
<a-descriptions-item label="创建时间" :span="2">
|
||||
@@ -203,36 +203,38 @@
|
||||
</a-descriptions>
|
||||
<!-- 操作 -->
|
||||
<div class="actions">
|
||||
<!-- 编辑 -->
|
||||
<a-button v-if="!editing"
|
||||
type="primary"
|
||||
long
|
||||
@click="toggleEditing">
|
||||
编辑
|
||||
</a-button>
|
||||
<!-- 保存 -->
|
||||
<a-button v-if="editing"
|
||||
type="primary"
|
||||
long
|
||||
@click="saveSpec">
|
||||
保存
|
||||
</a-button>
|
||||
<!-- 取消 -->
|
||||
<a-button v-if="editing"
|
||||
class="extra-button"
|
||||
type="primary"
|
||||
long
|
||||
@click="fetchHostSpec">
|
||||
取消
|
||||
</a-button>
|
||||
<!-- 新增规格 -->
|
||||
<a-button v-if="editing"
|
||||
class="extra-button"
|
||||
type="primary"
|
||||
long
|
||||
@click="addSpec">
|
||||
新增规格
|
||||
</a-button>
|
||||
<!-- 未编辑 -->
|
||||
<template v-if="!editing">
|
||||
<!-- 编辑 -->
|
||||
<a-button type="primary"
|
||||
long
|
||||
@click="() => toggleEditing()">
|
||||
编辑
|
||||
</a-button>
|
||||
</template>
|
||||
<!-- 编辑中 -->
|
||||
<template v-else>
|
||||
<!-- 保存 -->
|
||||
<a-button type="primary"
|
||||
long
|
||||
@click="saveSpec">
|
||||
保存
|
||||
</a-button>
|
||||
<!-- 取消 -->
|
||||
<a-button class="extra-button"
|
||||
type="primary"
|
||||
long
|
||||
@click="fetchHostSpec">
|
||||
取消
|
||||
</a-button>
|
||||
<!-- 新增规格 -->
|
||||
<a-button class="extra-button"
|
||||
type="primary"
|
||||
long
|
||||
@click="addSpec">
|
||||
新增规格
|
||||
</a-button>
|
||||
</template>
|
||||
</div>
|
||||
</a-spin>
|
||||
</template>
|
||||
@@ -245,8 +247,9 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { HostSpecExtraModel } from '@/api/asset/host-extra';
|
||||
import type { HostQueryResponse } from '@/api/asset/host';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { updateHostSpec } from '@/api/asset/host';
|
||||
import { getHost, updateHostSpec } from '@/api/asset/host';
|
||||
import { getHostExtraItem } from '@/api/asset/host-extra';
|
||||
import { addSuffix, dateFormat } from '@/utils';
|
||||
import { useToggle } from '@vueuse/core';
|
||||
@@ -260,6 +263,7 @@
|
||||
const { loading, setLoading } = useLoading();
|
||||
const [editing, toggleEditing] = useToggle();
|
||||
|
||||
const hostRef = ref<HostQueryResponse>({} as HostQueryResponse);
|
||||
const formModel = ref<HostSpecExtraModel>({} as HostSpecExtraModel);
|
||||
|
||||
// 加载配置
|
||||
@@ -267,6 +271,10 @@
|
||||
setLoading(true);
|
||||
editing.value = false;
|
||||
try {
|
||||
// 查询主机信息
|
||||
const { data: host } = await getHost(props.hostId, true);
|
||||
hostRef.value = host;
|
||||
// 查询规格信息
|
||||
const { data } = await getHostExtraItem<HostSpecExtraModel>({ hostId: props.hostId, item: 'SPEC' });
|
||||
formModel.value = data;
|
||||
} catch (e) {
|
||||
|
||||
@@ -233,7 +233,7 @@
|
||||
:wrap="true">
|
||||
<template v-for="groupId in record.groupIdList"
|
||||
:key="groupId">
|
||||
<a-tag>{{ hostGroupList.find(s => s.key === groupId)?.title || groupId }}</a-tag>
|
||||
<a-tag>{{ hostGroupList.find((s: any) => s.key === groupId)?.title || groupId }}</a-tag>
|
||||
</template>
|
||||
</a-space>
|
||||
</template>
|
||||
@@ -288,18 +288,6 @@
|
||||
@click="emits('openUpdate', record)">
|
||||
修改
|
||||
</a-button>
|
||||
<!-- 删除 -->
|
||||
<a-popconfirm content="确认删除这条记录吗?"
|
||||
position="left"
|
||||
type="warning"
|
||||
@ok="deleteRow(record)">
|
||||
<a-button v-permission="['asset:host:delete']"
|
||||
type="text"
|
||||
size="mini"
|
||||
status="danger">
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
<!-- 更多 -->
|
||||
<a-dropdown trigger="hover" :popup-max-height="false">
|
||||
<a-button type="text" size="mini">
|
||||
@@ -314,6 +302,11 @@
|
||||
{{ toggleDictValue(hostStatusKey, record.status, 'label') }}
|
||||
</span>
|
||||
</a-doption>
|
||||
<!-- 删除 -->
|
||||
<a-doption v-permission="['asset:host:delete']"
|
||||
@click="deleteRow(record)">
|
||||
<span class="more-doption error">删除</span>
|
||||
</a-doption>
|
||||
<!-- 复制 -->
|
||||
<a-doption v-permission="['asset:host:create']"
|
||||
@click="emits('openCopy', record)">
|
||||
@@ -413,17 +406,25 @@
|
||||
|
||||
// 删除当前行
|
||||
const deleteRow = async (record: HostQueryResponse) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 调用删除接口
|
||||
await deleteHost(record.id);
|
||||
Message.success('删除成功');
|
||||
// 重新加载
|
||||
reload();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '删除前确认!',
|
||||
titleAlign: 'start',
|
||||
content: '确定要删除这条记录吗?',
|
||||
okText: '删除',
|
||||
onOk: async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 调用删除接口
|
||||
await deleteHost(record.id);
|
||||
Message.success('删除成功');
|
||||
// 重新加载
|
||||
reload();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 删除选中行
|
||||
|
||||
@@ -56,6 +56,18 @@ export const HostAuthType = {
|
||||
IDENTITY: 'IDENTITY'
|
||||
};
|
||||
|
||||
// 探针安装状态
|
||||
export const AgentInstallStatus = {
|
||||
NOT_INSTALL: 0,
|
||||
INSTALLED: 1,
|
||||
};
|
||||
|
||||
// 探针在线状态
|
||||
export const AgentOnlineStatus = {
|
||||
OFFLINE: 0,
|
||||
ONLINE: 1,
|
||||
};
|
||||
|
||||
// 获取系统类型 icon
|
||||
export const getHostOsIcon = (osType: string) => {
|
||||
return HostOsType[osType as keyof typeof HostOsType]?.icon;
|
||||
|
||||
@@ -14,7 +14,7 @@ const columns = [
|
||||
title: '主机信息',
|
||||
dataIndex: 'hostInfo',
|
||||
slotName: 'hostInfo',
|
||||
width: 288,
|
||||
width: 248,
|
||||
align: 'left',
|
||||
fixed: 'left',
|
||||
default: true,
|
||||
@@ -100,7 +100,7 @@ const columns = [
|
||||
}, {
|
||||
title: '操作',
|
||||
slotName: 'handle',
|
||||
width: 198,
|
||||
width: 168,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
default: true,
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { WorkplaceStatisticsData } from '../types/const';
|
||||
import { createLineSeries, LineSeriesColors } from '@/types/chart';
|
||||
import { createTimeSeries } from '@/types/chart';
|
||||
import { useRouter } from 'vue-router';
|
||||
import useChartOption from '@/hooks/chart-option';
|
||||
|
||||
@@ -91,7 +91,10 @@
|
||||
trigger: 'axis',
|
||||
},
|
||||
series: [
|
||||
createLineSeries('操作数量', LineSeriesColors.BLUE.lineColor, LineSeriesColors.BLUE.itemBorderColor, props.data.infra?.operatorChart.data || []),
|
||||
createTimeSeries({
|
||||
name: '操作数量',
|
||||
data: props.data.infra?.operatorChart.data || []
|
||||
}),
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
@@ -80,6 +80,7 @@
|
||||
import type { WorkplaceStatisticsData } from '@/views/dashboard/workplace/types/const';
|
||||
import { computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { TimeSeriesColors } from '@/types/chart';
|
||||
import useThemes from '@/hooks/themes';
|
||||
import useChartOption from '@/hooks/chart-option';
|
||||
|
||||
@@ -165,7 +166,7 @@
|
||||
showSymbol: false,
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
color: '#165DFF',
|
||||
color: TimeSeriesColors.BLUE.lineColor,
|
||||
width: 3,
|
||||
type: 'dashed',
|
||||
},
|
||||
@@ -202,7 +203,9 @@
|
||||
return {
|
||||
x: x[index],
|
||||
value: s,
|
||||
itemStyle: { color: '#2CAB40' },
|
||||
itemStyle: {
|
||||
color: TimeSeriesColors.GREEN.lineColor,
|
||||
},
|
||||
};
|
||||
}),
|
||||
type: 'bar',
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
<template>
|
||||
<a-modal v-model:visible="visible"
|
||||
modal-class="modal-form-large"
|
||||
title-align="start"
|
||||
:title="title"
|
||||
:top="80"
|
||||
:align-center="false"
|
||||
:draggable="true"
|
||||
:mask-closable="false"
|
||||
:unmount-on-close="true"
|
||||
: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"
|
||||
:auto-label-width="true"
|
||||
:rules="formRules">
|
||||
<!-- 指标名称 -->
|
||||
<a-form-item field="name" label="指标名称">
|
||||
<a-input v-model="formModel.name"
|
||||
placeholder="请输入指标名称"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 数据集 -->
|
||||
<a-form-item field="measurement" label="数据集">
|
||||
<a-select v-model="formModel.measurement"
|
||||
:options="toOptions(MeasurementKey)"
|
||||
placeholder="请选择数据集"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 指标项 -->
|
||||
<a-form-item field="value" label="指标项">
|
||||
<a-input v-model="formModel.value"
|
||||
placeholder="请输入指标项"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 单位 -->
|
||||
<a-form-item field="unit" label="单位">
|
||||
<a-select v-model="formModel.unit"
|
||||
:options="toOptions(MetricsUnitKey)"
|
||||
placeholder="请选择单位"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 后缀文本 -->
|
||||
<a-form-item v-if="formModel.unit === MetricsUnit.TEXT"
|
||||
field="suffix"
|
||||
label="后缀文本">
|
||||
<a-input v-model="formModel.suffix"
|
||||
placeholder="请输入后缀文本"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 指标描述 -->
|
||||
<a-form-item field="description" label="指标描述">
|
||||
<a-textarea v-model="formModel.description"
|
||||
:auto-size="{ minRows: 3, maxRows: 3 }"
|
||||
placeholder="请输入指标描述"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'metricsFormModal'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { MetricsUpdateRequest } from '@/api/monitor/metrics';
|
||||
import { ref } from 'vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import formRules from '../types/form.rules';
|
||||
import { MeasurementKey, MetricsUnitKey } from '../types/const';
|
||||
import { createMetrics, updateMetrics } from '@/api/monitor/metrics';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { useDictStore } from '@/store';
|
||||
import { MetricsUnit } from '@/utils/metrics';
|
||||
|
||||
const emits = defineEmits(['added', 'updated']);
|
||||
|
||||
const { visible, setVisible } = useVisible();
|
||||
const { loading, setLoading } = useLoading();
|
||||
const { toOptions } = useDictStore();
|
||||
|
||||
const title = ref<string>();
|
||||
const isAddHandle = ref<boolean>(true);
|
||||
const formRef = ref<any>();
|
||||
const formModel = ref<MetricsUpdateRequest>({});
|
||||
|
||||
const defaultForm = (): MetricsUpdateRequest => {
|
||||
return {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
measurement: undefined,
|
||||
value: undefined,
|
||||
unit: MetricsUnit.NONE,
|
||||
suffix: undefined,
|
||||
description: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
// 打开新增
|
||||
const openAdd = () => {
|
||||
title.value = '添加监控指标';
|
||||
isAddHandle.value = true;
|
||||
renderForm({ ...defaultForm() });
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
// 打开修改
|
||||
const openUpdate = (record: any) => {
|
||||
title.value = '修改监控指标';
|
||||
isAddHandle.value = false;
|
||||
renderForm({ ...defaultForm(), ...record });
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
// 渲染表单
|
||||
const renderForm = (record: any) => {
|
||||
formModel.value = Object.assign({}, record);
|
||||
};
|
||||
|
||||
defineExpose({ openAdd, openUpdate });
|
||||
|
||||
// 确定
|
||||
const handlerOk = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 验证参数
|
||||
const error = await formRef.value.validate();
|
||||
if (error) {
|
||||
return false;
|
||||
}
|
||||
// 非文本后缀置空
|
||||
if (MetricsUnit.TEXT !== formModel.value.unit) {
|
||||
formModel.value.suffix = '';
|
||||
}
|
||||
if (isAddHandle.value) {
|
||||
// 新增
|
||||
await createMetrics(formModel.value);
|
||||
Message.success('创建成功');
|
||||
emits('added');
|
||||
} else {
|
||||
// 修改
|
||||
await updateMetrics(formModel.value);
|
||||
Message.success('修改成功');
|
||||
emits('updated');
|
||||
}
|
||||
// 清空
|
||||
handlerClear();
|
||||
} catch (e) {
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭
|
||||
const handleClose = () => {
|
||||
handlerClear();
|
||||
};
|
||||
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<!-- 搜索 -->
|
||||
<a-card class="general-card table-search-card">
|
||||
<query-header :model="formModel"
|
||||
label-align="left"
|
||||
@submit="fetchTableData"
|
||||
@reset="fetchTableData"
|
||||
@keyup.enter="() => fetchTableData()">
|
||||
<!-- 指标名称 -->
|
||||
<a-form-item field="name" label="指标名称">
|
||||
<a-input v-model="formModel.name"
|
||||
placeholder="请输入指标名称"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 数据集 -->
|
||||
<a-form-item field="measurement" label="数据集">
|
||||
<a-select v-model="formModel.measurement"
|
||||
:options="toOptions(MeasurementKey)"
|
||||
placeholder="请选择数据集"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 指标项 -->
|
||||
<a-form-item field="value" label="指标项">
|
||||
<a-input v-model="formModel.value"
|
||||
placeholder="请输入指标项"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 指标描述 -->
|
||||
<a-form-item field="description" label="指标描述">
|
||||
<a-input v-model="formModel.description"
|
||||
placeholder="请输入指标描述"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
</query-header>
|
||||
</a-card>
|
||||
<!-- 表格 -->
|
||||
<a-card class="general-card table-card">
|
||||
<template #title>
|
||||
<!-- 左侧操作 -->
|
||||
<div class="table-left-bar-handle">
|
||||
<!-- 标题 -->
|
||||
<div class="table-title">
|
||||
监控指标列表
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧操作 -->
|
||||
<div class="table-right-bar-handle">
|
||||
<a-space>
|
||||
<!-- 新增 -->
|
||||
<a-button v-permission="['monitor:monitor-metrics:create']"
|
||||
type="primary"
|
||||
@click="emits('openAdd')">
|
||||
新增
|
||||
<template #icon>
|
||||
<icon-plus />
|
||||
</template>
|
||||
</a-button>
|
||||
<!-- 调整 -->
|
||||
<table-adjust :columns="columns"
|
||||
:columns-hook="columnsHook"
|
||||
:query-order="queryOrder"
|
||||
@query="fetchTableData" />
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
<!-- table -->
|
||||
<a-table row-key="id"
|
||||
ref="tableRef"
|
||||
:loading="loading"
|
||||
:columns="tableColumns"
|
||||
:data="tableRenderData"
|
||||
:pagination="pagination"
|
||||
:bordered="false"
|
||||
@page-change="(page: number) => fetchTableData(page, pagination.pageSize)"
|
||||
@page-size-change="(size: number) => fetchTableData(1, size)">
|
||||
<!-- 数据集 -->
|
||||
<template #measurement="{ record }">
|
||||
<span class="span-blue text-copy" @click="copy(record.measurement, '数据集名称已复制')">
|
||||
{{ getDictValue(MeasurementKey, record.measurement) }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- 指标项 -->
|
||||
<template #value="{ record }">
|
||||
<span class="span-blue text-copy" @click="copy(record.measurement, true)">
|
||||
{{ record.value }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- 单位 -->
|
||||
<template #unit="{ record }">
|
||||
<div>
|
||||
<span v-if="record.unit === MetricsUnit.TEXT">
|
||||
{{ record.suffix }}
|
||||
</span>
|
||||
<span v-else-if="record.unit === MetricsUnit.NONE">
|
||||
-
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ getDictValue(MetricsUnitKey, record.unit) }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 操作 -->
|
||||
<template #handle="{ record }">
|
||||
<div class="table-handle-wrapper">
|
||||
<!-- 修改 -->
|
||||
<a-button v-permission="['monitor:monitor-metrics:update']"
|
||||
type="text"
|
||||
size="mini"
|
||||
@click="emits('openUpdate', record)">
|
||||
修改
|
||||
</a-button>
|
||||
<!-- 删除 -->
|
||||
<a-popconfirm content="确认删除这条记录吗?"
|
||||
position="left"
|
||||
type="warning"
|
||||
@ok="deleteRow(record)">
|
||||
<a-button v-permission="['monitor:monitor-metrics:delete']"
|
||||
type="text"
|
||||
size="mini"
|
||||
status="danger">
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'metricsTable'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { MetricsQueryRequest, MetricsQueryResponse } from '@/api/monitor/metrics';
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import { deleteMetrics, getMetricsPage } from '@/api/monitor/metrics';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import columns from '../types/table.columns';
|
||||
import { MetricsUnit } from '@/utils/metrics';
|
||||
import { TableName, MeasurementKey, MetricsUnitKey } from '../types/const';
|
||||
import { useTablePagination, useTableColumns } from '@/hooks/table';
|
||||
import { useDictStore } from '@/store';
|
||||
import { useQueryOrder, ASC } from '@/hooks/query-order';
|
||||
import { copy } from '@/hooks/copy';
|
||||
import TableAdjust from '@/components/app/table-adjust/index.vue';
|
||||
|
||||
const emits = defineEmits(['openAdd', 'openUpdate']);
|
||||
|
||||
const pagination = useTablePagination();
|
||||
const queryOrder = useQueryOrder(TableName, ASC);
|
||||
const { tableColumns, columnsHook } = useTableColumns(TableName, columns);
|
||||
const { loading, setLoading } = useLoading();
|
||||
const { toOptions, getDictValue } = useDictStore();
|
||||
|
||||
const tableRenderData = ref<Array<MetricsQueryResponse>>([]);
|
||||
const formModel = reactive<MetricsQueryRequest>({
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
measurement: undefined,
|
||||
value: undefined,
|
||||
unit: undefined,
|
||||
suffix: undefined,
|
||||
description: undefined,
|
||||
});
|
||||
|
||||
// 删除当前行
|
||||
const deleteRow = async (record: MetricsQueryResponse) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 调用删除接口
|
||||
await deleteMetrics(record.id);
|
||||
Message.success('删除成功');
|
||||
// 重新加载
|
||||
reload();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 重新加载
|
||||
const reload = () => {
|
||||
// 重新加载数据
|
||||
fetchTableData();
|
||||
};
|
||||
|
||||
defineExpose({ reload });
|
||||
|
||||
// 加载数据
|
||||
const doFetchTableData = async (request: MetricsQueryRequest) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data } = await getMetricsPage(queryOrder.markOrderly(request));
|
||||
tableRenderData.value = data.rows;
|
||||
pagination.total = data.total;
|
||||
pagination.current = request.page;
|
||||
pagination.pageSize = request.limit;
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 切换页码
|
||||
const fetchTableData = (page = 1, limit = pagination.pageSize, form = formModel) => {
|
||||
doFetchTableData({ page, limit, ...form });
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchTableData();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
46
orion-visor-ui/src/views/monitor/metrics/index.vue
Normal file
46
orion-visor-ui/src/views/monitor/metrics/index.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="layout-container" v-if="render">
|
||||
<!-- 列表-表格 -->
|
||||
<metrics-table ref="table"
|
||||
@open-add="() => modal.openAdd()"
|
||||
@open-update="(e) => modal.openUpdate(e)" />
|
||||
<!-- 添加修改模态框 -->
|
||||
<metrics-form-modal ref="modal"
|
||||
@added="reload"
|
||||
@updated="reload" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'metrics'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onBeforeMount } from 'vue';
|
||||
import { useDictStore } from '@/store';
|
||||
import { dictKeys } from './types/const';
|
||||
import MetricsTable from './components/metrics-table.vue';
|
||||
import MetricsFormModal from './components/metrics-form-modal.vue';
|
||||
|
||||
const render = ref(false);
|
||||
const table = ref();
|
||||
const modal = ref();
|
||||
|
||||
// 重新加载
|
||||
const reload = () => {
|
||||
table.value.reload();
|
||||
};
|
||||
|
||||
onBeforeMount(async () => {
|
||||
const dictStore = useDictStore();
|
||||
await dictStore.loadKeys(dictKeys);
|
||||
render.value = true;
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
10
orion-visor-ui/src/views/monitor/metrics/types/const.ts
Normal file
10
orion-visor-ui/src/views/monitor/metrics/types/const.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export const TableName = 'monitor_metrics';
|
||||
|
||||
// 监控指标类型 字典项
|
||||
export const MeasurementKey = 'metricsMeasurement';
|
||||
|
||||
// 监控指标单位 字典项
|
||||
export const MetricsUnitKey = 'metricsUnit';
|
||||
|
||||
// 加载的字典值
|
||||
export const dictKeys = [MeasurementKey, MetricsUnitKey];
|
||||
52
orion-visor-ui/src/views/monitor/metrics/types/form.rules.ts
Normal file
52
orion-visor-ui/src/views/monitor/metrics/types/form.rules.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { FieldRule } from '@arco-design/web-vue';
|
||||
|
||||
export const name = [{
|
||||
required: true,
|
||||
message: '请输入指标名称'
|
||||
}, {
|
||||
maxLength: 64,
|
||||
message: '指标名称长度不能大于64位'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const measurement = [{
|
||||
required: true,
|
||||
message: '请输入数据集'
|
||||
}, {
|
||||
maxLength: 64,
|
||||
message: '数据集长度不能大于64位'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const value = [{
|
||||
required: true,
|
||||
message: '请输入指标项'
|
||||
}, {
|
||||
maxLength: 128,
|
||||
message: '指标项长度不能大于128位'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const unit = [{
|
||||
required: true,
|
||||
message: '请选择单位'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const suffix = [{
|
||||
required: true,
|
||||
message: '请输入后缀文本'
|
||||
}, {
|
||||
maxLength: 32,
|
||||
message: '后缀文本长度不能大于32位'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const description = [{
|
||||
maxLength: 128,
|
||||
message: '指标描述长度不能大于128位'
|
||||
}] as FieldRule[];
|
||||
|
||||
export default {
|
||||
name,
|
||||
measurement,
|
||||
value,
|
||||
unit,
|
||||
suffix,
|
||||
description,
|
||||
} as Record<string, FieldRule | FieldRule[]>;
|
||||
@@ -0,0 +1,98 @@
|
||||
import type { TableColumnData } from '@arco-design/web-vue';
|
||||
import { dateFormat } from '@/utils';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'id',
|
||||
dataIndex: 'id',
|
||||
slotName: 'id',
|
||||
width: 68,
|
||||
align: 'left',
|
||||
fixed: 'left',
|
||||
default: true,
|
||||
}, {
|
||||
title: '指标名称',
|
||||
dataIndex: 'name',
|
||||
slotName: 'name',
|
||||
align: 'left',
|
||||
width: 238,
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
default: true,
|
||||
}, {
|
||||
title: '数据集',
|
||||
dataIndex: 'measurement',
|
||||
slotName: 'measurement',
|
||||
align: 'left',
|
||||
width: 148,
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
default: true,
|
||||
}, {
|
||||
title: '指标项',
|
||||
dataIndex: 'value',
|
||||
slotName: 'value',
|
||||
align: 'left',
|
||||
minWidth: 288,
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
default: true,
|
||||
}, {
|
||||
title: '指标单位',
|
||||
dataIndex: 'unit',
|
||||
slotName: 'unit',
|
||||
align: 'left',
|
||||
width: 168,
|
||||
default: true,
|
||||
}, {
|
||||
title: '指标描述',
|
||||
dataIndex: 'description',
|
||||
slotName: 'description',
|
||||
align: 'left',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
default: true,
|
||||
}, {
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
slotName: 'createTime',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
render: ({ record }) => {
|
||||
return dateFormat(new Date(record.createTime));
|
||||
},
|
||||
}, {
|
||||
title: '修改时间',
|
||||
dataIndex: 'updateTime',
|
||||
slotName: 'updateTime',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
render: ({ record }) => {
|
||||
return dateFormat(new Date(record.updateTime));
|
||||
},
|
||||
default: true,
|
||||
}, {
|
||||
title: '创建人',
|
||||
dataIndex: 'creator',
|
||||
slotName: 'creator',
|
||||
width: 148,
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
}, {
|
||||
title: '修改人',
|
||||
dataIndex: 'updater',
|
||||
slotName: 'updater',
|
||||
width: 148,
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
}, {
|
||||
title: '操作',
|
||||
slotName: 'handle',
|
||||
width: 130,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
default: true,
|
||||
},
|
||||
] as TableColumnData[];
|
||||
|
||||
export default columns;
|
||||
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<div class="header-container">
|
||||
<!-- 左侧 -->
|
||||
<div class="header-left">
|
||||
<!-- tab切换 -->
|
||||
<div class="tab-container">
|
||||
<a-tabs v-model:active-key="activeKey"
|
||||
type="rounded"
|
||||
:hide-content="true">
|
||||
<a-tab-pane :key="TabKeys.CHART" title="监控图表" />
|
||||
</a-tabs>
|
||||
<a-divider direction="vertical"
|
||||
style="height: 22px; margin: 0 16px 0 8px;"
|
||||
:size="2" />
|
||||
</div>
|
||||
<!-- 基本信息-->
|
||||
<div class="info-container">
|
||||
<!-- 标题 -->
|
||||
<div class="title">{{ host.name }}</div>
|
||||
<!-- 地址 -->
|
||||
<a-tag class="text-copy"
|
||||
color="arcoblue"
|
||||
@click="copy(host.address, true)">
|
||||
{{ host.address }}
|
||||
</a-tag>
|
||||
<!-- 编码 -->
|
||||
<a-tag color="arcoblue">{{ host.code }}</a-tag>
|
||||
<!-- tags -->
|
||||
<a-tag v-for="tag in (host.tags || [])" color="arcoblue">{{ tag.name }}</a-tag>
|
||||
<!-- 在线状态 -->
|
||||
<a-tag :color="getDictValue(OnlineStatusKey, host.agentOnlineStatus, 'color')">
|
||||
<template #icon>
|
||||
<component :is="getDictValue(OnlineStatusKey, host.agentOnlineStatus, 'icon')" />
|
||||
</template>
|
||||
{{ getDictValue(OnlineStatusKey, host.agentOnlineStatus) }}
|
||||
</a-tag>
|
||||
<!-- 版本号 -->
|
||||
<a-tag color="green">v{{ host.agentVersion }}</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧 -->
|
||||
<div class="header-right">
|
||||
<!-- 监控图表操作 -->
|
||||
<div v-if="activeKey === TabKeys.CHART" class="chart-handle">
|
||||
<a-space>
|
||||
<!-- 表格时间区间 -->
|
||||
<a-select v-model="chartRange"
|
||||
style="width: 138px;"
|
||||
:options="toOptions(ChartRangeKey)"
|
||||
@change="changeChartRange">
|
||||
<template #prefix>
|
||||
区间
|
||||
</template>
|
||||
</a-select>
|
||||
<!-- 表格窗口 -->
|
||||
<a-select v-model="chartWindow"
|
||||
style="width: 138px;"
|
||||
:options="chartWindowOptions">
|
||||
<template #prefix>
|
||||
窗口
|
||||
</template>
|
||||
</a-select>
|
||||
<!-- 刷新 -->
|
||||
<a-button class="fs16"
|
||||
title="刷新"
|
||||
@click="reloadChart">
|
||||
<template #icon>
|
||||
<icon-refresh />
|
||||
</template>
|
||||
</a-button>
|
||||
<!-- 切换视图 -->
|
||||
<a-button class="fs16"
|
||||
title="切换视图"
|
||||
@click="chartCompose = !chartCompose">
|
||||
<template #icon>
|
||||
<icon-menu v-if="chartCompose" />
|
||||
<icon-apps v-else />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'detail-header'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { HostQueryResponse } from '@/api/asset/host';
|
||||
import type { SelectOptionData } from '@arco-design/web-vue';
|
||||
import { ref, onMounted, nextTick } from 'vue';
|
||||
import { copy } from '@/hooks/copy';
|
||||
import { useDictStore } from '@/store';
|
||||
import { TabKeys, ChartRangeKey } from '../types/const';
|
||||
import { OnlineStatusKey } from '@/views/monitor/monitor-host/types/const';
|
||||
import { parseWindowUnit, WindowUnitFormatter } from '@/utils/metrics';
|
||||
|
||||
defineProps<{
|
||||
host: HostQueryResponse;
|
||||
}>();
|
||||
const emits = defineEmits(['reloadChart']);
|
||||
const activeKey = defineModel('activeKey', { type: String });
|
||||
const chartCompose = defineModel('chartCompose', { type: Boolean });
|
||||
|
||||
const chartRange = ref('-30m');
|
||||
const chartWindow = ref('1m');
|
||||
const chartWindowOptions = ref<Array<SelectOptionData>>([{ label: '1分钟', value: '1m' }]);
|
||||
|
||||
const { toOptions, getDictValue } = useDictStore();
|
||||
|
||||
// 加载图表
|
||||
const reloadChart = () => {
|
||||
emits('reloadChart', chartRange.value, chartWindow.value);
|
||||
};
|
||||
|
||||
// 切换图表区间
|
||||
const changeChartRange = (value: string) => {
|
||||
const windowValue = getDictValue(ChartRangeKey, value, 'window') as string;
|
||||
chartWindowOptions.value = windowValue.split(',').map(s => {
|
||||
const [value, unit] = parseWindowUnit(s);
|
||||
const label = WindowUnitFormatter[unit].labelFormatter(value);
|
||||
return {
|
||||
label,
|
||||
value: s,
|
||||
};
|
||||
});
|
||||
chartWindow.value = chartWindowOptions.value[0].value as string;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
reloadChart();
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.header-container {
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
margin-bottom: 16px;
|
||||
background-color: var(--color-bg-2);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.info-container {
|
||||
display: flex;
|
||||
color: var(--color-text-2);
|
||||
|
||||
& > * {
|
||||
margin-right: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
}
|
||||
|
||||
.tab-container {
|
||||
padding: 0 0 0 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.arco-tabs) {
|
||||
user-select: none;
|
||||
|
||||
.arco-tabs-tab {
|
||||
background-color: var(--color-fill-2);
|
||||
}
|
||||
|
||||
.arco-tabs-nav::before {
|
||||
height: 0 !important;
|
||||
}
|
||||
|
||||
.arco-tabs-tab {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-right {
|
||||
padding-right: 16px;
|
||||
|
||||
.chart-handle {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<div class="charts-container">
|
||||
<a-grid :cols="chartCols" :colGap="16" :rowGap="16">
|
||||
<a-grid-item v-for="(item, index) of chartItems"
|
||||
:span="item.option?.span || 1"
|
||||
:style="{ height: (chartCompose ? '400px': '270px') }">
|
||||
<metrics-chart v-bind="item" :ref="(el: any) => setRef(index, el)" />
|
||||
</a-grid-item>
|
||||
</a-grid>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'metricsChartTab'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { MetricsChartProps, MetricsChartOption } from '../types/const';
|
||||
import { computed, ref } from 'vue';
|
||||
import { parseWindowUnit, MetricsUnit, type MetricUnitType } from '@/utils/metrics';
|
||||
import { TimeSeriesColors } from '@/types/chart';
|
||||
import MetricsChart from './metrics-chart.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
agentKey: string;
|
||||
chartCompose: boolean;
|
||||
chartRange: string;
|
||||
chartWindow: string;
|
||||
}>();
|
||||
|
||||
const chartsRef = ref<Record<number, any>>({});
|
||||
|
||||
// 响应式布局
|
||||
const chartCols = computed(() => {
|
||||
return {
|
||||
xs: 1,
|
||||
sm: props.chartCompose ? 2 : 1,
|
||||
md: props.chartCompose ? 2 : 1,
|
||||
lg: props.chartCompose ? 2 : 1,
|
||||
xl: props.chartCompose ? 3 : 1,
|
||||
xxl: props.chartCompose ? 3 : 1,
|
||||
};
|
||||
});
|
||||
|
||||
// 设置图表引用
|
||||
const setRef = (index: number, el: any) => {
|
||||
chartsRef.value[index] = el;
|
||||
};
|
||||
|
||||
// 图表项
|
||||
const chartItems = computed<Array<MetricsChartProps>>(() => {
|
||||
// 获取窗口单位
|
||||
const [windowValue, windowUnit] = parseWindowUnit(props.chartWindow);
|
||||
const options: Array<MetricsChartOption> = [
|
||||
{
|
||||
name: 'CPU使用率',
|
||||
measurement: 'cpu',
|
||||
fields: ['cpu_total_seconds_total'],
|
||||
colors: [[TimeSeriesColors.BLUE.lineColor, TimeSeriesColors.BLUE.itemBorderColor]],
|
||||
aggregate: 'mean',
|
||||
unit: MetricsUnit.PER as MetricUnitType,
|
||||
unitOption: { digit: 3 }
|
||||
},
|
||||
{
|
||||
name: '内存使用率',
|
||||
measurement: 'memory',
|
||||
fields: ['mem_used_percent', 'mem_swap_used_percent'],
|
||||
colors: [[TimeSeriesColors.LIME.lineColor, TimeSeriesColors.LIME.itemBorderColor], [TimeSeriesColors.TEAL.lineColor, TimeSeriesColors.TEAL.itemBorderColor]],
|
||||
aggregate: 'mean',
|
||||
unit: MetricsUnit.PER as MetricUnitType,
|
||||
unitOption: { digit: 3 }
|
||||
},
|
||||
{
|
||||
name: '内存使用量',
|
||||
measurement: 'memory',
|
||||
fields: ['mem_used_bytes_total', 'mem_swap_used_bytes_total'],
|
||||
colors: [[TimeSeriesColors.LIME.lineColor, TimeSeriesColors.LIME.itemBorderColor], [TimeSeriesColors.TEAL.lineColor, TimeSeriesColors.TEAL.itemBorderColor]],
|
||||
aggregate: 'mean',
|
||||
unit: MetricsUnit.BYTES as MetricUnitType,
|
||||
unitOption: { digit: 2 }
|
||||
},
|
||||
{
|
||||
name: '系统负载',
|
||||
measurement: 'load',
|
||||
fields: ['load1', 'load5', 'load15'],
|
||||
span: 1,
|
||||
legend: true,
|
||||
background: false,
|
||||
colors: [[TimeSeriesColors.LIME.lineColor, TimeSeriesColors.LIME.itemBorderColor], [TimeSeriesColors.RED.lineColor, TimeSeriesColors.RED.itemBorderColor], [TimeSeriesColors.BLUE.lineColor, TimeSeriesColors.BLUE.itemBorderColor]],
|
||||
aggregate: 'mean',
|
||||
unit: MetricsUnit.NONE as MetricUnitType,
|
||||
unitOption: { digit: 2 }
|
||||
},
|
||||
{
|
||||
name: '磁盘使用率',
|
||||
measurement: 'disk',
|
||||
fields: ['disk_fs_used_percent'],
|
||||
colors: [[TimeSeriesColors.VIOLET.lineColor, TimeSeriesColors.VIOLET.itemBorderColor]],
|
||||
aggregate: 'mean',
|
||||
unit: MetricsUnit.PER as MetricUnitType,
|
||||
unitOption: { digit: 2 }
|
||||
},
|
||||
{
|
||||
name: '磁盘使用量',
|
||||
measurement: 'disk',
|
||||
fields: ['disk_fs_used_bytes_total'],
|
||||
colors: [[TimeSeriesColors.LIME.lineColor, TimeSeriesColors.LIME.itemBorderColor]],
|
||||
aggregate: 'mean',
|
||||
unit: MetricsUnit.BYTES as MetricUnitType,
|
||||
unitOption: { digit: 2 }
|
||||
},
|
||||
{
|
||||
name: '网络连接数',
|
||||
measurement: 'connections',
|
||||
fields: ['net_tcp_connections', 'net_udp_connections'],
|
||||
colors: [[TimeSeriesColors.CYAN.lineColor, TimeSeriesColors.CYAN.itemBorderColor]],
|
||||
aggregate: 'mean',
|
||||
unit: MetricsUnit.COUNT as MetricUnitType,
|
||||
unitOption: { digit: 0, suffix: '个' }
|
||||
},
|
||||
{
|
||||
name: '网络带宽',
|
||||
measurement: 'network',
|
||||
fields: ['net_sent_bytes_per_second', 'net_recv_bytes_per_second'],
|
||||
colors: [[TimeSeriesColors.BLUE.lineColor, TimeSeriesColors.BLUE.itemBorderColor], [TimeSeriesColors.GREEN.lineColor, TimeSeriesColors.GREEN.itemBorderColor]],
|
||||
aggregate: 'mean',
|
||||
unit: MetricsUnit.BITS_S as MetricUnitType,
|
||||
unitOption: { digit: 2 }
|
||||
},
|
||||
{
|
||||
name: '磁盘IO',
|
||||
measurement: 'io',
|
||||
fields: ['disk_io_read_bytes_per_second', 'disk_io_write_bytes_per_second'],
|
||||
colors: [[TimeSeriesColors.CYAN.lineColor, TimeSeriesColors.CYAN.itemBorderColor], [TimeSeriesColors.YELLOW.lineColor, TimeSeriesColors.YELLOW.itemBorderColor]],
|
||||
aggregate: 'mean',
|
||||
unit: MetricsUnit.BYTES_S as MetricUnitType,
|
||||
unitOption: { digit: 2 }
|
||||
},
|
||||
];
|
||||
return options.map(option => {
|
||||
return {
|
||||
agentKeys: [props.agentKey],
|
||||
range: props.chartRange,
|
||||
windowValue: windowValue,
|
||||
windowUnit,
|
||||
option,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// 重新加载
|
||||
const reload = async () => {
|
||||
const allCharts = Object.values(chartsRef.value);
|
||||
const chunks = [];
|
||||
// 分组
|
||||
for (let i = 0; i < allCharts.length; i += 3) {
|
||||
chunks.push(allCharts.slice(i, i + 3));
|
||||
}
|
||||
// 顺序刷新
|
||||
for (const chunk of chunks) {
|
||||
try {
|
||||
await Promise.all(chunk.map(s => s.refresh()));
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({ reload });
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.charts-container {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,241 @@
|
||||
<template>
|
||||
<a-card class="full"
|
||||
size="small"
|
||||
style="border-radius: 8px;"
|
||||
:loading="loading"
|
||||
:bordered="false"
|
||||
:header-style="{ height: '48px', padding: '8px 16px 0 16px', borderBottom: 'none' }"
|
||||
:body-style="{ padding: '0 0 0 16px', height: 'calc(100% - 48px)', position: 'relative' }">
|
||||
<!-- 标题 -->
|
||||
<template #title>
|
||||
<div class="chart-title">
|
||||
<!-- 名称 -->
|
||||
<h3>{{ option.name }}</h3>
|
||||
<!-- 聚合 -->
|
||||
<a-tag color="arcoblue">{{ getDictValue(MetricsAggregateKey, option.aggregate) }}</a-tag>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 无数据 -->
|
||||
<div v-if="!series.length && !loading"
|
||||
class="nodata-chart">
|
||||
<a-empty description="此监控指标暂无数据" />
|
||||
</div>
|
||||
<!-- 图表 -->
|
||||
<chart v-else-if="chartOption"
|
||||
:options="chartOption"
|
||||
class="chart"
|
||||
width="100%"
|
||||
height="100%" />
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'metricsChart'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TimeChartSeries } from '@/types/global';
|
||||
import type { MetricsChartProps } from '../types/const';
|
||||
import { ref } from 'vue';
|
||||
import { dateFormat } from '@/utils';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import useChartOption from '@/hooks/chart-option';
|
||||
import { getMonitorHostChart } from '@/api/monitor/monitor-host';
|
||||
import { createTimeSeries, TimeSeriesColors } from '@/types/chart';
|
||||
import { MetricsAggregateKey } from '../types/const';
|
||||
import { MetricUnitFormatter, WindowUnitFormatter } from '@/utils/metrics';
|
||||
import { useDictStore } from '@/store';
|
||||
|
||||
const props = defineProps<MetricsChartProps>();
|
||||
|
||||
const { getDictValue } = useDictStore();
|
||||
const { loading, setLoading } = useLoading(true);
|
||||
const series = ref<Array<TimeChartSeries>>([]);
|
||||
|
||||
// 数量图表配置
|
||||
const { chartOption } = useChartOption((dark, themeTextColor, themeLineColor) => {
|
||||
if (!series.value?.length) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
grid: {
|
||||
left: 64,
|
||||
right: 24,
|
||||
top: 28,
|
||||
bottom: 32,
|
||||
},
|
||||
backgroundColor: 'transparent',
|
||||
animation: false,
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
textStyle: {
|
||||
color: 'rgba(0, 0, 0, 0.8)',
|
||||
},
|
||||
formatter: function(params: any) {
|
||||
if (!params.length) {
|
||||
return '';
|
||||
}
|
||||
// 表头
|
||||
const allTagKeys = new Set();
|
||||
params.forEach((item: any) => {
|
||||
const tags = series.value?.[item.seriesIndex]?.tags || {};
|
||||
Object.keys(tags).forEach(key => allTagKeys.add(key));
|
||||
});
|
||||
const headerNames = [...allTagKeys, 'value'];
|
||||
const gridColumnCount = headerNames.length;
|
||||
const gridTemplate = `grid-template-columns: repeat(${gridColumnCount}, auto);`;
|
||||
|
||||
// 模板
|
||||
let result = `
|
||||
<div class="chart-tooltip-wrapper">
|
||||
<div class="chart-tooltip-time">${dateFormat(new Date(params[0].value[0]))}</div>
|
||||
<div class="chart-tooltip-header">
|
||||
<div class="chart-tooltip-header-grid" style="${gridTemplate}"">
|
||||
`;
|
||||
|
||||
// 表头
|
||||
headerNames.forEach(key => {
|
||||
const textAlign = key === 'value' ? 'right' : 'left';
|
||||
result += `
|
||||
<div class="chart-tooltip-header-item" style="text-align: ${textAlign};">${key}</div>
|
||||
`;
|
||||
});
|
||||
|
||||
// 数据行
|
||||
params.forEach((item: any) => {
|
||||
const tags = series.value?.[item.seriesIndex]?.tags || {};
|
||||
const value = item.data[1];
|
||||
let displayValue: string;
|
||||
if (value === undefined || value === null) {
|
||||
displayValue = '-';
|
||||
} else {
|
||||
displayValue = MetricUnitFormatter[props.option.unit](value, props.option.unitOption);
|
||||
}
|
||||
headerNames.forEach((key, index) => {
|
||||
const cellValue = key === 'value' ? displayValue : (tags[key as any] || '-');
|
||||
const justifyContent = key === 'value' ? 'flex-end' : 'flex-start';
|
||||
// 圆点
|
||||
const dot = index === 0
|
||||
? `<span class="chart-tooltip-dot" style="background-color: ${item.color};"></span>`
|
||||
: '';
|
||||
result += `
|
||||
<div class="chart-tooltip-col" style="justify-content: ${justifyContent};">${dot}${cellValue}</div>
|
||||
`;
|
||||
});
|
||||
});
|
||||
|
||||
// 闭合
|
||||
result += `</div></div></div>`;
|
||||
return result;
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: 'time',
|
||||
boundaryGap: false,
|
||||
minInterval: WindowUnitFormatter[props.windowUnit].windowInterval(props.windowValue) * 6,
|
||||
axisLabel: {
|
||||
formatter: (value: number) => WindowUnitFormatter[props.windowUnit].dateFormatter(value),
|
||||
rotate: 0,
|
||||
interval: 'auto',
|
||||
margin: 12,
|
||||
hideOverlap: true,
|
||||
color: themeTextColor,
|
||||
},
|
||||
axisLine: {
|
||||
show: false,
|
||||
},
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
axisPointer: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: TimeSeriesColors.BLUE.lineColor,
|
||||
width: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
color: themeTextColor,
|
||||
formatter: (s: number) => MetricUnitFormatter[props.option.unit](s, props.option.unitOption)
|
||||
},
|
||||
axisLine: {
|
||||
show: false,
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: themeLineColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: props.option.legend === true,
|
||||
type: 'scroll'
|
||||
},
|
||||
series: series.value.map((s, index) => {
|
||||
let colors = props.option.colors[index];
|
||||
return createTimeSeries({
|
||||
name: s.name,
|
||||
type: props.option.type,
|
||||
area: props.option.background,
|
||||
lineColor: colors?.[0],
|
||||
itemBorderColor: colors?.[1],
|
||||
data: s.data
|
||||
});
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
// 刷新数据
|
||||
const refresh = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 查询数据
|
||||
const { data } = await getMonitorHostChart({
|
||||
agentKeys: props.agentKeys,
|
||||
range: props.range,
|
||||
measurement: props.option.measurement,
|
||||
fields: props.option.fields,
|
||||
aggregate: props.option.aggregate,
|
||||
window: WindowUnitFormatter[props.windowUnit].windowFormatter(props.windowValue),
|
||||
});
|
||||
series.value = data;
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({ refresh });
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.chart-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.nodata-chart {
|
||||
padding-top: 42px;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
112
orion-visor-ui/src/views/monitor/monitor-detail/index.vue
Normal file
112
orion-visor-ui/src/views/monitor/monitor-detail/index.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<a-spin v-if="hostId"
|
||||
class="container"
|
||||
:loading="!host">
|
||||
<!-- 头部 -->
|
||||
<detail-header v-if="host"
|
||||
v-model:activeKey="activeKey"
|
||||
v-model:chartCompose="chartCompose"
|
||||
:host="host"
|
||||
@reload-chart="reloadChart" />
|
||||
<!-- 内容部分 -->
|
||||
<div v-if="host" class="content-container">
|
||||
<!-- 监控图表 -->
|
||||
<metrics-chart-tab v-show="activeKey === TabKeys.CHART"
|
||||
ref="chartRef"
|
||||
:agentKey="host.agentKey"
|
||||
:chartCompose="chartCompose"
|
||||
:chartRange="chartRange"
|
||||
:chartWindow="chartWindow" />
|
||||
</div>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'monitorDetail'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type HostQueryResponse, getHost } from '@/api/asset/host';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { onMounted, ref, onUnmounted, onActivated, onDeactivated } from 'vue';
|
||||
import { TabKeys } from './types/const';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { useDictStore } from '@/store';
|
||||
import { dictKeys } from './types/const';
|
||||
import { parseWindowUnit, WindowUnitFormatter } from '@/utils/metrics';
|
||||
import DetailHeader from './compoments/detail-header.vue';
|
||||
import MetricsChartTab from './compoments/metrics-chart-tab.vue';
|
||||
|
||||
const hostId = ref<number>();
|
||||
const host = ref<HostQueryResponse>();
|
||||
const activeKey = ref(TabKeys.CHART);
|
||||
const chartCompose = ref(true);
|
||||
|
||||
const chartRef = ref();
|
||||
const reloadChartId = ref<number>();
|
||||
const chartRange = ref<string>('-30m');
|
||||
const chartWindow = ref<string>('1m');
|
||||
|
||||
// 重新加载
|
||||
const reloadChart = (_chartRange: string, _chartWindow: string) => {
|
||||
chartRange.value = _chartRange;
|
||||
chartWindow.value = _chartWindow;
|
||||
// 立即加载和定时加载
|
||||
setTimeout(() => {
|
||||
chartRef.value.reload();
|
||||
}, 50);
|
||||
// 重置定时加载表格;
|
||||
resetReloadChartInterval();
|
||||
};
|
||||
|
||||
// 重置定时加载表格
|
||||
const resetReloadChartInterval = () => {
|
||||
if (!chartWindow.value) {
|
||||
return;
|
||||
}
|
||||
// 清除定时
|
||||
window.clearInterval(reloadChartId.value);
|
||||
// 计算窗口
|
||||
const [windowTime, windowUnit] = parseWindowUnit(chartWindow.value as string);
|
||||
const interval = WindowUnitFormatter[windowUnit].windowInterval(windowTime) + 5000;
|
||||
// 重新设置定时
|
||||
reloadChartId.value = window.setInterval(() => {
|
||||
chartRef.value.reload();
|
||||
}, interval);
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
const route = useRoute();
|
||||
hostId.value = parseInt(route.query.hostId as string);
|
||||
if (!hostId.value) {
|
||||
Message.error('参数错误');
|
||||
return;
|
||||
}
|
||||
// 加载字典项
|
||||
const dictStore = useDictStore();
|
||||
await dictStore.loadKeys(dictKeys);
|
||||
// 查询主机信息
|
||||
const { data } = await getHost(hostId.value);
|
||||
host.value = data;
|
||||
});
|
||||
|
||||
onActivated(resetReloadChartInterval);
|
||||
onDeactivated(() => window.clearInterval(reloadChartId.value));
|
||||
onUnmounted(() => window.clearInterval(reloadChartId.value));
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { WindowUnit, MetricUnitType, MetricUnitFormatOptions } from '@/utils/metrics';
|
||||
|
||||
// 图表组件配置
|
||||
export interface MetricsChartProps {
|
||||
agentKeys: Array<string>;
|
||||
range: string;
|
||||
windowValue: number;
|
||||
windowUnit: WindowUnit;
|
||||
option: MetricsChartOption;
|
||||
}
|
||||
|
||||
// 图表显示配置
|
||||
export interface MetricsChartOption {
|
||||
name: string;
|
||||
type?: 'line' | 'bar';
|
||||
measurement: string;
|
||||
fields: Array<string>;
|
||||
span?: number;
|
||||
legend?: boolean;
|
||||
background?: boolean;
|
||||
colors: Array<[string, string]>;
|
||||
aggregate: string;
|
||||
unit: MetricUnitType;
|
||||
unitOption: MetricUnitFormatOptions;
|
||||
}
|
||||
|
||||
// tab
|
||||
export const TabKeys = {
|
||||
CHART: 'chart'
|
||||
};
|
||||
|
||||
// 探针在线状态 字典项
|
||||
export const OnlineStatusKey = 'agentOnlineStatus';
|
||||
|
||||
// 监控告警开关 字典项
|
||||
export const AlarmSwitchKey = 'monitorAlarmSwitch';
|
||||
|
||||
// 指标图表区间 字典项
|
||||
export const ChartRangeKey = 'metricsChartRange';
|
||||
|
||||
// 指标聚合函数 字典项
|
||||
export const MetricsAggregateKey = 'metricsAggregate';
|
||||
|
||||
// 加载的字典值
|
||||
export const dictKeys = [AlarmSwitchKey, OnlineStatusKey, ChartRangeKey, MetricsAggregateKey];
|
||||
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div v-if="record.agentInstallStatus === AgentInstallStatus.INSTALLED">
|
||||
<!-- 数据列 -->
|
||||
<template v-if="dataCell">
|
||||
<div v-if="!record.metricsData?.noData"
|
||||
class="metrics-wrapper"
|
||||
:class="dataClass">
|
||||
<slot name="default" />
|
||||
</div>
|
||||
<span v-else class="nodata">{{ NODATA_TIPS }}</span>
|
||||
</template>
|
||||
<!-- 非数据列 -->
|
||||
<template v-else>
|
||||
<slot name="default" />
|
||||
</template>
|
||||
</div>
|
||||
<!-- 未安装则不显示 -->
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'monitorCell'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { MonitorHostQueryResponse } from '@/api/monitor/monitor-host';
|
||||
import { NODATA_TIPS } from '@/views/monitor/monitor-host/types/const';
|
||||
import { AgentInstallStatus } from '@/views/asset/host-list/types/const';
|
||||
|
||||
defineProps<{
|
||||
dataCell: boolean;
|
||||
dataClass?: string;
|
||||
record: MonitorHostQueryResponse;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.nodata {
|
||||
color: var(--color-text-3);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.metrics-wrapper) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&.network {
|
||||
flex-direction: column;
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
|
||||
.metrics-value-per {
|
||||
width: 60px;
|
||||
text-align: end;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
|
||||
&::after {
|
||||
content: '%';
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,418 @@
|
||||
<template>
|
||||
<card-list v-model:searchValue="formModel.searchValue"
|
||||
search-input-placeholder="输入 id / 名称 / 编码 / 地址"
|
||||
:create-card-position="false"
|
||||
:loading="loading"
|
||||
:field-config="cardFieldConfig"
|
||||
:list="list"
|
||||
:pagination="pagination"
|
||||
:card-layout-cols="cardColLayout"
|
||||
:filter-count="filterCount"
|
||||
:fields-hook="fieldsHook"
|
||||
:handle-visible="{ disableAdd: true }"
|
||||
@reset="reset"
|
||||
@search="fetchCardData"
|
||||
@page-change="fetchCardData">
|
||||
<!-- 左侧操作 -->
|
||||
<template #leftHandle>
|
||||
<a-space>
|
||||
<!-- 自动刷新 -->
|
||||
<a-tooltip content="开启后每 60s 会自动刷新" mini>
|
||||
<a-button class="card-header-button" @click="toggleAutoRefresh">
|
||||
{{ autoRefresh ? '关闭自动刷新' : '开启自动刷新' }}
|
||||
<template #icon>
|
||||
<icon-refresh />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<!-- 上传发布包 -->
|
||||
<a-button class="card-header-button" @click="emits('openUpload')">
|
||||
上传发布包
|
||||
<template #icon>
|
||||
<icon-upload />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
<!-- 过滤条件 -->
|
||||
<template #filterContent>
|
||||
<a-form :model="formModel"
|
||||
class="card-filter-form"
|
||||
size="small"
|
||||
ref="formRef"
|
||||
label-align="right"
|
||||
:auto-label-width="true"
|
||||
@keyup.enter="() => fetchCardData()">
|
||||
<!-- 主机名称 -->
|
||||
<a-form-item field="name" label="主机名称">
|
||||
<a-input v-model="formModel.name" placeholder="请输入主机名称" allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 主机地址 -->
|
||||
<a-form-item field="address" label="主机地址">
|
||||
<a-input v-model="formModel.address" placeholder="请输入主机地址" allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 在线状态 -->
|
||||
<a-form-item field="agentOnlineStatus" label="在线状态">
|
||||
<a-select v-model="formModel.agentOnlineStatus"
|
||||
:options="toOptions(OnlineStatusKey)"
|
||||
placeholder="请选择在线状态"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 探针状态 -->
|
||||
<a-form-item field="agentInstallStatus" label="探针状态">
|
||||
<a-select v-model="formModel.agentInstallStatus"
|
||||
:options="toOptions(InstallStatusKey)"
|
||||
placeholder="请选择探针状态"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 告警开关 -->
|
||||
<a-form-item field="alarmSwitch" label="告警开关">
|
||||
<a-select v-model="formModel.alarmSwitch"
|
||||
:options="toOptions(AlarmSwitchKey)"
|
||||
placeholder="请选择告警开关"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- agentKey -->
|
||||
<a-form-item field="agentKey" label="agentKey">
|
||||
<a-input v-model="formModel.agentKey"
|
||||
placeholder="请输入 agentKey"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 负责人 -->
|
||||
<a-form-item field="ownerUserId" label="负责人">
|
||||
<user-selector v-model="formModel.ownerUserId"
|
||||
placeholder="请选择负责人"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
<!-- 标题 -->
|
||||
<template #title="{ record }">
|
||||
{{ record.name }}
|
||||
</template>
|
||||
<!-- 主机地址 -->
|
||||
<template #address="{ record }">
|
||||
<span class="span-blue text-copy"
|
||||
title="复制"
|
||||
@click="copy(record.address)">
|
||||
{{ record.address }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- 在线状态 -->
|
||||
<template #agentOnlineStatus="{ record }">
|
||||
<monitor-cell :data-cell="false" :record="record">
|
||||
<a-tooltip :content="'切换分区时间: ' + dateFormat(new Date(record.lastChangeOnlineTime))" mini>
|
||||
<a-tag :color="getDictValue(OnlineStatusKey, record.agentOnlineStatus, 'color')">
|
||||
<template #icon>
|
||||
<component :is="getDictValue(OnlineStatusKey, record.agentOnlineStatus, 'icon')" />
|
||||
</template>
|
||||
{{ getDictValue(OnlineStatusKey, record.agentOnlineStatus) }}
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
</monitor-cell>
|
||||
</template>
|
||||
<!-- cpu -->
|
||||
<template #cpuUsage="{ record }">
|
||||
<monitor-cell :data-cell="true" :record="record">
|
||||
<a-tooltip :content="'CPU' + record.config?.cpuName +': ' + record.metricsData?.cpuUsagePercent?.toFixed(2) + '%'" mini>
|
||||
<a-progress size="large"
|
||||
:animation="true"
|
||||
:show-text="false"
|
||||
:color="getPercentProgressColor(record.metricsData?.cpuUsagePercent / 100)"
|
||||
:percent="record.metricsData.cpuUsagePercent / 100" />
|
||||
</a-tooltip>
|
||||
<span class="metrics-value-per">{{ record.metricsData.cpuUsagePercent.toFixed(2) }}</span>
|
||||
</monitor-cell>
|
||||
</template>
|
||||
<!-- 内存 -->
|
||||
<template #memoryUsage="{ record }">
|
||||
<monitor-cell :data-cell="true" :record="record">
|
||||
<a-tooltip :content="getFileSize(record.metricsData?.memoryUsageBytes)" mini>
|
||||
<a-progress size="large"
|
||||
:animation="true"
|
||||
:show-text="false"
|
||||
:color="getPercentProgressColor(record.metricsData?.memoryUsagePercent / 100)"
|
||||
:percent="record.metricsData.memoryUsagePercent / 100" />
|
||||
</a-tooltip>
|
||||
<span class="metrics-value-per">{{ record.metricsData?.memoryUsagePercent?.toFixed(2) }}</span>
|
||||
</monitor-cell>
|
||||
</template>
|
||||
<!-- 磁盘 -->
|
||||
<template #diskUsage="{ record }">
|
||||
<monitor-cell :data-cell="true" :record="record">
|
||||
<a-tooltip :content="record.config?.diskName +': ' + getFileSize(record.metricsData?.diskUsageBytes)" mini>
|
||||
<a-progress size="large"
|
||||
:animation="true"
|
||||
:show-text="false"
|
||||
:color="getPercentProgressColor(record.metricsData?.diskUsagePercent / 100)"
|
||||
:percent="record.metricsData.diskUsagePercent / 100" />
|
||||
</a-tooltip>
|
||||
<span class="metrics-value-per">{{ record.metricsData?.diskUsagePercent?.toFixed(2) }}</span>
|
||||
</monitor-cell>
|
||||
</template>
|
||||
<!-- 网络 -->
|
||||
<template #network="{ record }">
|
||||
<monitor-cell data-class="network"
|
||||
:data-cell="true"
|
||||
:record="record">
|
||||
<div class="network-inline">
|
||||
<!-- 上行速度 -->
|
||||
<a-tooltip :content="record.config?.networkName +': ' + getFileSize(record.metricsData?.networkSentPreBytes) + '/s'" mini>
|
||||
<b class="span-green fs12" title="上行速度">
|
||||
<icon-arrow-up />
|
||||
{{ getFileSize(record.metricsData?.networkSentPreBytes) }}/s
|
||||
</b>
|
||||
</a-tooltip>
|
||||
<!-- 下行速度 -->
|
||||
<a-tooltip :content="record.config?.networkName +': ' + getFileSize(record.metricsData?.networkRecvPreBytes) + '/s'" mini>
|
||||
<b class="span-blue fs12" title="下行速度">
|
||||
<icon-arrow-down />
|
||||
{{ getFileSize(record.metricsData?.networkRecvPreBytes) }}/s
|
||||
</b>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</monitor-cell>
|
||||
</template>
|
||||
<!-- 负载 -->
|
||||
<template #load="{ record }">
|
||||
<monitor-cell :data-cell="true" :record="record">
|
||||
<b class="fs12">
|
||||
{{ record.metricsData?.load1?.toFixed(2) }}, {{ record.metricsData?.load5?.toFixed(2) }}, {{ record.metricsData?.load15?.toFixed(2) }}
|
||||
</b>
|
||||
</monitor-cell>
|
||||
</template>
|
||||
<!-- 告警策略 -->
|
||||
<template #alarmPolicy="{ record }">
|
||||
<monitor-cell :data-cell="false" :record="record">
|
||||
{{ getDictValue(AlarmSwitchKey, record.alarmSwitch) }}
|
||||
</monitor-cell>
|
||||
</template>
|
||||
<!-- 告警负责人 -->
|
||||
<template #ownerUsername="{ record }">
|
||||
<monitor-cell :data-cell="false" :record="record">
|
||||
{{ record.ownerUsername }}
|
||||
</monitor-cell>
|
||||
</template>
|
||||
<!-- 标签 -->
|
||||
<template #tags="{ record }">
|
||||
<a-space v-if="record.tags?.length"
|
||||
style="margin-bottom: -8px;"
|
||||
:wrap="true">
|
||||
<a-tag v-for="tag in record.tags"
|
||||
:key="tag.id"
|
||||
:color="dataColor(tag.name, tagColor)">
|
||||
{{ tag.name }}
|
||||
</a-tag>
|
||||
</a-space>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<!-- agentKey -->
|
||||
<template #agentKey="{ record }">
|
||||
<span class="text-copy text-ellipsis"
|
||||
:title="record.agentKey"
|
||||
@click="copy(record.agentKey, true)">
|
||||
{{ record.agentKey }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- 探针版本 -->
|
||||
<template #agentVersion="{ record }">
|
||||
<!-- 安装状态 -->
|
||||
<div v-if="record.installLog?.status === AgentLogStatus.WAIT
|
||||
|| record.installLog?.status === AgentLogStatus.RUNNING
|
||||
|| record.installLog?.status === AgentLogStatus.FAILED"
|
||||
class="flex-center">
|
||||
<!-- 当前状态 -->
|
||||
<a-tag :color="getDictValue(AgentLogStatusKey, record.installLog.status, 'color')"
|
||||
:loading="getDictValue(AgentLogStatusKey, record.installLog.status, 'loading')">
|
||||
{{ getDictValue(AgentLogStatusKey, record.installLog.status, 'installLabel') }}
|
||||
</a-tag>
|
||||
<!-- 提示信息 -->
|
||||
<a-tooltip v-if="record.installLog.message"
|
||||
:content="record.installLog.message"
|
||||
mini>
|
||||
<icon-question-circle class="fs16 span-red ml4" />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<!-- 已安装显示版本号 -->
|
||||
<b v-else-if="record.agentInstallStatus === AgentInstallStatus.INSTALLED"
|
||||
class="fs12"
|
||||
:class="record.latestVersion && record.latestVersion !== record.agentVersion ? 'span-red' : ''">
|
||||
{{ record.agentVersion ? 'v' + record.agentVersion : '-' }}
|
||||
<a-tooltip v-if="record.latestVersion && record.latestVersion !== record.agentVersion"
|
||||
:content="'存在新版本 v' + record.latestVersion + ', 请及时升级'"
|
||||
mini>
|
||||
<icon-arrow-rise />
|
||||
</a-tooltip>
|
||||
</b>
|
||||
<!-- 显示未安装 -->
|
||||
<span v-else>
|
||||
<a-tag>{{ getDictValue(InstallStatusKey, record.agentInstallStatus) }}</a-tag>
|
||||
</span>
|
||||
</template>
|
||||
<!-- 拓展操作 -->
|
||||
<template #extra="{ record }">
|
||||
<a-space>
|
||||
<a-button v-permission="['monitor:monitor-host:query']"
|
||||
type="text"
|
||||
size="mini"
|
||||
:disabled="record.agentInstallStatus !== AgentInstallStatus.INSTALLED"
|
||||
@click="openDetail(record.hostId, record.name)">
|
||||
详情
|
||||
</a-button>
|
||||
<!-- 更多操作 -->
|
||||
<a-dropdown trigger="hover" :popup-max-height="false">
|
||||
<icon-more class="card-extra-icon" />
|
||||
<template #content>
|
||||
<!-- 修改 -->
|
||||
<a-doption v-if="record.agentInstallStatus === AgentInstallStatus.INSTALLED"
|
||||
v-permission="['monitor:monitor-host:update']"
|
||||
type="text"
|
||||
size="mini"
|
||||
@click="emits('openUpdate', record)">
|
||||
<span class="more-doption normal">修改配置</span>
|
||||
</a-doption>
|
||||
<!-- 安装探针 -->
|
||||
<a-doption v-permission="['asset:host:install-agent']"
|
||||
:disabled="record.installLog?.status === AgentLogStatus.WAIT || record.installLog?.status === AgentLogStatus.RUNNING"
|
||||
type="text"
|
||||
size="mini"
|
||||
@click="installAgent([record.hostId])">
|
||||
<span class="more-doption normal">安装探针</span>
|
||||
</a-doption>
|
||||
<!-- 安装成功 -->
|
||||
<a-doption v-if="record.installLog?.id && record.installLog?.status !== AgentLogStatus.SUCCESS"
|
||||
v-permission="['asset:host:install-agent']"
|
||||
type="text"
|
||||
size="mini"
|
||||
@click="setInstallSuccess(record.installLog)">
|
||||
<span class="more-doption normal">安装成功</span>
|
||||
</a-doption>
|
||||
<!-- 报警开关 -->
|
||||
<a-doption v-if="record.id"
|
||||
v-permission="['monitor:monitor-host:update', 'monitor:monitor-host:update-switch']"
|
||||
type="text"
|
||||
size="mini"
|
||||
@click="toggleAlarmSwitch(record)">
|
||||
<span class="more-doption normal">
|
||||
{{ toggleDictValue(AlarmSwitchKey, record.alarmSwitch, 'label') + '报警' }}
|
||||
</span>
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
</template>
|
||||
</card-list>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'monitorHostCardList'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { MonitorHostQueryRequest, MonitorHostQueryResponse } from '@/api/monitor/monitor-host';
|
||||
import { useCardPagination, useCardColLayout, useCardFieldConfig } from '@/hooks/card';
|
||||
import { computed, reactive, ref, onMounted, } from 'vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { dateFormat, objectTruthKeyCount, resetObject, dataColor } from '@/utils';
|
||||
import fieldConfig from '../types/card.fields';
|
||||
import { TableName, AlarmSwitchKey, OnlineStatusKey, InstallStatusKey, AgentLogStatus, AgentLogStatusKey } from '../types/const';
|
||||
import { getMonitorHostPage } from '@/api/monitor/monitor-host';
|
||||
import { useDictStore } from '@/store';
|
||||
import { AgentInstallStatus, tagColor } from '@/views/asset/host-list/types/const';
|
||||
import { copy } from '@/hooks/copy';
|
||||
import { getFileSize } from '@/utils/file';
|
||||
import { getPercentProgressColor } from '@/utils/charts';
|
||||
import useMonitorHostList from '../types/use-monitor-host-list';
|
||||
import MonitorCell from './monitor-cell.vue';
|
||||
import UserSelector from '@/components/user/user/selector/index.vue';
|
||||
|
||||
const emits = defineEmits(['openUpdate', 'openUpload']);
|
||||
|
||||
const cardColLayout = useCardColLayout();
|
||||
const pagination = useCardPagination();
|
||||
const { loading, setLoading } = useLoading();
|
||||
const { cardFieldConfig, fieldsHook } = useCardFieldConfig(TableName, fieldConfig);
|
||||
const { toOptions, getDictValue, toggleDictValue } = useDictStore();
|
||||
|
||||
const list = ref<Array<MonitorHostQueryResponse>>([]);
|
||||
const formRef = ref();
|
||||
const formModel = reactive<MonitorHostQueryRequest>({
|
||||
searchValue: undefined,
|
||||
alarmSwitch: undefined,
|
||||
ownerUserId: undefined,
|
||||
policyId: undefined,
|
||||
name: undefined,
|
||||
code: undefined,
|
||||
address: undefined,
|
||||
agentInstallStatus: AgentInstallStatus.INSTALLED,
|
||||
agentOnlineStatus: undefined,
|
||||
description: undefined,
|
||||
tags: undefined,
|
||||
});
|
||||
|
||||
// 条件数量
|
||||
const filterCount = computed(() => {
|
||||
return objectTruthKeyCount(formModel, ['searchValue']);
|
||||
});
|
||||
|
||||
// 重新加载
|
||||
const reload = () => {
|
||||
// 重新加载数据
|
||||
fetchCardData();
|
||||
};
|
||||
|
||||
defineExpose({ reload });
|
||||
|
||||
const {
|
||||
autoRefresh,
|
||||
openDetail,
|
||||
toggleAutoRefresh,
|
||||
installAgent,
|
||||
setInstallSuccess,
|
||||
toggleAlarmSwitch,
|
||||
} = useMonitorHostList({
|
||||
hosts: list,
|
||||
setLoading,
|
||||
reload,
|
||||
});
|
||||
|
||||
// 重置条件
|
||||
const reset = () => {
|
||||
resetObject(formModel);
|
||||
fetchCardData();
|
||||
};
|
||||
|
||||
// 加载数据
|
||||
const doFetchCardData = async (request: MonitorHostQueryRequest) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data } = await getMonitorHostPage(request);
|
||||
list.value = data.rows;
|
||||
pagination.total = data.total;
|
||||
pagination.current = request.page;
|
||||
pagination.pageSize = request.limit;
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 切换页码
|
||||
const fetchCardData = (page = 1, limit = pagination.pageSize, form = formModel) => {
|
||||
doFetchCardData({ page, limit, ...form });
|
||||
};
|
||||
|
||||
onMounted(fetchCardData);
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.network-inline {
|
||||
b {
|
||||
width: 100px;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<a-drawer v-model:visible="visible"
|
||||
title="修改监控主机配置"
|
||||
:width="470"
|
||||
:mask-closable="false"
|
||||
:unmount-on-close="true"
|
||||
:ok-button-props="{ disabled: loading }"
|
||||
:cancel-button-props="{ disabled: loading }"
|
||||
:on-before-ok="handlerOk"
|
||||
@cancel="handleClose">
|
||||
<a-spin class="full drawer-form-small" :loading="loading">
|
||||
<a-form :model="formModel"
|
||||
ref="formRef"
|
||||
label-align="right"
|
||||
:label-width="120"
|
||||
:rules="formRules">
|
||||
<!-- 主机名称 -->
|
||||
<a-form-item field="name" label="主机名称">
|
||||
<a-input v-model="hostRecord.name" readonly />
|
||||
</a-form-item>
|
||||
<!-- 主机地址 -->
|
||||
<a-form-item field="address" label="主机地址">
|
||||
<a-input v-model="hostRecord.address" readonly />
|
||||
</a-form-item>
|
||||
<!-- 负责人 -->
|
||||
<a-form-item field="ownerUserId" label="负责人">
|
||||
<user-selector v-model="formModel.ownerUserId"
|
||||
placeholder="请选择负责人"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 告警开关 -->
|
||||
<a-form-item field="alarmSwitch"
|
||||
label="告警开关"
|
||||
style="margin-bottom: 0;"
|
||||
hide-asterisk>
|
||||
<a-switch v-model="formModel.alarmSwitch"
|
||||
type="round"
|
||||
:checked-value="AlarmSwitch.ON"
|
||||
:unchecked-value="AlarmSwitch.OFF"
|
||||
checked-text="开"
|
||||
unchecked-text="关"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 展示设置 -->
|
||||
<a-divider direction="horizontal">
|
||||
展示设置
|
||||
</a-divider>
|
||||
<!-- CPU -->
|
||||
<!-- <a-form-item field="cpuName" label="CPU">-->
|
||||
<!-- <a-select v-model="formModel.cpuName"-->
|
||||
<!-- :options="hostRecord.meta?.cpus || []"-->
|
||||
<!-- placeholder="请选择展示的CPU" />-->
|
||||
<!-- </a-form-item>-->
|
||||
<!-- 磁盘 -->
|
||||
<a-form-item field="diskName" label="磁盘">
|
||||
<a-select v-model="formModel.diskName"
|
||||
:options="hostRecord.meta?.disks || []"
|
||||
placeholder="请选择展示的磁盘名称" />
|
||||
</a-form-item>
|
||||
<!-- 网卡 -->
|
||||
<a-form-item field="networkName" label="网卡">
|
||||
<a-select v-model="formModel.networkName"
|
||||
:options="hostRecord.meta?.nets || []"
|
||||
placeholder="请选择展示的网卡名称" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-spin>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'monitorHostFormDrawer'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { MonitorHostQueryResponse, MonitorHostUpdateRequest } from '@/api/monitor/monitor-host';
|
||||
import { ref } from 'vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import formRules from '../types/form.rules';
|
||||
import { AlarmSwitch, } from '../types/const';
|
||||
import { updateMonitorHost } from '@/api/monitor/monitor-host';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import UserSelector from '@/components/user/user/selector/index.vue';
|
||||
|
||||
const emits = defineEmits(['updated']);
|
||||
|
||||
const { visible, setVisible } = useVisible();
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
const formRef = ref<any>();
|
||||
const formModel = ref<MonitorHostUpdateRequest>({});
|
||||
const hostRecord = ref<MonitorHostQueryResponse>({} as MonitorHostQueryResponse);
|
||||
|
||||
// 打开修改
|
||||
const openUpdate = (record: MonitorHostQueryResponse) => {
|
||||
hostRecord.value = record;
|
||||
formModel.value = {
|
||||
id: record.id,
|
||||
policyId: record.policyId,
|
||||
alarmSwitch: record.alarmSwitch,
|
||||
ownerUserId: record.ownerUserId,
|
||||
cpuName: record.config?.cpuName,
|
||||
diskName: record.config?.diskName,
|
||||
networkName: record.config?.networkName,
|
||||
};
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
defineExpose({ openUpdate });
|
||||
|
||||
// 确定
|
||||
const handlerOk = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 验证参数
|
||||
const error = await formRef.value.validate();
|
||||
if (error) {
|
||||
return false;
|
||||
}
|
||||
// 修改
|
||||
await updateMonitorHost(formModel.value);
|
||||
Message.success('修改成功');
|
||||
emits('updated');
|
||||
// 清空
|
||||
handlerClear();
|
||||
} catch (e) {
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭
|
||||
const handleClose = () => {
|
||||
handlerClear();
|
||||
};
|
||||
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,484 @@
|
||||
<template>
|
||||
<!-- 搜索 -->
|
||||
<a-card class="general-card table-search-card">
|
||||
<query-header :model="formModel"
|
||||
label-align="left"
|
||||
@submit="fetchTableData"
|
||||
@reset="fetchTableData"
|
||||
@keyup.enter="() => fetchTableData()">
|
||||
<!-- 主机名称 -->
|
||||
<a-form-item field="name" label="主机名称">
|
||||
<a-input v-model="formModel.name" placeholder="请输入主机名称" allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 主机地址 -->
|
||||
<a-form-item field="address" label="主机地址">
|
||||
<a-input v-model="formModel.address" placeholder="请输入主机地址" allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 在线状态 -->
|
||||
<a-form-item field="agentOnlineStatus" label="在线状态">
|
||||
<a-select v-model="formModel.agentOnlineStatus"
|
||||
:options="toOptions(OnlineStatusKey)"
|
||||
placeholder="请选择在线状态"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 探针状态 -->
|
||||
<a-form-item field="agentInstallStatus" label="探针状态">
|
||||
<a-select v-model="formModel.agentInstallStatus"
|
||||
:options="toOptions(InstallStatusKey)"
|
||||
placeholder="请选择探针状态"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 告警开关 -->
|
||||
<a-form-item field="alarmSwitch" label="告警开关">
|
||||
<a-select v-model="formModel.alarmSwitch"
|
||||
:options="toOptions(AlarmSwitchKey)"
|
||||
placeholder="请选择告警开关"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- agentKey -->
|
||||
<a-form-item field="agentKey" label="agentKey">
|
||||
<a-input v-model="formModel.agentKey"
|
||||
placeholder="请输入 agentKey"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 负责人 -->
|
||||
<a-form-item field="ownerUserId" label="负责人">
|
||||
<user-selector v-model="formModel.ownerUserId"
|
||||
placeholder="请选择负责人"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
</query-header>
|
||||
</a-card>
|
||||
<!-- 表格 -->
|
||||
<a-card class="general-card table-card">
|
||||
<template #title>
|
||||
<!-- 左侧操作 -->
|
||||
<div class="table-left-bar-handle">
|
||||
<!-- 标题 -->
|
||||
<div class="table-title">
|
||||
监控主机列表
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧操作 -->
|
||||
<div class="table-right-bar-handle">
|
||||
<a-space>
|
||||
<!-- 安装 -->
|
||||
<a-button v-permission="['asset:host:install-agent']"
|
||||
type="primary"
|
||||
:disabled="selectedKeys.length === 0"
|
||||
@click="installAgent(selectedKeys)">
|
||||
安装
|
||||
<template #icon>
|
||||
<icon-plus />
|
||||
</template>
|
||||
</a-button>
|
||||
<!-- 自动刷新 -->
|
||||
<a-tooltip content="开启后每 60s 会自动刷新" mini>
|
||||
<a-button @click="toggleAutoRefresh">
|
||||
{{ autoRefresh ? '关闭自动刷新' : '开启自动刷新' }}
|
||||
<template #icon>
|
||||
<icon-refresh />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<!-- 上传发布包 -->
|
||||
<a-button @click="emits('openUpload')">
|
||||
上传发布包
|
||||
<template #icon>
|
||||
<icon-upload />
|
||||
</template>
|
||||
</a-button>
|
||||
<!-- 调整 -->
|
||||
<table-adjust :columns="columns"
|
||||
:columns-hook="columnsHook"
|
||||
@query="fetchTableData" />
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
<!-- table -->
|
||||
<a-table v-model:selected-keys="selectedKeys"
|
||||
row-key="hostId"
|
||||
ref="tableRef"
|
||||
:loading="loading"
|
||||
:columns="tableColumns"
|
||||
:data="tableRenderData"
|
||||
:pagination="pagination"
|
||||
:row-class="setRowClassName"
|
||||
:row-selection="rowSelection"
|
||||
:bordered="false"
|
||||
@page-change="(page: number) => fetchTableData(page, pagination.pageSize)"
|
||||
@page-size-change="(size: number) => fetchTableData(1, size)">
|
||||
<!-- 主机信息 -->
|
||||
<template #hostInfo="{ record }">
|
||||
<div class="info-wrapper">
|
||||
<div class="info-item">
|
||||
<span class="info-label">主机名称</span>
|
||||
<span class="info-value text-copy text-ellipsis"
|
||||
:title="record.name"
|
||||
@click="copy(record.name, true)">
|
||||
{{ record.name }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">主机地址</span>
|
||||
<span class="info-value span-blue text-copy text-ellipsis"
|
||||
:title="record.address"
|
||||
@click="copy(record.address, true)">
|
||||
{{ record.address }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 在线状态 -->
|
||||
<template #agentOnlineStatus="{ record }">
|
||||
<monitor-cell :data-cell="false" :record="record">
|
||||
<a-tooltip :content="'切换分区时间: ' + dateFormat(new Date(record.lastChangeOnlineTime))" mini>
|
||||
<a-tag :color="getDictValue(OnlineStatusKey, record.agentOnlineStatus, 'color')">
|
||||
<template #icon>
|
||||
<component :is="getDictValue(OnlineStatusKey, record.agentOnlineStatus, 'icon')" />
|
||||
</template>
|
||||
{{ getDictValue(OnlineStatusKey, record.agentOnlineStatus) }}
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
</monitor-cell>
|
||||
</template>
|
||||
<!-- cpu -->
|
||||
<template #cpuUsage="{ record }">
|
||||
<monitor-cell :data-cell="true" :record="record">
|
||||
<a-tooltip :content="'CPU' + record.config?.cpuName +': ' + record.metricsData?.cpuUsagePercent?.toFixed(2) + '%'" mini>
|
||||
<a-progress size="large"
|
||||
width="120px"
|
||||
:animation="true"
|
||||
:show-text="false"
|
||||
:color="getPercentProgressColor(record.metricsData.cpuUsagePercent / 100)"
|
||||
:percent="record.metricsData.cpuUsagePercent / 100" />
|
||||
</a-tooltip>
|
||||
<span class="metrics-value-per">{{ record.metricsData.cpuUsagePercent.toFixed(2) }}</span>
|
||||
</monitor-cell>
|
||||
</template>
|
||||
<!-- 内存 -->
|
||||
<template #memoryUsage="{ record }">
|
||||
<monitor-cell :data-cell="true" :record="record">
|
||||
<a-tooltip :content="getFileSize(record.metricsData?.memoryUsageBytes)" mini>
|
||||
<a-progress size="large"
|
||||
width="120px"
|
||||
:animation="true"
|
||||
:show-text="false"
|
||||
:color="getPercentProgressColor(record.metricsData.memoryUsagePercent / 100)"
|
||||
:percent="record.metricsData.memoryUsagePercent / 100" />
|
||||
</a-tooltip>
|
||||
<span class="metrics-value-per">{{ record.metricsData.memoryUsagePercent.toFixed(2) }}</span>
|
||||
</monitor-cell>
|
||||
</template>
|
||||
<!-- 磁盘 -->
|
||||
<template #diskUsage="{ record }">
|
||||
<monitor-cell :data-cell="true" :record="record">
|
||||
<a-tooltip :content="record.config?.diskName +': ' + getFileSize(record.metricsData?.diskUsageBytes)" mini>
|
||||
<a-progress size="large"
|
||||
width="120px"
|
||||
:animation="true"
|
||||
:show-text="false"
|
||||
:color="getPercentProgressColor(record.metricsData.diskUsagePercent / 100)"
|
||||
:percent="record.metricsData.diskUsagePercent / 100" />
|
||||
</a-tooltip>
|
||||
<span class="metrics-value-per">{{ record.metricsData.diskUsagePercent.toFixed(2) }}</span>
|
||||
</monitor-cell>
|
||||
</template>
|
||||
<!-- 网络 -->
|
||||
<template #network="{ record }">
|
||||
<monitor-cell data-class="network"
|
||||
:data-cell="true"
|
||||
:record="record">
|
||||
<!-- 上行速度 -->
|
||||
<a-tooltip :content="record.config?.networkName +': ' + getFileSize(record.metricsData?.networkSentPreBytes) + '/s'" mini>
|
||||
<b class="span-green" title="上行速度">
|
||||
<icon-arrow-up class="mr2" />
|
||||
{{ getFileSize(record.metricsData.networkSentPreBytes) }}/s
|
||||
</b>
|
||||
</a-tooltip>
|
||||
<!-- 下行速度 -->
|
||||
<a-tooltip :content="record.config?.networkName +': ' + getFileSize(record.metricsData?.networkRecvPreBytes) + '/s'" mini>
|
||||
<b class="mt2 span-blue" title="下行速度">
|
||||
<icon-arrow-down class="mr2" />
|
||||
{{ getFileSize(record.metricsData.networkRecvPreBytes) }}/s
|
||||
</b>
|
||||
</a-tooltip>
|
||||
</monitor-cell>
|
||||
</template>
|
||||
<!-- 负载 -->
|
||||
<template #load="{ record }">
|
||||
<monitor-cell :data-cell="true" :record="record">
|
||||
<b>{{ record.metricsData?.load1?.toFixed(2) }}, {{ record.metricsData?.load5?.toFixed(2) }}, {{ record.metricsData?.load15?.toFixed(2) }}</b>
|
||||
</monitor-cell>
|
||||
</template>
|
||||
<!-- 告警策略 -->
|
||||
<template #alarmPolicy="{ record }">
|
||||
<monitor-cell :data-cell="false" :record="record">
|
||||
{{ getDictValue(AlarmSwitchKey, record.alarmSwitch) }}
|
||||
</monitor-cell>
|
||||
</template>
|
||||
<!-- 告警负责人 -->
|
||||
<template #ownerUsername="{ record }">
|
||||
<monitor-cell :data-cell="false" :record="record">
|
||||
{{ record.ownerUsername }}
|
||||
</monitor-cell>
|
||||
</template>
|
||||
<!-- 探针版本 -->
|
||||
<template #agentVersion="{ record }">
|
||||
<!-- 安装状态 -->
|
||||
<div v-if="record.installLog?.status === AgentLogStatus.WAIT
|
||||
|| record.installLog?.status === AgentLogStatus.RUNNING
|
||||
|| record.installLog?.status === AgentLogStatus.FAILED"
|
||||
class="flex-center">
|
||||
<!-- 当前状态 -->
|
||||
<a-tag :color="getDictValue(AgentLogStatusKey, record.installLog.status, 'color')"
|
||||
:loading="getDictValue(AgentLogStatusKey, record.installLog.status, 'loading')">
|
||||
{{ getDictValue(AgentLogStatusKey, record.installLog.status, 'installLabel') }}
|
||||
</a-tag>
|
||||
<!-- 提示信息 -->
|
||||
<a-tooltip v-if="record.installLog.message"
|
||||
:content="record.installLog.message"
|
||||
mini>
|
||||
<icon-question-circle class="fs16 span-red ml4" />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<!-- 已安装显示版本号 -->
|
||||
<b v-else-if="record.agentInstallStatus === AgentInstallStatus.INSTALLED"
|
||||
:class="record.latestVersion && record.latestVersion !== record.agentVersion ? 'span-red' : ''">
|
||||
{{ record.agentVersion ? 'v' + record.agentVersion : '-' }}
|
||||
<a-tooltip v-if="record.latestVersion && record.latestVersion !== record.agentVersion"
|
||||
:content="'存在新版本 v' + record.latestVersion + ', 请及时升级'"
|
||||
mini>
|
||||
<icon-arrow-rise />
|
||||
</a-tooltip>
|
||||
</b>
|
||||
<!-- 显示未安装 -->
|
||||
<span v-else>
|
||||
<a-tag>{{ getDictValue(InstallStatusKey, record.agentInstallStatus) }}</a-tag>
|
||||
</span>
|
||||
</template>
|
||||
<!-- 标签 -->
|
||||
<template #tags="{ record }">
|
||||
<a-space v-if="record.tags?.length"
|
||||
style="margin-bottom: -8px;"
|
||||
:wrap="true">
|
||||
<template v-for="tag in record.tags"
|
||||
:key="tag.id">
|
||||
<a-tag :color="dataColor(tag.name, tagColor)">
|
||||
{{ tag.name }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</a-space>
|
||||
</template>
|
||||
<!-- agentKey -->
|
||||
<template #agentKey="{ record }">
|
||||
<span class="text-copy text-ellipsis"
|
||||
:title="record.agentKey"
|
||||
@click="copy(record.agentKey, true)">
|
||||
{{ record.agentKey }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- 操作 -->
|
||||
<template #handle="{ record }">
|
||||
<div class="table-handle-wrapper">
|
||||
<a-button v-permission="['monitor:monitor-host:query']"
|
||||
type="text"
|
||||
size="mini"
|
||||
:disabled="record.agentInstallStatus !== AgentInstallStatus.INSTALLED"
|
||||
@click="openDetail(record.hostId, record.name)">
|
||||
详情
|
||||
</a-button>
|
||||
<a-dropdown trigger="hover" :popup-max-height="false">
|
||||
<a-button type="text" size="mini">
|
||||
操作
|
||||
</a-button>
|
||||
<template #content>
|
||||
<!-- 修改 -->
|
||||
<a-doption v-if="record.agentInstallStatus === AgentInstallStatus.INSTALLED"
|
||||
v-permission="['monitor:monitor-host:update']"
|
||||
type="text"
|
||||
size="mini"
|
||||
@click="emits('openUpdate', record)">
|
||||
<span class="more-doption normal">修改配置</span>
|
||||
</a-doption>
|
||||
<!-- 安装探针 -->
|
||||
<a-doption v-permission="['asset:host:install-agent']"
|
||||
:disabled="record.installLog?.status === AgentLogStatus.WAIT || record.installLog?.status === AgentLogStatus.RUNNING"
|
||||
type="text"
|
||||
size="mini"
|
||||
@click="installAgent([record.hostId])">
|
||||
<span class="more-doption normal">安装探针</span>
|
||||
</a-doption>
|
||||
<!-- 安装成功 -->
|
||||
<a-doption v-if="record.installLog?.id && record.installLog?.status !== AgentLogStatus.SUCCESS"
|
||||
v-permission="['asset:host:install-agent']"
|
||||
type="text"
|
||||
size="mini"
|
||||
@click="setInstallSuccess(record.installLog)">
|
||||
<span class="more-doption normal">安装成功</span>
|
||||
</a-doption>
|
||||
<!-- 报警开关 -->
|
||||
<a-doption v-if="record.id"
|
||||
v-permission="['monitor:monitor-host:update', 'monitor:monitor-host:update-switch']"
|
||||
type="text"
|
||||
size="mini"
|
||||
@click="toggleAlarmSwitch(record)">
|
||||
<span class="more-doption normal">
|
||||
{{ toggleDictValue(AlarmSwitchKey, record.alarmSwitch, 'label') + '报警' }}
|
||||
</span>
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'monitorHostTable'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { MonitorHostQueryRequest, MonitorHostQueryResponse } from '@/api/monitor/monitor-host';
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import { getMonitorHostPage } from '@/api/monitor/monitor-host';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import columns from '../types/table.columns';
|
||||
import { TableName, AlarmSwitchKey, OnlineStatusKey, InstallStatusKey, AgentLogStatus, AgentLogStatusKey } from '../types/const';
|
||||
import { AgentInstallStatus, tagColor } from '@/views/asset/host-list/types/const';
|
||||
import { useTablePagination, useTableColumns, useRowSelection } from '@/hooks/table';
|
||||
import { useDictStore } from '@/store';
|
||||
import { copy } from '@/hooks/copy';
|
||||
import { getPercentProgressColor } from '@/utils/charts';
|
||||
import { getFileSize } from '@/utils/file';
|
||||
import { dateFormat, dataColor } from '@/utils';
|
||||
import useMonitorHostList from '../types/use-monitor-host-list';
|
||||
import MonitorCell from './monitor-cell.vue';
|
||||
import TableAdjust from '@/components/app/table-adjust/index.vue';
|
||||
import UserSelector from '@/components/user/user/selector/index.vue';
|
||||
|
||||
const emits = defineEmits(['openUpdate', 'openUpload']);
|
||||
|
||||
const rowSelection = useRowSelection();
|
||||
const pagination = useTablePagination();
|
||||
const { loading, setLoading } = useLoading();
|
||||
const { tableColumns, columnsHook } = useTableColumns(TableName, columns);
|
||||
const { toOptions, getDictValue, toggleDictValue } = useDictStore();
|
||||
|
||||
const selectedKeys = ref<Array<number>>([]);
|
||||
const tableRenderData = ref<Array<MonitorHostQueryResponse>>([]);
|
||||
const formModel = reactive<MonitorHostQueryRequest>({
|
||||
alarmSwitch: undefined,
|
||||
ownerUserId: undefined,
|
||||
policyId: undefined,
|
||||
name: undefined,
|
||||
code: undefined,
|
||||
address: undefined,
|
||||
agentInstallStatus: undefined,
|
||||
agentOnlineStatus: undefined,
|
||||
description: undefined,
|
||||
tags: undefined,
|
||||
});
|
||||
|
||||
// 重新加载
|
||||
const reload = () => {
|
||||
// 重新加载数据
|
||||
fetchTableData();
|
||||
};
|
||||
|
||||
defineExpose({ reload });
|
||||
|
||||
const {
|
||||
autoRefresh,
|
||||
openDetail,
|
||||
toggleAutoRefresh,
|
||||
installAgent,
|
||||
setInstallSuccess,
|
||||
toggleAlarmSwitch,
|
||||
} = useMonitorHostList({
|
||||
hosts: tableRenderData,
|
||||
setLoading,
|
||||
reload,
|
||||
});
|
||||
|
||||
// 获取行样式
|
||||
const setRowClassName = (record: MonitorHostQueryResponse) => {
|
||||
if (record.agentInstallStatus === AgentInstallStatus.NOT_INSTALL) {
|
||||
return 'not-install';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
// 加载数据
|
||||
const doFetchTableData = async (request: MonitorHostQueryRequest) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data } = await getMonitorHostPage(request);
|
||||
tableRenderData.value = data.rows;
|
||||
pagination.total = data.total;
|
||||
pagination.current = request.page;
|
||||
pagination.pageSize = request.limit;
|
||||
selectedKeys.value = [];
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 切换页码
|
||||
const fetchTableData = (page = 1, limit = pagination.pageSize, form = formModel) => {
|
||||
doFetchTableData({ page, limit, ...form });
|
||||
};
|
||||
|
||||
onMounted(fetchTableData);
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
:deep(.not-install) {
|
||||
.arco-table-td {
|
||||
background-color: rgb(var(--gray-1)) !important;
|
||||
}
|
||||
|
||||
.arco-table-td::before {
|
||||
background-color: rgb(var(--gray-1)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.info-wrapper {
|
||||
padding: 4px 0;
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
width: 60px;
|
||||
margin-right: 8px;
|
||||
user-select: none;
|
||||
font-weight: 600;
|
||||
|
||||
&::after {
|
||||
content: ':';
|
||||
}
|
||||
}
|
||||
|
||||
.info-value {
|
||||
width: calc(100% - 68px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.row-handle-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<a-modal v-model:visible="visible"
|
||||
top="80px"
|
||||
width="400px"
|
||||
title-align="start"
|
||||
title="探针发布包上传"
|
||||
ok-text="上传"
|
||||
:body-style="{ padding: 0 }"
|
||||
:align-center="false"
|
||||
:mask-closable="false"
|
||||
:unmount-on-close="true"
|
||||
:on-before-ok="handlerOk"
|
||||
:ok-button-props="{ disabled: !fileList.length }"
|
||||
@cancel="handleClose">
|
||||
<a-spin class="upload-container" :loading="loading">
|
||||
<!-- 选择文件 -->
|
||||
<a-upload class="file-list-uploader"
|
||||
v-model:file-list="fileList"
|
||||
accept=".tar.gz"
|
||||
:auto-upload="false"
|
||||
:show-file-list="true"
|
||||
draggable
|
||||
@change="onSelectFile" />
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'releaseUploadModal'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FileItem } from '@arco-design/web-vue';
|
||||
import { ref } from 'vue';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { uploadAgentRelease } from '@/api/asset/host-agent';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import useLoading from '@/hooks/loading';
|
||||
|
||||
const { visible, setVisible } = useVisible();
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
const fileList = ref<FileItem[]>([]);
|
||||
|
||||
// 打开
|
||||
const open = () => {
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
defineExpose({ open });
|
||||
|
||||
// 选择文件回调
|
||||
const onSelectFile = (files: Array<FileItem>) => {
|
||||
if (files.length) {
|
||||
fileList.value = [files[files.length - 1]];
|
||||
} else {
|
||||
fileList.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
// 确定
|
||||
const handlerOk = async () => {
|
||||
if (!fileList.value.length) {
|
||||
Message.error('请选择文件');
|
||||
return false;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data } = await uploadAgentRelease(fileList.value[0].file as File);
|
||||
Message.success(`上传成功: 版本 v${data}`);
|
||||
// 清空
|
||||
handlerClear();
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭
|
||||
const handleClose = () => {
|
||||
handlerClear();
|
||||
};
|
||||
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
fileList.value = [];
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.upload-container {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.file-list-uploader {
|
||||
:deep(.arco-upload-list-item:first-of-type) {
|
||||
margin-top: 12px !important;
|
||||
}
|
||||
|
||||
:deep(.arco-upload-list-item .arco-upload-progress) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
65
orion-visor-ui/src/views/monitor/monitor-host/index.vue
Normal file
65
orion-visor-ui/src/views/monitor/monitor-host/index.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div class="layout-container" v-if="render">
|
||||
<!-- 列表-表格 -->
|
||||
<monitor-host-table v-if="renderTable"
|
||||
ref="table"
|
||||
@open-upload="() => uploadModal.open()"
|
||||
@open-update="(e: any) => drawer.openUpdate(e)" />
|
||||
<!-- 列表-卡片 -->
|
||||
<monitor-host-card-list v-else
|
||||
ref="card"
|
||||
@open-upload="() => uploadModal.open()"
|
||||
@open-update="(e: any) => drawer.openUpdate(e)" />
|
||||
<!-- 添加修改抽屉 -->
|
||||
<monitor-host-form-drawer ref="drawer"
|
||||
@added="reload"
|
||||
@updated="reload" />
|
||||
<!-- 发布包上传模态框 -->
|
||||
<release-upload-modal ref="uploadModal" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'monitorHost'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, onBeforeMount } from 'vue';
|
||||
import { useAppStore, useDictStore } from '@/store';
|
||||
import { dictKeys } from './types/const';
|
||||
import MonitorHostTable from './components/monitor-host-table.vue';
|
||||
import MonitorHostCardList from './components/monitor-host-card-list.vue';
|
||||
import MonitorHostFormDrawer from './components/monitor-host-form-drawer.vue';
|
||||
import ReleaseUploadModal from './components/release-upload-modal.vue';
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
const renderTable = computed(() => appStore.monitorHostView === 'table');
|
||||
|
||||
const render = ref(false);
|
||||
const table = ref();
|
||||
const card = ref();
|
||||
const drawer = ref();
|
||||
const uploadModal = ref();
|
||||
|
||||
// 重新加载
|
||||
const reload = () => {
|
||||
if (renderTable.value) {
|
||||
table.value.reload();
|
||||
} else {
|
||||
card.value.reload();
|
||||
}
|
||||
};
|
||||
|
||||
onBeforeMount(async () => {
|
||||
const dictStore = useDictStore();
|
||||
await dictStore.loadKeys(dictKeys);
|
||||
render.value = true;
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { CardField, CardFieldConfig } from '@/types/card';
|
||||
|
||||
const fieldConfig = {
|
||||
rowGap: '10px',
|
||||
labelSpan: 6,
|
||||
minHeight: '22px',
|
||||
fields: [
|
||||
{
|
||||
label: '主机ID',
|
||||
dataIndex: 'hostId',
|
||||
slotName: 'hostId',
|
||||
default: true,
|
||||
}, {
|
||||
label: '主机地址',
|
||||
dataIndex: 'address',
|
||||
slotName: 'address',
|
||||
default: true,
|
||||
}, {
|
||||
label: '主机编码',
|
||||
dataIndex: 'code',
|
||||
slotName: 'code',
|
||||
default: false,
|
||||
}, {
|
||||
label: '负责人',
|
||||
dataIndex: 'ownerUsername',
|
||||
slotName: 'ownerUsername',
|
||||
ellipsis: true,
|
||||
default: true,
|
||||
}, {
|
||||
label: '设备状态',
|
||||
dataIndex: 'agentOnlineStatus',
|
||||
slotName: 'agentOnlineStatus',
|
||||
default: true,
|
||||
}, {
|
||||
label: 'CPU',
|
||||
dataIndex: 'cpuUsage',
|
||||
slotName: 'cpuUsage',
|
||||
default: true,
|
||||
}, {
|
||||
label: '内存',
|
||||
dataIndex: 'memoryUsage',
|
||||
slotName: 'memoryUsage',
|
||||
default: true,
|
||||
}, {
|
||||
label: '磁盘',
|
||||
dataIndex: 'diskUsage',
|
||||
slotName: 'diskUsage',
|
||||
default: true,
|
||||
}, {
|
||||
label: '网络',
|
||||
dataIndex: 'network',
|
||||
slotName: 'network',
|
||||
default: true,
|
||||
}, {
|
||||
label: '负载',
|
||||
dataIndex: 'load',
|
||||
slotName: 'load',
|
||||
default: true,
|
||||
}, {
|
||||
label: '标签',
|
||||
dataIndex: 'tags',
|
||||
slotName: 'tags',
|
||||
default: false,
|
||||
}, {
|
||||
label: 'agentKey',
|
||||
dataIndex: 'agentKey',
|
||||
slotName: 'agentKey',
|
||||
ellipsis: true,
|
||||
default: false,
|
||||
}, {
|
||||
label: '探针版本',
|
||||
dataIndex: 'agentVersion',
|
||||
slotName: 'agentVersion',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
default: true,
|
||||
}
|
||||
] as CardField[]
|
||||
} as CardFieldConfig;
|
||||
|
||||
export default fieldConfig;
|
||||
32
orion-visor-ui/src/views/monitor/monitor-host/types/const.ts
Normal file
32
orion-visor-ui/src/views/monitor/monitor-host/types/const.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
export const TableName = 'monitor_host';
|
||||
|
||||
// 监控告警开关
|
||||
export const AlarmSwitch = {
|
||||
OFF: 0,
|
||||
ON: 1,
|
||||
};
|
||||
|
||||
// 探针日志状态
|
||||
export const AgentLogStatus = {
|
||||
WAIT: 'WAIT',
|
||||
RUNNING: 'RUNNING',
|
||||
SUCCESS: 'SUCCESS',
|
||||
FAILED: 'FAILED',
|
||||
};
|
||||
|
||||
export const NODATA_TIPS = '暂无数据';
|
||||
|
||||
// 探针安装状态 字典项
|
||||
export const InstallStatusKey = 'agentInstallStatus';
|
||||
|
||||
// 探针在线状态 字典项
|
||||
export const OnlineStatusKey = 'agentOnlineStatus';
|
||||
|
||||
// 监控告警开关 字典项
|
||||
export const AlarmSwitchKey = 'monitorAlarmSwitch';
|
||||
|
||||
// 探针日志状态 字典项
|
||||
export const AgentLogStatusKey = 'agentLogStatus';
|
||||
|
||||
// 加载的字典值
|
||||
export const dictKeys = [InstallStatusKey, AlarmSwitchKey, OnlineStatusKey, AgentLogStatusKey];
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { FieldRule } from '@arco-design/web-vue';
|
||||
|
||||
export const policyId = [{
|
||||
required: false,
|
||||
message: '请输入策略id'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const alarmSwitch = [{
|
||||
required: true,
|
||||
message: '请输入告警开关'
|
||||
}] as FieldRule[];
|
||||
|
||||
export default {
|
||||
policyId,
|
||||
alarmSwitch,
|
||||
} as Record<string, FieldRule | FieldRule[]>;
|
||||
@@ -0,0 +1,112 @@
|
||||
import type { TableColumnData } from '@arco-design/web-vue';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '主机ID',
|
||||
dataIndex: 'hostId',
|
||||
slotName: 'hostId',
|
||||
width: 80,
|
||||
align: 'left',
|
||||
fixed: 'left',
|
||||
default: true,
|
||||
}, {
|
||||
title: '主机信息',
|
||||
dataIndex: 'hostInfo',
|
||||
slotName: 'hostInfo',
|
||||
width: 248,
|
||||
align: 'left',
|
||||
fixed: 'left',
|
||||
default: true,
|
||||
}, {
|
||||
title: '设备状态',
|
||||
dataIndex: 'agentOnlineStatus',
|
||||
slotName: 'agentOnlineStatus',
|
||||
align: 'center',
|
||||
width: 120,
|
||||
default: true,
|
||||
}, {
|
||||
title: 'CPU',
|
||||
dataIndex: 'cpuUsage',
|
||||
slotName: 'cpuUsage',
|
||||
align: 'left',
|
||||
width: 198,
|
||||
default: true,
|
||||
}, {
|
||||
title: '内存',
|
||||
dataIndex: 'memoryUsage',
|
||||
slotName: 'memoryUsage',
|
||||
align: 'left',
|
||||
width: 198,
|
||||
default: true,
|
||||
}, {
|
||||
title: '磁盘',
|
||||
dataIndex: 'diskUsage',
|
||||
slotName: 'diskUsage',
|
||||
align: 'left',
|
||||
width: 198,
|
||||
default: true,
|
||||
}, {
|
||||
title: '网络',
|
||||
dataIndex: 'network',
|
||||
slotName: 'network',
|
||||
align: 'left',
|
||||
width: 148,
|
||||
default: true,
|
||||
}, {
|
||||
title: '负载',
|
||||
dataIndex: 'load',
|
||||
slotName: 'load',
|
||||
align: 'left',
|
||||
width: 148,
|
||||
default: true,
|
||||
}, {
|
||||
title: '标签',
|
||||
dataIndex: 'tags',
|
||||
slotName: 'tags',
|
||||
align: 'left',
|
||||
minWidth: 148,
|
||||
default: false,
|
||||
}, {
|
||||
title: 'agentKey',
|
||||
dataIndex: 'agentKey',
|
||||
slotName: 'agentKey',
|
||||
align: 'left',
|
||||
width: 288,
|
||||
default: false,
|
||||
}, {
|
||||
// TODO
|
||||
// title: '告警策略',
|
||||
// dataIndex: 'alarmPolicy',
|
||||
// slotName: 'alarmPolicy',
|
||||
// align: 'left',
|
||||
// width: 120,
|
||||
// default: true,
|
||||
// }, {
|
||||
title: '负责人',
|
||||
dataIndex: 'ownerUsername',
|
||||
slotName: 'ownerUsername',
|
||||
align: 'left',
|
||||
width: 108,
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
default: true,
|
||||
}, {
|
||||
title: '探针版本',
|
||||
dataIndex: 'agentVersion',
|
||||
slotName: 'agentVersion',
|
||||
align: 'left',
|
||||
width: 118,
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
default: true,
|
||||
}, {
|
||||
title: '操作',
|
||||
slotName: 'handle',
|
||||
width: 128,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
default: true,
|
||||
},
|
||||
] as TableColumnData[];
|
||||
|
||||
export default columns;
|
||||
@@ -0,0 +1,273 @@
|
||||
import type { MonitorHostQueryResponse, } from '@/api/monitor/monitor-host';
|
||||
import { getMonitorHostMetrics, updateMonitorHostAlarmSwitch } from '@/api/monitor/monitor-host';
|
||||
import type { HostAgentLogResponse } from '@/api/asset/host-agent';
|
||||
import { getAgentInstallLogStatus, getHostAgentStatus, installHostAgent, updateAgentInstallStatus } from '@/api/asset/host-agent';
|
||||
import type { Ref } from 'vue';
|
||||
import { onActivated, onDeactivated, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useDictStore } from '@/store';
|
||||
import { Message, Modal } from '@arco-design/web-vue';
|
||||
import { AgentInstallStatus, HostOsType } from '@/views/asset/host-list/types/const';
|
||||
import { AgentLogStatus, AlarmSwitchKey } from '@/views/monitor/monitor-host/types/const';
|
||||
|
||||
// 监控主机列表配置
|
||||
export interface UseMonitorHostListOptions {
|
||||
// 主机信息
|
||||
hosts: Ref<Array<MonitorHostQueryResponse>>;
|
||||
// 设置加载中
|
||||
setLoading: (loading: boolean) => void;
|
||||
// 重新加载
|
||||
reload: () => void;
|
||||
}
|
||||
|
||||
// 使用监控主机列表
|
||||
export default function useMonitorHostList(options: UseMonitorHostListOptions) {
|
||||
const autoRefresh = ref(true);
|
||||
const autoRefreshId = ref();
|
||||
const fetchInstallStatusId = ref();
|
||||
const lastRefreshTime = ref(0);
|
||||
|
||||
const router = useRouter();
|
||||
const { toggleDict } = useDictStore();
|
||||
const { hosts, setLoading, reload } = options;
|
||||
|
||||
// 打开详情
|
||||
const openDetail = (hostId: number, name: string) => {
|
||||
router.push({ name: 'monitorDetail', query: { hostId, name } });
|
||||
};
|
||||
|
||||
// 安装探针
|
||||
const installAgent = async (hostIdList: Array<number>) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 获取全部数据
|
||||
const installHosts = hosts.value.filter(s => hostIdList.includes(s.hostId));
|
||||
let hasWindows = false;
|
||||
// 安装前检查
|
||||
for (let host of installHosts) {
|
||||
// 检查状态
|
||||
if (host?.installLog?.status === AgentLogStatus.WAIT || host?.installLog?.status === AgentLogStatus.RUNNING) {
|
||||
Message.error('主机' + host.name + '正在安装中, 请勿重复操作');
|
||||
return;
|
||||
}
|
||||
// 检查系统类型
|
||||
if (host.osType === HostOsType.WINDOWS.value) {
|
||||
hasWindows = true;
|
||||
}
|
||||
}
|
||||
// 二次确认
|
||||
Modal.confirm({
|
||||
title: '安装提示',
|
||||
titleAlign: 'start',
|
||||
bodyStyle: { 'white-space': 'pre-wrap' },
|
||||
content: `请确保探针已关闭\n请确认文件夹是否有权限${hasWindows ? '\nWindows 系统仅支持探针上传, 请手动进行安装' : ''}`,
|
||||
okText: '确定',
|
||||
onOk: async () => {
|
||||
try {
|
||||
// 调用安装
|
||||
await installHostAgent({ idList: hostIdList });
|
||||
Message.success('开始安装');
|
||||
// 重新加载
|
||||
reload();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 手动安装成功
|
||||
const setInstallSuccess = (log: HostAgentLogResponse) => {
|
||||
Modal.confirm({
|
||||
title: '修正状态',
|
||||
titleAlign: 'start',
|
||||
content: `确定要手动将安装记录修正为完成吗?`,
|
||||
okText: '确定',
|
||||
onOk: async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 调用修改接口
|
||||
await updateAgentInstallStatus({
|
||||
id: log.id,
|
||||
status: AgentLogStatus.SUCCESS,
|
||||
message: '手动修正',
|
||||
});
|
||||
log.status = AgentLogStatus.SUCCESS;
|
||||
Message.success('状态已修正');
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 更新报警开关
|
||||
const toggleAlarmSwitch = async (record: MonitorHostQueryResponse) => {
|
||||
const dict = toggleDict(AlarmSwitchKey, record.alarmSwitch);
|
||||
Modal.confirm({
|
||||
title: `${dict.label}确认`,
|
||||
titleAlign: 'start',
|
||||
content: `确定要${dict.label}报警功能吗?`,
|
||||
okText: '确定',
|
||||
onOk: async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const newSwitch = dict.value as number;
|
||||
// 调用修改接口
|
||||
await updateMonitorHostAlarmSwitch({
|
||||
id: record.id,
|
||||
alarmSwitch: newSwitch,
|
||||
});
|
||||
record.alarmSwitch = newSwitch;
|
||||
Message.success(`已${dict.label}`);
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 获取探针安装状态
|
||||
const pullInstallLogStatus = async () => {
|
||||
// 获取安装中的记录
|
||||
const runningIdList = hosts.value.filter(s => s.installLog?.status === AgentLogStatus.WAIT || s.installLog?.status === AgentLogStatus.RUNNING)
|
||||
.map(s => s.installLog?.id)
|
||||
.filter(Boolean);
|
||||
if (!runningIdList.length) {
|
||||
return;
|
||||
}
|
||||
// 查询状态
|
||||
const { data } = await getAgentInstallLogStatus(runningIdList);
|
||||
data.forEach(item => {
|
||||
hosts.value.filter(s => s.installLog?.id === item.id).forEach(s => {
|
||||
s.installLog.status = item.status;
|
||||
s.installLog.message = item.message;
|
||||
// 若安装成功则修改探针信息
|
||||
if (item.status === AgentLogStatus.SUCCESS && item.agentStatus) {
|
||||
s.agentVersion = item.agentStatus.agentVersion;
|
||||
s.latestVersion = item.agentStatus.latestVersion;
|
||||
s.agentInstallStatus = item.agentStatus.agentInstallStatus;
|
||||
s.agentOnlineStatus = item.agentStatus.agentOnlineStatus;
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 获取探针状态
|
||||
const getAgentStatus = async () => {
|
||||
// 获取全部 hostId
|
||||
const hostIds = hosts.value.map(s => s.hostId);
|
||||
if (hostIds.length) {
|
||||
try {
|
||||
// 查询状态
|
||||
const { data } = await getHostAgentStatus(hostIds);
|
||||
data.forEach(item => {
|
||||
hosts.value.filter(s => s.hostId === item.id).forEach(s => {
|
||||
s.agentVersion = item.agentVersion;
|
||||
s.latestVersion = item.latestVersion;
|
||||
s.agentInstallStatus = item.agentInstallStatus;
|
||||
s.agentOnlineStatus = item.agentOnlineStatus;
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 获取指标信息
|
||||
const getHostMetrics = async () => {
|
||||
// 获取全部已安装 agentKey
|
||||
const agentKeys = hosts.value
|
||||
.filter(s => s.agentInstallStatus === AgentInstallStatus.INSTALLED)
|
||||
.map(s => s.agentKey)
|
||||
.filter(Boolean);
|
||||
if (agentKeys.length) {
|
||||
try {
|
||||
// 查询指标
|
||||
const { data } = await getMonitorHostMetrics(agentKeys);
|
||||
data.forEach(item => {
|
||||
hosts.value.filter(s => s.agentKey === item.agentKey).forEach(s => {
|
||||
s.metricsData = item;
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新指标
|
||||
const refreshMetrics = async () => {
|
||||
// 加载状态信息
|
||||
await getAgentStatus();
|
||||
// 加载指标数据
|
||||
await getHostMetrics();
|
||||
// 设置刷新时间
|
||||
lastRefreshTime.value = Date.now();
|
||||
};
|
||||
|
||||
// 切换自动刷新
|
||||
const toggleAutoRefresh = async () => {
|
||||
autoRefresh.value = !autoRefresh.value;
|
||||
if (autoRefresh.value) {
|
||||
// 开启自动刷新
|
||||
await openAutoRefresh();
|
||||
} else {
|
||||
// 关闭自动刷新
|
||||
closeAutoRefresh();
|
||||
}
|
||||
};
|
||||
|
||||
// 开启自动刷新
|
||||
const openAutoRefresh = async () => {
|
||||
window.clearInterval(autoRefreshId.value);
|
||||
if (!autoRefresh.value) {
|
||||
return;
|
||||
}
|
||||
if (lastRefreshTime.value === 0) {
|
||||
// 防止首次就刷新
|
||||
lastRefreshTime.value = 1;
|
||||
} else if (Date.now() - lastRefreshTime.value > 60000) {
|
||||
// 超过刷新的时间
|
||||
await refreshMetrics();
|
||||
}
|
||||
// 设置自动刷新
|
||||
autoRefreshId.value = window.setInterval(refreshMetrics, 60000);
|
||||
};
|
||||
|
||||
// 关闭自动刷新
|
||||
const closeAutoRefresh = () => {
|
||||
window.clearInterval(autoRefreshId.value);
|
||||
autoRefreshId.value = undefined;
|
||||
};
|
||||
|
||||
onMounted(openAutoRefresh);
|
||||
onActivated(openAutoRefresh);
|
||||
onDeactivated(closeAutoRefresh);
|
||||
onUnmounted(closeAutoRefresh);
|
||||
|
||||
onMounted(() => {
|
||||
window.clearInterval(fetchInstallStatusId.value);
|
||||
fetchInstallStatusId.value = window.setInterval(pullInstallLogStatus, 5000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.clearInterval(fetchInstallStatusId.value);
|
||||
});
|
||||
|
||||
return {
|
||||
autoRefresh,
|
||||
openDetail,
|
||||
installAgent,
|
||||
setInstallSuccess,
|
||||
toggleAlarmSwitch,
|
||||
toggleAutoRefresh,
|
||||
};
|
||||
|
||||
};
|
||||
@@ -206,7 +206,11 @@
|
||||
|
||||
// 选择文件回调
|
||||
const onSelectFile = (files: Array<FileItem>) => {
|
||||
fileList.value = [files[files.length - 1]];
|
||||
if (files.length) {
|
||||
fileList.value = [files[files.length - 1]];
|
||||
} else {
|
||||
fileList.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
// 上传文件
|
||||
|
||||
Reference in New Issue
Block a user