integrates Highcharts for live hashrate visualization and updates dashboard layout

This commit is contained in:
Snider 2025-11-10 02:22:37 +00:00
parent 0d2dc1ddd8
commit 27834c332c
10 changed files with 249 additions and 148 deletions

View file

@ -27,8 +27,7 @@
}
],
"styles": [
"src/styles.css",
"node_modules/@awesome.me/webawesome/dist/styles/themes/awesome.css"
"src/styles.css"
],
"scripts": [],
"outputHashing": "none",

23
ui/package-lock.json generated
View file

@ -17,6 +17,8 @@
"@angular/platform-browser": "^20.3.0",
"@angular/router": "^20.3.0",
"@awesome.me/webawesome": "^3.0.0",
"highcharts": "^12.4.0",
"highcharts-angular": "^5.2.0",
"install": "^0.13.0",
"npm": "^11.6.2",
"rxjs": "~7.8.0",
@ -8099,6 +8101,27 @@
"node": ">= 0.4"
}
},
"node_modules/highcharts": {
"version": "12.4.0",
"resolved": "https://registry.npmjs.org/highcharts/-/highcharts-12.4.0.tgz",
"integrity": "sha512-o6UxxfChSUrvrZUbWrAuqL1HO/+exhAUPcZY6nnqLsadZQlnP16d082sg7DnXKZCk1gtfkyfkp6g3qkIZ9miZg==",
"license": "https://www.highcharts.com/license",
"peer": true
},
"node_modules/highcharts-angular": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/highcharts-angular/-/highcharts-angular-5.2.0.tgz",
"integrity": "sha512-ba7Q0d3F0SDNWqGSzbe+Khi2FF7wt0kY3hwocAWpaaKSKF3EHajUqOSqs9LY+nXXOOz8SCRtSbgaSf0cLdx1xA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"@angular/common": ">=19.0.0",
"@angular/core": ">=19.0.0",
"highcharts": ">=12.2.0"
}
},
"node_modules/hosted-git-info": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz",

View file

@ -31,6 +31,8 @@
"@angular/platform-browser": "^20.3.0",
"@angular/router": "^20.3.0",
"@awesome.me/webawesome": "^3.0.0",
"highcharts": "^12.4.0",
"highcharts-angular": "^5.2.0",
"install": "^0.13.0",
"npm": "^11.6.2",
"rxjs": "~7.8.0",

View file

@ -1,5 +1,6 @@
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHighcharts } from 'highcharts-angular';
import { routes } from './app.routes';
@ -7,6 +8,31 @@ export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes)
provideRouter(routes),
provideHighcharts({
// Optional: Define the Highcharts instance dynamically
instance: () => import('highcharts'),
// Global chart options applied across all charts
options: {
title: {
style: {
color: 'tomato',
},
},
legend: {
enabled: false,
},
},
// Include Highcharts additional modules (e.g., exporting, accessibility) or custom themes
modules: () => {
return [
import('highcharts/esm/modules/accessibility'),
import('highcharts/esm/modules/exporting'),
import('highcharts/esm/themes/sunset'),
];
},
}),
]
};

View file

