Builds CPU history widget

This commit is contained in:
Alicia Sykes 2021-12-15 02:17:17 +00:00
parent 66067b002f
commit 283d8c750e
4 changed files with 193 additions and 3 deletions

View File

@ -16,7 +16,6 @@ Dashy has support for displaying dynamic content in the form of widgets. There a
- [Stock Price History](#stock-price-history) - [Stock Price History](#stock-price-history)
- [Joke of the Day](#joke) - [Joke of the Day](#joke)
- [Flight Data](#flight-data) - [Flight Data](#flight-data)
- [Example Widget](#example-widget)
- [Self-Hosted Services Widgets](#dynamic-widgets) - [Self-Hosted Services Widgets](#dynamic-widgets)
- [Dynamic Widgets](#dynamic-widgets) - [Dynamic Widgets](#dynamic-widgets)
- [Iframe Widget](#iframe-widget) - [Iframe Widget](#iframe-widget)
@ -367,6 +366,30 @@ Displays airport departure and arrival flights, using data from [AeroDataBox](ht
--- ---
## Self-Hosted Services Widgets
### CPU History (NetData)
Pull recent CPU usage history from NetData.
<p align="center"><img width="400" src="https://i.ibb.co/ZdyR5nJ/nd-cpu-history.png" /></p>
##### Options
**Field** | **Type** | **Required** | **Description**
--- | --- | --- | ---
**`host`** | `string` | Required | The URL to your NetData instance
##### Example
```yaml
- type: nd-cpu-history
options:
host: http://192.168.1.1:19999
```
---
## Dynamic Widgets ## Dynamic Widgets
### Iframe Widget ### Iframe Widget
@ -397,7 +420,7 @@ Many websites and apps provide their own embeddable widgets. These can be used w
⚠️ **NOTE:** Use with extreme caution. Embedding a script from an untrustworthy source may have serious unintended consequences. ⚠️ **NOTE:** Use with extreme caution. Embedding a script from an untrustworthy source may have serious unintended consequences.
<p align="center"><img width="400" src="https://i.ibb.co/yn0SGtL/embed-widget.png" /></p> <p align="center"><img width="400" src="https://i.ibb.co/fkwNnxT/embed-widget-2.png" /></p>
##### Options ##### Options
@ -410,6 +433,22 @@ Many websites and apps provide their own embeddable widgets. These can be used w
##### Example ##### Example
```yaml
- type: embed
options:
scriptSrc: https://cdn.speedcheck.org/basic/scbjs.min.js
html: |
<div id="sc-container">
<div id="sc-branding" class="sc-bb">
<a target="_blank" href="https://www.speedcheck.org/">
<img src="https://cdn.speedcheck.org/branding/speedcheck-logo-18.png" alt="Speedcheck"/>
</a>
</div>
</div>
```
Or
```yaml ```yaml
- type: embed - type: embed
options: options:

View File

@ -0,0 +1,141 @@
<template>
<div class="cpu-history-chart" :id="chartId"></div>
</template>
<script>
import axios from 'axios';
import { Chart } from 'frappe-charts/dist/frappe-charts.min.esm';
import WidgetMixin from '@/mixins/WidgetMixin';
export default {
mixins: [WidgetMixin],
components: {},
data() {
return {
chartTitle: null,
chartData: null,
chartDom: null,
};
},
mounted() {
this.fetchData();
},
computed: {
/* URL where NetData is hosted */
netDataHost() {
const usersChoice = this.options.host;
if (!usersChoice || typeof usersChoice !== 'string') {
this.error('Host parameter is required');
return '';
}
return usersChoice;
},
apiVersion() {
return this.options.apiVersion || 'v1';
},
endpoint() {
return `${this.netDataHost}/api/${this.apiVersion}/data?chart=system.cpu`;
},
/* A sudo-random ID for the chart DOM element */
chartId() {
return `cpu-history-chart-${Math.round(Math.random() * 10000)}`;
},
chartHeight() {
return this.options.chartHeight || 300;
},
getChartColor() {
const cssVars = getComputedStyle(document.documentElement);
return cssVars.getPropertyValue('--widget-text-color').trim() || '#7cd6fd';
},
},
methods: {
/* Make GET request to NetData */
fetchData() {
axios.get(this.endpoint)
.then((response) => {
this.processData(response.data);
})
.catch((dataFetchError) => {
this.error('Unable to fetch data', dataFetchError);
})
.finally(() => {
this.finishLoading();
});
},
/* Assign data variables to the returned data */
processData(data) {
const timeData = [];
const systemCpu = [];
const userCpu = [];
data.data.reverse().forEach((reading) => {
timeData.push(this.formatDate(reading[0]));
systemCpu.push(reading[2]);
userCpu.push(reading[3]);
});
this.chartData = {
labels: timeData,
datasets: [
{ name: 'System CPU', type: 'bar', values: systemCpu },
{ name: 'User CPU', type: 'bar', values: userCpu },
],
};
this.chartTitle = this.makeChartTitle(data.data);
this.renderChart();
},
makeChartTitle(data) {
if (!data || !data[0][0]) return '';
const diff = Math.round((data[data.length - 1][0] - data[0][0]) / 60);
return `Past ${diff} minutes`;
},
renderChart() {
this.chartDom = this.generateChart();
},
/* Create new chart, using the crypto data */
generateChart() {
return new Chart(`#${this.chartId}`, {
title: this.chartTitle,
data: this.chartData,
type: 'axis-mixed',
height: this.chartHeight,
colors: [this.getChartColor, '#743ee2'],
truncateLegends: true,
lineOptions: {
regionFill: 1,
hideDots: 1,
},
axisOptions: {
xIsSeries: true,
xAxisMode: 'tick',
},
tooltipOptions: {
formatTooltipY: d => `${Math.round(d)}%`,
},
});
},
/* Format the date for a given time stamp, also include time if required */
formatDate(timestamp) {
const localFormat = navigator.language;
const dateFormat = { weekday: 'short', day: 'numeric', month: 'short' };
const timeFormat = { hour: 'numeric', minute: 'numeric', second: 'numeric' };
const date = new Date(timestamp * 1000).toLocaleDateString(localFormat, dateFormat);
const time = Intl.DateTimeFormat(localFormat, timeFormat).format(timestamp);
return `${date} ${time}`;
},
},
};
</script>
<style lang="scss">
.cpu-history-chart .chart-container {
text.title {
text-transform: capitalize;
color: var(--widget-text-color);
}
.axis, .chart-label {
fill: var(--widget-text-color);
opacity: var(--dimming-factor);
&:hover { opacity: 1; }
}
}
</style>

View File

@ -102,6 +102,13 @@
@error="handleError" @error="handleError"
:ref="widgetRef" :ref="widgetRef"
/> />
<NdCpuHistory
v-else-if="widgetType === 'nd-cpu-history'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<IframeWidget <IframeWidget
v-else-if="widgetType === 'iframe'" v-else-if="widgetType === 'iframe'"
:options="widgetOptions" :options="widgetOptions"
@ -117,7 +124,7 @@
:ref="widgetRef" :ref="widgetRef"
/> />
<!-- No widget type specified --> <!-- No widget type specified -->
<div v-else>{{ handleError('No widget type was specified') }}</div> <div v-else>{{ handleError('Widget type was not found') }}</div>
</div> </div>
</div> </div>
</template> </template>
@ -143,6 +150,7 @@ import ExchangeRates from '@/components/Widgets/ExchangeRates.vue';
import StockPriceChart from '@/components/Widgets/StockPriceChart.vue'; import StockPriceChart from '@/components/Widgets/StockPriceChart.vue';
import Jokes from '@/components/Widgets/Jokes.vue'; import Jokes from '@/components/Widgets/Jokes.vue';
import Flights from '@/components/Widgets/Flights.vue'; import Flights from '@/components/Widgets/Flights.vue';
import NdCpuHistory from '@/components/Widgets/NdCpuHistory.vue';
import IframeWidget from '@/components/Widgets/IframeWidget.vue'; import IframeWidget from '@/components/Widgets/IframeWidget.vue';
import EmbedWidget from '@/components/Widgets/EmbedWidget.vue'; import EmbedWidget from '@/components/Widgets/EmbedWidget.vue';
@ -165,6 +173,7 @@ export default {
StockPriceChart, StockPriceChart,
Jokes, Jokes,
Flights, Flights,
NdCpuHistory,
IframeWidget, IframeWidget,
EmbedWidget, EmbedWidget,
}, },

View File

@ -258,6 +258,7 @@ export default {
else { else {
let itemsFound = true; let itemsFound = true;
this.sections.forEach((section) => { this.sections.forEach((section) => {
if (section.widgets && section.widgets.length > 0) itemsFound = false;
if (this.filterTiles(section.items, this.searchValue).length > 0) itemsFound = false; if (this.filterTiles(section.items, this.searchValue).length > 0) itemsFound = false;
}); });
return itemsFound; return itemsFound;