Using websockets for resource monitor instead of rest api
This commit is contained in:
parent
ce74b6b733
commit
eb934c9db1
|
|
@ -0,0 +1,110 @@
|
|||
import pickle
|
||||
import os
|
||||
from flask import Blueprint, request, jsonify, make_response
|
||||
from flask_restx import Api, Resource, fields, Namespace
|
||||
|
||||
# Create a Blueprint for the jobs controller
|
||||
jobs_bp = Blueprint('jobs', __name__)
|
||||
jobs_api = Api(jobs_bp, version='1.0', title='Jobs API',
|
||||
description='API for managing jobs')
|
||||
|
||||
# Define a namespace for jobs
|
||||
jobs_ns = Namespace('jobs', description='Job operations')
|
||||
|
||||
# Define the model for a job
|
||||
job_model = jobs_ns.model('Job', {
|
||||
'id': fields.Integer(required=True, description='The unique identifier of the job'),
|
||||
'description': fields.String(required=True, description='Description of the job'),
|
||||
'status': fields.String(description='Status of the job')
|
||||
})
|
||||
|
||||
|
||||
# File to persist data
|
||||
DATA_FILE = 'jobs.pkl'
|
||||
|
||||
|
||||
def load_jobs():
|
||||
if os.path.exists(DATA_FILE):
|
||||
with open(DATA_FILE, 'rb') as file:
|
||||
return pickle.load(file)
|
||||
else:
|
||||
# Create an empty file if it doesn't exist
|
||||
with open(DATA_FILE, 'wb') as file:
|
||||
pickle.dump({}, file)
|
||||
return {}
|
||||
|
||||
|
||||
def save_jobs(jobs):
|
||||
with open(DATA_FILE, 'wb') as file:
|
||||
pickle.dump(jobs, file)
|
||||
|
||||
|
||||
# Load initial data
|
||||
jobs_store = load_jobs()
|
||||
|
||||
|
||||
@jobs_ns.route('/')
|
||||
class JobList(Resource):
|
||||
def get(self):
|
||||
"""List all jobs"""
|
||||
jobs_store = load_jobs()
|
||||
return _corsify_actual_response(jsonify(list(jobs_store.values())))
|
||||
|
||||
@jobs_ns.expect(job_model)
|
||||
def post(self):
|
||||
"""Create a new job"""
|
||||
if request.method == "OPTIONS": # CORS preflight
|
||||
return _build_cors_preflight_response()
|
||||
|
||||
job = request.json
|
||||
job_id = job['id']
|
||||
if job_id in jobs_store:
|
||||
return {'message': 'Job already exists'}, 400
|
||||
jobs_store[job_id] = job
|
||||
save_jobs(jobs_store) # Save to file
|
||||
return _corsify_actual_response(jsonify(job))
|
||||
|
||||
|
||||
@jobs_ns.route('/<int:job_id>')
|
||||
class JobItem(Resource):
|
||||
def get(self, job_id):
|
||||
"""Get a job by ID"""
|
||||
job = jobs_store.get(job_id)
|
||||
if job is None:
|
||||
return {'message': 'Job not found'}, 404
|
||||
return _corsify_actual_response(jsonify(job))
|
||||
|
||||
@jobs_ns.expect(job_model)
|
||||
def put(self, job_id):
|
||||
"""Update a job by ID"""
|
||||
if request.method == "OPTIONS": # CORS preflight
|
||||
return _build_cors_preflight_response()
|
||||
job = request.json
|
||||
if job_id not in jobs_store:
|
||||
return {'message': 'Job not found'}, 404
|
||||
jobs_store[job_id] = job
|
||||
save_jobs(jobs_store) # Save to file
|
||||
return _corsify_actual_response(jsonify(job))
|
||||
|
||||
def delete(self, job_id):
|
||||
"""Delete a job by ID"""
|
||||
if request.method == "OPTIONS": # CORS preflight
|
||||
return _build_cors_preflight_response()
|
||||
if job_id not in jobs_store:
|
||||
return {'message': 'Job not found'}, 404
|
||||
del jobs_store[job_id]
|
||||
save_jobs(jobs_store) # Save to file
|
||||
return _corsify_actual_response(jsonify({'message': 'Job deleted'}))
|
||||
|
||||
|
||||
def _build_cors_preflight_response():
|
||||
response = make_response()
|
||||
response.headers.add("Access-Control-Allow-Origin", "*")
|
||||
response.headers.add("Access-Control-Allow-Headers", "*")
|
||||
response.headers.add("Access-Control-Allow-Methods", "*")
|
||||
return response
|
||||
|
||||
|
||||
def _corsify_actual_response(response):
|
||||
response.headers.add("Access-Control-Allow-Origin", "*")
|
||||
return response
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import json
|
||||
import os
|
||||
from flask import Blueprint, jsonify, request
|
||||
from flask_restx import Api, Resource, fields, Namespace
|
||||
|
||||
# Create a Blueprint for the settings controller
|
||||
settings_bp = Blueprint('settings', __name__)
|
||||
settings_api = Api(settings_bp, version='1.0', title='Settings API',
|
||||
description='API for managing settings')
|
||||
|
||||
# Define a namespace for settings
|
||||
settings_ns = Namespace('settings', description='Settings operations')
|
||||
|
||||
# Define the model for settings
|
||||
settings_model = settings_ns.model('Setting', {
|
||||
'key': fields.String(required=True, description='The key of the setting'),
|
||||
'value': fields.String(required=True, description='The value of the setting')
|
||||
})
|
||||
|
||||
# File to persist settings data
|
||||
SETTINGS_FILE = 'settings.json'
|
||||
|
||||
|
||||
def load_settings():
|
||||
if os.path.exists(SETTINGS_FILE):
|
||||
with open(SETTINGS_FILE, 'r') as file:
|
||||
return json.load(file)
|
||||
return {}
|
||||
|
||||
|
||||
def save_settings(settings):
|
||||
with open(SETTINGS_FILE, 'w') as file:
|
||||
json.dump(settings, file, indent=4)
|
||||
|
||||
|
||||
# Load initial data
|
||||
settings_store = load_settings()
|
||||
|
||||
|
||||
@settings_ns.route('/')
|
||||
class SettingsList(Resource):
|
||||
def get(self):
|
||||
"""List all settings"""
|
||||
return jsonify({'settings': list(settings_store.values())})
|
||||
|
||||
@settings_ns.expect(settings_model)
|
||||
def post(self):
|
||||
"""Create or update a setting"""
|
||||
setting = request.json
|
||||
key = setting['key']
|
||||
settings_store[key] = setting
|
||||
save_settings(settings_store) # Save to file
|
||||
return jsonify(setting)
|
||||
|
||||
|
||||
@settings_ns.route('/<string:key>')
|
||||
class SettingItem(Resource):
|
||||
def get(self, key):
|
||||
"""Get a setting by key"""
|
||||
setting = settings_store.get(key)
|
||||
if setting is None:
|
||||
return {'message': 'Setting not found'}, 404
|
||||
return jsonify(setting)
|
||||
|
||||
def delete(self, key):
|
||||
"""Delete a setting by key"""
|
||||
if key not in settings_store:
|
||||
return {'message': 'Setting not found'}, 404
|
||||
del settings_store[key]
|
||||
save_settings(settings_store) # Save to file
|
||||
return {'message': 'Setting deleted'}
|
||||
|
|
@ -43,6 +43,7 @@ def check_GPUtil_installed():
|
|||
|
||||
try:
|
||||
import GPUtil
|
||||
import psutil
|
||||
return True
|
||||
except ImportError:
|
||||
import_GPUtil()
|
||||
|
|
@ -55,6 +56,8 @@ def check_flask_installed():
|
|||
import flask
|
||||
import flask_restx
|
||||
import flask_cors
|
||||
|
||||
import flask_socketio
|
||||
return True
|
||||
except ImportError:
|
||||
import_flask()
|
||||
|
|
@ -137,11 +140,13 @@ def import_tkinter():
|
|||
|
||||
|
||||
def import_GPUtil():
|
||||
run_pip(f"install GPUtil", desc="GPU Utility for NVIDIA GPUs")
|
||||
run_pip(f"install GPUtil psutil", desc="GPU Utility for NVIDIA GPUs")
|
||||
|
||||
try:
|
||||
GPUtil = importlib.import_module(
|
||||
"GPUtil")
|
||||
psutil = importlib.import_module(
|
||||
"psutil")
|
||||
return GPUtil
|
||||
except ImportError:
|
||||
print("Failed to import GPUtil after installation.")
|
||||
|
|
@ -149,11 +154,12 @@ def import_GPUtil():
|
|||
|
||||
|
||||
def import_flask():
|
||||
run_pip(f"install flask flask-restx flask-cors", desc="Flask Rest API")
|
||||
run_pip(f"install flask flask-restx flask-cors flask_socketio", desc="Flask Rest API")
|
||||
|
||||
try:
|
||||
flask = importlib.import_module("flask")
|
||||
restx = importlib.import_module("flask-restx")
|
||||
flask_socketio = importlib.import_module("flask_socketio")
|
||||
return restx
|
||||
except ImportError:
|
||||
print("Failed to import flask after installation.")
|
||||
|
|
@ -203,6 +209,6 @@ def run_pip(command, desc=None, live=default_command_live):
|
|||
return None
|
||||
|
||||
|
||||
# check_tkinter_installed()
|
||||
check_tkinter_installed()
|
||||
check_GPUtil_installed()
|
||||
check_flask_installed()
|
||||
|
|
|
|||
|
|
@ -1,48 +1,116 @@
|
|||
|
||||
from .dependency_installer import *
|
||||
from flask import Flask, send_from_directory, jsonify, render_template
|
||||
from flask import Flask, send_from_directory, render_template
|
||||
from flask_socketio import SocketIO, emit
|
||||
from flask_restx import Api
|
||||
import threading
|
||||
import logging
|
||||
from flask_cors import CORS
|
||||
import time
|
||||
from .controllers import register_blueprints
|
||||
import os
|
||||
import psutil
|
||||
import GPUtil
|
||||
import threading
|
||||
|
||||
# Initialize Flask app
|
||||
title = f"Resource Monitor"
|
||||
app = Flask(title, static_folder='web/assets', template_folder='web/templates')
|
||||
app.config['CORS_HEADERS'] = 'Content-Type'
|
||||
|
||||
def load_page(filename):
|
||||
"""Load an HTML file as a string and return it"""
|
||||
file_path = os.path.join("web", filename)
|
||||
with open(file_path, 'r') as file:
|
||||
content = file.read()
|
||||
return content
|
||||
# Initialize Flask-RESTx API
|
||||
api = Api(app, version='1.0', title=title, description='API for system resource monitoring')
|
||||
|
||||
# Register blueprints (API endpoints)
|
||||
register_blueprints(app, api)
|
||||
|
||||
# Initialize SocketIO with the Flask app
|
||||
socketio = SocketIO(app, cors_allowed_origins="*")
|
||||
|
||||
# Cache for system usage data
|
||||
cache = {
|
||||
'timestamp': 0,
|
||||
'data': {
|
||||
'cpu': 0,
|
||||
'ram': 0,
|
||||
'gpu': 0,
|
||||
'vram': 0,
|
||||
'hdd': 0,
|
||||
'temp': 0
|
||||
}
|
||||
}
|
||||
CACHE_DURATION = 1 # Cache duration in seconds
|
||||
|
||||
# Suppress the Flask development server warning
|
||||
log = logging.getLogger('werkzeug')
|
||||
log.setLevel(logging.ERROR) # Set level to ERROR to suppress warnings
|
||||
|
||||
title = f"Elegant Resource Monitor"
|
||||
app = Flask(title, static_folder='web/assets', template_folder='web/templates')
|
||||
app.config['CORS_HEADERS'] = 'Content-Type'
|
||||
api = Api(app, version='1.0', title=title,
|
||||
description='Elegant Resource Monitor REST API')
|
||||
def get_cache(current_time):
|
||||
# Get CPU utilization
|
||||
cpu_percent = psutil.cpu_percent(interval=0)
|
||||
|
||||
# Register blueprints (API endpoints)
|
||||
register_blueprints(app, api)
|
||||
# Get Memory utilization
|
||||
mem = psutil.virtual_memory()
|
||||
mem_percent = mem.percent
|
||||
|
||||
# Enable CORS for all origins
|
||||
CORS(app, resources={r"/*": {"origins": "*"}})
|
||||
# Get GPU utilization (considering only the first GPU)
|
||||
gpus = GPUtil.getGPUs()
|
||||
gpu_percent = gpus[0].load * 100 if gpus else 0
|
||||
|
||||
# Get VRAM usage (considering only the first GPU)
|
||||
vram_usage = 0
|
||||
if gpus:
|
||||
used = gpus[0].memoryUsed
|
||||
total = gpus[0].memoryTotal
|
||||
vram_usage = (used / total) * 100
|
||||
|
||||
# Get HDD usage (assuming usage of the primary disk)
|
||||
hdd = psutil.disk_usage('/')
|
||||
hdd_percent = hdd.percent
|
||||
|
||||
# Get temperature (if available)
|
||||
temperature = gpus[0].temperature if gpus else 0
|
||||
|
||||
# Update the cache
|
||||
cache['data'] = {
|
||||
'cpu': cpu_percent,
|
||||
'ram': mem_percent,
|
||||
'gpu': gpu_percent,
|
||||
'vram': vram_usage, # Convert bytes to MB
|
||||
'hdd': hdd_percent,
|
||||
'temp': temperature # Add temperature
|
||||
}
|
||||
cache['timestamp'] = current_time
|
||||
|
||||
@app.route('/')
|
||||
def home():
|
||||
return render_template('index.html')
|
||||
|
||||
@app.route('/<path:filename>')
|
||||
def serve_static(filename):
|
||||
return send_from_directory('web', filename)
|
||||
|
||||
@socketio.on('connect')
|
||||
def handle_connect():
|
||||
# Emit initial data
|
||||
current_time = time.time()
|
||||
get_cache(current_time)
|
||||
emit('data_update', cache['data'])
|
||||
|
||||
@socketio.on('disconnect')
|
||||
def handle_disconnect():
|
||||
pass
|
||||
|
||||
def background_thread():
|
||||
while True:
|
||||
current_time = time.time()
|
||||
get_cache(current_time)
|
||||
socketio.emit('data_update', cache['data'])
|
||||
time.sleep(.5)
|
||||
|
||||
def run_app():
|
||||
app.run(port=5000)
|
||||
|
||||
time.sleep(1) # Sleep for a short while to let the server start
|
||||
# Start the background thread for emitting data
|
||||
socketio.start_background_task(target=background_thread)
|
||||
# Run the Flask app with SocketIO
|
||||
socketio.run(app, port=5000)
|
||||
|
||||
# Start Flask app in a separate thread
|
||||
thread = threading.Thread(target=run_app)
|
||||
thread.start()
|
||||
thread.start()
|
||||
|
|
@ -21,16 +21,16 @@
|
|||
left: 0px;
|
||||
height: 0% !important;
|
||||
width: 0% !important;
|
||||
transform: scale(0);
|
||||
position: fixed;
|
||||
z-index: 9;
|
||||
z-index: 9991;
|
||||
will-change: transform;
|
||||
text-align: center;
|
||||
transition: width 0.8s ease, height 0.8s ease, transform 0.3s ease;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#chart-button-container.active {
|
||||
display: block;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
#mydivheader {
|
||||
|
|
@ -214,6 +214,7 @@ button {
|
|||
}
|
||||
|
||||
#settingsMenu.show {
|
||||
min-width: 110px;
|
||||
opacity: 1 !important;
|
||||
z-index: 1000 !important;
|
||||
transform: scale(0.8) translateY(-30px) translateX(-10px) !important; /* Animate to visible state */
|
||||
|
|
@ -324,3 +325,74 @@ button {
|
|||
.window {
|
||||
transition: width 0.3s ease, height 0.3s ease;
|
||||
}
|
||||
|
||||
#show_resource_monitor_link {
|
||||
cursor: pointer !important;
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 10px;
|
||||
z-index: 9991;
|
||||
margin: 5px;
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.resource-monitor-icon {
|
||||
margin: 5px;
|
||||
height: 14px;
|
||||
}
|
||||
#item-table {
|
||||
width: -webkit-fill-available;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
margin-bottom: 5px;
|
||||
border-collapse: separate; /* Ensures that border-radius works */
|
||||
border-spacing: 0; /* Removes spacing between cells */
|
||||
border: 0px solid #ddd; /* Adds border to the table */
|
||||
border-radius: 8px; /* Adjust the value for desired corner radius */
|
||||
overflow: hidden; /* Ensures rounded corners are visible */
|
||||
}
|
||||
|
||||
#item-table th,
|
||||
#item-table td {
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
text-align: left;
|
||||
line-height: 10px;
|
||||
}
|
||||
|
||||
#item-table thead {
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#item-table tbody tr:nth-child(even) {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
#item-table tbody tr:nth-child(odd) {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
#item-table tbody td {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#progress-bar-container {
|
||||
width: -webkit-fill-available;
|
||||
|
||||
height: 20px; /* Height of the progress bar */
|
||||
background-color: #e0e0e0; /* Light gray background */
|
||||
border-radius: 10px; /* Rounded corners for the progress bar container */
|
||||
margin: 0px 5px; /* Space between the progress bar and the table */
|
||||
display: none;
|
||||
}
|
||||
|
||||
#progress-bar {
|
||||
height: 100%;
|
||||
width: 0%; /* Start with 0% width */
|
||||
background-color: #76c7c0; /* Fill color of the progress bar */
|
||||
border-radius: 10px; /* Rounded corners for the progress bar */
|
||||
transition: width 0.5s ease; /* Smooth transition effect */
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,137 +1,139 @@
|
|||
// VARS AND CONST
|
||||
const hostname = "localhost"; // Gets the host without port
|
||||
const baseUrl = `http://${hostname}:5000`; // Append the port 5000
|
||||
const apiUrl = `${baseUrl}/gpu_usage/`;
|
||||
const hostname = 'localhost' // Gets the host without port
|
||||
const baseUrl = `http://${hostname}:5000` // Append the port 5000
|
||||
const apiUrl = `${baseUrl}/gpu_usage/`
|
||||
|
||||
const colorPalette = [
|
||||
"rgb(240, 193, 90, 0.2)",
|
||||
"rgb(240, 142, 219, 0.2)",
|
||||
"rgb(24, 90, 219, 0.2)",
|
||||
"rgb(127, 161, 195, 0.2)",
|
||||
"rgb(128, 239, 145, 0.2)",
|
||||
"rgb(245, 245, 245, 0.2)",
|
||||
"rgb(240, 142, 219, 0.2)",
|
||||
"rgb(159, 238, 209, 0.2)",
|
||||
];
|
||||
'rgb(240, 193, 90, 0.2)',
|
||||
'rgb(240, 142, 219, 0.2)',
|
||||
'rgb(24, 90, 219, 0.2)',
|
||||
'rgb(127, 161, 195, 0.2)',
|
||||
'rgb(128, 239, 145, 0.2)',
|
||||
'rgb(245, 245, 245, 0.2)',
|
||||
'rgb(240, 142, 219, 0.2)',
|
||||
'rgb(159, 238, 209, 0.2)',
|
||||
]
|
||||
|
||||
const borderColors = [
|
||||
"rgb(240, 193, 90)",
|
||||
"rgb(240, 142, 219)",
|
||||
"rgb(24, 90, 219)",
|
||||
"rgb(127, 161, 195)",
|
||||
"rgb(128, 239, 145)",
|
||||
"rgb(245, 245, 245)",
|
||||
"rgb(240, 142, 219)",
|
||||
"rgb(159, 238, 209)",
|
||||
];
|
||||
'rgb(240, 193, 90)',
|
||||
'rgb(240, 142, 219)',
|
||||
'rgb(24, 90, 219)',
|
||||
'rgb(127, 161, 195)',
|
||||
'rgb(128, 239, 145)',
|
||||
'rgb(245, 245, 245)',
|
||||
'rgb(240, 142, 219)',
|
||||
'rgb(159, 238, 209)',
|
||||
]
|
||||
|
||||
let currentChart = null; // Track the current chart instance
|
||||
const MAX_DATA_POINTS = 50; // Number of data points to keep
|
||||
let currentChart = null // Track the current chart instance
|
||||
const MAX_DATA_POINTS = 50 // Number of data points to keep
|
||||
|
||||
// Custom plugin to draw fixed labels in the middle of the chart area
|
||||
const fixedLabelPlugin = {
|
||||
id: "fixedLabelPlugin",
|
||||
id: 'fixedLabelPlugin',
|
||||
afterDatasetsDraw(chart) {
|
||||
const { ctx, scales, data } = chart;
|
||||
ctx.save();
|
||||
const { ctx, scales, data } = chart
|
||||
ctx.save()
|
||||
|
||||
const centerX = scales.x.left + scales.x.width / 2;
|
||||
const labelPositions = [];
|
||||
const centerX = scales.x.left + scales.x.width / 2
|
||||
const labelPositions = []
|
||||
data.datasets[0].data.forEach((value, index) => {
|
||||
const yPos = chart.getDatasetMeta(0).data[index].y;
|
||||
const yPos = chart.getDatasetMeta(0).data[index].y
|
||||
|
||||
// Store yPos for positioning labels
|
||||
labelPositions.push({
|
||||
x: centerX,
|
||||
y: yPos,
|
||||
value: `${+value.toFixed(2)}` + `${index == 5 ? "\u00B0" : "%"}`,
|
||||
});
|
||||
});
|
||||
const size = localStorage.getItem("chart-size") ?? "small";
|
||||
let fontSize = 10; // Default font size
|
||||
value: `${+value.toFixed(2)}` + `${index == 5 ? '\u00B0' : '%'}`,
|
||||
})
|
||||
})
|
||||
const size = localStorage.getItem('chart-size') ?? 'small'
|
||||
let fontSize = 10 // Default font size
|
||||
|
||||
switch (size) {
|
||||
case "small":
|
||||
fontSize = "10px";
|
||||
break;
|
||||
case "medium":
|
||||
fontSize = "16px";
|
||||
break;
|
||||
case 'small':
|
||||
fontSize = '7px'
|
||||
break
|
||||
case 'medium':
|
||||
fontSize = '16px'
|
||||
break
|
||||
default:
|
||||
fontSize = "18px";
|
||||
break;
|
||||
fontSize = '18px'
|
||||
break
|
||||
}
|
||||
|
||||
ctx.font = fontSize;
|
||||
ctx.fillStyle = "#FFFFFF";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.font = fontSize
|
||||
ctx.fillStyle = '#FFFFFF'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
|
||||
labelPositions.forEach((label) => {
|
||||
ctx.fillText(label.value, label.x, label.y);
|
||||
});
|
||||
ctx.fillText(label.value, label.x, label.y)
|
||||
})
|
||||
|
||||
ctx.restore();
|
||||
ctx.restore()
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Initialize the bar chart
|
||||
function initializeBarChart() {
|
||||
localStorage.setItem("active-chart", "bar");
|
||||
const chartContainer = document.getElementById("chart-container");
|
||||
const existingCanvas = document.getElementById("usage-chart");
|
||||
const chartWrapper = document.getElementById("chart-wrapper");
|
||||
window.initializeBarChart = function () {
|
||||
localStorage.setItem('active-chart', 'bar')
|
||||
var table = document.getElementById('item-table')
|
||||
table.style.display = 'none'
|
||||
const chartContainer = document.getElementById('chart-container')
|
||||
const existingCanvas = document.getElementById('usage-chart')
|
||||
const chartWrapper = document.getElementById('chart-wrapper')
|
||||
if (existingCanvas) {
|
||||
chartContainer.removeChild(existingCanvas);
|
||||
chartContainer.removeChild(existingCanvas)
|
||||
}
|
||||
const size = localStorage.getItem("chart-size") ?? "small";
|
||||
let fontSize = 10; // Default font size
|
||||
const size = localStorage.getItem('chart-size') ?? 'small'
|
||||
let fontSize = 10 // Default font size
|
||||
|
||||
switch (size) {
|
||||
case "small":
|
||||
fontSize = "10px";
|
||||
break;
|
||||
case "medium":
|
||||
fontSize = "16px";
|
||||
break;
|
||||
case 'small':
|
||||
fontSize = '7px'
|
||||
break
|
||||
case 'medium':
|
||||
fontSize = '16px'
|
||||
break
|
||||
default:
|
||||
fontSize = "18px";
|
||||
break;
|
||||
fontSize = '18px'
|
||||
break
|
||||
}
|
||||
|
||||
// Create a new canvas element
|
||||
const newCanvas = document.createElement("canvas");
|
||||
newCanvas.id = "usage-chart";
|
||||
newCanvas.classList.add("bar"); // Add the class directly to the canvas element
|
||||
chartContainer.appendChild(newCanvas);
|
||||
const newCanvas = document.createElement('canvas')
|
||||
newCanvas.id = 'usage-chart'
|
||||
newCanvas.classList.add('bar') // Add the class directly to the canvas element
|
||||
chartContainer.appendChild(newCanvas)
|
||||
|
||||
const ctx = newCanvas.getContext("2d");
|
||||
$(chartWrapper).hide();
|
||||
const ctx = newCanvas.getContext('2d')
|
||||
$(chartWrapper).hide()
|
||||
|
||||
currentChart = new Chart(ctx, {
|
||||
type: "bar",
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ["CPU", "RAM", "GPU", "VRAM", "HDD", "TEMP"],
|
||||
labels: ['CPU', 'RAM', 'GPU', 'VRAM', 'HDD', 'TEMP'],
|
||||
datasets: [
|
||||
{
|
||||
label: "Usage",
|
||||
label: 'Usage',
|
||||
data: [0, 0, 0, 0, 0],
|
||||
barPercentage: 0.8, // Adjust space occupied by bars
|
||||
categoryPercentage: 1, // Adjust space between bars
|
||||
backgroundColor: function (context) {
|
||||
const value = context.dataset.data[context.dataIndex];
|
||||
return value > 90 ? "#D9534F" : colorPalette[context.dataIndex];
|
||||
const value = context.dataset.data[context.dataIndex]
|
||||
return value > 90 ? '#D9534F' : colorPalette[context.dataIndex]
|
||||
},
|
||||
borderColor: function (context) {
|
||||
const value = context.dataset.data[context.dataIndex];
|
||||
return value > 90 ? "#D9534F" : borderColors[context.dataIndex];
|
||||
const value = context.dataset.data[context.dataIndex]
|
||||
return value > 90 ? '#D9534F' : borderColors[context.dataIndex]
|
||||
},
|
||||
borderWidth: 1.5,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
indexAxis: "y", // Horizontal bars
|
||||
indexAxis: 'y', // Horizontal bars
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
|
|
@ -143,14 +145,14 @@ function initializeBarChart() {
|
|||
beginAtZero: true,
|
||||
max: 100,
|
||||
ticks: {
|
||||
color: "#ffffff",
|
||||
color: '#ffffff',
|
||||
font: {
|
||||
size: fontSize,
|
||||
weight: 600,
|
||||
},
|
||||
align: "center",
|
||||
align: 'center',
|
||||
callback: function (value, index, ticks) {
|
||||
return value + "%";
|
||||
return value + '%'
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -159,12 +161,12 @@ function initializeBarChart() {
|
|||
display: false,
|
||||
},
|
||||
border: {
|
||||
color: "#ffffff30",
|
||||
color: '#ffffff30',
|
||||
width: 1, // Width of the axis border
|
||||
},
|
||||
ticks: {
|
||||
color: "#FFFFFF",
|
||||
crossAlign: "far",
|
||||
color: '#FFFFFF',
|
||||
crossAlign: 'far',
|
||||
font: {
|
||||
weight: 600,
|
||||
size: fontSize,
|
||||
|
|
@ -189,207 +191,213 @@ function initializeBarChart() {
|
|||
maintainAspectRatio: false,
|
||||
},
|
||||
plugins: [fixedLabelPlugin], // Register the custom plugins
|
||||
});
|
||||
})
|
||||
|
||||
currentChart.options.animation = true;
|
||||
const legendContainer = document.getElementById("custom-legend");
|
||||
legendContainer.innerHTML = "";
|
||||
currentChart.options.animation = true
|
||||
const legendContainer = document.getElementById('custom-legend')
|
||||
legendContainer.innerHTML = ''
|
||||
|
||||
document.getElementById("settingsMenu").classList.remove("show"); // Hide the menu
|
||||
document.getElementById('settingsMenu').classList.remove('show') // Hide the menu
|
||||
|
||||
document.querySelectorAll("canvas").forEach((row) => {
|
||||
row.classList.remove("no-drag");
|
||||
row.classList.add("drag");
|
||||
});
|
||||
document.querySelectorAll('canvas').forEach((row) => {
|
||||
row.classList.remove('no-drag')
|
||||
row.classList.add('drag')
|
||||
})
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
currentChart.resize();
|
||||
});
|
||||
$(chartWrapper).fadeIn(300);
|
||||
window.addEventListener('resize', () => {
|
||||
currentChart.resize()
|
||||
})
|
||||
$(chartWrapper).fadeIn(300)
|
||||
if (size == 'large') {
|
||||
$(table).fadeIn(800)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the line chart
|
||||
function initializeLineChart() {
|
||||
localStorage.setItem("active-chart", "line");
|
||||
const existingCanvas = document.getElementById("usage-chart");
|
||||
const chartContainer = document.getElementById("chart-container");
|
||||
const chartWrapper = document.getElementById("chart-wrapper");
|
||||
window.initializeLineChart = function () {
|
||||
localStorage.setItem('active-chart', 'line')
|
||||
var table = document.getElementById('item-table')
|
||||
table.style.display = 'none'
|
||||
const size = localStorage.getItem('chart-size') ?? 'medium'
|
||||
const existingCanvas = document.getElementById('usage-chart')
|
||||
const chartContainer = document.getElementById('chart-container')
|
||||
const chartWrapper = document.getElementById('chart-wrapper')
|
||||
if (existingCanvas) {
|
||||
chartContainer.removeChild(existingCanvas);
|
||||
chartContainer.removeChild(existingCanvas)
|
||||
}
|
||||
|
||||
// Create a new canvas element
|
||||
const newCanvas = document.createElement("canvas");
|
||||
newCanvas.id = "usage-chart";
|
||||
newCanvas.classList.add("line"); // Add the class directly to the canvas element
|
||||
chartContainer.appendChild(newCanvas);
|
||||
$(chartWrapper).hide();
|
||||
const newCanvas = document.createElement('canvas')
|
||||
newCanvas.id = 'usage-chart'
|
||||
newCanvas.classList.add('line') // Add the class directly to the canvas element
|
||||
chartContainer.appendChild(newCanvas)
|
||||
$(chartWrapper).hide()
|
||||
|
||||
const ctx = newCanvas.getContext("2d");
|
||||
const ctx = newCanvas.getContext('2d')
|
||||
|
||||
currentChart = new Chart(ctx, {
|
||||
type: "line",
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: "CPU",
|
||||
label: 'CPU',
|
||||
data: [],
|
||||
borderColor: function (context) {
|
||||
const dataset = context.dataset;
|
||||
const datasetIndex = context.datasetIndex;
|
||||
const shouldUseRed = dataset.data.some((value) => value > 90);
|
||||
const dataset = context.dataset
|
||||
const datasetIndex = context.datasetIndex
|
||||
const shouldUseRed = dataset.data.some((value) => value > 90)
|
||||
|
||||
if (shouldUseRed) {
|
||||
return "#D9534F"; // Return red color if any value exceeds 90
|
||||
return '#D9534F' // Return red color if any value exceeds 90
|
||||
}
|
||||
return borderColors[datasetIndex % borderColors.length];
|
||||
return borderColors[datasetIndex % borderColors.length]
|
||||
},
|
||||
borderWidth: 1.5,
|
||||
backgroundColor: function (context) {
|
||||
const dataset = context.dataset;
|
||||
const datasetIndex = context.datasetIndex;
|
||||
const shouldUseRed = dataset.data.some((value) => value > 90);
|
||||
const dataset = context.dataset
|
||||
const datasetIndex = context.datasetIndex
|
||||
const shouldUseRed = dataset.data.some((value) => value > 90)
|
||||
|
||||
if (shouldUseRed) {
|
||||
return "#D9534F"; // Return red color if any value exceeds 90
|
||||
return '#D9534F' // Return red color if any value exceeds 90
|
||||
}
|
||||
return colorPalette[datasetIndex % borderColors.length];
|
||||
return colorPalette[datasetIndex % borderColors.length]
|
||||
},
|
||||
fill: false,
|
||||
tension: 0.1,
|
||||
},
|
||||
{
|
||||
label: "RAM",
|
||||
label: 'RAM',
|
||||
data: [],
|
||||
borderColor: function (context) {
|
||||
const dataset = context.dataset;
|
||||
const datasetIndex = context.datasetIndex;
|
||||
const shouldUseRed = dataset.data.some((value) => value > 90);
|
||||
const dataset = context.dataset
|
||||
const datasetIndex = context.datasetIndex
|
||||
const shouldUseRed = dataset.data.some((value) => value > 90)
|
||||
|
||||
if (shouldUseRed) {
|
||||
return "#D9534F"; // Return red color if any value exceeds 90
|
||||
return '#D9534F' // Return red color if any value exceeds 90
|
||||
}
|
||||
return borderColors[datasetIndex % borderColors.length];
|
||||
return borderColors[datasetIndex % borderColors.length]
|
||||
},
|
||||
borderWidth: 1.5,
|
||||
backgroundColor: function (context) {
|
||||
const dataset = context.dataset;
|
||||
const datasetIndex = context.datasetIndex;
|
||||
const shouldUseRed = dataset.data.some((value) => value > 90);
|
||||
const dataset = context.dataset
|
||||
const datasetIndex = context.datasetIndex
|
||||
const shouldUseRed = dataset.data.some((value) => value > 90)
|
||||
|
||||
if (shouldUseRed) {
|
||||
return "#D9534F"; // Return red color if any value exceeds 90
|
||||
return '#D9534F' // Return red color if any value exceeds 90
|
||||
}
|
||||
return colorPalette[datasetIndex % borderColors.length];
|
||||
return colorPalette[datasetIndex % borderColors.length]
|
||||
},
|
||||
fill: false,
|
||||
tension: 0.1,
|
||||
},
|
||||
{
|
||||
label: "GPU",
|
||||
label: 'GPU',
|
||||
data: [],
|
||||
borderColor: function (context) {
|
||||
const dataset = context.dataset;
|
||||
const datasetIndex = context.datasetIndex;
|
||||
const shouldUseRed = dataset.data.some((value) => value > 90);
|
||||
const dataset = context.dataset
|
||||
const datasetIndex = context.datasetIndex
|
||||
const shouldUseRed = dataset.data.some((value) => value > 90)
|
||||
|
||||
if (shouldUseRed) {
|
||||
return "#D9534F"; // Return red color if any value exceeds 90
|
||||
return '#D9534F' // Return red color if any value exceeds 90
|
||||
}
|
||||
return borderColors[datasetIndex % borderColors.length];
|
||||
return borderColors[datasetIndex % borderColors.length]
|
||||
},
|
||||
borderWidth: 1.5,
|
||||
backgroundColor: function (context) {
|
||||
const dataset = context.dataset;
|
||||
const datasetIndex = context.datasetIndex;
|
||||
const shouldUseRed = dataset.data.some((value) => value > 90);
|
||||
const dataset = context.dataset
|
||||
const datasetIndex = context.datasetIndex
|
||||
const shouldUseRed = dataset.data.some((value) => value > 90)
|
||||
|
||||
if (shouldUseRed) {
|
||||
return "#D9534F"; // Return red color if any value exceeds 90
|
||||
return '#D9534F' // Return red color if any value exceeds 90
|
||||
}
|
||||
return colorPalette[datasetIndex % borderColors.length];
|
||||
return colorPalette[datasetIndex % borderColors.length]
|
||||
},
|
||||
fill: false,
|
||||
tension: 0.1,
|
||||
},
|
||||
{
|
||||
label: "VRAM",
|
||||
label: 'VRAM',
|
||||
data: [],
|
||||
borderColor: function (context) {
|
||||
const dataset = context.dataset;
|
||||
const datasetIndex = context.datasetIndex;
|
||||
const shouldUseRed = dataset.data.some((value) => value > 90);
|
||||
const dataset = context.dataset
|
||||
const datasetIndex = context.datasetIndex
|
||||
const shouldUseRed = dataset.data.some((value) => value > 90)
|
||||
|
||||
if (shouldUseRed) {
|
||||
return "#D9534F"; // Return red color if any value exceeds 90
|
||||
return '#D9534F' // Return red color if any value exceeds 90
|
||||
}
|
||||
return borderColors[datasetIndex % borderColors.length];
|
||||
return borderColors[datasetIndex % borderColors.length]
|
||||
},
|
||||
borderWidth: 1.5,
|
||||
backgroundColor: function (context) {
|
||||
const dataset = context.dataset;
|
||||
const datasetIndex = context.datasetIndex;
|
||||
const shouldUseRed = dataset.data.some((value) => value > 90);
|
||||
const dataset = context.dataset
|
||||
const datasetIndex = context.datasetIndex
|
||||
const shouldUseRed = dataset.data.some((value) => value > 90)
|
||||
|
||||
if (shouldUseRed) {
|
||||
return "#D9534F"; // Return red color if any value exceeds 90
|
||||
return '#D9534F' // Return red color if any value exceeds 90
|
||||
}
|
||||
return colorPalette[datasetIndex % borderColors.length];
|
||||
return colorPalette[datasetIndex % borderColors.length]
|
||||
},
|
||||
fill: false,
|
||||
tension: 0.1,
|
||||
},
|
||||
{
|
||||
label: "HDD",
|
||||
label: 'HDD',
|
||||
data: [],
|
||||
borderColor: function (context) {
|
||||
const dataset = context.dataset;
|
||||
const datasetIndex = context.datasetIndex;
|
||||
const shouldUseRed = dataset.data.some((value) => value > 90);
|
||||
const dataset = context.dataset
|
||||
const datasetIndex = context.datasetIndex
|
||||
const shouldUseRed = dataset.data.some((value) => value > 90)
|
||||
|
||||
if (shouldUseRed) {
|
||||
return "#D9534F"; // Return red color if any value exceeds 90
|
||||
return '#D9534F' // Return red color if any value exceeds 90
|
||||
}
|
||||
return borderColors[datasetIndex % borderColors.length];
|
||||
return borderColors[datasetIndex % borderColors.length]
|
||||
},
|
||||
borderWidth: 1.5,
|
||||
backgroundColor: function (context) {
|
||||
const dataset = context.dataset;
|
||||
const datasetIndex = context.datasetIndex;
|
||||
const shouldUseRed = dataset.data.some((value) => value > 90);
|
||||
const dataset = context.dataset
|
||||
const datasetIndex = context.datasetIndex
|
||||
const shouldUseRed = dataset.data.some((value) => value > 90)
|
||||
|
||||
if (shouldUseRed) {
|
||||
return "#D9534F"; // Return red color if any value exceeds 90
|
||||
return '#D9534F' // Return red color if any value exceeds 90
|
||||
}
|
||||
return colorPalette[datasetIndex % borderColors.length];
|
||||
return colorPalette[datasetIndex % borderColors.length]
|
||||
},
|
||||
fill: false,
|
||||
tension: 0.1,
|
||||
},
|
||||
{
|
||||
label: "TEMP",
|
||||
label: 'TEMP',
|
||||
data: [],
|
||||
borderColor: function (context) {
|
||||
const dataset = context.dataset;
|
||||
const datasetIndex = context.datasetIndex;
|
||||
const shouldUseRed = dataset.data.some((value) => value > 90);
|
||||
const dataset = context.dataset
|
||||
const datasetIndex = context.datasetIndex
|
||||
const shouldUseRed = dataset.data.some((value) => value > 90)
|
||||
|
||||
if (shouldUseRed) {
|
||||
return "#D9534F"; // Return red color if any value exceeds 90
|
||||
return '#D9534F' // Return red color if any value exceeds 90
|
||||
}
|
||||
return borderColors[datasetIndex % borderColors.length];
|
||||
return borderColors[datasetIndex % borderColors.length]
|
||||
},
|
||||
borderWidth: 1.5,
|
||||
backgroundColor: function (context) {
|
||||
const dataset = context.dataset;
|
||||
const datasetIndex = context.datasetIndex;
|
||||
const shouldUseRed = dataset.data.some((value) => value > 90);
|
||||
const dataset = context.dataset
|
||||
const datasetIndex = context.datasetIndex
|
||||
const shouldUseRed = dataset.data.some((value) => value > 90)
|
||||
|
||||
if (shouldUseRed) {
|
||||
return "#D9534F"; // Return red color if any value exceeds 90
|
||||
return '#D9534F' // Return red color if any value exceeds 90
|
||||
}
|
||||
return colorPalette[datasetIndex % borderColors.length];
|
||||
return colorPalette[datasetIndex % borderColors.length]
|
||||
},
|
||||
fill: false,
|
||||
tension: 0.1,
|
||||
|
|
@ -401,7 +409,7 @@ function initializeLineChart() {
|
|||
enabled: false,
|
||||
tension: {
|
||||
duration: 1000,
|
||||
easing: "linear",
|
||||
easing: 'linear',
|
||||
from: 1,
|
||||
to: 0,
|
||||
loop: true,
|
||||
|
|
@ -422,15 +430,15 @@ function initializeLineChart() {
|
|||
beginAtZero: true,
|
||||
max: 100,
|
||||
ticks: {
|
||||
color: "#FFFFFF",
|
||||
crossAlign: "far",
|
||||
color: '#FFFFFF',
|
||||
crossAlign: 'far',
|
||||
padding: 0,
|
||||
font: {
|
||||
weight: 600,
|
||||
size: 7,
|
||||
},
|
||||
callback: function (value, index, ticks) {
|
||||
return value + "%";
|
||||
return value + '%'
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -448,61 +456,64 @@ function initializeLineChart() {
|
|||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
currentChart.options.animation = false;
|
||||
generateCustomLegend();
|
||||
document.getElementById("settingsMenu").classList.remove("show"); // Hide the menu
|
||||
currentChart.options.animation = false
|
||||
generateCustomLegend()
|
||||
document.getElementById('settingsMenu').classList.remove('show') // Hide the menu
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
currentChart.resize();
|
||||
});
|
||||
$(chartWrapper).fadeIn(300);
|
||||
window.addEventListener('resize', () => {
|
||||
currentChart.resize()
|
||||
})
|
||||
$(chartWrapper).fadeIn(300)
|
||||
if (size == 'large') {
|
||||
$(table).fadeIn(800)
|
||||
}
|
||||
}
|
||||
|
||||
function generateCustomLegend() {
|
||||
const legendContainer = document.getElementById("custom-legend");
|
||||
legendContainer.innerHTML = "";
|
||||
const legendContainer = document.getElementById('custom-legend')
|
||||
legendContainer.innerHTML = ''
|
||||
|
||||
currentChart.data.datasets.forEach((dataset, index) => {
|
||||
const legendItem = document.createElement("div");
|
||||
legendItem.className = "custom-legend-item";
|
||||
const legendItem = document.createElement('div')
|
||||
legendItem.className = 'custom-legend-item'
|
||||
|
||||
// Create text element
|
||||
const legendText = document.createElement("span");
|
||||
legendText.className = "custom-legend-text";
|
||||
legendText.textContent = dataset.label;
|
||||
const shouldUseRed = dataset.data.some((value) => value > 90);
|
||||
legendText.style.color = shouldUseRed
|
||||
? "#D9534F"
|
||||
: `${borderColors[index]}`;
|
||||
const legendText = document.createElement('span')
|
||||
legendText.className = 'custom-legend-text'
|
||||
legendText.textContent = dataset.label
|
||||
const shouldUseRed = dataset.data.some((value) => value > 90)
|
||||
legendText.style.color = shouldUseRed ? '#D9534F' : `${borderColors[index]}`
|
||||
|
||||
legendText.style.fontWeight = shouldUseRed ? "800" : `600`;
|
||||
const size = localStorage.getItem("chart-size") ?? "small";
|
||||
legendText.style.fontWeight = shouldUseRed ? '800' : `600`
|
||||
const size = localStorage.getItem('chart-size') ?? 'small'
|
||||
switch (size) {
|
||||
case "small":
|
||||
legendText.style.fontSize = "10px";
|
||||
break;
|
||||
case "medium":
|
||||
legendText.style.fontSize = "16px";
|
||||
break;
|
||||
case 'small':
|
||||
legendText.style.fontSize = '7px'
|
||||
break
|
||||
case 'medium':
|
||||
legendText.style.fontSize = '16px'
|
||||
break
|
||||
default:
|
||||
legendText.style.fontSize = "18px";
|
||||
break;
|
||||
legendText.style.fontSize = '18px'
|
||||
break
|
||||
}
|
||||
|
||||
legendItem.appendChild(legendText);
|
||||
legendContainer.appendChild(legendItem);
|
||||
});
|
||||
legendItem.appendChild(legendText)
|
||||
legendContainer.appendChild(legendItem)
|
||||
})
|
||||
}
|
||||
async function updateUsage() {
|
||||
try {
|
||||
const response = await fetch(apiUrl);
|
||||
const data = await response.json();
|
||||
const timestamp = new Date();
|
||||
|
||||
setTimeout(() => {
|
||||
const socket = io('http://localhost:5000')
|
||||
|
||||
// Function to update the chart with new data
|
||||
function updateChart(data) {
|
||||
const timestamp = new Date().toLocaleTimeString()
|
||||
|
||||
if (currentChart) {
|
||||
if (currentChart.config.type === "bar") {
|
||||
if (currentChart.config.type === 'bar') {
|
||||
// Update data for bar chart
|
||||
currentChart.data.datasets[0].data = [
|
||||
data.cpu,
|
||||
|
|
@ -511,42 +522,42 @@ async function updateUsage() {
|
|||
data.vram,
|
||||
data.hdd,
|
||||
data.temp,
|
||||
];
|
||||
} else if (currentChart.config.type === "line") {
|
||||
]
|
||||
} else if (currentChart.config.type === 'line') {
|
||||
// Update data for line chart
|
||||
currentChart.data.labels.push(timestamp);
|
||||
currentChart.data.datasets[0].data.push(data.cpu);
|
||||
currentChart.data.datasets[1].data.push(data.ram);
|
||||
currentChart.data.datasets[2].data.push(data.gpu);
|
||||
currentChart.data.datasets[3].data.push(data.vram);
|
||||
currentChart.data.datasets[4].data.push(data.hdd);
|
||||
currentChart.data.datasets[5].data.push(data.temp);
|
||||
currentChart.data.labels.push(timestamp)
|
||||
currentChart.data.datasets[0].data.push(data.cpu)
|
||||
currentChart.data.datasets[1].data.push(data.ram)
|
||||
currentChart.data.datasets[2].data.push(data.gpu)
|
||||
currentChart.data.datasets[3].data.push(data.vram)
|
||||
currentChart.data.datasets[4].data.push(data.hdd)
|
||||
currentChart.data.datasets[5].data.push(data.temp)
|
||||
|
||||
// Prune old data if the number of points exceeds the limit
|
||||
const MAX_DATA_POINTS = 50 // Adjust as needed
|
||||
if (currentChart.data.labels.length > MAX_DATA_POINTS) {
|
||||
currentChart.data.labels.shift(); // Remove the oldest label
|
||||
currentChart.data.datasets.forEach((dataset) => dataset.data.shift()); // Remove the oldest data points
|
||||
currentChart.data.labels.shift() // Remove the oldest label
|
||||
currentChart.data.datasets.forEach((dataset) => dataset.data.shift()) // Remove the oldest data points
|
||||
}
|
||||
generateCustomLegend();
|
||||
generateCustomLegend()
|
||||
}
|
||||
|
||||
// Update the chart with new data
|
||||
currentChart.update();
|
||||
currentChart.update()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch usage data.", error);
|
||||
}
|
||||
}
|
||||
|
||||
let intervalId; // Variable to store the interval ID
|
||||
// Function to start the interval
|
||||
function startInterval() {
|
||||
intervalId = setInterval(updateUsage, 500);
|
||||
}
|
||||
// Function to stop the interval
|
||||
function stopInterval() {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
intervalId = null; // Optional: Reset intervalId to indicate no active interval
|
||||
}
|
||||
}
|
||||
// Handle data updates from the WebSocket server
|
||||
socket.on('data_update', function (data) {
|
||||
updateChart(data)
|
||||
})
|
||||
|
||||
// Optional: Handle WebSocket connection errors
|
||||
socket.on('connect_error', function (error) {
|
||||
console.error('Connection error:', error)
|
||||
})
|
||||
|
||||
// Optional: Handle WebSocket disconnection
|
||||
socket.on('disconnect', function () {
|
||||
console.log('WebSocket disconnected')
|
||||
})
|
||||
}, 4000)
|
||||
|
|
|
|||
|
|
@ -11694,6 +11694,7 @@
|
|||
);
|
||||
});
|
||||
//# sourceMappingURL=chart.umd.js.map
|
||||
|
||||
/*!
|
||||
* chartjs-plugin-datalabels v2.2.0
|
||||
* https://chartjs-plugin-datalabels.netlify.app
|
||||
|
|
|
|||
|
|
@ -19,6 +19,14 @@ var script = document.createElement('script')
|
|||
script.src = '/file=web/assets/js/jquery-3.7.1.min.js'
|
||||
document.body.appendChild(script)
|
||||
|
||||
var script = document.createElement('script')
|
||||
script.src = '/file=web/assets/js/elegant-resource-monitor.js'
|
||||
document.body.appendChild(script)
|
||||
|
||||
var script = document.createElement('script')
|
||||
script.src = '/file=web/assets/js/socket.io.min.js'
|
||||
document.body.appendChild(script)
|
||||
|
||||
var script = document.createElement('script')
|
||||
script.src = '/file=web/assets/js/chart.js'
|
||||
document.body.appendChild(script)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,211 @@
|
|||
import { api } from '/scripts/api.js'
|
||||
import { getResolver } from './shared_utils.js'
|
||||
export class ElegantResourceMonitorExecution {
|
||||
constructor(id) {
|
||||
this.promptApi = null
|
||||
this.executedNodeIds = []
|
||||
this.totalNodes = 0
|
||||
this.currentlyExecuting = null
|
||||
this.errorDetails = null
|
||||
this.apiPrompt = getResolver()
|
||||
this.id = id
|
||||
}
|
||||
setPrompt(prompt) {
|
||||
this.promptApi = prompt.output
|
||||
this.totalNodes = Object.keys(this.promptApi).length
|
||||
this.apiPrompt.resolve(null)
|
||||
}
|
||||
getApiNode(nodeId) {
|
||||
var _a
|
||||
return ((_a = this.promptApi) === null || _a === void 0 ? void 0 : _a[String(nodeId)]) || null
|
||||
}
|
||||
getNodeLabel(nodeId) {
|
||||
var _a, _b
|
||||
const apiNode = this.getApiNode(nodeId)
|
||||
let label =
|
||||
((_a = apiNode === null || apiNode === void 0 ? void 0 : apiNode._meta) === null ||
|
||||
_a === void 0
|
||||
? void 0
|
||||
: _a.title) ||
|
||||
(apiNode === null || apiNode === void 0 ? void 0 : apiNode.class_type) ||
|
||||
undefined
|
||||
if (!label) {
|
||||
const graphNode =
|
||||
(_b = this.maybeGetComfyGraph()) === null || _b === void 0
|
||||
? void 0
|
||||
: _b.getNodeById(Number(nodeId))
|
||||
label =
|
||||
(graphNode === null || graphNode === void 0 ? void 0 : graphNode.title) ||
|
||||
(graphNode === null || graphNode === void 0 ? void 0 : graphNode.type) ||
|
||||
undefined
|
||||
}
|
||||
return label
|
||||
}
|
||||
executing(nodeId, step, maxSteps) {
|
||||
var _a
|
||||
if (nodeId == null) {
|
||||
this.currentlyExecuting = null
|
||||
return
|
||||
}
|
||||
if (
|
||||
((_a = this.currentlyExecuting) === null || _a === void 0 ? void 0 : _a.nodeId) !== nodeId
|
||||
) {
|
||||
if (this.currentlyExecuting != null) {
|
||||
this.executedNodeIds.push(nodeId)
|
||||
}
|
||||
this.currentlyExecuting = { nodeId, nodeLabel: this.getNodeLabel(nodeId), pass: 0 }
|
||||
this.apiPrompt.promise.then(() => {
|
||||
var _a
|
||||
if (this.currentlyExecuting == null) {
|
||||
return
|
||||
}
|
||||
const apiNode = this.getApiNode(nodeId)
|
||||
if (!this.currentlyExecuting.nodeLabel) {
|
||||
this.currentlyExecuting.nodeLabel = this.getNodeLabel(nodeId)
|
||||
}
|
||||
if (
|
||||
(apiNode === null || apiNode === void 0 ? void 0 : apiNode.class_type) ===
|
||||
'UltimateSDUpscale'
|
||||
) {
|
||||
this.currentlyExecuting.pass--
|
||||
this.currentlyExecuting.maxPasses = -1
|
||||
} else if (
|
||||
(apiNode === null || apiNode === void 0 ? void 0 : apiNode.class_type) ===
|
||||
'IterativeImageUpscale'
|
||||
) {
|
||||
this.currentlyExecuting.maxPasses =
|
||||
(_a = apiNode === null || apiNode === void 0 ? void 0 : apiNode.inputs['steps']) !==
|
||||
null && _a !== void 0
|
||||
? _a
|
||||
: -1
|
||||
}
|
||||
})
|
||||
}
|
||||
if (step != null) {
|
||||
if (!this.currentlyExecuting.step || step < this.currentlyExecuting.step) {
|
||||
this.currentlyExecuting.pass++
|
||||
}
|
||||
this.currentlyExecuting.step = step
|
||||
this.currentlyExecuting.maxSteps = maxSteps
|
||||
}
|
||||
}
|
||||
error(details) {
|
||||
this.errorDetails = details
|
||||
}
|
||||
maybeGetComfyGraph() {
|
||||
var _a
|
||||
return (
|
||||
((_a = window === null || window === void 0 ? void 0 : window.app) === null || _a === void 0
|
||||
? void 0
|
||||
: _a.graph) || null
|
||||
)
|
||||
}
|
||||
}
|
||||
class ElegantResourceMonitorService extends EventTarget {
|
||||
constructor(api) {
|
||||
super()
|
||||
this.promptsMap = new Map()
|
||||
this.currentExecution = null
|
||||
this.lastQueueRemaining = 0
|
||||
const that = this
|
||||
const queuePrompt = api.queuePrompt
|
||||
api.queuePrompt = async function (num, prompt) {
|
||||
let response
|
||||
try {
|
||||
response = await queuePrompt.apply(api, [...arguments])
|
||||
} catch (e) {
|
||||
const promptExecution = that.getOrMakePrompt('error')
|
||||
promptExecution.error({ exception_type: 'Unknown.' })
|
||||
throw e
|
||||
}
|
||||
const promptExecution = that.getOrMakePrompt(response.prompt_id)
|
||||
promptExecution.setPrompt(prompt)
|
||||
if (!that.currentExecution) {
|
||||
that.currentExecution = promptExecution
|
||||
}
|
||||
that.promptsMap.set(response.prompt_id, promptExecution)
|
||||
|
||||
return response
|
||||
}
|
||||
api.addEventListener('status', (e) => {
|
||||
var _a
|
||||
if (!((_a = e.detail) === null || _a === void 0 ? void 0 : _a.exec_info)) return
|
||||
this.lastQueueRemaining = e.detail.exec_info.queue_remaining
|
||||
this.dispatchProgressUpdate()
|
||||
})
|
||||
api.addEventListener('execution_start', (e) => {
|
||||
if (!this.promptsMap.has(e.detail.prompt_id)) {
|
||||
console.warn("'execution_start' fired before prompt was made.")
|
||||
}
|
||||
const prompt = this.getOrMakePrompt(e.detail.prompt_id)
|
||||
this.currentExecution = prompt
|
||||
this.dispatchProgressUpdate()
|
||||
})
|
||||
api.addEventListener('executing', (e) => {
|
||||
if (!this.currentExecution) {
|
||||
this.currentExecution = this.getOrMakePrompt('unknown')
|
||||
console.warn("'executing' fired before prompt was made.")
|
||||
}
|
||||
this.currentExecution.executing(e.detail)
|
||||
this.dispatchProgressUpdate()
|
||||
if (e.detail == null) {
|
||||
this.currentExecution = null
|
||||
}
|
||||
})
|
||||
api.addEventListener('progress', (e) => {
|
||||
if (!this.currentExecution) {
|
||||
this.currentExecution = this.getOrMakePrompt(e.detail.prompt_id)
|
||||
console.warn("'progress' fired before prompt was made.")
|
||||
}
|
||||
this.currentExecution.executing(e.detail.node, e.detail.value, e.detail.max)
|
||||
this.dispatchProgressUpdate()
|
||||
})
|
||||
api.addEventListener('execution_cached', (e) => {
|
||||
if (!this.currentExecution) {
|
||||
this.currentExecution = this.getOrMakePrompt(e.detail.prompt_id)
|
||||
console.warn("'execution_cached' fired before prompt was made.")
|
||||
}
|
||||
for (const cached of e.detail.nodes) {
|
||||
this.currentExecution.executing(cached)
|
||||
}
|
||||
this.dispatchProgressUpdate()
|
||||
})
|
||||
api.addEventListener('executed', (e) => {
|
||||
if (!this.currentExecution) {
|
||||
this.currentExecution = this.getOrMakePrompt(e.detail.prompt_id)
|
||||
console.warn("'executed' fired before prompt was made.")
|
||||
}
|
||||
})
|
||||
api.addEventListener('execution_error', (e) => {
|
||||
var _a
|
||||
if (!this.currentExecution) {
|
||||
this.currentExecution = this.getOrMakePrompt(e.detail.prompt_id)
|
||||
console.warn("'execution_error' fired before prompt was made.")
|
||||
}
|
||||
;(_a = this.currentExecution) === null || _a === void 0 ? void 0 : _a.error(e.detail)
|
||||
this.dispatchProgressUpdate()
|
||||
})
|
||||
}
|
||||
async queuePrompt(prompt) {
|
||||
return await api.queuePrompt(-1, prompt)
|
||||
}
|
||||
dispatchProgressUpdate() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('elegant-resource-monitor-update', {
|
||||
detail: {
|
||||
queue: this.lastQueueRemaining,
|
||||
prompt: this.currentExecution,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
getOrMakePrompt(id) {
|
||||
let prompt = this.promptsMap.get(id)
|
||||
if (!prompt) {
|
||||
prompt = new ElegantResourceMonitorExecution(id)
|
||||
this.promptsMap.set(id, prompt)
|
||||
}
|
||||
return prompt
|
||||
}
|
||||
}
|
||||
export const MONITOR_SERVICE = new ElegantResourceMonitorService(api)
|
||||
|
|
@ -0,0 +1,288 @@
|
|||
class ElegantResourceMonitor extends HTMLElement {
|
||||
constructor() {
|
||||
super()
|
||||
this.shadow = null
|
||||
this.currentPromptExecution = null
|
||||
this.onProgressUpdateBound = this.onProgressUpdate.bind(this)
|
||||
this.connected = false
|
||||
}
|
||||
|
||||
render() {
|
||||
this.innerHTML = `
|
||||
<div id="chart-button-container" draggable="true">
|
||||
<div id="chart-button">
|
||||
<div class="chart-row">
|
||||
<div class="left-col">
|
||||
<i class="material-icons" id="popupTrigger">settings</i>
|
||||
<div id="settingsMenu" class="settings-menu">
|
||||
<div class="settings-row"><div class="settings-col">Settings</div></div>
|
||||
<hr id="settings-hr" class="settings-hr" />
|
||||
<div class="settings-row">
|
||||
<div class="settings-row">
|
||||
<div class="settings-col">Layout:</div>
|
||||
<div class="settings-col">
|
||||
<a href="#" onclick="barChart()">1</a> |
|
||||
<a href="#" onclick="lineChart()">2</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<div class="settings-col">Size:</div>
|
||||
<div class="settings-col">
|
||||
<a href="#" onclick="smallChart()">S</a> |
|
||||
<a href="#" onclick="mediumChart()">M</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<div class="settings-col">Position</div>
|
||||
<div id="positionMenu" class="position-menu">
|
||||
<button class="position-btn position-clickable" id="top-left"><i class="material-icons">north_west</i></button>
|
||||
<button class="position-btn position-clickable" id="top-center"><i class="material-icons">north</i></button>
|
||||
<button class="position-btn position-clickable" id="top-right"><i class="material-icons">north_east</i></button>
|
||||
<button class="position-btn position-clickable" id="left-center"><i class="material-icons">west</i></button>
|
||||
<button class="position-btn position-clickable" id="center" onclick="largeChart()"><i class="material-icons">radio_button_checked</i></button>
|
||||
<button class="position-btn position-clickable" id="right-center"><i class="material-icons">east</i></button>
|
||||
<button class="position-btn position-clickable" id="bottom-left"><i class="material-icons">south_west</i></button>
|
||||
<button class="position-btn position-clickable" id="bottom-center"><i class="material-icons">south</i></button>
|
||||
<button class="position-btn position-clickable" id="bottom-right"><i class="material-icons">south_east</i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-col">
|
||||
<i class="material-icons" id="close-button">close</i>
|
||||
</div>
|
||||
</div>
|
||||
<div id="chart-wrapper">
|
||||
<div id="progress-bar-container">
|
||||
<div id="progress-bar"></div>
|
||||
</div>
|
||||
<div id="chart-container">
|
||||
<canvas id="usage-chart" style="width: 100%; height: 100%"></canvas>
|
||||
</div>
|
||||
<div id="custom-legend"></div>
|
||||
<div id="table-view">
|
||||
<table id="item-table">
|
||||
<tbody id="item-body">
|
||||
<!-- Table rows will be dynamically added here -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
addEventListeners() {
|
||||
this.querySelector('#popupTrigger').addEventListener('click', () => {
|
||||
const settingsMenu = this.querySelector('#settingsMenu')
|
||||
settingsMenu.style.display = settingsMenu.style.display === 'block' ? 'none' : 'block'
|
||||
})
|
||||
|
||||
this.querySelector('#close-button').addEventListener('click', () => {
|
||||
this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true }))
|
||||
})
|
||||
}
|
||||
|
||||
getSizes() {
|
||||
const savedChart = localStorage.getItem('active-chart') ?? 'bar'
|
||||
var sizes = {}
|
||||
if (savedChart == 'bar') {
|
||||
sizes = {
|
||||
small: { height: '120', width: '150' },
|
||||
medium: { height: '300', width: '410' },
|
||||
large: { height: '450', width: '700' },
|
||||
}
|
||||
} else {
|
||||
sizes = {
|
||||
small: { height: '110', width: '160' },
|
||||
medium: { height: '245', width: '425' },
|
||||
large: { height: '380', width: '700' },
|
||||
}
|
||||
}
|
||||
return sizes
|
||||
}
|
||||
getButtonSize() {
|
||||
const size = localStorage.getItem('chart-size') ?? 'medium'
|
||||
const sizes = this.getSizes()
|
||||
const sizeStyles = sizes[size]
|
||||
var buttonHeight = +sizeStyles.height + 25
|
||||
const buttonWidth = +sizeStyles.width + 25
|
||||
var table = this.querySelector('#item-table')
|
||||
var totalRowCount = table.rows.length // 5
|
||||
var tableHeight = totalRowCount * 30
|
||||
if (size == 'large') {
|
||||
buttonHeight = +buttonHeight + tableHeight
|
||||
}
|
||||
|
||||
return { buttonHeight, buttonWidth }
|
||||
}
|
||||
|
||||
addListItem(itemContent) {
|
||||
const itemBody = this.querySelector('#item-body')
|
||||
const row = document.createElement('tr')
|
||||
const cell = document.createElement('td')
|
||||
cell.innerText = itemContent
|
||||
row.appendChild(cell)
|
||||
itemBody.appendChild(row)
|
||||
}
|
||||
|
||||
get currentNodeId() {
|
||||
var _a, _b
|
||||
const prompt = this.currentPromptExecution
|
||||
const nodeId =
|
||||
((_a = prompt === null || prompt === void 0 ? void 0 : prompt.errorDetails) === null ||
|
||||
_a === void 0
|
||||
? void 0
|
||||
: _a.node_id) ||
|
||||
((_b = prompt === null || prompt === void 0 ? void 0 : prompt.currentlyExecuting) === null ||
|
||||
_b === void 0
|
||||
? void 0
|
||||
: _b.nodeId)
|
||||
return nodeId || null
|
||||
}
|
||||
|
||||
adjustButtonHeight(e) {
|
||||
const chartButtonContainer = this.querySelector('#chart-button-container')
|
||||
|
||||
const shouldShowPerfMonitor = JSON.parse(localStorage.getItem('shouldShowPerfMonitor')) ?? false
|
||||
if (!shouldShowPerfMonitor || chartButtonContainer.clientHeight == 0) return
|
||||
|
||||
const { buttonHeight, buttonWidth } = this.getButtonSize()
|
||||
const chartContainer = this.querySelector('#chart-container')
|
||||
const savedChart = localStorage.getItem('active-chart') ?? 'bar'
|
||||
const size = localStorage.getItem('chart-size') ?? 'medium'
|
||||
|
||||
const height = this.querySelector('#chart-container').style.height.replace('px', '')
|
||||
let bar = this.querySelector('#progress-bar-container')
|
||||
|
||||
var actulaButtonHeight = 0
|
||||
var actualButtonWidth = 0
|
||||
|
||||
var viewportHeight = +buttonHeight
|
||||
var viewportWidth = +buttonWidth
|
||||
|
||||
$(chartButtonContainer).each(function () {
|
||||
this.style.setProperty('height', `${viewportHeight}px`, 'important')
|
||||
this.style.setProperty('width', `${viewportWidth}px`, 'important')
|
||||
})
|
||||
var table = this.querySelector('#item-table')
|
||||
var totalRowCount = table.rows.length // 5
|
||||
var tableHeight = totalRowCount * 35
|
||||
var workArea = actulaButtonHeight
|
||||
if (size == 'large') {
|
||||
workArea = viewportHeight - tableHeight
|
||||
}
|
||||
|
||||
const prompt = e.detail.prompt
|
||||
if (prompt && prompt.currentlyExecuting) {
|
||||
bar.style.display = 'block'
|
||||
workArea = workArea - 30
|
||||
} else {
|
||||
bar.style.display = 'none'
|
||||
}
|
||||
|
||||
if (savedChart == 'bar') {
|
||||
$(chartContainer).each(function () {
|
||||
this.style.setProperty('height', `${workArea * 0.95}px`, 'important')
|
||||
})
|
||||
} else {
|
||||
$(chartContainer).each(function () {
|
||||
this.style.setProperty('height', `${workArea * 0.87}px`, 'important')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onProgressUpdate(e) {
|
||||
// this.adjustButtonHeight(e)
|
||||
var _a, _b, _c, _d
|
||||
if (!this.connected) return
|
||||
const prompt = e.detail.prompt
|
||||
|
||||
this.currentPromptExecution = prompt
|
||||
|
||||
// Default progress to 0 if no totalNodes
|
||||
let progressPercentage = 0
|
||||
|
||||
if (prompt === null || prompt === void 0 ? void 0 : prompt.errorDetails) {
|
||||
let progressText = `${
|
||||
(_a = prompt.errorDetails) === null || _a === void 0 ? void 0 : _a.exception_type
|
||||
} ${((_b = prompt.errorDetails) === null || _b === void 0 ? void 0 : _b.node_id) || ''} ${
|
||||
((_c = prompt.errorDetails) === null || _c === void 0 ? void 0 : _c.node_type) || ''
|
||||
}`
|
||||
console.log(progressText)
|
||||
// Set the progress bar to 0% or some error state if needed
|
||||
this.querySelector('#progress-bar').style.width = '0%'
|
||||
return
|
||||
}
|
||||
if (prompt === null || prompt === void 0 ? void 0 : prompt.currentlyExecuting) {
|
||||
const current = prompt === null || prompt === void 0 ? void 0 : prompt.currentlyExecuting
|
||||
|
||||
let progressText = `(${e.detail.queue}) `
|
||||
if (!prompt.totalNodes) {
|
||||
progressText += `??%`
|
||||
} else {
|
||||
progressPercentage = (prompt.executedNodeIds.length / prompt.totalNodes) * 100
|
||||
progressText += `${Math.round(progressPercentage)}%`
|
||||
}
|
||||
|
||||
let nodeLabel = (_d = current.nodeLabel) === null || _d === void 0 ? void 0 : _d.trim()
|
||||
const monitor = document.querySelector('elegant-resource-monitor')
|
||||
// monitor.addListItem(nodeLabel)
|
||||
|
||||
let stepsLabel = ''
|
||||
if (current.step != null && current.maxSteps) {
|
||||
const percent = (current.step / current.maxSteps) * 100
|
||||
if (current.pass > 1 || current.maxPasses != null) {
|
||||
stepsLabel += `#${current.pass}`
|
||||
if (current.maxPasses && current.maxPasses > 0) {
|
||||
stepsLabel += `/${current.maxPasses}`
|
||||
}
|
||||
stepsLabel += ` - `
|
||||
}
|
||||
stepsLabel += `${Math.round(percent)}%`
|
||||
}
|
||||
|
||||
if (nodeLabel || stepsLabel) {
|
||||
progressText += ` - ${nodeLabel || '???'}${stepsLabel ? ` (${stepsLabel})` : ''}`
|
||||
}
|
||||
console.log(progressText)
|
||||
} else {
|
||||
if (e === null || e === void 0 ? void 0 : e.detail.queue) {
|
||||
console.log(`(${e.detail.queue}) Running... in another tab`)
|
||||
} else {
|
||||
console.log('Idle')
|
||||
}
|
||||
}
|
||||
|
||||
// Update the progress bar width
|
||||
this.querySelector('#progress-bar').style.width = `${Math.round(progressPercentage)}%`
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (!this.connected) {
|
||||
console.log('Adding event listener to MONITOR_SERVICE')
|
||||
// MONITOR_SERVICE.addEventListener(
|
||||
// 'elegant-resource-monitor-update',
|
||||
// this.onProgressUpdateBound,
|
||||
// )
|
||||
|
||||
this.render()
|
||||
this.addEventListeners()
|
||||
|
||||
this.connected = true
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.connected = false
|
||||
// MONITOR_SERVICE.removeEventListener(
|
||||
// 'elegant-resource-monitor-update',
|
||||
// this.onProgressUpdateBound,
|
||||
// )
|
||||
}
|
||||
}
|
||||
|
||||
// Register the custom element
|
||||
customElements.define('elegant-resource-monitor', ElegantResourceMonitor)
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
dragElement(document.getElementById('chart-button-container'))
|
||||
|
||||
var wasDragged = false
|
||||
function dragElement(elmnt) {
|
||||
var isDragging = false
|
||||
|
|
@ -52,13 +53,7 @@ function dragElement(elmnt) {
|
|||
}
|
||||
|
||||
function moveButtonToCenter(duration = 300) {
|
||||
const widget = document.getElementById('chart-button-container')
|
||||
const size = localStorage.getItem('chart-size') ?? 'medium'
|
||||
const sizes = getSizes()
|
||||
const sizeStyles = sizes[size]
|
||||
const buttonHeight = +sizeStyles.height + 25
|
||||
const buttonWidth = +sizeStyles.width + 25
|
||||
|
||||
const { buttonHeight, buttonWidth } = getButtonSize()
|
||||
// Get button dimensions and viewport dimensions
|
||||
const widgetWidth = buttonWidth
|
||||
const widgetHeight = buttonHeight
|
||||
|
|
@ -84,13 +79,6 @@ function moveButtonToCenter(duration = 300) {
|
|||
|
||||
// HELPER FUNCTIONS //
|
||||
|
||||
if (localStorage.getItem('lastClass') && localStorage.getItem('lastInactiveClass')) {
|
||||
var lastClass = JSON.parse(localStorage.getItem('lastClass'))
|
||||
var lastInactiveClass = JSON.parse(localStorage.getItem('lastInactiveClass'))
|
||||
addCSS(lastInactiveClass.key, lastInactiveClass.values[0])
|
||||
addCSS(lastClass.key, lastClass.values[0])
|
||||
}
|
||||
|
||||
function getCSSRule(ruleName) {
|
||||
ruleName = ruleName.toLowerCase()
|
||||
var result = null
|
||||
|
|
@ -112,22 +100,6 @@ function getCSSRule(ruleName) {
|
|||
return result
|
||||
}
|
||||
|
||||
function addCSS(selector, styles) {
|
||||
var rule = getCSSRule(selector)
|
||||
|
||||
for (var property in styles) {
|
||||
if (styles.hasOwnProperty(property)) {
|
||||
rule.style.setProperty(property, styles[property], 'important')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateCSS(selector, styles) {
|
||||
var button = getCSSRule(selector)
|
||||
button.style.setProperty('bottom', styles.bottom, 'important')
|
||||
button.style.setProperty('right', styles.right, 'important')
|
||||
}
|
||||
|
||||
window.barChart = async function () {
|
||||
checkForUpdates('active-chart', 'bar')
|
||||
await updateChartSize()
|
||||
|
|
@ -151,9 +123,9 @@ window.mediumChart = async function () {
|
|||
window.largeChart = async function () {
|
||||
setTimeout(async () => {
|
||||
checkForUpdates('perf-monitor-position', 'center')
|
||||
checkForUpdates('chart-size', 'large')
|
||||
await updateChartSize()
|
||||
}, 50)
|
||||
checkForUpdates('chart-size', 'large')
|
||||
}
|
||||
|
||||
function moveToCenter() {
|
||||
|
|
@ -169,14 +141,10 @@ function checkForUpdates(key, value) {
|
|||
}
|
||||
|
||||
function isWindowOutsideWorkingArea() {
|
||||
const size = localStorage.getItem('chart-size') ?? 'medium'
|
||||
const sizes = getSizes()
|
||||
const sizeStyles = sizes[size]
|
||||
const buttonHeight = +sizeStyles.height + 25
|
||||
const buttonWidth = +sizeStyles.width + 25
|
||||
const { buttonHeight, buttonWidth } = getButtonSize()
|
||||
|
||||
// Get display bounds
|
||||
const { displayBounds, windowBounds } = getDisplayAndWindowBounds()
|
||||
const { displayBounds } = getDisplayAndWindowBounds()
|
||||
|
||||
const widget = document.getElementById('chart-button-container')
|
||||
const rect = widget.getBoundingClientRect()
|
||||
|
|
@ -327,8 +295,6 @@ function goToPosition(pos) {
|
|||
const currentLeft = +widget.style.left.replace('px', '')
|
||||
|
||||
// Target position
|
||||
const targetTop = pos.y
|
||||
const targetLeft = pos.x
|
||||
|
||||
const offsetX = pos.x - currentLeft
|
||||
const offsetY = pos.y - currentTop
|
||||
|
|
@ -342,8 +308,6 @@ function goToPosition(pos) {
|
|||
// MAIN METHODS //
|
||||
|
||||
function getDisplayAndWindowBounds() {
|
||||
const screenWidth = window.screen.width
|
||||
const screenHeight = window.screen.height
|
||||
const availWidth = window.screen.availWidth
|
||||
const availHeight = window.screen.availHeight
|
||||
|
||||
|
|
@ -373,11 +337,7 @@ function getDisplayAndWindowBounds() {
|
|||
}
|
||||
|
||||
function getPositions() {
|
||||
const size = localStorage.getItem('chart-size') ?? 'medium'
|
||||
const sizes = getSizes()
|
||||
const sizeStyles = sizes[size]
|
||||
const buttonHeight = +sizeStyles.height + 25
|
||||
const buttonWidth = +sizeStyles.width + 25
|
||||
const { buttonHeight, buttonWidth } = getButtonSize()
|
||||
|
||||
// Get button dimensions and viewport dimensions
|
||||
const widgetWidth = buttonWidth
|
||||
|
|
@ -394,8 +354,6 @@ function getPositions() {
|
|||
const buttonCenterY = widgetHeight / 2
|
||||
|
||||
// Calculate the translation offsets needed to center the button
|
||||
const offsetX = windowCenterX - buttonCenterX
|
||||
const offsetY = windowCenterY - buttonCenterY
|
||||
|
||||
// Define positions based on work area
|
||||
const positions = {
|
||||
|
|
@ -437,7 +395,7 @@ function getPositions() {
|
|||
}
|
||||
|
||||
function getCoordinates(isOutside) {
|
||||
var position = localStorage.getItem('perf-monitor-position') ?? 'bottom-right'
|
||||
var position = localStorage.getItem('perf-monitor-position')
|
||||
|
||||
if (isOutside) {
|
||||
var outsidePosition = getNearestPosition()
|
||||
|
|
@ -450,24 +408,18 @@ function getCoordinates(isOutside) {
|
|||
}
|
||||
|
||||
function getNearestPosition() {
|
||||
const size = localStorage.getItem('chart-size') ?? 'medium'
|
||||
const sizes = getSizes()
|
||||
const sizeStyles = sizes[size]
|
||||
const buttonHeight = +sizeStyles.height + 25
|
||||
const buttonWidth = +sizeStyles.width + 25
|
||||
const { buttonHeight, buttonWidth } = getButtonSize()
|
||||
const widget = document.getElementById('chart-button-container')
|
||||
const viewportWidth = window.innerWidth
|
||||
const viewportHeight = window.innerHeight
|
||||
// Get display bounds
|
||||
const { displayBounds, windowBounds } = getDisplayAndWindowBounds()
|
||||
const { displayBounds } = getDisplayAndWindowBounds()
|
||||
// Define positions based on work area
|
||||
const positions = getPositions()
|
||||
|
||||
// Get current window position
|
||||
const currentX = $(widget).offset().left
|
||||
let currentY = $(widget).offset().top
|
||||
const buttonCenterX = buttonWidth / 2
|
||||
const buttonCenterY = buttonHeight / 2
|
||||
const windowCenter = {
|
||||
x: $(widget).offset().left,
|
||||
y: $(widget).offset().top,
|
||||
|
|
@ -481,6 +433,7 @@ function getNearestPosition() {
|
|||
y: Math.abs(workAreaCenter.y - windowCenter.y),
|
||||
}
|
||||
var threshold = 100 // Define a threshold to determine proximity
|
||||
const size = localStorage.getItem('chart-size') ?? 'medium'
|
||||
switch (size) {
|
||||
case 'small':
|
||||
threshold = 250
|
||||
|
|
@ -494,6 +447,8 @@ function getNearestPosition() {
|
|||
break
|
||||
}
|
||||
|
||||
var nearestPosition = ''
|
||||
|
||||
if (distanceToCenter.x < threshold && distanceToCenter.y < threshold) {
|
||||
nearestPosition = 'center'
|
||||
} else {
|
||||
|
|
@ -532,8 +487,25 @@ function getNearestPosition() {
|
|||
|
||||
return pos
|
||||
}
|
||||
function getButtonSize() {
|
||||
const size = localStorage.getItem('chart-size') ?? 'medium'
|
||||
const sizes = getSizes()
|
||||
const sizeStyles = sizes[size]
|
||||
var buttonHeight = +sizeStyles.height + 25
|
||||
const buttonWidth = +sizeStyles.width + 25
|
||||
var table = document.getElementById('item-table')
|
||||
var totalRowCount = table.rows.length // 5
|
||||
var tableHeight = totalRowCount * 30
|
||||
if (size == 'large') {
|
||||
buttonHeight = +buttonHeight + tableHeight
|
||||
}
|
||||
|
||||
return { buttonHeight, buttonWidth }
|
||||
}
|
||||
async function updateChartSize() {
|
||||
var table = document.getElementById('item-table')
|
||||
table.style.display = 'none'
|
||||
|
||||
const settingsMenu = document.getElementById('settingsMenu')
|
||||
settingsMenu.classList.remove('show') // Hide the menu if visible
|
||||
$('#chart-wrapper').fadeOut()
|
||||
|
|
@ -555,15 +527,18 @@ async function updateChartSize() {
|
|||
chartContainer.classList.add(size)
|
||||
chartContainer.classList.add(savedChart)
|
||||
|
||||
const sizes = getSizes()
|
||||
const sizeStyles = sizes[size]
|
||||
const buttonHeight = +sizeStyles.height
|
||||
const buttonWidth = +sizeStyles.width
|
||||
const { buttonHeight, buttonWidth } = getButtonSize()
|
||||
const chartButtonContainer = document.getElementById('chart-button-container')
|
||||
|
||||
var actulaButtonHeight = 0
|
||||
var actualButtonWidth = 0
|
||||
|
||||
var viewportHeight = +buttonHeight
|
||||
var viewportWidth = +buttonWidth
|
||||
|
||||
$(chartButtonContainer).each(function () {
|
||||
this.style.setProperty('height', `${+buttonHeight + 25}px`, 'important')
|
||||
this.style.setProperty('width', `${+buttonWidth + 25}px`, 'important')
|
||||
this.style.setProperty('height', `${viewportHeight}px`, 'important')
|
||||
this.style.setProperty('width', `${viewportWidth}px`, 'important')
|
||||
})
|
||||
|
||||
var position = localStorage.getItem('perf-monitor-position')
|
||||
|
|
@ -593,11 +568,6 @@ async function updateChartSize() {
|
|||
}
|
||||
})
|
||||
|
||||
var actulaButtonHeight = 0
|
||||
|
||||
viewportHeight = +buttonHeight + 25
|
||||
viewportWidth = +buttonWidth + 25
|
||||
|
||||
switch (size) {
|
||||
case 'small':
|
||||
actulaButtonHeight = viewportHeight * 0.83
|
||||
|
|
@ -626,18 +596,24 @@ async function updateChartSize() {
|
|||
this.style.setProperty('background-color', ` #00000096`, 'important')
|
||||
}
|
||||
})
|
||||
var totalRowCount = table.rows.length // 5
|
||||
var tableHeight = totalRowCount * 30
|
||||
var workArea = actulaButtonHeight
|
||||
if (size == 'large') {
|
||||
workArea = viewportHeight - tableHeight
|
||||
}
|
||||
|
||||
const hasUpdates = localStorage.getItem('hasUpdates') ?? 'false'
|
||||
if (hasUpdates === 'true') {
|
||||
if (savedChart == 'bar') {
|
||||
$(chartContainer).each(function () {
|
||||
this.style.setProperty('height', `${actulaButtonHeight * 0.95}px`, 'important')
|
||||
this.style.setProperty('height', `${workArea * 0.95}px`, 'important')
|
||||
})
|
||||
|
||||
initializeBarChart()
|
||||
} else {
|
||||
$(chartContainer).each(function () {
|
||||
this.style.setProperty('height', `${actulaButtonHeight * 0.87}px`, 'important')
|
||||
this.style.setProperty('height', `${workArea * 0.87}px`, 'important')
|
||||
})
|
||||
setTimeout(() => {
|
||||
initializeLineChart()
|
||||
|
|
@ -646,10 +622,11 @@ async function updateChartSize() {
|
|||
} else {
|
||||
$('#chart-wrapper').fadeIn()
|
||||
}
|
||||
|
||||
localStorage.setItem('hasUpdates', 'false')
|
||||
|
||||
const active = `#chart-button.top-left.active`
|
||||
positionStyles = {
|
||||
var positionStyles = {
|
||||
bottom: bottom,
|
||||
right: right,
|
||||
}
|
||||
|
|
@ -663,9 +640,13 @@ async function updateChartSize() {
|
|||
|
||||
const pos = getCoordinates(false)
|
||||
goToPosition(pos)
|
||||
|
||||
var shouldShowPerfMonitor = false
|
||||
var appIsLoaded = false
|
||||
var shouldShowPerfMonitor = false
|
||||
if (JSON.parse(localStorage.getItem('shouldShowPerfMonitor')) ?? false) {
|
||||
setTimeout(() => {
|
||||
// showPerfMonitor()
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
window.showPerfMonitor = async function () {
|
||||
shouldShowPerfMonitor = !shouldShowPerfMonitor
|
||||
|
|
@ -673,21 +654,25 @@ window.showPerfMonitor = async function () {
|
|||
const chartButton = document.getElementById('chart-button')
|
||||
const chartWrapper = document.getElementById('chart-wrapper')
|
||||
const chartButtonContainer = document.getElementById('chart-button-container')
|
||||
const resourceMonitorLink = document.getElementById('show_resource_monitor')
|
||||
|
||||
if (shouldShowPerfMonitor === true) {
|
||||
$(chartButtonContainer).toggleClass('active')
|
||||
|
||||
localStorage.setItem('hasUpdates', 'true')
|
||||
startInterval()
|
||||
await updateChartSize()
|
||||
$(resourceMonitorLink).fadeOut(500)
|
||||
appIsLoaded = true
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
stopInterval()
|
||||
$(chartButtonContainer).toggleClass('active')
|
||||
$(chartButtonContainer).each(function () {
|
||||
this.style.setProperty('height', `0px`, 'important')
|
||||
})
|
||||
}, 500)
|
||||
chartButton.classList.remove('small', 'medium', 'large')
|
||||
$(chartWrapper).fadeOut()
|
||||
$(resourceMonitorLink).fadeIn(500)
|
||||
}
|
||||
|
||||
$(chartButton).toggleClass('active')
|
||||
|
|
@ -701,7 +686,4 @@ document.getElementById('close-button').addEventListener('click', function () {
|
|||
row.classList.add('drag')
|
||||
})
|
||||
showPerfMonitor()
|
||||
setTimeout(() => {
|
||||
window.electron.ipcRenderer.send('close-window')
|
||||
}, 500)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,115 @@
|
|||
export function getResolver(timeout = 5000) {
|
||||
const resolver = {}
|
||||
resolver.id = generateId(8)
|
||||
resolver.completed = false
|
||||
resolver.resolved = false
|
||||
resolver.rejected = false
|
||||
resolver.promise = new Promise((resolve, reject) => {
|
||||
resolver.reject = () => {
|
||||
resolver.completed = true
|
||||
resolver.rejected = true
|
||||
reject()
|
||||
}
|
||||
resolver.resolve = (data) => {
|
||||
resolver.completed = true
|
||||
resolver.resolved = true
|
||||
resolve(data)
|
||||
}
|
||||
})
|
||||
resolver.timeout = setTimeout(() => {
|
||||
if (!resolver.completed) {
|
||||
resolver.reject()
|
||||
}
|
||||
}, timeout)
|
||||
return resolver
|
||||
}
|
||||
const DEBOUNCE_FN_TO_PROMISE = new WeakMap()
|
||||
export function debounce(fn, ms = 64) {
|
||||
if (!DEBOUNCE_FN_TO_PROMISE.get(fn)) {
|
||||
DEBOUNCE_FN_TO_PROMISE.set(
|
||||
fn,
|
||||
wait(ms).then(() => {
|
||||
DEBOUNCE_FN_TO_PROMISE.delete(fn)
|
||||
fn()
|
||||
}),
|
||||
)
|
||||
}
|
||||
return DEBOUNCE_FN_TO_PROMISE.get(fn)
|
||||
}
|
||||
export function wait(ms = 16) {
|
||||
if (ms === 16) {
|
||||
return new Promise((resolve) => {
|
||||
requestAnimationFrame(() => {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve()
|
||||
}, ms)
|
||||
})
|
||||
}
|
||||
function dec2hex(dec) {
|
||||
return dec.toString(16).padStart(2, '0')
|
||||
}
|
||||
export function generateId(length) {
|
||||
const arr = new Uint8Array(length / 2)
|
||||
crypto.getRandomValues(arr)
|
||||
return Array.from(arr, dec2hex).join('')
|
||||
}
|
||||
export function getObjectValue(obj, objKey, def) {
|
||||
if (!obj || !objKey) return def
|
||||
const keys = objKey.split('.')
|
||||
const key = keys.shift()
|
||||
const found = obj[key]
|
||||
if (keys.length) {
|
||||
return getObjectValue(found, keys.join('.'), def)
|
||||
}
|
||||
return found
|
||||
}
|
||||
export function setObjectValue(obj, objKey, value, createMissingObjects = true) {
|
||||
if (!obj || !objKey) return obj
|
||||
const keys = objKey.split('.')
|
||||
const key = keys.shift()
|
||||
if (obj[key] === undefined) {
|
||||
if (!createMissingObjects) {
|
||||
return
|
||||
}
|
||||
obj[key] = {}
|
||||
}
|
||||
if (!keys.length) {
|
||||
obj[key] = value
|
||||
} else {
|
||||
if (typeof obj[key] != 'object') {
|
||||
obj[key] = {}
|
||||
}
|
||||
setObjectValue(obj[key], keys.join('.'), value, createMissingObjects)
|
||||
}
|
||||
return obj
|
||||
}
|
||||
export function moveArrayItem(arr, itemOrFrom, to) {
|
||||
const from = typeof itemOrFrom === 'number' ? itemOrFrom : arr.indexOf(itemOrFrom)
|
||||
arr.splice(to, 0, arr.splice(from, 1)[0])
|
||||
}
|
||||
export function removeArrayItem(arr, itemOrIndex) {
|
||||
const index = typeof itemOrIndex === 'number' ? itemOrIndex : arr.indexOf(itemOrIndex)
|
||||
arr.splice(index, 1)
|
||||
}
|
||||
export function injectCss(href) {
|
||||
if (document.querySelector(`link[href^="${href}"]`)) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
const link = document.createElement('link')
|
||||
link.setAttribute('rel', 'stylesheet')
|
||||
link.setAttribute('type', 'text/css')
|
||||
const timeout = setTimeout(resolve, 1000)
|
||||
link.addEventListener('load', (e) => {
|
||||
clearInterval(timeout)
|
||||
resolve()
|
||||
})
|
||||
link.href = href
|
||||
document.head.appendChild(link)
|
||||
})
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,61 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>System Monitor</title>
|
||||
<script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 20px;
|
||||
}
|
||||
.data-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.data-item {
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Real-Time System Monitor</h1>
|
||||
<div class="data-container" id="data-container">
|
||||
<div class="data-item" id="cpu">CPU: Loading...</div>
|
||||
<div class="data-item" id="ram">RAM: Loading...</div>
|
||||
<div class="data-item" id="gpu">GPU: Loading...</div>
|
||||
<div class="data-item" id="vram">VRAM: Loading...</div>
|
||||
<div class="data-item" id="hdd">HDD: Loading...</div>
|
||||
<div class="data-item" id="temp">Temperature: Loading...</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const socket = io('http://localhost:5000')
|
||||
|
||||
// Listen for 'data_update' event and update the data on the page
|
||||
socket.on('data_update', (data) => {
|
||||
document.getElementById('cpu').textContent = `CPU: ${data.cpu.toFixed(2)}%`
|
||||
document.getElementById('ram').textContent = `RAM: ${data.ram.toFixed(2)}%`
|
||||
document.getElementById('gpu').textContent = `GPU: ${data.gpu.toFixed(2)}%`
|
||||
document.getElementById('vram').textContent = `VRAM: ${data.vram.toFixed(2)}%`
|
||||
document.getElementById('hdd').textContent = `HDD: ${data.hdd.toFixed(2)}%`
|
||||
document.getElementById('temp').textContent = `Temperature: ${data.temp.toFixed(2)}°C`
|
||||
})
|
||||
|
||||
socket.on('connect', () => {
|
||||
console.log('Connected to the server')
|
||||
})
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('Disconnected from the server')
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,75 +1,6 @@
|
|||
<div id="chart-button-container" draggable="true" ondragstart="onDragStart(event);">
|
||||
<div id="chart-button" class="">
|
||||
<div class="chart-row">
|
||||
<div class="left-col">
|
||||
<i class="material-icons" id="popupTrigger">settings</i>
|
||||
<div id="settingsMenu" class="settings-menu">
|
||||
<div class="settings-row">
|
||||
<div class="settings-col">Settings</div>
|
||||
</div>
|
||||
<hr id="settings-hr" class="settings-hr" />
|
||||
|
||||
<div class="settings-row">
|
||||
<div class="settings-row">
|
||||
<div class="settings-col">Layout:</div>
|
||||
<div class="settings-col">
|
||||
<a onclick="barChart()"> 1 </a> |
|
||||
<a onclick="lineChart()"> 2 </a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<div class="settings-col">Size:</div>
|
||||
<div class="settings-col">
|
||||
<a onclick="smallChart()"> S </a> |
|
||||
<a onclick="mediumChart()"> M </a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-row">
|
||||
<div class="settings-col">Position</div>
|
||||
<div id="positionMenu" class="position-menu">
|
||||
<button class="position-btn position-clickable" id="top-left">
|
||||
<i class="material-icons">north_west</i>
|
||||
</button>
|
||||
<button class="position-btn position-clickable" id="top-center">
|
||||
<i class="material-icons">north</i>
|
||||
</button>
|
||||
<button class="position-btn position-clickable" id="top-right">
|
||||
<i class="material-icons">north_east</i>
|
||||
</button>
|
||||
<button class="position-btn position-clickable" id="left-center">
|
||||
<i class="material-icons">west</i>
|
||||
</button>
|
||||
<button class="position-btn" id="center" onclick="largeChart()">
|
||||
<i class="material-icons">radio_button_checked</i>
|
||||
</button>
|
||||
<button class="position-btn position-clickable" id="right-center">
|
||||
<i class="material-icons">east</i>
|
||||
</button>
|
||||
<button class="position-btn position-clickable" id="bottom-left">
|
||||
<i class="material-icons">south_west</i>
|
||||
</button>
|
||||
<button class="position-btn position-clickable" id="bottom-center">
|
||||
<i class="material-icons">south</i>
|
||||
</button>
|
||||
<button class="position-btn position-clickable" id="bottom-right">
|
||||
<i class="material-icons">south_east</i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-col">
|
||||
<i class="material-icons" id="close-button">close</i>
|
||||
</div>
|
||||
</div>
|
||||
<div id="chart-wrapper">
|
||||
<div id="chart-container">
|
||||
<canvas id="usage-chart" style="width: 100%; height: 100%"></canvas>
|
||||
</div>
|
||||
<div id="custom-legend"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<body>
|
||||
<elegant-resource-monitor></elegant-resource-monitor>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Reference in New Issue