integrates Highcharts for live hashrate visualization and updates dashboard layout
This commit is contained in:
parent
0d2dc1ddd8
commit
27834c332c
10 changed files with 249 additions and 148 deletions
|
|
@ -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
23
ui/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
];
|
||||
},
|
||||
}),
|
||||
]
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue