Merge branch 'enhancement/add-storage-to-dash' into 'dev'

Add host storage display to the dashboard

See merge request crafty-controller/crafty-4!551
This commit is contained in:
Iain Powrie 2023-02-17 00:08:03 +00:00
commit 206941e26e
7 changed files with 146 additions and 23 deletions

View File

@ -5,6 +5,7 @@
- Add ignored exit codes for crash detection ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/553))
- Allow users to change the directory where Crafty Stores Servers ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/539)) <br>
*(Only for non-docker, docker users should change host volume mount)*
- Add host storage display option to the dashboard ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/551))
### Bug fixes
- Fix exception related to page data on server start ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/544))
- Fix logical issue with uploading dynamic files ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/555))

View File

@ -417,6 +417,7 @@ class Helpers:
"allow_nsfw_profile_pictures": False,
"enable_user_self_delete": False,
"reset_secrets_on_next_boot": False,
"monitored_mounts": Helpers.get_all_mounts(),
}
def get_all_settings(self):
@ -435,6 +436,14 @@ class Helpers:
return data
@staticmethod
def get_all_mounts():
mounts = []
for item in psutil.disk_partitions(all=False):
mounts.append(item.mountpoint)
return mounts
def is_subdir(self, server_path, root_dir):
server_path = os.path.realpath(server_path)
root_dir = os.path.realpath(root_dir)
@ -1040,9 +1049,9 @@ class Helpers:
if filename not in self.ignored_names:
output += f"""<li id="{dpath}li" class="tree-item"
data-path="{dpath}">
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}"
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}"
class="tree-caret tree-ctx-item tree-folder">
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}"
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}"
data-name="{filename}" onclick="getDirView(event)">
<i style="color: var(--info);" class="far fa-folder"></i>
<i style="color: var(--info);" class="far fa-folder-open"></i>
@ -1081,9 +1090,9 @@ class Helpers:
if filename not in self.ignored_names:
output += f"""<li id="{dpath}li" class="tree-item"
data-path="{dpath}">
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}"
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}"
class="tree-caret tree-ctx-item tree-folder">
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}"
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}"
data-name="{filename}" onclick="getDirView(event)">
<i style="color: var(--info);" class="far fa-folder"></i>
<i style="color: var(--info);" class="far fa-folder-open"></i>

View File

@ -4,6 +4,7 @@ import logging
import threading
import asyncio
import datetime
import json
from tzlocal import get_localzone
from tzlocal.utils import ZoneInfoNotFoundError
@ -686,18 +687,37 @@ class TasksManager:
host_stats = HelpersManagement.get_latest_hosts_stats()
if len(self.helper.websocket_helper.clients) > 0:
# There are clients
self.helper.websocket_helper.broadcast_page(
"/panel/dashboard",
"update_host_stats",
{
"cpu_usage": host_stats.get("cpu_usage"),
"cpu_cores": host_stats.get("cpu_cores"),
"cpu_cur_freq": host_stats.get("cpu_cur_freq"),
"cpu_max_freq": host_stats.get("cpu_max_freq"),
"mem_percent": host_stats.get("mem_percent"),
"mem_usage": host_stats.get("mem_usage"),
},
)
try:
self.helper.websocket_helper.broadcast_page(
"/panel/dashboard",
"update_host_stats",
{
"cpu_usage": host_stats.get("cpu_usage"),
"cpu_cores": host_stats.get("cpu_cores"),
"cpu_cur_freq": host_stats.get("cpu_cur_freq"),
"cpu_max_freq": host_stats.get("cpu_max_freq"),
"mem_percent": host_stats.get("mem_percent"),
"mem_usage": host_stats.get("mem_usage"),
"disk_usage": json.loads(
host_stats.get("disk_json").replace("'", '"')
),
"mounts": self.helper.get_setting("monitored_mounts"),
},
)
except:
self.helper.websocket_helper.broadcast_page(
"/panel/dashboard",
"update_host_stats",
{
"cpu_usage": host_stats.get("cpu_usage"),
"cpu_cores": host_stats.get("cpu_cores"),
"cpu_cur_freq": host_stats.get("cpu_cur_freq"),
"cpu_max_freq": host_stats.get("cpu_max_freq"),
"mem_percent": host_stats.get("mem_percent"),
"mem_usage": host_stats.get("mem_usage"),
"disk_usage": {},
},
)
time.sleep(1)
def check_for_updates(self):

