feat: Enhance dashboard layout with responsive stats bar and chart integration
This commit is contained in:
parent
7d6e6e9c42
commit
9dbcf7885c
8 changed files with 194 additions and 199 deletions
|
|
@ -66,7 +66,7 @@ const docTemplate = `{
|
|||
},
|
||||
"/miners": {
|
||||
"get": {
|
||||
"description": "Get a list of all running miners",
|
||||
"description": "Get a list of all running miners, including their full stats.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
|
|
@ -79,9 +79,7 @@ const docTemplate = `{
|
|||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/mining.XMRigMiner"
|
||||
}
|
||||
"items": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@
|
|||
},
|
||||
"/miners": {
|
||||
"get": {
|
||||
"description": "Get a list of all running miners",
|
||||
"description": "Get a list of all running miners, including their full stats.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
|
|
@ -73,9 +73,7 @@
|
|||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/mining.XMRigMiner"
|
||||
}
|
||||
"items": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -318,15 +318,14 @@ paths:
|
|||
- system
|
||||
/miners:
|
||||
get:
|
||||
description: Get a list of all running miners
|
||||
description: Get a list of all running miners, including their full stats.
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/mining.XMRigMiner'
|
||||
items: {}
|
||||
type: array
|
||||
summary: List all running miners
|
||||
tags:
|
||||
|
|
|
|||
|
|
@ -1,74 +1,56 @@
|
|||
.admin-panel {
|
||||
padding: 1rem;
|
||||
/* Set up the container */
|
||||
.dashboard-view {
|
||||
container-type: inline-size;
|
||||
container-name: dashboard;
|
||||
height: 100%;
|
||||
display: flex; /* Ensure it fills height */
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.admin-title {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.miner-list, .path-list {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.miner-item, .path-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.path-list ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.path-list li {
|
||||
background-color: #f5f5f5;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 0.5rem;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.dashboard-summary {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.miner-summary-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.miner-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.start-buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.start-options {
|
||||
padding: 1rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
margin-top: 0.5rem;
|
||||
.dashboard-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
height: 100%;
|
||||
flex-grow: 1; /* Allow content to grow */
|
||||
}
|
||||
|
||||
.dashboard-charts {
|
||||
padding: 1rem;
|
||||
/* Default (mobile/narrow) layout */
|
||||
.stats-list-container {
|
||||
display: none; /* Hide the detailed list by default */
|
||||
}
|
||||
|
||||
.miner-chart-item {
|
||||
margin-bottom: 1.5rem;
|
||||
.stats-bar-container {
|
||||
display: block; /* Show the bar by default */
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
flex-grow: 1;
|
||||
min-height: 200px; /* Ensure chart has a minimum height */
|
||||
}
|
||||
|
||||
/* Wide layout using container queries */
|
||||
@container dashboard (min-width: 768px) {
|
||||
.dashboard-content {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr; /* 2/3 for chart, 1/3 for stats */
|
||||
gap: 1.5rem;
|
||||
align-items: stretch; /* Stretch items to fill height */
|
||||
}
|
||||
|
||||
.stats-bar-container {
|
||||
display: none; /* Hide the bar in wide view */
|
||||
}
|
||||
|
||||
.stats-list-container {
|
||||
display: block; /* Show the detailed list in wide view */
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.centered-container {
|
||||
|
|
@ -78,10 +60,7 @@
|
|||
justify-content: center;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.button-spinner {
|
||||
font-size: 1rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.card-error {
|
||||
|
|
|
|||
|
|
@ -9,11 +9,22 @@
|
|||
</wa-card>
|
||||
}
|
||||
|
||||
<!-- Consolidated Chart for All Running Miners -->
|
||||
@if (state().runningMiners.length > 0) {
|
||||
<div class="dashboard-charts">
|
||||
<snider-mining-stats-bar [stats]="state().runningMiners[0]?.full_stats"></snider-mining-stats-bar>
|
||||
<snider-mining-chart></snider-mining-chart>
|
||||
<div class="dashboard-content">
|
||||
<!-- Stats Bar for small containers -->
|
||||
<div class="stats-bar-container">
|
||||
<snider-mining-stats-bar [stats]="state().runningMiners[0]?.full_stats" mode="bar"></snider-mining-stats-bar>
|
||||
</div>
|
||||
|
||||
<!-- Chart -->
|
||||
<div class="chart-container">
|
||||
<snider-mining-chart></snider-mining-chart>
|
||||
</div>
|
||||
|
||||
<!-- Stats List for large containers -->
|
||||
<div class="stats-list-container">
|
||||
<snider-mining-stats-bar [stats]="state().runningMiners[0]?.full_stats" mode="list"></snider-mining-stats-bar>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="centered-container">
|
||||
|
|
|
|||
|
|
@ -4,9 +4,6 @@ import { FormsModule } from '@angular/forms';
|
|||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { MinerService } from './miner.service';
|
||||
import { ChartComponent } from './chart.component';
|
||||
import { ProfileListComponent } from './profile-list.component';
|
||||
import { ProfileCreateComponent } from './profile-create.component';
|
||||
import { StatsBarComponent } from './stats-bar.component';
|
||||
|
||||
// Import Web Awesome components
|
||||
import "@awesome.me/webawesome/dist/webawesome.js";
|
||||
|
|
@ -17,84 +14,18 @@ import '@awesome.me/webawesome/dist/components/icon/icon.js';
|
|||
import '@awesome.me/webawesome/dist/components/spinner/spinner.js';
|
||||
import '@awesome.me/webawesome/dist/components/input/input.js';
|
||||
import '@awesome.me/webawesome/dist/components/select/select.js';
|
||||
import {StatsBarComponent} from './stats-bar.component';
|
||||
|
||||
@Component({
|
||||
selector: 'snider-mining-dashboard',
|
||||
standalone: true,
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
imports: [CommonModule, FormsModule, ChartComponent, ProfileListComponent, ProfileCreateComponent, StatsBarComponent],
|
||||
imports: [CommonModule, FormsModule, ChartComponent, StatsBarComponent], // Add to imports
|
||||
templateUrl: './dashboard.component.html',
|
||||
styleUrls: ['./dashboard.component.css']
|
||||
})
|
||||
export class MiningDashboardComponent {
|
||||
minerService = inject(MinerService);
|
||||
state = this.minerService.state;
|
||||
|
||||
actionInProgress = signal<string | null>(null);
|
||||
error = signal<string | null>(null);
|
||||
|
||||
showProfileManager = signal(false);
|
||||
// Use a map to track the selected profile for each miner type
|
||||
selectedProfileIds = signal<Map<string, string>>(new Map());
|
||||
|
||||
handleProfileSelection(minerType: string, event: Event) {
|
||||
const selectedValue = (event.target as HTMLSelectElement).value;
|
||||
this.selectedProfileIds.update(m => m.set(minerType, selectedValue));
|
||||
}
|
||||
|
||||
private handleError(err: HttpErrorResponse, defaultMessage: string) {
|
||||
console.error(err);
|
||||
this.actionInProgress.set(null);
|
||||
if (err.error && err.error.error) {
|
||||
this.error.set(`${defaultMessage}: ${err.error.error}`);
|
||||
} else if (typeof err.error === 'string' && err.error.length < 200) {
|
||||
this.error.set(`${defaultMessage}: ${err.error}`);
|
||||
} else {
|
||||
this.error.set(`${defaultMessage}. Please check the console for details.`);
|
||||
}
|
||||
}
|
||||
|
||||
startMiner(minerType: string): void {
|
||||
const profileId = this.selectedProfileIds().get(minerType);
|
||||
if (!profileId) {
|
||||
this.error.set('Please select a profile to start.');
|
||||
return;
|
||||
}
|
||||
this.actionInProgress.set(`start-${profileId}`);
|
||||
this.error.set(null);
|
||||
this.minerService.startMiner(profileId).subscribe({
|
||||
next: () => { this.actionInProgress.set(null); },
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this.handleError(err, `Failed to start miner for profile ${profileId}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
stopMiner(miner: any): void {
|
||||
const runningInstance = this.getRunningMinerInstance(miner);
|
||||
if (!runningInstance) {
|
||||
this.error.set("Cannot stop a miner that is not running.");
|
||||
return;
|
||||
}
|
||||
this.actionInProgress.set(`stop-${miner.type}`);
|
||||
this.error.set(null);
|
||||
this.minerService.stopMiner(runningInstance.name).subscribe({
|
||||
next: () => { this.actionInProgress.set(null); },
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this.handleError(err, `Failed to stop ${runningInstance.name}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getRunningMinerInstance(miner: any): any {
|
||||
return this.state().runningMiners.find((m: any) => m.name.startsWith(miner.type));
|
||||
}
|
||||
|
||||
isMinerRunning(miner: any): boolean {
|
||||
return !!this.getRunningMinerInstance(miner);
|
||||
}
|
||||
|
||||
toggleProfileManager() {
|
||||
this.showProfileManager.set(!this.showProfileManager());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,44 +8,112 @@ import { CommonModule } from '@angular/common';
|
|||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
template: `
|
||||
@if(stats) {
|
||||
<div class="stats-bar">
|
||||
<div class="stat-item">
|
||||
<span class="label">Hashrate:</span>
|
||||
<span class="value">{{ stats.hashrate?.total[0] | number:'1.0-2' }} H/s</span>
|
||||
@if (mode === 'list') {
|
||||
<div class="stats-container list-mode">
|
||||
<dl class="stats-dl">
|
||||
<!-- General -->
|
||||
<dt>Algorithm</dt><dd>{{ stats.algo }}</dd>
|
||||
<dt>Uptime</dt><dd>{{ stats.uptime }}s</dd>
|
||||
<dt>Version</dt><dd>{{ stats.version }}</dd>
|
||||
|
||||
<!-- Hashrate -->
|
||||
<dt>Hashrate (10s)</dt><dd>{{ stats.hashrate?.total[0] | number:'1.0-2' }} H/s</dd>
|
||||
<dt>Hashrate (60s)</dt><dd>{{ stats.hashrate?.total[1] | number:'1.0-2' }} H/s</dd>
|
||||
<dt>Hashrate (15m)</dt><dd>{{ stats.hashrate?.total[2] | number:'1.0-2' }} H/s</dd>
|
||||
<dt>Highest Hashrate</dt><dd>{{ stats.hashrate?.highest | number:'1.0-2' }} H/s</dd>
|
||||
|
||||
<!-- Results -->
|
||||
<dt>Good Shares</dt><dd>{{ stats.results?.shares_good }}</dd>
|
||||
<dt>Total Shares</dt><dd>{{ stats.results?.shares_total }}</dd>
|
||||
<dt>Avg. Time</dt><dd>{{ stats.results?.avg_time }}s</dd>
|
||||
<dt>Total Hashes</dt><dd>{{ stats.results?.hashes_total | number }}</dd>
|
||||
|
||||
<!-- Connection -->
|
||||
<dt>Pool</dt><dd>{{ stats.connection?.pool }}</dd>
|
||||
<dt>Pool Uptime</dt><dd>{{ stats.connection?.uptime }}s</dd>
|
||||
<dt>Pool Ping</dt><dd>{{ stats.connection?.ping }}ms</dd>
|
||||
<dt>Current Difficulty</dt><dd>{{ stats.connection?.diff | number }}</dd>
|
||||
<dt>Accepted Shares</dt><dd>{{ stats.connection?.accepted }}</dd>
|
||||
<dt>Rejected Shares</dt><dd>{{ stats.connection?.rejected }}</dd>
|
||||
|
||||
<!-- CPU -->
|
||||
<dt>CPU Brand</dt><dd>{{ stats.cpu?.brand }}</dd>
|
||||
<dt>CPU Cores/Threads</dt><dd>{{ stats.cpu?.cores }} / {{ stats.cpu?.threads }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="label">Algorithm:</span>
|
||||
<span class="value">{{ stats.algo }}</span>
|
||||
} @else {
|
||||
<div class="stats-bar bar-mode">
|
||||
<div class="stat-item">
|
||||
<span class="label">Hashrate:</span>
|
||||
<span class="value">{{ stats.hashrate?.total[0] | number:'1.0-2' }} H/s</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="label">Algorithm:</span>
|
||||
<span class="value">{{ stats.algo }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="label">Difficulty:</span>
|
||||
<span class="value">{{ stats.connection?.diff | number }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="label">Accepted:</span>
|
||||
<span class="value">{{ stats.results?.shares_good }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="label">Rejected:</span>
|
||||
<span class="value">{{ stats.connection?.rejected }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="label">Avg Time:</span>
|
||||
<span class="value">{{ stats.results?.avg_time | number }}s</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="label">Uptime:</span>
|
||||
<span class="value">{{ stats.uptime | number }}s</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="label">Pool Uptime:</span>
|
||||
<span class="value">{{ stats.connection?.uptime | number }}s</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="label">Difficulty:</span>
|
||||
<span class="value">{{ stats.connection?.diff | number }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="label">Accepted:</span>
|
||||
<span class="value">{{ stats.results?.shares_good }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="label">Rejected:</span>
|
||||
<span class="value">{{ stats.connection?.rejected }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="label">Avg Time:</span>
|
||||
<span class="value">{{ stats.results?.avg_time | number }}s</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="label">Uptime:</span>
|
||||
<span class="value">{{ stats.uptime | number }}s</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="label">Pool Uptime:</span>
|
||||
<span class="value">{{ stats.connection?.uptime | number }}s</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
`,
|
||||
styles: [`
|
||||
.stats-bar {
|
||||
/* List Mode Styles */
|
||||
.stats-container.list-mode {
|
||||
height: 100%;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
padding-right: 1rem;
|
||||
background-color: #f9f9f9;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.list-mode .stats-dl {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 0.5rem 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
.list-mode dt {
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
grid-column: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.list-mode dd {
|
||||
margin: 0;
|
||||
grid-column: 2;
|
||||
text-align: right;
|
||||
font-family: monospace;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Bar Mode Styles */
|
||||
.stats-bar.bar-mode {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0.5rem;
|
||||
|
|
@ -55,7 +123,7 @@ import { CommonModule } from '@angular/common';
|
|||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.stat-item {
|
||||
.bar-mode .stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
|
@ -64,12 +132,12 @@ import { CommonModule } from '@angular/common';
|
|||
border-radius: 6px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
}
|
||||
.label {
|
||||
.bar-mode .label {
|
||||
font-size: 0.8rem;
|
||||
color: #666;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.value {
|
||||
.bar-mode .value {
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
|
@ -77,4 +145,5 @@ import { CommonModule } from '@angular/common';
|
|||
})
|
||||
export class StatsBarComponent {
|
||||
@Input() stats: any;
|
||||
@Input() mode: 'bar' | 'list' = 'bar';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="h-full">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Snider Mining</title>
|
||||
|
|
@ -8,10 +8,10 @@
|
|||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body class="bg-gray-100 text-gray-800">
|
||||
<body class="bg-gray-100 text-gray-800 h-full">
|
||||
<div class="flex flex-col h-screen">
|
||||
<!-- Top Bar -->
|
||||
<header class="flex items-center justify-between h-16 px-6 bg-white border-b">
|
||||
<header class="flex items-center justify-between h-16 px-6 bg-white border-b flex-shrink-0">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">Mining Dashboard (Dev View)</h2>
|
||||
</div>
|
||||
|
|
@ -22,10 +22,23 @@
|
|||
|
||||
<!-- Main Content Area -->
|
||||
<main class="flex-1 p-6 overflow-y-auto">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div class="bg-white shadow-md rounded-lg p-4">
|
||||
<snider-mining-dashboard></snider-mining-dashboard>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 h-full">
|
||||
<!-- Wide Dashboard Instance -->
|
||||
<div class="bg-white shadow-md rounded-lg p-4 lg:col-span-2 flex flex-col">
|
||||
<h3 class="font-bold mb-2 flex-shrink-0">Wide Dashboard</h3>
|
||||
<div class="flex-grow">
|
||||
<snider-mining-dashboard></snider-mining-dashboard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Standard Dashboard Instance -->
|
||||
<div class="bg-white shadow-md rounded-lg p-4 flex flex-col">
|
||||
<h3 class="font-bold mb-2 flex-shrink-0">Standard Dashboard</h3>
|
||||
<div class="flex-grow">
|
||||
<snider-mining-dashboard></snider-mining-dashboard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow-md rounded-lg p-4">
|
||||
<snider-mining-admin></snider-mining-admin>
|
||||
</div>
|
||||
|
|
@ -35,9 +48,6 @@
|
|||
<div class="bg-white shadow-md rounded-lg p-4">
|
||||
<snider-mining-setup-wizard></snider-mining-setup-wizard>
|
||||
</div>
|
||||
<div class="bg-white shadow-md rounded-lg p-4">
|
||||
<snider-mining-chart></snider-mining-chart>
|
||||
</div>
|
||||
<div class="bg-white shadow-md rounded-lg p-4">
|
||||
<snider-mining-profile-list></snider-mining-profile-list>
|
||||
</div>
|
||||
|
|
@ -46,7 +56,7 @@
|
|||
</main>
|
||||
|
||||
<!-- Sticky Footer -->
|
||||
<footer class="h-12 px-6 bg-white border-t flex items-center">
|
||||
<footer class="h-12 px-6 bg-white border-t flex items-center flex-shrink-0">
|
||||
<p class="text-sm text-gray-600">© 2025 Snider. All rights reserved.</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue