mirror of
https://github.com/Lissy93/dashy.git
synced 2025-04-08 17:06:18 +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
|
||||
|
||||
## ⚡️ 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)
|
||||
- Adds icon.horse to supported favicon APIs
|
||||
- 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
|
||||
**`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)
|
||||
|
||||
**[⬆️ Back to Top](#configuring)**
|
||||
@ -198,6 +199,18 @@ For more info, see the **[Authentication Docs](/docs/authentication.md)**
|
||||
|
||||
**[⬆️ 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)_
|
||||
|
||||
**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
|
||||
|
||||
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.
|
||||
|
90
netlify.toml
90
netlify.toml
@ -1,42 +1,48 @@
|
||||
# Enables you to easily deploy a fork of Dashy to Netlify
|
||||
# without the need to configure anything in admin UI
|
||||
# Docs: https://www.netlify.com/docs/netlify-toml-reference/
|
||||
|
||||
# Essential site config
|
||||
[build]
|
||||
base = "/"
|
||||
command = "yarn build"
|
||||
publish = "dist"
|
||||
functions = "services/serverless-functions"
|
||||
|
||||
# Site info, used for the 1-Click deploy page
|
||||
[template.environment]
|
||||
STATUSKIT_PAGE_TITLE = "Dashy"
|
||||
STATUSKIT_COMPANY_LOGO = "https://raw.githubusercontent.com/Lissy93/dashy/master/docs/assets/logo.png"
|
||||
STATUSKIT_SUPPORT_CONTACT_LINK = "https://github.com/lissy93/dashy"
|
||||
STATUSKIT_RESOURCES_LINK = "https://dashy.to/docs"
|
||||
|
||||
# Redirect the Node endpoints to serverless functions
|
||||
[[redirects]]
|
||||
from = "/status-check"
|
||||
to = "/.netlify/functions/cloud-status-check"
|
||||
status = 301
|
||||
force = true
|
||||
[[redirects]]
|
||||
from = "/config-manager/*"
|
||||
to = "/.netlify/functions/not-supported"
|
||||
status = 301
|
||||
force = true
|
||||
|
||||
# For router history mode, ensure pages land on index
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "/index.html"
|
||||
status = 200
|
||||
|
||||
# Set any security headers here
|
||||
[[headers]]
|
||||
for = "/*"
|
||||
[headers.values]
|
||||
# Uncomment to enable Netlify user control. You must have a paid plan.
|
||||
# Basic-Auth = "someuser:somepassword anotheruser:anotherpassword"
|
||||
# Enables you to easily deploy a fork of Dashy to Netlify
|
||||
# without the need to configure anything in admin UI
|
||||
# Docs: https://www.netlify.com/docs/netlify-toml-reference/
|
||||
|
||||
# Essential site config
|
||||
[build]
|
||||
base = "/"
|
||||
command = "yarn build"
|
||||
publish = "dist"
|
||||
functions = "services/serverless-functions"
|
||||
|
||||
# Site info, used for the 1-Click deploy page
|
||||
[template.environment]
|
||||
STATUSKIT_PAGE_TITLE = "Dashy"
|
||||
STATUSKIT_COMPANY_LOGO = "https://raw.githubusercontent.com/Lissy93/dashy/master/docs/assets/logo.png"
|
||||
STATUSKIT_SUPPORT_CONTACT_LINK = "https://github.com/lissy93/dashy"
|
||||
STATUSKIT_RESOURCES_LINK = "https://dashy.to/docs"
|
||||
|
||||
# Redirect the Node endpoints to serverless functions
|
||||
[[redirects]]
|
||||
from = "/status-check"
|
||||
to = "/.netlify/functions/cloud-status-check"
|
||||
status = 301
|
||||
force = true
|
||||
[[redirects]]
|
||||
from = "/config-manager/*"
|
||||
to = "/.netlify/functions/not-supported"
|
||||
status = 301
|
||||
force = true
|
||||
[[redirects]]
|
||||
from = "/cors-proxy"
|
||||
to = "/.netlify/functions/netlify-cors"
|
||||
status = 301
|
||||
force = true
|
||||
|
||||
# For router history mode, ensure pages land on index
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "/index.html"
|
||||
status = 200
|
||||
|
||||
# Set any security headers here
|
||||
[[headers]]
|
||||
for = "/*"
|
||||
[headers.values]
|
||||
# Uncomment to enable Netlify user control. You must have a paid plan.
|
||||
# Basic-Auth = "someuser:somepassword anotheruser:anotherpassword"
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Dashy",
|
||||
"version": "1.9.5",
|
||||
"version": "1.9.6",
|
||||
"license": "MIT",
|
||||
"main": "server",
|
||||
"author": "Alicia Sykes <alicia@omg.lol> (https://aliciasykes.com)",
|
||||
|
@ -32,7 +32,7 @@ module.exports = (req, res) => {
|
||||
// Prepare the request
|
||||
const requestConfig = {
|
||||
method: req.method,
|
||||
url: targetURL + req.url,
|
||||
url: targetURL,
|
||||
json: req.body,
|
||||
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",
|
||||
"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": {
|
||||
"uptime": "Uptime"
|
||||
},
|
||||
|
@ -17,9 +17,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||
import { widgetApiEndpoints, serviceEndpoints } from '@/utils/defaults';
|
||||
import { widgetApiEndpoints } from '@/utils/defaults';
|
||||
import { capitalize, timestampToDateTime } from '@/utils/MiscHelpers';
|
||||
|
||||
export default {
|
||||
@ -48,10 +47,6 @@ export default {
|
||||
if (this.options.host) return `${this.options.host}/api/v1/checks`;
|
||||
return `${widgetApiEndpoints.healthChecks}`;
|
||||
},
|
||||
proxyReqEndpoint() {
|
||||
const baseUrl = process.env.VUE_APP_DOMAIN || window.location.origin;
|
||||
return `${baseUrl}${serviceEndpoints.corsProxy}`;
|
||||
},
|
||||
apiKey() {
|
||||
if (!this.options.apiKey) {
|
||||
this.error('An API key is required, please see the docs for more info');
|
||||
@ -62,23 +57,11 @@ export default {
|
||||
methods: {
|
||||
/* Make GET request to CoinGecko API endpoint */
|
||||
fetchData() {
|
||||
const requestConfig = {
|
||||
method: 'GET',
|
||||
url: this.proxyReqEndpoint,
|
||||
headers: {
|
||||
'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();
|
||||
});
|
||||
this.overrideProxyChoice = true;
|
||||
const authHeaders = { 'X-Api-Key': this.apiKey };
|
||||
this.makeRequest(this.endpoint, authHeaders).then(
|
||||
(response) => { this.processData(response); },
|
||||
);
|
||||
},
|
||||
/* Assign data variables to the returned data */
|
||||
processData(data) {
|
||||
|
@ -3,20 +3,12 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||
import ChartingMixin from '@/mixins/ChartingMixin';
|
||||
|
||||
export default {
|
||||
mixins: [WidgetMixin, ChartingMixin],
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
chartTitle: null,
|
||||
chartData: null,
|
||||
chartDom: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
/* URL where NetData is hosted */
|
||||
netDataHost() {
|
||||
@ -41,50 +33,44 @@ export default {
|
||||
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();
|
||||
});
|
||||
this.makeRequest(this.endpoint).then(
|
||||
(response) => { this.processData(response); },
|
||||
);
|
||||
},
|
||||
/* 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] * 1000));
|
||||
systemCpu.push(reading[2]);
|
||||
userCpu.push(reading[3]);
|
||||
processData(inputData) {
|
||||
const { labels, data } = inputData;
|
||||
const timeData = []; // List of timestamps for axis
|
||||
const resultGroup = {}; // List of datasets, for each label
|
||||
data.reverse().forEach((reading) => {
|
||||
labels.forEach((label, indx) => {
|
||||
if (indx === 0) { // First value is the timestamp, add to axis
|
||||
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,
|
||||
datasets: [
|
||||
{ name: 'System CPU', type: 'bar', values: systemCpu },
|
||||
{ name: 'User CPU', type: 'bar', values: userCpu },
|
||||
],
|
||||
};
|
||||
this.chartTitle = this.makeChartTitle(data.data);
|
||||
this.renderChart();
|
||||
const datasets = [];
|
||||
Object.keys(resultGroup).forEach((label) => {
|
||||
datasets.push({ name: label, type: 'bar', values: resultGroup[label] });
|
||||
});
|
||||
const timeChartData = { labels: timeData, datasets };
|
||||
const chartTitle = this.makeChartTitle(data);
|
||||
this.generateChart(timeChartData, chartTitle);
|
||||
},
|
||||
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);
|
||||
return `Past ${diff} minutes`;
|
||||
},
|
||||
renderChart() {
|
||||
this.chartDom = this.generateChart();
|
||||
return `${prefix}: Past ${diff} minutes`;
|
||||
},
|
||||
/* Create new chart, using the crypto data */
|
||||
generateChart() {
|
||||
generateChart(timeChartData, chartTitle) {
|
||||
return new this.Chart(`#${this.chartId}`, {
|
||||
title: this.chartTitle,
|
||||
data: this.chartData,
|
||||
title: chartTitle,
|
||||
data: timeChartData,
|
||||
type: 'axis-mixed',
|
||||
height: this.chartHeight,
|
||||
colors: this.chartColors,
|
||||
|
@ -10,13 +10,6 @@ import ChartingMixin from '@/mixins/ChartingMixin';
|
||||
export default {
|
||||
mixins: [WidgetMixin, ChartingMixin],
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
chartTitle: null,
|
||||
chartData: null,
|
||||
chartDom: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
/* URL where NetData is hosted */
|
||||
netDataHost() {
|
||||
@ -31,7 +24,7 @@ export default {
|
||||
return this.options.apiVersion || 'v1';
|
||||
},
|
||||
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 */
|
||||
chartId() {
|
||||
@ -64,7 +57,7 @@ export default {
|
||||
load5mins.push(reading[2]);
|
||||
load15mins.push(reading[3]);
|
||||
});
|
||||
this.chartData = {
|
||||
const chartData = {
|
||||
labels: timeData,
|
||||
datasets: [
|
||||
{ name: '1 Min', type: 'bar', values: load1min },
|
||||
@ -72,22 +65,20 @@ export default {
|
||||
{ name: '15 Mins', type: 'bar', values: load15mins },
|
||||
],
|
||||
};
|
||||
this.chartTitle = this.makeChartTitle(data.data);
|
||||
this.renderChart();
|
||||
const chartTitle = this.makeChartTitle(data.data);
|
||||
this.generateChart(chartData, chartTitle);
|
||||
},
|
||||
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);
|
||||
return `Past ${diff} minutes`;
|
||||
},
|
||||
renderChart() {
|
||||
this.chartDom = this.generateChart();
|
||||
return `${prefix}: Past ${diff} minutes`;
|
||||
},
|
||||
/* Create new chart, using the crypto data */
|
||||
generateChart() {
|
||||
generateChart(chartData, chartTitle) {
|
||||
return new this.Chart(`#${this.chartId}`, {
|
||||
title: this.chartTitle,
|
||||
data: this.chartData,
|
||||
title: chartTitle,
|
||||
data: chartData,
|
||||
type: 'axis-mixed',
|
||||
height: this.chartHeight,
|
||||
colors: this.chartColors,
|
||||
|
@ -6,7 +6,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||
import ChartingMixin from '@/mixins/ChartingMixin';
|
||||
|
||||
@ -37,16 +36,9 @@ export default {
|
||||
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();
|
||||
});
|
||||
this.makeRequest(this.endpoint).then(
|
||||
(response) => { this.processData(response); },
|
||||
);
|
||||
},
|
||||
/* Assign data variables to the returned data */
|
||||
processData(inputData) {
|
||||
@ -86,7 +78,7 @@ export default {
|
||||
/* Create new chart, using the crypto data */
|
||||
generateHistoryChart(timeChartData) {
|
||||
return new this.Chart(`#${this.chartId}`, {
|
||||
title: 'History',
|
||||
title: this.$t('widgets.net-data.mem-chart-title'),
|
||||
data: timeChartData,
|
||||
type: 'axis-mixed',
|
||||
height: this.chartHeight,
|
||||
@ -107,7 +99,7 @@ export default {
|
||||
},
|
||||
generateAggregateChart(aggregateChartData) {
|
||||
return new this.Chart(`#aggregate-${this.chartId}`, {
|
||||
title: 'Averages',
|
||||
title: this.$t('widgets.net-data.mem-breakdown-title'),
|
||||
data: aggregateChartData,
|
||||
type: 'percentage',
|
||||
height: 100,
|
||||
|
@ -18,7 +18,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
// import axios from 'axios';
|
||||
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||
import ChartingMixin from '@/mixins/ChartingMixin';
|
||||
import { capitalize } from '@/utils/MiscHelpers';
|
||||
@ -55,15 +55,9 @@ export default {
|
||||
methods: {
|
||||
/* Make GET request to local pi-hole instance */
|
||||
fetchData() {
|
||||
axios.get(this.endpoint)
|
||||
this.makeRequest(this.endpoint)
|
||||
.then((response) => {
|
||||
this.processData(response.data);
|
||||
})
|
||||
.catch((dataFetchError) => {
|
||||
this.error('Unable to fetch data', dataFetchError);
|
||||
})
|
||||
.finally(() => {
|
||||
this.finishLoading();
|
||||
this.processData(response);
|
||||
});
|
||||
},
|
||||
/* Assign data variables to the returned data */
|
||||
|
@ -11,7 +11,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||
import { showNumAsThousand } from '@/utils/MiscHelpers';
|
||||
|
||||
@ -46,19 +45,13 @@ export default {
|
||||
methods: {
|
||||
/* Make GET request to local pi-hole instance */
|
||||
fetchData() {
|
||||
axios.get(this.endpoint)
|
||||
this.makeRequest(this.endpoint)
|
||||
.then((response) => {
|
||||
if (Array.isArray(response.data)) {
|
||||
if (Array.isArray(response)) {
|
||||
this.error('Got success, but found no results, possible authorization error');
|
||||
} 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 */
|
||||
|
@ -3,7 +3,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||
import ChartingMixin from '@/mixins/ChartingMixin';
|
||||
|
||||
@ -31,17 +30,23 @@ export default {
|
||||
methods: {
|
||||
/* Make GET request to local pi-hole instance */
|
||||
fetchData() {
|
||||
axios.get(this.endpoint)
|
||||
this.makeRequest(this.endpoint)
|
||||
.then((response) => {
|
||||
this.processData(response.data);
|
||||
})
|
||||
.catch((dataFetchError) => {
|
||||
this.error('Unable to fetch data', dataFetchError);
|
||||
})
|
||||
.finally(() => {
|
||||
this.finishLoading();
|
||||
if (this.validate(response)) {
|
||||
this.processData(response);
|
||||
}
|
||||
});
|
||||
},
|
||||
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 */
|
||||
processData(data) {
|
||||
const timeData = [];
|
||||
|
@ -319,7 +319,10 @@ export default {
|
||||
},
|
||||
/* Returns users specified widget options, or empty object */
|
||||
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 */
|
||||
widgetRef() {
|
||||
|
@ -2,8 +2,10 @@
|
||||
* Mixin that all pre-built and custom widgets extend from.
|
||||
* Manages loading state, error handling, data updates and user options
|
||||
*/
|
||||
import axios from 'axios';
|
||||
import ProgressBar from 'rsup-progress';
|
||||
import ErrorHandler from '@/utils/ErrorHandler';
|
||||
import { serviceEndpoints } from '@/utils/defaults';
|
||||
|
||||
const WidgetMixin = {
|
||||
props: {
|
||||
@ -14,11 +16,21 @@ const WidgetMixin = {
|
||||
},
|
||||
data: () => ({
|
||||
progress: new ProgressBar({ color: 'var(--progress-bar)' }),
|
||||
overrideProxyChoice: false,
|
||||
}),
|
||||
/* When component mounted, fetch initial data */
|
||||
mounted() {
|
||||
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: {
|
||||
/* Re-fetches external data, called by parent. Usually overridden by widget */
|
||||
update() {
|
||||
@ -44,9 +56,36 @@ const WidgetMixin = {
|
||||
fetchData() {
|
||||
this.finishLoading();
|
||||
},
|
||||
/* Used as v-tooltip, pass text content in, and will show on hover */
|
||||
tooltip(content) {
|
||||
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