🥅 Adds graceful error hadling to widgets

This commit is contained in:
Alicia Sykes 2021-12-13 21:40:13 +00:00
parent 19d3c03001
commit 0a4d021b4e
13 changed files with 148 additions and 79 deletions

View File

@ -6,7 +6,6 @@
import { Chart } from 'frappe-charts/dist/frappe-charts.min.esm';
import axios from 'axios';
import WidgetMixin from '@/mixins/WidgetMixin';
import ErrorHandler from '@/utils/ErrorHandler';
import { widgetApiEndpoints } from '@/utils/defaults';
export default {
@ -67,6 +66,7 @@ export default {
methods: {
/* Extends mixin, and updates data. Called by parent component */
update() {
this.startLoading();
this.fetchData();
},
/* Create new chart, using the crypto data */
@ -98,11 +98,14 @@ export default {
try {
this.lineStatuses = this.processData(response.data);
} catch (chartingError) {
ErrorHandler('Unable to plot results on chart', chartingError);
this.error('Unable to plot results on chart', chartingError);
}
})
.catch((dataFetchError) => {
ErrorHandler('Unable to fetch crypto data', dataFetchError);
this.error('Unable to fetch crypto data', dataFetchError);
})
.finally(() => {
this.finishLoading();
});
},
/* Generate price history in a format that can be consumed by the chart

View File

@ -21,7 +21,6 @@
<script>
import axios from 'axios';
import WidgetMixin from '@/mixins/WidgetMixin';
import ErrorHandler from '@/utils/ErrorHandler';
import { widgetApiEndpoints } from '@/utils/defaults';
import { findCurrencySymbol, convertTimestampToDate } from '@/utils/MiscHelpers';
@ -39,7 +38,9 @@ export default {
computed: {
/* The crypto assets to fetch price data for */
assets() {
return this.options.assets.join(',');
const usersChoice = this.options.assets;
if (!usersChoice) return '';
return usersChoice.join(',');
},
/* The fiat currency to calculate price data in */
currency() {
@ -47,6 +48,11 @@ export default {
if (typeof userChoice === 'string') return userChoice;
return 'USD';
},
limit() {
const userChoice = this.options.limit;
if (userChoice && !Number.isNaN(userChoice) && userChoice > 0) return userChoice;
return 100;
},
/* How results should be sorted */
order() {
const userChoice = this.options.sortBy;
@ -60,7 +66,7 @@ export default {
/* The formatted GET request API endpoint to fetch crypto data from */
endpoint() {
return `${widgetApiEndpoints.cryptoWatchList}?`
+ `ids=${this.assets}&vs_currency=${this.currency}&order=${this.order}`;
+ `ids=${this.assets}&vs_currency=${this.currency}&order=${this.order}&per_page=${this.limit}`;
},
},
filters: {
@ -70,6 +76,7 @@ export default {
},
/* Append percentage symbol, and up/ down arrow */
percentage(change) {
if (!change) return '';
const symbol = change > 0 ? '↑' : '↓';
return `${symbol} ${change.toFixed(2)}%`;
},
@ -77,6 +84,7 @@ export default {
methods: {
/* Extends mixin, and updates data. Called by parent component */
update() {
this.startLoading();
this.fetchData();
},
/* Make GET request to CoinGecko API endpoint */
@ -86,7 +94,10 @@ export default {
this.processData(response.data);
})
.catch((error) => {
ErrorHandler('Unable to fetch crypto watch list', error);
this.error('Unable to fetch crypto watch list', error);
})
.finally(() => {
this.finishLoading();
});
},
/* Convert response data into JSON to be consumed by the UI */

View File

@ -13,7 +13,6 @@
<script>
import axios from 'axios';
import WidgetMixin from '@/mixins/WidgetMixin';
import ErrorHandler from '@/utils/ErrorHandler';
import { widgetApiEndpoints } from '@/utils/defaults';
import { findCurrencySymbol } from '@/utils/MiscHelpers';
@ -54,6 +53,7 @@ export default {
methods: {
/* Extends mixin, and updates data. Called by parent component */
update() {
this.startLoading();
this.fetchData();
},
/* Make GET request to CoinGecko API endpoint */
@ -62,7 +62,10 @@ export default {
.then(response => {
this.processData(response.data);
}).catch(error => {
ErrorHandler('Unable to fetch or process exchange rate data', error);
this.error('Unable to fetch or process exchange rate data', error);
})
.finally(() => {
this.finishLoading();
});
},
/* Assign data variables to the returned data */

View File

@ -6,7 +6,6 @@
<script>
import WidgetMixin from '@/mixins/WidgetMixin';
import ErrorHandler from '@/utils/ErrorHandler';
export default {
mixins: [WidgetMixin],
@ -15,7 +14,7 @@ export default {
frameUrl() {
const usersChoice = this.options.url;
if (!usersChoice || typeof usersChoice !== 'string') {
ErrorHandler('Iframe widget expects a URL');
this.error('Iframe widget expects a URL');
return null;
}
return usersChoice;
@ -28,7 +27,9 @@ export default {
methods: {
/* Refreshes iframe contents, called by parent */
update() {
this.startLoading();
(document.getElementById(this.frameId) || {}).src = this.frameUrl;
this.finishLoading();
},
},
};

View File

@ -8,7 +8,6 @@
<script>
import axios from 'axios';
import WidgetMixin from '@/mixins/WidgetMixin';
import ErrorHandler from '@/utils/ErrorHandler';
import { widgetApiEndpoints } from '@/utils/defaults';
export default {
@ -56,6 +55,7 @@ export default {
methods: {
/* Extends mixin, and updates data. Called by parent component */
update() {
this.startLoading();
this.fetchData();
},
/* Make GET request to Jokes API endpoint */
@ -63,12 +63,15 @@ export default {
axios.get(this.endpoint)
.then((response) => {
if (response.data.error) {
ErrorHandler('No matching jokes returned', response.data.additionalInfo);
this.error('No matching jokes returned', response.data.additionalInfo);
}
this.processData(response.data);
})
.catch((dataFetchError) => {
ErrorHandler('Unable to fetch any jokes', dataFetchError);
this.error('Unable to fetch any jokes', dataFetchError);
})
.finally(() => {
this.finishLoading();
});
},
/* Assign data variables to the returned data */

View File

@ -6,7 +6,6 @@
import { Chart } from 'frappe-charts/dist/frappe-charts.min.esm';
import axios from 'axios';
import WidgetMixin from '@/mixins/WidgetMixin';
import ErrorHandler from '@/utils/ErrorHandler';
import { widgetApiEndpoints } from '@/utils/defaults';
export default {
@ -74,6 +73,7 @@ export default {
methods: {
/* Extends mixin, and updates data. Called by parent component */
update() {
this.startLoading();
this.fetchData();
},
/* Create new chart, using the crypto data */
@ -103,15 +103,18 @@ export default {
axios.get(this.endpoint)
.then((response) => {
if (response.data.note) {
ErrorHandler('API Error', response.data.Note);
this.error('API Error', response.data.Note);
} else if (response.data['Error Message']) {
ErrorHandler('API Error', response.data['Error Message']);
this.error('API Error', response.data['Error Message']);
} else {
this.processData(response.data);
}
})
.catch((error) => {
ErrorHandler('Unable to fetch stock price data', error);
this.error('Unable to fetch stock price data', error);
})
.finally(() => {
this.finishLoading();
});
},
/* Convert data returned by API into a format that can be consumed by the chart

View File

@ -25,7 +25,6 @@
<script>
import axios from 'axios';
import WidgetMixin from '@/mixins/WidgetMixin';
import ErrorHandler from '@/utils/ErrorHandler';
import { widgetApiEndpoints } from '@/utils/defaults';
export default {
@ -52,6 +51,7 @@ export default {
methods: {
/* Extends mixin, and updates data. Called by parent component */
update() {
this.startLoading();
this.fetchData();
},
/* Makes GET request to the TFL API */
@ -61,7 +61,10 @@ export default {
this.lineStatuses = this.processData(response.data);
})
.catch(() => {
ErrorHandler('Unable to fetch data from TFL API');
this.error('Unable to fetch data from TFL API');
})
.finally(() => {
this.finishLoading();
});
},
/* Processes the results to be rendered by the UI */
@ -97,7 +100,7 @@ export default {
const chosenLines = usersLines.map(name => name.toLowerCase());
const filtered = allLines.filter((line) => chosenLines.includes(line.line.toLowerCase()));
if (filtered.length < 1) {
ErrorHandler('No TFL lines match your filter');
this.error('No TFL lines match your filter');
return allLines;
}
return filtered;

View File

@ -26,7 +26,6 @@
<script>
import axios from 'axios';
import WidgetMixin from '@/mixins/WidgetMixin';
import ErrorHandler from '@/utils/ErrorHandler';
import { widgetApiEndpoints } from '@/utils/defaults';
export default {
@ -34,7 +33,6 @@ export default {
data() {
return {
loading: true,
error: false,
icon: null,
description: null,
temp: null,
@ -68,6 +66,7 @@ export default {
methods: {
/* Extends mixin, and updates data. Called by parent component */
update() {
this.startLoading();
this.fetchWeather();
},
/* Adds units symbol to temperature, depending on metric or imperial */
@ -87,8 +86,11 @@ export default {
this.makeWeatherData(data);
}
})
.catch(() => {
this.throwError('Failed to fetch weather');
.catch((error) => {
this.throwError('Failed to fetch weather', error);
})
.finally(() => {
this.finishLoading();
});
},
/* If showing additional info, then generate this data too */
@ -131,9 +133,8 @@ export default {
return valid;
},
/* Just outputs an error message */
throwError(error) {
ErrorHandler(error);
this.error = true;
throwError(msg, error) {
this.error(msg, error);
},
},
created() {

View File

@ -33,7 +33,6 @@
<script>
import axios from 'axios';
import WidgetMixin from '@/mixins/WidgetMixin';
import ErrorHandler from '@/utils/ErrorHandler';
import { widgetApiEndpoints } from '@/utils/defaults';
export default {
@ -41,7 +40,6 @@ export default {
data() {
return {
loading: true,
error: false,
showDetails: false,
weatherData: [],
moreInfo: [],
@ -77,6 +75,7 @@ export default {
methods: {
/* Extends mixin, and updates data. Called by parent component */
update() {
this.startLoading();
this.fetchWeather();
},
/* Adds units symbol to temperature, depending on metric or imperial */
@ -97,8 +96,11 @@ export default {
this.processApiResults(response.data);
}
})
.catch(() => {
this.throwError('Failed to fetch weather');
.catch((error) => {
this.error('Failed to fetch weather', error);
})
.finally(() => {
this.finishLoading();
});
},
/* Process the results from the Axios request */
@ -154,24 +156,19 @@ export default {
const ops = this.options;
let valid = true;
if (!ops.apiKey) {
this.throwError('Missing API key for OpenWeatherMap');
this.error('Missing API key for OpenWeatherMap');
valid = false;
}
if (!ops.city) {
this.throwError('A city name is required to fetch weather');
this.error('A city name is required to fetch weather');
valid = false;
}
if (ops.units && ops.units !== 'metric' && ops.units !== 'imperial') {
this.throwError('Invalid units specified, must be either \'metric\' or \'imperial\'');
this.error('Invalid units specified, must be either \'metric\' or \'imperial\'');
valid = false;
}
return valid;
},
/* Just outputs an error message */
throwError(error) {
ErrorHandler(error);
this.error = true;
},
},
/* When the widget loads, the props are checked, and weather fetched */
created() {

View File

@ -1,61 +1,80 @@
<template>
<div class="widget-base">
<Button :click="update" class="update-btn">
<Button :click="update" class="action-btn update-btn" v-if="!error && !loading">
<UpdateIcon />
</Button>
<Button :click="fullScreenWidget" class="action-btn open-btn" v-if="!error && !loading">
<OpenIcon />
</Button>
<div v-if="loading">Loading...</div>
<div v-else-if="error" class="widget-error">
<p class="error-msg">An error occurred, see the logs for more info.</p>
<p class="error-output">{{ errorMsg }}</p>
</div>
<Clock
v-if="widgetType === 'clock'"
v-else-if="widgetType === 'clock'"
:options="widgetOptions"
@error="handleError"
:ref="widgetRef"
/>
<Weather
v-else-if="widgetType === 'weather'"
:options="widgetOptions"
@error="handleError"
:ref="widgetRef"
/>
<WeatherForecast
v-else-if="widgetType === 'weather-forecast'"
:options="widgetOptions"
@error="handleError"
:ref="widgetRef"
/>
<TflStatus
v-else-if="widgetType === 'tfl-status'"
:options="widgetOptions"
@error="handleError"
:ref="widgetRef"
/>
<CryptoPriceChart
v-else-if="widgetType === 'crypto-price-chart'"
:options="widgetOptions"
@error="handleError"
:ref="widgetRef"
/>
<CryptoWatchList
v-else-if="widgetType === 'crypto-watch-list'"
:options="widgetOptions"
@error="handleError"
:ref="widgetRef"
/>
<XkcdComic
v-else-if="widgetType === 'xkcd-comic'"
:options="widgetOptions"
@error="handleError"
:ref="widgetRef"
/>
<ExchangeRates
v-else-if="widgetType === 'exchange-rates'"
:options="widgetOptions"
@error="handleError"
:ref="widgetRef"
/>
<StockPriceChart
v-else-if="widgetType === 'stock-price-chart'"
:options="widgetOptions"
@error="handleError"
:ref="widgetRef"
/>
<Jokes
v-else-if="widgetType === 'joke'"
:options="widgetOptions"
@error="handleError"
:ref="widgetRef"
/>
<IframeWidget
v-else-if="widgetType === 'iframe'"
:options="widgetOptions"
@error="handleError"
:ref="widgetRef"
/>
</div>
@ -65,6 +84,7 @@
import ErrorHandler from '@/utils/ErrorHandler';
import Button from '@/components/FormElements/Button';
import UpdateIcon from '@/assets/interface-icons/widget-update.svg';
import OpenIcon from '@/assets/interface-icons/open-new-tab.svg';
import Clock from '@/components/Widgets/Clock.vue';
import Weather from '@/components/Widgets/Weather.vue';
@ -83,6 +103,7 @@ export default {
components: {
Button,
UpdateIcon,
OpenIcon,
Clock,
Weather,
WeatherForecast,
@ -99,6 +120,11 @@ export default {
widget: Object,
index: Number,
},
data: () => ({
loading: false,
error: false,
errorMsg: null,
}),
computed: {
/* Returns the widget type, shows error if not specified */
widgetType() {
@ -120,6 +146,13 @@ export default {
update() {
this.$refs[this.widgetRef].update();
},
handleError(msg) {
this.error = true;
this.errorMsg = msg;
},
fullScreenWidget() {
this.$emit('navigateToSection');
},
},
};
</script>
@ -129,15 +162,14 @@ export default {
.widget-base {
position: relative;
padding-top: 0.75rem;
button.update-btn {
height: 1.5rem;
button.action-btn {
height: 1rem;
min-width: auto;
width: 2rem;
width: 1.75rem;
margin: 0;
padding: 0.1rem 0;
position: absolute;
right: -0.25rem;
top: -0.25rem;
top: 0;
border: none;
opacity: var(--dimming-factor);
color: var(--widget-text-color);
@ -145,6 +177,27 @@ export default {
opacity: 1;
color: var(--widget-background-color);
}
&.update-btn {
right: -0.25rem;
}
&.open-btn {
right: 1.75rem;
}
}
.widget-error {
p.error-msg {
color: var(--warning);
font-weight: bold;
font-size: 1rem;
margin: 0 auto 0.5rem auto;
}
p.error-output {
font-family: var(--font-monospace);
color: var(--widget-text-color);
font-size: 0.85rem;
margin: 0.5rem auto;
}
}
}

View File

@ -1,30 +0,0 @@
<template>
<div>
<WidgetBase
v-for="(widget, widgetIndex) in widgets"
:key="widgetIndex"
:widget="widget"
:index="widgetIndex"
/>
</div>
</template>
<script>
import WidgetBase from '@/components/Widgets/WidgetBase';
export default {
name: 'WidgetGroup',
components: {
WidgetBase,
},
props: {
widgets: Array,
},
computed: {},
};
</script>
<style scoped lang="scss">
@import '@/styles/media-queries.scss';
</style>

View File

@ -10,7 +10,6 @@
<script>
import axios from 'axios';
import WidgetMixin from '@/mixins/WidgetMixin';
import ErrorHandler from '@/utils/ErrorHandler';
import { widgetApiEndpoints } from '@/utils/defaults';
export default {
@ -47,6 +46,7 @@ export default {
methods: {
/* Extends mixin, and updates data. Called by parent component */
update() {
this.startLoading();
this.fetchData();
},
/* Make GET request to CoinGecko API endpoint */
@ -56,7 +56,10 @@ export default {
this.processData(response.data);
})
.catch((dataFetchError) => {
ErrorHandler('Unable to fetch data', dataFetchError);
this.error('Unable to fetch data', dataFetchError);
})
.finally(() => {
this.finishLoading();
});
},
/* Assign data variables to the returned data */

View File

@ -1,3 +1,5 @@
import ProgressBar from 'rsup-progress';
import ErrorHandler from '@/utils/ErrorHandler';
import LoadingAnimation from '@/assets/interface-icons/loader.svg';
const WidgetMixin = {
@ -13,6 +15,7 @@ const WidgetMixin = {
},
data: () => ({
loading: true, // Indicates current loading status, to display spinner
progress: new ProgressBar({ color: 'var(--progress-bar)' }),
}),
methods: {
/* Overridden by widget component. Re-fetches and renders any external data *
@ -21,6 +24,21 @@ const WidgetMixin = {
// eslint-disable-next-line no-console
console.log('No update method configured for this widget');
},
/* Called when an error occurs */
error(msg, stackTrace) {
ErrorHandler(msg, stackTrace);
this.$emit('error', msg);
},
/* When a data request update starts, show loader */
startLoading() {
this.loading = true;
this.progress.start();
},
/* When a data request finishes, hide loader */
finishLoading() {
this.loading = false;
setTimeout(() => { this.progress.end(); }, 500);
},
},
mounted() {
// If the mounted function isn't overridden,then hide loader