mirror of https://github.com/Lissy93/dashy.git
🥅 Better error handling when config cannot be found
This commit is contained in:
parent
f295958c44
commit
ecef01b034
|
@ -3,6 +3,7 @@
|
||||||
<EditModeTopBanner v-if="isEditMode" />
|
<EditModeTopBanner v-if="isEditMode" />
|
||||||
<LoadingScreen :isLoading="isLoading" v-if="shouldShowSplash" />
|
<LoadingScreen :isLoading="isLoading" v-if="shouldShowSplash" />
|
||||||
<Header :pageInfo="pageInfo" />
|
<Header :pageInfo="pageInfo" />
|
||||||
|
<CriticalError />
|
||||||
<router-view v-if="!isFetching" />
|
<router-view v-if="!isFetching" />
|
||||||
<Footer :text="footerText" v-if="visibleComponents.footer && !isFetching" />
|
<Footer :text="footerText" v-if="visibleComponents.footer && !isFetching" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,6 +13,7 @@
|
||||||
import Header from '@/components/PageStrcture/Header.vue';
|
import Header from '@/components/PageStrcture/Header.vue';
|
||||||
import Footer from '@/components/PageStrcture/Footer.vue';
|
import Footer from '@/components/PageStrcture/Footer.vue';
|
||||||
import EditModeTopBanner from '@/components/InteractiveEditor/EditModeTopBanner.vue';
|
import EditModeTopBanner from '@/components/InteractiveEditor/EditModeTopBanner.vue';
|
||||||
|
import CriticalError from '@/components/PageStrcture/CriticalError.vue';
|
||||||
import LoadingScreen from '@/components/PageStrcture/LoadingScreen.vue';
|
import LoadingScreen from '@/components/PageStrcture/LoadingScreen.vue';
|
||||||
import { welcomeMsg } from '@/utils/CoolConsole';
|
import { welcomeMsg } from '@/utils/CoolConsole';
|
||||||
import ErrorHandler from '@/utils/ErrorHandler';
|
import ErrorHandler from '@/utils/ErrorHandler';
|
||||||
|
@ -29,6 +31,7 @@ export default {
|
||||||
Footer,
|
Footer,
|
||||||
LoadingScreen,
|
LoadingScreen,
|
||||||
EditModeTopBanner,
|
EditModeTopBanner,
|
||||||
|
CriticalError,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -0,0 +1,115 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="critical-error-wrap" v-if="shouldShow">
|
||||||
|
<h3>Configuration Load Error</h3>
|
||||||
|
<p>
|
||||||
|
It looks like there was an error loading the configuration.<br>
|
||||||
|
</p>
|
||||||
|
<p>Please ensure that:</p>
|
||||||
|
<ul>
|
||||||
|
<li>The configuration file can be found at the specified location</li>
|
||||||
|
<li>There are no CORS rules preventing client-side access</li>
|
||||||
|
<li>The YAML is valid, parsable and matches the schema</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
You can check the browser console for more details.<br>
|
||||||
|
If this issue persists, open a ticket on our GitHub.
|
||||||
|
</p>
|
||||||
|
<h4>Error Details:</h4>
|
||||||
|
<p class="the-error">{{ this.$store.state.criticalError }}</p>
|
||||||
|
|
||||||
|
<button class="user-doesnt-care" @click="ignoreWarning">Ignore Error</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { localStorageKeys } from '@/utils/defaults';
|
||||||
|
import Keys from '@/utils/StoreMutations';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CriticalError',
|
||||||
|
props: {
|
||||||
|
text: String,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
shouldShow() {
|
||||||
|
return this.$store.state.criticalError
|
||||||
|
&& !localStorage[localStorageKeys.DISABLE_CRITICAL_WARNING];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
ignoreWarning() {
|
||||||
|
this.$store.commit(Keys.CRITICAL_ERROR_MSG, null);
|
||||||
|
localStorage.setItem(localStorageKeys.DISABLE_CRITICAL_WARNING, true);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import '@/styles/media-queries.scss';
|
||||||
|
.critical-error-wrap {
|
||||||
|
position: absolute;
|
||||||
|
top: 30%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 2;
|
||||||
|
background: var(--background-darker);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: var(--curve-factor);
|
||||||
|
color: var(--danger);
|
||||||
|
border: 2px solid var(--danger);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0.95;
|
||||||
|
gap: 0.5rem;
|
||||||
|
@include tablet-down {
|
||||||
|
top: 50%;
|
||||||
|
width: 85vw;
|
||||||
|
}
|
||||||
|
p, ul, h4 {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
h4 {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
text-align: center;
|
||||||
|
background: var(--danger);
|
||||||
|
color: white;
|
||||||
|
margin: -1rem -1rem 1rem -1rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
padding-left: 1rem;
|
||||||
|
}
|
||||||
|
.the-error {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
.user-doesnt-care {
|
||||||
|
background: var(--background-darker);
|
||||||
|
color: var(--white);
|
||||||
|
border-radius: var(--curve-factor);
|
||||||
|
border: none;
|
||||||
|
text-decoration: underline;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
width: fit-content;
|
||||||
|
margin: 0 auto;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
&:hover {
|
||||||
|
background: var(--danger);
|
||||||
|
color: var(--background-darker);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
50
src/store.js
50
src/store.js
|
@ -41,6 +41,7 @@ const {
|
||||||
INSERT_ITEM,
|
INSERT_ITEM,
|
||||||
UPDATE_CUSTOM_CSS,
|
UPDATE_CUSTOM_CSS,
|
||||||
CONF_MENU_INDEX,
|
CONF_MENU_INDEX,
|
||||||
|
CRITICAL_ERROR_MSG,
|
||||||
} = Keys;
|
} = Keys;
|
||||||
|
|
||||||
const store = new Vuex.Store({
|
const store = new Vuex.Store({
|
||||||
|
@ -51,6 +52,7 @@ const store = new Vuex.Store({
|
||||||
modalOpen: false, // KB shortcut functionality will be disabled when modal is open
|
modalOpen: false, // KB shortcut functionality will be disabled when modal is open
|
||||||
currentConfigInfo: {}, // For multi-page support, will store info about config file
|
currentConfigInfo: {}, // For multi-page support, will store info about config file
|
||||||
isUsingLocalConfig: false, // If true, will use local config instead of fetched
|
isUsingLocalConfig: false, // If true, will use local config instead of fetched
|
||||||
|
criticalError: null, // Will store a message, if a critical error occurs
|
||||||
navigateConfToTab: undefined, // Used to switch active tab in config modal
|
navigateConfToTab: undefined, // Used to switch active tab in config modal
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
|
@ -174,6 +176,10 @@ const store = new Vuex.Store({
|
||||||
state.editMode = editMode;
|
state.editMode = editMode;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
[CRITICAL_ERROR_MSG](state, message) {
|
||||||
|
if (message) ErrorHandler(message);
|
||||||
|
state.criticalError = message;
|
||||||
|
},
|
||||||
[UPDATE_ITEM](state, payload) {
|
[UPDATE_ITEM](state, payload) {
|
||||||
const { itemId, newItem } = payload;
|
const { itemId, newItem } = payload;
|
||||||
const newConfig = { ...state.config };
|
const newConfig = { ...state.config };
|
||||||
|
@ -320,16 +326,38 @@ const store = new Vuex.Store({
|
||||||
actions: {
|
actions: {
|
||||||
/* Fetches the root config file, only ever called by INITIALIZE_CONFIG */
|
/* Fetches the root config file, only ever called by INITIALIZE_CONFIG */
|
||||||
async [INITIALIZE_ROOT_CONFIG]({ commit }) {
|
async [INITIALIZE_ROOT_CONFIG]({ commit }) {
|
||||||
// Load and parse config from root config file
|
|
||||||
const configFilePath = process.env.VUE_APP_CONFIG_PATH || '/conf.yml';
|
const configFilePath = process.env.VUE_APP_CONFIG_PATH || '/conf.yml';
|
||||||
const data = await yaml.load((await axios.get(configFilePath)).data);
|
try {
|
||||||
// Replace missing root properties with empty objects
|
// Attempt to fetch the YAML file
|
||||||
if (!data.appConfig) data.appConfig = {};
|
const response = await axios.get(configFilePath);
|
||||||
if (!data.pageInfo) data.pageInfo = {};
|
let data;
|
||||||
if (!data.sections) data.sections = [];
|
try {
|
||||||
// Set the state, and return data
|
data = yaml.load(response.data);
|
||||||
commit(SET_ROOT_CONFIG, data);
|
} catch (parseError) {
|
||||||
return data;
|
commit(CRITICAL_ERROR_MSG, `Failed to parse YAML: ${parseError.message}`);
|
||||||
|
}
|
||||||
|
// Replace missing root properties with empty objects
|
||||||
|
if (!data.appConfig) data.appConfig = {};
|
||||||
|
if (!data.pageInfo) data.pageInfo = {};
|
||||||
|
if (!data.sections) data.sections = [];
|
||||||
|
// Set the state, and return data
|
||||||
|
commit(SET_ROOT_CONFIG, data);
|
||||||
|
commit(CRITICAL_ERROR_MSG, null);
|
||||||
|
return data;
|
||||||
|
} catch (fetchError) {
|
||||||
|
if (fetchError.response) {
|
||||||
|
commit(
|
||||||
|
CRITICAL_ERROR_MSG,
|
||||||
|
'Failed to fetch configuration: Server responded with status '
|
||||||
|
+ `${fetchError.response?.status || 'mystery status'}`,
|
||||||
|
);
|
||||||
|
} else if (fetchError.request) {
|
||||||
|
commit(CRITICAL_ERROR_MSG, 'Failed to fetch configuration: No response from server');
|
||||||
|
} else {
|
||||||
|
commit(CRITICAL_ERROR_MSG, `Failed to fetch configuration: ${fetchError.message}`);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Fetches config and updates state
|
* Fetches config and updates state
|
||||||
|
@ -351,7 +379,7 @@ const store = new Vuex.Store({
|
||||||
const json = JSON.parse(localSectionsRaw);
|
const json = JSON.parse(localSectionsRaw);
|
||||||
if (json.length >= 1) localSections = json;
|
if (json.length >= 1) localSections = json;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ErrorHandler('Malformed section data in local storage');
|
commit(CRITICAL_ERROR_MSG, 'Malformed section data in local storage');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (localSections.length > 0) {
|
if (localSections.length > 0) {
|
||||||
|
@ -366,7 +394,7 @@ const store = new Vuex.Store({
|
||||||
)?.path);
|
)?.path);
|
||||||
|
|
||||||
if (!subConfigPath) {
|
if (!subConfigPath) {
|
||||||
ErrorHandler(`Unable to find config for '${subConfigId}'`);
|
commit(CRITICAL_ERROR_MSG, `Unable to find config for '${subConfigId}'`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,11 @@ html {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#dashy {
|
||||||
|
position: relative;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
/* Hide text, and show 'Loading...' while Vue is initializing tags */
|
/* Hide text, and show 'Loading...' while Vue is initializing tags */
|
||||||
[v-cloak] > * { display:none }
|
[v-cloak] > * { display:none }
|
||||||
[v-cloak]::before { content: "loading…" }
|
[v-cloak]::before { content: "loading…" }
|
||||||
|
|
|
@ -29,6 +29,7 @@ const KEY_NAMES = [
|
||||||
'INSERT_ITEM',
|
'INSERT_ITEM',
|
||||||
'UPDATE_CUSTOM_CSS',
|
'UPDATE_CUSTOM_CSS',
|
||||||
'CONF_MENU_INDEX',
|
'CONF_MENU_INDEX',
|
||||||
|
'CRITICAL_ERROR_MSG',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Convert array of key names into an object, and export
|
// Convert array of key names into an object, and export
|
||||||
|
|
|
@ -135,6 +135,7 @@ module.exports = {
|
||||||
MOST_USED: 'mostUsed',
|
MOST_USED: 'mostUsed',
|
||||||
LAST_USED: 'lastUsed',
|
LAST_USED: 'lastUsed',
|
||||||
KEYCLOAK_INFO: 'keycloakInfo',
|
KEYCLOAK_INFO: 'keycloakInfo',
|
||||||
|
DISABLE_CRITICAL_WARNING: 'disableCriticalWarning',
|
||||||
},
|
},
|
||||||
/* Key names for cookie identifiers */
|
/* Key names for cookie identifiers */
|
||||||
cookieKeys: {
|
cookieKeys: {
|
||||||
|
|
Loading…
Reference in New Issue