mirror of https://github.com/openspug/spug
				
				
				
			Merge c1cb8e525d into 0e7b5ec77e
				
					
				
			
						commit
						7f73b45eec
					
				| 
						 | 
				
			
			@ -17,4 +17,6 @@ urlpatterns = [
 | 
			
		|||
    path('import/region/', get_regions),
 | 
			
		||||
    path('parse/', post_parse),
 | 
			
		||||
    path('valid/', batch_valid),
 | 
			
		||||
    path('processes/', get_processes),
 | 
			
		||||
    path('ports/', get_ports),
 | 
			
		||||
]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,20 +1,22 @@
 | 
			
		|||
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
 | 
			
		||||
# Copyright: (c) <spug.dev@gmail.com>
 | 
			
		||||
# Released under the AGPL-3.0 License.
 | 
			
		||||
from django_redis import get_redis_connection
 | 
			
		||||
from libs.helper import make_ali_request, make_tencent_request
 | 
			
		||||
from libs.ssh import SSH, AuthenticationException
 | 
			
		||||
from libs.utils import AttrDict, human_datetime
 | 
			
		||||
from libs.validators import ip_validator
 | 
			
		||||
from apps.host.models import HostExtend
 | 
			
		||||
from apps.setting.utils import AppSetting
 | 
			
		||||
from collections import defaultdict
 | 
			
		||||
from datetime import datetime, timezone
 | 
			
		||||
from concurrent import futures
 | 
			
		||||
import ipaddress
 | 
			
		||||
import json
 | 
			
		||||
import math
 | 
			
		||||
import os
 | 
			
		||||
from collections import defaultdict
 | 
			
		||||
from concurrent import futures
 | 
			
		||||
from datetime import datetime, timezone
 | 
			
		||||
 | 
			
		||||
from django_redis import get_redis_connection
 | 
			
		||||
 | 
			
		||||
from apps.host.models import HostExtend
 | 
			
		||||
from apps.setting.utils import AppSetting
 | 
			
		||||
from libs.helper import make_ali_request, make_tencent_request
 | 
			
		||||
from libs.ssh import SSH, AuthenticationException
 | 
			
		||||
from libs.utils import AttrDict, human_datetime
 | 
			
		||||
from libs.validators import ip_validator
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def check_os_type(os_name):
 | 
			
		||||
| 
						 | 
				
			
			@ -201,7 +203,7 @@ def fetch_host_extend(ssh):
 | 
			
		|||
    code, out = ssh.exec_command_raw('hostname -I')
 | 
			
		||||
    if code == 0:
 | 
			
		||||
        for ip in out.strip().split():
 | 
			
		||||
            if len(ip) > 15:   # ignore ipv6
 | 
			
		||||
            if len(ip) > 15:  # ignore ipv6
 | 
			
		||||
                continue
 | 
			
		||||
            if ipaddress.ip_address(ip).is_global:
 | 
			
		||||
                if len(public_ip_address) < 10:
 | 
			
		||||
