🥅 Better error handling when config cannot be found

This commit is contained in:
Alicia Sykes 2024-04-21 14:46:38 +01:00
parent f295958c44
commit ecef01b034
6 changed files with 164 additions and 11 deletions

View File

@ -3,6 +3,7 @@
<EditModeTopBanner v-if="isEditMode" />
<LoadingScreen :isLoading="isLoading" v-if="shouldShowSplash" />
<Header :pageInfo="pageInfo" />
<CriticalError />
<router-view v-if="!isFetching" />
<Footer :text="footerText" v-if="visibleComponents.footer && !isFetching" />
</div>
@ -12,6 +13,7 @@
import Header from '@/components/PageStrcture/Header.vue';
import Footer from '@/components/PageStrcture/Footer.vue';
import EditModeTopBanner from '@/components/InteractiveEditor/EditModeTopBanner.vue';
import CriticalError from '@/components/PageStrcture/CriticalError.vue';
import LoadingScreen from '@/components/PageStrcture/LoadingScreen.vue';
import { welcomeMsg } from '@/utils/CoolConsole';
import ErrorHandler from '@/utils/ErrorHandler';
@ -29,6 +31,7 @@ export default {
Footer,
LoadingScreen,
EditModeTopBanner,
CriticalError,
},
data() {
return {

View File

@ -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>

View File

@ -41,6 +41,7 @@ const {
INSERT_ITEM,
UPDATE_CUSTOM_CSS,
CONF_MENU_INDEX,
CRITICAL_ERROR_MSG,
} = Keys;
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
currentConfigInfo: {}, // For multi-page support, will store info about config file
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
},
getters: {
@ -174,6 +176,10 @@ const store = new Vuex.Store({
state.editMode = editMode;
}
},
[CRITICAL_ERROR_MSG](state, message) {
if (message) ErrorHandler(message);
state.criticalError = message;
},
[UPDATE_ITEM](state, payload) {
const { itemId, newItem } = payload;
const newConfig = { ...state.config };
@ -320,16 +326,38 @@ const store = new Vuex.Store({
actions: {
/* Fetches the root config file, only ever called by INITIALIZE_CONFIG */
async [INITIALIZE_ROOT_CONFIG]({ commit }) {
// Load and parse config from root config file
const configFilePath = process.env.VUE_APP_CONFIG_PATH || '/conf.yml';
const data = await yaml.load((await axios.get(configFilePath)).data);
// 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);
return data;
try {
// Attempt to fetch the YAML file
const response = await axios.get(configFilePath);
let data;
try {
data = yaml.load(response.data);
} catch (parseError) {
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
@ -351,7 +379,7 @@ const store = new Vuex.Store({
const json = JSON.parse(localSectionsRaw);
if (json.length >= 1) localSections = json;
} catch (e) {
ErrorHandler('Malformed section data in local storage');
commit(CRITICAL_ERROR_MSG, 'Malformed section data in local storage');
}
}
if (localSections.length > 0) {
@ -366,7 +394,7 @@ const store = new Vuex.Store({
)?.path);
if (!subConfigPath) {
ErrorHandler(`Unable to find config for '${subConfigId}'`);
commit(CRITICAL_ERROR_MSG, `Unable to find config for '${subConfigId}'`);
return null;
}

View File

@ -22,6 +22,11 @@ html {
}
}
#dashy {
position: relative;
min-height: 100vh;
}
/* Hide text, and show 'Loading...' while Vue is initializing tags */
[v-cloak] > * { display:none }
[v-cloak]::before { content: "loading…" }

View File

@ -29,6 +29,7 @@ const KEY_NAMES = [
'INSERT_ITEM',
'UPDATE_CUSTOM_CSS',
'CONF_MENU_INDEX',
'CRITICAL_ERROR_MSG',
];
// Convert array of key names into an object, and export

View File

@ -135,6 +135,7 @@ module.exports = {
MOST_USED: 'mostUsed',
LAST_USED: 'lastUsed',
KEYCLOAK_INFO: 'keycloakInfo',
DISABLE_CRITICAL_WARNING: 'disableCriticalWarning',
},
/* Key names for cookie identifiers */
cookieKeys: {