View File

@ -294,6 +294,7 @@ class PanelHandler(BaseHandler):
"background": self.controller.cached_login,
"login_opacity": self.controller.management.get_login_opacity(),
"serverTZ": tz,
"monitored": self.helper.get_setting("monitored_mounts"),
"version_data": self.helper.get_version_string(),
"failed_servers": self.controller.servers.failed_servers,
"user_data": exec_user,
@ -333,7 +334,12 @@ class PanelHandler(BaseHandler):
else None,
"superuser": superuser,
}
try:
page_data["hosts_data"]["disk_json"] = json.loads(
page_data["hosts_data"]["disk_json"].replace("'", '"')
)
except:
page_data["hosts_data"]["disk_json"] = {}
if page == "unauthorized":
template = "panel/denied.html"
@ -884,6 +890,7 @@ class PanelHandler(BaseHandler):
page_data["config-json"] = data
page_data["availables_languages"] = []
page_data["all_languages"] = []
page_data["all_partitions"] = self.helper.get_all_mounts()
for file in sorted(
os.listdir(

View File

@ -71,7 +71,7 @@
<div class="input-group">
<button type="button" class="btn btn-outline-default custom-picker" onclick="$('option', $('#lang_select')).each(function(element) {
$(this).removeAttr('selected').prop('selected', false); $('.selectpicker').selectpicker('refresh')
});">Enable all Languages</button>
});">{{ translate('panelConfig', 'enableLang', data['lang']) }}</button>
<select id="lang_select" class="form-control selectpicker show-tick" data-icon-base="fas"
data-tick-icon="fa-check" multiple data-style="custom-picker">
{% for lang in data['all_languages'] %}
@ -86,6 +86,25 @@
rows="{{ len(data['all_languages']) }}" value="{{','.join(item[1])}}"
hidden>{{','.join(item[1])}}</textarea>
</div>
{% elif item[0] == 'monitored_mounts'%}
<div class="input-group">
<button type="button" class="btn btn-outline-default custom-picker" onclick="$('option', $('#mount_select')).each(function(element) {
$(this).removeAttr('selected').prop('selected', false); $('.selectpicker').selectpicker('refresh')
});">{{ translate('panelConfig', 'noMounts', data['lang']) }}</button>
<select id="mount_select" class="form-control selectpicker show-tick" data-icon-base="fas"
data-tick-icon="fa-check" multiple data-style="custom-picker">
{% for mount in data['all_partitions'] %}
{% if mount in item[1] %}
<option selected>{{mount}}</option>
{% else %}
<option>{{mount}}</option>
{% end %}
{% end %}
</select>
<textarea id="monitored_mounts" name="{{item[0]}}" class="form-control list hidden"
rows="{{ len(data['all_partitions']) }}" value="{{','.join(item[1])}}"
hidden>{{','.join(item[1])}}</textarea>
</div>
{% elif isinstance(item[1], list) %}
<textarea value="{{','.join(item[1])}}" type="text" name="{{item[0]}}"
class="form-control list">{{','.join(item[1])}}</textarea>
@ -160,6 +179,9 @@
let selected_Lang = $('#lang_select').val();
$('#disabled_lang').val(selected_Lang);
let mounts = $('#mount_select').val();
$('#monitored_mounts').val(mounts);
let class_list = document.getElementsByClassName("list");
let form_json = convertFormToJSON($("#config-form"));
for (let i = 0; i < class_list.length; i++) {

View File

@ -42,7 +42,6 @@
},
});
});
</script>
{% end %}
<div class="row">
@ -50,7 +49,7 @@
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-lg-4 col-md-6">
<div class="col-xl-4 col-md-5">
<div class="d-flex">
<div class="wrapper">
<h5 class="mb-1 font-weight-medium text-primary"> {{ translate('dashboard', 'host', data['lang']) }}
@ -72,7 +71,7 @@
</div>
</div>
</div>
<div class="col-lg-4 col-md-6 mt-md-0 mt-4">
<div class="col-xl-4 col-md-4 mt-md-0 mt-4">
<div class="d-flex">
<div class="wrapper">
<h5 class="mb-1 font-weight-medium text-primary">{{ translate('dashboard', 'servers', data['lang']) }}
@ -88,7 +87,7 @@
</div>
</div>
</div>
<div class="col-lg-4 col-md-6 mt-md-0 mt-4">
<div class="col-xl-4 col-md-3 mt-md-0 mt-4">
<div class="d-flex">
<div class="wrapper">
<h5 class="mb-1 font-weight-medium text-primary">{{ translate('dashboard', 'players', data['lang']) }}
@ -101,6 +100,43 @@
</div>
</div>
</div>
<div class="col-12 mt-4">
<div class="d-flex">
<div class="wrapper" style="width: 100%;">
<h5 class="mb-1 font-weight-medium text-primary">Storage
</h5>
<div id="storage_data">
<div class="row">
{% for item in data.get('hosts_data').get('disk_json') %}
{% if item["mount"] in data["monitored"] %}
<div id="{{item['device']}}" class="col-xl-3 col-lg-3 col-md-4 col-12">
<h4 class="mb-0 font-weight-semibold d-inline-block text-truncate storage-heading"
id="title_{{item['device']}}" data-toggle="tooltip" data-placement="bottom"
title="{{item['mount']}}" style="max-width: 100%;"><i class="fas fa-hdd"></i>
{{item["mount"]}}</h4>
<div class="progress d-inline-block"
style="height: 20px; width: 100%; background-color: rgb(139, 139, 139) !important;">
<div class="progress-bar
{% if item['percent_used'] <= 58 %}
bg-success
{% elif 59 <= item['percent_used'] <= 75 %}
bg-warning
{% else %}
bg-danger
{% end %}
" role="progressbar" style="color: black; height: 100%; width: {{item['percent_used']}}%;"
aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">{{item["used"]}} /
{{item["total"]}}
</div>
</div>
</div>
{% end %}
{% end %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@ -891,6 +927,32 @@
cpu_usage.textContent = hostStats.cpu_usage;
mem_usage.setAttribute('data-original-title', `{% raw translate("dashboard", "memUsage", data['lang']) %}: ${hostStats.mem_usage}`);
mem_percent.textContent = hostStats.mem_percent + '%';
var storage_html = '<div class="row">';
for (i = 0; i < hostStats.disk_usage.length; i++) {
if (hostStats.mounts.includes(hostStats.disk_usage[i].mount)) {
storage_html += `<div id="{{item['device']}}" class="col-xl-3 col-lg-3 col-md-4 col-12">
<h4 class="mb-0 font-weight-semibold d-inline-block text-truncate storage-heading" id="title_{{item['device']}}" data-toggle="tooltip" data-placement="bottom" title="${hostStats.disk_usage[i].mount}" style="max-width: 100%;"><i class="fas fa-hdd"></i> ${hostStats.disk_usage[i].mount}</h4>
<div class="progress" style="display: inline-block; height: 20px; width: 100%; background-color: rgb(139, 139, 139) !important;">
<div class="progress-bar`;
if (hostStats.disk_usage[i].percent_used <= 58) {
storage_html += ` bg-success`;
} else if (hostStats.disk_usage[i].percent_used <= 75 && hostStats.disk_usage[i].percent_used >= 59) {
storage_html += ` bg-warning`;
} else {
storage_html += ` bg-danger`;
}
storage_html += `" role="progressbar" style="color: black; height: 100%; width: ${hostStats.disk_usage[i].percent_used}%;"
aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">${hostStats.disk_usage[i].used} / ${hostStats.disk_usage[i].total}
</div>
</div>
</div>`;
}
}
storage_html += `</div>`;
$(".storage-heading").tooltip('hide');
$("#storage_data").html(storage_html);
$("#storage_data").tooltip({ selector: '.storage-heading' });
});
}

View File

@ -233,6 +233,8 @@
"user": "User",
"users": "Users",
"title": "Crafty Configuration",
"enableLang": "Enable All Languages",
"noMounts": "Show no Mounts on Dash",
"globalServer": "Global Servers Directory",
"globalExplain": "Where Crafty stores all your server files. (We will append the path with /servers/[uuid of server])"
},
@ -610,4 +612,4 @@
"manager": "Manager",
"selectManager": "Select Manager for User"
}
}
}