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" />
|
||||
<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 {
|
||||
|
|
|
@ -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,
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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…" }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: {
|
||||
|
|
Loading…
Reference in New Issue