@ -0,0 +1,79 @@
:host {
display: block;
font-family: sans-serif;
}
.card {
max-width: 800px;
margin: 20px auto;
}
.header-container {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.header-title {
margin: 0;
font-size: 1.25rem;
flex-grow: 1;
}
.title-quiet {
color: var(--wa-color-text-quiet);
}
.header-stats {
display: flex;
gap: 1rem;
align-items: center;
}
.stat-item {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
}
.chart {
width: 100%;
height: 400px;
display: block;
}
.details-section {
padding: 1rem;
background-color: var(--wa-color-neutral-50);
border-radius: var(--wa-border-radius-medium);
}
.history-list {
list-style: none;
padding: 0;
}
.card-footer {
display: flex;
justify-content: flex-end;
}
.error-message {
color: var(--wa-color-danger-600);
}
/* Responsive stacking for smaller screens */
@media (max-width: 600px) {
.header-container {
flex-direction: column;
align-items: flex-start;
}
.header-stats {
width: 100%;
justify-content: space-between;
}
}

View file

@ -0,0 +1,51 @@
<wa-card class="card">
<div slot="header" class="header-container">
<h3 class="header-title">
<span class="title-quiet">Mining Dashboard:</span> {{ minerName }}
</h3>
<div *ngIf="!loading && !error" class="header-stats">
<div class="stat-item">
<wa-icon name="speedometer"></wa-icon>
<span>{{ currentHashrate.toFixed(2) }} H/s</span>
</div>
<div class="stat-item">
<wa-icon name="clock"></wa-icon>
<span>{{ lastUpdated | date:'mediumTime' }}</span>
</div>
</div>
<wa-button appearance="plain" slot="header-actions" size="small" (click)="toggleDetails()">
<wa-icon name="cog" label="Toggle Details"></wa-icon>
</wa-button>
</div>
<div class="wa-stack wa-gap-sm">
<p *ngIf="loading">Loading hashrate data...</p>
<p *ngIf="error" class="error-message">Error: {{ error }}</p>
<div *ngIf="!loading && !error">
<highcharts-chart
[options]="chartOptions"
[(update)]="updateFlag"
class="chart"
></highcharts-chart>
</div>
<div *ngIf="showDetails" class="details-section">
<h4>Hashrate History ({{ hashrateHistory.length }} points)</h4>
<ul class="history-list">
<li *ngFor="let point of hashrateHistory | slice:0:5">
{{ point.timestamp | date:'mediumTime' }}: {{ point.hashrate }} H/s
</li>
<li *ngIf="hashrateHistory.length > 5">...</li>
</ul>
<h5>Raw History Data:</h5>
<pre>{{ hashrateHistory | json }}</pre>
</div>
</div>
<wa-divider></wa-divider>
<div slot="footer" class="card-footer">
<wa-button variant="neutral" (click)="toggleDetails()">{{ showDetails ? 'Hide Details' : 'Show Details' }}</wa-button>
</div>
</wa-card>

View file

@ -11,12 +11,15 @@ import { HttpClient, HttpClientModule } from '@angular/common/http';
import { CommonModule } from '@angular/common';
import { interval, Subscription } from 'rxjs';
import { switchMap, startWith } from 'rxjs/operators';
import "@awesome.me/webawesome/dist/webawesome.js";
import * as Highcharts from 'highcharts';
import { HighchartsChartComponent } from 'highcharts-angular';
// Import Shoelace components
import "@awesome.me/webawesome/dist/webawesome.js";
import '@awesome.me/webawesome/dist/components/card/card.js';
import '@awesome.me/webawesome/dist/components/button/button.js';
import '@awesome.me/webawesome/dist/components/tooltip/tooltip.js';
import '@awesome.me/webawesome/dist/components/icon/icon.js';
// Add other Shoelace components as needed for your UI
interface HashratePoint {
timestamp: string; // ISO string
@ -24,118 +27,17 @@ interface HashratePoint {
}
@Component({
selector: 'mde-mining-dashboard', // This will be your custom element tag
selector: 'mde-mining-dashboard',
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
imports: [CommonModule, HttpClientModule], // HttpClientModule is needed for HttpClient
template: `
<wa-card class="mining-dashboard-card">
<div slot="header" class="card-header">
<wa-icon name="cpu"></wa-icon>
<h3>Mining Dashboard: {{ minerName }}</h3>
</div>
<p *ngIf="loading">Loading hashrate data...</p>
<p *ngIf="error" class="error-message">Error: {{ error }}</p>
<div *ngIf="!loading && !error">
<p>Current Hashrate: <strong>{{ currentHashrate }} H/s</strong></p>
<p>Last Updated: {{ lastUpdated | date:'mediumTime' }}</p>
<h4>Hashrate History ({{ hashrateHistory.length }} points)</h4>
<ul class="history-list">
<li *ngFor="let point of hashrateHistory | slice:0:5">
{{ point.timestamp | date:'mediumTime' }}: {{ point.hashrate }} H/s
</li>
<li *ngIf="hashrateHistory.length > 5">...</li>
</ul>
<!-- Here you would integrate a charting library -->
<!-- <canvas #hashrateChart></canvas> -->
</div>
<div slot="footer" class="card-footer">
<wa-button variant="brand" (click)="fetchHashrate()">Refresh</wa-button>
<wa-button variant="neutral" (click)="toggleDetails()">{{ showDetails ? 'Hide Details' : 'Show Details' }}</wa-button>
</div>
<div *ngIf="showDetails" class="details-section">
<h5>Raw History Data:</h5>
<pre>{{ hashrateHistory | json }}</pre>
</div>
</wa-card>
`,
styles: [`
.mining-dashboard-card {
width: 100%;
max-width: 500px;
margin: 20px auto;
border: 1px solid var(--wa-color-neutral-300);
border-radius: var(--wa-border-radius-medium);
box-shadow: var(--wa-shadow-medium);
}
.card-header {
display: flex;
align-items: center;
gap: 10px;
padding: var(--wa-spacing-medium);
border-bottom: 1px solid var(--wa-color-neutral-200);
}
.card-header h3 {
margin: 0;
font-size: var(--wa-font-size-large);
}
.card-header sl-icon {
font-size: var(--wa-font-size-x-large);
color: var(--wa-color-primary-500);
}
.error-message {
color: var(--wa-color-danger-500);
font-weight: bold;
}
.history-list {
list-style-type: none;
padding: 0;
max-height: 150px;
overflow-y: auto;
border: 1px solid var(--wa-color-neutral-100);
border-radius: var(--wa-border-radius-small);
padding: var(--wa-spacing-x-small);
background-color: var(--wa-color-neutral-50);
}
.history-list li {
padding: var(--wa-spacing-2x-small) 0;
border-bottom: 1px dotted var(--wa-color-neutral-100);
}
.history-list li:last-child {
border-bottom: none;
}
.card-footer {
display: flex;
justify-content: flex-end;
gap: var(--wa-spacing-small);
padding-top: var(--wa-spacing-medium);
border-top: 1px solid var(--wa-color-neutral-200);
}
.details-section {
margin-top: var(--wa-spacing-medium);
padding: var(--wa-spacing-small);
background-color: var(--wa-color-neutral-50);
border: 1px solid var(--wa-color-neutral-200);
border-radius: var(--wa-border-radius-small);
}
.details-section pre {
white-space: pre-wrap;
word-break: break-all;
font-size: var(--wa-font-size-x-small);
max-height: 200px;
overflow-y: auto;
}
`],
encapsulation: ViewEncapsulation.ShadowDom // Crucial for isolation
imports: [CommonModule, HttpClientModule, HighchartsChartComponent],
templateUrl: './app.html',
styleUrls: ["app.css"],
encapsulation: ViewEncapsulation.ShadowDom
})
export class MiningDashboardElementComponent implements OnInit, OnDestroy {
@Input() minerName: string = 'xmrig'; // Default miner name
@Input() apiBaseUrl: string = 'http://localhost:9090/api/v1/mining'; // Default API base URL
@Input() minerName: string = 'xmrig';
@Input() apiBaseUrl: string = 'http://localhost:9090/api/v1/mining';
hashrateHistory: HashratePoint[] = [];
currentHashrate: number = 0;
@ -146,6 +48,36 @@ export class MiningDashboardElementComponent implements OnInit, OnDestroy {
private refreshSubscription: Subscription | undefined;
chartOptions: Highcharts.Options = {
chart: {
type: 'spline',
},
title: {
text: 'Live Hashrate'
},
xAxis: {
type: 'datetime',
title: {
text: 'Time'
}
},
yAxis: {
title: {
text: 'Hashrate (H/s)'
},
min: 0
},
series: [{
name: 'Hashrate',
type: 'line',
data: []
}],
credits: {
enabled: false
}
};
updateFlag = false;
constructor(private http: HttpClient, private elementRef: ElementRef) {}
ngOnInit(): void {
@ -157,18 +89,34 @@ export class MiningDashboardElementComponent implements OnInit, OnDestroy {
}
startAutoRefresh(): void {
this.stopAutoRefresh(); // Stop any existing refresh
this.refreshSubscription = interval(10000) // Refresh every 10 seconds
this.stopAutoRefresh();
this.refreshSubscription = interval(10000)
.pipe(startWith(0), switchMap(() => this.fetchHashrateObservable()))
.subscribe({
next: (history) => {
this.hashrateHistory = history;
if (history.length > 0) {
if (history && history.length > 0) {
this.currentHashrate = history[history.length - 1].hashrate;
this.lastUpdated = new Date(history[history.length - 1].timestamp);
const chartData = history.map(point => [
new Date(point.timestamp).getTime(),
point.hashrate
]);
// Safely update the chart data with type assertion
if (this.chartOptions.series && this.chartOptions.series[0]) {
(this.chartOptions.series[0] as Highcharts.SeriesLineOptions).data = chartData;
this.updateFlag = true; // Trigger chart update
}
} else {
this.currentHashrate = 0;
this.lastUpdated = null;
// Safely clear the chart data
if (this.chartOptions.series && this.chartOptions.series[0]) {
(this.chartOptions.series[0] as Highcharts.SeriesLineOptions).data = [];
this.updateFlag = true;
}
}
this.loading = false;
this.error = null;
@ -188,29 +136,6 @@ export class MiningDashboardElementComponent implements OnInit, OnDestroy {
}
}
fetchHashrate(): void {
this.loading = true;
this.error = null;
this.fetchHashrateObservable().subscribe({
next: (history) => {
this.hashrateHistory = history;
if (history.length > 0) {
this.currentHashrate = history[history.length - 1].hashrate;
this.lastUpdated = new Date(history[history.length - 1].timestamp);
} else {
this.currentHashrate = 0;
this.lastUpdated = null;
}
this.loading = false;
},
error: (err) => {
console.error('Failed to fetch hashrate history:', err);
this.error = 'Failed to fetch hashrate history.';
this.loading = false;
}
});
}
private fetchHashrateObservable() {
const url = `${this.apiBaseUrl}/miners/${this.minerName}/hashrate-history`;
return this.http.get<HashratePoint[]>(url);

View file

@ -1,5 +1,5 @@
<!doctype html>
<html lang="en">
<html lang="en" class="wa-theme-awesome wa-palette-bright wa-brand-blue">
<head>
<meta charset="utf-8">
<title>Ui</title>

View file

@ -1,18 +1,12 @@
import { createApplication } from '@angular/platform-browser';
import { createCustomElement } from '@angular/elements';
import { MiningDashboardElementComponent } from './app/app'; // Renamed App to MiningDashboardElementComponent
import { importProvidersFrom } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { CommonModule } from '@angular/common';
import {appConfig} from './app/app.config';
(async () => {
// Bootstrap a minimal Angular application to provide
// necessary services like HttpClient to the custom element.
const app = await createApplication({
providers: [
importProvidersFrom(HttpClientModule, CommonModule)
]
});
const app = await createApplication(appConfig);
// Define your custom element
const MiningDashboardElement = createCustomElement(MiningDashboardElementComponent, { injector: app.injector });

View file

@ -1 +1,3 @@
/* You can add global styles to this file, and also import other style files */
@import "@awesome.me/webawesome/dist/styles/webawesome.css";
@import "@awesome.me/webawesome/dist/styles/themes/awesome.css";