mirror of
https://github.com/Lissy93/dashy.git
synced 2025-07-26 07:05:43 +02:00
🔀 Merge pull request #392 from Lissy93/FEATURE/add-request-proxy
[FIX] Adds support for proxying CORS requests
Happy new year :) 🎇
This commit is contained in:
commit
3b7d5a6ff7
6
.github/CHANGELOG.md
vendored
6
.github/CHANGELOG.md
vendored
@ -1,5 +1,11 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## ⚡️ 1.9.6 - Adds Proxy Support for Widget Requests [PR #392](https://github.com/Lissy93/dashy/pull/392)
|
||||||
|
- Refactors widget mixin to include data requests, so that code can be shared between widgets
|
||||||
|
- Adds a Node endpoint for proxying requests server-side, used for APIs that are not CORS enabled
|
||||||
|
- Adds option to config file for user to force proxying of requests
|
||||||
|
- Writes a Netlify cloud function to support proxying when the app is hosted on Netlify
|
||||||
|
|
||||||
## 🐛 1.9.5 - Bug fixes and Minor Improvements [PR #388](https://github.com/Lissy93/dashy/pull/388)
|
## 🐛 1.9.5 - Bug fixes and Minor Improvements [PR #388](https://github.com/Lissy93/dashy/pull/388)
|
||||||
- Adds icon.horse to supported favicon APIs
|
- Adds icon.horse to supported favicon APIs
|
||||||
- Fixes tile move bug, Re: #366
|
- Fixes tile move bug, Re: #366
|
||||||
|
@ -172,7 +172,8 @@ For more info, see the **[Authentication Docs](/docs/authentication.md)**
|
|||||||
--- | --- | --- | ---
|
--- | --- | --- | ---
|
||||||
**`name`** | `string` | Required | The title for the section
|
**`name`** | `string` | Required | The title for the section
|
||||||
**`icon`** | `string` | _Optional_ | An single icon to be displayed next to the title. See [`section.icon`](#sectionicon-and-sectionitemicon)
|
**`icon`** | `string` | _Optional_ | An single icon to be displayed next to the title. See [`section.icon`](#sectionicon-and-sectionitemicon)
|
||||||
**`items`** | `array` | Required | An array of items to be displayed within the section. See [`item`](#sectionitem)
|
**`items`** | `array` | _Optional_ | An array of items to be displayed within the section. See [`item`](#sectionitem). Sections must include either 1 or more items, or 1 or more widgets.
|
||||||
|
**`widgets`** | `array` | _Optional_ | An array of widgets to be displayed within the section. See [`widget`](#sectionwidget-optional)
|
||||||
**`displayData`** | `object` | _Optional_ | Meta-data to optionally overide display settings for a given section. See [`displayData`](#sectiondisplaydata-optional)
|
**`displayData`** | `object` | _Optional_ | Meta-data to optionally overide display settings for a given section. See [`displayData`](#sectiondisplaydata-optional)
|
||||||
|
|
||||||
**[⬆️ Back to Top](#configuring)**
|
**[⬆️ Back to Top](#configuring)**
|
||||||
@ -198,6 +199,18 @@ For more info, see the **[Authentication Docs](/docs/authentication.md)**
|
|||||||
|
|
||||||
**[⬆️ Back to Top](#configuring)**
|
**[⬆️ Back to Top](#configuring)**
|
||||||
|
|
||||||
|
### `section.widget` _(optional)_
|
||||||
|
|
||||||
|
**Field** | **Type** | **Required**| **Description**
|
||||||
|
--- | --- | --- | ---
|
||||||
|
**`type`** | `string` | Required | The widget type. See [Widget Docs](/docs/widgets.md) for full list of supported widgets
|
||||||
|
**`options`** | `object` | _Optional_ | Some widgets accept either optional or required additional options. Again, see the [Widget Docs](/docs/widgets.md) for full list of options
|
||||||
|
**`updateInterval`** | `number` | _Optional_ | You can keep a widget constantly updated by specifying an update interval, in seconds. See [Continuous Updates Docs](/docs/widgets.md#continuous-updates) for more info
|
||||||
|
**`useProxy`** | `boolean` | _Optional_ | Some widgets make API requests to services that are not CORS-enabled. For these instances, you will need to route requests through a proxy, Dashy has a built in CORS-proxy, which you can use by setting this option to `true`. Defaults to `false`. See the [Proxying Requests Docs](/docs/widgets.md#proxying-requests) for more info
|
||||||
|
|
||||||
|
**[⬆️ Back to Top](#configuring)**
|
||||||
|
|
||||||
|
|
||||||
### `section.displayData` _(optional)_
|
### `section.displayData` _(optional)_
|
||||||
|
|
||||||
**Field** | **Type** | **Required**| **Description**
|
**Field** | **Type** | **Required**| **Description**
|
||||||
|
@ -1208,6 +1208,31 @@ For more info on how to apply custom variables, see the [Theming Docs](/docs/the
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Proxying Requests
|
||||||
|
|
||||||
|
If a widget fails to make a data request, and the console shows a CORS error, this means the server is blocking client-side requests.
|
||||||
|
|
||||||
|
Dashy has a built-in CORS proxy ([`services/cors-proxy.js`](https://github.com/Lissy93/dashy/blob/master/services/cors-proxy.js)), which will be used automatically by some widgets, or can be forced to use by other by setting the `useProxy` option.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
widgets:
|
||||||
|
- type: pi-hole-stats
|
||||||
|
useProxy: true
|
||||||
|
options:
|
||||||
|
hostname: http://pi-hole.local
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternativley, and more securley, you can set the auth headers on your service to accept requests from Dashy. For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
Access-Control-Allow-Origin: https://location-of-dashy/
|
||||||
|
Vary: Origin
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Language Translations
|
### Language Translations
|
||||||
|
|
||||||
Since most of the content displayed within widgets is fetched from an external API, unless that API supports multiple languages, translating dynamic content is not possible.
|
Since most of the content displayed within widgets is fetched from an external API, unless that API supports multiple languages, translating dynamic content is not possible.
|
||||||
|
@ -27,6 +27,11 @@
|
|||||||
to = "/.netlify/functions/not-supported"
|
to = "/.netlify/functions/not-supported"
|
||||||
status = 301
|
status = 301
|
||||||
force = true
|
force = true
|
||||||
|
[[redirects]]
|
||||||
|
from = "/cors-proxy"
|
||||||
|
to = "/.netlify/functions/netlify-cors"
|
||||||
|
status = 301
|
||||||
|
force = true
|
||||||
|
|
||||||
# For router history mode, ensure pages land on index
|
# For router history mode, ensure pages land on index
|
||||||
[[redirects]]
|
[[redirects]]
|
||||||
@ -40,3 +45,4 @@
|
|||||||
[headers.values]
|
[headers.values]
|
||||||
# Uncomment to enable Netlify user control. You must have a paid plan.
|
# Uncomment to enable Netlify user control. You must have a paid plan.
|
||||||
# Basic-Auth = "someuser:somepassword anotheruser:anotherpassword"
|
# Basic-Auth = "someuser:somepassword anotheruser:anotherpassword"
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Dashy",
|
"name": "Dashy",
|
||||||
"version": "1.9.5",
|
"version": "1.9.6",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "server",
|
"main": "server",
|
||||||
"author": "Alicia Sykes <alicia@omg.lol> (https://aliciasykes.com)",
|
"author": "Alicia Sykes <alicia@omg.lol> (https://aliciasykes.com)",
|
||||||
|
@ -32,7 +32,7 @@ module.exports = (req, res) => {
|
|||||||
// Prepare the request
|
// Prepare the request
|
||||||
const requestConfig = {
|
const requestConfig = {
|
||||||
method: req.method,
|
method: req.method,
|
||||||
url: targetURL + req.url,
|
url: targetURL,
|
||||||
json: req.body,
|
json: req.body,
|
||||||
headers,
|
headers,
|
||||||
};
|
};
|
||||||
|
48
services/serverless-functions/netlify-cors.js
Normal file
48
services/serverless-functions/netlify-cors.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/* A Netlify cloud function to handle requests to CORS-disabled services */
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
exports.handler = (event, context, callback) => {
|
||||||
|
// Get input data
|
||||||
|
const { body, headers, queryStringParameters } = event;
|
||||||
|
|
||||||
|
// Get URL from header or GET param
|
||||||
|
const requestUrl = queryStringParameters.url || headers['Target-URL'] || headers['target-url'];
|
||||||
|
|
||||||
|
const returnError = (msg, error) => {
|
||||||
|
callback(null, {
|
||||||
|
statusCode: 400,
|
||||||
|
body: JSON.stringify({ success: false, msg, error }),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// If URL missing, return error
|
||||||
|
if (!requestUrl) {
|
||||||
|
returnError('Missing Target-URL header', null);
|
||||||
|
}
|
||||||
|
|
||||||
|
let custom = {};
|
||||||
|
try {
|
||||||
|
custom = JSON.parse(headers.CustomHeaders || headers.customheaders || '{}');
|
||||||
|
} catch (e) { returnError('Unable to parse custom headers'); }
|
||||||
|
|
||||||
|
// Response headers
|
||||||
|
const requestHeaders = {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
...custom,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prepare request
|
||||||
|
const requestConfig = {
|
||||||
|
method: 'GET',
|
||||||
|
url: requestUrl,
|
||||||
|
json: body,
|
||||||
|
headers: requestHeaders,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make request
|
||||||
|
axios.request(requestConfig)
|
||||||
|
.then((response) => {
|
||||||
|
callback(null, { statusCode: 200, body: JSON.stringify(response.data) });
|
||||||
|
}).catch((error) => {
|
||||||
|
returnError('Request failed', error);
|
||||||
|
});
|
||||||
|
};
|
@ -263,6 +263,12 @@
|
|||||||
"up": "Online",
|
"up": "Online",
|
||||||
"down": "Offline"
|
"down": "Offline"
|
||||||
},
|
},
|
||||||
|
"net-data": {
|
||||||
|
"cpu-chart-title": "CPU History",
|
||||||
|
"mem-chart-title": "Memory Usage",
|
||||||
|
"mem-breakdown-title": "Memory Breakdown",
|
||||||
|
"load-chart-title": "System Load"
|
||||||
|
},
|
||||||
"system-info": {
|
"system-info": {
|
||||||
"uptime": "Uptime"
|
"uptime": "Uptime"
|
||||||
},
|
},
|
||||||
|
@ -17,9 +17,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios';
|
|
||||||
import WidgetMixin from '@/mixins/WidgetMixin';
|
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||||
import { widgetApiEndpoints, serviceEndpoints } from '@/utils/defaults';
|
import { widgetApiEndpoints } from '@/utils/defaults';
|
||||||
import { capitalize, timestampToDateTime } from '@/utils/MiscHelpers';
|
import { capitalize, timestampToDateTime } from '@/utils/MiscHelpers';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -48,10 +47,6 @@ export default {
|
|||||||
if (this.options.host) return `${this.options.host}/api/v1/checks`;
|
if (this.options.host) return `${this.options.host}/api/v1/checks`;
|
||||||
return `${widgetApiEndpoints.healthChecks}`;
|
return `${widgetApiEndpoints.healthChecks}`;
|
||||||
},
|
},
|
||||||
proxyReqEndpoint() {
|
|
||||||
const baseUrl = process.env.VUE_APP_DOMAIN || window.location.origin;
|
|
||||||
return `${baseUrl}${serviceEndpoints.corsProxy}`;
|
|
||||||
},
|
|
||||||
apiKey() {
|
apiKey() {
|
||||||
if (!this.options.apiKey) {
|
if (!this.options.apiKey) {
|
||||||
this.error('An API key is required, please see the docs for more info');
|
this.error('An API key is required, please see the docs for more info');
|
||||||
@ -62,23 +57,11 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
/* Make GET request to CoinGecko API endpoint */
|
/* Make GET request to CoinGecko API endpoint */
|
||||||
fetchData() {
|
fetchData() {
|
||||||
const requestConfig = {
|
this.overrideProxyChoice = true;
|
||||||
method: 'GET',
|
const authHeaders = { 'X-Api-Key': this.apiKey };
|
||||||
url: this.proxyReqEndpoint,
|
this.makeRequest(this.endpoint, authHeaders).then(
|
||||||
headers: {
|
(response) => { this.processData(response); },
|
||||||
'access-control-request-headers': '*',
|
);
|
||||||
'Target-URL': this.endpoint,
|
|
||||||
CustomHeaders: JSON.stringify({ 'X-Api-Key': this.apiKey }),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
axios.request(requestConfig)
|
|
||||||
.then((response) => {
|
|
||||||
this.processData(response.data);
|
|
||||||
}).catch((error) => {
|
|
||||||
this.error('Unable to fetch cron data', error);
|
|
||||||
}).finally(() => {
|
|
||||||
this.finishLoading();
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
/* Assign data variables to the returned data */
|
/* Assign data variables to the returned data */
|
||||||
processData(data) {
|
processData(data) {
|
||||||
|
@ -3,20 +3,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios';
|
|
||||||
import WidgetMixin from '@/mixins/WidgetMixin';
|
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||||
import ChartingMixin from '@/mixins/ChartingMixin';
|
import ChartingMixin from '@/mixins/ChartingMixin';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [WidgetMixin, ChartingMixin],
|
mixins: [WidgetMixin, ChartingMixin],
|
||||||
components: {},
|
components: {},
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
chartTitle: null,
|
|
||||||
chartData: null,
|
|
||||||
chartDom: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
/* URL where NetData is hosted */
|
/* URL where NetData is hosted */
|
||||||
netDataHost() {
|
netDataHost() {
|
||||||
@ -41,50 +33,44 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
/* Make GET request to NetData */
|
/* Make GET request to NetData */
|
||||||
fetchData() {
|
fetchData() {
|
||||||
axios.get(this.endpoint)
|
this.makeRequest(this.endpoint).then(
|
||||||
.then((response) => {
|
(response) => { this.processData(response); },
|
||||||
this.processData(response.data);
|
);
|
||||||
})
|
|
||||||
.catch((dataFetchError) => {
|
|
||||||
this.error('Unable to fetch data', dataFetchError);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.finishLoading();
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
/* Assign data variables to the returned data */
|
/* Assign data variables to the returned data */
|
||||||
processData(data) {
|
processData(inputData) {
|
||||||
const timeData = [];
|
const { labels, data } = inputData;
|
||||||
const systemCpu = [];
|
const timeData = []; // List of timestamps for axis
|
||||||
const userCpu = [];
|
const resultGroup = {}; // List of datasets, for each label
|
||||||
data.data.reverse().forEach((reading) => {
|
data.reverse().forEach((reading) => {
|
||||||
timeData.push(this.formatDate(reading[0] * 1000));
|
labels.forEach((label, indx) => {
|
||||||
systemCpu.push(reading[2]);
|
if (indx === 0) { // First value is the timestamp, add to axis
|
||||||
userCpu.push(reading[3]);
|
timeData.push(this.formatTime(reading[indx] * 1000));
|
||||||
|
} else { // All other values correspond to a label
|
||||||
|
if (!resultGroup[label]) resultGroup[label] = [];
|
||||||
|
resultGroup[label].push(reading[indx]);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
this.chartData = {
|
});
|
||||||
labels: timeData,
|
const datasets = [];
|
||||||
datasets: [
|
Object.keys(resultGroup).forEach((label) => {
|
||||||
{ name: 'System CPU', type: 'bar', values: systemCpu },
|
datasets.push({ name: label, type: 'bar', values: resultGroup[label] });
|
||||||
{ name: 'User CPU', type: 'bar', values: userCpu },
|
});
|
||||||
],
|
const timeChartData = { labels: timeData, datasets };
|
||||||
};
|
const chartTitle = this.makeChartTitle(data);
|
||||||
this.chartTitle = this.makeChartTitle(data.data);
|
this.generateChart(timeChartData, chartTitle);
|
||||||
this.renderChart();
|
|
||||||
},
|
},
|
||||||
makeChartTitle(data) {
|
makeChartTitle(data) {
|
||||||
if (!data || !data[0][0]) return '';
|
const prefix = this.$t('widgets.net-data.cpu-chart-title');
|
||||||
|
if (!data || !data[0][0]) return prefix;
|
||||||
const diff = Math.round((data[data.length - 1][0] - data[0][0]) / 60);
|
const diff = Math.round((data[data.length - 1][0] - data[0][0]) / 60);
|
||||||
return `Past ${diff} minutes`;
|
return `${prefix}: Past ${diff} minutes`;
|
||||||
},
|
|
||||||
renderChart() {
|
|
||||||
this.chartDom = this.generateChart();
|
|
||||||
},
|
},
|
||||||
/* Create new chart, using the crypto data */
|
/* Create new chart, using the crypto data */
|
||||||
generateChart() {
|
generateChart(timeChartData, chartTitle) {
|
||||||
return new this.Chart(`#${this.chartId}`, {
|
return new this.Chart(`#${this.chartId}`, {
|
||||||
title: this.chartTitle,
|
title: chartTitle,
|
||||||
data: this.chartData,
|
data: timeChartData,
|
||||||
type: 'axis-mixed',
|
type: 'axis-mixed',
|
||||||
height: this.chartHeight,
|
height: this.chartHeight,
|
||||||
colors: this.chartColors,
|
colors: this.chartColors,
|
||||||
|
@ -10,13 +10,6 @@ import ChartingMixin from '@/mixins/ChartingMixin';
|
|||||||
export default {
|
export default {
|
||||||
mixins: [WidgetMixin, ChartingMixin],
|
mixins: [WidgetMixin, ChartingMixin],
|
||||||
components: {},
|
components: {},
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
chartTitle: null,
|
|
||||||
chartData: null,
|
|
||||||
chartDom: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
/* URL where NetData is hosted */
|
/* URL where NetData is hosted */
|
||||||
netDataHost() {
|
netDataHost() {
|
||||||
@ -31,7 +24,7 @@ export default {
|
|||||||
return this.options.apiVersion || 'v1';
|
return this.options.apiVersion || 'v1';
|
||||||
},
|
},
|
||||||
endpoint() {
|
endpoint() {
|
||||||
return `${this.netDataHost}/api/${this.apiVersion}/data?chart=system.cpu`;
|
return `${this.netDataHost}/api/${this.apiVersion}/data?chart=system.load`;
|
||||||
},
|
},
|
||||||
/* A sudo-random ID for the chart DOM element */
|
/* A sudo-random ID for the chart DOM element */
|
||||||
chartId() {
|
chartId() {
|
||||||
@ -64,7 +57,7 @@ export default {
|
|||||||
load5mins.push(reading[2]);
|
load5mins.push(reading[2]);
|
||||||
load15mins.push(reading[3]);
|
load15mins.push(reading[3]);
|
||||||
});
|
});
|
||||||
this.chartData = {
|
const chartData = {
|
||||||
labels: timeData,
|
labels: timeData,
|
||||||
datasets: [
|
datasets: [
|
||||||
{ name: '1 Min', type: 'bar', values: load1min },
|
{ name: '1 Min', type: 'bar', values: load1min },
|
||||||
@ -72,22 +65,20 @@ export default {
|
|||||||
{ name: '15 Mins', type: 'bar', values: load15mins },
|
{ name: '15 Mins', type: 'bar', values: load15mins },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
this.chartTitle = this.makeChartTitle(data.data);
|
const chartTitle = this.makeChartTitle(data.data);
|
||||||
this.renderChart();
|
this.generateChart(chartData, chartTitle);
|
||||||
},
|
},
|
||||||
makeChartTitle(data) {
|
makeChartTitle(data) {
|
||||||
if (!data || !data[0][0]) return '';
|
const prefix = this.$t('widgets.net-data.load-chart-title');
|
||||||
|
if (!data || !data[0][0]) return prefix;
|
||||||
const diff = Math.round((data[data.length - 1][0] - data[0][0]) / 60);
|
const diff = Math.round((data[data.length - 1][0] - data[0][0]) / 60);
|
||||||
return `Past ${diff} minutes`;
|
return `${prefix}: Past ${diff} minutes`;
|
||||||
},
|
|
||||||
renderChart() {
|
|
||||||
this.chartDom = this.generateChart();
|
|
||||||
},
|
},
|
||||||
/* Create new chart, using the crypto data */
|
/* Create new chart, using the crypto data */
|
||||||
generateChart() {
|
generateChart(chartData, chartTitle) {
|
||||||
return new this.Chart(`#${this.chartId}`, {
|
return new this.Chart(`#${this.chartId}`, {
|
||||||
title: this.chartTitle,
|
title: chartTitle,
|
||||||
data: this.chartData,
|
data: chartData,
|
||||||
type: 'axis-mixed',
|
type: 'axis-mixed',
|
||||||
height: this.chartHeight,
|
height: this.chartHeight,
|
||||||
colors: this.chartColors,
|
colors: this.chartColors,
|
||||||
|
@ -6,7 +6,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios';
|
|
||||||
import WidgetMixin from '@/mixins/WidgetMixin';
|
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||||
import ChartingMixin from '@/mixins/ChartingMixin';
|
import ChartingMixin from '@/mixins/ChartingMixin';
|
||||||
|
|
||||||
@ -37,16 +36,9 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
/* Make GET request to NetData */
|
/* Make GET request to NetData */
|
||||||
fetchData() {
|
fetchData() {
|
||||||
axios.get(this.endpoint)
|
this.makeRequest(this.endpoint).then(
|
||||||
.then((response) => {
|
(response) => { this.processData(response); },
|
||||||
this.processData(response.data);
|
);
|
||||||
})
|
|
||||||
.catch((dataFetchError) => {
|
|
||||||
this.error('Unable to fetch data', dataFetchError);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.finishLoading();
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
/* Assign data variables to the returned data */
|
/* Assign data variables to the returned data */
|
||||||
processData(inputData) {
|
processData(inputData) {
|
||||||
@ -86,7 +78,7 @@ export default {
|
|||||||
/* Create new chart, using the crypto data */
|
/* Create new chart, using the crypto data */
|
||||||
generateHistoryChart(timeChartData) {
|
generateHistoryChart(timeChartData) {
|
||||||
return new this.Chart(`#${this.chartId}`, {
|
return new this.Chart(`#${this.chartId}`, {
|
||||||
title: 'History',
|
title: this.$t('widgets.net-data.mem-chart-title'),
|
||||||
data: timeChartData,
|
data: timeChartData,
|
||||||
type: 'axis-mixed',
|
type: 'axis-mixed',
|
||||||
height: this.chartHeight,
|
height: this.chartHeight,
|
||||||
@ -107,7 +99,7 @@ export default {
|
|||||||
},
|
},
|
||||||
generateAggregateChart(aggregateChartData) {
|
generateAggregateChart(aggregateChartData) {
|
||||||
return new this.Chart(`#aggregate-${this.chartId}`, {
|
return new this.Chart(`#aggregate-${this.chartId}`, {
|
||||||
title: 'Averages',
|
title: this.$t('widgets.net-data.mem-breakdown-title'),
|
||||||
data: aggregateChartData,
|
data: aggregateChartData,
|
||||||
type: 'percentage',
|
type: 'percentage',
|
||||||
height: 100,
|
height: 100,
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios';
|
// import axios from 'axios';
|
||||||
import WidgetMixin from '@/mixins/WidgetMixin';
|
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||||
import ChartingMixin from '@/mixins/ChartingMixin';
|
import ChartingMixin from '@/mixins/ChartingMixin';
|
||||||
import { capitalize } from '@/utils/MiscHelpers';
|
import { capitalize } from '@/utils/MiscHelpers';
|
||||||
@ -55,15 +55,9 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
/* Make GET request to local pi-hole instance */
|
/* Make GET request to local pi-hole instance */
|
||||||
fetchData() {
|
fetchData() {
|
||||||
axios.get(this.endpoint)
|
this.makeRequest(this.endpoint)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
this.processData(response.data);
|
this.processData(response);
|
||||||
})
|
|
||||||
.catch((dataFetchError) => {
|
|
||||||
this.error('Unable to fetch data', dataFetchError);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.finishLoading();
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
/* Assign data variables to the returned data */
|
/* Assign data variables to the returned data */
|
||||||
|
@ -11,7 +11,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios';
|
|
||||||
import WidgetMixin from '@/mixins/WidgetMixin';
|
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||||
import { showNumAsThousand } from '@/utils/MiscHelpers';
|
import { showNumAsThousand } from '@/utils/MiscHelpers';
|
||||||
|
|
||||||
@ -46,19 +45,13 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
/* Make GET request to local pi-hole instance */
|
/* Make GET request to local pi-hole instance */
|
||||||
fetchData() {
|
fetchData() {
|
||||||
axios.get(this.endpoint)
|
this.makeRequest(this.endpoint)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (Array.isArray(response.data)) {
|
if (Array.isArray(response)) {
|
||||||
this.error('Got success, but found no results, possible authorization error');
|
this.error('Got success, but found no results, possible authorization error');
|
||||||
} else {
|
} else {
|
||||||
this.processData(response.data);
|
this.processData(response);
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch((dataFetchError) => {
|
|
||||||
this.error('Unable to fetch data', dataFetchError);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.finishLoading();
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
/* Assign data variables to the returned data */
|
/* Assign data variables to the returned data */
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios';
|
|
||||||
import WidgetMixin from '@/mixins/WidgetMixin';
|
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||||
import ChartingMixin from '@/mixins/ChartingMixin';
|
import ChartingMixin from '@/mixins/ChartingMixin';
|
||||||
|
|
||||||
@ -31,17 +30,23 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
/* Make GET request to local pi-hole instance */
|
/* Make GET request to local pi-hole instance */
|
||||||
fetchData() {
|
fetchData() {
|
||||||
axios.get(this.endpoint)
|
this.makeRequest(this.endpoint)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
this.processData(response.data);
|
if (this.validate(response)) {
|
||||||
})
|
this.processData(response);
|
||||||
.catch((dataFetchError) => {
|
}
|
||||||
this.error('Unable to fetch data', dataFetchError);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.finishLoading();
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
validate(response) {
|
||||||
|
if (!response.ads_over_time || !response.domains_over_time) {
|
||||||
|
this.error('Expected data was not returned from Pi-Hole');
|
||||||
|
return false;
|
||||||
|
} else if (response.ads_over_time.length < 1) {
|
||||||
|
this.error('Request completed succesfully, but no data in Pi-Hole yet');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
/* Assign data variables to the returned data */
|
/* Assign data variables to the returned data */
|
||||||
processData(data) {
|
processData(data) {
|
||||||
const timeData = [];
|
const timeData = [];
|
||||||
|
@ -319,7 +319,10 @@ export default {
|
|||||||
},
|
},
|
||||||
/* Returns users specified widget options, or empty object */
|
/* Returns users specified widget options, or empty object */
|
||||||
widgetOptions() {
|
widgetOptions() {
|
||||||
return this.widget.options || {};
|
const options = this.widget.options || {};
|
||||||
|
const useProxy = !!this.widget.useProxy;
|
||||||
|
const updateInterval = this.widget.updateInterval || 0;
|
||||||
|
return { useProxy, updateInterval, ...options };
|
||||||
},
|
},
|
||||||
/* A unique string to reference the widget by */
|
/* A unique string to reference the widget by */
|
||||||
widgetRef() {
|
widgetRef() {
|
||||||
|
@ -2,8 +2,10 @@
|
|||||||
* Mixin that all pre-built and custom widgets extend from.
|
* Mixin that all pre-built and custom widgets extend from.
|
||||||
* Manages loading state, error handling, data updates and user options
|
* Manages loading state, error handling, data updates and user options
|
||||||
*/
|
*/
|
||||||
|
import axios from 'axios';
|
||||||
import ProgressBar from 'rsup-progress';
|
import ProgressBar from 'rsup-progress';
|
||||||
import ErrorHandler from '@/utils/ErrorHandler';
|
import ErrorHandler from '@/utils/ErrorHandler';
|
||||||
|
import { serviceEndpoints } from '@/utils/defaults';
|
||||||
|
|
||||||
const WidgetMixin = {
|
const WidgetMixin = {
|
||||||
props: {
|
props: {
|
||||||
@ -14,11 +16,21 @@ const WidgetMixin = {
|
|||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
progress: new ProgressBar({ color: 'var(--progress-bar)' }),
|
progress: new ProgressBar({ color: 'var(--progress-bar)' }),
|
||||||
|
overrideProxyChoice: false,
|
||||||
}),
|
}),
|
||||||
/* When component mounted, fetch initial data */
|
/* When component mounted, fetch initial data */
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchData();
|
this.fetchData();
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
proxyReqEndpoint() {
|
||||||
|
const baseUrl = process.env.VUE_APP_DOMAIN || window.location.origin;
|
||||||
|
return `${baseUrl}${serviceEndpoints.corsProxy}`;
|
||||||
|
},
|
||||||
|
useProxy() {
|
||||||
|
return this.options.useProxy || this.overrideProxyChoice;
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
/* Re-fetches external data, called by parent. Usually overridden by widget */
|
/* Re-fetches external data, called by parent. Usually overridden by widget */
|
||||||
update() {
|
update() {
|
||||||
@ -44,9 +56,36 @@ const WidgetMixin = {
|
|||||||
fetchData() {
|
fetchData() {
|
||||||
this.finishLoading();
|
this.finishLoading();
|
||||||
},
|
},
|
||||||
|
/* Used as v-tooltip, pass text content in, and will show on hover */
|
||||||
tooltip(content) {
|
tooltip(content) {
|
||||||
return { content, trigger: 'hover focus', delay: 250 };
|
return { content, trigger: 'hover focus', delay: 250 };
|
||||||
},
|
},
|
||||||
|
/* Makes data request, returns promise */
|
||||||
|
makeRequest(endpoint, options) {
|
||||||
|
// Request Options
|
||||||
|
const method = 'GET';
|
||||||
|
const url = this.useProxy ? this.proxyReqEndpoint : endpoint;
|
||||||
|
const CustomHeaders = options ? JSON.stringify(options) : null;
|
||||||
|
const headers = this.useProxy
|
||||||
|
? { 'Target-URL': endpoint, CustomHeaders } : CustomHeaders;
|
||||||
|
// Make request
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
axios.request({ method, url, headers })
|
||||||
|
.then((response) => {
|
||||||
|
if (response.data.success === false) {
|
||||||
|
this.error('Proxy returned error from target server', response.data.message);
|
||||||
|
}
|
||||||
|
resolve(response.data);
|
||||||
|
})
|
||||||
|
.catch((dataFetchError) => {
|
||||||
|
this.error('Unable to fetch data', dataFetchError);
|
||||||
|
reject(dataFetchError);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.finishLoading();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user