| 
						 | 
				
			
			@ -301,3 +303,51 @@ def _get_ssh(kwargs, pkey=None, private_key=None, public_key=None, password=None
 | 
			
		|||
                ssh.add_public_key(public_key)
 | 
			
		||||
            return _get_ssh(kwargs, private_key)
 | 
			
		||||
        raise e
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _sync_host_process(host, private_key=None, public_key=None, password=None, ssh=None):
 | 
			
		||||
    if not ssh:
 | 
			
		||||
        kwargs = host.to_dict(selects=('hostname', 'port', 'username'))
 | 
			
		||||
        with _get_ssh(kwargs, host.pkey, private_key, public_key, password) as ssh:
 | 
			
		||||
            return _sync_host_process(host, ssh=ssh)
 | 
			
		||||
    process_list = fetch_host_processes(ssh)
 | 
			
		||||
    return process_list
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def fetch_host_processes(ssh):
 | 
			
		||||
    command = '''ps_info=$(ps -e -o pid=,comm=,ppid=,user=,%cpu=,%mem=,rss,uid= --no-headers); echo -n '['; while read -r line; do read pid name ppid username cpu_usage memory_usage memory uid <<<$(echo $line); if [ -e \"/proc/${pid}/cmdline\" ]; then command=$(tr '\\0' ' ' <\"/proc/${pid}/cmdline\" | awk '{$1=$1};1'| sed 's/\\\\/\\\\\\\\/g' | sed 's/\"/\\\\\"/g' 2>/dev/null); start_time=$(stat -c %Y \"/proc/${pid}\" 2>/dev/null); echo \"{\\\"name\\\":\\\"${name}\\\",\\\"pid\\\":${pid},\\\"ppid\\\":${ppid},\\\"username\\\":\\\"${username}\\\",\\\"uid\\\":${uid},\\\"start_time\\\":${start_time},\\\"cpu_usage\\\":\\\"${cpu_usage}\\\",\\\"memory_usage\\\":\\\"${memory_usage}\\\",\\\"memory\\\":\\\"${memory}\\\",\\\"command\\\":\\\"${command}\\\"},\"; fi; done <<<\"$ps_info\" | sed '$s/,$/]/';'''
 | 
			
		||||
    code, out = ssh.exec_command(command)
 | 
			
		||||
    if code == 0:
 | 
			
		||||
        try:
 | 
			
		||||
            _j = json.loads(out.strip())
 | 
			
		||||
            return _j
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            print(e)
 | 
			
		||||
            print(out)
 | 
			
		||||
    elif code != 0:
 | 
			
		||||
        print(code, out)
 | 
			
		||||
        return []
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _sync_host_ports(host, private_key=None, public_key=None, password=None, ssh=None):
 | 
			
		||||
    if not ssh:
 | 
			
		||||
        kwargs = host.to_dict(selects=('hostname', 'port', 'username'))
 | 
			
		||||
        with _get_ssh(kwargs, host.pkey, private_key, public_key, password) as ssh:
 | 
			
		||||
            return _sync_host_ports(host, ssh=ssh)
 | 
			
		||||
    ports_list = fetch_host_ports(ssh)
 | 
			
		||||
    return ports_list
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def fetch_host_ports(ssh):
 | 
			
		||||
    command = '''netstat -nltp | awk 'NR>2 {cmd=\"netstat -n | grep -c \\\"\"$4\"\\\"\"; cmd | getline conn_count; close(cmd); printf \"{\\\"protocol\\\":\\\"%s\\\",\\\"listen\\\":\\\"%s\\\",\\\"pid\\\":\\\"%s\\\",\\\"connections\\\":\\\"%s\\\"},\", $1, $4, $7, conn_count}' | sed 's/,$/]/' | awk 'BEGIN {printf\"[\"} {print}' '''
 | 
			
		||||
    code, out = ssh.exec_command(command)
 | 
			
		||||
    if code == 0:
 | 
			
		||||
        try:
 | 
			
		||||
            _j = json.loads(out.strip())
 | 
			
		||||
            return _j
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            print(e)
 | 
			
		||||
            print(out)
 | 
			
		||||
    elif code != 0:
 | 
			
		||||
        print(code, out)
 | 
			
		||||
        return []
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,7 @@ from django.db.models import F
 | 
			
		|||
from django.http.response import HttpResponseBadRequest
 | 
			
		||||
from libs import json_response, JsonParser, Argument, AttrDict, auth
 | 
			
		||||
from apps.setting.utils import AppSetting
 | 
			
		||||
from apps.account.utils import get_host_perms
 | 
			
		||||
from apps.account.utils import get_host_perms, has_host_perm
 | 
			
		||||
from apps.host.models import Host, Group
 | 
			
		||||
from apps.host.utils import batch_sync_host, _sync_host_extend
 | 
			
		||||
from apps.exec.models import ExecTemplate
 | 
			
		||||
| 
						 | 
				
			
			@ -230,3 +230,45 @@ def _do_host_verify(form):
 | 
			
		|||
    except socket.timeout:
 | 
			
		||||
        raise Exception('连接主机超时,请检查网络')
 | 
			
		||||
    return True
 | 
			
		||||
 | 
			
		||||
@auth('host.host.view')
 | 
			
		||||
def get_processes(request):
 | 
			
		||||
    form, error = JsonParser(
 | 
			
		||||
        Argument('host_id', type=int, help='参数错误'),
 | 
			
		||||
    ).parse(request.body)
 | 
			
		||||
    if error is None:
 | 
			
		||||
        if not has_host_perm(request.user, form.host_id):
 | 
			
		||||
            return json_response(error='无权访问主机,请联系管理员')
 | 
			
		||||
        private_key, public_key = AppSetting.get_ssh_key()
 | 
			
		||||
        host = Host.objects.filter(id=form.host_id).first()
 | 
			
		||||
        if host.is_verified:
 | 
			
		||||
            try:
 | 
			
		||||
                result = _sync_host_process(host=host, private_key=private_key)
 | 
			
		||||
            except socket.timeout:
 | 
			
		||||
                return json_response(error='连接主机超时,请检查网络')
 | 
			
		||||
            return json_response(result)
 | 
			
		||||
        else:
 | 
			
		||||
            return json_response(error='该主机未验证,请先验证')
 | 
			
		||||
    return json_response(error=error)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@auth('host.host.view')
 | 
			
		||||
def get_ports(request):
 | 
			
		||||
    form, error = JsonParser(
 | 
			
		||||
        Argument('host_id', type=int, help='参数错误'),
 | 
			
		||||
    ).parse(request.body)
 | 
			
		||||
    if error is None:
 | 
			
		||||
        if not has_host_perm(request.user, form.host_id):
 | 
			
		||||
            return json_response(error='无权访问主机,请联系管理员')
 | 
			
		||||
        private_key, public_key = AppSetting.get_ssh_key()
 | 
			
		||||
        host = Host.objects.filter(id=form.host_id).first()
 | 
			
		||||
        if host.is_verified:
 | 
			
		||||
            try:
 | 
			
		||||
                result = _sync_host_ports(host=host, private_key=private_key)
 | 
			
		||||
            except socket.timeout:
 | 
			
		||||
                return json_response(error='连接主机超时,请检查网络')
 | 
			
		||||
            return json_response(result)
 | 
			
		||||
        else:
 | 
			
		||||
            return json_response(error='该主机未验证,请先验证')
 | 
			
		||||
    return json_response(error=error)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,7 +5,7 @@
 | 
			
		|||
 */
 | 
			
		||||
import React, { useState, useEffect, useRef } from 'react';
 | 
			
		||||
import { observer } from 'mobx-react';
 | 
			
		||||
import { Drawer, Descriptions, List, Button, Input, Select, DatePicker, Tag, message } from 'antd';
 | 
			
		||||
import {Drawer, Descriptions, List, Button, Input, Select, DatePicker, Tag, message, Tabs} from 'antd';
 | 
			
		||||
import { EditOutlined, SaveOutlined, PlusOutlined, SyncOutlined } from '@ant-design/icons';
 | 
			
		||||
import { AuthButton } from 'components';
 | 
			
		||||
import { http } from 'libs';
 | 
			
		||||
| 
						 | 
				
			
			@ -13,7 +13,8 @@ import store from './store';
 | 
			
		|||
import lds from 'lodash';
 | 
			
		||||
import moment from 'moment';
 | 
			
		||||
import styles from './index.module.less';
 | 
			
		||||
 | 
			
		||||
import ProcessesTable from "./Processes";
 | 
			
		||||
import PortsTable from "./Ports";
 | 
			
		||||
export default observer(function () {
 | 
			
		||||
  const [edit, setEdit] = useState(false);
 | 
			
		||||
  const [host, setHost] = useState(store.record);
 | 
			
		||||
| 
						 | 
				
			
			@ -110,7 +111,7 @@ export default observer(function () {
 | 
			
		|||
 | 
			
		||||
  return (
 | 
			
		||||
    <Drawer
 | 
			
		||||
      width={550}
 | 
			
		||||
      width={1500}
 | 
			
		||||
      title={host.name}
 | 
			
		||||
      placement="right"
 | 
			
		||||
      onClose={handleClose}
 | 
			
		||||
| 
						 | 
				
			
			@ -118,9 +119,10 @@ export default observer(function () {
 | 
			
		|||
      <Descriptions
 | 
			
		||||
        bordered
 | 
			
		||||
        size="small"
 | 
			
		||||
        labelStyle={{width: 150}}
 | 
			
		||||
        // labelStyle={{width: 150}}
 | 
			
		||||
        title={<span style={{fontWeight: 500}}>基本信息</span>}
 | 
			
		||||
        column={1}>
 | 
			
		||||
        // column={1}
 | 
			
		||||
      >
 | 
			
		||||
        <Descriptions.Item label="主机名称">{host.name}</Descriptions.Item>
 | 
			
		||||
        <Descriptions.Item label="连接地址">{host.username}@{host.hostname}</Descriptions.Item>
 | 
			
		||||
        <Descriptions.Item label="连接端口">{host.port}</Descriptions.Item>
 | 
			
		||||
| 
						 | 
				
			
			@ -137,9 +139,9 @@ export default observer(function () {
 | 
			
		|||
      <Descriptions
 | 
			
		||||
        bordered
 | 
			
		||||
        size="small"
 | 
			
		||||
        column={1}
 | 
			
		||||
        // column={1}
 | 
			
		||||
        className={edit ? styles.hostExtendEdit : null}
 | 
			
		||||
        labelStyle={{width: 150}}
 | 
			
		||||
        // labelStyle={{width: 150}}
 | 
			
		||||
        style={{marginTop: 24}}
 | 
			
		||||
        extra={edit ? ([
 | 
			
		||||
          <Button key="1" type="link" loading={fetching} icon={<SyncOutlined/>} onClick={handleFetch}>同步</Button>,
 | 
			
		||||
| 
						 | 
				
			
			@ -270,6 +272,16 @@ export default observer(function () {
 | 
			
		|||
        </Descriptions.Item>
 | 
			
		||||
        <Descriptions.Item label="更新时间">{host.updated_at}</Descriptions.Item>
 | 
			
		||||
      </Descriptions>
 | 
			
		||||
      {host.id !== undefined && store.detailVisible ? (
 | 
			
		||||
          <Tabs>
 | 
			
		||||
            <Tabs.TabPane tab="进程清单" key="item-1">
 | 
			
		||||
              <ProcessesTable host_id={store.record.id}/>
 | 
			
		||||
            </Tabs.TabPane>
 | 
			
		||||
            <Tabs.TabPane tab="网络端口" key="item-2">
 | 
			
		||||
              <PortsTable host_id={store.record.id}/>
 | 
			
		||||
            </Tabs.TabPane>
 | 
			
		||||
          </Tabs>
 | 
			
		||||
      ) : null}
 | 
			
		||||
    </Drawer>
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,71 @@
 | 
			
		|||
/**
 | 
			
		||||
 * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
 | 
			
		||||
 * Copyright (c) <spug.dev@gmail.com>
 | 
			
		||||
 * Released under the AGPL-3.0 License.
 | 
			
		||||
 */
 | 
			
		||||
import React, {useEffect, useState} from 'react';
 | 
			
		||||
import {Input, Table} from 'antd';
 | 
			
		||||
import {TableCard} from 'components';
 | 
			
		||||
import {observer} from "mobx-react";
 | 
			
		||||
import {http} from "../../libs";
 | 
			
		||||
 | 
			
		||||
export default observer(function PortsTable(value) {
 | 
			
		||||
    let [portFetching, setPortFetching] = useState(false)
 | 
			
		||||
    let [dataSource, setDataSource] = useState([])
 | 
			
		||||
    let [searchText, setSearchText] = useState(''); // 新增搜索文本状态
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        fetchPorts(value.host_id)
 | 
			
		||||
    }, [])
 | 
			
		||||
 | 
			
		||||
    function fetchPorts() {
 | 
			
		||||
        console.log("host_id:" + value.host_id, "portFetching:" + portFetching)
 | 
			
		||||
        setPortFetching(true);
 | 
			
		||||
        return http.post('/api/host/ports/', {
 | 
			
		||||
            'host_id': value.host_id
 | 
			
		||||
        })
 | 
			
		||||
            .then(res => {
 | 
			
		||||
                setDataSource(res)
 | 
			
		||||
            })
 | 
			
		||||
            .finally(() => setPortFetching(false))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function handleSearch(value) {
 | 
			
		||||
        setSearchText(value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const filteredDataSource = dataSource.filter(item =>
 | 
			
		||||
        item.listen.toLowerCase().includes(searchText.toLowerCase()) ||
 | 
			
		||||
        item.pid.toLowerCase().includes(searchText.toLowerCase())
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return (<TableCard
 | 
			
		||||
        tKey="mi"
 | 
			
		||||
        rowKey="id"
 | 
			
		||||
        title={<Input.Search allowClear value={searchText} placeholder="输入端口/PID检索" style={{maxWidth: 250}}
 | 
			
		||||
                             onChange={e => handleSearch(e.target.value)}/>}
 | 
			
		||||
        loading={portFetching}
 | 
			
		||||
        dataSource={filteredDataSource}
 | 
			
		||||
        onReload={fetchPorts}
 | 
			
		||||
        pagination={{
 | 
			
		||||
            showSizeChanger: true,
 | 
			
		||||
            showLessItems: true,
 | 
			
		||||
            showTotal: total => `共 ${total} 条`,
 | 
			
		||||
            defaultPageSize: 50,
 | 
			
		||||
            pageSizeOptions: ['50', '100']
 | 
			
		||||
        }}
 | 
			
		||||
        scroll={{
 | 
			
		||||
            y: 240,
 | 
			
		||||
        }}
 | 
			
		||||
    >
 | 
			
		||||
        <Table.Column title="协议" dataIndex="protocol"/>
 | 
			
		||||
        <Table.Column title="监听地址" dataIndex="listen"/>
 | 
			
		||||
        <Table.Column title="连接数" dataIndex="connections"
 | 
			
		||||
                      sorter={(a, b) => a.connections.localeCompare(b.connections)}
 | 
			
		||||
                      sortDirections={['descend']}
 | 
			
		||||
                      defaultSortOrder="descend"
 | 
			
		||||
        />
 | 
			
		||||
        <Table.Column title="PID/进程名" dataIndex="pid"
 | 
			
		||||
        />
 | 
			
		||||
    </TableCard>)
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,111 @@
 | 
			
		|||
/**
 | 
			
		||||
 * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
 | 
			
		||||
 * Copyright (c) <spug.dev@gmail.com>
 | 
			
		||||
 * Released under the AGPL-3.0 License.
 | 
			
		||||
 */
 | 
			
		||||
import React, {useEffect, useState} from 'react';
 | 
			
		||||
import {Input, Table} from 'antd';
 | 
			
		||||
import {TableCard} from 'components';
 | 
			
		||||
import {observer} from "mobx-react";
 | 
			
		||||
import {http} from "../../libs";
 | 
			
		||||
 | 
			
		||||
export default observer(function ProcessesTable(value) {
 | 
			
		||||
    let [processFetching, setProcessFetching] = useState(false)
 | 
			
		||||
    let [dataSource, setDataSource] = useState([])
 | 
			
		||||
    let [searchText, setSearchText] = useState(''); // 新增搜索文本状态
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        fetchProcesses(value.host_id)
 | 
			
		||||
    }, [])
 | 
			
		||||
 | 
			
		||||
    function TimestampConverter(timestampInSeconds) {
 | 
			
		||||
        const date = new Date(timestampInSeconds * 1000);
 | 
			
		||||
 | 
			
		||||
        const year = date.getFullYear();
 | 
			
		||||
        const month = ('0' + (date.getMonth() + 1)).slice(-2);
 | 
			
		||||
        const day = ('0' + date.getDate()).slice(-2);
 | 
			
		||||
        const hours = ('0' + date.getHours()).slice(-2);
 | 
			
		||||
        const minutes = ('0' + date.getMinutes()).slice(-2);
 | 
			
		||||
        const seconds = ('0' + date.getSeconds()).slice(-2);
 | 
			
		||||
 | 
			
		||||
        return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    function formatMemory(memoryUsed) {
 | 
			
		||||
 | 
			
		||||
        if (memoryUsed >= 1024) {
 | 
			
		||||
            return (memoryUsed / 1024).toFixed(2) + ' MB';
 | 
			
		||||
        } else if (memoryUsed >= 0) {
 | 
			
		||||
            return (memoryUsed) + ' KB';
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function fetchProcesses() {
 | 
			
		||||
        console.log("host_id:" + value.host_id, "processFetching:" + processFetching)
 | 
			
		||||
        setProcessFetching(true);
 | 
			
		||||
        return http.post('/api/host/processes/', {
 | 
			
		||||
            'host_id': value.host_id
 | 
			
		||||
        })
 | 
			
		||||
            .then(res => {
 | 
			
		||||
                setDataSource(res)
 | 
			
		||||
            })
 | 
			
		||||
            .finally(() => setProcessFetching(false))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function handleSearch(value) {
 | 
			
		||||
        setSearchText(value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const filteredDataSource = dataSource.filter(item =>
 | 
			
		||||
        item.name.toLowerCase().includes(searchText.toLowerCase()) ||
 | 
			
		||||
        item.pid.toString().includes(searchText.toLowerCase()) ||
 | 
			
		||||
        item.command.toLowerCase().includes(searchText.toLowerCase())
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return (<TableCard
 | 
			
		||||
        tKey="mi"
 | 
			
		||||
        rowKey="id"
 | 
			
		||||
        title={<Input.Search allowClear value={searchText} placeholder="输入进程名/PID/命令检索" style={{maxWidth: 250}}
 | 
			
		||||
                             onChange={e => handleSearch(e.target.value)}/>}
 | 
			
		||||
        loading={processFetching}
 | 
			
		||||
        dataSource={filteredDataSource}
 | 
			
		||||
        onReload={fetchProcesses}
 | 
			
		||||
        pagination={{
 | 
			
		||||
            showSizeChanger: true,
 | 
			
		||||
            showLessItems: true,
 | 
			
		||||
            showTotal: total => `共 ${total} 条`,
 | 
			
		||||
            defaultPageSize: 50,
 | 
			
		||||
            pageSizeOptions: ['50', '100']
 | 
			
		||||
        }}
 | 
			
		||||
        scroll={{
 | 
			
		||||
            y: 240,
 | 
			
		||||
        }}
 | 
			
		||||
    >
 | 
			
		||||
        <Table.Column title="进程名 / PID / PPID" width={200}
 | 
			
		||||
                      render={info => `${info.name} / ${info.pid} / ${info.ppid}`}/>
 | 
			
		||||
        <Table.Column title="用户 / UID" width={100}
 | 
			
		||||
                      render={info => `${info.username} / ${info.uid}`}/>
 | 
			
		||||
        <Table.Column title="启动时间" width={200}
 | 
			
		||||
                      render={info => `${TimestampConverter(info.start_time)}`}/>
 | 
			
		||||
        <Table.Column title="CPU" width={100}
 | 
			
		||||
                      render={info => `${info.cpu_usage}%`}
 | 
			
		||||
                      sorter={(a, b) => a.cpu_usage.localeCompare(b.cpu_usage)}
 | 
			
		||||
                      defaultSortOrder="descend"
 | 
			
		||||
                      sortDirections={['descend']}
 | 
			
		||||
        />
 | 
			
		||||
        <Table.Column title="内存" width={200}
 | 
			
		||||
                      render={info => {
 | 
			
		||||
                          return formatMemory(info.memory) + `(${info.memory_usage}%)`
 | 
			
		||||
                      }}
 | 
			
		||||
                      sorter={(a, b) => a.memory_usage.localeCompare(b.memory_usage)}
 | 
			
		||||
                      defaultSortOrder="descend"
 | 
			
		||||
                      sortDirections={['descend']}
 | 
			
		||||
        />
 | 
			
		||||
        <Table.Column title="命令参数"
 | 
			
		||||
                      render={info => `${info.command}`}
 | 
			
		||||
                      ellipsis
 | 
			
		||||
        />
 | 
			
		||||
    </TableCard>)
 | 
			
		||||
})
 | 
			
		||||
		Loading…
	
		Reference in New Issue